Merge "Revert "Skip testEcAttestation and testRsaAttestation if device has no lock screen"" into rvc-dev
diff --git a/apps/CtsVerifier/AndroidManifest.xml b/apps/CtsVerifier/AndroidManifest.xml
index fb7cb0e..0ed1126 100644
--- a/apps/CtsVerifier/AndroidManifest.xml
+++ b/apps/CtsVerifier/AndroidManifest.xml
@@ -2586,6 +2586,18 @@
                     android:value="android.hardware.type.watch:android.hardware.type.television:android.software.leanback" />
         </activity>
 
+        <activity android:name=".security.CaCertInstallViaIntentTest"
+                  android:label="@string/cacert_install_via_intent">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.cts.intent.category.MANUAL_TEST" />
+            </intent-filter>
+            <meta-data android:name="test_category" android:value="@string/test_category_security" />
+            <!-- Skip certificate installation on devices that do not support KeyChain -->
+            <meta-data android:name="test_excluded_features"
+                       android:value="android.hardware.type.watch:android.hardware.type.television:android.software.leanback" />
+        </activity>
+
         <activity android:name=".wifi.NetworkRequestSpecificNetworkSpecifierTestActivity"
                   android:label="@string/wifi_test_network_request_specific"
                   android:configChanges="keyboardHidden|orientation|screenSize" />
diff --git a/apps/CtsVerifier/res/layout/ca_install_via_intent.xml b/apps/CtsVerifier/res/layout/ca_install_via_intent.xml
new file mode 100644
index 0000000..4b529d1
--- /dev/null
+++ b/apps/CtsVerifier/res/layout/ca_install_via_intent.xml
@@ -0,0 +1,51 @@
+<?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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        android:orientation="vertical"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+    <TextView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:padding="10dip"
+        android:text="@string/cacert_install_via_intent_title"
+        android:textSize="18dip" />
+
+    <TextView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="@string/cacert_install_via_intent_info"
+        android:padding="10dip"
+        android:textSize="18dip"/>
+
+    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal">
+
+        <Button
+            android:id="@+id/run_test_button"
+            android:layout_width="204dip"
+            android:layout_height="wrap_content"
+            android:text="@string/go_button_text"/>
+
+    </LinearLayout>
+
+    <include layout="@layout/pass_fail_buttons" />
+
+</LinearLayout>
+
diff --git a/apps/CtsVerifier/res/values/strings.xml b/apps/CtsVerifier/res/values/strings.xml
index d1a8c1a..80ab389 100755
--- a/apps/CtsVerifier/res/values/strings.xml
+++ b/apps/CtsVerifier/res/values/strings.xml
@@ -2283,6 +2283,11 @@
  1. Open Encryption and credentials.\n
  2. Tap Clear credentials.</string>
 
+    <!-- Strings for CA cert installation via intent test -->
+    <string name="cacert_install_via_intent">CA Cert install via intent</string>
+    <string name="cacert_install_via_intent_title">This test attempts to install a CA certificate via an intent.</string>
+    <string name="cacert_install_via_intent_info">Attempt installing a CA certificate via an intent, which should fail. If you see a dialog redirecting the user to Settings for installing the certificate, the test passed. If a any other dialog comes up, the test failed.</string>
+
     <!-- Strings for Widget -->
     <string name="widget_framework_test">Widget Framework Test</string>
     <string name="widget_framework_test_info">This test checks some basic features of the widget
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/security/CaCertInstallViaIntentTest.java b/apps/CtsVerifier/src/com/android/cts/verifier/security/CaCertInstallViaIntentTest.java
new file mode 100644
index 0000000..caf9bc4
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/security/CaCertInstallViaIntentTest.java
@@ -0,0 +1,85 @@
+/*
+ * 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.
+ */
+
+package com.android.cts.verifier.security;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.security.KeyChain;
+import android.util.Log;
+import android.view.View;
+import android.widget.Button;
+
+import com.android.cts.verifier.PassFailButtons;
+import com.android.cts.verifier.R;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public class CaCertInstallViaIntentTest extends PassFailButtons.Activity implements
+        View.OnClickListener {
+    private static final String TAG = CaCertInstallViaIntentTest.class.getSimpleName();
+    private static final String CERT_ASSET_NAME = "myCA.cer";
+    private static final int REQUEST_CA_CERT_INSTALL = 1;
+
+    private Button mRunButton;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        View root = getLayoutInflater().inflate(R.layout.ca_install_via_intent, null);
+        setContentView(root);
+
+        setInfoResources(R.string.cacert_install_via_intent_info,
+                R.string.cacert_install_via_intent_title, -1);
+        setPassFailButtonClickListeners();
+
+        mRunButton = root.findViewById(R.id.run_test_button);
+        mRunButton.setOnClickListener(this);
+
+        getPassButton().setEnabled(false);
+    }
+
+    @Override
+    public void onClick(View v) {
+        Intent installIntent = KeyChain.createInstallIntent();
+        installIntent.putExtra(KeyChain.EXTRA_NAME, "My CA");
+        try {
+            InputStream is = getAssets().open(CERT_ASSET_NAME);
+            byte[] cert = new byte[is.available()];
+            is.read(cert);
+            installIntent.putExtra(KeyChain.EXTRA_CERTIFICATE, cert);
+        } catch (IOException e) {
+            Log.d(TAG, "Failed reading certificate", e);
+            return;
+        }
+        startActivityForResult(installIntent, REQUEST_CA_CERT_INSTALL);
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        switch (requestCode) {
+            case REQUEST_CA_CERT_INSTALL: {
+                getPassButton().setEnabled(true);
+                Log.d(TAG, "Result: " + resultCode);
+                break;
+            }
+            default:
+                throw new IllegalStateException("requestCode == " + requestCode);
+        }
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/SystemUtil.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/SystemUtil.java
index 0aa8121..a050094 100644
--- a/common/device-side/util-axt/src/com/android/compatibility/common/util/SystemUtil.java
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/SystemUtil.java
@@ -301,7 +301,7 @@
                         throw new RuntimeException(e);
                     }
                 } else {
-                    throw e;
+                    throw new RuntimeException(e);
                 }
             }
         }
@@ -347,17 +347,4 @@
             }
         }
     }
-
-    public interface ThrowingRunnable extends Runnable {
-        void runOrThrow() throws Exception;
-
-        @Override
-        default void run() {
-            try {
-                runOrThrow();
-            } catch (Exception ex) {
-                throw new RuntimeException(ex);
-            }
-        }
-    }
 }
diff --git a/hostsidetests/appcompat/compatchanges/src/com/android/cts/appcompat/CompatChangesValidConfigTest.java b/hostsidetests/appcompat/compatchanges/src/com/android/cts/appcompat/CompatChangesValidConfigTest.java
index fa1e4ec..c5e72b9 100644
--- a/hostsidetests/appcompat/compatchanges/src/com/android/cts/appcompat/CompatChangesValidConfigTest.java
+++ b/hostsidetests/appcompat/compatchanges/src/com/android/cts/appcompat/CompatChangesValidConfigTest.java
@@ -16,27 +16,27 @@
 
 package com.android.cts.appcompat;
 
- import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertThat;
 
 
- import java.util.ArrayList;
- import java.util.Arrays;
- import java.util.List;
- import java.util.Objects;
- import java.util.regex.Matcher;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.regex.Matcher;
 import java.util.regex.Pattern;
- import java.util.stream.Collectors;
+import java.util.stream.Collectors;
 
- import android.compat.cts.CompatChangeGatingTestCase;
+import android.compat.cts.CompatChangeGatingTestCase;
 
- import org.w3c.dom.Document;
- import org.w3c.dom.Element;
- import org.w3c.dom.NamedNodeMap;
- import org.w3c.dom.Node;
- import org.w3c.dom.NodeList;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
 
- import javax.xml.parsers.DocumentBuilder;
- import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
 
 public final class CompatChangesValidConfigTest extends CompatChangeGatingTestCase {
 
@@ -185,7 +185,11 @@
         NodeList changeNodes = root.getElementsByTagName("compat-change");
         List<Change> changes = new ArrayList<>();
         for (int nodeIdx = 0; nodeIdx < changeNodes.getLength(); ++nodeIdx) {
-            changes.add(Change.fromNode(changeNodes.item(nodeIdx)));
+            Change change = Change.fromNode(changeNodes.item(nodeIdx));
+            // Exclude logging only changes from the expected config. See b/155264388.
+            if (!change.loggingOnly) {
+                changes.add(change);
+            }
         }
         return changes;
     }
diff --git a/hostsidetests/appsecurity/certs/pkgsigverify/ec-p256-por_1_2-no-shUid-cap b/hostsidetests/appsecurity/certs/pkgsigverify/ec-p256-por_1_2-no-shUid-cap
new file mode 100644
index 0000000..3c5e2f6
--- /dev/null
+++ b/hostsidetests/appsecurity/certs/pkgsigverify/ec-p256-por_1_2-no-shUid-cap
Binary files differ
diff --git a/hostsidetests/appsecurity/certs/pkgsigverify/por_Y_1_2-default-caps b/hostsidetests/appsecurity/certs/pkgsigverify/por_Y_1_2-default-caps
new file mode 100644
index 0000000..92c2109
--- /dev/null
+++ b/hostsidetests/appsecurity/certs/pkgsigverify/por_Y_1_2-default-caps
Binary files differ
diff --git a/hostsidetests/appsecurity/certs/pkgsigverify/por_Z_1_2-default-caps b/hostsidetests/appsecurity/certs/pkgsigverify/por_Z_1_2-default-caps
new file mode 100644
index 0000000..e6515d60
--- /dev/null
+++ b/hostsidetests/appsecurity/certs/pkgsigverify/por_Z_1_2-default-caps
Binary files differ
diff --git a/hostsidetests/appsecurity/res/pkgsigverify/README.md b/hostsidetests/appsecurity/res/pkgsigverify/README.md
index 41b7509..a065c25 100644
--- a/hostsidetests/appsecurity/res/pkgsigverify/README.md
+++ b/hostsidetests/appsecurity/res/pkgsigverify/README.md
@@ -31,6 +31,4 @@
 ## Invalid cases
 
 Some of the APKs in this directory were generated by modifying the apksig library (see
-README in tools/apksig/) to create invalid or unsupported outcomes. When possible, their usage is 
-preceded by a description of how `apksig` was modified, and the commit should explicitly show how
-`apksig` was modified.
\ No newline at end of file
+README in tools/apksig/) to create invalid or unsupported outcomes.
\ No newline at end of file
diff --git a/hostsidetests/appsecurity/res/pkgsigverify/generate-apks.sh b/hostsidetests/appsecurity/res/pkgsigverify/generate-apks.sh
index 5a248dd..c1f5ba1 100644
--- a/hostsidetests/appsecurity/res/pkgsigverify/generate-apks.sh
+++ b/hostsidetests/appsecurity/res/pkgsigverify/generate-apks.sh
@@ -23,4 +23,18 @@
 # testInstallV4WithV3VeritySigner (with verity, and only for the DSA key / the smaller-sized keys of RSA/EC, since the verity algorithm is not used otherwise)
 apksigner sign --v2-signing-enabled false --v3-signing-enabled true --v4-signing-enabled --verity-enabled --key cts/hostsidetests/appsecurity/certs/pkgsigverify/dsa-3072.pk8 --cert cts/hostsidetests/appsecurity/certs/pkgsigverify/dsa-3072.x509.pem -out cts/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-Sha256withDSA-Verity.apk cts/hostsidetests/appsecurity/res/pkgsigverify/original.apk
 apksigner sign --v2-signing-enabled false --v3-signing-enabled true --v4-signing-enabled --verity-enabled --key cts/hostsidetests/appsecurity/certs/pkgsigverify/ec-p256.pk8 --cert cts/hostsidetests/appsecurity/certs/pkgsigverify/ec-p256.x509.pem -out cts/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-Sha256withEC-Verity.apk cts/hostsidetests/appsecurity/res/pkgsigverify/original.apk
-apksigner sign --v2-signing-enabled false --v3-signing-enabled true --v4-signing-enabled --verity-enabled --key cts/hostsidetests/appsecurity/certs/pkgsigverify/rsa-2048.pk8 --cert cts/hostsidetests/appsecurity/certs/pkgsigverify/rsa-2048.x509.pem -out cts/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-Sha256withRSA-Verity.apk cts/hostsidetests/appsecurity/res/pkgsigverify/original.apk
\ No newline at end of file
+apksigner sign --v2-signing-enabled false --v3-signing-enabled true --v4-signing-enabled --verity-enabled --key cts/hostsidetests/appsecurity/certs/pkgsigverify/rsa-2048.pk8 --cert cts/hostsidetests/appsecurity/certs/pkgsigverify/rsa-2048.x509.pem -out cts/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-Sha256withRSA-Verity.apk cts/hostsidetests/appsecurity/res/pkgsigverify/original.apk
+
+
+# testV4IncToV3NonInc* tests
+apksigner rotate --out ~/tmp/rotated_key --old-signer   --key /ssd/android/cts/hostsidetests/appsecurity/certs/pkgsigverify/ec-p256.pk8 --cert /ssd/android/cts/hostsidetests/appsecurity/certs/pkgsigverify/ec-p256.x509.pem --new-signer  --key /ssd/android/cts/hostsidetests/appsecurity/certs/pkgsigverify/ec-p384.pk8 --cert /ssd/android/cts/hostsidetests/appsecurity/certs/pkgsigverify/ec-p384.x509.pem
+apksigner sign --v2-signing-enabled false --v3-signing-enabled true --v4-signing-enabled  --key cts/hostsidetests/appsecurity/certs/pkgsigverify/dsa-3072.pk8 --cert cts/hostsidetests/appsecurity/certs/pkgsigverify/dsa-3072.x509.pem -out cts/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v3-noninc-dsa-3072-appv1.apk cts/hostsidetests/appsecurity/res/pkgsigverify/original.apk
+apksigner sign --v2-signing-enabled false --v3-signing-enabled true --v4-signing-enabled  --key cts/hostsidetests/appsecurity/certs/pkgsigverify/ec-p256.pk8 --cert cts/hostsidetests/appsecurity/certs/pkgsigverify/ec-p256.x509.pem -out cts/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v3-noninc-ec-p256-appv1.apk cts/hostsidetests/appsecurity/res/pkgsigverify/original.apk
+apksigner sign --v1-signing-enabled false --v2-signing-enabled false --v3-signing-enabled true  --v4-signing-enabled --key cts/hostsidetests/appsecurity/certs/pkgsigverify/ec-p384.pk8 --cert cts/hostsidetests/appsecurity/certs/pkgsigverify/ec-p384.x509.pem -out cts/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v3-noninc-ec-p384-rotated-ec-p256-appv2.apk --lineage /tmp/ec_p256_to_ec_384_rotated_key cts/hostsidetests/appsecurity/res/pkgsigverify/originalv2.apk
+apksigner sign --v1-signing-enabled false --v2-signing-enabled false --v3-signing-enabled true  --v4-signing-enabled --key cts/hostsidetests/appsecurity/certs/pkgsigverify/ec-p384.pk8 --cert cts/hostsidetests/appsecurity/certs/pkgsigverify/ec-p384.x509.pem -out cts/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v3-noninc-ec-p384-appv2.apk cts/hostsidetests/appsecurity/res/pkgsigverify/originalv2.apk
+apksigner sign --v2-signing-enabled false --v3-signing-enabled true --v4-signing-enabled  --key cts/hostsidetests/appsecurity/certs/pkgsigverify/ec-p256.pk8 --cert cts/hostsidetests/appsecurity/certs/pkgsigverify/ec-p256.x509.pem -out cts/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v3-noninc-ec-p256-appv2.apk cts/hostsidetests/appsecurity/res/pkgsigverify/originalv2.apk
+
+# testV4IncToV2NonInc* tests
+apksigner sign --v2-signing-enabled true --v3-signing-enabled false --v4-signing-enabled  --key cts/hostsidetests/appsecurity/certs/pkgsigverify/ec-p256.pk8 --cert cts/hostsidetests/appsecurity/certs/pkgsigverify/ec-p256.x509.pem -out cts/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v2-noninc-ec-p256-appv1.apk cts/hostsidetests/appsecurity/res/pkgsigverify/original.apk
+apksigner sign --v1-signing-enabled false --v2-signing-enabled true --v3-signing-enabled false  --v4-signing-enabled --key cts/hostsidetests/appsecurity/certs/pkgsigverify/ec-p384.pk8 --cert cts/hostsidetests/appsecurity/certs/pkgsigverify/ec-p384.x509.pem -out cts/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v2-noninc-ec-p384-appv2.apk cts/hostsidetests/appsecurity/res/pkgsigverify/originalv2.apk
+apksigner sign --v2-signing-enabled true --v3-signing-enabled false --v4-signing-enabled  --key cts/hostsidetests/appsecurity/certs/pkgsigverify/ec-p256.pk8 --cert cts/hostsidetests/appsecurity/certs/pkgsigverify/ec-p256.x509.pem -out cts/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v2-noninc-ec-p256-appv2.apk cts/hostsidetests/appsecurity/res/pkgsigverify/originalv2.apk
\ No newline at end of file
diff --git a/hostsidetests/appsecurity/res/pkgsigverify/originalv2.apk b/hostsidetests/appsecurity/res/pkgsigverify/originalv2.apk
new file mode 100644
index 0000000..a528dab
--- /dev/null
+++ b/hostsidetests/appsecurity/res/pkgsigverify/originalv2.apk
Binary files differ
diff --git a/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-10mbytes-additional-data.apk b/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-10mbytes-additional-data.apk
new file mode 100644
index 0000000..710c7e7
--- /dev/null
+++ b/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-10mbytes-additional-data.apk
Binary files differ
diff --git a/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-10mbytes-additional-data.apk.idsig b/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-10mbytes-additional-data.apk.idsig
new file mode 100644
index 0000000..e1f7f53
--- /dev/null
+++ b/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-10mbytes-additional-data.apk.idsig
Binary files differ
diff --git a/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-128bytes-additional-data.apk b/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-128bytes-additional-data.apk
new file mode 100644
index 0000000..710c7e7
--- /dev/null
+++ b/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-128bytes-additional-data.apk
Binary files differ
diff --git a/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-128bytes-additional-data.apk.idsig b/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-128bytes-additional-data.apk.idsig
new file mode 100644
index 0000000..b78d2b4
--- /dev/null
+++ b/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-128bytes-additional-data.apk.idsig
Binary files differ
diff --git a/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-merkle-tree-10mb-trailing-data.apk b/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-merkle-tree-10mb-trailing-data.apk
new file mode 100644
index 0000000..710c7e7
--- /dev/null
+++ b/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-merkle-tree-10mb-trailing-data.apk
Binary files differ
diff --git a/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-merkle-tree-10mb-trailing-data.apk.idsig b/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-merkle-tree-10mb-trailing-data.apk.idsig
new file mode 100644
index 0000000..72c856e
--- /dev/null
+++ b/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-merkle-tree-10mb-trailing-data.apk.idsig
Binary files differ
diff --git a/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-merkle-tree-different-block-size.apk b/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-merkle-tree-different-block-size.apk
new file mode 100644
index 0000000..710c7e7
--- /dev/null
+++ b/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-merkle-tree-different-block-size.apk
Binary files differ
diff --git a/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-merkle-tree-different-block-size.apk.idsig b/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-merkle-tree-different-block-size.apk.idsig
new file mode 100644
index 0000000..7dc9d29
--- /dev/null
+++ b/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-merkle-tree-different-block-size.apk.idsig
Binary files differ
diff --git a/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-merkle-tree-non-zero-padding.apk b/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-merkle-tree-non-zero-padding.apk
new file mode 100644
index 0000000..710c7e7
--- /dev/null
+++ b/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-merkle-tree-non-zero-padding.apk
Binary files differ
diff --git a/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-merkle-tree-non-zero-padding.apk.idsig b/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-merkle-tree-non-zero-padding.apk.idsig
new file mode 100644
index 0000000..234365b
--- /dev/null
+++ b/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-merkle-tree-non-zero-padding.apk.idsig
Binary files differ
diff --git a/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-no-merkle-tree.apk b/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-no-merkle-tree.apk
new file mode 100644
index 0000000..710c7e7
--- /dev/null
+++ b/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-no-merkle-tree.apk
Binary files differ
diff --git a/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-no-merkle-tree.apk.idsig b/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-no-merkle-tree.apk.idsig
new file mode 100644
index 0000000..cfbf08c
--- /dev/null
+++ b/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-no-merkle-tree.apk.idsig
Binary files differ
diff --git a/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-wrong-block-size.apk b/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-wrong-block-size.apk
new file mode 100644
index 0000000..710c7e7
--- /dev/null
+++ b/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-wrong-block-size.apk
Binary files differ
diff --git a/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-wrong-block-size.apk.idsig b/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-wrong-block-size.apk.idsig
new file mode 100644
index 0000000..c85d6b6
--- /dev/null
+++ b/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-wrong-block-size.apk.idsig
Binary files differ
diff --git a/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-wrong-raw-root-hash.apk b/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-wrong-raw-root-hash.apk
new file mode 100644
index 0000000..710c7e7
--- /dev/null
+++ b/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-wrong-raw-root-hash.apk
Binary files differ
diff --git a/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-wrong-raw-root-hash.apk.idsig b/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-wrong-raw-root-hash.apk.idsig
new file mode 100644
index 0000000..979775b
--- /dev/null
+++ b/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-wrong-raw-root-hash.apk.idsig
Binary files differ
diff --git a/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-wrong-sig-bytes-size.apk b/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-wrong-sig-bytes-size.apk
new file mode 100644
index 0000000..710c7e7
--- /dev/null
+++ b/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-wrong-sig-bytes-size.apk
Binary files differ
diff --git a/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-wrong-sig-bytes-size.apk.idsig b/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-wrong-sig-bytes-size.apk.idsig
new file mode 100644
index 0000000..17ea4a8
--- /dev/null
+++ b/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-wrong-sig-bytes-size.apk.idsig
Binary files differ
diff --git a/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-wrong-sig-bytes.apk b/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-wrong-sig-bytes.apk
new file mode 100644
index 0000000..710c7e7
--- /dev/null
+++ b/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-wrong-sig-bytes.apk
Binary files differ
diff --git a/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-wrong-sig-bytes.apk.idsig b/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-wrong-sig-bytes.apk.idsig
new file mode 100644
index 0000000..e0a68db
--- /dev/null
+++ b/hostsidetests/appsecurity/res/pkgsigverify/v4-digest-v3-wrong-sig-bytes.apk.idsig
Binary files differ
diff --git a/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v2-noninc-ec-p256-appv1.apk b/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v2-noninc-ec-p256-appv1.apk
new file mode 100644
index 0000000..8ec583b
--- /dev/null
+++ b/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v2-noninc-ec-p256-appv1.apk
Binary files differ
diff --git a/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v2-noninc-ec-p256-appv1.apk.idsig b/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v2-noninc-ec-p256-appv1.apk.idsig
new file mode 100644
index 0000000..ff31b12
--- /dev/null
+++ b/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v2-noninc-ec-p256-appv1.apk.idsig
Binary files differ
diff --git a/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v2-noninc-ec-p256-appv2.apk b/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v2-noninc-ec-p256-appv2.apk
new file mode 100644
index 0000000..e4ebf8d
--- /dev/null
+++ b/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v2-noninc-ec-p256-appv2.apk
Binary files differ
diff --git a/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v2-noninc-ec-p256-appv2.apk.idsig b/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v2-noninc-ec-p256-appv2.apk.idsig
new file mode 100644
index 0000000..865eeb1
--- /dev/null
+++ b/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v2-noninc-ec-p256-appv2.apk.idsig
Binary files differ
diff --git a/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v2-noninc-ec-p384-appv2.apk b/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v2-noninc-ec-p384-appv2.apk
new file mode 100644
index 0000000..55f76d6
--- /dev/null
+++ b/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v2-noninc-ec-p384-appv2.apk
Binary files differ
diff --git a/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v2-noninc-ec-p384-appv2.apk.idsig b/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v2-noninc-ec-p384-appv2.apk.idsig
new file mode 100644
index 0000000..24181d1
--- /dev/null
+++ b/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v2-noninc-ec-p384-appv2.apk.idsig
Binary files differ
diff --git a/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v3-noninc-dsa-3072-appv1.apk b/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v3-noninc-dsa-3072-appv1.apk
new file mode 100644
index 0000000..ed4bae7
--- /dev/null
+++ b/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v3-noninc-dsa-3072-appv1.apk
Binary files differ
diff --git a/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v3-noninc-dsa-3072-appv1.apk.idsig b/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v3-noninc-dsa-3072-appv1.apk.idsig
new file mode 100644
index 0000000..ad4087c
--- /dev/null
+++ b/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v3-noninc-dsa-3072-appv1.apk.idsig
Binary files differ
diff --git a/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v3-noninc-ec-p256-appv1.apk b/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v3-noninc-ec-p256-appv1.apk
new file mode 100644
index 0000000..3e4263f
--- /dev/null
+++ b/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v3-noninc-ec-p256-appv1.apk
Binary files differ
diff --git a/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v3-noninc-ec-p256-appv1.apk.idsig b/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v3-noninc-ec-p256-appv1.apk.idsig
new file mode 100644
index 0000000..fda7406
--- /dev/null
+++ b/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v3-noninc-ec-p256-appv1.apk.idsig
Binary files differ
diff --git a/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v3-noninc-ec-p256-appv2.apk b/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v3-noninc-ec-p256-appv2.apk
new file mode 100644
index 0000000..fb039f9
--- /dev/null
+++ b/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v3-noninc-ec-p256-appv2.apk
Binary files differ
diff --git a/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v3-noninc-ec-p256-appv2.apk.idsig b/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v3-noninc-ec-p256-appv2.apk.idsig
new file mode 100644
index 0000000..11368ae
--- /dev/null
+++ b/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v3-noninc-ec-p256-appv2.apk.idsig
Binary files differ
diff --git a/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v3-noninc-ec-p384-appv2.apk b/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v3-noninc-ec-p384-appv2.apk
new file mode 100644
index 0000000..8a346e9
--- /dev/null
+++ b/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v3-noninc-ec-p384-appv2.apk
Binary files differ
diff --git a/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v3-noninc-ec-p384-appv2.apk.idsig b/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v3-noninc-ec-p384-appv2.apk.idsig
new file mode 100644
index 0000000..c877213
--- /dev/null
+++ b/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v3-noninc-ec-p384-appv2.apk.idsig
Binary files differ
diff --git a/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v3-noninc-ec-p384-rotated-ec-p256-appv2.apk b/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v3-noninc-ec-p384-rotated-ec-p256-appv2.apk
new file mode 100644
index 0000000..2b033ee
--- /dev/null
+++ b/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v3-noninc-ec-p384-rotated-ec-p256-appv2.apk
Binary files differ
diff --git a/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v3-noninc-ec-p384-rotated-ec-p256-appv2.apk.idsig b/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v3-noninc-ec-p384-rotated-ec-p256-appv2.apk.idsig
new file mode 100644
index 0000000..3d1a6ca
--- /dev/null
+++ b/hostsidetests/appsecurity/res/pkgsigverify/v4-inc-to-v3-noninc-ec-p384-rotated-ec-p256-appv2.apk.idsig
Binary files differ
diff --git a/hostsidetests/appsecurity/src/android/appsecurity/cts/PkgInstallSignatureVerificationTest.java b/hostsidetests/appsecurity/src/android/appsecurity/cts/PkgInstallSignatureVerificationTest.java
index 406c410..3f54a05 100644
--- a/hostsidetests/appsecurity/src/android/appsecurity/cts/PkgInstallSignatureVerificationTest.java
+++ b/hostsidetests/appsecurity/src/android/appsecurity/cts/PkgInstallSignatureVerificationTest.java
@@ -16,8 +16,6 @@
 
 package android.appsecurity.cts;
 
-import static org.junit.Assume.assumeTrue;
-
 import android.platform.test.annotations.SecurityTest;
 
 import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
@@ -42,6 +40,7 @@
 
     private static final String TEST_PKG = "android.appsecurity.cts.tinyapp";
     private static final String COMPANION_TEST_PKG = "android.appsecurity.cts.tinyapp_companion";
+    private static final String COMPANION2_TEST_PKG = "android.appsecurity.cts.tinyapp_companion2";
     private static final String DEVICE_TESTS_APK = "CtsV3SigningSchemeRotationTest.apk";
     private static final String DEVICE_TESTS_PKG = "android.appsecurity.cts.v3rotationtests";
     private static final String DEVICE_TESTS_CLASS = DEVICE_TESTS_PKG + ".V3RotationTest";
@@ -68,7 +67,7 @@
         Utils.prepareSingleUser(getDevice());
         assertNotNull(mCtsBuild);
         uninstallPackage();
-        uninstallCompanionPackage();
+        uninstallCompanionPackages();
         installDeviceTestPkg();
     }
 
@@ -612,6 +611,59 @@
         assertInstallSucceeds("v3-rsa-pkcs1-sha256-2048-2-sharedUid.apk");
     }
 
+    public void testInstallV3MultipleAppsOneDeniesOldKeySharedUid() throws Exception {
+        // If two apps are installed as part of a sharedUid, one granting access to the sharedUid
+        // to the previous key and the other revoking access to the sharedUid, then when an app
+        // signed with the old key attempts to join the sharedUid the installation should be blocked
+        assertInstallFromBuildSucceeds(
+                "v3-ec-p256-with-por_1_2-default-caps-sharedUid-companion.apk");
+        assertInstallFromBuildSucceeds("v3-ec-p256-with-por_1_2-no-shUid-cap-sharedUid.apk");
+        assertInstallFromBuildFails("v3-ec-p256-1-sharedUid-companion2.apk");
+    }
+
+    public void testInstallV3MultipleAppsOneUpdatedToDenyOldKeySharedUid() throws Exception {
+        // Similar to the test above if two apps are installed as part of a sharedUid with both
+        // granting access to the sharedUid to the previous key then an app signed with the previous
+        // key should be allowed to install and join the sharedUid. If one of the first two apps
+        // is then updated with a lineage that denies access to the sharedUid for the old key the
+        // installation of this updated app should be blocked.
+        assertInstallFromBuildSucceeds("v3-ec-p256-with-por_1_2-default-caps-sharedUid.apk");
+        assertInstallFromBuildSucceeds(
+                "v3-ec-p256-with-por_1_2-default-caps-sharedUid-companion.apk");
+        assertInstallFromBuildSucceeds("v3-ec-p256-1-sharedUid-companion2.apk");
+        assertInstallFromBuildFails("v3-ec-p256-with-por_1_2-no-shUid-cap-sharedUid.apk");
+    }
+
+    public void testInstallV3FirstAppOnlySignedByNewKeyLastAppOldKey() throws Exception {
+        // This test verifies the following scenario:
+        // - First installed app in sharedUid only signed with new key without lineage.
+        // - Second installed app in sharedUid signed with new key and includes lineage granting
+        //   access to the old key to join the sharedUid.
+        // - Last installed app in sharedUid signed with old key.
+        // The lineage should be updated when the second app is installed to allow the installation
+        // of the app signed with the old key.
+        assertInstallFromBuildSucceeds("v3-ec-p256-2-sharedUid-companion.apk");
+        assertInstallFromBuildSucceeds("v3-ec-p256-with-por_1_2-default-caps-sharedUid.apk");
+        assertInstallFromBuildSucceeds("v3-ec-p256-1-sharedUid-companion2.apk");
+    }
+
+    public void testInstallV3AppSignedWithOldKeyUpdatedLineageDeniesShUidCap() throws Exception {
+        // If an app is installed as part of a sharedUid, and then that app is signed with a new key
+        // that rejects the previous key in the lineage the update should be allowed to proceed
+        // as the app is being updated to the newly rotated key.
+        assertInstallFromBuildSucceeds("v3-ec-p256-1-sharedUid.apk");
+        assertInstallFromBuildSucceeds("v3-ec-p256-with-por_1_2-no-shUid-cap-sharedUid.apk");
+    }
+
+    public void testInstallV3TwoSharedUidAppsWithDivergedLineages() throws Exception {
+        // Apps that are installed as part of the sharedUserId with a lineage must have common
+        // ancestors; the platform will allow the installation if the lineage of an app being
+        // installed as part of the sharedUserId is the same, a subset, or a superset of the
+        // existing lineage, but if the lineage diverges then the installation should be blocked.
+        assertInstallFromBuildSucceeds("v3-por_Y_1_2-default-caps-sharedUid.apk");
+        assertInstallFromBuildFails("v3-por_Z_1_2-default-caps-sharedUid-companion.apk");
+    }
+
     public void testInstallV3KeyRotationSigPerm() throws Exception {
         // tests that a v3 signed APK can still get a signature permission from an app with its
         // older signing certificate.
@@ -722,7 +774,7 @@
         // the current signer(s) via getApkContentsSigners. This test verifies when a V3 signed
         // package with a rotated key is queried getApkContentsSigners only returns the current
         // signer.
-        installApkFromBuild("v3-ec-p256-with-por_1_2-default-caps.apk");
+        assertInstallFromBuildSucceeds("v3-ec-p256-with-por_1_2-default-caps.apk");
         Utils.runDeviceTests(
                 getDevice(), DEVICE_TESTS_PKG, DEVICE_TESTS_CLASS,
                 "testGetApkContentsSignersShowsCurrent");
@@ -731,7 +783,7 @@
     public void testInstallV2MultipleSignersGetApkContentsSigners() throws Exception {
         // Similar to the above test, but verifies when an APK is signed with two V2 signers
         // getApkContentsSigners returns both of the V2 signers.
-        installApkFromBuild("v1v2-ec-p256-two-signers-targetSdk-30.apk");
+        assertInstallFromBuildSucceeds("v1v2-ec-p256-two-signers-targetSdk-30.apk");
         Utils.runDeviceTests(
                 getDevice(), DEVICE_TESTS_PKG, DEVICE_TESTS_CLASS,
                 "testGetApkContentsSignersShowsMultipleSigners");
@@ -842,11 +894,11 @@
         // APK generated with:
         // --v2-signing-enabled true --v3-signing-enabled false --v4-signing-enabled
         // Full commands in generate-apks.sh
-        assertInstallV4Succeeds("v4-digest-v2-Sha256withDSA.apk");
-        assertInstallV4Succeeds("v4-digest-v2-Sha256withEC.apk");
-        assertInstallV4Succeeds("v4-digest-v2-Sha256withRSA.apk");
-        assertInstallV4Succeeds("v4-digest-v2-Sha512withEC.apk");
-        assertInstallV4Succeeds("v4-digest-v2-Sha512withRSA.apk");
+        assertInstallV4SucceedsAndUninstall("v4-digest-v2-Sha256withDSA.apk");
+        assertInstallV4SucceedsAndUninstall("v4-digest-v2-Sha256withEC.apk");
+        assertInstallV4SucceedsAndUninstall("v4-digest-v2-Sha256withRSA.apk");
+        assertInstallV4SucceedsAndUninstall("v4-digest-v2-Sha512withEC.apk");
+        assertInstallV4SucceedsAndUninstall("v4-digest-v2-Sha512withRSA.apk");
     }
 
     public void testInstallV4WithV2VeritySigner() throws Exception {
@@ -859,9 +911,9 @@
         // --v2-signing-enabled true --v3-signing-enabled false
         // --v4-signing-enabled --verity-enabled
         // Full commands in generate-apks.sh
-        assertInstallV4Succeeds("v4-digest-v2-Sha256withDSA-Verity.apk");
-        assertInstallV4Succeeds("v4-digest-v2-Sha256withEC-Verity.apk");
-        assertInstallV4Succeeds("v4-digest-v2-Sha256withRSA-Verity.apk");
+        assertInstallV4SucceedsAndUninstall("v4-digest-v2-Sha256withDSA-Verity.apk");
+        assertInstallV4SucceedsAndUninstall("v4-digest-v2-Sha256withEC-Verity.apk");
+        assertInstallV4SucceedsAndUninstall("v4-digest-v2-Sha256withRSA-Verity.apk");
     }
 
     public void testInstallV4WithV3NoVeritySigner() throws Exception {
@@ -873,11 +925,11 @@
         // APK generated with:
         // --v2-signing-enabled false --v3-signing-enabled true --v4-signing-enabled
         // Full commands in generate-apks.sh
-        assertInstallV4Succeeds("v4-digest-v3-Sha256withDSA.apk");
-        assertInstallV4Succeeds("v4-digest-v3-Sha256withEC.apk");
-        assertInstallV4Succeeds("v4-digest-v3-Sha256withRSA.apk");
-        assertInstallV4Succeeds("v4-digest-v3-Sha512withEC.apk");
-        assertInstallV4Succeeds("v4-digest-v3-Sha512withRSA.apk");
+        assertInstallV4SucceedsAndUninstall("v4-digest-v3-Sha256withDSA.apk");
+        assertInstallV4SucceedsAndUninstall("v4-digest-v3-Sha256withEC.apk");
+        assertInstallV4SucceedsAndUninstall("v4-digest-v3-Sha256withRSA.apk");
+        assertInstallV4SucceedsAndUninstall("v4-digest-v3-Sha512withEC.apk");
+        assertInstallV4SucceedsAndUninstall("v4-digest-v3-Sha512withRSA.apk");
     }
 
     public void testInstallV4WithV3VeritySigner() throws Exception {
@@ -890,9 +942,9 @@
         // --v2-signing-enabled false --v3-signing-enabled true
         // --v4-signing-enabled --verity-enabled
         // Full commands in generate-apks.sh
-        assertInstallV4Succeeds("v4-digest-v3-Sha256withDSA-Verity.apk");
-        assertInstallV4Succeeds("v4-digest-v3-Sha256withEC-Verity.apk");
-        assertInstallV4Succeeds("v4-digest-v3-Sha256withRSA-Verity.apk");
+        assertInstallV4SucceedsAndUninstall("v4-digest-v3-Sha256withDSA-Verity.apk");
+        assertInstallV4SucceedsAndUninstall("v4-digest-v3-Sha256withEC-Verity.apk");
+        assertInstallV4SucceedsAndUninstall("v4-digest-v3-Sha256withRSA-Verity.apk");
     }
 
     public void testInstallV4WithV2SignerDoesNotVerify() throws Exception {
@@ -943,6 +995,191 @@
         assertInstallV4FailsWithError("v4-digest-v2v3-badv2v3digest.apk", "did not verify");
     }
 
+    public void testInstallV4With128BytesAdditionalDataSucceeds() throws Exception {
+        // V4 is only enabled on devices with Incremental feature
+        if (!hasIncrementalFeature()) {
+            return;
+        }
+
+        // Editing apksigner to fill additional data of size 128 bytes.
+        assertInstallV4Succeeds("v4-digest-v3-128bytes-additional-data.apk");
+    }
+
+    public void testInstallV4With10MBytesAdditionalDataFails() throws Exception {
+        // V4 is only enabled on devices with Incremental feature
+        if (!hasIncrementalFeature()) {
+            return;
+        }
+
+        // Editing apksigner to fill additional data of size 10 * 1024 * 1024 bytes..
+        assertInstallV4FailsWithError("v4-digest-v3-10mbytes-additional-data.apk",
+                "additionalData has to be at most 128 bytes");
+    }
+
+    public void testInstallV4WithWrongBlockSize() throws Exception {
+        // V4 is only enabled on devices with Incremental feature
+        if (!hasIncrementalFeature()) {
+            return;
+        }
+
+        // Editing apksigner with the wrong block size in the v4 signature.
+        assertInstallV4FailsWithError("v4-digest-v3-wrong-block-size.apk",
+                "did not verify");
+    }
+
+    public void testInstallV4WithDifferentBlockSize() throws Exception {
+        // V4 is only enabled on devices with Incremental feature
+        if (!hasIncrementalFeature()) {
+            return;
+        }
+
+        // Editing apksigner with the different block size (2048 instead of 4096).
+        assertInstallV4FailsWithError("v4-digest-v3-merkle-tree-different-block-size.apk",
+                "Unsupported log2BlockSize: 11");
+    }
+
+    public void testInstallV4WithWrongRawRootHash() throws Exception {
+        // V4 is only enabled on devices with Incremental feature
+        if (!hasIncrementalFeature()) {
+            return;
+        }
+
+        // Editing apksigner with the wrong raw root hash in the v4 signature.
+        assertInstallV4FailsWithError("v4-digest-v3-wrong-raw-root-hash.apk", "Failure");
+    }
+
+    public void testInstallV4WithWrongSignatureBytes() throws Exception {
+        // V4 is only enabled on devices with Incremental feature
+        if (!hasIncrementalFeature()) {
+            return;
+        }
+
+        // Editing apksigner with the wrong signature bytes in the v4 signature.
+        assertInstallV4FailsWithError("v4-digest-v3-wrong-sig-bytes.apk",
+                "did not verify");
+    }
+
+    public void testInstallV4WithWrongSignatureBytesSize() throws Exception {
+        // V4 is only enabled on devices with Incremental feature
+        if (!hasIncrementalFeature()) {
+            return;
+        }
+
+        // Editing apksigner with the wrong signature byte size in the v4 signature.
+        assertInstallV4FailsWithError("v4-digest-v3-wrong-sig-bytes-size.apk",
+                "Failure");
+    }
+
+    public void testInstallV4WithNoMerkleTree() throws Exception {
+        // V4 is only enabled on devices with Incremental feature
+        if (!hasIncrementalFeature()) {
+            return;
+        }
+
+        // Editing apksigner to not include the Merkle tree.
+        assertInstallV4FailsWithError("v4-digest-v3-no-merkle-tree.apk",
+                "Failure");
+    }
+
+    public void testInstallV4WithWithTrailingDataInMerkleTree() throws Exception {
+        // V4 is only enabled on devices with Incremental feature
+        if (!hasIncrementalFeature()) {
+            return;
+        }
+
+        // Editing apksigner to add trailing data after the Merkle tree
+        assertInstallV4FailsWithError("v4-digest-v3-merkle-tree-10mb-trailing-data.apk",
+                "Failure");
+    }
+
+    public void testV4IncToV3NonIncSameKeyUpgradeSucceeds() throws Exception {
+        // V4 is only enabled on devices with Incremental feature
+        if (!hasIncrementalFeature()) {
+            return;
+        }
+
+        // See cts/hostsidetests/appsecurity/res/pkgsigverify/generate-apks.sh for the command
+        // to generate the apks
+        assertInstallV4Succeeds("v4-inc-to-v3-noninc-ec-p256-appv1.apk");
+
+        // non-incremental upgrade with the same key.
+        assertInstallSucceeds("v4-inc-to-v3-noninc-ec-p256-appv2.apk");
+    }
+
+    public void testV4IncToV3NonIncMismatchingKeyUpgradeFails() throws Exception {
+        // V4 is only enabled on devices with Incremental feature
+        if (!hasIncrementalFeature()) {
+            return;
+        }
+
+        // See cts/hostsidetests/appsecurity/res/pkgsigverify/generate-apks.sh for the command
+        // to generate the apks
+        assertInstallV4Succeeds("v4-inc-to-v3-noninc-ec-p256-appv1.apk");
+
+        // non-incremental upgrade with a mismatching key.
+        assertInstallFailsWithError("v4-inc-to-v3-noninc-ec-p384-appv2.apk",
+                "signatures do not match previously installed version");
+    }
+
+    public void testV4IncToV3NonIncRotatedKeyUpgradeSucceeds() throws Exception {
+        // V4 is only enabled on devices with Incremental feature
+        if (!hasIncrementalFeature()) {
+            return;
+        }
+
+        // See cts/hostsidetests/appsecurity/res/pkgsigverify/generate-apks.sh for the command
+        // to generate the apks
+        assertInstallV4Succeeds("v4-inc-to-v3-noninc-ec-p256-appv1.apk");
+
+        // non-incremental upgrade with key rotation.
+        assertInstallSucceeds("v4-inc-to-v3-noninc-ec-p384-rotated-ec-p256-appv2.apk");
+    }
+
+    public void testV4IncToV3NonIncMismatchedRotatedKeyUpgradeFails() throws Exception {
+        // V4 is only enabled on devices with Incremental feature
+        if (!hasIncrementalFeature()) {
+            return;
+        }
+
+        // See cts/hostsidetests/appsecurity/res/pkgsigverify/generate-apks.sh for the command
+        // to generate the apks
+        assertInstallV4Succeeds("v4-inc-to-v3-noninc-dsa-3072-appv1.apk");
+
+        // non-incremental upgrade with key rotation mismatch with key used in app v1.
+        assertInstallFailsWithError("v4-inc-to-v3-noninc-ec-p384-rotated-ec-p256-appv2.apk",
+                "signatures do not match previously installed version");
+    }
+
+
+    public void testV4IncToV2NonIncSameKeyUpgradeSucceeds() throws Exception {
+        // V4 is only enabled on devices with Incremental feature
+        if (!hasIncrementalFeature()) {
+            return;
+        }
+
+        // See cts/hostsidetests/appsecurity/res/pkgsigverify/generate-apks.sh for the command
+        // to generate the apks
+        assertInstallV4Succeeds("v4-inc-to-v2-noninc-ec-p256-appv1.apk");
+
+        // non-incremental upgrade with the same key.
+        assertInstallSucceeds("v4-inc-to-v2-noninc-ec-p256-appv2.apk");
+    }
+
+    public void testV4IncToV2NonIncMismatchingKeyUpgradeFails() throws Exception {
+        // V4 is only enabled on devices with Incremental feature
+        if (!hasIncrementalFeature()) {
+            return;
+        }
+
+        // See cts/hostsidetests/appsecurity/res/pkgsigverify/generate-apks.sh for the command
+        // to generate the apks
+        assertInstallV4Succeeds("v4-inc-to-v2-noninc-ec-p256-appv1.apk");
+
+        // non-incremental upgrade with a mismatching key.
+        assertInstallFailsWithError("v4-inc-to-v2-noninc-ec-p384-appv2.apk",
+                "signatures do not match previously installed version");
+    }
+
     private boolean hasIncrementalFeature() throws DeviceNotAvailableException {
         return getDevice().hasFeature("android.software.incremental_delivery");
     }
@@ -984,6 +1221,11 @@
         if (!installResult.equals("Success\n")) {
             fail("Failed to install " + apkFilenameInResources + ": " + installResult);
         }
+    }
+
+    private void assertInstallV4SucceedsAndUninstall(String apkFilenameInResources)
+            throws Exception {
+        assertInstallV4Succeeds(apkFilenameInResources);
         try {
             uninstallPackage();
         } catch (Exception e) {
@@ -1050,14 +1292,23 @@
     }
 
     private void installDeviceTestPkg() throws Exception {
-        installApkFromBuild(DEVICE_TESTS_APK);
+        assertInstallFromBuildSucceeds(DEVICE_TESTS_APK);
     }
 
-    private void installApkFromBuild(String apkName) throws Exception {
+    private void assertInstallFromBuildSucceeds(String apkName) throws Exception {
+        String result = installApkFromBuild(apkName);
+        assertNull("failed to install " + apkName + ", Reason: " + result, result);
+    }
+
+    private void assertInstallFromBuildFails(String apkName) throws Exception {
+        String result = installApkFromBuild(apkName);
+        assertNotNull("Successfully installed " + apkName + " when failure was expected", result);
+    }
+
+    private String installApkFromBuild(String apkName) throws Exception {
         CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(mCtsBuild);
         File apk = buildHelper.getTestFile(apkName);
-        String result = getDevice().installPackage(apk, true, INSTALL_ARG_FORCE_QUERYABLE);
-        assertNull("failed to install " + apkName + ", Reason: " + result, result);
+        return getDevice().installPackage(apk, true, INSTALL_ARG_FORCE_QUERYABLE);
     }
 
     private String installPackageFromResource(String apkFilenameInResources, boolean ephemeral)
@@ -1146,8 +1397,10 @@
         return getDevice().uninstallPackage(TEST_PKG);
     }
 
-    private String uninstallCompanionPackage() throws DeviceNotAvailableException {
-        return getDevice().uninstallPackage(COMPANION_TEST_PKG);
+    private String uninstallCompanionPackages() throws DeviceNotAvailableException {
+        String result1 = getDevice().uninstallPackage(COMPANION_TEST_PKG);
+        String result2 = getDevice().uninstallPackage(COMPANION2_TEST_PKG);
+        return result1 != null ? result1 : result2;
     }
 
     private String uninstallDeviceTestPackage() throws DeviceNotAvailableException {
@@ -1156,7 +1409,7 @@
 
     private void uninstallPackages() throws DeviceNotAvailableException {
         uninstallPackage();
-        uninstallCompanionPackage();
+        uninstallCompanionPackages();
         uninstallDeviceTestPackage();
     }
 }
diff --git a/hostsidetests/appsecurity/test-apps/tinyapp/Android.mk b/hostsidetests/appsecurity/test-apps/tinyapp/Android.mk
index e037d67..6e18c71 100644
--- a/hostsidetests/appsecurity/test-apps/tinyapp/Android.mk
+++ b/hostsidetests/appsecurity/test-apps/tinyapp/Android.mk
@@ -23,6 +23,12 @@
 LOCAL_PACKAGE_NAME := CtsPkgInstallTinyApp
 include $(BUILD_CTS_SUPPORT_PACKAGE)
 
+# This is the test package v2 signed with the default key.
+include $(LOCAL_PATH)/base.mk
+LOCAL_MANIFEST_FILE := AndroidManifest-v2.xml
+LOCAL_PACKAGE_NAME := CtsPkgInstallTinyAppV2
+include $(BUILD_CTS_SUPPORT_PACKAGE)
+
 # This is the test package signed using the V1/V2 signature schemes with
 # two signers targeting SDK version 30 with sandbox version 1. From this
 # package the v1-ec-p256-two-signers-targetSdk-30.apk is created with the
@@ -39,6 +45,14 @@
 LOCAL_ADDITIONAL_CERTIFICATES := $(cert_dir)/ec-p256_2
 include $(BUILD_CTS_SUPPORT_PACKAGE)
 
+# This is the test package signed using the V3 signature scheme
+# with the previous key in the lineage and part of a sharedUid.
+include $(LOCAL_PATH)/base.mk
+LOCAL_PACKAGE_NAME := v3-ec-p256-1-sharedUid
+LOCAL_MANIFEST_FILE := AndroidManifest-shareduid.xml
+LOCAL_CERTIFICATE := $(cert_dir)/ec-p256
+include $(BUILD_CTS_SUPPORT_PACKAGE)
+
 # This is the test package signed using the V3 signature scheme with
 # a rotated key and one signer in the lineage with default capabilities.
 include $(LOCAL_PATH)/base.mk
@@ -48,5 +62,82 @@
 LOCAL_CERTIFICATE_LINEAGE := $(cert_dir)/ec-p256-por_1_2-default-caps
 include $(BUILD_CTS_SUPPORT_PACKAGE)
 
+# This is the test package signed using the V3 signature scheme with
+# a rotated key and part of a shareduid. The capabilities of this lineage
+# grant access to the previous key in the lineage to join the sharedUid.
+include $(LOCAL_PATH)/base.mk
+LOCAL_PACKAGE_NAME := v3-ec-p256-with-por_1_2-default-caps-sharedUid
+LOCAL_MANIFEST_FILE := AndroidManifest-shareduid.xml
+LOCAL_CERTIFICATE := $(cert_dir)/ec-p256_2
+LOCAL_ADDITIONAL_CERTIFICATES := $(cert_dir)/ec-p256
+LOCAL_CERTIFICATE_LINEAGE := $(cert_dir)/ec-p256-por_1_2-default-caps
+include $(BUILD_CTS_SUPPORT_PACKAGE)
+
+# This is the test package signed using the V3 signature scheme with
+# a rotated key and part of a shareduid. The signing lineage begins
+# with a key that is not in any of the other lineages and is intended
+# to verify that two packages signed with lineages that have diverged
+# ancestors are not allowed to be installed in the same sharedUserId.
+include $(LOCAL_PATH)/base.mk
+LOCAL_PACKAGE_NAME := v3-por_Y_1_2-default-caps-sharedUid
+LOCAL_MANIFEST_FILE := AndroidManifest-shareduid.xml
+LOCAL_CERTIFICATE := $(cert_dir)/ec-p256_2
+LOCAL_ADDITIONAL_CERTIFICATES := $(cert_dir)/rsa-2048 $(cert_dir)/ec-p256
+LOCAL_CERTIFICATE_LINEAGE := $(cert_dir)/por_Y_1_2-default-caps
+include $(BUILD_CTS_SUPPORT_PACKAGE)
+
+# This is the test package signed using the V3 signature scheme with
+# a rotated key and part of a shareduid. The capabilities of this lineage
+# prevent the previous key in the lineage from joining the sharedUid.
+include $(LOCAL_PATH)/base.mk
+LOCAL_PACKAGE_NAME := v3-ec-p256-with-por_1_2-no-shUid-cap-sharedUid
+LOCAL_MANIFEST_FILE := AndroidManifest-shareduid.xml
+LOCAL_CERTIFICATE := $(cert_dir)/ec-p256_2
+LOCAL_ADDITIONAL_CERTIFICATES := $(cert_dir)/ec-p256
+LOCAL_CERTIFICATE_LINEAGE := $(cert_dir)/ec-p256-por_1_2-no-shUid-cap
+include $(BUILD_CTS_SUPPORT_PACKAGE)
+
+# This is the first companion package signed using the V3 signature scheme
+# with a rotated key and part of a sharedUid. The capabilities of this lineage
+# grant access to the previous key in the lineage to join the sharedUid.
+include $(LOCAL_PATH)/base.mk
+LOCAL_PACKAGE_NAME := v3-ec-p256-with-por_1_2-default-caps-sharedUid-companion
+LOCAL_MANIFEST_FILE := AndroidManifest-companion-shareduid.xml
+LOCAL_CERTIFICATE := $(cert_dir)/ec-p256_2
+LOCAL_ADDITIONAL_CERTIFICATES := $(cert_dir)/ec-p256
+LOCAL_CERTIFICATE_LINEAGE := $(cert_dir)/ec-p256-por_1_2-default-caps
+include $(BUILD_CTS_SUPPORT_PACKAGE)
+
+# This is the companion package signed using the V3 signature scheme with
+# a rotated key and part of a shareduid. The signing lineage begins
+# with a key that is not in any of the other lineages and is intended
+# to verify that two packages signed with lineages that have diverged
+# ancestors are not allowed to be installed in the same sharedUserId.
+include $(LOCAL_PATH)/base.mk
+LOCAL_PACKAGE_NAME := v3-por_Z_1_2-default-caps-sharedUid-companion
+LOCAL_MANIFEST_FILE := AndroidManifest-shareduid.xml
+LOCAL_CERTIFICATE := $(cert_dir)/ec-p256_2
+LOCAL_ADDITIONAL_CERTIFICATES := $(cert_dir)/dsa-2048 $(cert_dir)/ec-p256
+LOCAL_CERTIFICATE_LINEAGE := $(cert_dir)/por_Z_1_2-default-caps
+include $(BUILD_CTS_SUPPORT_PACKAGE)
+
+# This is the first companion package signed using the V3 signature scheme
+# # with a rotated key and part of a sharedUid but without the signing lineage.
+# This app is intended to test lineage scenarios where an app is only signed
+# with the latest key in the lineage.
+include $(LOCAL_PATH)/base.mk
+LOCAL_PACKAGE_NAME := v3-ec-p256-2-sharedUid-companion
+LOCAL_MANIFEST_FILE := AndroidManifest-companion-shareduid.xml
+LOCAL_CERTIFICATE := $(cert_dir)/ec-p256_2
+include $(BUILD_CTS_SUPPORT_PACKAGE)
+
+# This is the second companion package signed using the V3 signature scheme
+# with the previous key in the lineage and part of a sharedUid.
+include $(LOCAL_PATH)/base.mk
+LOCAL_PACKAGE_NAME := v3-ec-p256-1-sharedUid-companion2
+LOCAL_MANIFEST_FILE := AndroidManifest-companion2-shareduid.xml
+LOCAL_CERTIFICATE := $(cert_dir)/ec-p256
+include $(BUILD_CTS_SUPPORT_PACKAGE)
+
 cert_dir :=
 
diff --git a/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest-companion-shareduid.xml b/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest-companion-shareduid.xml
new file mode 100644
index 0000000..642653f
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest-companion-shareduid.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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="android.appsecurity.cts.tinyapp_companion"
+        android:sharedUserId="android.appsecurity.cts.tinyapp.shareduser"
+        android:versionCode="10"
+        android:versionName="1.0"
+        android:targetSandboxVersion="2">
+    <application android:label="@string/app_name">
+        <activity
+                android:name=".MainActivity"
+                android:label="@string/app_name" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest-companion2-shareduid.xml b/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest-companion2-shareduid.xml
new file mode 100644
index 0000000..f7a639d
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest-companion2-shareduid.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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="android.appsecurity.cts.tinyapp_companion2"
+        android:sharedUserId="android.appsecurity.cts.tinyapp.shareduser"
+        android:versionCode="10"
+        android:versionName="1.0"
+        android:targetSandboxVersion="2">
+    <application android:label="@string/app_name">
+        <activity
+                android:name=".MainActivity"
+                android:label="@string/app_name" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest-shareduid.xml b/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest-shareduid.xml
new file mode 100644
index 0000000..2c4d3d9
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest-shareduid.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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="android.appsecurity.cts.tinyapp"
+        android:sharedUserId="android.appsecurity.cts.tinyapp.shareduser"
+        android:versionCode="10"
+        android:versionName="1.0"
+        android:targetSandboxVersion="2">
+    <application android:label="@string/app_name">
+        <activity
+                android:name=".MainActivity"
+                android:label="@string/app_name" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest-v2.xml b/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest-v2.xml
new file mode 100644
index 0000000..ef62aac
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest-v2.xml
@@ -0,0 +1,32 @@
+<?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="android.appsecurity.cts.tinyapp"
+        android:versionCode="20"
+        android:versionName="2.0"
+        android:targetSandboxVersion="2">
+    <application android:label="@string/app_name">
+        <activity
+                android:name=".MainActivity"
+                android:label="@string/app_name" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/OrgOwnedProfileOwnerParentTest.java b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/OrgOwnedProfileOwnerParentTest.java
index 6ca7bd3..a2c8832 100644
--- a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/OrgOwnedProfileOwnerParentTest.java
+++ b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/OrgOwnedProfileOwnerParentTest.java
@@ -24,14 +24,11 @@
 
 import android.app.admin.DevicePolicyManager;
 import android.content.Context;
-import android.hardware.camera2.CameraManager;
 import android.os.Bundle;
-import android.os.Handler;
-import android.os.HandlerThread;
 import android.os.UserManager;
 import android.test.InstrumentationTestCase;
 
-import org.mockito.internal.util.collections.Sets;
+import com.google.common.collect.ImmutableSet;
 
 import java.util.Set;
 
@@ -39,70 +36,40 @@
 
     protected Context mContext;
     private DevicePolicyManager mParentDevicePolicyManager;
-    private DevicePolicyManager mDevicePolicyManager;
-
-    private CameraManager mCameraManager;
-
-    private HandlerThread mBackgroundThread;
-
-    /**
-     * A {@link Handler} for running tasks in the background.
-     */
-    private Handler mBackgroundHandler;
 
     @Override
     protected void setUp() throws Exception {
         super.setUp();
         mContext = getInstrumentation().getContext();
 
-        mDevicePolicyManager = (DevicePolicyManager)
+        DevicePolicyManager devicePolicyManager = (DevicePolicyManager)
                 mContext.getSystemService(Context.DEVICE_POLICY_SERVICE);
+        assertNotNull(devicePolicyManager);
         mParentDevicePolicyManager =
-                mDevicePolicyManager.getParentProfileInstance(ADMIN_RECEIVER_COMPONENT);
-        mCameraManager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE);
-
-        assertNotNull(mDevicePolicyManager);
+                devicePolicyManager.getParentProfileInstance(ADMIN_RECEIVER_COMPONENT);
         assertNotNull(mParentDevicePolicyManager);
-        assertNotNull(mCameraManager);
 
-        assertTrue(mDevicePolicyManager.isAdminActive(ADMIN_RECEIVER_COMPONENT));
+        assertTrue(devicePolicyManager.isAdminActive(ADMIN_RECEIVER_COMPONENT));
         assertTrue(
-                mDevicePolicyManager.isProfileOwnerApp(ADMIN_RECEIVER_COMPONENT.getPackageName()));
-        assertTrue(mDevicePolicyManager.isManagedProfile(ADMIN_RECEIVER_COMPONENT));
-        startBackgroundThread();
-    }
-
-    @Override
-    protected void tearDown() throws Exception {
-        stopBackgroundThread();
-        super.tearDown();
-    }
-
-    public void testSetAndGetCameraDisabled_onParent() throws Exception {
-        mParentDevicePolicyManager.setCameraDisabled(ADMIN_RECEIVER_COMPONENT, true);
-        boolean actualDisabled =
-                mParentDevicePolicyManager.getCameraDisabled(ADMIN_RECEIVER_COMPONENT);
-
-        assertThat(actualDisabled).isTrue();
-        checkCanOpenCamera(false);
-
-        mParentDevicePolicyManager.setCameraDisabled(ADMIN_RECEIVER_COMPONENT, false);
-        actualDisabled = mParentDevicePolicyManager.getCameraDisabled(ADMIN_RECEIVER_COMPONENT);
-
-        assertThat(actualDisabled).isFalse();
-        checkCanOpenCamera(true);
+                devicePolicyManager.isProfileOwnerApp(ADMIN_RECEIVER_COMPONENT.getPackageName()));
+        assertTrue(devicePolicyManager.isManagedProfile(ADMIN_RECEIVER_COMPONENT));
     }
 
     private static final Set<String> PROFILE_OWNER_ORGANIZATION_OWNED_GLOBAL_RESTRICTIONS =
-            Sets.newSet(
+            ImmutableSet.of(
+                    UserManager.DISALLOW_CONFIG_PRIVATE_DNS,
                     UserManager.DISALLOW_CONFIG_DATE_TIME,
+                    UserManager.DISALLOW_AIRPLANE_MODE
+            );
+
+    private static final Set<String> PROFILE_OWNER_ORGANIZATION_OWNED_LOCAL_RESTRICTIONS =
+            ImmutableSet.of(
                     UserManager.DISALLOW_BLUETOOTH,
                     UserManager.DISALLOW_BLUETOOTH_SHARING,
                     UserManager.DISALLOW_CONFIG_BLUETOOTH,
                     UserManager.DISALLOW_CONFIG_CELL_BROADCASTS,
                     UserManager.DISALLOW_CONFIG_LOCATION,
                     UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS,
-                    UserManager.DISALLOW_CONFIG_PRIVATE_DNS,
                     UserManager.DISALLOW_CONFIG_TETHERING,
                     UserManager.DISALLOW_CONFIG_WIFI,
                     UserManager.DISALLOW_CONTENT_CAPTURE,
@@ -112,7 +79,6 @@
                     UserManager.DISALLOW_SHARE_LOCATION,
                     UserManager.DISALLOW_SMS,
                     UserManager.DISALLOW_USB_FILE_TRANSFER,
-                    UserManager.DISALLOW_AIRPLANE_MODE,
                     UserManager.DISALLOW_MOUNT_PHYSICAL_MEDIA,
                     UserManager.DISALLOW_OUTGOING_CALLS,
                     UserManager.DISALLOW_UNMUTE_MICROPHONE
@@ -124,6 +90,9 @@
         for (String restriction : PROFILE_OWNER_ORGANIZATION_OWNED_GLOBAL_RESTRICTIONS) {
             testAddGetAndClearUserRestriction_onParent(restriction);
         }
+        for (String restriction : PROFILE_OWNER_ORGANIZATION_OWNED_LOCAL_RESTRICTIONS) {
+            testAddGetAndClearUserRestriction_onParent(restriction);
+        }
     }
 
     private void testAddGetAndClearUserRestriction_onParent(String restriction) {
@@ -147,56 +116,15 @@
     }
 
     private void testUnableToAddBaseUserRestriction(String restriction) {
-        assertThrows(UnsupportedOperationException.class,
+        assertThrows(SecurityException.class,
                 () -> mParentDevicePolicyManager.addUserRestriction(ADMIN_RECEIVER_COMPONENT,
                         restriction));
     }
 
     private void testUnableToClearBaseUserRestriction(String restriction) {
-        assertThrows(UnsupportedOperationException.class,
+        assertThrows(SecurityException.class,
                 () -> mParentDevicePolicyManager.clearUserRestriction(ADMIN_RECEIVER_COMPONENT,
                         restriction));
     }
 
-    private void checkCanOpenCamera(boolean canOpen) throws Exception {
-        // If the device does not support a camera it will return an empty camera ID list.
-        if (mCameraManager.getCameraIdList() == null
-                || mCameraManager.getCameraIdList().length == 0) {
-            return;
-        }
-        int retries = 10;
-        boolean successToOpen = !canOpen;
-        while (successToOpen != canOpen && retries > 0) {
-            retries--;
-            Thread.sleep(500);
-            successToOpen = CameraUtils
-                    .blockUntilOpenCamera(mCameraManager, mBackgroundHandler);
-        }
-        assertEquals(String.format("Timed out waiting the value to change to %b (actual=%b)",
-                canOpen, successToOpen), canOpen, successToOpen);
-    }
-
-    /**
-     * Starts a background thread and its {@link Handler}.
-     */
-    private void startBackgroundThread() {
-        mBackgroundThread = new HandlerThread("CameraBackground");
-        mBackgroundThread.start();
-        mBackgroundHandler = new Handler(mBackgroundThread.getLooper());
-    }
-
-    /**
-     * Stops the background thread and its {@link Handler}.
-     */
-    private void stopBackgroundThread() {
-        mBackgroundThread.quitSafely();
-        try {
-            mBackgroundThread.join();
-            mBackgroundThread = null;
-            mBackgroundHandler = null;
-        } catch (InterruptedException e) {
-            e.printStackTrace();
-        }
-    }
-
 }
\ No newline at end of file
diff --git a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/SecondaryLockscreenTest.java b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/SecondaryLockscreenTest.java
new file mode 100644
index 0000000..8baf460
--- /dev/null
+++ b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/SecondaryLockscreenTest.java
@@ -0,0 +1,51 @@
+/*
+ * 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.
+ */
+
+package com.android.cts.deviceandprofileowner;
+
+import static org.testng.Assert.assertThrows;
+
+import android.os.Process;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class SecondaryLockscreenTest extends BaseDeviceAdminTest {
+
+    @Before
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        assertFalse(mDevicePolicyManager.isSecondaryLockscreenEnabled(Process.myUserHandle()));
+    }
+
+    @After
+    @Override
+    public void tearDown() throws Exception {
+        assertFalse(mDevicePolicyManager.isSecondaryLockscreenEnabled(Process.myUserHandle()));
+        super.tearDown();
+    }
+
+    public void testSetSecondaryLockscreen_notSupervisionApp_throwsSecurityException() {
+        // This API is only available to the configured supervision app, which is not possible to
+        // override as part of a CTS test, so just test that a security exception is thrown as
+        // expected even for the DO/PO.
+        assertThrows(SecurityException.class,
+                () -> mDevicePolicyManager.setSecondaryLockscreenEnabled(ADMIN_RECEIVER_COMPONENT,
+                        true));
+    }
+}
\ No newline at end of file
diff --git a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/UserRestrictionsParentTest.java b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/UserRestrictionsParentTest.java
index 04eebca..82ce679 100644
--- a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/UserRestrictionsParentTest.java
+++ b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/UserRestrictionsParentTest.java
@@ -22,15 +22,35 @@
 
 import android.app.admin.DevicePolicyManager;
 import android.content.Context;
+import android.hardware.camera2.CameraManager;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
 import android.os.UserManager;
 import android.test.InstrumentationTestCase;
+import android.util.Log;
+
+import com.google.common.collect.ImmutableSet;
+
+import java.util.Set;
 
 public class UserRestrictionsParentTest extends InstrumentationTestCase {
 
+    private static final String TAG = "UserRestrictionsParentTest";
+
     protected Context mContext;
     private DevicePolicyManager mDevicePolicyManager;
     private UserManager mUserManager;
 
+    private CameraManager mCameraManager;
+
+    private HandlerThread mBackgroundThread;
+
+    /**
+     * A {@link Handler} for running tasks in the background.
+     */
+    private Handler mBackgroundHandler;
+
     @Override
     protected void setUp() throws Exception {
         super.setUp();
@@ -40,8 +60,19 @@
                 mContext.getSystemService(Context.DEVICE_POLICY_SERVICE);
         assertNotNull(mDevicePolicyManager);
 
+        mCameraManager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE);
+        assertNotNull(mCameraManager);
+
         mUserManager = mContext.getSystemService(UserManager.class);
         assertNotNull(mUserManager);
+
+        startBackgroundThread();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        stopBackgroundThread();
+        super.tearDown();
     }
 
     public void testAddUserRestrictionDisallowConfigDateTime_onParent() {
@@ -54,11 +85,187 @@
     }
 
     public void testHasUserRestrictionDisallowConfigDateTime() {
-        assertThat(mUserManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_DATE_TIME)).isTrue();
+        assertThat(mUserManager.
+                hasUserRestriction(UserManager.DISALLOW_CONFIG_DATE_TIME)).isTrue();
     }
 
     public void testUserRestrictionDisallowConfigDateTimeIsNotPersisted() {
-        assertThat(
-                mUserManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_DATE_TIME)).isFalse();
+        assertThat(mUserManager.
+                hasUserRestriction(UserManager.DISALLOW_CONFIG_DATE_TIME)).isFalse();
     }
+
+    public void testAddUserRestrictionDisallowAddUser_onParent() {
+        DevicePolicyManager parentDevicePolicyManager =
+                mDevicePolicyManager.getParentProfileInstance(ADMIN_RECEIVER_COMPONENT);
+        assertNotNull(parentDevicePolicyManager);
+
+        parentDevicePolicyManager.addUserRestriction(ADMIN_RECEIVER_COMPONENT,
+                UserManager.DISALLOW_ADD_USER);
+    }
+
+    public void testHasUserRestrictionDisallowAddUser() {
+        assertThat(hasUserRestriction(UserManager.DISALLOW_ADD_USER)).isTrue();
+    }
+
+    public void testClearUserRestrictionDisallowAddUser() {
+        DevicePolicyManager parentDevicePolicyManager =
+                mDevicePolicyManager.getParentProfileInstance(ADMIN_RECEIVER_COMPONENT);
+
+        parentDevicePolicyManager.clearUserRestriction(ADMIN_RECEIVER_COMPONENT,
+                UserManager.DISALLOW_ADD_USER);
+    }
+
+    public void testAddUserRestrictionCameraDisabled_onParent() {
+        DevicePolicyManager parentDevicePolicyManager =
+                mDevicePolicyManager.getParentProfileInstance(ADMIN_RECEIVER_COMPONENT);
+        parentDevicePolicyManager.setCameraDisabled(ADMIN_RECEIVER_COMPONENT, true);
+        boolean actualDisabled =
+                parentDevicePolicyManager.getCameraDisabled(ADMIN_RECEIVER_COMPONENT);
+
+        assertThat(actualDisabled).isTrue();
+    }
+
+    public void testRemoveUserRestrictionCameraEnabled_onParent() {
+        DevicePolicyManager parentDevicePolicyManager =
+                mDevicePolicyManager.getParentProfileInstance(ADMIN_RECEIVER_COMPONENT);
+        parentDevicePolicyManager.setCameraDisabled(ADMIN_RECEIVER_COMPONENT, false);
+        boolean actualDisabled =
+                parentDevicePolicyManager.getCameraDisabled(ADMIN_RECEIVER_COMPONENT);
+
+        assertThat(actualDisabled).isFalse();
+    }
+
+    public void testCannotOpenCamera() throws Exception {
+        checkCanOpenCamera(false);
+    }
+
+    public void testCanOpenCamera() throws Exception {
+        checkCanOpenCamera(true);
+    }
+
+    private void checkCanOpenCamera(boolean canOpen) throws Exception {
+        // If the device does not support a camera it will return an empty camera ID list.
+        if (mCameraManager.getCameraIdList() == null
+                || mCameraManager.getCameraIdList().length == 0) {
+            return;
+        }
+        int retries = 10;
+        boolean successToOpen = !canOpen;
+        while (successToOpen != canOpen && retries > 0) {
+            retries--;
+            Thread.sleep(500);
+            successToOpen = CameraUtils
+                    .blockUntilOpenCamera(mCameraManager, mBackgroundHandler);
+        }
+        assertEquals(String.format("Timed out waiting the value to change to %b (actual=%b)",
+                canOpen, successToOpen), canOpen, successToOpen);
+    }
+
+    private static final Set<String> PROFILE_OWNER_ORGANIZATION_OWNED_LOCAL_RESTRICTIONS =
+            ImmutableSet.of(
+                    UserManager.DISALLOW_BLUETOOTH,
+                    UserManager.DISALLOW_BLUETOOTH_SHARING,
+                    UserManager.DISALLOW_CONFIG_BLUETOOTH,
+                    UserManager.DISALLOW_CONFIG_CELL_BROADCASTS,
+                    UserManager.DISALLOW_CONFIG_LOCATION,
+                    UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS,
+                    UserManager.DISALLOW_CONFIG_TETHERING,
+                    UserManager.DISALLOW_CONFIG_WIFI,
+                    UserManager.DISALLOW_CONTENT_CAPTURE,
+                    UserManager.DISALLOW_CONTENT_SUGGESTIONS,
+                    UserManager.DISALLOW_DATA_ROAMING,
+                    UserManager.DISALLOW_SAFE_BOOT,
+                    UserManager.DISALLOW_SHARE_LOCATION,
+                    UserManager.DISALLOW_SMS,
+                    UserManager.DISALLOW_USB_FILE_TRANSFER,
+                    UserManager.DISALLOW_MOUNT_PHYSICAL_MEDIA,
+                    UserManager.DISALLOW_OUTGOING_CALLS,
+                    UserManager.DISALLOW_UNMUTE_MICROPHONE
+                    // This restriction disables ADB, so is not used in test.
+                    // UserManager.DISALLOW_DEBUGGING_FEATURES
+            );
+
+    public void testPerProfileUserRestriction_onParent() {
+        DevicePolicyManager parentDevicePolicyManager =
+                mDevicePolicyManager.getParentProfileInstance(ADMIN_RECEIVER_COMPONENT);
+        assertNotNull(parentDevicePolicyManager);
+
+        for (String restriction : PROFILE_OWNER_ORGANIZATION_OWNED_LOCAL_RESTRICTIONS) {
+            try {
+                boolean hasRestrictionOnManagedProfile = mUserManager.hasUserRestriction(
+                        restriction);
+
+                parentDevicePolicyManager.addUserRestriction(ADMIN_RECEIVER_COMPONENT, restriction);
+                // Assert user restriction on personal profile has been added
+                assertThat(hasUserRestriction(restriction)).isTrue();
+                // Assert user restriction on managed profile has not changed
+                assertThat(mUserManager.hasUserRestriction(restriction)).isEqualTo(
+                        hasRestrictionOnManagedProfile);
+            } finally {
+                parentDevicePolicyManager.clearUserRestriction(ADMIN_RECEIVER_COMPONENT,
+                        restriction);
+                assertThat(hasUserRestriction(restriction)).isFalse();
+            }
+        }
+    }
+
+    private static final Set<String> PROFILE_OWNER_ORGANIZATION_OWNED_GLOBAL_RESTRICTIONS =
+            ImmutableSet.of(
+                    UserManager.DISALLOW_CONFIG_PRIVATE_DNS,
+                    UserManager.DISALLOW_CONFIG_DATE_TIME,
+                    UserManager.DISALLOW_AIRPLANE_MODE
+            );
+
+    public void testPerDeviceUserRestriction_onParent() {
+        DevicePolicyManager parentDevicePolicyManager =
+                mDevicePolicyManager.getParentProfileInstance(ADMIN_RECEIVER_COMPONENT);
+        assertNotNull(parentDevicePolicyManager);
+
+        for (String restriction : PROFILE_OWNER_ORGANIZATION_OWNED_GLOBAL_RESTRICTIONS) {
+            try {
+                parentDevicePolicyManager.addUserRestriction(ADMIN_RECEIVER_COMPONENT, restriction);
+                // Assert user restriction on personal profile has been added
+                assertThat(hasUserRestriction(restriction)).isTrue();
+                // Assert user restriction on managed profile has been added
+                assertThat(mUserManager.hasUserRestriction(restriction)).isTrue();
+            } finally {
+                parentDevicePolicyManager.clearUserRestriction(ADMIN_RECEIVER_COMPONENT,
+                        restriction);
+                assertThat(hasUserRestriction(restriction)).isFalse();
+                assertThat(mUserManager.hasUserRestriction(restriction)).isFalse();
+            }
+        }
+    }
+
+    private boolean hasUserRestriction(String key) {
+        DevicePolicyManager parentDevicePolicyManager =
+                mDevicePolicyManager.getParentProfileInstance(ADMIN_RECEIVER_COMPONENT);
+        Bundle userRestrictions =
+                parentDevicePolicyManager.getUserRestrictions(ADMIN_RECEIVER_COMPONENT);
+        return userRestrictions.getBoolean(key);
+    }
+
+    /**
+     * Starts a background thread and its {@link Handler}.
+     */
+    private void startBackgroundThread() {
+        mBackgroundThread = new HandlerThread("CameraBackground");
+        mBackgroundThread.start();
+        mBackgroundHandler = new Handler(mBackgroundThread.getLooper());
+    }
+
+    /**
+     * Stops the background thread and its {@link Handler}.
+     */
+    private void stopBackgroundThread() {
+        mBackgroundThread.quitSafely();
+        try {
+            mBackgroundThread.join();
+            mBackgroundThread = null;
+            mBackgroundHandler = null;
+        } catch (InterruptedException e) {
+            Log.e(TAG, "Interrupted exception thrown while stopping background thread.");
+        }
+    }
+
 }
diff --git a/hostsidetests/devicepolicy/app/DummyApps/dummyapp1/AndroidManifest.xml b/hostsidetests/devicepolicy/app/DummyApps/dummyapp1/AndroidManifest.xml
index 00fe83d..3bec42a 100644
--- a/hostsidetests/devicepolicy/app/DummyApps/dummyapp1/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/DummyApps/dummyapp1/AndroidManifest.xml
@@ -31,7 +31,8 @@
             </intent-filter>
         </receiver>
         <activity
-            android:name="android.app.Activity">
+            android:name="android.app.Activity"
+            android:exported="true">
         </activity>
     </application>
 </manifest>
diff --git a/hostsidetests/devicepolicy/app/DummyApps/dummyapp2/AndroidManifest.xml b/hostsidetests/devicepolicy/app/DummyApps/dummyapp2/AndroidManifest.xml
index 97c7d76..1f1975f 100644
--- a/hostsidetests/devicepolicy/app/DummyApps/dummyapp2/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/DummyApps/dummyapp2/AndroidManifest.xml
@@ -31,7 +31,8 @@
             </intent-filter>
         </receiver>
         <activity
-            android:name="android.app.Activity">
+            android:name="android.app.Activity"
+            android:exported="true">
         </activity>
     </application>
 </manifest>
diff --git a/hostsidetests/devicepolicy/app/DummyApps/dummyapp3/AndroidManifest.xml b/hostsidetests/devicepolicy/app/DummyApps/dummyapp3/AndroidManifest.xml
index dbd0778..446c3a1 100644
--- a/hostsidetests/devicepolicy/app/DummyApps/dummyapp3/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/DummyApps/dummyapp3/AndroidManifest.xml
@@ -31,7 +31,8 @@
             </intent-filter>
         </receiver>
         <activity
-            android:name="android.app.Activity">
+            android:name="android.app.Activity"
+            android:exported="true">
         </activity>
     </application>
 </manifest>
diff --git a/hostsidetests/devicepolicy/app/DummyApps/dummyapp4/AndroidManifest.xml b/hostsidetests/devicepolicy/app/DummyApps/dummyapp4/AndroidManifest.xml
index 7001a87..8c0356c 100644
--- a/hostsidetests/devicepolicy/app/DummyApps/dummyapp4/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/DummyApps/dummyapp4/AndroidManifest.xml
@@ -31,7 +31,8 @@
             </intent-filter>
         </receiver>
         <activity
-            android:name="android.app.Activity">
+            android:name="android.app.Activity"
+            android:exported="true">
         </activity>
     </application>
 </manifest>
diff --git a/hostsidetests/devicepolicy/app/ManagedProfile/AndroidManifest.xml b/hostsidetests/devicepolicy/app/ManagedProfile/AndroidManifest.xml
index b6a7b6d..e5786f6 100644
--- a/hostsidetests/devicepolicy/app/ManagedProfile/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/ManagedProfile/AndroidManifest.xml
@@ -32,6 +32,7 @@
     <uses-permission android:name="android.permission.READ_CALL_LOG"/>
     <uses-permission android:name="android.permission.WRITE_CALL_LOG"/>
     <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"/>
+    <uses-permission android:name="android.permission.WRITE_SETTINGS"/>
     <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
     <uses-permission android:name="android.permission.READ_CALENDAR" />
     <uses-permission android:name="android.permission.WRITE_CALENDAR" />
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceAndProfileOwnerTest.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceAndProfileOwnerTest.java
index fd6109c..07e0ace 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceAndProfileOwnerTest.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceAndProfileOwnerTest.java
@@ -2068,6 +2068,15 @@
                 "testCallingIsOrganizationOwnedWithManagedProfileExpectingFalse");
     }
 
+    @LockSettingsTest
+    @Test
+    public void testSecondaryLockscreen() throws Exception {
+        if (!mHasFeature) {
+            return;
+        }
+        executeDeviceTestClass(".SecondaryLockscreenTest");
+    }
+
     private String getLaunchableSystemPackage() throws DeviceNotAvailableException {
         final List<String> enabledSystemPackageNames = getEnabledSystemPackageNames();
         for (String enabledSystemPackage : enabledSystemPackageNames) {
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/OrgOwnedProfileOwnerTest.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/OrgOwnedProfileOwnerTest.java
index ea90988..acb524b 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/OrgOwnedProfileOwnerTest.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/OrgOwnedProfileOwnerTest.java
@@ -114,15 +114,6 @@
     }
 
     @Test
-    public void testCannotAddSecondaryUser() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
-        failToCreateUser();
-    }
-
-    @Test
     public void testCanRelinquishControlOverDevice() throws Exception {
         if (!mHasFeature) {
             return;
@@ -192,15 +183,13 @@
 
     @Test
     public void testUserRestrictionsSetOnParentAreNotPersisted() throws Exception {
-        if (!mHasFeature) {
+        if (!mHasFeature || !canCreateAdditionalUsers(1)) {
             return;
         }
         installAppAsUser(DEVICE_ADMIN_APK, mPrimaryUserId);
         runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".UserRestrictionsParentTest",
                 "testAddUserRestrictionDisallowConfigDateTime_onParent", mUserId);
         runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".UserRestrictionsParentTest",
-                "testHasUserRestrictionDisallowConfigDateTime", mUserId);
-        runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".UserRestrictionsParentTest",
                 "testHasUserRestrictionDisallowConfigDateTime", mPrimaryUserId);
         removeOrgOwnedProfile();
         assertHasNoUser(mUserId);
@@ -214,6 +203,46 @@
     }
 
     @Test
+    public void testPerProfileUserRestrictionOnParent() throws Exception {
+        if (!mHasFeature) {
+            return;
+        }
+
+        runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".UserRestrictionsParentTest",
+                "testPerProfileUserRestriction_onParent", mUserId);
+    }
+
+    @Test
+    public void testPerDeviceUserRestrictionOnParent() throws Exception {
+        if (!mHasFeature) {
+            return;
+        }
+
+        runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".UserRestrictionsParentTest",
+                "testPerDeviceUserRestriction_onParent", mUserId);
+    }
+
+    @Test
+    public void testCameraDisabledOnParentIsEnforced() throws Exception {
+        if (!mHasFeature) {
+            return;
+        }
+
+        installAppAsUser(DEVICE_ADMIN_APK, mPrimaryUserId);
+        try {
+            runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".UserRestrictionsParentTest",
+                    "testAddUserRestrictionCameraDisabled_onParent", mUserId);
+            runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".UserRestrictionsParentTest",
+                    "testCannotOpenCamera", mPrimaryUserId);
+        } finally {
+            runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".UserRestrictionsParentTest",
+                    "testRemoveUserRestrictionCameraEnabled_onParent", mUserId);
+            runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".UserRestrictionsParentTest",
+                    "testCanOpenCamera", mPrimaryUserId);
+        }
+    }
+
+    @Test
     public void testCameraDisabledOnParentLogged() throws Exception {
         if (!mHasFeature || !isStatsdEnabled(getDevice())) {
             return;
diff --git a/hostsidetests/scopedstorage/Android.bp b/hostsidetests/scopedstorage/Android.bp
new file mode 100644
index 0000000..1d4b77a
--- /dev/null
+++ b/hostsidetests/scopedstorage/Android.bp
@@ -0,0 +1,81 @@
+// 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.
+
+android_test_helper_app {
+    name: "CtsScopedStorageTestAppA",
+    manifest: "ScopedStorageTestHelper/TestAppA.xml",
+    static_libs: ["androidx.test.rules", "cts-scopedstorage-lib"],
+    sdk_version: "test_current",
+    srcs: ["ScopedStorageTestHelper/src/**/*.java"],
+}
+android_test_helper_app {
+    name: "CtsScopedStorageTestAppB",
+    manifest: "ScopedStorageTestHelper/TestAppB.xml",
+    static_libs: ["androidx.test.rules", "cts-scopedstorage-lib"],
+    sdk_version: "test_current",
+    srcs: ["ScopedStorageTestHelper/src/**/*.java"],
+}
+android_test_helper_app {
+    name: "CtsScopedStorageTestAppC",
+    manifest: "ScopedStorageTestHelper/TestAppC.xml",
+    static_libs: ["androidx.test.rules", "cts-scopedstorage-lib"],
+    sdk_version: "test_current",
+    srcs: ["ScopedStorageTestHelper/src/**/*.java"],
+}
+android_test_helper_app {
+    name: "CtsScopedStorageTestAppCLegacy",
+    manifest: "ScopedStorageTestHelper/TestAppCLegacy.xml",
+    static_libs: ["androidx.test.rules", "cts-scopedstorage-lib"],
+    sdk_version: "test_current",
+    target_sdk_version: "28",
+    srcs: ["ScopedStorageTestHelper/src/**/*.java"],
+}
+
+android_test {
+    name: "ScopedStorageTest",
+    manifest: "AndroidManifest.xml",
+    srcs: ["src/**/*.java"],
+    static_libs: ["androidx.test.rules", "truth-prebuilt", "cts-scopedstorage-lib"],
+    compile_multilib: "both",
+    test_suites: ["general-tests", "mts"],
+    sdk_version: "test_current",
+    java_resources: [
+        ":CtsScopedStorageTestAppA",
+        ":CtsScopedStorageTestAppB",
+        ":CtsScopedStorageTestAppC",
+        ":CtsScopedStorageTestAppCLegacy",
+    ]
+}
+android_test {
+    name: "LegacyStorageTest",
+    manifest: "legacy/AndroidManifest.xml",
+    srcs: ["legacy/src/**/*.java"],
+    static_libs: ["androidx.test.rules", "truth-prebuilt",  "cts-scopedstorage-lib"],
+    compile_multilib: "both",
+    test_suites: ["general-tests", "mts"],
+    sdk_version: "test_current",
+    target_sdk_version: "29",
+    java_resources: [
+        ":CtsScopedStorageTestAppA",
+    ]
+}
+
+java_test_host {
+    name: "CtsScopedStorageHostTest",
+    srcs: ["host/src/**/*.java"],
+    libs: ["tradefed"],
+    static_libs: ["testng"],
+    test_suites: ["general-tests", "mts"],
+    test_config: "AndroidTest.xml",
+}
diff --git a/hostsidetests/scopedstorage/AndroidManifest.xml b/hostsidetests/scopedstorage/AndroidManifest.xml
new file mode 100644
index 0000000..0394f4b
--- /dev/null
+++ b/hostsidetests/scopedstorage/AndroidManifest.xml
@@ -0,0 +1,33 @@
+<?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="android.scopedstorage.cts" >
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
+    <application>
+        <receiver android:name="com.android.cts.install.lib.LocalIntentSender"
+                  android:exported="true" />
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="android.scopedstorage.cts"
+                     android:label="Tests for scoped storage"/>
+
+</manifest>
diff --git a/hostsidetests/scopedstorage/AndroidTest.xml b/hostsidetests/scopedstorage/AndroidTest.xml
new file mode 100644
index 0000000..64599d8
--- /dev/null
+++ b/hostsidetests/scopedstorage/AndroidTest.xml
@@ -0,0 +1,31 @@
+<?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.
+-->
+<configuration description="External storage host test for legacy and scoped storage">
+    <option name="test-suite-tag" value="cts" />
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="ScopedStorageTest.apk" />
+        <option name="test-file-name" value="LegacyStorageTest.apk" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.HostTest" >
+        <option name="class" value="android.scopedstorage.cts.host.LegacyStorageHostTest" />
+        <option name="class" value="android.scopedstorage.cts.host.ScopedStorageHostTest" />
+    </test>
+
+    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+        <option name="mainline-module-package-name" value="com.google.android.mediaprovider" />
+    </object>
+</configuration>
diff --git a/hostsidetests/scopedstorage/OWNERS b/hostsidetests/scopedstorage/OWNERS
new file mode 100644
index 0000000..9156e6b
--- /dev/null
+++ b/hostsidetests/scopedstorage/OWNERS
@@ -0,0 +1,6 @@
+jsharkey@android.com
+maco@google.com
+marcone@google.com
+nandana@google.com
+shafik@google.com
+zezeozue@google.com
diff --git a/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppA.xml b/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppA.xml
new file mode 100644
index 0000000..1747eb6
--- /dev/null
+++ b/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppA.xml
@@ -0,0 +1,35 @@
+<?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="android.scopedstorage.cts.testapp.A"
+    android:versionCode="1"
+    android:versionName="1.0" >
+
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
+
+    <application android:label="TestAppA">
+        <activity android:name="android.scopedstorage.cts.ScopedStorageTestHelper">
+            <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/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppB.xml b/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppB.xml
new file mode 100644
index 0000000..cf9a327
--- /dev/null
+++ b/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppB.xml
@@ -0,0 +1,35 @@
+<?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="android.scopedstorage.cts.testapp.B"
+    android:versionCode="1"
+    android:versionName="1.0" >
+
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
+
+    <application android:label="TestAppB">
+        <activity android:name="android.scopedstorage.cts.ScopedStorageTestHelper">
+            <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/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppC.xml b/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppC.xml
new file mode 100644
index 0000000..e6ee00a
--- /dev/null
+++ b/hostsidetests/scopedstorage/ScopedStorageTestHelper/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="android.scopedstorage.cts.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="android.scopedstorage.cts.ScopedStorageTestHelper">
+            <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/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppCLegacy.xml b/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppCLegacy.xml
new file mode 100644
index 0000000..be1bd75
--- /dev/null
+++ b/hostsidetests/scopedstorage/ScopedStorageTestHelper/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="android.scopedstorage.cts.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="android.scopedstorage.cts.ScopedStorageTestHelper">
+            <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/hostsidetests/scopedstorage/ScopedStorageTestHelper/src/android/scopedstorage/cts/ScopedStorageTestHelper.java b/hostsidetests/scopedstorage/ScopedStorageTestHelper/src/android/scopedstorage/cts/ScopedStorageTestHelper.java
new file mode 100644
index 0000000..86a7096
--- /dev/null
+++ b/hostsidetests/scopedstorage/ScopedStorageTestHelper/src/android/scopedstorage/cts/ScopedStorageTestHelper.java
@@ -0,0 +1,180 @@
+/*
+ * 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.
+ */
+package android.scopedstorage.cts;
+
+import static android.scopedstorage.cts.lib.RedactionTestHelper.EXIF_METADATA_QUERY;
+import static android.scopedstorage.cts.lib.RedactionTestHelper.getExifMetadata;
+import static android.scopedstorage.cts.lib.TestUtils.CAN_READ_WRITE_QUERY;
+import static android.scopedstorage.cts.lib.TestUtils.CREATE_FILE_QUERY;
+import static android.scopedstorage.cts.lib.TestUtils.DELETE_FILE_QUERY;
+import static android.scopedstorage.cts.lib.TestUtils.INTENT_EXCEPTION;
+import static android.scopedstorage.cts.lib.TestUtils.INTENT_EXTRA_PATH;
+import static android.scopedstorage.cts.lib.TestUtils.OPEN_FILE_FOR_READ_QUERY;
+import static android.scopedstorage.cts.lib.TestUtils.OPEN_FILE_FOR_WRITE_QUERY;
+import static android.scopedstorage.cts.lib.TestUtils.QUERY_TYPE;
+import static android.scopedstorage.cts.lib.TestUtils.READDIR_QUERY;
+import static android.scopedstorage.cts.lib.TestUtils.canOpen;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Environment;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+
+/**
+ * Helper app for ScopedStorageTest.
+ *
+ * <p>Used to perform ScopedStorageTest functions as a different app. Based on the Query type
+ * app can perform different functions and send the result back to host app.
+ */
+public class ScopedStorageTestHelper extends Activity {
+    private static final String TAG = "ScopedStorageTestHelper";
+    private static final File ANDROID_DIR =
+            new File(Environment.getExternalStorageDirectory(), "Android");
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        String queryType = getIntent().getStringExtra(QUERY_TYPE);
+        queryType = queryType == null ? "null" : queryType;
+        Intent returnIntent;
+        try {
+            switch (queryType) {
+                case READDIR_QUERY:
+                    returnIntent = sendDirectoryEntries(queryType);
+                    break;
+                case CAN_READ_WRITE_QUERY:
+                case CREATE_FILE_QUERY:
+                case DELETE_FILE_QUERY:
+                case OPEN_FILE_FOR_READ_QUERY:
+                case OPEN_FILE_FOR_WRITE_QUERY:
+                    returnIntent = accessFile(queryType);
+                    break;
+                case EXIF_METADATA_QUERY:
+                    returnIntent = sendMetadata(queryType);
+                    break;
+                case "null":
+                default:
+                    throw new IllegalStateException(
+                            "Unknown query received from launcher app: " + queryType);
+            }
+        } catch (Exception e) {
+            returnIntent = new Intent(queryType);
+            returnIntent.putExtra(INTENT_EXCEPTION, e);
+        }
+        sendBroadcast(returnIntent);
+    }
+
+    private Intent sendMetadata(String queryType) throws IOException {
+        final Intent intent = new Intent(queryType);
+        if (getIntent().hasExtra(INTENT_EXTRA_PATH)) {
+            final String filePath = getIntent().getStringExtra(INTENT_EXTRA_PATH);
+            if (EXIF_METADATA_QUERY.equals(queryType)) {
+                intent.putExtra(queryType, getExifMetadata(new File(filePath)));
+            }
+        } else {
+            throw new IllegalStateException(
+                    EXIF_METADATA_QUERY + ": File path not set from launcher app");
+        }
+        return intent;
+    }
+
+    private Intent sendDirectoryEntries(String queryType) throws IOException {
+        if (getIntent().hasExtra(INTENT_EXTRA_PATH)) {
+            final String directoryPath = getIntent().getStringExtra(INTENT_EXTRA_PATH);
+            ArrayList<String> directoryEntriesList = new ArrayList<>();
+            if (queryType.equals(READDIR_QUERY)) {
+                final String[] directoryEntries = new File(directoryPath).list();
+                if (directoryEntries == null) {
+                    throw new IOException(
+                            "I/O exception while listing entries for " + directoryPath);
+                }
+                Collections.addAll(directoryEntriesList, directoryEntries);
+            }
+            final Intent intent = new Intent(queryType);
+            intent.putStringArrayListExtra(queryType, directoryEntriesList);
+            return intent;
+        } else {
+            throw new IllegalStateException(
+                    READDIR_QUERY + ": Directory path not set from launcher app");
+        }
+    }
+
+    private Intent accessFile(String queryType) throws IOException {
+        if (getIntent().hasExtra(INTENT_EXTRA_PATH)) {
+            final String filePath = getIntent().getStringExtra(INTENT_EXTRA_PATH);
+            final File file = new File(filePath);
+            boolean returnStatus = false;
+            if (queryType.equals(CAN_READ_WRITE_QUERY)) {
+                returnStatus = file.exists() && file.canRead() && file.canWrite();
+            } else if (queryType.equals(CREATE_FILE_QUERY)) {
+                maybeCreateParentDirInAndroid(file);
+                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 */);
+            }
+            final Intent intent = new Intent(queryType);
+            intent.putExtra(queryType, returnStatus);
+            return intent;
+        } else {
+            throw new IllegalStateException(queryType + ": File path not set from launcher app");
+        }
+    }
+
+    private void maybeCreateParentDirInAndroid(File file) {
+        if (!file.getAbsolutePath().startsWith(ANDROID_DIR.getAbsolutePath())) {
+            return;
+        }
+        String[] segments = file.getAbsolutePath().split("/");
+        int index = ANDROID_DIR.getAbsolutePath().split("/").length;
+        if (index < segments.length) {
+            // Create the external app dir first.
+            if (createExternalAppDir(segments[index])) {
+                // Then create everything along the path.
+                file.getParentFile().mkdirs();
+            }
+        }
+    }
+
+    private boolean createExternalAppDir(String name) {
+        // Apps are not allowed to create data/cache/obb etc under Android directly and are
+        // expected to call one of the following methods.
+        switch (name) {
+            case "data":
+                getApplicationContext().getExternalFilesDir(null);
+                return true;
+            case "cache":
+                getApplicationContext().getExternalCacheDir();
+                return true;
+            case "obb":
+                getApplicationContext().getObbDir();
+                return true;
+            case "media":
+                getApplicationContext().getExternalMediaDirs();
+                return true;
+            default:
+                return false;
+        }
+    }
+}
diff --git a/hostsidetests/scopedstorage/host/src/android/scopedstorage/cts/host/LegacyStorageHostTest.java b/hostsidetests/scopedstorage/host/src/android/scopedstorage/cts/host/LegacyStorageHostTest.java
new file mode 100644
index 0000000..20da224
--- /dev/null
+++ b/hostsidetests/scopedstorage/host/src/android/scopedstorage/cts/host/LegacyStorageHostTest.java
@@ -0,0 +1,206 @@
+/*
+ * 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.
+ */
+
+package android.scopedstorage.cts.host;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertTrue;
+
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Runs the legacy file path access tests.
+ */
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class LegacyStorageHostTest extends BaseHostJUnit4Test {
+    public static final String SHELL_FILE = "/sdcard/LegacyAccessHostTest_shell";
+
+    private boolean isExternalStorageSetup = false;
+
+    private String executeShellCommand(String cmd) throws Exception {
+        return getDevice().executeShellCommand(cmd);
+    }
+
+    /**
+     * Runs the given phase of LegacyFileAccessTest by calling into the device.
+     * Throws an exception if the test phase fails.
+     */
+    private void runDeviceTest(String phase) throws Exception {
+        assertTrue(runDeviceTests("android.scopedstorage.cts.legacy",
+                "android.scopedstorage.cts.legacy.LegacyStorageTest", phase));
+    }
+
+    /**
+     * <p> Keep in mind that granting WRITE_EXTERNAL_STORAGE also grants READ_EXTERNAL_STORAGE,
+     * so in order to test a case where the reader has only WRITE, we must explicitly revoke READ.
+     */
+    private void grantPermissions(String... perms) throws Exception {
+        for (String perm : perms) {
+            executeShellCommand("pm grant android.scopedstorage.cts.legacy " + perm);
+        }
+    }
+
+    private void revokePermissions(String... perms) throws Exception {
+        for (String perm : perms) {
+            executeShellCommand("pm revoke android.scopedstorage.cts.legacy " + perm);
+        }
+    }
+
+    /**
+     * Creates a file {@code filePath} in shell and may bypass Media Provider restrictions for
+     * creating file.
+     */
+    private void createFileAsShell(String filePath) throws Exception {
+        executeShellCommand("touch " + filePath);
+        assertThat(getDevice().doesFileExist(filePath)).isTrue();
+    }
+
+    private void setupExternalStorage() throws Exception {
+        if (!isExternalStorageSetup) {
+            runDeviceTest("setupExternalStorage");
+            isExternalStorageSetup = true;
+        }
+    }
+
+    @Before
+    public void setup() throws Exception {
+        setupExternalStorage();
+        // Granting WRITE automatically grants READ as well, so we grant them both explicitly by
+        // default in order to avoid confusion. Test cases that don't want any of those permissions
+        // have to revoke the unwanted permissions.
+        grantPermissions("android.permission.WRITE_EXTERNAL_STORAGE",
+                "android.permission.READ_EXTERNAL_STORAGE");
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        revokePermissions("android.permission.WRITE_EXTERNAL_STORAGE",
+                "android.permission.READ_EXTERNAL_STORAGE");
+    }
+
+    @Test
+    public void testCreateFilesInRandomPlaces_hasW() throws Exception {
+        revokePermissions("android.permission.READ_EXTERNAL_STORAGE");
+        executeShellCommand("mkdir -p /sdcard/Android/data/com.android.shell -m 2770");
+        runDeviceTest("testCreateFilesInRandomPlaces_hasW");
+    }
+
+    @Test
+    public void testMkdirInRandomPlaces_hasW() throws Exception {
+        revokePermissions("android.permission.READ_EXTERNAL_STORAGE");
+        executeShellCommand("mkdir -p /sdcard/Android/data/com.android.shell -m 2770");
+        runDeviceTest("testMkdirInRandomPlaces_hasW");
+    }
+
+    @Test
+    public void testReadOnlyExternalStorage_hasR() throws Exception {
+        revokePermissions("android.permission.WRITE_EXTERNAL_STORAGE");
+        createFileAsShell(SHELL_FILE);
+        try {
+            runDeviceTest("testReadOnlyExternalStorage_hasR");
+        } finally {
+            executeShellCommand("rm " + SHELL_FILE);
+        }
+    }
+
+    @Test
+    public void testCantAccessExternalStorage() throws Exception {
+        revokePermissions("android.permission.WRITE_EXTERNAL_STORAGE",
+                "android.permission.READ_EXTERNAL_STORAGE");
+        createFileAsShell(SHELL_FILE);
+        try {
+            runDeviceTest("testCantAccessExternalStorage");
+        } finally {
+            executeShellCommand("rm " + SHELL_FILE);
+        }
+    }
+
+    @Test
+    public void testListFiles_hasR() throws Exception {
+        revokePermissions("android.permission.WRITE_EXTERNAL_STORAGE");
+        createFileAsShell(SHELL_FILE);
+        try {
+            runDeviceTest("testListFiles_hasR");
+        } finally {
+            executeShellCommand("rm " + SHELL_FILE);
+        }
+    }
+
+    @Test
+    public void testCanRename_hasRW() throws Exception {
+        runDeviceTest("testCanRename_hasRW");
+    }
+
+    @Test
+    public void testCantRename_hasR() throws Exception {
+        revokePermissions("android.permission.WRITE_EXTERNAL_STORAGE");
+        createFileAsShell(SHELL_FILE);
+        try {
+            runDeviceTest("testCantRename_hasR");
+        } finally {
+            executeShellCommand("rm " + SHELL_FILE);
+        }
+    }
+
+    @Test
+    public void testCantRename_noStoragePermission() throws Exception {
+        revokePermissions("android.permission.WRITE_EXTERNAL_STORAGE",
+                "android.permission.READ_EXTERNAL_STORAGE");
+        createFileAsShell(SHELL_FILE);
+        try {
+            runDeviceTest("testCantRename_noStoragePermission");
+        } finally {
+            executeShellCommand("rm " + SHELL_FILE);
+        }
+    }
+
+    @Test
+    public void testRenameDirectoryAndUpdateDB_hasW() throws Exception {
+        runDeviceTest("testRenameDirectoryAndUpdateDB_hasW");
+    }
+
+    @Test
+    public void testCanDeleteAllFiles_hasRW() throws Exception {
+        runDeviceTest("testCanDeleteAllFiles_hasRW");
+    }
+
+    @Test
+    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/hostsidetests/scopedstorage/host/src/android/scopedstorage/cts/host/ScopedStorageHostTest.java b/hostsidetests/scopedstorage/host/src/android/scopedstorage/cts/host/ScopedStorageHostTest.java
new file mode 100644
index 0000000..9fdb746
--- /dev/null
+++ b/hostsidetests/scopedstorage/host/src/android/scopedstorage/cts/host/ScopedStorageHostTest.java
@@ -0,0 +1,354 @@
+/*
+ * 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.
+ */
+
+package android.scopedstorage.cts.host;
+
+import static org.junit.Assert.assertTrue;
+
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Runs the ScopedStorageTest tests.
+ */
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class ScopedStorageHostTest extends BaseHostJUnit4Test {
+    private boolean isExternalStorageSetup = false;
+
+    /**
+     * Runs the given phase of FilePathAccessTest by calling into the device.
+     * Throws an exception if the test phase fails.
+     */
+    private void runDeviceTest(String phase) throws Exception {
+        assertTrue(runDeviceTests("android.scopedstorage.cts",
+                "android.scopedstorage.cts.ScopedStorageTest", phase));
+    }
+
+    private String executeShellCommand(String cmd) throws Exception {
+        return getDevice().executeShellCommand(cmd);
+    }
+
+    private void setupExternalStorage() throws Exception {
+        if (!isExternalStorageSetup) {
+            runDeviceTest("setupExternalStorage");
+            isExternalStorageSetup = true;
+        }
+    }
+
+    @Before
+    public void setup() throws Exception {
+        setupExternalStorage();
+        executeShellCommand("mkdir /sdcard/Android/data/com.android.shell -m 2770");
+        executeShellCommand("mkdir /sdcard/Android/data/com.android.shell/files -m 2770");
+    }
+
+    @Before
+    public void revokeStoragePermissions() throws Exception {
+        revokePermissions("android.permission.WRITE_EXTERNAL_STORAGE",
+                "android.permission.READ_EXTERNAL_STORAGE");
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        executeShellCommand("rm -r /sdcard/Android/data/com.android.shell");
+    }
+
+    @Test
+    public void testTypePathConformity() throws Exception {
+        runDeviceTest("testTypePathConformity");
+    }
+
+    @Test
+    public void testCreateFileInAppExternalDir() throws Exception {
+        runDeviceTest("testCreateFileInAppExternalDir");
+    }
+
+    @Test
+    public void testCreateFileInOtherAppExternalDir() throws Exception {
+        runDeviceTest("testCreateFileInOtherAppExternalDir");
+    }
+
+    @Test
+    public void testContributeMediaFile() throws Exception {
+        runDeviceTest("testContributeMediaFile");
+    }
+
+    @Test
+    public void testCreateAndDeleteEmptyDir() throws Exception {
+        runDeviceTest("testCreateAndDeleteEmptyDir");
+    }
+
+    @Test
+    public void testCantDeleteOtherAppsContents() throws Exception {
+        runDeviceTest("testCantDeleteOtherAppsContents");
+    }
+
+    @Test
+    public void testOpendirRestrictions() throws Exception {
+        runDeviceTest("testOpendirRestrictions");
+    }
+
+    @Test
+    public void testLowLevelFileIO() throws Exception {
+        runDeviceTest("testLowLevelFileIO");
+    }
+
+    @Test
+    public void testListDirectoriesWithMediaFiles() throws Exception {
+        runDeviceTest("testListDirectoriesWithMediaFiles");
+    }
+
+    @Test
+    public void testListDirectoriesWithNonMediaFiles() throws Exception {
+        runDeviceTest("testListDirectoriesWithNonMediaFiles");
+    }
+
+    @Test
+    public void testListFilesFromExternalFilesDirectory() throws Exception {
+        runDeviceTest("testListFilesFromExternalFilesDirectory");
+    }
+
+    @Test
+    public void testListFilesFromExternalMediaDirectory() throws Exception {
+        runDeviceTest("testListFilesFromExternalMediaDirectory");
+    }
+
+    @Test
+    public void testListUnsupportedFileType() throws Exception {
+        runDeviceTest("testListUnsupportedFileType");
+    }
+
+    @Test
+    public void testMetaDataRedaction() throws Exception {
+        runDeviceTest("testMetaDataRedaction");
+    }
+
+    @Test
+    public void testVfsCacheConsistency() throws Exception {
+        runDeviceTest("testOpenFilePathFirstWriteContentResolver");
+        runDeviceTest("testOpenContentResolverFirstWriteContentResolver");
+        runDeviceTest("testOpenFilePathFirstWriteFilePath");
+        runDeviceTest("testOpenContentResolverFirstWriteFilePath");
+        runDeviceTest("testOpenContentResolverWriteOnly");
+        runDeviceTest("testOpenContentResolverDup");
+        runDeviceTest("testContentResolverDelete");
+        runDeviceTest("testContentResolverUpdate");
+        runDeviceTest("testOpenContentResolverClose");
+    }
+
+    @Test
+    public void testCaseInsensitivity() throws Exception {
+        runDeviceTest("testCreateLowerCaseDeleteUpperCase");
+        runDeviceTest("testCreateUpperCaseDeleteLowerCase");
+        runDeviceTest("testCreateMixedCaseDeleteDifferentMixedCase");
+    }
+
+    @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");
+    }
+
+    @Test
+    public void testRenameFileType() throws Exception {
+        runDeviceTest("testRenameFileType");
+    }
+
+    @Test
+    public void testRenameAndReplaceFile() throws Exception {
+        runDeviceTest("testRenameAndReplaceFile");
+    }
+
+    @Test
+    public void testRenameFileNotOwned() throws Exception {
+        runDeviceTest("testRenameFileNotOwned");
+    }
+
+    @Test
+    public void testRenameDirectory() throws Exception {
+        runDeviceTest("testRenameDirectory");
+    }
+
+    @Test
+    public void testRenameDirectoryNotOwned() throws Exception {
+        runDeviceTest("testRenameDirectoryNotOwned");
+    }
+
+    @Test
+    public void testRenameEmptyDirectory() throws Exception {
+        runDeviceTest("testRenameEmptyDirectory");
+    }
+
+    @Test
+    public void testSystemGalleryAppHasFullAccessToImages() throws Exception {
+        runDeviceTest("testSystemGalleryAppHasFullAccessToImages");
+    }
+
+    @Test
+    public void testSystemGalleryAppHasNoFullAccessToAudio() throws Exception {
+        runDeviceTest("testSystemGalleryAppHasNoFullAccessToAudio");
+    }
+
+    @Test
+    public void testSystemGalleryCanRenameImagesAndVideos() throws Exception {
+        runDeviceTest("testSystemGalleryCanRenameImagesAndVideos");
+    }
+
+    @Test
+    public void testManageExternalStorageCanCreateFilesAnywhere() throws Exception {
+        runDeviceTest("testManageExternalStorageCanCreateFilesAnywhere");
+    }
+
+    @Test
+    public void testManageExternalStorageCanDeleteOtherAppsContents() throws Exception {
+        runDeviceTest("testManageExternalStorageCanDeleteOtherAppsContents");
+    }
+
+    @Test
+    public void testManageExternalStorageReaddir() throws Exception {
+        runDeviceTest("testManageExternalStorageReaddir");
+    }
+
+    @Test
+    public void testManageExternalStorageCanRenameOtherAppsContents() throws Exception {
+        runDeviceTest("testManageExternalStorageCanRenameOtherAppsContents");
+    }
+
+    @Test
+    public void testCantAccessOtherAppsContents() throws Exception {
+        runDeviceTest("testCantAccessOtherAppsContents");
+    }
+
+    @Test
+    public void testCanCreateHiddenFile() throws Exception {
+        runDeviceTest("testCanCreateHiddenFile");
+    }
+
+    @Test
+    public void testCanRenameHiddenFile() throws Exception {
+        runDeviceTest("testCanRenameHiddenFile");
+    }
+
+    @Test
+    public void testHiddenDirectory() throws Exception {
+        runDeviceTest("testHiddenDirectory");
+    }
+
+    @Test
+    public void testHiddenDirectory_nomedia() throws Exception {
+        runDeviceTest("testHiddenDirectory_nomedia");
+    }
+
+    @Test
+    public void testListHiddenFile() throws Exception {
+        runDeviceTest("testListHiddenFile");
+    }
+
+    @Test
+    public void testCanCreateDefaultDirectory() throws Exception {
+        runDeviceTest("testCanCreateDefaultDirectory");
+    }
+
+    @Test
+    public void testManageExternalStorageQueryOtherAppsFile() throws Exception {
+        runDeviceTest("testManageExternalStorageQueryOtherAppsFile");
+    }
+
+    @Test
+    public void testSystemGalleryQueryOtherAppsFiles() throws Exception {
+        runDeviceTest("testSystemGalleryQueryOtherAppsFiles");
+    }
+
+    @Test
+    public void testQueryOtherAppsFiles() throws Exception {
+        runDeviceTest("testQueryOtherAppsFiles");
+    }
+
+    @Test
+    public void testSystemGalleryCanRenameImageAndVideoDirs() throws Exception {
+        runDeviceTest("testSystemGalleryCanRenameImageAndVideoDirs");
+    }
+
+    @Test
+    public void testCreateCanRestoreDeletedRowId() throws Exception {
+        runDeviceTest("testCreateCanRestoreDeletedRowId");
+    }
+
+    @Test
+    public void testRenameCanRestoreDeletedRowId() throws Exception {
+        runDeviceTest("testRenameCanRestoreDeletedRowId");
+    }
+
+    @Test
+    public void testCantCreateOrRenameFileWithInvalidName() throws Exception {
+        runDeviceTest("testCantCreateOrRenameFileWithInvalidName");
+    }
+
+    @Test
+    public void testAccess_file() throws Exception {
+        grantPermissions("android.permission.READ_EXTERNAL_STORAGE");
+        try {
+            runDeviceTest("testAccess_file");
+        } finally {
+            revokePermissions("android.permission.READ_EXTERNAL_STORAGE");
+        }
+    }
+
+    @Test
+    public void testAccess_directory() throws Exception {
+        grantPermissions("android.permission.WRITE_EXTERNAL_STORAGE",
+                "android.permission.READ_EXTERNAL_STORAGE");
+        try {
+            runDeviceTest("testAccess_directory");
+        } finally {
+            revokePermissions("android.permission.READ_EXTERNAL_STORAGE",
+                    "android.permission.READ_EXTERNAL_STORAGE");
+        }
+    }
+
+    private void grantPermissions(String... perms) throws Exception {
+        for (String perm : perms) {
+            executeShellCommand("pm grant android.scopedstorage.cts " + perm);
+        }
+    }
+
+    private void revokePermissions(String... perms) throws Exception {
+        for (String perm : perms) {
+            executeShellCommand("pm revoke android.scopedstorage.cts " + perm);
+        }
+    }
+}
diff --git a/hostsidetests/scopedstorage/legacy/AndroidManifest.xml b/hostsidetests/scopedstorage/legacy/AndroidManifest.xml
new file mode 100644
index 0000000..07fbfe8
--- /dev/null
+++ b/hostsidetests/scopedstorage/legacy/AndroidManifest.xml
@@ -0,0 +1,33 @@
+<?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="android.scopedstorage.cts.legacy" >
+
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
+    <application  android:requestLegacyExternalStorage="true" >
+        <receiver android:name="com.android.cts.install.lib.LocalIntentSender"
+                  android:exported="true" />
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="android.scopedstorage.cts.legacy"
+                     android:label="Tests for legacy storage under scoped storage world"/>
+
+</manifest>
diff --git a/hostsidetests/scopedstorage/legacy/src/android/scopedstorage/cts/legacy/LegacyStorageTest.java b/hostsidetests/scopedstorage/legacy/src/android/scopedstorage/cts/legacy/LegacyStorageTest.java
new file mode 100644
index 0000000..fedd206
--- /dev/null
+++ b/hostsidetests/scopedstorage/legacy/src/android/scopedstorage/cts/legacy/LegacyStorageTest.java
@@ -0,0 +1,664 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+
+package android.scopedstorage.cts.legacy;
+
+import static android.scopedstorage.cts.lib.TestUtils.BYTES_DATA1;
+import static android.scopedstorage.cts.lib.TestUtils.BYTES_DATA2;
+import static android.scopedstorage.cts.lib.TestUtils.DCIM_DIR;
+import static android.scopedstorage.cts.lib.TestUtils.EXTERNAL_STORAGE_DIR;
+import static android.scopedstorage.cts.lib.TestUtils.MOVIES_DIR;
+import static android.scopedstorage.cts.lib.TestUtils.STR_DATA1;
+import static android.scopedstorage.cts.lib.TestUtils.STR_DATA2;
+import static android.scopedstorage.cts.lib.TestUtils.assertCanRenameDirectory;
+import static android.scopedstorage.cts.lib.TestUtils.assertCanRenameFile;
+import static android.scopedstorage.cts.lib.TestUtils.assertCantRenameFile;
+import static android.scopedstorage.cts.lib.TestUtils.assertDirectoryContains;
+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.executeShellCommand;
+import static android.scopedstorage.cts.lib.TestUtils.getContentResolver;
+import static android.scopedstorage.cts.lib.TestUtils.getFileOwnerPackageFromDatabase;
+import static android.scopedstorage.cts.lib.TestUtils.getFileRowIdFromDatabase;
+import static android.scopedstorage.cts.lib.TestUtils.installApp;
+import static android.scopedstorage.cts.lib.TestUtils.listAs;
+import static android.scopedstorage.cts.lib.TestUtils.pollForExternalStorageState;
+import static android.scopedstorage.cts.lib.TestUtils.pollForPermission;
+import static android.scopedstorage.cts.lib.TestUtils.setupDefaultDirectories;
+import static android.scopedstorage.cts.lib.TestUtils.uninstallApp;
+
+import static com.google.common.truth.Truth.assertThat;
+
+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.provider.MediaStore;
+import android.scopedstorage.cts.lib.TestUtils;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+import android.util.Log;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.cts.install.lib.TestApp;
+
+import com.google.common.io.Files;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * Test app targeting Q and requesting legacy storage - tests legacy file path access.
+ * Designed to be run by LegacyAccessHostTest.
+ *
+ * <p> Test cases that assume we have WRITE_EXTERNAL_STORAGE only are appended with hasW,
+ * those that assume we have READ_EXTERNAL_STORAGE only are appended with hasR, those who assume we
+ * have both are appended with hasRW.
+ */
+@RunWith(AndroidJUnit4.class)
+public class LegacyStorageTest {
+    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";
+
+    private static final TestApp TEST_APP_A = new TestApp("TestAppA",
+            "android.scopedstorage.cts.testapp.A", 1, false, "CtsScopedStorageTestAppA.apk");
+
+    /**
+     * This method needs to be called once before running the whole test.
+     */
+    @Test
+    public void setupExternalStorage() {
+        setupDefaultDirectories();
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        pollForExternalStorageState();
+    }
+
+    /**
+     * Tests that legacy apps bypass the type-path conformity restrictions imposed by
+     * MediaProvider. <p> Assumes we have WRITE_EXTERNAL_STORAGE.
+     */
+    @Test
+    public void testCreateFilesInRandomPlaces_hasW() throws Exception {
+        pollForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, /*granted*/ false);
+        pollForPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, /*granted*/ true);
+        // Can create file under root dir
+        assertCanCreateFile(new File(EXTERNAL_STORAGE_DIR, "LegacyFileAccessTest.txt"));
+
+        // Can create music file under DCIM
+        assertCanCreateFile(new File(TestUtils.DCIM_DIR, "LegacyFileAccessTest.mp3"));
+
+        // Can create random file under external files dir
+        assertCanCreateFile(new File(InstrumentationRegistry.getContext().getExternalFilesDir(null),
+                "LegacyFileAccessTest"));
+
+        // However, even legacy apps can't create files under other app's directories
+        final File otherAppDir = new File(TestUtils.ANDROID_DATA_DIR, "com.android.shell");
+        final File file = new File(otherAppDir, "LegacyFileAccessTest.txt");
+
+        // otherAppDir was already created by the host test
+        try {
+            file.createNewFile();
+            fail("File creation expected to fail: " + file);
+        } catch (IOException expected) {
+        }
+    }
+
+    /**
+     * Tests that legacy apps bypass dir creation/deletion restrictions imposed by MediaProvider.
+     * <p> Assumes we have WRITE_EXTERNAL_STORAGE.
+     */
+    @Test
+    public void testMkdirInRandomPlaces_hasW() throws Exception {
+        pollForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, /*granted*/ false);
+        pollForPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, /*granted*/ true);
+        // Can create a top-level direcotry
+        final File topLevelDir = new File(EXTERNAL_STORAGE_DIR, "LegacyFileAccessTest");
+        assertCanCreateDir(topLevelDir);
+
+        final File otherAppDir = new File(TestUtils.ANDROID_DATA_DIR, "com.android.shell");
+
+        // However, even legacy apps can't create dirs under other app's directories
+        final File subDir = new File(otherAppDir, "LegacyFileAccessTest");
+        // otherAppDir was already created by the host test
+        assertThat(subDir.mkdir()).isFalse();
+
+        // Try to list a directory and fail because it requires READ permission
+        assertThat(TestUtils.MUSIC_DIR.list()).isNull();
+    }
+
+    /**
+     * Tests that an app can't access external storage without permissions.
+     */
+    @Test
+    public void testCantAccessExternalStorage() throws Exception {
+        pollForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, /*granted*/ false);
+        pollForPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, /*granted*/ false);
+        // Can't create file under root dir
+        final File newTxtFile = new File(EXTERNAL_STORAGE_DIR, "LegacyFileAccessTest.txt");
+        try {
+            newTxtFile.createNewFile();
+            fail("File creation expected to fail: " + newTxtFile);
+        } catch (IOException expected) {
+        }
+
+        // Can't create music file under /MUSIC
+        final File newMusicFile = new File(TestUtils.MUSIC_DIR, "LegacyFileAccessTest.mp3");
+        try {
+            newMusicFile.createNewFile();
+            fail("File creation expected to fail: " + newMusicFile);
+        } catch (IOException expected) {
+        }
+
+        // Can't create a top-level direcotry
+        final File topLevelDir = new File(EXTERNAL_STORAGE_DIR, "LegacyFileAccessTest");
+        assertThat(topLevelDir.mkdir()).isFalse();
+
+        // Can't read existing file
+        final File existingFile = new File(EXTERNAL_STORAGE_DIR, "LegacyAccessHostTest_shell");
+        try {
+            Os.open(existingFile.getPath(), OsConstants.O_RDONLY, /*mode*/ 0);
+            fail("Opening file for read expected to fail: " + existingFile);
+        } catch (ErrnoException expected) {
+        }
+
+        // Can't delete file
+        assertThat(existingFile.delete()).isFalse();
+
+        // try to list a directory and fail
+        assertThat(TestUtils.MUSIC_DIR.list()).isNull();
+        assertThat(EXTERNAL_STORAGE_DIR.list()).isNull();
+
+        // However, even without permissions, we can access our own external dir
+        final File fileInDataDir =
+                new File(InstrumentationRegistry.getContext().getExternalFilesDir(null),
+                        "LegacyFileAccessTest");
+        try {
+            assertThat(fileInDataDir.createNewFile()).isTrue();
+            assertThat(Arrays.asList(fileInDataDir.getParentFile().list()))
+                    .containsExactly("LegacyFileAccessTest");
+        } finally {
+            fileInDataDir.delete();
+        }
+
+        // we can access our own external media directory without permissions.
+        final File fileInMediaDir =
+                new File(InstrumentationRegistry.getContext().getExternalMediaDirs()[0],
+                        "LegacyFileAccessTest");
+        try {
+            assertThat(fileInMediaDir.createNewFile()).isTrue();
+            assertThat(Arrays.asList(fileInMediaDir.getParentFile().list()))
+                    .containsExactly("LegacyFileAccessTest");
+        } finally {
+            fileInMediaDir.delete();
+        }
+    }
+
+    // test read storage permission
+    @Test
+    public void testReadOnlyExternalStorage_hasR() throws Exception {
+        pollForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, /*granted*/ true);
+        pollForPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, /*granted*/ false);
+        // can list directory content
+        assertThat(TestUtils.MUSIC_DIR.list()).isNotNull();
+
+        // try to write a file and fail
+        final File existingFile = new File(EXTERNAL_STORAGE_DIR, "LegacyAccessHostTest_shell");
+
+        // can open file for read
+        FileDescriptor fd = null;
+        try {
+            fd = Os.open(existingFile.getPath(), OsConstants.O_RDONLY, /*mode*/ 0);
+        } finally {
+            if (fd != null) {
+                Os.close(fd);
+            }
+        }
+
+        try {
+            fd = Os.open(existingFile.getPath(), OsConstants.O_WRONLY, /*mode*/ 0);
+            Os.close(fd);
+            fail("Opening file for write expected to fail: " + existingFile);
+        } catch (ErrnoException expected) {
+        }
+
+        // try to create file and fail, because it requires WRITE
+        final File newFile = new File(TestUtils.MUSIC_DIR, "LegacyFileAccessTest.mp3");
+        try {
+            newFile.createNewFile();
+            fail("Creating file expected to fail: " + newFile);
+        } catch (IOException expected) {
+        }
+
+        // try to mkdir and fail, because it requires WRITE
+        final File newDir = new File(EXTERNAL_STORAGE_DIR, "LegacyFileAccessTest");
+        try {
+            assertThat(newDir.mkdir()).isFalse();
+        } finally {
+            newDir.delete();
+        }
+    }
+
+    /**
+     * Test that legacy app with storage permission can list all files
+     */
+    @Test
+    public void testListFiles_hasR() throws Exception {
+        pollForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, /*granted*/ true);
+        pollForPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, /*granted*/ false);
+
+        // can list a non-media file created by other package.
+        assertThat(Arrays.asList(EXTERNAL_STORAGE_DIR.list()))
+                .contains("LegacyAccessHostTest_shell");
+    }
+
+    /**
+     * Test that rename for legacy app with WRITE_EXTERNAL_STORAGE permission bypasses rename
+     * restrictions imposed by MediaProvider
+     */
+    @Test
+    public void testCanRename_hasRW() throws Exception {
+        pollForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, /*granted*/ true);
+        pollForPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, /*granted*/ true);
+
+        final File musicFile1 = new File(TestUtils.DCIM_DIR, "LegacyFileAccessTest.mp3");
+        final File musicFile2 = new File(EXTERNAL_STORAGE_DIR, "LegacyFileAccessTest.mp3");
+        final File musicFile3 = new File(TestUtils.MOVIES_DIR, "LegacyFileAccessTest.mp3");
+        final File nonMediaDir1 = new File(TestUtils.DCIM_DIR, "LegacyFileAccessTest");
+        final File nonMediaDir2 = new File(EXTERNAL_STORAGE_DIR, "LegacyFileAccessTest");
+        final File pdfFile1 = new File(nonMediaDir1, "LegacyFileAccessTest.pdf");
+        final File pdfFile2 = new File(nonMediaDir2, "LegacyFileAccessTest.pdf");
+        try {
+            // can rename a file to root directory.
+            assertThat(musicFile1.createNewFile()).isTrue();
+            assertCanRenameFile(musicFile1, musicFile2);
+
+            // can rename a music file to Movies directory.
+            assertCanRenameFile(musicFile2, musicFile3);
+
+            assertThat(nonMediaDir1.mkdir()).isTrue();
+            assertThat(pdfFile1.createNewFile()).isTrue();
+            // can rename directory to root directory.
+            assertCanRenameDirectory(
+                    nonMediaDir1, nonMediaDir2, new File[] {pdfFile1}, new File[] {pdfFile2});
+        } finally {
+            musicFile1.delete();
+            musicFile2.delete();
+            musicFile3.delete();
+
+            pdfFile1.delete();
+            pdfFile2.delete();
+            nonMediaDir1.delete();
+            nonMediaDir2.delete();
+        }
+    }
+
+    /**
+     * Test that legacy app with only READ_EXTERNAL_STORAGE can only rename files in app external
+     * directories.
+     */
+    @Test
+    public void testCantRename_hasR() throws Exception {
+        pollForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, /*granted*/ true);
+        pollForPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, /*granted*/ false);
+
+        final File shellFile1 = new File(EXTERNAL_STORAGE_DIR, "LegacyAccessHostTest_shell");
+        final File shellFile2 = new File(TestUtils.DOWNLOAD_DIR, "LegacyFileAccessTest_shell");
+        final File mediaFile1 =
+                new File(InstrumentationRegistry.getContext().getExternalMediaDirs()[0],
+                        "LegacyFileAccessTest1");
+        final File mediaFile2 =
+                new File(InstrumentationRegistry.getContext().getExternalMediaDirs()[0],
+                        "LegacyFileAccessTest2");
+        try {
+            // app can't rename shell file.
+            assertCantRenameFile(shellFile1, shellFile2);
+            // app can't move shell file to its media directory.
+            assertCantRenameFile(shellFile1, mediaFile1);
+            // However, even without permissions, app can rename files in its own external media
+            // directory.
+            assertThat(mediaFile1.createNewFile()).isTrue();
+            assertThat(mediaFile1.renameTo(mediaFile2)).isTrue();
+            assertThat(mediaFile2.exists()).isTrue();
+        } finally {
+            mediaFile1.delete();
+            mediaFile2.delete();
+        }
+    }
+
+    /**
+     * Test that legacy app with no storage permission can only rename files in app external
+     * directories.
+     */
+    @Test
+    public void testCantRename_noStoragePermission() throws Exception {
+        pollForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, /*granted*/ false);
+        pollForPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, /*granted*/ false);
+
+        final File shellFile1 = new File(EXTERNAL_STORAGE_DIR, "LegacyAccessHostTest_shell");
+        final File shellFile2 = new File(TestUtils.DOWNLOAD_DIR, "LegacyFileAccessTest_shell");
+        final File mediaFile1 =
+                new File(InstrumentationRegistry.getContext().getExternalMediaDirs()[0],
+                        "LegacyFileAccessTest1");
+        final File mediaFile2 =
+                new File(InstrumentationRegistry.getContext().getExternalMediaDirs()[0],
+                        "LegacyFileAccessTest2");
+        try {
+            // app can't rename shell file.
+            assertCantRenameFile(shellFile1, shellFile2);
+            // app can't move shell file to its media directory.
+            assertCantRenameFile(shellFile1, mediaFile1);
+            // However, even without permissions, app can rename files in its own external media
+            // directory.
+            assertThat(mediaFile1.createNewFile()).isTrue();
+            assertThat(mediaFile1.renameTo(mediaFile2)).isTrue();
+            assertThat(mediaFile2.exists()).isTrue();
+        } finally {
+            mediaFile1.delete();
+            mediaFile2.delete();
+        }
+    }
+
+    /**
+     * b/156046098, Test that MediaProvider doesn't throw UNIQUE constraint error while updating db
+     * rows corresponding to renamed directory.
+     */
+    @Test
+    public void testRenameDirectoryAndUpdateDB_hasW() throws Exception {
+        final String testDirectoryName = "LegacyFileAccessTestDirectory";
+        File directoryOldPath = new File(DCIM_DIR, testDirectoryName);
+        File directoryNewPath = new File(MOVIES_DIR, testDirectoryName);
+        try {
+            if (directoryOldPath.exists()) {
+                executeShellCommand("rm -r " + directoryOldPath.getPath());
+            }
+            assertThat(directoryOldPath.mkdirs()).isTrue();
+            assertCanRenameDirectory(directoryOldPath, directoryNewPath, null, null);
+
+            ContentValues values = new ContentValues();
+            values.put(MediaStore.MediaColumns.DATA, directoryNewPath.getPath());
+            // Verify that updating directoryOldPath to directoryNewPath doesn't throw
+            // UNIQUE constraint error.
+            getContentResolver().update(MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL),
+                    values, /*where*/ MediaStore.MediaColumns.DATA + "=?",
+                    /*whereArgs*/ new String[] {directoryOldPath.getPath()});
+        } finally {
+            directoryOldPath.delete();
+            directoryNewPath.delete();
+        }
+    }
+
+    /**
+     * Test that legacy app with WRITE_EXTERNAL_STORAGE can delete all files, and corresponding
+     * database entry is deleted on deleting the file.
+     */
+    @Test
+    public void testCanDeleteAllFiles_hasRW() throws Exception {
+        pollForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, /*granted*/ true);
+        pollForPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, /*granted*/ true);
+
+        final File videoFile = new File(EXTERNAL_STORAGE_DIR, VIDEO_FILE_NAME);
+        final File otherAppPdfFile = new File(TestUtils.DOWNLOAD_DIR, NONMEDIA_FILE_NAME);
+
+        try {
+            assertThat(videoFile.createNewFile()).isTrue();
+            assertDirectoryContains(videoFile.getParentFile(), videoFile);
+
+            assertThat(getFileRowIdFromDatabase(videoFile)).isNotEqualTo(-1);
+            // Legacy app can delete its own file.
+            assertThat(videoFile.delete()).isTrue();
+            // Deleting the file will remove videoFile entry from database.
+            assertThat(getFileRowIdFromDatabase(videoFile)).isEqualTo(-1);
+
+            installApp(TEST_APP_A, false);
+            assertThat(createFileAs(TEST_APP_A, otherAppPdfFile.getAbsolutePath())).isTrue();
+            assertThat(getFileRowIdFromDatabase(otherAppPdfFile)).isNotEqualTo(-1);
+            // Legacy app with write permission can delete the pdfFile owned by TestApp.
+            assertThat(otherAppPdfFile.delete()).isTrue();
+            // Deleting the pdfFile also removes pdfFile from database.
+            assertThat(getFileRowIdFromDatabase(otherAppPdfFile)).isEqualTo(-1);
+        } finally {
+            deleteFileAsNoThrow(TEST_APP_A, otherAppPdfFile.getAbsolutePath());
+            uninstallApp(TEST_APP_A);
+            videoFile.delete();
+        }
+    }
+
+    /**
+     * Test that file created by legacy app is inserted to MediaProvider database. And,
+     * MediaColumns.OWNER_PACKAGE_NAME is updated with calling package's name.
+     */
+    @Test
+    public void testLegacyAppCanOwnAFile_hasW() throws Exception {
+        pollForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, /*granted*/ true);
+        pollForPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, /*granted*/ true);
+
+        final File videoFile = new File(EXTERNAL_STORAGE_DIR, VIDEO_FILE_NAME);
+        try {
+            assertThat(videoFile.createNewFile()).isTrue();
+
+            installApp(TEST_APP_A, true);
+            // videoFile is inserted to database, non-legacy app can see this videoFile on 'ls'.
+            assertThat(listAs(TEST_APP_A, EXTERNAL_STORAGE_DIR.getAbsolutePath()))
+                    .contains(VIDEO_FILE_NAME);
+
+            // videoFile is in database, row ID for videoFile can not be -1.
+            assertNotEquals(-1, getFileRowIdFromDatabase(videoFile));
+            assertEquals(THIS_PACKAGE_NAME, getFileOwnerPackageFromDatabase(videoFile));
+
+            assertTrue(videoFile.delete());
+            // videoFile is removed from database on delete, hence row ID is -1.
+            assertEquals(-1, getFileRowIdFromDatabase(videoFile));
+        } finally {
+            videoFile.delete();
+            uninstallApp(TEST_APP_A);
+        }
+    }
+
+    @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 videoFile = new File(TestUtils.DCIM_DIR, VIDEO_FILE_NAME);
+        final File renamedVideoFile = new File(TestUtils.DCIM_DIR, "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();
+        }
+    }
+
+    /**
+     * 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 imageFile = new File(TestUtils.DCIM_DIR, IMAGE_FILE_NAME);
+        final File temporaryImageFile = new File(TestUtils.DCIM_DIR, 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();
+        }
+    }
+
+    /**
+     * 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 directoryNoMedia = new File(TestUtils.DCIM_DIR, ".directoryNoMedia");
+        final File imageInNoMediaDir = new File(directoryNoMedia, IMAGE_FILE_NAME);
+        final File renamedImageInDCIM = new File(TestUtils.DCIM_DIR, 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();
+            noMediaFile.delete();
+            directoryNoMedia.delete();
+        }
+    }
+
+    private static void assertCanCreateFile(File file) throws IOException {
+        if (file.exists()) {
+            file.delete();
+        }
+        try {
+            if (!file.createNewFile()) {
+                fail("Could not create file: " + file);
+            }
+        } finally {
+            file.delete();
+        }
+    }
+
+    private static void assertCanCreateDir(File dir) throws IOException {
+        if (dir.exists()) {
+            if (!dir.delete()) {
+                Log.w(TAG,
+                        "Can't create dir " + dir + " because it already exists and we can't "
+                                + "delete it!");
+                return;
+            }
+        }
+        try {
+            if (!dir.mkdir()) {
+                fail("Could not mkdir: " + dir);
+            }
+        } finally {
+            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/hostsidetests/scopedstorage/libs/ScopedStorageTestLib/Android.bp b/hostsidetests/scopedstorage/libs/ScopedStorageTestLib/Android.bp
new file mode 100644
index 0000000..3a5ba59
--- /dev/null
+++ b/hostsidetests/scopedstorage/libs/ScopedStorageTestLib/Android.bp
@@ -0,0 +1,20 @@
+// 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.
+
+java_library {
+    name: "cts-scopedstorage-lib",
+    srcs: ["src/**/*.java"],
+    static_libs: ["androidx.test.rules", "cts-install-lib"],
+    sdk_version: "test_current"
+}
diff --git a/hostsidetests/scopedstorage/libs/ScopedStorageTestLib/src/android/scopedstorage/cts/lib/RedactionTestHelper.java b/hostsidetests/scopedstorage/libs/ScopedStorageTestLib/src/android/scopedstorage/cts/lib/RedactionTestHelper.java
new file mode 100644
index 0000000..b7509e6
--- /dev/null
+++ b/hostsidetests/scopedstorage/libs/ScopedStorageTestLib/src/android/scopedstorage/cts/lib/RedactionTestHelper.java
@@ -0,0 +1,119 @@
+/**
+ * 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.
+ */
+
+package android.scopedstorage.cts.lib;
+
+import static androidx.test.InstrumentationRegistry.getContext;
+
+import static org.junit.Assert.fail;
+
+import android.media.ExifInterface;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RawRes;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Objects;
+
+/**
+ * Helper functions and utils for redactions tests
+ */
+public class RedactionTestHelper {
+    private static final String TAG = "RedactionTestHelper";
+
+    private static final String[] EXIF_GPS_TAGS = {
+            ExifInterface.TAG_GPS_ALTITUDE,
+            ExifInterface.TAG_GPS_DOP,
+            ExifInterface.TAG_GPS_DATESTAMP,
+            ExifInterface.TAG_GPS_LATITUDE,
+            ExifInterface.TAG_GPS_LATITUDE_REF,
+            ExifInterface.TAG_GPS_LONGITUDE,
+            ExifInterface.TAG_GPS_LONGITUDE_REF,
+            ExifInterface.TAG_GPS_PROCESSING_METHOD,
+            ExifInterface.TAG_GPS_TIMESTAMP,
+            ExifInterface.TAG_GPS_VERSION_ID,
+    };
+
+    public static final String EXIF_METADATA_QUERY = "android.scopedstorage.cts.exif";
+
+    /**
+     * Retrieve the EXIF metadata from the given file.
+     */
+    @NonNull
+    public static HashMap<String, String> getExifMetadata(@NonNull File file) throws IOException {
+        final ExifInterface exif = new ExifInterface(file);
+        return dumpExifGpsTagsToMap(exif);
+    }
+
+    /**
+     * Retrieve the EXIF metadata from the given resource.
+     */
+    @NonNull
+    public static HashMap<String, String> getExifMetadataFromRawResource(@RawRes int resId)
+            throws IOException {
+        final ExifInterface exif;
+        try (InputStream in = getContext().getResources().openRawResource(resId)) {
+            exif = new ExifInterface(in);
+        }
+        return dumpExifGpsTagsToMap(exif);
+    }
+
+    /**
+     * Asserts the 2 given EXIF maps have the same content.
+     */
+    public static void assertExifMetadataMatch(
+            @NonNull HashMap<String, String> actual, @NonNull HashMap<String, String> expected) {
+        for (String tag : EXIF_GPS_TAGS) {
+            assertMetadataEntryMatch(tag, actual.get(tag), expected.get(tag));
+        }
+    }
+
+    /**
+     * Asserts the 2 given EXIF maps don't have the same content.
+     */
+    public static void assertExifMetadataMismatch(
+            @NonNull HashMap<String, String> actual, @NonNull HashMap<String, String> expected) {
+        for (String tag : EXIF_GPS_TAGS) {
+            assertMetadataEntryMismatch(tag, actual.get(tag), expected.get(tag));
+        }
+    }
+
+    private static void assertMetadataEntryMatch(String tag, String actual, String expected) {
+        if (!Objects.equals(actual, expected)) {
+            fail("Unexpected metadata mismatch for tag: " + tag + "\n"
+                    + "expected:" + expected + "\n"
+                    + "but was: " + actual);
+        }
+    }
+
+    private static void assertMetadataEntryMismatch(String tag, String actual, String expected) {
+        if (Objects.equals(actual, expected)) {
+            fail("Unexpected metadata match for tag: " + tag + "\n"
+                    + "expected not to be:" + expected);
+        }
+    }
+
+    private static HashMap<String, String> dumpExifGpsTagsToMap(ExifInterface exif) {
+        final HashMap<String, String> res = new HashMap<>();
+        for (String tag : EXIF_GPS_TAGS) {
+            res.put(tag, exif.getAttribute(tag));
+        }
+        return res;
+    }
+}
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
new file mode 100644
index 0000000..ce546e4
--- /dev/null
+++ b/hostsidetests/scopedstorage/libs/ScopedStorageTestLib/src/android/scopedstorage/cts/lib/TestUtils.java
@@ -0,0 +1,851 @@
+/**
+ * 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.
+ */
+
+package android.scopedstorage.cts.lib;
+
+import static android.scopedstorage.cts.lib.RedactionTestHelper.EXIF_METADATA_QUERY;
+
+import static androidx.test.InstrumentationRegistry.getContext;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.fail;
+
+import android.Manifest;
+import android.app.ActivityManager;
+import android.app.AppOpsManager;
+import android.app.UiAutomation;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Environment;
+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;
+import androidx.annotation.Nullable;
+import androidx.test.InstrumentationRegistry;
+
+import com.android.cts.install.lib.Install;
+import com.android.cts.install.lib.InstallUtils;
+import com.android.cts.install.lib.TestApp;
+import com.android.cts.install.lib.Uninstall;
+
+import com.google.common.io.ByteStreams;
+
+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;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * General helper functions for ScopedStorageTest tests.
+ */
+public class TestUtils {
+    static final String TAG = "ScopedStorageTest";
+
+    public static final String QUERY_TYPE = "android.scopedstorage.cts.queryType";
+    public static final String INTENT_EXTRA_PATH = "android.scopedstorage.cts.path";
+    public static final String INTENT_EXCEPTION = "android.scopedstorage.cts.exception";
+    public static final String CREATE_FILE_QUERY = "android.scopedstorage.cts.createfile";
+    public static final String DELETE_FILE_QUERY = "android.scopedstorage.cts.deletefile";
+    public static final String OPEN_FILE_FOR_READ_QUERY = "android.scopedstorage.cts.openfile_read";
+    public static final String OPEN_FILE_FOR_WRITE_QUERY =
+            "android.scopedstorage.cts.openfile_write";
+    public static final String CAN_READ_WRITE_QUERY =
+            "android.scopedstorage.cts.can_read_and_write";
+    public static final String READDIR_QUERY = "android.scopedstorage.cts.readdir";
+
+    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();
+
+    // Root of external storage
+    public static final File EXTERNAL_STORAGE_DIR = Environment.getExternalStorageDirectory();
+    // Default top-level directories
+    public static final File ALARMS_DIR =
+            new File(EXTERNAL_STORAGE_DIR, Environment.DIRECTORY_ALARMS);
+    public static final File ANDROID_DIR = new File(EXTERNAL_STORAGE_DIR, "Android");
+    public static final File AUDIOBOOKS_DIR =
+            new File(EXTERNAL_STORAGE_DIR, Environment.DIRECTORY_AUDIOBOOKS);
+    public static final File DCIM_DIR = new File(EXTERNAL_STORAGE_DIR, Environment.DIRECTORY_DCIM);
+    public static final File DOCUMENTS_DIR =
+            new File(EXTERNAL_STORAGE_DIR, Environment.DIRECTORY_DOCUMENTS);
+    public static final File DOWNLOAD_DIR =
+            new File(EXTERNAL_STORAGE_DIR, Environment.DIRECTORY_DOWNLOADS);
+    public static final File MUSIC_DIR =
+            new File(EXTERNAL_STORAGE_DIR, Environment.DIRECTORY_MUSIC);
+    public static final File MOVIES_DIR =
+            new File(EXTERNAL_STORAGE_DIR, Environment.DIRECTORY_MOVIES);
+    public static final File NOTIFICATIONS_DIR =
+            new File(EXTERNAL_STORAGE_DIR, Environment.DIRECTORY_NOTIFICATIONS);
+    public static final File PICTURES_DIR =
+            new File(EXTERNAL_STORAGE_DIR, Environment.DIRECTORY_PICTURES);
+    public static final File PODCASTS_DIR =
+            new File(EXTERNAL_STORAGE_DIR, Environment.DIRECTORY_PODCASTS);
+    public static final File RINGTONES_DIR =
+            new File(EXTERNAL_STORAGE_DIR, Environment.DIRECTORY_RINGTONES);
+
+    public static final File[] DEFAULT_TOP_LEVEL_DIRS = new File[] {ALARMS_DIR, ANDROID_DIR,
+            AUDIOBOOKS_DIR, DCIM_DIR, DOCUMENTS_DIR, DOWNLOAD_DIR, MUSIC_DIR, MOVIES_DIR,
+            NOTIFICATIONS_DIR, PICTURES_DIR, PODCASTS_DIR, RINGTONES_DIR};
+
+    public static final File ANDROID_DATA_DIR = new File(ANDROID_DIR, "data");
+    public static final File ANDROID_MEDIA_DIR = new File(ANDROID_DIR, "media");
+
+    private static final long POLLING_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(10);
+    private static final long POLLING_SLEEP_MILLIS = 100;
+
+    /**
+     * Creates the top level default directories.
+     *
+     * <p>Those are usually created by MediaProvider, but some naughty tests might delete them
+     * and not restore them afterwards. so we make sure we create them before we make any
+     * assumptions about their existence.
+     */
+    public static void setupDefaultDirectories() {
+        for (File dir : DEFAULT_TOP_LEVEL_DIRS) {
+            dir.mkdir();
+        }
+    }
+
+    /**
+     * Grants {@link Manifest.permission#GRANT_RUNTIME_PERMISSIONS} to the given package.
+     */
+    public static void grantPermission(String packageName, String permission) {
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        uiAutomation.adoptShellPermissionIdentity("android.permission.GRANT_RUNTIME_PERMISSIONS");
+        try {
+            uiAutomation.grantRuntimePermission(packageName, permission);
+            // Wait for OP_READ_EXTERNAL_STORAGE to get updated.
+            SystemClock.sleep(1000);
+        } finally {
+            uiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+    /**
+     * Revokes permissions from the given package.
+     */
+    public static void revokePermission(String packageName, String permission) {
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        uiAutomation.adoptShellPermissionIdentity("android.permission.REVOKE_RUNTIME_PERMISSIONS");
+        try {
+            uiAutomation.revokeRuntimePermission(packageName, permission);
+        } finally {
+            uiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+    /**
+     * Adopts shell permission identity for the given permissions.
+     */
+    public static void adoptShellPermissionIdentity(String... permissions) {
+        InstrumentationRegistry.getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
+                permissions);
+    }
+
+    /**
+     * Drops shell permission identity for all permissions.
+     */
+    public static void dropShellPermissionIdentity() {
+        InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                .dropShellPermissionIdentity();
+    }
+
+    /**
+     * Executes a shell command.
+     */
+    public static String executeShellCommand(String cmd) throws Exception {
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        try (FileInputStream output = new FileInputStream(
+                     uiAutomation.executeShellCommand(cmd).getFileDescriptor())) {
+            return new String(ByteStreams.toByteArray(output));
+        }
+    }
+
+    /**
+     * Makes the given {@code testApp} list the content of the given directory and returns the
+     * result as an {@link ArrayList}
+     */
+    public static ArrayList<String> listAs(TestApp testApp, String dirPath) throws Exception {
+        return getContentsFromTestApp(testApp, dirPath, READDIR_QUERY);
+    }
+
+    /**
+     * Returns {@code true} iff the given {@code path} exists and is readable and
+     * writable for for {@code testApp}.
+     */
+    public static boolean canReadAndWriteAs(TestApp testApp, String path) throws Exception {
+        return getResultFromTestApp(testApp, path, CAN_READ_WRITE_QUERY);
+    }
+
+    /**
+     * Makes the given {@code testApp} read the EXIF metadata from the given file and returns the
+     * result as an {@link HashMap}
+     */
+    public static HashMap<String, String> readExifMetadataFromTestApp(
+            TestApp testApp, String filePath) throws Exception {
+        HashMap<String, String> res =
+                getMetadataFromTestApp(testApp, filePath, EXIF_METADATA_QUERY);
+        return res;
+    }
+
+    /**
+     * Makes the given {@code testApp} create a file.
+     *
+     * <p>This method drops shell permission identity.
+     */
+    public static boolean createFileAs(TestApp testApp, String path) throws Exception {
+        return getResultFromTestApp(testApp, path, CREATE_FILE_QUERY);
+    }
+
+    /**
+     * Makes the given {@code testApp} delete a file.
+     *
+     * <p>This method drops shell permission identity.
+     */
+    public static boolean deleteFileAs(TestApp testApp, String path) throws Exception {
+        return getResultFromTestApp(testApp, path, DELETE_FILE_QUERY);
+    }
+
+    /**
+     * Makes the given {@code testApp} delete a file. Doesn't throw in case of failure.
+     */
+    public static boolean deleteFileAsNoThrow(TestApp testApp, String path) {
+        try {
+            return deleteFileAs(testApp, path);
+        } catch (Exception e) {
+            Log.e(TAG,
+                    "Error occurred while deleting file: " + path + " on behalf of app: " + testApp,
+                    e);
+            return false;
+        }
+    }
+
+    /**
+     * 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} without storage permissions.
+     */
+    public static void installApp(TestApp testApp) throws Exception {
+        installApp(testApp, /* grantStoragePermission */ false);
+    }
+
+    /**
+     * Installs a {@link TestApp} with storage permissions.
+     */
+    public static void installAppWithStoragePermissions(TestApp testApp) throws Exception {
+        installApp(testApp, /* grantStoragePermission */ true);
+    }
+
+    /**
+     * 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();
+            uiAutomation.adoptShellPermissionIdentity(
+                    Manifest.permission.INSTALL_PACKAGES, Manifest.permission.DELETE_PACKAGES);
+            if (InstallUtils.getInstalledVersion(packageName) != -1) {
+                Uninstall.packages(packageName);
+            }
+            Install.single(testApp).commit();
+            assertThat(InstallUtils.getInstalledVersion(packageName)).isEqualTo(1);
+            if (grantStoragePermission) {
+                grantPermission(packageName, Manifest.permission.READ_EXTERNAL_STORAGE);
+            }
+        } finally {
+            uiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+    /**
+     * Uninstalls a {@link TestApp}.
+     */
+    public static void uninstallApp(TestApp testApp) throws Exception {
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        try {
+            final String packageName = testApp.getPackageName();
+            uiAutomation.adoptShellPermissionIdentity(Manifest.permission.DELETE_PACKAGES);
+
+            Uninstall.packages(packageName);
+            assertThat(InstallUtils.getInstalledVersion(packageName)).isEqualTo(-1);
+        } finally {
+            uiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+    /**
+     * Uninstalls a {@link TestApp}. Doesn't throw in case of failure.
+     */
+    public static void uninstallAppNoThrow(TestApp testApp) {
+        try {
+            uninstallApp(testApp);
+        } catch (Exception e) {
+            Log.e(TAG, "Exception occurred while uninstalling app: " + testApp, e);
+        }
+    }
+
+    public static ContentResolver getContentResolver() {
+        return getContext().getContentResolver();
+    }
+
+    /**
+     * Queries {@link ContentResolver} for a file and returns the corresponding {@link Uri} for its
+     * entry in the database. Returns {@code null} if file doesn't exist in the database.
+     */
+    @Nullable
+    public static Uri getFileUri(@NonNull File file) {
+        final Uri contentUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL);
+        final int id = getFileRowIdFromDatabase(file);
+        return id == -1 ? null : ContentUris.withAppendedId(contentUri, id);
+    }
+
+    /**
+     * Queries {@link ContentResolver} for a file and returns the corresponding row ID for its
+     * entry in the database.
+     */
+    public static int getFileRowIdFromDatabase(@NonNull File file) {
+        int id = -1;
+        try (Cursor c = queryFile(file, MediaStore.MediaColumns._ID)) {
+            if (c.moveToFirst()) {
+                id = c.getInt(0);
+            }
+        }
+        return id;
+    }
+
+    /**
+     * Queries {@link ContentResolver} for a file and returns the corresponding owner package name
+     * for its entry in the database.
+     */
+    @Nullable
+    public static String getFileOwnerPackageFromDatabase(@NonNull File file) {
+        String ownerPackage = null;
+        try (Cursor c = queryFile(file, MediaStore.MediaColumns.OWNER_PACKAGE_NAME)) {
+            if (c.moveToFirst()) {
+                ownerPackage = c.getString(0);
+            }
+        }
+        return ownerPackage;
+    }
+
+    /**
+     * Queries {@link ContentResolver} for a file and returns a {@link Cursor} with the given
+     * columns.
+     */
+    @NonNull
+    public static Cursor queryImageFile(File file, String... projection) {
+        return queryFile(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, file, projection);
+    }
+
+    /**
+     * Queries {@link ContentResolver} for a file and returns the corresponding mime type for its
+     * entry in the database.
+     */
+    @NonNull
+    public static String getFileMimeTypeFromDatabase(@NonNull File file) {
+        String mimeType = "";
+        try (Cursor c = queryFile(file, MediaStore.MediaColumns.MIME_TYPE)) {
+            if (c.moveToFirst()) {
+                mimeType = c.getString(0);
+            }
+        }
+        return mimeType;
+    }
+
+    /**
+     * Sets {@link AppOpsManager#MODE_ALLOWED} for the given {@code ops} and the given {@code uid}.
+     *
+     * <p>This method drops shell permission identity.
+     */
+    public static void allowAppOpsToUid(int uid, @NonNull String... ops) {
+        setAppOpsModeForUid(uid, AppOpsManager.MODE_ALLOWED, ops);
+    }
+
+    /**
+     * Sets {@link AppOpsManager#MODE_ERRORED} for the given {@code ops} and the given {@code uid}.
+     *
+     * <p>This method drops shell permission identity.
+     */
+    public static void denyAppOpsToUid(int uid, @NonNull String... ops) {
+        setAppOpsModeForUid(uid, AppOpsManager.MODE_ERRORED, ops);
+    }
+
+    /**
+     * Deletes the given file through {@link ContentResolver} and {@link MediaStore} APIs,
+     * and asserts that the file was successfully deleted from the database.
+     */
+    public static void deleteWithMediaProvider(@NonNull File file) {
+        assertThat(getContentResolver().delete(
+                           MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL),
+                           /*where*/ MediaStore.MediaColumns.DATA + " = ?",
+                           /*selectionArgs*/ new String[] {file.getPath()}))
+                .isEqualTo(1);
+    }
+
+    /**
+     * Renames the given file through {@link ContentResolver} and {@link MediaStore} APIs,
+     * and asserts that the file was updated in the database.
+     */
+    public static void updateDisplayNameWithMediaProvider(
+            String relativePath, String oldDisplayName, String newDisplayName) {
+        String selection = MediaStore.MediaColumns.RELATIVE_PATH + " = ? AND "
+                + MediaStore.MediaColumns.DISPLAY_NAME + " = ?";
+        String[] selectionArgs = {relativePath + '/', oldDisplayName};
+        String[] projection = {MediaStore.MediaColumns._ID, MediaStore.MediaColumns.DATA};
+
+        ContentValues values = new ContentValues();
+        values.put(MediaStore.MediaColumns.DISPLAY_NAME, newDisplayName);
+
+        try (Cursor cursor =
+                        getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+                                projection, selection, selectionArgs, null)) {
+            assertThat(cursor.getCount()).isEqualTo(1);
+            cursor.moveToFirst();
+            int id = cursor.getInt(cursor.getColumnIndex(MediaStore.MediaColumns._ID));
+            String data = cursor.getString(cursor.getColumnIndex(MediaStore.MediaColumns.DATA));
+            Uri uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);
+            Log.i(TAG, "Uri: " + uri + ". Data: " + data);
+            assertThat(getContentResolver().update(uri, values, selection, selectionArgs))
+                    .isEqualTo(1);
+        }
+    }
+
+    /**
+     * Opens the given file through {@link ContentResolver} and {@link MediaStore} APIs.
+     */
+    @NonNull
+    public static ParcelFileDescriptor openWithMediaProvider(@NonNull File file, String mode)
+            throws Exception {
+        final Uri fileUri = getFileUri(file);
+        assertThat(fileUri).isNotNull();
+        Log.i(TAG, "Uri: " + fileUri + ". Data: " + file.getPath());
+        ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(fileUri, mode);
+        assertThat(pfd).isNotNull();
+        return pfd;
+    }
+
+    /**
+     * Asserts the given operation throws an exception of type {@code T}.
+     */
+    public static <T extends Exception> void assertThrows(Class<T> clazz, Operation<Exception> r)
+            throws Exception {
+        assertThrows(clazz, "", r);
+    }
+
+    /**
+     * Asserts the given operation throws an exception of type {@code T}.
+     */
+    public static <T extends Exception> void assertThrows(
+            Class<T> clazz, String errMsg, Operation<Exception> r) throws Exception {
+        try {
+            r.run();
+            fail("Expected " + clazz + " to be thrown");
+        } catch (Exception e) {
+            if (!clazz.isAssignableFrom(e.getClass()) || !e.getMessage().contains(errMsg)) {
+                Log.e(TAG, "Expected " + clazz + " exception with error message: " + errMsg, e);
+                throw e;
+            }
+        }
+    }
+
+    /**
+     * A functional interface representing an operation that takes no arguments,
+     * returns no arguments and might throw an {@link Exception} of any kind.
+     *
+     * @param T the subclass of {@link java.lang.Exception} that this operation might throw.
+     */
+    @FunctionalInterface
+    public interface Operation<T extends Exception> {
+        /**
+         * This is the method that gets called for any object that implements this interface.
+         */
+        void run() throws T;
+    }
+
+    /**
+     * Deletes the given file. If the file is a directory, then deletes all of its children (files
+     * or directories) recursively.
+     */
+    public static boolean deleteRecursively(@NonNull File path) {
+        if (path.isDirectory()) {
+            for (File child : path.listFiles()) {
+                if (!deleteRecursively(child)) {
+                    return false;
+                }
+            }
+        }
+        return path.delete();
+    }
+
+    /**
+     * Asserts can rename file.
+     */
+    public static void assertCanRenameFile(File oldFile, File newFile) {
+        assertThat(oldFile.renameTo(newFile)).isTrue();
+        assertThat(oldFile.exists()).isFalse();
+        assertThat(newFile.exists()).isTrue();
+        assertThat(getFileRowIdFromDatabase(oldFile)).isEqualTo(-1);
+        assertThat(getFileRowIdFromDatabase(newFile)).isNotEqualTo(-1);
+    }
+
+    /**
+     * Asserts cannot rename file.
+     */
+    public static void assertCantRenameFile(File oldFile, File newFile) {
+        final int rowId = getFileRowIdFromDatabase(oldFile);
+        assertThat(oldFile.renameTo(newFile)).isFalse();
+        assertThat(oldFile.exists()).isTrue();
+        assertThat(getFileRowIdFromDatabase(oldFile)).isEqualTo(rowId);
+    }
+
+    /**
+     * Asserts can rename directory.
+     */
+    public static void assertCanRenameDirectory(File oldDirectory, File newDirectory,
+            @Nullable File[] oldFilesList, @Nullable File[] newFilesList) {
+        assertThat(oldDirectory.renameTo(newDirectory)).isTrue();
+        assertThat(oldDirectory.exists()).isFalse();
+        assertThat(newDirectory.exists()).isTrue();
+        for (File file : oldFilesList != null ? oldFilesList : new File[0]) {
+            assertThat(file.exists()).isFalse();
+            assertThat(getFileRowIdFromDatabase(file)).isEqualTo(-1);
+        }
+        for (File file : newFilesList != null ? newFilesList : new File[0]) {
+            assertThat(file.exists()).isTrue();
+            assertThat(getFileRowIdFromDatabase(file)).isNotEqualTo(-1);
+        }
+    }
+
+    /**
+     * Asserts cannot rename directory.
+     */
+    public static void assertCantRenameDirectory(
+            File oldDirectory, File newDirectory, @Nullable File[] oldFilesList) {
+        assertThat(oldDirectory.renameTo(newDirectory)).isFalse();
+        assertThat(oldDirectory.exists()).isTrue();
+        for (File file : oldFilesList != null ? oldFilesList : new File[0]) {
+            assertThat(file.exists()).isTrue();
+            assertThat(getFileRowIdFromDatabase(file)).isNotEqualTo(-1);
+        }
+    }
+
+    /**
+     * Returns whether we can open the file.
+     */
+    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;
+            }
+        }
+    }
+
+    /**
+     * Polls for external storage to be mounted.
+     */
+    public static void pollForExternalStorageState() throws Exception {
+        for (int i = 0; i < POLLING_TIMEOUT_MILLIS / POLLING_SLEEP_MILLIS; i++) {
+            if (Environment.getExternalStorageState(Environment.getExternalStorageDirectory())
+                            .equals(Environment.MEDIA_MOUNTED)) {
+                return;
+            }
+            Thread.sleep(POLLING_SLEEP_MILLIS);
+        }
+        fail("Timed out while waiting for ExternalStorageState to be MEDIA_MOUNTED");
+    }
+
+    /**
+     * Polls until we're granted or denied a given permission.
+     */
+    public static void pollForPermission(String perm, boolean granted) throws Exception {
+        for (int i = 0; i < POLLING_TIMEOUT_MILLIS / POLLING_SLEEP_MILLIS; i++) {
+            if (granted == checkPermissionAndAppOp(perm)) {
+                return;
+            }
+            Thread.sleep(POLLING_SLEEP_MILLIS);
+        }
+        fail("Timed out while waiting for permission " + perm + " to be "
+                + (granted ? "granted" : "revoked"));
+    }
+
+    /**
+     * Asserts the entire content of the file equals exactly {@code expectedContent}.
+     */
+    public static void assertFileContent(File file, byte[] expectedContent) throws IOException {
+        try (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 (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) {
+        final int pid = Os.getpid();
+        final int uid = Os.getuid();
+        final Context context = getContext();
+        final String packageName = context.getPackageName();
+        if (context.checkPermission(permission, pid, uid) != PackageManager.PERMISSION_GRANTED) {
+            return false;
+        }
+
+        final String op = AppOpsManager.permissionToOp(permission);
+        // No AppOp associated with the given permission, skip AppOp check.
+        if (op == null) {
+            return true;
+        }
+
+        final AppOpsManager appOps = context.getSystemService(AppOpsManager.class);
+        try {
+            appOps.checkPackage(uid, packageName);
+        } catch (SecurityException e) {
+            return false;
+        }
+
+        return appOps.unsafeCheckOpNoThrow(op, uid, packageName) == AppOpsManager.MODE_ALLOWED;
+    }
+
+    /**
+     * <p>This method drops shell permission identity.
+     */
+    private static void forceStopApp(String packageName) throws Exception {
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        try {
+            uiAutomation.adoptShellPermissionIdentity(Manifest.permission.FORCE_STOP_PACKAGES);
+
+            getContext().getSystemService(ActivityManager.class).forceStopPackage(packageName);
+            Thread.sleep(1000);
+        } finally {
+            uiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+    /**
+     * <p>This method drops shell permission identity.
+     */
+    private static void sendIntentToTestApp(TestApp testApp, String dirPath, String actionName,
+            BroadcastReceiver broadcastReceiver, CountDownLatch latch) throws Exception {
+        final String packageName = testApp.getPackageName();
+        forceStopApp(packageName);
+        // Register broadcast receiver
+        final IntentFilter intentFilter = new IntentFilter();
+        intentFilter.addAction(actionName);
+        intentFilter.addCategory(Intent.CATEGORY_DEFAULT);
+        getContext().registerReceiver(broadcastReceiver, intentFilter);
+
+        // Launch the test app.
+        final Intent intent = new Intent(Intent.ACTION_MAIN);
+        intent.setPackage(packageName);
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        intent.putExtra(QUERY_TYPE, actionName);
+        intent.putExtra(INTENT_EXTRA_PATH, dirPath);
+        intent.addCategory(Intent.CATEGORY_LAUNCHER);
+        getContext().startActivity(intent);
+        latch.await();
+        getContext().unregisterReceiver(broadcastReceiver);
+    }
+
+    /**
+     * Gets images/video metadata from a test app.
+     *
+     * <p>This method drops shell permission identity.
+     */
+    private static HashMap<String, String> getMetadataFromTestApp(
+            TestApp testApp, String dirPath, String actionName) throws Exception {
+        final CountDownLatch latch = new CountDownLatch(1);
+        final HashMap<String, String> appOutputList = new HashMap<>();
+        final Exception[] exception = new Exception[1];
+        exception[0] = null;
+        final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                if (intent.hasExtra(INTENT_EXCEPTION)) {
+                    exception[0] = (Exception) (intent.getExtras().get(INTENT_EXCEPTION));
+                } else if (intent.hasExtra(actionName)) {
+                    HashMap<String, String> res =
+                            (HashMap<String, String>) intent.getExtras().get(actionName);
+                    appOutputList.putAll(res);
+                }
+                latch.countDown();
+            }
+        };
+        sendIntentToTestApp(testApp, dirPath, actionName, broadcastReceiver, latch);
+        if (exception[0] != null) {
+            throw exception[0];
+        }
+        return appOutputList;
+    }
+
+    /**
+     * <p>This method drops shell permission identity.
+     */
+    private static ArrayList<String> getContentsFromTestApp(
+            TestApp testApp, String dirPath, String actionName) throws Exception {
+        final CountDownLatch latch = new CountDownLatch(1);
+        final ArrayList<String> appOutputList = new ArrayList<String>();
+        final Exception[] exception = new Exception[1];
+        exception[0] = null;
+        final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                if (intent.hasExtra(INTENT_EXCEPTION)) {
+                    exception[0] = (Exception) (intent.getSerializableExtra(INTENT_EXCEPTION));
+                } else if (intent.hasExtra(actionName)) {
+                    appOutputList.addAll(intent.getStringArrayListExtra(actionName));
+                }
+                latch.countDown();
+            }
+        };
+
+        sendIntentToTestApp(testApp, dirPath, actionName, broadcastReceiver, latch);
+        if (exception[0] != null) {
+            throw exception[0];
+        }
+        return appOutputList;
+    }
+
+    /**
+     * <p>This method drops shell permission identity.
+     */
+    private static boolean getResultFromTestApp(TestApp testApp, String dirPath, String actionName)
+            throws Exception {
+        final CountDownLatch latch = new CountDownLatch(1);
+        final boolean[] appOutput = new boolean[1];
+        final Exception[] exception = new Exception[1];
+        exception[0] = null;
+        BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                if (intent.hasExtra(INTENT_EXCEPTION)) {
+                    exception[0] = (Exception) (intent.getSerializableExtra(INTENT_EXCEPTION));
+                } else if (intent.hasExtra(actionName)) {
+                    appOutput[0] = intent.getBooleanExtra(actionName, false);
+                }
+                latch.countDown();
+            }
+        };
+
+        sendIntentToTestApp(testApp, dirPath, actionName, broadcastReceiver, latch);
+        if (exception[0] != null) {
+            throw exception[0];
+        }
+        return appOutput[0];
+    }
+
+    /**
+     * Sets {@code mode} for the given {@code ops} and the given {@code uid}.
+     *
+     * <p>This method drops shell permission identity.
+     */
+    private static void setAppOpsModeForUid(int uid, int mode, @NonNull String... ops) {
+        adoptShellPermissionIdentity(null);
+        try {
+            for (String op : ops) {
+                getContext().getSystemService(AppOpsManager.class).setUidMode(op, uid, mode);
+            }
+        } finally {
+            dropShellPermissionIdentity();
+        }
+    }
+
+    @NonNull
+    private static Cursor queryFile(@NonNull File file, String... projection) {
+        return queryFile(
+                MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL), file, projection);
+    }
+
+    @NonNull
+    private static Cursor queryFile(@NonNull Uri uri, @NonNull File file, String... projection) {
+        final Cursor c = getContentResolver().query(uri, projection,
+                /*selection*/ MediaStore.MediaColumns.DATA + " = ?",
+                /*selectionArgs*/ new String[] {file.getAbsolutePath()},
+                /*sortOrder*/ null);
+        assertThat(c).isNotNull();
+        return c;
+    }
+
+    /**
+     * Asserts that {@code dir} is a directory and that it contains all of {@code expectedContent}
+     */
+    public static void assertDirectoryContains(@NonNull File dir, File... expectedContent) {
+        assertThat(dir.isDirectory()).isTrue();
+        final List<File> actualContent = Arrays.asList(dir.listFiles());
+        for (File f : expectedContent) {
+            assertThat(actualContent).contains(f);
+        }
+    }
+}
diff --git a/hostsidetests/scopedstorage/res/raw/img_with_metadata.jpg b/hostsidetests/scopedstorage/res/raw/img_with_metadata.jpg
new file mode 100644
index 0000000..c9063f8
--- /dev/null
+++ b/hostsidetests/scopedstorage/res/raw/img_with_metadata.jpg
Binary files differ
diff --git a/hostsidetests/scopedstorage/src/android/scopedstorage/cts/ScopedStorageTest.java b/hostsidetests/scopedstorage/src/android/scopedstorage/cts/ScopedStorageTest.java
new file mode 100644
index 0000000..2867aec
--- /dev/null
+++ b/hostsidetests/scopedstorage/src/android/scopedstorage/cts/ScopedStorageTest.java
@@ -0,0 +1,2246 @@
+/*
+ * 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.
+ */
+
+package android.scopedstorage.cts;
+
+import static android.app.AppOpsManager.permissionToOp;
+import static android.os.SystemProperties.getBoolean;
+import static android.provider.MediaStore.MediaColumns;
+import static android.scopedstorage.cts.lib.RedactionTestHelper.assertExifMetadataMatch;
+import static android.scopedstorage.cts.lib.RedactionTestHelper.assertExifMetadataMismatch;
+import static android.scopedstorage.cts.lib.RedactionTestHelper.getExifMetadata;
+import static android.scopedstorage.cts.lib.RedactionTestHelper.getExifMetadataFromRawResource;
+import static android.scopedstorage.cts.lib.TestUtils.ALARMS_DIR;
+import static android.scopedstorage.cts.lib.TestUtils.ANDROID_DATA_DIR;
+import static android.scopedstorage.cts.lib.TestUtils.ANDROID_MEDIA_DIR;
+import static android.scopedstorage.cts.lib.TestUtils.AUDIOBOOKS_DIR;
+import static android.scopedstorage.cts.lib.TestUtils.BYTES_DATA1;
+import static android.scopedstorage.cts.lib.TestUtils.BYTES_DATA2;
+import static android.scopedstorage.cts.lib.TestUtils.DCIM_DIR;
+import static android.scopedstorage.cts.lib.TestUtils.DEFAULT_TOP_LEVEL_DIRS;
+import static android.scopedstorage.cts.lib.TestUtils.DOCUMENTS_DIR;
+import static android.scopedstorage.cts.lib.TestUtils.DOWNLOAD_DIR;
+import static android.scopedstorage.cts.lib.TestUtils.MOVIES_DIR;
+import static android.scopedstorage.cts.lib.TestUtils.MUSIC_DIR;
+import static android.scopedstorage.cts.lib.TestUtils.NOTIFICATIONS_DIR;
+import static android.scopedstorage.cts.lib.TestUtils.PICTURES_DIR;
+import static android.scopedstorage.cts.lib.TestUtils.PODCASTS_DIR;
+import static android.scopedstorage.cts.lib.TestUtils.RINGTONES_DIR;
+import static android.scopedstorage.cts.lib.TestUtils.STR_DATA1;
+import static android.scopedstorage.cts.lib.TestUtils.STR_DATA2;
+import static android.scopedstorage.cts.lib.TestUtils.allowAppOpsToUid;
+import static android.scopedstorage.cts.lib.TestUtils.assertCanRenameDirectory;
+import static android.scopedstorage.cts.lib.TestUtils.assertCanRenameFile;
+import static android.scopedstorage.cts.lib.TestUtils.assertCantRenameDirectory;
+import static android.scopedstorage.cts.lib.TestUtils.assertCantRenameFile;
+import static android.scopedstorage.cts.lib.TestUtils.assertDirectoryContains;
+import static android.scopedstorage.cts.lib.TestUtils.assertFileContent;
+import static android.scopedstorage.cts.lib.TestUtils.assertThrows;
+import static android.scopedstorage.cts.lib.TestUtils.canOpen;
+import static android.scopedstorage.cts.lib.TestUtils.createFileAs;
+import static android.scopedstorage.cts.lib.TestUtils.deleteFileAs;
+import static android.scopedstorage.cts.lib.TestUtils.deleteFileAsNoThrow;
+import static android.scopedstorage.cts.lib.TestUtils.deleteRecursively;
+import static android.scopedstorage.cts.lib.TestUtils.deleteWithMediaProvider;
+import static android.scopedstorage.cts.lib.TestUtils.denyAppOpsToUid;
+import static android.scopedstorage.cts.lib.TestUtils.executeShellCommand;
+import static android.scopedstorage.cts.lib.TestUtils.getContentResolver;
+import static android.scopedstorage.cts.lib.TestUtils.getFileMimeTypeFromDatabase;
+import static android.scopedstorage.cts.lib.TestUtils.getFileRowIdFromDatabase;
+import static android.scopedstorage.cts.lib.TestUtils.getFileUri;
+import static android.scopedstorage.cts.lib.TestUtils.grantPermission;
+import static android.scopedstorage.cts.lib.TestUtils.installApp;
+import static android.scopedstorage.cts.lib.TestUtils.installAppWithStoragePermissions;
+import static android.scopedstorage.cts.lib.TestUtils.listAs;
+import static android.scopedstorage.cts.lib.TestUtils.openFileAs;
+import static android.scopedstorage.cts.lib.TestUtils.openWithMediaProvider;
+import static android.scopedstorage.cts.lib.TestUtils.pollForExternalStorageState;
+import static android.scopedstorage.cts.lib.TestUtils.pollForPermission;
+import static android.scopedstorage.cts.lib.TestUtils.queryImageFile;
+import static android.scopedstorage.cts.lib.TestUtils.readExifMetadataFromTestApp;
+import static android.scopedstorage.cts.lib.TestUtils.revokePermission;
+import static android.scopedstorage.cts.lib.TestUtils.setupDefaultDirectories;
+import static android.scopedstorage.cts.lib.TestUtils.uninstallApp;
+import static android.scopedstorage.cts.lib.TestUtils.uninstallAppNoThrow;
+import static android.scopedstorage.cts.lib.TestUtils.updateDisplayNameWithMediaProvider;
+import static android.system.OsConstants.F_OK;
+import static android.system.OsConstants.O_APPEND;
+import static android.system.OsConstants.O_CREAT;
+import static android.system.OsConstants.O_EXCL;
+import static android.system.OsConstants.O_RDWR;
+import static android.system.OsConstants.O_TRUNC;
+import static android.system.OsConstants.R_OK;
+import static android.system.OsConstants.S_IRWXU;
+import static android.system.OsConstants.W_OK;
+
+import static androidx.test.InstrumentationRegistry.getContext;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
+
+import android.Manifest;
+import android.app.AppOpsManager;
+import android.content.ContentResolver;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Environment;
+import android.os.FileUtils;
+import android.os.ParcelFileDescriptor;
+import android.os.Process;
+import android.provider.MediaStore;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.cts.install.lib.TestApp;
+
+import com.google.common.io.Files;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.HashMap;
+
+@RunWith(AndroidJUnit4.class)
+public class ScopedStorageTest {
+    static final String TAG = "ScopedStorageTest";
+    static final String THIS_PACKAGE_NAME = getContext().getPackageName();
+
+    static final File EXTERNAL_STORAGE_DIR = Environment.getExternalStorageDirectory();
+
+    static final String TEST_DIRECTORY_NAME = "ScopedStorageTestDirectory";
+
+    static final File EXTERNAL_FILES_DIR = getContext().getExternalFilesDir(null);
+    static final File EXTERNAL_MEDIA_DIR = getContext().getExternalMediaDirs()[0];
+
+    static final String AUDIO_FILE_NAME = "ScopedStorageTest_file.mp3";
+    static final String PLAYLIST_FILE_NAME = "ScopedStorageTest_file.m3u";
+    static final String SUBTITLE_FILE_NAME = "ScopedStorageTest_file.srt";
+    static final String VIDEO_FILE_NAME = "ScopedStorageTest_file.mp4";
+    static final String IMAGE_FILE_NAME = "ScopedStorageTest_file.jpg";
+    static final String NONMEDIA_FILE_NAME = "ScopedStorageTest_file.pdf";
+
+    static final String FILE_CREATION_ERROR_MESSAGE = "No such file or directory";
+    private static final File ANDROID_DIR =
+            new File(Environment.getExternalStorageDirectory(), "Android");
+
+    private static final TestApp TEST_APP_A = new TestApp("TestAppA",
+            "android.scopedstorage.cts.testapp.A", 1, false, "CtsScopedStorageTestAppA.apk");
+    private static final TestApp TEST_APP_B = new TestApp("TestAppB",
+            "android.scopedstorage.cts.testapp.B", 1, false, "CtsScopedStorageTestAppB.apk");
+    private static final TestApp TEST_APP_C = new TestApp("TestAppC",
+            "android.scopedstorage.cts.testapp.C", 1, false, "CtsScopedStorageTestAppC.apk");
+    private static final TestApp TEST_APP_C_LEGACY = new TestApp("TestAppCLegacy",
+            "android.scopedstorage.cts.testapp.C", 1, false, "CtsScopedStorageTestAppCLegacy.apk");
+    private static final String[] SYSTEM_GALERY_APPOPS = {
+            AppOpsManager.OPSTR_WRITE_MEDIA_IMAGES, AppOpsManager.OPSTR_WRITE_MEDIA_VIDEO};
+    private static final String OPSTR_MANAGE_EXTERNAL_STORAGE =
+            permissionToOp(Manifest.permission.MANAGE_EXTERNAL_STORAGE);
+
+    @Before
+    public void setup() throws Exception {
+        // skips all test cases if FUSE is not active.
+        assumeTrue(getBoolean("persist.sys.fuse", false));
+
+        pollForExternalStorageState();
+        EXTERNAL_FILES_DIR.mkdirs();
+    }
+
+    /**
+     * This method needs to be called once before running the whole test.
+     */
+    @Test
+    public void setupExternalStorage() {
+        setupDefaultDirectories();
+    }
+
+    /**
+     * Test that we enforce certain media types can only be created in certain directories.
+     */
+    @Test
+    public void testTypePathConformity() throws Exception {
+        // Only audio files can be created in Music
+        assertThrows(IOException.class, "Operation not permitted",
+                () -> { new File(MUSIC_DIR, NONMEDIA_FILE_NAME).createNewFile(); });
+        assertThrows(IOException.class, "Operation not permitted",
+                () -> { new File(MUSIC_DIR, VIDEO_FILE_NAME).createNewFile(); });
+        assertThrows(IOException.class, "Operation not permitted",
+                () -> { new File(MUSIC_DIR, IMAGE_FILE_NAME).createNewFile(); });
+        // Only video files can be created in Movies
+        assertThrows(IOException.class, "Operation not permitted",
+                () -> { new File(MOVIES_DIR, NONMEDIA_FILE_NAME).createNewFile(); });
+        assertThrows(IOException.class, "Operation not permitted",
+                () -> { new File(MOVIES_DIR, AUDIO_FILE_NAME).createNewFile(); });
+        assertThrows(IOException.class, "Operation not permitted",
+                () -> { new File(MOVIES_DIR, IMAGE_FILE_NAME).createNewFile(); });
+        // Only image and video files can be created in DCIM
+        assertThrows(IOException.class, "Operation not permitted",
+                () -> { new File(DCIM_DIR, NONMEDIA_FILE_NAME).createNewFile(); });
+        assertThrows(IOException.class, "Operation not permitted",
+                () -> { new File(DCIM_DIR, AUDIO_FILE_NAME).createNewFile(); });
+        // Only image and video files can be created in Pictures
+        assertThrows(IOException.class, "Operation not permitted",
+                () -> { new File(PICTURES_DIR, NONMEDIA_FILE_NAME).createNewFile(); });
+        assertThrows(IOException.class, "Operation not permitted",
+                () -> { new File(PICTURES_DIR, AUDIO_FILE_NAME).createNewFile(); });
+        assertThrows(IOException.class, "Operation not permitted",
+                () -> { new File(PICTURES_DIR, PLAYLIST_FILE_NAME).createNewFile(); });
+        assertThrows(IOException.class, "Operation not permitted",
+                () -> { new File(DCIM_DIR, SUBTITLE_FILE_NAME).createNewFile(); });
+
+        assertCanCreateFile(new File(ALARMS_DIR, AUDIO_FILE_NAME));
+        assertCanCreateFile(new File(AUDIOBOOKS_DIR, AUDIO_FILE_NAME));
+        assertCanCreateFile(new File(DCIM_DIR, IMAGE_FILE_NAME));
+        assertCanCreateFile(new File(DCIM_DIR, VIDEO_FILE_NAME));
+        assertCanCreateFile(new File(DOCUMENTS_DIR, AUDIO_FILE_NAME));
+        assertCanCreateFile(new File(DOCUMENTS_DIR, IMAGE_FILE_NAME));
+        assertCanCreateFile(new File(DOCUMENTS_DIR, NONMEDIA_FILE_NAME));
+        assertCanCreateFile(new File(DOCUMENTS_DIR, VIDEO_FILE_NAME));
+        assertCanCreateFile(new File(DOWNLOAD_DIR, AUDIO_FILE_NAME));
+        assertCanCreateFile(new File(DOWNLOAD_DIR, IMAGE_FILE_NAME));
+        assertCanCreateFile(new File(DOWNLOAD_DIR, NONMEDIA_FILE_NAME));
+        assertCanCreateFile(new File(DOWNLOAD_DIR, VIDEO_FILE_NAME));
+        assertCanCreateFile(new File(MOVIES_DIR, VIDEO_FILE_NAME));
+        assertCanCreateFile(new File(MOVIES_DIR, SUBTITLE_FILE_NAME));
+        assertCanCreateFile(new File(MUSIC_DIR, AUDIO_FILE_NAME));
+        assertCanCreateFile(new File(MUSIC_DIR, PLAYLIST_FILE_NAME));
+        assertCanCreateFile(new File(NOTIFICATIONS_DIR, AUDIO_FILE_NAME));
+        assertCanCreateFile(new File(PICTURES_DIR, IMAGE_FILE_NAME));
+        assertCanCreateFile(new File(PICTURES_DIR, VIDEO_FILE_NAME));
+        assertCanCreateFile(new File(PODCASTS_DIR, AUDIO_FILE_NAME));
+        assertCanCreateFile(new File(RINGTONES_DIR, AUDIO_FILE_NAME));
+
+        // No file whatsoever can be created in the top level directory
+        assertThrows(IOException.class, "Operation not permitted",
+                () -> { new File(EXTERNAL_STORAGE_DIR, NONMEDIA_FILE_NAME).createNewFile(); });
+        assertThrows(IOException.class, "Operation not permitted",
+                () -> { new File(EXTERNAL_STORAGE_DIR, AUDIO_FILE_NAME).createNewFile(); });
+        assertThrows(IOException.class, "Operation not permitted",
+                () -> { new File(EXTERNAL_STORAGE_DIR, IMAGE_FILE_NAME).createNewFile(); });
+        assertThrows(IOException.class, "Operation not permitted",
+                () -> { new File(EXTERNAL_STORAGE_DIR, VIDEO_FILE_NAME).createNewFile(); });
+    }
+
+    /**
+     * Test that we can create a file in app's external files directory,
+     * and that we can write and read to/from the file.
+     */
+    @Test
+    public void testCreateFileInAppExternalDir() throws Exception {
+        final File file = new File(EXTERNAL_FILES_DIR, "text.txt");
+        try {
+            assertThat(file.createNewFile()).isTrue();
+            assertThat(file.delete()).isTrue();
+            // Ensure the file is properly deleted and can be created again
+            assertThat(file.createNewFile()).isTrue();
+
+            // Write to file
+            try (final FileOutputStream fos = new FileOutputStream(file)) {
+                fos.write(BYTES_DATA1);
+            }
+
+            // Read the same data from file
+            assertFileContent(file, BYTES_DATA1);
+        } finally {
+            file.delete();
+        }
+    }
+
+    /**
+     * Test that we can't create a file in another app's external files directory,
+     * and that we'll get the same error regardless of whether the app exists or not.
+     */
+    @Test
+    public void testCreateFileInOtherAppExternalDir() throws Exception {
+        // Creating a file in a non existent package dir should return ENOENT, as expected
+        final File nonexistentPackageFileDir = new File(
+                EXTERNAL_FILES_DIR.getPath().replace(THIS_PACKAGE_NAME, "no.such.package"));
+        final File file1 = new File(nonexistentPackageFileDir, NONMEDIA_FILE_NAME);
+        assertThrows(
+                IOException.class, FILE_CREATION_ERROR_MESSAGE, () -> { file1.createNewFile(); });
+
+        // Creating a file in an existent package dir should give the same error string to avoid
+        // leaking installed app names, and we know the following directory exists because shell
+        // mkdirs it in test setup
+        final File shellPackageFileDir = new File(
+                EXTERNAL_FILES_DIR.getPath().replace(THIS_PACKAGE_NAME, "com.android.shell"));
+        final File file2 = new File(shellPackageFileDir, NONMEDIA_FILE_NAME);
+        assertThrows(
+                IOException.class, FILE_CREATION_ERROR_MESSAGE, () -> { file1.createNewFile(); });
+    }
+
+    /**
+     * Test that we can contribute media without any permissions.
+     */
+    @Test
+    public void testContributeMediaFile() throws Exception {
+        final File imageFile = new File(DCIM_DIR, IMAGE_FILE_NAME);
+
+        ContentResolver cr = getContentResolver();
+        final String selection =
+                MediaColumns.RELATIVE_PATH + " = ? AND " + MediaColumns.DISPLAY_NAME + " = ?";
+        final String[] selectionArgs = {Environment.DIRECTORY_DCIM + '/', IMAGE_FILE_NAME};
+
+        try {
+            assertThat(imageFile.createNewFile()).isTrue();
+
+            // Ensure that the file was successfully added to the MediaProvider database
+            try (final Cursor c = cr.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+                         /* projection */ new String[] {MediaColumns.OWNER_PACKAGE_NAME},
+                         selection, selectionArgs, null)) {
+                assertThat(c.getCount()).isEqualTo(1);
+                c.moveToFirst();
+                assertThat(c.getString(c.getColumnIndex(MediaColumns.OWNER_PACKAGE_NAME)))
+                        .isEqualTo(THIS_PACKAGE_NAME);
+            }
+
+            // Try to write random data to the file
+            try (final FileOutputStream fos = new FileOutputStream(imageFile)) {
+                fos.write(BYTES_DATA1);
+                fos.write(BYTES_DATA2);
+            }
+
+            final byte[] expected = (STR_DATA1 + STR_DATA2).getBytes();
+            assertFileContent(imageFile, expected);
+
+            // Closing the file after writing will not trigger a MediaScan. Call scanFile to update
+            // file's entry in MediaProvider's database.
+            assertThat(MediaStore.scanFile(getContentResolver(), imageFile)).isNotNull();
+
+            // Ensure that the scan was completed and the file's size was updated.
+            try (final Cursor c = cr.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+                         /* projection */ new String[] {MediaColumns.SIZE}, selection,
+                         selectionArgs, null)) {
+                assertThat(c.getCount()).isEqualTo(1);
+                c.moveToFirst();
+                assertThat(c.getInt(c.getColumnIndex(MediaColumns.SIZE)))
+                        .isEqualTo(BYTES_DATA1.length + BYTES_DATA2.length);
+            }
+        } finally {
+            imageFile.delete();
+        }
+        // Ensure that delete makes a call to MediaProvider to remove the file from its database.
+        try (final Cursor c = cr.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+                     /* projection */ new String[] {MediaColumns.OWNER_PACKAGE_NAME}, selection,
+                     selectionArgs, null)) {
+            assertThat(c.getCount()).isEqualTo(0);
+        }
+    }
+
+    @Test
+    public void testCreateAndDeleteEmptyDir() throws Exception {
+        // Remove directory in order to create it again
+        EXTERNAL_FILES_DIR.delete();
+
+        // Can create own external files dir
+        assertThat(EXTERNAL_FILES_DIR.mkdir()).isTrue();
+
+        final File dir1 = new File(EXTERNAL_FILES_DIR, "random_dir");
+        // Can create dirs inside it
+        assertThat(dir1.mkdir()).isTrue();
+
+        final File dir2 = new File(dir1, "random_dir_inside_random_dir");
+        // And create a dir inside the new dir
+        assertThat(dir2.mkdir()).isTrue();
+
+        // And can delete them all
+        assertThat(dir2.delete()).isTrue();
+        assertThat(dir1.delete()).isTrue();
+        assertThat(EXTERNAL_FILES_DIR.delete()).isTrue();
+
+        // Can't create external dir for other apps
+        final File nonexistentPackageFileDir = new File(
+                EXTERNAL_FILES_DIR.getPath().replace(THIS_PACKAGE_NAME, "no.such.package"));
+        final File shellPackageFileDir = new File(
+                EXTERNAL_FILES_DIR.getPath().replace(THIS_PACKAGE_NAME, "com.android.shell"));
+
+        assertThat(nonexistentPackageFileDir.mkdir()).isFalse();
+        assertThat(shellPackageFileDir.mkdir()).isFalse();
+    }
+
+    @Test
+    public void testCantAccessOtherAppsContents() throws Exception {
+        final File mediaFile = new File(PICTURES_DIR, IMAGE_FILE_NAME);
+        final File nonMediaFile = new File(DOWNLOAD_DIR, NONMEDIA_FILE_NAME);
+        try {
+            installApp(TEST_APP_A);
+
+            assertThat(createFileAs(TEST_APP_A, mediaFile.getPath())).isTrue();
+            assertThat(createFileAs(TEST_APP_A, nonMediaFile.getPath())).isTrue();
+
+            // We can still see that the files exist
+            assertThat(mediaFile.exists()).isTrue();
+            assertThat(nonMediaFile.exists()).isTrue();
+
+            // But we can't access their content
+            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());
+            uninstallAppNoThrow(TEST_APP_A);
+        }
+    }
+
+    @Test
+    public void testCantDeleteOtherAppsContents() throws Exception {
+        final File dirInDownload = new File(DOWNLOAD_DIR, TEST_DIRECTORY_NAME);
+        final File mediaFile = new File(dirInDownload, IMAGE_FILE_NAME);
+        final File nonMediaFile = new File(dirInDownload, NONMEDIA_FILE_NAME);
+        try {
+            installApp(TEST_APP_A);
+            assertThat(dirInDownload.mkdir()).isTrue();
+            // Have another app create a media file in the directory
+            assertThat(createFileAs(TEST_APP_A, mediaFile.getPath())).isTrue();
+
+            // Can't delete the directory since it contains another app's content
+            assertThat(dirInDownload.delete()).isFalse();
+            // Can't delete another app's content
+            assertThat(deleteRecursively(dirInDownload)).isFalse();
+
+            // Have another app create a non-media file in the directory
+            assertThat(createFileAs(TEST_APP_A, nonMediaFile.getPath())).isTrue();
+
+            // Can't delete the directory since it contains another app's content
+            assertThat(dirInDownload.delete()).isFalse();
+            // Can't delete another app's content
+            assertThat(deleteRecursively(dirInDownload)).isFalse();
+
+            // Delete only the media file and keep the non-media file
+            assertThat(deleteFileAs(TEST_APP_A, mediaFile.getPath())).isTrue();
+            // Directory now has only the non-media file contributed by another app, so we still
+            // can't delete it nor its content
+            assertThat(dirInDownload.delete()).isFalse();
+            assertThat(deleteRecursively(dirInDownload)).isFalse();
+
+            // Delete the last file belonging to another app
+            assertThat(deleteFileAs(TEST_APP_A, nonMediaFile.getPath())).isTrue();
+            // Create our own file
+            assertThat(nonMediaFile.createNewFile()).isTrue();
+
+            // Now that the directory only has content that was contributed by us, we can delete it
+            assertThat(deleteRecursively(dirInDownload)).isTrue();
+        } finally {
+            deleteFileAsNoThrow(TEST_APP_A, nonMediaFile.getPath());
+            deleteFileAsNoThrow(TEST_APP_A, mediaFile.getPath());
+            // At this point, we're not sure who created this file, so we'll have both apps
+            // deleting it
+            mediaFile.delete();
+            uninstallAppNoThrow(TEST_APP_A);
+            dirInDownload.delete();
+        }
+    }
+
+    /**
+     * This test relies on the fact that {@link File#list} uses opendir internally, and that it
+     * returns {@code null} if opendir fails.
+     */
+    @Test
+    public void testOpendirRestrictions() throws Exception {
+        // Opening a non existent package directory should fail, as expected
+        final File nonexistentPackageFileDir = new File(
+                EXTERNAL_FILES_DIR.getPath().replace(THIS_PACKAGE_NAME, "no.such.package"));
+        assertThat(nonexistentPackageFileDir.list()).isNull();
+
+        // Opening another package's external directory should fail as well, even if it exists
+        final File shellPackageFileDir = new File(
+                EXTERNAL_FILES_DIR.getPath().replace(THIS_PACKAGE_NAME, "com.android.shell"));
+        assertThat(shellPackageFileDir.list()).isNull();
+
+        // We can open our own external files directory
+        final String[] filesList = EXTERNAL_FILES_DIR.list();
+        assertThat(filesList).isNotNull();
+        assertThat(filesList).isEmpty();
+
+        // We can open any public directory in external storage
+        assertThat(DCIM_DIR.list()).isNotNull();
+        assertThat(DOWNLOAD_DIR.list()).isNotNull();
+        assertThat(MOVIES_DIR.list()).isNotNull();
+        assertThat(MUSIC_DIR.list()).isNotNull();
+
+        // We can open the root directory of external storage
+        final String[] topLevelDirs = EXTERNAL_STORAGE_DIR.list();
+        assertThat(topLevelDirs).isNotNull();
+        // TODO(b/145287327): This check fails on a device with no visible files.
+        // This can be fixed if we display default directories.
+        // assertThat(topLevelDirs).isNotEmpty();
+    }
+
+    @Test
+    public void testLowLevelFileIO() throws Exception {
+        String filePath = new File(DOWNLOAD_DIR, NONMEDIA_FILE_NAME).toString();
+        try {
+            int createFlags = O_CREAT | O_RDWR;
+            int createExclFlags = createFlags | O_EXCL;
+
+            FileDescriptor fd = Os.open(filePath, createExclFlags, S_IRWXU);
+            Os.close(fd);
+            assertThrows(
+                    ErrnoException.class, () -> { Os.open(filePath, createExclFlags, S_IRWXU); });
+
+            fd = Os.open(filePath, createFlags, S_IRWXU);
+            try {
+                assertThat(Os.write(fd, ByteBuffer.wrap(BYTES_DATA1))).isEqualTo(BYTES_DATA1.length);
+                assertFileContent(fd, BYTES_DATA1);
+            } finally {
+                Os.close(fd);
+            }
+            // should just append the data
+            fd = Os.open(filePath, createFlags | O_APPEND, S_IRWXU);
+            try {
+                assertThat(Os.write(fd, ByteBuffer.wrap(BYTES_DATA2))).isEqualTo(BYTES_DATA2.length);
+                final byte[] expected = (STR_DATA1 + STR_DATA2).getBytes();
+                assertFileContent(fd, expected);
+            } finally {
+                Os.close(fd);
+            }
+            // should overwrite everything
+            fd = Os.open(filePath, createFlags | O_TRUNC, S_IRWXU);
+            try {
+                final byte[] otherData = "this is different data".getBytes();
+                assertThat(Os.write(fd, ByteBuffer.wrap(otherData))).isEqualTo(otherData.length);
+                assertFileContent(fd, otherData);
+            } finally {
+                Os.close(fd);
+            }
+        } finally {
+            new File(filePath).delete();
+        }
+    }
+
+    /**
+     * Test that media files from other packages are only visible to apps with storage permission.
+     */
+    @Test
+    public void testListDirectoriesWithMediaFiles() throws Exception {
+        final File dir = new File(DCIM_DIR, TEST_DIRECTORY_NAME);
+        final File videoFile = new File(dir, VIDEO_FILE_NAME);
+        final String videoFileName = videoFile.getName();
+        try {
+            if (!dir.exists()) {
+                assertThat(dir.mkdir()).isTrue();
+            }
+
+            // Install TEST_APP_A and create media file in the new directory.
+            installApp(TEST_APP_A);
+            assertThat(createFileAs(TEST_APP_A, videoFile.getPath())).isTrue();
+            // TEST_APP_A should see TEST_DIRECTORY in DCIM and new file in TEST_DIRECTORY.
+            assertThat(listAs(TEST_APP_A, DCIM_DIR.getPath())).contains(TEST_DIRECTORY_NAME);
+            assertThat(listAs(TEST_APP_A, dir.getPath())).containsExactly(videoFileName);
+
+            // Install TEST_APP_B with storage permission.
+            installAppWithStoragePermissions(TEST_APP_B);
+            // TEST_APP_B with storage permission should see TEST_DIRECTORY in DCIM and new file
+            // in TEST_DIRECTORY.
+            assertThat(listAs(TEST_APP_B, DCIM_DIR.getPath())).contains(TEST_DIRECTORY_NAME);
+            assertThat(listAs(TEST_APP_B, dir.getPath())).containsExactly(videoFileName);
+
+            // Revoke storage permission for TEST_APP_B
+            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);
+            assertThat(listAs(TEST_APP_B, dir.getPath())).doesNotContain(videoFileName);
+        } finally {
+            uninstallAppNoThrow(TEST_APP_B);
+            deleteFileAsNoThrow(TEST_APP_A, videoFile.getPath());
+            dir.delete();
+            uninstallAppNoThrow(TEST_APP_A);
+        }
+    }
+
+    /**
+     * Test that app can't see non-media files created by other packages
+     */
+    @Test
+    public void testListDirectoriesWithNonMediaFiles() throws Exception {
+        final File dir = new File(DOWNLOAD_DIR, TEST_DIRECTORY_NAME);
+        final File pdfFile = new File(dir, NONMEDIA_FILE_NAME);
+        final String pdfFileName = pdfFile.getName();
+        try {
+            if (!dir.exists()) {
+                assertThat(dir.mkdir()).isTrue();
+            }
+
+            // Install TEST_APP_A and create non media file in the new directory.
+            installApp(TEST_APP_A);
+            assertThat(createFileAs(TEST_APP_A, pdfFile.getPath())).isTrue();
+
+            // TEST_APP_A should see TEST_DIRECTORY in DOWNLOAD_DIR and new non media file in
+            // TEST_DIRECTORY.
+            assertThat(listAs(TEST_APP_A, DOWNLOAD_DIR.getPath())).contains(TEST_DIRECTORY_NAME);
+            assertThat(listAs(TEST_APP_A, dir.getPath())).containsExactly(pdfFileName);
+
+            // Install TEST_APP_B with storage permission.
+            installAppWithStoragePermissions(TEST_APP_B);
+            // TEST_APP_B with storage permission should see TEST_DIRECTORY in DOWNLOAD_DIR
+            // and should not see new non media file in TEST_DIRECTORY.
+            assertThat(listAs(TEST_APP_B, DOWNLOAD_DIR.getPath())).contains(TEST_DIRECTORY_NAME);
+            assertThat(listAs(TEST_APP_B, dir.getPath())).doesNotContain(pdfFileName);
+        } finally {
+            uninstallAppNoThrow(TEST_APP_B);
+            deleteFileAsNoThrow(TEST_APP_A, pdfFile.getPath());
+            dir.delete();
+            uninstallAppNoThrow(TEST_APP_A);
+        }
+    }
+
+    /**
+     * Test that app can only see its directory in Android/data.
+     */
+    @Test
+    public void testListFilesFromExternalFilesDirectory() throws Exception {
+        final String packageName = THIS_PACKAGE_NAME;
+        final File videoFile = new File(EXTERNAL_FILES_DIR, NONMEDIA_FILE_NAME);
+
+        try {
+            // Create a file in app's external files directory
+            if (!videoFile.exists()) {
+                assertThat(videoFile.createNewFile()).isTrue();
+            }
+            // App should see its directory and directories of shared packages. App should see all
+            // files and directories in its external directory.
+            assertDirectoryContains(videoFile.getParentFile(), videoFile);
+
+            // Install TEST_APP_A with READ_EXTERNAL_STORAGE permission.
+            // TEST_APP_A should not see other app's external files directory.
+            installAppWithStoragePermissions(TEST_APP_A);
+
+            assertThrows(IOException.class, () -> listAs(TEST_APP_A, ANDROID_DATA_DIR.getPath()));
+            assertThrows(IOException.class, () -> listAs(TEST_APP_A, EXTERNAL_FILES_DIR.getPath()));
+        } finally {
+            videoFile.delete();
+            uninstallAppNoThrow(TEST_APP_A);
+        }
+    }
+
+    /**
+     * Test that app can see files and directories in Android/media.
+     */
+    @Test
+    public void testListFilesFromExternalMediaDirectory() throws Exception {
+        final File videoFile = new File(EXTERNAL_MEDIA_DIR, VIDEO_FILE_NAME);
+
+        try {
+            // Create a file in app's external media directory
+            if (!videoFile.exists()) {
+                assertThat(videoFile.createNewFile()).isTrue();
+            }
+
+            // App should see its directory and other app's external media directories with media
+            // files.
+            assertDirectoryContains(videoFile.getParentFile(), videoFile);
+
+            // Install TEST_APP_A with READ_EXTERNAL_STORAGE permission.
+            // TEST_APP_A with storage permission should see other app's external media directory.
+            installAppWithStoragePermissions(TEST_APP_A);
+            // Apps with READ_EXTERNAL_STORAGE can list files in other app's external media directory.
+            assertThat(listAs(TEST_APP_A, ANDROID_MEDIA_DIR.getPath())).contains(THIS_PACKAGE_NAME);
+            assertThat(listAs(TEST_APP_A, EXTERNAL_MEDIA_DIR.getPath()))
+                    .containsExactly(videoFile.getName());
+        } finally {
+            videoFile.delete();
+            uninstallAppNoThrow(TEST_APP_A);
+        }
+    }
+
+    /**
+     * Test that readdir lists unsupported file types in default directories.
+     */
+    @Test
+    public void testListUnsupportedFileType() throws Exception {
+        final File pdfFile = new File(DCIM_DIR, NONMEDIA_FILE_NAME);
+        final File videoFile = new File(MUSIC_DIR, VIDEO_FILE_NAME);
+        try {
+            // TEST_APP_A with storage permission should not see pdf file in DCIM
+            executeShellCommand("touch " + pdfFile.getAbsolutePath());
+            assertThat(pdfFile.exists()).isTrue();
+            assertThat(MediaStore.scanFile(getContentResolver(), pdfFile)).isNotNull();
+
+            installAppWithStoragePermissions(TEST_APP_A);
+            assertThat(listAs(TEST_APP_A, DCIM_DIR.getPath())).doesNotContain(NONMEDIA_FILE_NAME);
+
+            executeShellCommand("touch " + videoFile.getAbsolutePath());
+            // We don't insert files to db for files created by shell.
+            assertThat(MediaStore.scanFile(getContentResolver(), videoFile)).isNotNull();
+            // TEST_APP_A with storage permission should see video file in Music directory.
+            assertThat(listAs(TEST_APP_A, MUSIC_DIR.getPath())).contains(VIDEO_FILE_NAME);
+        } finally {
+            executeShellCommand("rm " + pdfFile.getAbsolutePath());
+            executeShellCommand("rm " + videoFile.getAbsolutePath());
+            uninstallAppNoThrow(TEST_APP_A);
+        }
+    }
+
+    @Test
+    public void testMetaDataRedaction() throws Exception {
+        File jpgFile = new File(PICTURES_DIR, "img_metadata.jpg");
+        try {
+            if (jpgFile.exists()) {
+                assertThat(jpgFile.delete()).isTrue();
+            }
+
+            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(jpgFile)) {
+                // Dump the image we have to external storage
+                FileUtils.copy(in, out);
+            }
+
+            HashMap<String, String> exif = getExifMetadata(jpgFile);
+            assertExifMetadataMatch(exif, originalExif);
+
+            installAppWithStoragePermissions(TEST_APP_A);
+            HashMap<String, String> exifFromTestApp =
+                    readExifMetadataFromTestApp(TEST_APP_A, jpgFile.getPath());
+            // Other apps shouldn't have access to the same metadata without explicit permission
+            assertExifMetadataMismatch(exifFromTestApp, originalExif);
+
+            // TODO(b/146346138): Test that if we give TEST_APP_A write URI permission,
+            //  it would be able to access the metadata.
+        } finally {
+            jpgFile.delete();
+            uninstallAppNoThrow(TEST_APP_A);
+        }
+    }
+
+    @Test
+    public void testOpenFilePathFirstWriteContentResolver() throws Exception {
+        String displayName = "open_file_path_write_content_resolver.jpg";
+        File file = new File(DCIM_DIR, displayName);
+
+        try {
+            assertThat(file.createNewFile()).isTrue();
+
+            ParcelFileDescriptor readPfd =
+                    ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE);
+            ParcelFileDescriptor writePfd = openWithMediaProvider(file, "rw");
+
+            assertRWR(readPfd, writePfd);
+            assertUpperFsFd(writePfd); // With cache
+        } finally {
+            file.delete();
+        }
+    }
+
+    @Test
+    public void testOpenContentResolverFirstWriteContentResolver() throws Exception {
+        String displayName = "open_content_resolver_write_content_resolver.jpg";
+        File file = new File(DCIM_DIR, displayName);
+
+        try {
+            assertThat(file.createNewFile()).isTrue();
+
+            ParcelFileDescriptor writePfd = openWithMediaProvider(file, "rw");
+            ParcelFileDescriptor readPfd =
+                    ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE);
+
+            assertRWR(readPfd, writePfd);
+            assertLowerFsFd(writePfd);
+        } finally {
+            file.delete();
+        }
+    }
+
+    @Test
+    public void testOpenFilePathFirstWriteFilePath() throws Exception {
+        String displayName = "open_file_path_write_file_path.jpg";
+        File file = new File(DCIM_DIR, displayName);
+
+        try {
+            assertThat(file.createNewFile()).isTrue();
+
+            ParcelFileDescriptor writePfd =
+                    ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE);
+            ParcelFileDescriptor readPfd = openWithMediaProvider(file, "rw");
+
+            assertRWR(readPfd, writePfd);
+            assertUpperFsFd(readPfd); // With cache
+        } finally {
+            file.delete();
+        }
+    }
+
+    @Test
+    public void testOpenContentResolverFirstWriteFilePath() throws Exception {
+        String displayName = "open_content_resolver_write_file_path.jpg";
+        File file = new File(DCIM_DIR, displayName);
+
+        try {
+            assertThat(file.createNewFile()).isTrue();
+
+            ParcelFileDescriptor readPfd = openWithMediaProvider(file, "rw");
+            ParcelFileDescriptor writePfd =
+                    ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE);
+
+            assertRWR(readPfd, writePfd);
+            assertLowerFsFd(readPfd);
+        } finally {
+            file.delete();
+        }
+    }
+
+    @Test
+    public void testOpenContentResolverWriteOnly() throws Exception {
+        String displayName = "open_content_resolver_write_only.jpg";
+        File file = new File(DCIM_DIR, displayName);
+
+        try {
+            assertThat(file.createNewFile()).isTrue();
+
+            // Since we can only place one F_WRLCK, the second open for readPfd will go
+            // throuh FUSE
+            ParcelFileDescriptor writePfd = openWithMediaProvider(file, "w");
+            ParcelFileDescriptor readPfd = openWithMediaProvider(file, "rw");
+
+            assertRWR(readPfd, writePfd);
+            assertLowerFsFd(writePfd);
+            assertUpperFsFd(readPfd); // Without cache
+        } finally {
+            file.delete();
+        }
+    }
+
+    @Test
+    public void testOpenContentResolverDup() throws Exception {
+        String displayName = "open_content_resolver_dup.jpg";
+        File file = new File(DCIM_DIR, displayName);
+
+        try {
+            file.delete();
+            assertThat(file.createNewFile()).isTrue();
+
+            // Even if we close the original fd, since we have a dup open
+            // the FUSE IO should still bypass the cache
+            try (ParcelFileDescriptor writePfd = openWithMediaProvider(file, "rw")) {
+                try (ParcelFileDescriptor writePfdDup = writePfd.dup();
+                        ParcelFileDescriptor readPfd = ParcelFileDescriptor.open(
+                                file, ParcelFileDescriptor.MODE_READ_WRITE)) {
+                    writePfd.close();
+
+                    assertRWR(readPfd, writePfdDup);
+                    assertLowerFsFd(writePfdDup);
+                }
+            }
+        } finally {
+            file.delete();
+        }
+    }
+
+    @Test
+    public void testOpenContentResolverClose() throws Exception {
+        String displayName = "open_content_resolver_close.jpg";
+        File file = new File(DCIM_DIR, displayName);
+
+        try {
+            byte[] readBuffer = new byte[10];
+            byte[] writeBuffer = new byte[10];
+            Arrays.fill(writeBuffer, (byte) 1);
+
+            assertThat(file.createNewFile()).isTrue();
+
+            // Lower fs open and write
+            ParcelFileDescriptor writePfd = openWithMediaProvider(file, "rw");
+            Os.pwrite(writePfd.getFileDescriptor(), writeBuffer, 0, 10, 0);
+
+            // Close so upper fs open will not use direct_io
+            writePfd.close();
+
+            // Upper fs open and read without direct_io
+            ParcelFileDescriptor readPfd =
+                    ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE);
+            Os.pread(readPfd.getFileDescriptor(), readBuffer, 0, 10, 0);
+
+            // Last write on lower fs is visible via upper fs
+            assertThat(readBuffer).isEqualTo(writeBuffer);
+            assertThat(readPfd.getStatSize()).isEqualTo(writeBuffer.length);
+        } finally {
+            file.delete();
+        }
+    }
+
+    @Test
+    public void testContentResolverDelete() throws Exception {
+        String displayName = "content_resolver_delete.jpg";
+        File file = new File(DCIM_DIR, displayName);
+
+        try {
+            assertThat(file.createNewFile()).isTrue();
+
+            deleteWithMediaProvider(file);
+
+            assertThat(file.exists()).isFalse();
+            assertThat(file.createNewFile()).isTrue();
+        } finally {
+            file.delete();
+        }
+    }
+
+    @Test
+    public void testContentResolverUpdate() throws Exception {
+        String oldDisplayName = "content_resolver_update_old.jpg";
+        String newDisplayName = "content_resolver_update_new.jpg";
+        File oldFile = new File(DCIM_DIR, oldDisplayName);
+        File newFile = new File(DCIM_DIR, newDisplayName);
+
+        try {
+            assertThat(oldFile.createNewFile()).isTrue();
+
+            updateDisplayNameWithMediaProvider(
+                    Environment.DIRECTORY_DCIM, oldDisplayName, newDisplayName);
+
+            assertThat(oldFile.exists()).isFalse();
+            assertThat(oldFile.createNewFile()).isTrue();
+            assertThat(newFile.exists()).isTrue();
+            assertThat(newFile.createNewFile()).isFalse();
+        } finally {
+            oldFile.delete();
+            newFile.delete();
+        }
+    }
+
+    @Test
+    public void testCreateLowerCaseDeleteUpperCase() throws Exception {
+        File upperCase = new File(DOWNLOAD_DIR, "CREATE_LOWER_DELETE_UPPER");
+        File lowerCase = new File(DOWNLOAD_DIR, "create_lower_delete_upper");
+
+        createDeleteCreate(lowerCase, upperCase);
+    }
+
+    @Test
+    public void testCreateUpperCaseDeleteLowerCase() throws Exception {
+        File upperCase = new File(DOWNLOAD_DIR, "CREATE_UPPER_DELETE_LOWER");
+        File lowerCase = new File(DOWNLOAD_DIR, "create_upper_delete_lower");
+
+        createDeleteCreate(upperCase, lowerCase);
+    }
+
+    @Test
+    public void testCreateMixedCaseDeleteDifferentMixedCase() throws Exception {
+        File mixedCase1 = new File(DOWNLOAD_DIR, "CrEaTe_MiXeD_dElEtE_mIxEd");
+        File mixedCase2 = new File(DOWNLOAD_DIR, "cReAtE_mIxEd_DeLeTe_MiXeD");
+
+        createDeleteCreate(mixedCase1, mixedCase2);
+    }
+
+    private void createDeleteCreate(File create, File delete) throws Exception {
+        try {
+            assertThat(create.createNewFile()).isTrue();
+            Thread.sleep(5);
+
+            assertThat(delete.delete()).isTrue();
+            Thread.sleep(5);
+
+            assertThat(create.createNewFile()).isTrue();
+            Thread.sleep(5);
+        } finally {
+            create.delete();
+            create.delete();
+        }
+    }
+
+    @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
+            installAppWithStoragePermissions(TEST_APP_C);
+
+            // 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
+            installAppWithStoragePermissions(TEST_APP_C_LEGACY);
+            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
+            installAppWithStoragePermissions(TEST_APP_C);
+            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
+            installAppWithStoragePermissions(TEST_APP_C);
+            assertThat(openFileAs(TEST_APP_C, file.getPath(), /* forWrite */ false)).isTrue();
+
+            // Re-install
+            uninstallAppNoThrow(TEST_APP_C);
+            installApp(TEST_APP_C);
+            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);
+            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);
+        final File imageInAnObviouslyWrongPlace = new File(MUSIC_DIR, IMAGE_FILE_NAME);
+
+        try {
+            installApp(TEST_APP_A);
+            allowAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
+
+            // Have another app create an image file
+            assertThat(createFileAs(TEST_APP_A, otherAppImageFile.getPath())).isTrue();
+            assertThat(otherAppImageFile.exists()).isTrue();
+
+            // Assert we can write to the file
+            try (final FileOutputStream fos = new FileOutputStream(otherAppImageFile)) {
+                fos.write(BYTES_DATA1);
+            }
+
+            // Assert we can read from the file
+            assertFileContent(otherAppImageFile, BYTES_DATA1);
+
+            // Assert we can delete the file
+            assertThat(otherAppImageFile.delete()).isTrue();
+            assertThat(otherAppImageFile.exists()).isFalse();
+
+            // Can create an image anywhere
+            assertCanCreateFile(topLevelImageFile);
+            assertCanCreateFile(imageInAnObviouslyWrongPlace);
+
+            // Put the file back in its place and let TEST_APP_A delete it
+            assertThat(otherAppImageFile.createNewFile()).isTrue();
+        } finally {
+            deleteFileAsNoThrow(TEST_APP_A, otherAppImageFile.getAbsolutePath());
+            otherAppImageFile.delete();
+            uninstallApp(TEST_APP_A);
+            denyAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
+        }
+    }
+
+    @Test
+    public void testSystemGalleryAppHasNoFullAccessToAudio() throws Exception {
+        final File otherAppAudioFile = new File(MUSIC_DIR, "other_" + AUDIO_FILE_NAME);
+        final File topLevelAudioFile = new File(EXTERNAL_STORAGE_DIR, AUDIO_FILE_NAME);
+        final File audioInAnObviouslyWrongPlace = new File(PICTURES_DIR, AUDIO_FILE_NAME);
+
+        try {
+            installApp(TEST_APP_A);
+            allowAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
+
+            // Have another app create an audio file
+            assertThat(createFileAs(TEST_APP_A, otherAppAudioFile.getPath())).isTrue();
+            assertThat(otherAppAudioFile.exists()).isTrue();
+
+            // 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();
+
+            // Can't create an audio file where it doesn't belong
+            assertThrows(IOException.class, "Operation not permitted",
+                    () -> { topLevelAudioFile.createNewFile(); });
+            assertThrows(IOException.class, "Operation not permitted",
+                    () -> { audioInAnObviouslyWrongPlace.createNewFile(); });
+        } finally {
+            deleteFileAs(TEST_APP_A, otherAppAudioFile.getPath());
+            uninstallApp(TEST_APP_A);
+            topLevelAudioFile.delete();
+            audioInAnObviouslyWrongPlace.delete();
+            denyAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
+        }
+    }
+
+    @Test
+    public void testSystemGalleryCanRenameImagesAndVideos() throws Exception {
+        final File otherAppVideoFile = new File(DCIM_DIR, "other_" + VIDEO_FILE_NAME);
+        final File imageFile = new File(PICTURES_DIR, IMAGE_FILE_NAME);
+        final File videoFile = new File(PICTURES_DIR, VIDEO_FILE_NAME);
+        final File topLevelVideoFile = new File(EXTERNAL_STORAGE_DIR, VIDEO_FILE_NAME);
+        final File musicFile = new File(MUSIC_DIR, AUDIO_FILE_NAME);
+        try {
+            installApp(TEST_APP_A);
+            allowAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
+
+            // Have another app create a video file
+            assertThat(createFileAs(TEST_APP_A, otherAppVideoFile.getPath())).isTrue();
+            assertThat(otherAppVideoFile.exists()).isTrue();
+
+            // Write some data to the file
+            try (final FileOutputStream fos = new FileOutputStream(otherAppVideoFile)) {
+                fos.write(BYTES_DATA1);
+            }
+            assertFileContent(otherAppVideoFile, BYTES_DATA1);
+
+            // Assert we can rename the file and ensure the file has the same content
+            assertCanRenameFile(otherAppVideoFile, videoFile);
+            assertFileContent(videoFile, BYTES_DATA1);
+            // We can even move it to the top level directory
+            assertCanRenameFile(videoFile, topLevelVideoFile);
+            assertFileContent(topLevelVideoFile, BYTES_DATA1);
+            // And we can even convert it into an image file, because why not?
+            assertCanRenameFile(topLevelVideoFile, imageFile);
+            assertFileContent(imageFile, BYTES_DATA1);
+
+            // We can convert it to a music file, but we won't have access to music file after
+            // renaming.
+            assertThat(imageFile.renameTo(musicFile)).isTrue();
+            assertThat(getFileRowIdFromDatabase(musicFile)).isEqualTo(-1);
+        } finally {
+            deleteFileAsNoThrow(TEST_APP_A, otherAppVideoFile.getAbsolutePath());
+            uninstallApp(TEST_APP_A);
+            imageFile.delete();
+            videoFile.delete();
+            topLevelVideoFile.delete();
+            executeShellCommand("rm  " + musicFile.getAbsolutePath());
+            denyAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
+        }
+    }
+
+    /**
+     * Test that basic file path restrictions are enforced on file rename.
+     */
+    @Test
+    public void testRenameFile() throws Exception {
+        final File nonMediaDir = new File(DOWNLOAD_DIR, TEST_DIRECTORY_NAME);
+        final File pdfFile1 = new File(DOWNLOAD_DIR, NONMEDIA_FILE_NAME);
+        final File pdfFile2 = new File(nonMediaDir, NONMEDIA_FILE_NAME);
+        final File videoFile1 = new File(DCIM_DIR, VIDEO_FILE_NAME);
+        final File videoFile2 = new File(MOVIES_DIR, VIDEO_FILE_NAME);
+        final File videoFile3 = new File(DOWNLOAD_DIR, VIDEO_FILE_NAME);
+
+        try {
+            // Renaming non media file to media directory is not allowed.
+            assertThat(pdfFile1.createNewFile()).isTrue();
+            assertCantRenameFile(pdfFile1, new File(DCIM_DIR, NONMEDIA_FILE_NAME));
+            assertCantRenameFile(pdfFile1, new File(MUSIC_DIR, NONMEDIA_FILE_NAME));
+            assertCantRenameFile(pdfFile1, new File(MOVIES_DIR, NONMEDIA_FILE_NAME));
+
+            // Renaming non media files to non media directories is allowed.
+            if (!nonMediaDir.exists()) {
+                assertThat(nonMediaDir.mkdirs()).isTrue();
+            }
+            // App can rename pdfFile to non media directory.
+            assertCanRenameFile(pdfFile1, pdfFile2);
+
+            assertThat(videoFile1.createNewFile()).isTrue();
+            // App can rename video file to Movies directory
+            assertCanRenameFile(videoFile1, videoFile2);
+            // App can rename video file to Download directory
+            assertCanRenameFile(videoFile2, videoFile3);
+        } finally {
+            pdfFile1.delete();
+            pdfFile2.delete();
+            videoFile1.delete();
+            videoFile2.delete();
+            videoFile3.delete();
+            nonMediaDir.delete();
+        }
+    }
+
+    /**
+     * Test that renaming file to different mime type is allowed.
+     */
+    @Test
+    public void testRenameFileType() throws Exception {
+        final File pdfFile = new File(DOWNLOAD_DIR, NONMEDIA_FILE_NAME);
+        final File videoFile = new File(DCIM_DIR, VIDEO_FILE_NAME);
+        try {
+            assertThat(pdfFile.createNewFile()).isTrue();
+            assertThat(videoFile.exists()).isFalse();
+            // Moving pdfFile to DCIM directory is not allowed.
+            assertCantRenameFile(pdfFile, new File(DCIM_DIR, NONMEDIA_FILE_NAME));
+            // However, moving pdfFile to DCIM directory with changing the mime type to video is
+            // allowed.
+            assertCanRenameFile(pdfFile, videoFile);
+
+            // On rename, MediaProvider database entry for pdfFile should be updated with new
+            // videoFile path and mime type should be updated to video/mp4.
+            assertThat(getFileMimeTypeFromDatabase(videoFile)).isEqualTo("video/mp4");
+        } finally {
+            pdfFile.delete();
+            videoFile.delete();
+        }
+    }
+
+    /**
+     * Test that renaming files overwrites files in newPath.
+     */
+    @Test
+    public void testRenameAndReplaceFile() throws Exception {
+        final File videoFile1 = new File(DCIM_DIR, VIDEO_FILE_NAME);
+        final File videoFile2 = new File(MOVIES_DIR, VIDEO_FILE_NAME);
+        final ContentResolver cr = getContentResolver();
+        try {
+            assertThat(videoFile1.createNewFile()).isTrue();
+            assertThat(videoFile2.createNewFile()).isTrue();
+            final Uri uriVideoFile1 = MediaStore.scanFile(cr, videoFile1);
+            final Uri uriVideoFile2 = MediaStore.scanFile(cr, videoFile2);
+
+            // Renaming a file which replaces file in newPath videoFile2 is allowed.
+            assertCanRenameFile(videoFile1, videoFile2);
+
+            // Uri of videoFile2 should be accessible after rename.
+            assertThat(cr.openFileDescriptor(uriVideoFile2, "rw")).isNotNull();
+            // Uri of videoFile1 should not be accessible after rename.
+            assertThrows(FileNotFoundException.class,
+                    () -> { cr.openFileDescriptor(uriVideoFile1, "rw"); });
+        } finally {
+            videoFile1.delete();
+            videoFile2.delete();
+        }
+    }
+
+    /**
+     * Test that app without write permission for file can't update the file.
+     */
+    @Test
+    public void testRenameFileNotOwned() throws Exception {
+        final File videoFile1 = new File(DCIM_DIR, VIDEO_FILE_NAME);
+        final File videoFile2 = new File(MOVIES_DIR, VIDEO_FILE_NAME);
+        try {
+            installApp(TEST_APP_A);
+            assertThat(createFileAs(TEST_APP_A, videoFile1.getAbsolutePath())).isTrue();
+            // App can't rename a file owned by TEST_APP_A.
+            assertCantRenameFile(videoFile1, videoFile2);
+
+            assertThat(videoFile2.createNewFile()).isTrue();
+            // App can't rename a file to videoFile1 which is owned by TEST_APP_A
+            assertCantRenameFile(videoFile2, videoFile1);
+            // TODO(b/146346138): Test that app with right URI permission should be able to rename
+            // the corresponding file
+        } finally {
+            deleteFileAsNoThrow(TEST_APP_A, videoFile1.getAbsolutePath());
+            videoFile2.delete();
+            uninstallAppNoThrow(TEST_APP_A);
+        }
+    }
+
+    /**
+     * Test that renaming directories is allowed and aligns to default directory restrictions.
+     */
+    @Test
+    public void testRenameDirectory() throws Exception {
+        final String nonMediaDirectoryName = TEST_DIRECTORY_NAME + "NonMedia";
+        final File nonMediaDirectory = new File(DOWNLOAD_DIR, nonMediaDirectoryName);
+        final File pdfFile = new File(nonMediaDirectory, NONMEDIA_FILE_NAME);
+
+        final String mediaDirectoryName = TEST_DIRECTORY_NAME + "Media";
+        final File mediaDirectory1 = new File(DCIM_DIR, mediaDirectoryName);
+        final File videoFile1 = new File(mediaDirectory1, VIDEO_FILE_NAME);
+        final File mediaDirectory2 = new File(DOWNLOAD_DIR, mediaDirectoryName);
+        final File videoFile2 = new File(mediaDirectory2, VIDEO_FILE_NAME);
+        final File mediaDirectory3 = new File(MOVIES_DIR, TEST_DIRECTORY_NAME);
+        final File videoFile3 = new File(mediaDirectory3, VIDEO_FILE_NAME);
+        final File mediaDirectory4 = new File(mediaDirectory3, mediaDirectoryName);
+
+        try {
+            if (!nonMediaDirectory.exists()) {
+                assertThat(nonMediaDirectory.mkdirs()).isTrue();
+            }
+            assertThat(pdfFile.createNewFile()).isTrue();
+            // Move directory with pdf file to DCIM directory is not allowed.
+            assertThat(nonMediaDirectory.renameTo(new File(DCIM_DIR, nonMediaDirectoryName)))
+                    .isFalse();
+
+            if (!mediaDirectory1.exists()) {
+                assertThat(mediaDirectory1.mkdirs()).isTrue();
+            }
+            assertThat(videoFile1.createNewFile()).isTrue();
+            // Renaming to and from default directories is not allowed.
+            assertThat(mediaDirectory1.renameTo(DCIM_DIR)).isFalse();
+            // Moving top level default directories is not allowed.
+            assertCantRenameDirectory(DOWNLOAD_DIR, new File(DCIM_DIR, TEST_DIRECTORY_NAME), null);
+
+            // Moving media directory to Download directory is allowed.
+            assertCanRenameDirectory(mediaDirectory1, mediaDirectory2, new File[] {videoFile1},
+                    new File[] {videoFile2});
+
+            // Moving media directory to Movies directory and renaming directory in new path is
+            // allowed.
+            assertCanRenameDirectory(mediaDirectory2, mediaDirectory3, new File[] {videoFile2},
+                    new File[] {videoFile3});
+
+            // Can't rename a mediaDirectory to non empty non Media directory.
+            assertCantRenameDirectory(mediaDirectory3, nonMediaDirectory, new File[] {videoFile3});
+            // Can't rename a file to a directory.
+            assertCantRenameFile(videoFile3, mediaDirectory3);
+            // Can't rename a directory to file.
+            assertCantRenameDirectory(mediaDirectory3, pdfFile, null);
+            if (!mediaDirectory4.exists()) {
+                assertThat(mediaDirectory4.mkdir()).isTrue();
+            }
+            // Can't rename a directory to subdirectory of itself.
+            assertCantRenameDirectory(mediaDirectory3, mediaDirectory4, new File[] {videoFile3});
+
+        } finally {
+            pdfFile.delete();
+            nonMediaDirectory.delete();
+
+            videoFile1.delete();
+            videoFile2.delete();
+            videoFile3.delete();
+            mediaDirectory1.delete();
+            mediaDirectory2.delete();
+            mediaDirectory3.delete();
+            mediaDirectory4.delete();
+        }
+    }
+
+    /**
+     * Test that renaming directory checks file ownership permissions.
+     */
+    @Test
+    public void testRenameDirectoryNotOwned() throws Exception {
+        final String mediaDirectoryName = TEST_DIRECTORY_NAME + "Media";
+        File mediaDirectory1 = new File(DCIM_DIR, mediaDirectoryName);
+        File mediaDirectory2 = new File(MOVIES_DIR, mediaDirectoryName);
+        File videoFile = new File(mediaDirectory1, VIDEO_FILE_NAME);
+
+        try {
+            installApp(TEST_APP_A);
+
+            if (!mediaDirectory1.exists()) {
+                assertThat(mediaDirectory1.mkdirs()).isTrue();
+            }
+            assertThat(createFileAs(TEST_APP_A, videoFile.getAbsolutePath())).isTrue();
+            // App doesn't have access to videoFile1, can't rename mediaDirectory1.
+            assertThat(mediaDirectory1.renameTo(mediaDirectory2)).isFalse();
+            assertThat(videoFile.exists()).isTrue();
+            // Test app can delete the file since the file is not moved to new directory.
+            assertThat(deleteFileAs(TEST_APP_A, videoFile.getAbsolutePath())).isTrue();
+        } finally {
+            deleteFileAsNoThrow(TEST_APP_A, videoFile.getAbsolutePath());
+            uninstallAppNoThrow(TEST_APP_A);
+            mediaDirectory1.delete();
+        }
+    }
+
+    /**
+     * Test renaming empty directory is allowed
+     */
+    @Test
+    public void testRenameEmptyDirectory() throws Exception {
+        final String emptyDirectoryName = TEST_DIRECTORY_NAME + "Media";
+        File emptyDirectoryOldPath = new File(DCIM_DIR, emptyDirectoryName);
+        File emptyDirectoryNewPath = new File(MOVIES_DIR, TEST_DIRECTORY_NAME);
+        try {
+            if (emptyDirectoryOldPath.exists()) {
+                executeShellCommand("rm -r " + emptyDirectoryOldPath.getPath());
+            }
+            assertThat(emptyDirectoryOldPath.mkdirs()).isTrue();
+            assertCanRenameDirectory(emptyDirectoryOldPath, emptyDirectoryNewPath, null, null);
+        } finally {
+            emptyDirectoryOldPath.delete();
+            emptyDirectoryNewPath.delete();
+        }
+    }
+
+    @Test
+    public void testManageExternalStorageCanCreateFilesAnywhere() throws Exception {
+        final File topLevelPdf = new File(EXTERNAL_STORAGE_DIR, NONMEDIA_FILE_NAME);
+        final File musicFileInMovies = new File(MOVIES_DIR, AUDIO_FILE_NAME);
+        final File imageFileInDcim = new File(DCIM_DIR, IMAGE_FILE_NAME);
+        try {
+            allowAppOpsToUid(Process.myUid(), OPSTR_MANAGE_EXTERNAL_STORAGE);
+            // Nothing special about this, anyone can create an image file in DCIM
+            assertCanCreateFile(imageFileInDcim);
+            // This is where we see the special powers of MANAGE_EXTERNAL_STORAGE, because it can
+            // create a top level file
+            assertCanCreateFile(topLevelPdf);
+            // It can even create a music file in Pictures
+            assertCanCreateFile(musicFileInMovies);
+        } finally {
+            denyAppOpsToUid(Process.myUid(), OPSTR_MANAGE_EXTERNAL_STORAGE);
+        }
+    }
+
+    /**
+     * Test that apps can create and delete hidden file.
+     */
+    @Test
+    public void testCanCreateHiddenFile() throws Exception {
+        final File hiddenImageFile = new File(DOWNLOAD_DIR, ".hiddenFile" + IMAGE_FILE_NAME);
+        try {
+            assertThat(hiddenImageFile.createNewFile()).isTrue();
+            // Write to hidden file is allowed.
+            try (final FileOutputStream fos = new FileOutputStream(hiddenImageFile)) {
+                fos.write(BYTES_DATA1);
+            }
+            assertFileContent(hiddenImageFile, BYTES_DATA1);
+
+            assertNotMediaTypeImage(hiddenImageFile);
+
+            assertDirectoryContains(DOWNLOAD_DIR, hiddenImageFile);
+            assertThat(getFileRowIdFromDatabase(hiddenImageFile)).isNotEqualTo(-1);
+
+            // We can delete hidden file
+            assertThat(hiddenImageFile.delete()).isTrue();
+            assertThat(hiddenImageFile.exists()).isFalse();
+        } finally {
+            hiddenImageFile.delete();
+        }
+    }
+
+    /**
+     * Test that apps can rename a hidden file.
+     */
+    @Test
+    public void testCanRenameHiddenFile() throws Exception {
+        final String hiddenFileName = ".hidden" + IMAGE_FILE_NAME;
+        final File hiddenImageFile1 = new File(DCIM_DIR, hiddenFileName);
+        final File hiddenImageFile2 = new File(DOWNLOAD_DIR, hiddenFileName);
+        final File imageFile = new File(DOWNLOAD_DIR, IMAGE_FILE_NAME);
+        try {
+            assertThat(hiddenImageFile1.createNewFile()).isTrue();
+            assertCanRenameFile(hiddenImageFile1, hiddenImageFile2);
+            assertNotMediaTypeImage(hiddenImageFile2);
+
+            // We can also rename hidden file to non-hidden
+            assertCanRenameFile(hiddenImageFile2, imageFile);
+            assertIsMediaTypeImage(imageFile);
+
+            // We can rename non-hidden file to hidden
+            assertCanRenameFile(imageFile, hiddenImageFile1);
+            assertNotMediaTypeImage(hiddenImageFile1);
+        } finally {
+            hiddenImageFile1.delete();
+            hiddenImageFile2.delete();
+            imageFile.delete();
+        }
+    }
+
+    /**
+     * Test that files in hidden directory have MEDIA_TYPE=MEDIA_TYPE_NONE
+     */
+    @Test
+    public void testHiddenDirectory() throws Exception {
+        final File hiddenDir = new File(DOWNLOAD_DIR, ".hidden" + TEST_DIRECTORY_NAME);
+        final File hiddenImageFile = new File(hiddenDir, IMAGE_FILE_NAME);
+        final File nonHiddenDir = new File(DOWNLOAD_DIR, TEST_DIRECTORY_NAME);
+        final File imageFile = new File(nonHiddenDir, IMAGE_FILE_NAME);
+        try {
+            if (!hiddenDir.exists()) {
+                assertThat(hiddenDir.mkdir()).isTrue();
+            }
+            assertThat(hiddenImageFile.createNewFile()).isTrue();
+
+            assertNotMediaTypeImage(hiddenImageFile);
+
+            // Renaming hiddenDir to nonHiddenDir makes the imageFile non-hidden and vice versa
+            assertCanRenameDirectory(
+                    hiddenDir, nonHiddenDir, new File[] {hiddenImageFile}, new File[] {imageFile});
+            assertIsMediaTypeImage(imageFile);
+
+            assertCanRenameDirectory(
+                    nonHiddenDir, hiddenDir, new File[] {imageFile}, new File[] {hiddenImageFile});
+            assertNotMediaTypeImage(hiddenImageFile);
+        } finally {
+            hiddenImageFile.delete();
+            imageFile.delete();
+            hiddenDir.delete();
+            nonHiddenDir.delete();
+        }
+    }
+
+    /**
+     * Test that files in directory with nomedia have MEDIA_TYPE=MEDIA_TYPE_NONE
+     */
+    @Test
+    public void testHiddenDirectory_nomedia() throws Exception {
+        final File directoryNoMedia = new File(DOWNLOAD_DIR, "nomedia" + TEST_DIRECTORY_NAME);
+        final File noMediaFile = new File(directoryNoMedia, ".nomedia");
+        final File imageFile = new File(directoryNoMedia, IMAGE_FILE_NAME);
+        final File videoFile = new File(directoryNoMedia, VIDEO_FILE_NAME);
+        try {
+            if (!directoryNoMedia.exists()) {
+                assertThat(directoryNoMedia.mkdir()).isTrue();
+            }
+            assertThat(noMediaFile.createNewFile()).isTrue();
+            assertThat(imageFile.createNewFile()).isTrue();
+
+            assertNotMediaTypeImage(imageFile);
+
+            // Deleting the .nomedia file makes the parent directory non hidden.
+            noMediaFile.delete();
+            MediaStore.scanFile(getContentResolver(), directoryNoMedia);
+            assertIsMediaTypeImage(imageFile);
+
+            // Creating the .nomedia file makes the parent directory hidden again
+            assertThat(noMediaFile.createNewFile()).isTrue();
+            MediaStore.scanFile(getContentResolver(), directoryNoMedia);
+            assertNotMediaTypeImage(imageFile);
+
+            // Renaming the .nomedia file to non hidden file makes the parent directory non hidden.
+            assertCanRenameFile(noMediaFile, videoFile);
+            assertIsMediaTypeImage(imageFile);
+        } finally {
+            noMediaFile.delete();
+            imageFile.delete();
+            videoFile.delete();
+            directoryNoMedia.delete();
+        }
+    }
+
+    /**
+     * Test that only file manager and app that created the hidden file can list it.
+     */
+    @Test
+    public void testListHiddenFile() throws Exception {
+        final String hiddenImageFileName = ".hidden" + IMAGE_FILE_NAME;
+        final File hiddenImageFile = new File(DCIM_DIR, hiddenImageFileName);
+        try {
+            assertThat(hiddenImageFile.createNewFile()).isTrue();
+            assertNotMediaTypeImage(hiddenImageFile);
+
+            assertDirectoryContains(DCIM_DIR, hiddenImageFile);
+
+            installApp(TEST_APP_A, true);
+            // TestApp with read permissions can't see the hidden image file created by other app
+            assertThat(listAs(TEST_APP_A, DCIM_DIR.getAbsolutePath()))
+                    .doesNotContain(hiddenImageFileName);
+
+            final int testAppUid =
+                    getContext().getPackageManager().getPackageUid(TEST_APP_A.getPackageName(), 0);
+            // FileManager can see the hidden image file created by other app
+            try {
+                allowAppOpsToUid(testAppUid, OPSTR_MANAGE_EXTERNAL_STORAGE);
+                assertThat(listAs(TEST_APP_A, DCIM_DIR.getAbsolutePath()))
+                        .contains(hiddenImageFileName);
+            } finally {
+                denyAppOpsToUid(testAppUid, OPSTR_MANAGE_EXTERNAL_STORAGE);
+            }
+
+            // Gallery can not see the hidden image file created by other app
+            try {
+                allowAppOpsToUid(testAppUid, SYSTEM_GALERY_APPOPS);
+                assertThat(listAs(TEST_APP_A, DCIM_DIR.getAbsolutePath()))
+                        .doesNotContain(hiddenImageFileName);
+            } finally {
+                denyAppOpsToUid(testAppUid, SYSTEM_GALERY_APPOPS);
+            }
+        } finally {
+            hiddenImageFile.delete();
+            uninstallAppNoThrow(TEST_APP_A);
+        }
+    }
+
+    @Test
+    public void testManageExternalStorageCanDeleteOtherAppsContents() throws Exception {
+        final File otherAppPdf = new File(DOWNLOAD_DIR, "other" + NONMEDIA_FILE_NAME);
+        final File otherAppImage = new File(DCIM_DIR, "other" + IMAGE_FILE_NAME);
+        final File otherAppMusic = new File(MUSIC_DIR, "other" + AUDIO_FILE_NAME);
+        try {
+            installApp(TEST_APP_A);
+
+            // Create all of the files as another app
+            assertThat(createFileAs(TEST_APP_A, otherAppPdf.getPath())).isTrue();
+            assertThat(createFileAs(TEST_APP_A, otherAppImage.getPath())).isTrue();
+            assertThat(createFileAs(TEST_APP_A, otherAppMusic.getPath())).isTrue();
+
+            allowAppOpsToUid(Process.myUid(), OPSTR_MANAGE_EXTERNAL_STORAGE);
+
+            assertThat(otherAppPdf.delete()).isTrue();
+            assertThat(otherAppPdf.exists()).isFalse();
+
+            assertThat(otherAppImage.delete()).isTrue();
+            assertThat(otherAppImage.exists()).isFalse();
+
+            assertThat(otherAppMusic.delete()).isTrue();
+            assertThat(otherAppMusic.exists()).isFalse();
+        } finally {
+            denyAppOpsToUid(Process.myUid(), OPSTR_MANAGE_EXTERNAL_STORAGE);
+            deleteFileAsNoThrow(TEST_APP_A, otherAppPdf.getAbsolutePath());
+            deleteFileAsNoThrow(TEST_APP_A, otherAppImage.getAbsolutePath());
+            deleteFileAsNoThrow(TEST_APP_A, otherAppMusic.getAbsolutePath());
+            uninstallApp(TEST_APP_A);
+        }
+    }
+
+    @Test
+    public void testAccess_file() throws Exception {
+        pollForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, /*granted*/ true);
+
+        final File otherAppPdf = new File(DOWNLOAD_DIR, "other-" + NONMEDIA_FILE_NAME);
+        final File otherAppImage = new File(DCIM_DIR, "other-" + IMAGE_FILE_NAME);
+        final File myAppPdf = new File(DOWNLOAD_DIR, "my-" + NONMEDIA_FILE_NAME);
+        final File doesntExistPdf = new File(DOWNLOAD_DIR, "nada-" + NONMEDIA_FILE_NAME);
+
+        try {
+            installApp(TEST_APP_A);
+
+            assertThat(createFileAs(TEST_APP_A, otherAppPdf.getPath())).isTrue();
+            assertThat(createFileAs(TEST_APP_A, otherAppImage.getPath())).isTrue();
+
+            // We can read our image and pdf files.
+            assertThat(myAppPdf.createNewFile()).isTrue();
+            assertFileAccess_readWrite(myAppPdf);
+
+            // We can read the other app's image file because we hold R_E_S, but we can only
+            // check exists for the pdf file.
+            assertFileAccess_readOnly(otherAppImage);
+            assertFileAccess_existsOnly(otherAppPdf);
+            assertAccess(doesntExistPdf, false, false, false);
+        } finally {
+            deleteFileAsNoThrow(TEST_APP_A, otherAppPdf.getAbsolutePath());
+            deleteFileAsNoThrow(TEST_APP_A, otherAppImage.getAbsolutePath());
+            myAppPdf.delete();
+            uninstallApp(TEST_APP_A);
+        }
+    }
+
+    @Test
+    public void testAccess_directory() throws Exception {
+        pollForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, /*granted*/ true);
+        pollForPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, /*granted*/ true);
+        try {
+            installApp(TEST_APP_A);
+
+            // Let app A create a file in its data dir
+            final File otherAppExternalDataDir = new File(EXTERNAL_FILES_DIR.getPath().replace(
+                    THIS_PACKAGE_NAME, TEST_APP_A.getPackageName()));
+            final File otherAppExternalDataSubDir = new File(otherAppExternalDataDir, "subdir");
+            final File otherAppExternalDataFile = new File(otherAppExternalDataSubDir, "abc.jpg");
+            assertThat(createFileAs(TEST_APP_A, otherAppExternalDataFile.getAbsolutePath()))
+                    .isTrue();
+
+            // TODO(152645823): Readd app data dir testss
+            //            // We cannot read or write the file, but app A can.
+            //            assertThat(canReadAndWriteAs(TEST_APP_A,
+            //                    otherAppExternalDataFile.getAbsolutePath())).isTrue();
+            //            assertAccess(otherAppExternalDataFile, true, false, false);
+            //
+            //            // We cannot read or write the dir, but app A can.
+            //            assertThat(canReadAndWriteAs(TEST_APP_A,
+            //                    otherAppExternalDataDir.getAbsolutePath())).isTrue();
+            //            assertAccess(otherAppExternalDataDir, true, false, false);
+            //
+            //            // We cannot read or write the sub dir, but app A can.
+            //            assertThat(canReadAndWriteAs(TEST_APP_A,
+            //                    otherAppExternalDataSubDir.getAbsolutePath())).isTrue();
+            //            assertAccess(otherAppExternalDataSubDir, true, false, false);
+            //
+            //            // We can read and write our own app dir, but app A cannot.
+            //            assertThat(canReadAndWriteAs(TEST_APP_A,
+            //                    EXTERNAL_FILES_DIR.getAbsolutePath())).isFalse();
+            assertAccess(EXTERNAL_FILES_DIR, true, true, true);
+
+            assertDirectoryAccess(DCIM_DIR, /* exists */ true);
+            assertDirectoryAccess(EXTERNAL_STORAGE_DIR, true);
+            assertDirectoryAccess(new File(EXTERNAL_STORAGE_DIR, "Android"), true);
+            assertDirectoryAccess(new File(EXTERNAL_STORAGE_DIR, "doesnt/exist"), false);
+        } finally {
+            uninstallApp(TEST_APP_A); // Uninstalling deletes external app dirs
+        }
+    }
+
+    @Test
+    public void testManageExternalStorageCanRenameOtherAppsContents() throws Exception {
+        final File otherAppPdf = new File(DOWNLOAD_DIR, "other" + NONMEDIA_FILE_NAME);
+        final File pdf = new File(DOWNLOAD_DIR, NONMEDIA_FILE_NAME);
+        final File pdfInObviouslyWrongPlace = new File(PICTURES_DIR, NONMEDIA_FILE_NAME);
+        final File topLevelPdf = new File(EXTERNAL_STORAGE_DIR, NONMEDIA_FILE_NAME);
+        final File musicFile = new File(MUSIC_DIR, AUDIO_FILE_NAME);
+        try {
+            installApp(TEST_APP_A);
+
+            // Have another app create a PDF
+            assertThat(createFileAs(TEST_APP_A, otherAppPdf.getPath())).isTrue();
+            assertThat(otherAppPdf.exists()).isTrue();
+
+            allowAppOpsToUid(Process.myUid(), OPSTR_MANAGE_EXTERNAL_STORAGE);
+
+            // Write some data to the file
+            try (final FileOutputStream fos = new FileOutputStream(otherAppPdf)) {
+                fos.write(BYTES_DATA1);
+            }
+            assertFileContent(otherAppPdf, BYTES_DATA1);
+
+            // Assert we can rename the file and ensure the file has the same content
+            assertCanRenameFile(otherAppPdf, pdf);
+            assertFileContent(pdf, BYTES_DATA1);
+            // We can even move it to the top level directory
+            assertCanRenameFile(pdf, topLevelPdf);
+            assertFileContent(topLevelPdf, BYTES_DATA1);
+            // And even rename to a place where PDFs don't belong, because we're an omnipotent
+            // external storage manager
+            assertCanRenameFile(topLevelPdf, pdfInObviouslyWrongPlace);
+            assertFileContent(pdfInObviouslyWrongPlace, BYTES_DATA1);
+
+            // And we can even convert it into a music file, because why not?
+            assertCanRenameFile(pdfInObviouslyWrongPlace, musicFile);
+            assertFileContent(musicFile, BYTES_DATA1);
+        } finally {
+            pdf.delete();
+            pdfInObviouslyWrongPlace.delete();
+            topLevelPdf.delete();
+            musicFile.delete();
+            denyAppOpsToUid(Process.myUid(), OPSTR_MANAGE_EXTERNAL_STORAGE);
+            deleteFileAsNoThrow(TEST_APP_A, otherAppPdf.getAbsolutePath());
+            uninstallApp(TEST_APP_A);
+        }
+    }
+
+    @Test
+    public void testCanCreateDefaultDirectory() throws Exception {
+        try {
+            if (PODCASTS_DIR.exists()) {
+                // Apps can't delete top level directories, not even default directories, so we let
+                // shell do the deed for us.
+                executeShellCommand("rm -r " + PODCASTS_DIR);
+            }
+            assertThat(PODCASTS_DIR.mkdir()).isTrue();
+        } finally {
+            executeShellCommand("mkdir " + PODCASTS_DIR);
+        }
+    }
+
+    @Test
+    public void testManageExternalStorageReaddir() throws Exception {
+        final File otherAppPdf = new File(DOWNLOAD_DIR, "other" + NONMEDIA_FILE_NAME);
+        final File otherAppImg = new File(DCIM_DIR, "other" + IMAGE_FILE_NAME);
+        final File otherAppMusic = new File(MUSIC_DIR, "other" + AUDIO_FILE_NAME);
+        final File otherTopLevelFile = new File(EXTERNAL_STORAGE_DIR, "other" + NONMEDIA_FILE_NAME);
+        try {
+            installApp(TEST_APP_A);
+            assertCreateFilesAs(TEST_APP_A, otherAppImg, otherAppMusic, otherAppPdf);
+            executeShellCommand("touch " + otherTopLevelFile);
+
+            allowAppOpsToUid(Process.myUid(), OPSTR_MANAGE_EXTERNAL_STORAGE);
+
+            // We can list other apps' files
+            assertDirectoryContains(otherAppPdf.getParentFile(), otherAppPdf);
+            assertDirectoryContains(otherAppImg.getParentFile(), otherAppImg);
+            assertDirectoryContains(otherAppMusic.getParentFile(), otherAppMusic);
+            // We can list top level files
+            assertDirectoryContains(EXTERNAL_STORAGE_DIR, otherTopLevelFile);
+
+            // We can also list all top level directories
+            assertDirectoryContains(EXTERNAL_STORAGE_DIR, DEFAULT_TOP_LEVEL_DIRS);
+        } finally {
+            denyAppOpsToUid(Process.myUid(), OPSTR_MANAGE_EXTERNAL_STORAGE);
+            executeShellCommand("rm " + otherTopLevelFile);
+            deleteFilesAs(TEST_APP_A, otherAppImg, otherAppMusic, otherAppPdf);
+            uninstallApp(TEST_APP_A);
+        }
+    }
+
+    @Test
+    public void testManageExternalStorageQueryOtherAppsFile() throws Exception {
+        final File otherAppPdf = new File(DOWNLOAD_DIR, "other" + NONMEDIA_FILE_NAME);
+        final File otherAppImg = new File(DCIM_DIR, "other" + IMAGE_FILE_NAME);
+        final File otherAppMusic = new File(MUSIC_DIR, "other" + AUDIO_FILE_NAME);
+        final File otherHiddenFile = new File(PICTURES_DIR, ".otherHiddenFile.jpg");
+        try {
+            installApp(TEST_APP_A);
+            assertCreateFilesAs(
+                    TEST_APP_A, otherAppImg, otherAppMusic, otherAppPdf, otherHiddenFile);
+
+            // Once the test has permission to manage external storage, it can query for other
+            // apps' files and open them for read and write
+            allowAppOpsToUid(Process.myUid(), OPSTR_MANAGE_EXTERNAL_STORAGE);
+
+            assertCanQueryAndOpenFile(otherAppPdf, "rw");
+            assertCanQueryAndOpenFile(otherAppImg, "rw");
+            assertCanQueryAndOpenFile(otherAppMusic, "rw");
+            assertCanQueryAndOpenFile(otherHiddenFile, "rw");
+        } finally {
+            denyAppOpsToUid(Process.myUid(), OPSTR_MANAGE_EXTERNAL_STORAGE);
+            deleteFilesAs(TEST_APP_A, otherAppImg, otherAppMusic, otherAppPdf, otherHiddenFile);
+            uninstallApp(TEST_APP_A);
+        }
+    }
+
+    @Test
+    public void testQueryOtherAppsFiles() throws Exception {
+        final File otherAppPdf = new File(DOWNLOAD_DIR, "other" + NONMEDIA_FILE_NAME);
+        final File otherAppImg = new File(DCIM_DIR, "other" + IMAGE_FILE_NAME);
+        final File otherAppMusic = new File(MUSIC_DIR, "other" + AUDIO_FILE_NAME);
+        final File otherHiddenFile = new File(PICTURES_DIR, ".otherHiddenFile.jpg");
+        try {
+            installApp(TEST_APP_A);
+            assertCreateFilesAs(
+                    TEST_APP_A, otherAppImg, otherAppMusic, otherAppPdf, otherHiddenFile);
+
+            // Since the test doesn't have READ_EXTERNAL_STORAGE nor any other special permissions,
+            // it can't query for another app's contents.
+            assertCantQueryFile(otherAppImg);
+            assertCantQueryFile(otherAppMusic);
+            assertCantQueryFile(otherAppPdf);
+            assertCantQueryFile(otherHiddenFile);
+        } finally {
+            deleteFilesAs(TEST_APP_A, otherAppImg, otherAppMusic, otherAppPdf, otherHiddenFile);
+            uninstallApp(TEST_APP_A);
+        }
+    }
+
+    @Test
+    public void testSystemGalleryQueryOtherAppsFiles() throws Exception {
+        final File otherAppPdf = new File(DOWNLOAD_DIR, "other" + NONMEDIA_FILE_NAME);
+        final File otherAppImg = new File(DCIM_DIR, "other" + IMAGE_FILE_NAME);
+        final File otherAppMusic = new File(MUSIC_DIR, "other" + AUDIO_FILE_NAME);
+        final File otherHiddenFile = new File(PICTURES_DIR, ".otherHiddenFile.jpg");
+        try {
+            installApp(TEST_APP_A);
+            assertCreateFilesAs(
+                    TEST_APP_A, otherAppImg, otherAppMusic, otherAppPdf, otherHiddenFile);
+
+            // System gallery apps have access to video and image files
+            allowAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
+
+            assertCanQueryAndOpenFile(otherAppImg, "rw");
+            // System gallery doesn't have access to hidden image files of other app
+            assertCantQueryFile(otherHiddenFile);
+            // But no access to PDFs or music files
+            assertCantQueryFile(otherAppMusic);
+            assertCantQueryFile(otherAppPdf);
+        } finally {
+            denyAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
+            deleteFilesAs(TEST_APP_A, otherAppImg, otherAppMusic, otherAppPdf, otherHiddenFile);
+            uninstallApp(TEST_APP_A);
+        }
+    }
+
+    /**
+     * Test that System Gallery app can rename any directory under the default directories
+     * designated for images and videos, even if they contain other apps' contents that
+     * System Gallery doesn't have read access to.
+     */
+    @Test
+    public void testSystemGalleryCanRenameImageAndVideoDirs() throws Exception {
+        final File dirInDcim = new File(DCIM_DIR, TEST_DIRECTORY_NAME);
+        final File dirInPictures = new File(PICTURES_DIR, TEST_DIRECTORY_NAME);
+        final File dirInPodcasts = new File(PODCASTS_DIR, TEST_DIRECTORY_NAME);
+        final File otherAppImageFile1 = new File(dirInDcim, "other_" + IMAGE_FILE_NAME);
+        final File otherAppVideoFile1 = new File(dirInDcim, "other_" + VIDEO_FILE_NAME);
+        final File otherAppPdfFile1 = new File(dirInDcim, "other_" + NONMEDIA_FILE_NAME);
+        final File otherAppImageFile2 = new File(dirInPictures, "other_" + IMAGE_FILE_NAME);
+        final File otherAppVideoFile2 = new File(dirInPictures, "other_" + VIDEO_FILE_NAME);
+        final File otherAppPdfFile2 = new File(dirInPictures, "other_" + NONMEDIA_FILE_NAME);
+        try {
+            assertThat(dirInDcim.exists() || dirInDcim.mkdir()).isTrue();
+
+            executeShellCommand("touch " + otherAppPdfFile1);
+
+            installAppWithStoragePermissions(TEST_APP_A);
+            allowAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
+
+            assertCreateFilesAs(TEST_APP_A, otherAppImageFile1, otherAppVideoFile1);
+
+            // System gallery privileges don't go beyond DCIM, Movies and Pictures boundaries.
+            assertCantRenameDirectory(dirInDcim, dirInPodcasts, /*oldFilesList*/ null);
+
+            // Rename should succeed, but System Gallery still can't access that PDF file!
+            assertCanRenameDirectory(dirInDcim, dirInPictures,
+                    new File[] {otherAppImageFile1, otherAppVideoFile1},
+                    new File[] {otherAppImageFile2, otherAppVideoFile2});
+            assertThat(getFileRowIdFromDatabase(otherAppPdfFile1)).isEqualTo(-1);
+            assertThat(getFileRowIdFromDatabase(otherAppPdfFile2)).isEqualTo(-1);
+        } finally {
+            executeShellCommand("rm " + otherAppPdfFile1);
+            executeShellCommand("rm " + otherAppPdfFile2);
+            otherAppImageFile1.delete();
+            otherAppImageFile2.delete();
+            otherAppVideoFile1.delete();
+            otherAppVideoFile2.delete();
+            otherAppPdfFile1.delete();
+            otherAppPdfFile2.delete();
+            dirInDcim.delete();
+            dirInPictures.delete();
+            uninstallAppNoThrow(TEST_APP_A);
+            denyAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
+        }
+    }
+
+    /**
+     * Test that row ID corresponding to deleted path is restored on subsequent create.
+     */
+    @Test
+    public void testCreateCanRestoreDeletedRowId() throws Exception {
+        final File imageFile = new File(DCIM_DIR, IMAGE_FILE_NAME);
+        final ContentResolver cr = getContentResolver();
+
+        try {
+            assertThat(imageFile.createNewFile()).isTrue();
+            final long oldRowId = getFileRowIdFromDatabase(imageFile);
+            assertThat(oldRowId).isNotEqualTo(-1);
+            final Uri uriOfOldFile = MediaStore.scanFile(cr, imageFile);
+            assertThat(uriOfOldFile).isNotNull();
+
+            assertThat(imageFile.delete()).isTrue();
+            // We should restore old row Id corresponding to deleted imageFile.
+            assertThat(imageFile.createNewFile()).isTrue();
+            assertThat(getFileRowIdFromDatabase(imageFile)).isEqualTo(oldRowId);
+            assertThat(cr.openFileDescriptor(uriOfOldFile, "rw")).isNotNull();
+
+            assertThat(imageFile.delete()).isTrue();
+            installApp(TEST_APP_A);
+            assertThat(createFileAs(TEST_APP_A, imageFile.getAbsolutePath())).isTrue();
+
+            final Uri uriOfNewFile = MediaStore.scanFile(getContentResolver(), imageFile);
+            assertThat(uriOfNewFile).isNotNull();
+            // We shouldn't restore deleted row Id if delete & create are called from different apps
+            assertThat(Integer.getInteger(uriOfNewFile.getLastPathSegment())).isNotEqualTo(oldRowId);
+        } finally {
+            imageFile.delete();
+            deleteFileAsNoThrow(TEST_APP_A, imageFile.getAbsolutePath());
+            uninstallAppNoThrow(TEST_APP_A);
+        }
+    }
+
+    /**
+     * Test that row ID corresponding to deleted path is restored on subsequent rename.
+     */
+    @Test
+    public void testRenameCanRestoreDeletedRowId() throws Exception {
+        final File imageFile = new File(DCIM_DIR, IMAGE_FILE_NAME);
+        final File temporaryFile = new File(DOWNLOAD_DIR, IMAGE_FILE_NAME + "_.tmp");
+        final ContentResolver cr = getContentResolver();
+
+        try {
+            assertThat(imageFile.createNewFile()).isTrue();
+            final Uri oldUri = MediaStore.scanFile(cr, imageFile);
+            assertThat(oldUri).isNotNull();
+
+            Files.copy(imageFile, temporaryFile);
+            assertThat(imageFile.delete()).isTrue();
+            assertCanRenameFile(temporaryFile, imageFile);
+
+            final Uri newUri = MediaStore.scanFile(cr, imageFile);
+            assertThat(newUri).isNotNull();
+            assertThat(newUri.getLastPathSegment()).isEqualTo(oldUri.getLastPathSegment());
+            // oldUri of imageFile is still accessible after delete and rename.
+            assertThat(cr.openFileDescriptor(oldUri, "rw")).isNotNull();
+        } finally {
+            imageFile.delete();
+            temporaryFile.delete();
+        }
+    }
+
+    @Test
+    public void testCantCreateOrRenameFileWithInvalidName() throws Exception {
+        File invalidFile = new File(DOWNLOAD_DIR, "<>");
+        File validFile = new File(DOWNLOAD_DIR, NONMEDIA_FILE_NAME);
+        try {
+            assertThrows(IOException.class, "Operation not permitted",
+                    () -> { invalidFile.createNewFile(); });
+
+            assertThat(validFile.createNewFile()).isTrue();
+            // We can't rename a file to a file name with invalid FAT characters.
+            assertCantRenameFile(validFile, invalidFile);
+        } finally {
+            invalidFile.delete();
+            validFile.delete();
+        }
+    }
+
+    private static void assertIsMediaTypeImage(File file) {
+        final Cursor c = queryImageFile(file);
+        assertEquals(1, c.getCount());
+    }
+
+    private static void assertNotMediaTypeImage(File file) {
+        final Cursor c = queryImageFile(file);
+        assertEquals(0, c.getCount());
+    }
+
+    private static void assertCantQueryFile(File file) { assertThat(getFileUri(file)).isNull(); }
+
+    private static void assertCreateFilesAs(TestApp testApp, File... files) throws Exception {
+        for (File file : files) {
+            assertThat(createFileAs(testApp, file.getPath())).isTrue();
+        }
+    }
+
+    private static void deleteFilesAs(TestApp testApp, File... files) throws Exception {
+        for (File file : files) {
+            deleteFileAs(testApp, file.getPath());
+        }
+    }
+
+    /**
+     * For possible values of {@code mode}, look at {@link android.content.ContentProvider#openFile}
+     */
+    private static void assertCanQueryAndOpenFile(File file, String mode) throws IOException {
+        // This call performs the query
+        final Uri fileUri = getFileUri(file);
+        // The query succeeds iff it didn't return null
+        assertThat(fileUri).isNotNull();
+        // Now we assert that we can open the file through ContentResolver
+        try (final ParcelFileDescriptor pfd =
+                        getContentResolver().openFileDescriptor(fileUri, mode)) {
+            assertThat(pfd).isNotNull();
+        }
+    }
+
+    /**
+     * Assert that the last read in: read - write - read using {@code readFd} and {@code writeFd}
+     * see the last write. {@code readFd} and {@code writeFd} are fds pointing to the same
+     * underlying file on disk but may be derived from different mount points and in that case
+     * have separate VFS caches.
+     */
+    private void assertRWR(ParcelFileDescriptor readPfd, ParcelFileDescriptor writePfd)
+            throws Exception {
+        FileDescriptor readFd = readPfd.getFileDescriptor();
+        FileDescriptor writeFd = writePfd.getFileDescriptor();
+
+        byte[] readBuffer = new byte[10];
+        byte[] writeBuffer = new byte[10];
+        Arrays.fill(writeBuffer, (byte) 1);
+
+        // Write so readFd has content to read from next
+        Os.pwrite(readFd, readBuffer, 0, 10, 0);
+        // Read so readBuffer is in readFd's mount VFS cache
+        Os.pread(readFd, readBuffer, 0, 10, 0);
+
+        // Assert that readBuffer is zeroes
+        assertThat(readBuffer).isEqualTo(new byte[10]);
+
+        // Write so writeFd and readFd should now see writeBuffer
+        Os.pwrite(writeFd, writeBuffer, 0, 10, 0);
+
+        // Read so the last write can be verified on readFd
+        Os.pread(readFd, readBuffer, 0, 10, 0);
+
+        // Assert that the last write is indeed visible via readFd
+        assertThat(readBuffer).isEqualTo(writeBuffer);
+        assertThat(readPfd.getStatSize()).isEqualTo(writePfd.getStatSize());
+    }
+
+    private void assertLowerFsFd(ParcelFileDescriptor pfd) throws Exception {
+        assertThat(Os.readlink("/proc/self/fd/" + pfd.getFd()).startsWith("/storage")).isTrue();
+    }
+
+    private void assertUpperFsFd(ParcelFileDescriptor pfd) throws Exception {
+        assertThat(Os.readlink("/proc/self/fd/" + pfd.getFd()).startsWith("/mnt/user")).isTrue();
+    }
+
+    private static void assertCanCreateFile(File file) throws IOException {
+        // If the file somehow managed to survive a previous run, then the test app was uninstalled
+        // and MediaProvider will remove our its ownership of the file, so it's not guaranteed that
+        // we can create nor delete it.
+        if (!file.exists()) {
+            assertThat(file.createNewFile()).isTrue();
+            assertThat(file.delete()).isTrue();
+        } else {
+            Log.w(TAG,
+                    "Couldn't assertCanCreateFile(" + file + ") because file existed prior to "
+                            + "running the test!");
+        }
+    }
+
+    private static void assertFileAccess_existsOnly(File file) throws Exception {
+        assertThat(file.isFile()).isTrue();
+        assertAccess(file, true, false, false);
+    }
+
+    private static void assertFileAccess_readOnly(File file) throws Exception {
+        assertThat(file.isFile()).isTrue();
+        assertAccess(file, true, true, false);
+    }
+
+    private static void assertFileAccess_readWrite(File file) throws Exception {
+        assertThat(file.isFile()).isTrue();
+        assertAccess(file, true, true, true);
+    }
+
+    private static void assertDirectoryAccess(File dir, boolean exists) throws Exception {
+        // This util does not handle app data directories.
+        assumeFalse(dir.getAbsolutePath().startsWith(ANDROID_DIR.getAbsolutePath())
+                && !dir.equals(ANDROID_DIR));
+        assertThat(dir.isDirectory()).isEqualTo(exists);
+        // For non-app data directories, exists => canRead() and canWrite().
+        assertAccess(dir, exists, exists, exists);
+    }
+
+    private static void assertAccess(File file, boolean exists, boolean canRead, boolean canWrite)
+            throws Exception {
+        assertThat(file.exists()).isEqualTo(exists);
+        assertThat(file.canRead()).isEqualTo(canRead);
+        assertThat(file.canWrite()).isEqualTo(canWrite);
+        if (file.isDirectory()) {
+            assertThat(file.canExecute()).isEqualTo(exists);
+        } else {
+            assertThat(file.canExecute()).isFalse(); // Filesytem is mounted with MS_NOEXEC
+        }
+
+        // Test some combinations of mask.
+        assertAccess(file, R_OK, canRead);
+        assertAccess(file, W_OK, canWrite);
+        assertAccess(file, R_OK | W_OK, canRead && canWrite);
+        assertAccess(file, W_OK | F_OK, canWrite);
+        assertAccess(file, F_OK, exists);
+    }
+
+    private static void assertAccess(File file, int mask, boolean expected) throws Exception {
+        if (expected) {
+            assertThat(Os.access(file.getAbsolutePath(), mask)).isTrue();
+        } else {
+            assertThrows(ErrnoException.class, () -> { Os.access(file.getAbsolutePath(), mask); });
+        }
+    }
+}
diff --git a/hostsidetests/security/src/android/security/cts/KernelConfigTest.java b/hostsidetests/security/src/android/security/cts/KernelConfigTest.java
index 9eb5241..f45d9a5 100644
--- a/hostsidetests/security/src/android/security/cts/KernelConfigTest.java
+++ b/hostsidetests/security/src/android/security/cts/KernelConfigTest.java
@@ -294,7 +294,7 @@
         }
 
         int index = configPath.indexOf('=');
-        String path = configPath.substring(index+1);
+        String path = configPath.substring(index+1).replace("\"", "");
 
         assertTrue("Linux kernel must specify an absolute path for static usermodehelper path",
             configPath.contains("..") == false);
diff --git a/hostsidetests/stagedinstall/Android.bp b/hostsidetests/stagedinstall/Android.bp
index 8a73ab9..870c01e 100644
--- a/hostsidetests/stagedinstall/Android.bp
+++ b/hostsidetests/stagedinstall/Android.bp
@@ -28,6 +28,7 @@
 
     data:   [
         ":StagedInstallTest",
+        ":deapexer.zip",
     ],
 
     test_suites: [
@@ -59,6 +60,7 @@
         ":StagedInstallTestApexV2_WithPostInstallHook",
         ":StagedInstallTestApexV2_WithPreInstallHook",
         ":StagedInstallTestApexV2_WrongSha",
+        ":StagedInstallTestApexV2_WithoutApkInApex",
         ":StagedInstallTestAppSamePackageNameAsApex",
         ":StagedInstallTestApexV2_SdkTargetP",
         ":StagedInstallTestApexV2_ApkInApexSdkTargetP",
@@ -346,6 +348,26 @@
 }
 
 prebuilt_apex {
+    name: "StagedInstallTestApexV2_WithoutApkInApex",
+    arch: {
+        arm: {
+              src: "testdata/apex/arm/com.android.apex.cts.shim.v2_without_apk_in_apex.apex",
+        },
+        arm64: {
+              src: "testdata/apex/arm/com.android.apex.cts.shim.v2_without_apk_in_apex.apex",
+        },
+        x86: {
+              src: "testdata/apex/x86/com.android.apex.cts.shim.v2_without_apk_in_apex.apex",
+        },
+        x86_64: {
+              src: "testdata/apex/x86/com.android.apex.cts.shim.v2_without_apk_in_apex.apex",
+        },
+    },
+    filename: "com.android.apex.cts.shim.v2_without_apk_in_apex.apex",
+    installable: false,
+}
+
+prebuilt_apex {
     name: "StagedInstallTestApexV1_NotPreInstalled",
     arch: {
         arm: {
@@ -511,3 +533,20 @@
   filename: "com.android.apex.cts.shim.v2_unsigned_payload.apex",
   installable: false,
 }
+
+// collects deapexer and its dependency modules (libc++ and debugfs_static) to the zip file.
+genrule {
+  name: "deapexer.zip",
+  tools: [
+      "deapexer",
+      "debugfs_static",
+      "soong_zip",
+  ],
+  cmd: "rm -rf mkdir $(genDir)/deapexer && mkdir $(genDir)/deapexer && " +
+       "cp $(location deapexer) $(genDir)/deapexer && " +
+       "cp $(location debugfs_static) $(genDir)/deapexer && " +
+       "HOST_OUT_SHARED_LIBRARIES=$$(dirname $(location deapexer))/../lib64 && " +
+       "cp $${HOST_OUT_SHARED_LIBRARIES}/libc++.* $(genDir)/deapexer && " +
+       "$(location soong_zip) -o $(out) -C $(genDir)/deapexer -D $(genDir)/deapexer",
+  out: ["deapexer.zip"],
+}
diff --git a/hostsidetests/stagedinstall/app/src/com/android/tests/stagedinstall/StagedInstallTest.java b/hostsidetests/stagedinstall/app/src/com/android/tests/stagedinstall/StagedInstallTest.java
index 9e68c8c..1ce2daf 100644
--- a/hostsidetests/stagedinstall/app/src/com/android/tests/stagedinstall/StagedInstallTest.java
+++ b/hostsidetests/stagedinstall/app/src/com/android/tests/stagedinstall/StagedInstallTest.java
@@ -105,6 +105,7 @@
     private static final Duration SLEEP_DURATION = Duration.ofMillis(200);
 
     private static final String SHIM_PACKAGE_NAME = "com.android.apex.cts.shim";
+    private static final String APK_SHIM_PACKAGE_NAME = "com.android.cts.ctsshim";
     private static final String NOT_PREINSTALL_APEX_PACKAGE_NAME =
             "com.android.apex.cts.shim_not_pre_installed";
     private static final String DIFFERENT_APEX_PACKAGE_NAME = "com.android.apex.cts.shim.different";
@@ -133,6 +134,9 @@
     private static final TestApp ApexWrongSha2 = new TestApp(
             "ApexWrongSha2", SHIM_PACKAGE_NAME, 2, /*isApex*/true,
             "com.android.apex.cts.shim.v2_wrong_sha.apex");
+    private static final TestApp Apex2WithoutApkInApex = new TestApp(
+            "Apex2WithoutApkInApex", SHIM_PACKAGE_NAME, 2, /*isApex*/true,
+            "com.android.apex.cts.shim.v2_without_apk_in_apex.apex");
     private static final TestApp Apex3SignedBob = new TestApp(
             "Apex3SignedBob", SHIM_PACKAGE_NAME, 3, /*isApex*/true,
             "com.android.apex.cts.shim.v3_signed_bob.apex");
@@ -558,6 +562,7 @@
         int sessionId = retrieveLastSessionId();
         assertSessionApplied(sessionId);
         assertThat(getInstalledVersion(TestApp.Apex)).isEqualTo(2);
+        assertThat(getInstalledVersion(APK_SHIM_PACKAGE_NAME)).isNotEqualTo(-1);
     }
 
     @Test
@@ -1045,6 +1050,23 @@
                 });
     }
 
+    @Test
+    public void testInstallStagedApex_SameGrade_NewOneWins_Commit() throws Exception {
+        assertThat(getInstalledVersion(TestApp.Apex)).isEqualTo(2);
+        assertThat(getInstalledVersion(APK_SHIM_PACKAGE_NAME)).isNotEqualTo(-1);
+        int sessionId = Install.single(Apex2WithoutApkInApex).setStaged().commit();
+        assertSessionReady(sessionId);
+        storeSessionId(sessionId);
+    }
+
+    @Test
+    public void testInstallStagedApex_SameGrade_NewOneWins_VerifyPostReboot() throws Exception {
+        int sessionId = retrieveLastSessionId();
+        assertSessionApplied(sessionId);
+        assertThat(getInstalledVersion(TestApp.Apex)).isEqualTo(2);
+        assertThat(getInstalledVersion(APK_SHIM_PACKAGE_NAME)).isEqualTo(-1);
+    }
+
     /**
      * Should fail to verify apex targeting older dev sdk
      */
diff --git a/hostsidetests/stagedinstall/src/com/android/tests/stagedinstall/host/ApexShimValidationTest.java b/hostsidetests/stagedinstall/src/com/android/tests/stagedinstall/host/ApexShimValidationTest.java
index c41493b..2034faa 100644
--- a/hostsidetests/stagedinstall/src/com/android/tests/stagedinstall/host/ApexShimValidationTest.java
+++ b/hostsidetests/stagedinstall/src/com/android/tests/stagedinstall/host/ApexShimValidationTest.java
@@ -25,6 +25,12 @@
 
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+import com.android.tradefed.util.AaptParser;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.RunUtil;
+import com.android.tradefed.util.ZipUtil;
 
 import org.hamcrest.CoreMatchers;
 import org.junit.After;
@@ -32,9 +38,15 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Enumeration;
 import java.util.List;
 import java.util.stream.Collectors;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
 
 /**
  * Tests to validate that only what is considered a correct shim apex can be installed.
@@ -53,10 +65,22 @@
 
     private static final String SHIM_APEX_PACKAGE_NAME = "com.android.apex.cts.shim";
     private static final String SHIM_APK_CODE_PATH_PREFIX = "/apex/" + SHIM_APEX_PACKAGE_NAME + "/";
+    private static final String STAGED_INSTALL_TEST_FILE_NAME = "StagedInstallTest.apk";
+    private static final String APEX_FILE_SUFFIX = ".apex";
+    private static final String DEAPEXER_ZIP_FILE_NAME = "deapexer.zip";
+    private static final String DEAPEXING_FOLDER_NAME = "deapexing_";
+    private static final String DEAPEXER_FILE_NAME = "deapexer";
+    private static final String DEBUGFS_STATIC_FILE_NAME = "debugfs_static";
+
+    private static final long DEFAULT_RUN_TIMEOUT_MS = 30 * 1000L;
 
     private static final List<String> ALLOWED_SHIM_PACKAGE_NAMES = Arrays.asList(
             "com.android.cts.ctsshim", "com.android.cts.priv.ctsshim");
 
+    private File mDeapexingDir;
+    private File mDeapexerZip;
+    private File mAllApexesZip;
+
     /**
      * Runs the given phase of a test by calling into the device.
      * Throws an exception if the test phase fails.
@@ -73,6 +97,9 @@
         assertThat(runDeviceTests("com.android.tests.stagedinstall",
                 "com.android.tests.stagedinstall.StagedInstallTest",
                 "cleanUp")).isTrue();
+        if (mDeapexingDir != null) {
+            FileUtil.recursiveDelete(mDeapexingDir);
+        }
     }
 
     @Before
@@ -80,6 +107,9 @@
         final String updatable = getDevice().getProperty("ro.apex.updatable");
         assumeThat("Device doesn't support updating APEX", updatable, CoreMatchers.equalTo("true"));
         cleanUp();
+        mDeapexerZip = getTestInformation().getDependencyFile(DEAPEXER_ZIP_FILE_NAME, false);
+        mAllApexesZip = getTestInformation().getDependencyFile(STAGED_INSTALL_TEST_FILE_NAME,
+                false);
     }
 
     @After
@@ -105,6 +135,37 @@
                 .that(shimPackages).containsExactlyElementsIn(ALLOWED_SHIM_PACKAGE_NAMES);
     }
 
+    /**
+     * Deapexing all the apexes bundled in the staged install test. Verifies the package name of
+     * shim apk in the apex.
+     */
+    @Test
+    public void testPackageNameOfShimApkInAllBundledApexesIsAllowed() throws Exception {
+        mDeapexingDir = FileUtil.createTempDir(DEAPEXING_FOLDER_NAME);
+        final File deapexer = extractDeapexer(mDeapexingDir);
+        final File debugfs = new File(mDeapexingDir, DEBUGFS_STATIC_FILE_NAME);
+        final List<File> apexes = extractApexes(mDeapexingDir);
+        for (File apex : apexes) {
+            final File outDir = new File(apex.getParent(), apex.getName().substring(
+                    0, apex.getName().length() - APEX_FILE_SUFFIX.length()));
+            try {
+                runDeapexerExtract(deapexer, debugfs, apex, outDir);
+                final List<File> apkFiles = FileUtil.findFiles(outDir, ".+\\.apk").stream()
+                        .map(str -> new File(str)).collect(Collectors.toList());
+                for (File apkFile : apkFiles) {
+                    final AaptParser parser = AaptParser.parse(apkFile);
+                    assertWithMessage("Apk " + apkFile + " in apex " + apex + " is not valid")
+                            .that(parser).isNotNull();
+                    assertWithMessage("Apk " + apkFile + " in apex " + apex
+                            + " has incorrect package name " + parser.getPackageName())
+                            .that(ALLOWED_SHIM_PACKAGE_NAMES).contains(parser.getPackageName());
+                }
+            } finally {
+                FileUtil.recursiveDelete(outDir);
+            }
+        }
+    }
+
     @Test
     @LargeTest
     public void testRejectsApexWithAdditionalFile() throws Exception {
@@ -144,4 +205,81 @@
         getDevice().reboot();
         runPhase("testInstallRejected_VerifyPostReboot");
     }
+
+    /**
+     * Extracts {@link #DEAPEXER_ZIP_FILE_NAME} into the destination folder. Updates executable
+     * attribute for the binaries of deapexer and debugfs_static.
+     *
+     * @param destDir A tmp folder for the deapexing.
+     * @return the deapexer file.
+     */
+    private File extractDeapexer(File destDir) throws IOException {
+        ZipUtil.extractZip(new ZipFile(mDeapexerZip), destDir);
+        final File deapexer = FileUtil.findFile(destDir, DEAPEXER_FILE_NAME);
+        assertWithMessage("Can't find " + DEAPEXER_FILE_NAME + " binary file")
+                .that(deapexer).isNotNull();
+        deapexer.setExecutable(true);
+        final File debugfs = FileUtil.findFile(destDir, DEBUGFS_STATIC_FILE_NAME);
+        assertWithMessage("Can't find " + DEBUGFS_STATIC_FILE_NAME + " binary file")
+                .that(debugfs).isNotNull();
+        debugfs.setExecutable(true);
+        return deapexer;
+    }
+
+    /**
+     * Extracts all bundled apex files from {@link #STAGED_INSTALL_TEST_FILE_NAME} into the
+     * destination folder.
+     *
+     * @param destDir A tmp folder for the deapexing.
+     * @return A list of apex files.
+     */
+    private List<File> extractApexes(File destDir) throws IOException {
+        final List<File> apexes = new ArrayList<>();
+        final ZipFile apexZip = new ZipFile(mAllApexesZip);
+        final Enumeration<? extends ZipEntry> entries = apexZip.entries();
+        while (entries.hasMoreElements()) {
+            final ZipEntry entry = entries.nextElement();
+            if (entry.isDirectory() || !entry.getName().matches(
+                    SHIM_APEX_PACKAGE_NAME + ".*\\" + APEX_FILE_SUFFIX)) {
+                continue;
+            }
+            final File apex = new File(destDir, entry.getName());
+            apex.getParentFile().mkdirs();
+            FileUtil.writeToFile(apexZip.getInputStream(entry), apex);
+            apexes.add(apex);
+        }
+        assertWithMessage("No apex file in the " + mAllApexesZip)
+                .that(apexes).isNotEmpty();
+        return apexes;
+    }
+
+    /**
+     * Extracts all contents of the apex file into the {@code outDir} using the deapexer.
+     *
+     * @param deapexer The deapexer file.
+     * @param debugfs The debugfs file.
+     * @param apex The apex file to be extracted.
+     * @param outDir The out folder.
+     */
+    private void runDeapexerExtract(File deapexer, File debugfs, File apex, File outDir) {
+        final RunUtil runUtil = new RunUtil();
+        final String os = System.getProperty("os.name").toLowerCase();
+        final boolean isMacOs = (os.startsWith("mac") || os.startsWith("darwin"));
+        if (isMacOs) {
+            runUtil.setEnvVariable("DYLD_LIBRARY_PATH", mDeapexingDir.getAbsolutePath());
+        } else {
+            runUtil.setEnvVariable("LD_LIBRARY_PATH", mDeapexingDir.getAbsolutePath());
+        }
+        final CommandResult result = runUtil.runTimedCmd(DEFAULT_RUN_TIMEOUT_MS,
+                deapexer.getAbsolutePath(),
+                "--debugfs_path",
+                debugfs.getAbsolutePath(),
+                "extract",
+                apex.getAbsolutePath(),
+                outDir.getAbsolutePath());
+        assertWithMessage("deapexer(" + apex + ") failed: " + result)
+                .that(result.getStatus()).isEqualTo(CommandStatus.SUCCESS);
+        assertWithMessage("deapexer(" + apex + ") failed: no outDir created")
+                .that(outDir.exists()).isTrue();
+    }
 }
diff --git a/hostsidetests/stagedinstall/src/com/android/tests/stagedinstall/host/StagedInstallTest.java b/hostsidetests/stagedinstall/src/com/android/tests/stagedinstall/host/StagedInstallTest.java
index 2369a2d..06bbed8 100644
--- a/hostsidetests/stagedinstall/src/com/android/tests/stagedinstall/host/StagedInstallTest.java
+++ b/hostsidetests/stagedinstall/src/com/android/tests/stagedinstall/host/StagedInstallTest.java
@@ -325,9 +325,20 @@
     @LargeTest
     public void testInstallStagedApex_SameGrade() throws Exception {
         assumeTrue("Device does not support updating APEX", isUpdatingApexSupported());
+        installV3Apex();
+        installV3Apex();
+    }
 
-        installV3Apex();
-        installV3Apex();
+    @Test
+    @LargeTest
+    public void testInstallStagedApex_SameGrade_NewOneWins() throws Exception {
+        assumeTrue("Device does not support updating APEX", isUpdatingApexSupported());
+
+        installV2Apex();
+
+        runPhase("testInstallStagedApex_SameGrade_NewOneWins_Commit");
+        getDevice().reboot();
+        runPhase("testInstallStagedApex_SameGrade_NewOneWins_VerifyPostReboot");
     }
 
     @Test
diff --git a/hostsidetests/statsd/AndroidTest.xml b/hostsidetests/statsd/AndroidTest.xml
index 4598f33..6e97754 100644
--- a/hostsidetests/statsd/AndroidTest.xml
+++ b/hostsidetests/statsd/AndroidTest.xml
@@ -26,4 +26,8 @@
     <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
         <option name="run-command" value="setprop persist.traced.enable 1" />
     </target_preparer>
+
+    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+        <option name="mainline-module-package-name" value="com.google.android.os.statsd" />
+    </object>
 </configuration>
diff --git a/hostsidetests/statsd/apps/statsdapp/src/com/android/server/cts/device/statsd/AtomTests.java b/hostsidetests/statsd/apps/statsdapp/src/com/android/server/cts/device/statsd/AtomTests.java
index 6b3b981..62301dd 100644
--- a/hostsidetests/statsd/apps/statsdapp/src/com/android/server/cts/device/statsd/AtomTests.java
+++ b/hostsidetests/statsd/apps/statsdapp/src/com/android/server/cts/device/statsd/AtomTests.java
@@ -469,7 +469,8 @@
             int noteCount = APP_OPS_ENUM_MAP.getOrDefault(op, opsList.length) + 1;
             for (int j = 0; j < noteCount; j++) {
                 try {
-                    noteAppOp(appOpsManager, opsList[i], true);
+                    appOpsManager.noteOp(opsList[i], android.os.Process.myUid(), MY_PACKAGE_NAME,
+                            null, "statsdTest");
                 } catch (SecurityException e) {}
             }
         }
diff --git a/tests/BlobStore/src/com/android/cts/blob/BlobStoreManagerTest.java b/tests/BlobStore/src/com/android/cts/blob/BlobStoreManagerTest.java
index 8473f06..ba06090 100644
--- a/tests/BlobStore/src/com/android/cts/blob/BlobStoreManagerTest.java
+++ b/tests/BlobStore/src/com/android/cts/blob/BlobStoreManagerTest.java
@@ -21,6 +21,8 @@
 import static com.android.utils.blob.Utils.assertLeasedBlobs;
 import static com.android.utils.blob.Utils.assertNoLeasedBlobs;
 import static com.android.utils.blob.Utils.releaseLease;
+import static com.android.utils.blob.Utils.TAG;
+import static com.android.utils.blob.Utils.triggerIdleMaintenance;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -73,7 +75,6 @@
 
 @RunWith(AndroidJUnit4.class)
 public class BlobStoreManagerTest {
-    private static final String TAG = "BlobStoreTest";
 
     private static final long TIMEOUT_COMMIT_CALLBACK_SEC = 5;
 
@@ -82,6 +83,7 @@
     // TODO: Make it a @TestApi or move the test using this to a different location.
     // Copy of DeviceConfig.NAMESPACE_BLOBSTORE constant
     private static final String NAMESPACE_BLOBSTORE = "blobstore";
+    private static final String KEY_SESSION_EXPIRY_TIMEOUT_MS = "session_expiry_timeout_ms";
     private static final String KEY_LEASE_ACQUISITION_WAIT_DURATION_MS =
             "lease_acquisition_wait_time_ms";
     private static final String KEY_TOTAL_BYTES_PER_APP_LIMIT_FLOOR =
@@ -990,6 +992,56 @@
                 .isEqualTo(initialRemainingQuota);
     }
 
+    @Test
+    public void testCommitBlobAfterIdleMaintenance() throws Exception {
+        final DummyBlobData blobData = new DummyBlobData.Builder(mContext).build();
+        blobData.prepare();
+        final long waitDurationMs = TimeUnit.SECONDS.toMillis(2);
+        final long partialFileSize = 100L;
+        final long sessionId = mBlobStoreManager.createSession(blobData.getBlobHandle());
+        assertThat(sessionId).isGreaterThan(0L);
+
+        try (BlobStoreManager.Session session = mBlobStoreManager.openSession(sessionId)) {
+            blobData.writeToSession(session, 0, partialFileSize);
+        }
+
+        SystemClock.sleep(waitDurationMs);
+
+        // Trigger idle maintenance which takes of deleting expired sessions.
+        triggerIdleMaintenance(InstrumentationRegistry.getInstrumentation());
+
+        try (BlobStoreManager.Session session = mBlobStoreManager.openSession(sessionId)) {
+            blobData.writeToSession(session, partialFileSize,
+                    blobData.getFileSize() - partialFileSize);
+            final CompletableFuture<Integer> callback = new CompletableFuture<>();
+            session.commit(mContext.getMainExecutor(), callback::complete);
+            assertThat(callback.get(TIMEOUT_COMMIT_CALLBACK_SEC, TimeUnit.SECONDS))
+                    .isEqualTo(0);
+        }
+    }
+
+    @Test
+    public void testExpiredSessionsDeleted() throws Exception {
+        final DummyBlobData blobData = new DummyBlobData.Builder(mContext).build();
+        blobData.prepare();
+        final long waitDurationMs = TimeUnit.SECONDS.toMillis(2);
+        runWithKeyValues(() -> {
+            final long sessionId = mBlobStoreManager.createSession(blobData.getBlobHandle());
+            assertThat(sessionId).isGreaterThan(0L);
+
+            try (BlobStoreManager.Session session = mBlobStoreManager.openSession(sessionId)) {
+                blobData.writeToSession(session, 0, 100);
+            }
+
+            SystemClock.sleep(waitDurationMs);
+
+            // Trigger idle maintenance which takes of deleting expired sessions.
+            triggerIdleMaintenance(InstrumentationRegistry.getInstrumentation());
+
+            assertThrows(SecurityException.class, () -> mBlobStoreManager.openSession(sessionId));
+        }, Pair.create(KEY_SESSION_EXPIRY_TIMEOUT_MS, String.valueOf(waitDurationMs)));
+    }
+
     private static void runWithKeyValues(ThrowingRunnable runnable,
             Pair<String, String>... keyValues) throws Exception {
         final Map<String, String> previousValues = new ArrayMap();
diff --git a/tests/app/Android.bp b/tests/app/Android.bp
index ed63def..827375b 100644
--- a/tests/app/Android.bp
+++ b/tests/app/Android.bp
@@ -128,6 +128,7 @@
         "androidx.test.rules",
         "compatibility-device-util-axt",
         "CtsExternalServiceCommon",
+        "cts-wm-util",
     ],
     srcs: [
         "AppExitTest/src/**/*.java",
diff --git a/tests/app/AppExitTest/AndroidManifest.xml b/tests/app/AppExitTest/AndroidManifest.xml
index 316959d..4606ca5 100644
--- a/tests/app/AppExitTest/AndroidManifest.xml
+++ b/tests/app/AppExitTest/AndroidManifest.xml
@@ -20,6 +20,7 @@
 
     <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"/>
     <uses-permission android:name="android.permission.READ_LOGS"/>
+    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
 
     <application android:usesCleartextTraffic="true">
         <uses-library android:name="android.test.runner" />
diff --git a/tests/app/AppExitTest/src/android/app/cts/ActivityManagerAppExitInfoTest.java b/tests/app/AppExitTest/src/android/app/cts/ActivityManagerAppExitInfoTest.java
index 9a34f20..bd9e5af 100644
--- a/tests/app/AppExitTest/src/android/app/cts/ActivityManagerAppExitInfoTest.java
+++ b/tests/app/AppExitTest/src/android/app/cts/ActivityManagerAppExitInfoTest.java
@@ -43,6 +43,7 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.provider.Settings;
+import android.server.wm.settings.SettingsSession;
 import android.system.Os;
 import android.system.OsConstants;
 import android.test.InstrumentationTestCase;
@@ -129,6 +130,7 @@
     private int mOtherUserId;
     private UserHandle mOtherUserHandle;
     private DropBoxManager.Entry mAnrEntry;
+    private SettingsSession<String> mDataAnrSettings;
 
     @Override
     protected void setUp() throws Exception {
@@ -147,6 +149,11 @@
         mHandler = new H(mHandlerThread.getLooper());
         mMessenger = new Messenger(mHandler);
         executeShellCmd("cmd deviceidle whitelist +" + STUB_PACKAGE_NAME);
+        mDataAnrSettings = new SettingsSession<>(
+                Settings.Global.getUriFor(
+                        Settings.Global.DROPBOX_TAG_PREFIX + "data_app_anr"),
+                Settings.Global::getString, Settings.Global::putString);
+        mDataAnrSettings.set("enabled");
     }
 
     private void handleMessagePid(Message msg) {
@@ -207,6 +214,9 @@
         executeShellCmd("cmd deviceidle whitelist -" + STUB_PACKAGE_NAME);
         removeTestUserIfNecessary();
         mHandlerThread.quitSafely();
+        if (mDataAnrSettings != null) {
+            mDataAnrSettings.close();
+        }
     }
 
     private int createUser(String name, boolean guest) throws Exception {
diff --git a/tests/autofillservice/src/android/autofillservice/cts/augmented/AbstractLoginNotImportantForAutofillTestCase.java b/tests/autofillservice/src/android/autofillservice/cts/augmented/AbstractLoginNotImportantForAutofillTestCase.java
index 5d5852f..67169b4 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/augmented/AbstractLoginNotImportantForAutofillTestCase.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/augmented/AbstractLoginNotImportantForAutofillTestCase.java
@@ -157,7 +157,9 @@
         final AugmentedFillRequest request = sAugmentedReplier.getNextFillRequest();
 
         // Assert request
-        assertBasicRequestInfo(request, mActivity, usernameId, expectedFocusedValue);
+        // No inline request because didn't focus on any view.
+        assertBasicRequestInfo(request, mActivity, usernameId, expectedFocusedValue,
+                /* hasInlineRequest */ false);
 
         // Make sure standard Autofill UI is not shown.
         mUiBot.assertNoDatasetsEver();
diff --git a/tests/autofillservice/src/android/autofillservice/cts/augmented/AugmentedHelper.java b/tests/autofillservice/src/android/autofillservice/cts/augmented/AugmentedHelper.java
index 82eac21..56bf8b9 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/augmented/AugmentedHelper.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/augmented/AugmentedHelper.java
@@ -32,6 +32,7 @@
 import android.util.Pair;
 import android.view.autofill.AutofillId;
 import android.view.autofill.AutofillValue;
+import android.view.inputmethod.InlineSuggestionsRequest;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -85,6 +86,19 @@
     public static void assertBasicRequestInfo(@NonNull AugmentedFillRequest request,
             @NonNull Activity activity, @NonNull AutofillId expectedFocusedId,
             @NonNull String expectedFocusedValue) {
+        assertBasicRequestInfo(request, activity, expectedFocusedId, expectedFocusedValue, true);
+    }
+
+    public static void assertBasicRequestInfo(@NonNull AugmentedFillRequest request,
+            @NonNull Activity activity, @NonNull AutofillId expectedFocusedId,
+            @NonNull AutofillValue expectedFocusedValue, boolean hasInlineRequest) {
+        assertBasicRequestInfo(request, activity, expectedFocusedId,
+                expectedFocusedValue.getTextValue().toString(), hasInlineRequest);
+    }
+
+    private static void assertBasicRequestInfo(@NonNull AugmentedFillRequest request,
+            @NonNull Activity activity, @NonNull AutofillId expectedFocusedId,
+            @NonNull String expectedFocusedValue, boolean hasInlineRequest) {
         Objects.requireNonNull(activity);
         Objects.requireNonNull(expectedFocusedId);
         assertWithMessage("no AugmentedFillRequest").that(request).isNotNull();
@@ -108,6 +122,13 @@
         final AutofillValue actualFocusedValue = request.request.getFocusedValue();
         assertWithMessage("no focused value on %s", request).that(actualFocusedValue).isNotNull();
         assertAutofillValue(expectedFocusedValue, actualFocusedValue);
+        final InlineSuggestionsRequest inlineRequest =
+                request.request.getInlineSuggestionsRequest();
+        if (hasInlineRequest) {
+            assertWithMessage("no inline request on %s", request).that(inlineRequest).isNotNull();
+        } else {
+            assertWithMessage("exist inline request on %s", request).that(inlineRequest).isNull();
+        }
     }
 
     public static void assertAutofillValue(@NonNull AutofillValue expectedValue,
diff --git a/tests/autofillservice/src/android/autofillservice/cts/augmented/AugmentedLoginActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/augmented/AugmentedLoginActivityTest.java
index bbf2cdb..2683ec4 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/augmented/AugmentedLoginActivityTest.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/augmented/AugmentedLoginActivityTest.java
@@ -59,7 +59,6 @@
 import android.view.autofill.AutofillId;
 import android.view.autofill.AutofillManager;
 import android.view.autofill.AutofillValue;
-import android.view.inputmethod.InlineSuggestionsRequest;
 import android.widget.EditText;
 
 import org.junit.Test;
@@ -140,7 +139,9 @@
         final AugmentedFillRequest augmentedRequest = sAugmentedReplier.getNextFillRequest();
 
         // Assert request
-        assertBasicRequestInfo(augmentedRequest, mActivity, usernameId, expectedFocusedValue);
+        // No inline request because didn't focus on any view.
+        assertBasicRequestInfo(augmentedRequest, mActivity, usernameId, expectedFocusedValue,
+                /* hasInlineRequest */ false);
 
         // Make sure standard Autofill UI is not shown.
         mUiBot.assertNoDatasetsEver();
@@ -922,14 +923,9 @@
         final AugmentedFillRequest request = sAugmentedReplier.getNextFillRequest();
 
         // Assert request
-        assertBasicRequestInfo(request, mActivity, usernameId, usernameValue);
-        // TODO: Use helper function instead of assert here. There are some cases augment aufill
-        // will ask IME for inline suggestion request, we will have inline suggestion request in
-        // augment aufill cts, we need to re-visit all augment aufill tests. It is not suitable to
-        // use helper function to assert InlineSuggestionsRequest currently.
-        final InlineSuggestionsRequest inlineRequest =
-                request.request.getInlineSuggestionsRequest();
-        assertThat(inlineRequest).isNull();
+        // No inline request because didn't focus on any view.
+        assertBasicRequestInfo(request, mActivity, usernameId, usernameValue,
+                /* hasInlineRequest */ false);
 
         // Make sure standard Autofill UI is not shown.
         mUiBot.assertNoDatasetsEver();
diff --git a/tests/autofillservice/src/android/autofillservice/cts/inline/InlineAugmentedLoginActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/inline/InlineAugmentedLoginActivityTest.java
index 78361f1..541f5e0 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/inline/InlineAugmentedLoginActivityTest.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/inline/InlineAugmentedLoginActivityTest.java
@@ -37,7 +37,6 @@
 import android.service.autofill.FillEventHistory.Event;
 import android.view.autofill.AutofillId;
 import android.view.autofill.AutofillValue;
-import android.view.inputmethod.InlineSuggestionsRequest;
 import android.widget.EditText;
 
 import org.junit.Test;
@@ -160,13 +159,6 @@
 
         // Assert request
         assertBasicRequestInfo(request1, mActivity, usernameId, usernameValue);
-        // TODO: Use helper function instead of assert here. There are some cases augment aufill
-        // will ask IME for inline suggestion request, we will have inline suggestion request in
-        // augment aufill cts, we need to re-visit all augment aufill tests. It is not suitable to
-        // use helper function to assert InlineSuggestionsRequest currently.
-        final InlineSuggestionsRequest inlineRequest =
-                request1.request.getInlineSuggestionsRequest();
-        assertThat(inlineRequest).isNotNull();
 
         // Confirm one suggestion
         mUiBot.assertDatasets("dude", "DUDE");
@@ -205,13 +197,6 @@
 
         // Assert request
         assertBasicRequestInfo(request1, mActivity, usernameId, usernameValue);
-        // TODO: Use helper function instead of assert here. There are some cases augment aufill
-        // will ask IME for inline suggestion request, we will have inline suggestion request in
-        // augment aufill cts, we need to re-visit all augment aufill tests. It is not suitable to
-        // use helper function to assert InlineSuggestionsRequest currently.
-        final InlineSuggestionsRequest inlineRequest =
-                request1.request.getInlineSuggestionsRequest();
-        assertThat(inlineRequest).isNotNull();
 
         // Confirm two suggestion
         mUiBot.assertDatasets("dude");
diff --git a/tests/camera/src/android/hardware/camera2/cts/SurfaceViewPreviewTest.java b/tests/camera/src/android/hardware/camera2/cts/SurfaceViewPreviewTest.java
index 1f2e93e..ad41f9a 100644
--- a/tests/camera/src/android/hardware/camera2/cts/SurfaceViewPreviewTest.java
+++ b/tests/camera/src/android/hardware/camera2/cts/SurfaceViewPreviewTest.java
@@ -51,6 +51,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.HashMap;
 
 import org.junit.runners.Parameterized;
 import org.junit.runner.RunWith;
@@ -228,6 +229,9 @@
         updatePreviewSurface(maxPreviewSize);
 
         createImageReader(maxYuvSize, ImageFormat.YUV_420_888, MAX_IMAGES_TO_PREPARE, imageListener);
+        HashMap<Size, Long> yuvMinFrameDurations =
+                mStaticInfo.getAvailableMinFrameDurationsForFormatChecked(ImageFormat.YUV_420_888);
+        Long readerMinFrameDuration = yuvMinFrameDurations.get(maxYuvSize);
 
         List<Surface> outputSurfaces = new ArrayList<Surface>();
         outputSurfaces.add(mPreviewSurface);
@@ -331,14 +335,16 @@
                         cameraId,
                         frameDurationStats.first / 1e6, preparedFrameDurationStats.second / 1e6),
                 (preparedFrameDurationStats.second <=
-                        frameDurationStats.first * (1 + PREPARE_PEAK_RATE_BOUNDS)));
+                        Math.max(frameDurationStats.first, readerMinFrameDuration) *
+                        (1 + PREPARE_PEAK_RATE_BOUNDS)));
             mCollector.expectTrue(
                 String.format("Camera %s: Preview average frame interval affected by use of new " +
                         "stream: preview avg frame duration: %f ms, with new stream: %f ms",
                         cameraId,
                         frameDurationStats.first / 1e6, preparedFrameDurationStats.first / 1e6),
                 (preparedFrameDurationStats.first <=
-                        frameDurationStats.first * (1 + PREPARE_FRAME_RATE_BOUNDS)));
+                        Math.max(frameDurationStats.first, readerMinFrameDuration) *
+                        (1 + PREPARE_FRAME_RATE_BOUNDS)));
         }
     }
 
diff --git a/tests/framework/base/windowmanager/app/src/android/server/wm/app/HostActivity.java b/tests/framework/base/windowmanager/app/src/android/server/wm/app/HostActivity.java
index 11354fb..4a365a3 100644
--- a/tests/framework/base/windowmanager/app/src/android/server/wm/app/HostActivity.java
+++ b/tests/framework/base/windowmanager/app/src/android/server/wm/app/HostActivity.java
@@ -90,7 +90,7 @@
         Intent mIntent =  new Intent(this, RenderService.class);
         Bundle b = new Bundle();
         b.putBinder(EXTRAS_HOST_TOKEN, mSurfaceView.getHostToken());
-        b.putInt(EXTRAS_DISPLAY_ID, getDisplayId());
+        b.putInt(EXTRAS_DISPLAY_ID, getDisplay().getDisplayId());
         b.putInt(EXTRA_ON_MOTIONEVENT_DELAY_MS,
                 getIntent().getIntExtra(EXTRA_ON_MOTIONEVENT_DELAY_MS, 2000));
         mIntent.putExtra(EXTRAS_BUNDLE, b);
diff --git a/tests/framework/base/windowmanager/app/src/android/server/wm/app/RenderService.java b/tests/framework/base/windowmanager/app/src/android/server/wm/app/RenderService.java
index 58da2d4..f781d2e 100644
--- a/tests/framework/base/windowmanager/app/src/android/server/wm/app/RenderService.java
+++ b/tests/framework/base/windowmanager/app/src/android/server/wm/app/RenderService.java
@@ -73,10 +73,7 @@
         embeddedView.setOnTouchListener(this::onTouch);
         DisplayMetrics metrics = new DisplayMetrics();
         displayContext.getDisplay().getMetrics(metrics);
-        WindowManager.LayoutParams lp = new WindowManager.LayoutParams(metrics.widthPixels,
-                metrics.heightPixels, TYPE_APPLICATION, 0,
-                PixelFormat.OPAQUE);
-        surfaceControlViewHost.setView(embeddedView, lp);
+        surfaceControlViewHost.setView(embeddedView, metrics.widthPixels, metrics.heightPixels);
         return surfaceControlViewHost;
     }
 
diff --git a/tests/framework/base/windowmanager/backgroundactivity/src/android/server/wm/BackgroundActivityLaunchTest.java b/tests/framework/base/windowmanager/backgroundactivity/src/android/server/wm/BackgroundActivityLaunchTest.java
index 8bff8c4..3cc589a 100644
--- a/tests/framework/base/windowmanager/backgroundactivity/src/android/server/wm/BackgroundActivityLaunchTest.java
+++ b/tests/framework/base/windowmanager/backgroundactivity/src/android/server/wm/BackgroundActivityLaunchTest.java
@@ -65,6 +65,7 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 
 import java.util.List;
@@ -267,6 +268,7 @@
 
     @Test
     @FlakyTest(bugId = 130800326)
+    @Ignore  // TODO(b/145981637): Make this test work
     public void testActivityBlockedWhenForegroundActivityRestartsItself() throws Exception {
         // Start AppA foreground activity
         Intent intent = new Intent();
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/DecorInsetTests.java b/tests/framework/base/windowmanager/src/android/server/wm/DecorInsetTests.java
index 8997f60..964970a 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/DecorInsetTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/DecorInsetTests.java
@@ -46,6 +46,8 @@
 
         assertNotNull("test setup failed", activity.mLastDecorInsets);
         assertNull("unexpected content insets", activity.mLastContentInsets);
+
+        assertContentViewLocationMatchesInsets();
     }
 
     @Test
@@ -60,6 +62,8 @@
         assertNotNull("test setup failed", activity.mLastDecorInsets);
         assertEquals("unexpected bottom inset: ", 0, activity.mLastContentInsets.getInsets(
                 WindowInsets.Type.systemBars()).bottom);
+
+        assertContentViewLocationMatchesInsets();
     }
 
     @Test
@@ -75,20 +79,26 @@
         assertEquals("insets were unexpectedly consumed: ",
                 activity.mLastDecorInsets.getSystemWindowInsets(),
                 activity.mLastContentInsets.getSystemWindowInsets());
+
+        assertContentViewLocationMatchesInsets();
     }
 
     @Test
     public void testDecorView_doesntConsumeNavBar_ifDecorDoesntFitSystemWindows() throws Throwable {
         TestActivity activity = mDecorActivity.launchActivity(new Intent()
-                .putExtra(ARG_LAYOUT_STABLE, true)
+                .putExtra(ARG_LAYOUT_STABLE, false)
                 .putExtra(ARG_LAYOUT_FULLSCREEN, false)
                 .putExtra(ARG_LAYOUT_HIDE_NAV, false)
                 .putExtra(ARG_DECOR_FITS_SYSTEM_WINDOWS, false));
         activity.mLaidOut.await(4, TimeUnit.SECONDS);
 
+        assertEquals(0, activity.getWindow().getDecorView().getWindowSystemUiVisibility());
+
         assertNotNull("test setup failed", activity.mLastDecorInsets);
         assertEquals("insets were unexpectedly consumed: ",
                 activity.mLastDecorInsets.getSystemWindowInsets(),
                 activity.mLastContentInsets.getSystemWindowInsets());
+
+        assertContentViewLocationMatchesInsets();
     }
 }
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/DecorInsetTestsBase.java b/tests/framework/base/windowmanager/src/android/server/wm/DecorInsetTestsBase.java
index 10b3d0d..94c5e2b 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/DecorInsetTestsBase.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/DecorInsetTestsBase.java
@@ -16,8 +16,12 @@
 
 package android.server.wm;
 
+import static org.junit.Assert.assertEquals;
+
 import android.app.Activity;
 import android.content.Intent;
+import android.graphics.Insets;
+import android.graphics.Rect;
 import android.os.Bundle;
 import android.view.View;
 import android.view.ViewTreeObserver;
@@ -90,4 +94,39 @@
             return vis;
         }
     }
+
+    public void assertContentViewLocationMatchesInsets() {
+        TestActivity activity = mDecorActivity.getActivity();
+
+        Insets insetsConsumedByDecor = Insets.subtract(
+                systemWindowInsetsOrZero(activity.mLastDecorInsets),
+                systemWindowInsetsOrZero(activity.mLastContentInsets));
+        Rect expectedContentRect = rectInWindow(activity.getWindow().getDecorView());
+        insetRect(expectedContentRect, insetsConsumedByDecor);
+
+        Rect actualContentRect = rectInWindow(activity.findViewById(android.R.id.content));
+
+        assertEquals("Decor consumed " + insetsConsumedByDecor + ", content rect:",
+                expectedContentRect, actualContentRect);
+    }
+
+    public Insets systemWindowInsetsOrZero(WindowInsets wi) {
+        if (wi == null) {
+            return Insets.NONE;
+        }
+        return wi.getSystemWindowInsets();
+    }
+
+    private Rect rectInWindow(View view) {
+        int[] loc = new int[2];
+        view.getLocationInWindow(loc);
+        return new Rect(loc[0], loc[1], loc[0] + view.getWidth(), loc[1] + view.getHeight());
+    }
+
+    private static void insetRect(Rect rect, Insets insets) {
+        rect.left += insets.left;
+        rect.top += insets.top;
+        rect.right -= insets.right;
+        rect.bottom -= insets.bottom;
+    }
 }
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/LayoutTests.java b/tests/framework/base/windowmanager/src/android/server/wm/LayoutTests.java
index f0d685b..3a10984 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/LayoutTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/LayoutTests.java
@@ -20,11 +20,14 @@
 import static android.view.View.SYSTEM_UI_FLAG_FULLSCREEN;
 import static android.view.View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
 import static android.view.View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
+import static android.view.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
+import static android.view.View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
 import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_PANEL;
 
 import static androidx.test.InstrumentationRegistry.getInstrumentation;
 
+import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
@@ -37,9 +40,9 @@
 import android.provider.Settings;
 import android.view.KeyEvent;
 import android.view.View;
+import android.view.WindowInsets.Type;
 import android.view.WindowManager.LayoutParams;
 
-
 import com.android.compatibility.common.util.PollingCheck;
 import com.android.compatibility.common.util.SystemUtil;
 
@@ -47,6 +50,8 @@
 import org.junit.Before;
 import org.junit.Test;
 
+import java.util.ArrayList;
+
 /**
  * Test whether WindowManager performs the correct layout after we make some changes to it.
  *
@@ -105,16 +110,14 @@
         getInstrumentation().runOnMainSync(() -> {
             final View view = new View(activity);
             view.setSystemUiVisibility(systemUiFlags);
-            activity.getWindowManager().addView(view, new LayoutParams(TYPE_APPLICATION_PANEL));
-            activity.mView = view;
+            activity.addWindow(view, new LayoutParams());
         });
 
         // Wait for the global layout triggered by adding window.
         activity.waitForGlobalLayout();
 
         // Remove the window we added previously.
-        getInstrumentation().runOnMainSync(() ->
-                activity.getWindowManager().removeViewImmediate(activity.mView));
+        getInstrumentation().runOnMainSync(activity::removeAllWindows);
 
         // Wait for the global layout triggered by removing window.
         activity.waitForGlobalLayout();
@@ -152,7 +155,7 @@
                             }
                         }
                     });
-            activity.getWindowManager().addView(view, new LayoutParams(TYPE_APPLICATION_PANEL));
+            activity.addWindow(view, new LayoutParams());
         });
 
         // Wait for the possible failure.
@@ -166,6 +169,7 @@
 
     @Test
     public void testChangingFocusableFlag() throws Exception {
+        final View[] view = new View[1];
         final LayoutParams attrs = new LayoutParams(TYPE_APPLICATION_PANEL, FLAG_NOT_FOCUSABLE);
         final boolean[] childWindowHasFocus = { false };
         final boolean[] childWindowGotKeyEvent = { false };
@@ -173,7 +177,7 @@
 
         // Add a not-focusable window.
         getInstrumentation().runOnMainSync(() -> {
-            final View view = new View(activity) {
+            view[0] = new View(activity) {
                 public void onWindowFocusChanged(boolean hasWindowFocus) {
                     super.onWindowFocusChanged(hasWindowFocus);
                     childWindowHasFocus[0] = hasWindowFocus;
@@ -189,15 +193,14 @@
                     return super.onKeyDown(keyCode, event);
                 }
             };
-            activity.getWindowManager().addView(view, attrs);
-            activity.mView = view;
+            activity.addWindow(view[0], attrs);
         });
         getInstrumentation().waitForIdleSync();
 
         // Make the window focusable.
         getInstrumentation().runOnMainSync(() -> {
             attrs.flags &= ~FLAG_NOT_FOCUSABLE;
-            activity.getWindowManager().updateViewLayout(activity.mView, attrs);
+            activity.getWindowManager().updateViewLayout(view[0], attrs);
         });
         synchronized (activity) {
             activity.wait(TIMEOUT_WINDOW_FOCUS_CHANGED);
@@ -215,12 +218,60 @@
         });
     }
 
+    @Test
+    public void testSysuiFlagLayoutFullscreen() {
+        final TestActivity activity = startActivity(TestActivity.class);
+
+        final View[] views = new View[2];
+        getInstrumentation().runOnMainSync(() -> {
+            views[0] = new View(activity);
+            final LayoutParams attrs = new LayoutParams();
+            attrs.setFitInsetsTypes(attrs.getFitInsetsTypes() & ~Type.statusBars());
+            activity.addWindow(views[0], attrs);
+
+            views[1] = new View(activity);
+            views[1].setSystemUiVisibility(SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
+            activity.addWindow(views[1], new LayoutParams());
+        });
+        getInstrumentation().waitForIdleSync();
+
+        assertLayoutEquals(views[0], views[1]);
+    }
+
+    @Test
+    public void testSysuiFlagLayoutHideNavigation() {
+        final TestActivity activity = startActivity(TestActivity.class);
+
+        final View[] views = new View[2];
+        getInstrumentation().runOnMainSync(() -> {
+            views[0] = new View(activity);
+            final LayoutParams attrs = new LayoutParams();
+            attrs.setFitInsetsTypes(attrs.getFitInsetsTypes() & ~Type.systemBars());
+            activity.addWindow(views[0], attrs);
+
+            views[1] = new View(activity);
+            views[1].setSystemUiVisibility(SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
+            activity.addWindow(views[1], new LayoutParams());
+        });
+        getInstrumentation().waitForIdleSync();
+
+        assertLayoutEquals(views[0], views[1]);
+    }
+
+    private static void assertLayoutEquals(View view1, View view2) {
+        final int[][] locations = new int[2][2];
+        view1.getLocationOnScreen(locations[0]);
+        view2.getLocationOnScreen(locations[1]);
+        assertArrayEquals("Location must be the same.", locations[0], locations[1]);
+        assertEquals("Width must be the same.", view1.getWidth(), view2.getWidth());
+        assertEquals("Height must be the same.", view1.getHeight(), view2.getHeight());
+    }
+
     public static class TestActivity extends FocusableActivity {
         private static final long TIMEOUT_LAYOUT = 200; // milliseconds
 
         private final Object mLockGlobalLayout = new Object();
-
-        View mView = null;
+        private ArrayList<View> mViews = new ArrayList<>();
 
         @Override
         protected void onCreate(Bundle savedInstanceState) {
@@ -237,5 +288,23 @@
                 mLockGlobalLayout.wait(TIMEOUT_LAYOUT);
             }
         }
+
+        void addWindow(View view, LayoutParams attrs) {
+            getWindowManager().addView(view, attrs);
+            mViews.add(view);
+        }
+
+        void removeAllWindows() {
+            for (View view : mViews) {
+                getWindowManager().removeViewImmediate(view);
+            }
+            mViews.clear();
+        }
+
+        @Override
+        protected void onPause() {
+            super.onPause();
+            removeAllWindows();
+        }
     }
 }
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/PinnedStackTests.java b/tests/framework/base/windowmanager/src/android/server/wm/PinnedStackTests.java
index ab3d71a..1d6ea9e 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/PinnedStackTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/PinnedStackTests.java
@@ -983,6 +983,7 @@
         launchActivity(PIP_ACTIVITY);
         waitForExitPipToFullscreen(PIP_ACTIVITY);
         assertPinnedStackDoesNotExist();
+        mWmState.waitForLastOrientation(ORIENTATION_LANDSCAPE);
         assertEquals(ORIENTATION_LANDSCAPE, mWmState.getLastOrientation());
     }
 
diff --git a/tests/framework/base/windowmanager/testsdk29/src/android/server/wm/DecorInsetSdk29Tests.java b/tests/framework/base/windowmanager/testsdk29/src/android/server/wm/DecorInsetSdk29Tests.java
index 6d6d93d..b3413c4 100644
--- a/tests/framework/base/windowmanager/testsdk29/src/android/server/wm/DecorInsetSdk29Tests.java
+++ b/tests/framework/base/windowmanager/testsdk29/src/android/server/wm/DecorInsetSdk29Tests.java
@@ -46,6 +46,8 @@
 
         assertNotNull("test setup failed", activity.mLastDecorInsets);
         assertNull("unexpected content insets", activity.mLastContentInsets);
+
+        assertContentViewLocationMatchesInsets();
     }
 
     @Test
@@ -60,6 +62,8 @@
         assertNotNull("test setup failed", activity.mLastDecorInsets);
         assertEquals("unexpected bottom inset: ", 0, activity.mLastContentInsets.getInsets(
                 WindowInsets.Type.systemBars()).bottom);
+
+        assertContentViewLocationMatchesInsets();
     }
 
     @Test
@@ -75,20 +79,26 @@
         assertEquals("insets were unexpectedly consumed: ",
                 activity.mLastDecorInsets.getSystemWindowInsets(),
                 activity.mLastContentInsets.getSystemWindowInsets());
+
+        assertContentViewLocationMatchesInsets();
     }
 
     @Test
     public void testDecorView_doesntConsumeNavBar_ifDecorDoesntFitSystemWindows() throws Throwable {
         TestActivity activity = mDecorActivity.launchActivity(new Intent()
-                .putExtra(ARG_LAYOUT_STABLE, true)
+                .putExtra(ARG_LAYOUT_STABLE, false)
                 .putExtra(ARG_LAYOUT_FULLSCREEN, false)
                 .putExtra(ARG_LAYOUT_HIDE_NAV, false)
                 .putExtra(ARG_DECOR_FITS_SYSTEM_WINDOWS, false));
         activity.mLaidOut.await(4, TimeUnit.SECONDS);
 
+        assertEquals(0, activity.getWindow().getDecorView().getWindowSystemUiVisibility());
+
         assertNotNull("test setup failed", activity.mLastDecorInsets);
         assertEquals("insets were unexpectedly consumed: ",
                 activity.mLastDecorInsets.getSystemWindowInsets(),
                 activity.mLastContentInsets.getSystemWindowInsets());
+
+        assertContentViewLocationMatchesInsets();
     }
 }
diff --git a/tests/tests/media/libmediandkjni/Android.bp b/tests/tests/media/libmediandkjni/Android.bp
index 705a0ef..e501daa 100644
--- a/tests/tests/media/libmediandkjni/Android.bp
+++ b/tests/tests/media/libmediandkjni/Android.bp
@@ -27,7 +27,7 @@
         "libnativehelper_compat_libc++",
         "liblog",
     ],
-    sdk_version: "current",
+    sdk_version: "29",
     cflags: [
         "-Werror",
         "-Wall",
diff --git a/tests/tests/media/libmediandkjni/native-media-jni.cpp b/tests/tests/media/libmediandkjni/native-media-jni.cpp
index 15ba825..7a2671c 100644
--- a/tests/tests/media/libmediandkjni/native-media-jni.cpp
+++ b/tests/tests/media/libmediandkjni/native-media-jni.cpp
@@ -1086,7 +1086,9 @@
             AMEDIAFORMAT_KEY_BIT_RATE,
             AMEDIAFORMAT_KEY_FRAME_RATE,
             AMEDIAFORMAT_KEY_I_FRAME_INTERVAL,
-            AMEDIAFORMAT_KEY_LOW_LATENCY
+            // need to specify the actual string, since this test needs
+            // to run on API 29, where the symbol doesn't exist
+            "low-latency", // AMEDIAFORMAT_KEY_LOW_LATENCY
     };
 
     jint values[] = {width, height, colorFormat, bitRate, frameRate, iFrameInterval, lowLatency};
diff --git a/tests/tests/media/src/android/media/cts/DecoderTest.java b/tests/tests/media/src/android/media/cts/DecoderTest.java
index f270d33..3541454 100644
--- a/tests/tests/media/src/android/media/cts/DecoderTest.java
+++ b/tests/tests/media/src/android/media/cts/DecoderTest.java
@@ -3602,6 +3602,7 @@
                 true /* useNdk */);
     }
 
+    @NonMediaMainlineTest
     public void testLowLatencyAVCAt1280x720() throws Exception {
         testLowLatencyVideo(
                 R.raw.video_1280x720_mp4_h264_1000kbps_25fps_aac_stereo_128kbps_44100hz, 300,
@@ -3611,6 +3612,7 @@
                 true /* useNdk */);
     }
 
+    @NonMediaMainlineTest
     public void testLowLatencyHEVCAt480x360() throws Exception {
         testLowLatencyVideo(
                 R.raw.video_480x360_mp4_hevc_650kbps_30fps_aac_stereo_128kbps_48000hz, 300,
diff --git a/tests/tests/media/src/android/media/cts/MediaMuxerTest.java b/tests/tests/media/src/android/media/cts/MediaMuxerTest.java
index 858adb1..2aebc8d 100644
--- a/tests/tests/media/src/android/media/cts/MediaMuxerTest.java
+++ b/tests/tests/media/src/android/media/cts/MediaMuxerTest.java
@@ -16,6 +16,8 @@
 
 package android.media.cts;
 
+import android.media.cts.R;
+
 import android.content.Context;
 import android.content.res.AssetFileDescriptor;
 import android.content.res.Resources;
@@ -24,12 +26,13 @@
 import android.media.MediaFormat;
 import android.media.MediaMetadataRetriever;
 import android.media.MediaMuxer;
+import android.os.Build;
 import android.os.ParcelFileDescriptor;
 import android.platform.test.annotations.AppModeFull;
 import android.test.AndroidTestCase;
 import android.util.Log;
 
-import android.media.cts.R;
+import com.android.compatibility.common.util.MediaUtils;
 
 import java.io.File;
 import java.io.IOException;
@@ -52,6 +55,7 @@
     private static final float TOLERANCE = 0.0002f;
     private static final long OFFSET_TIME_US = 29 * 60 * 1000000L; // 29 minutes
     private Resources mResources;
+    private boolean mAndroid11 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R;
 
     @Override
     public void setContext(Context context) {
@@ -77,6 +81,8 @@
     }
 
     public void testDualAudioTrack() throws Exception {
+        if (!MediaUtils.check(mAndroid11, "test needs Android 11")) return;
+
         int source = R.raw.audio_aac_mono_70kbs_44100hz_aac_mono_70kbs_44100hz;
         String outputFilePath = File.createTempFile("MediaMuxerTest_testDualAudio", ".mp4")
                 .getAbsolutePath();
@@ -84,6 +90,8 @@
     }
 
     public void testDualVideoAndAudioTrack() throws Exception {
+        if (!MediaUtils.check(mAndroid11, "test needs Android 11")) return;
+
         int source = R.raw.video_h264_30fps_video_h264_30fps_aac_44100hz_aac_44100hz;
         String outputFilePath = File.createTempFile("MediaMuxerTest_testDualVideoAudio", ".mp4")
                 .getAbsolutePath();
@@ -444,6 +452,8 @@
      * when video and audio samples start after zero, audio later than video.
      */
     public void testTimestampsAudioBVideoStartOffsetAudioVideo() throws Exception {
+        if (!MediaUtils.check(mAndroid11, "test needs Android 11")) return;
+
         Vector<Integer> startOffsetUsVect = new Vector<Integer>();
         // Video starts at 200000us.
         startOffsetUsVect.add(200000);
@@ -457,6 +467,8 @@
      * when video starts after zero and audio starts before zero.
      */
     public void testTimestampsAudioBVideoStartOffsetNegativeAudioVideo() throws Exception {
+        if (!MediaUtils.check(mAndroid11, "test needs Android 11")) return;
+
         Vector<Integer> startOffsetUsVect = new Vector<Integer>();
         // Video starts at 200000us.
         startOffsetUsVect.add(200000);
@@ -470,6 +482,8 @@
      * samples start later than video.
      */
     public void testTimestampsAudioBVideoStartOffsetAudio() throws Exception {
+        if (!MediaUtils.check(mAndroid11, "test needs Android 11")) return;
+
         Vector<Integer> startOffsetUsVect = new Vector<Integer>();
         // Video starts at 0us.
         startOffsetUsVect.add(0);
@@ -483,6 +497,8 @@
      * audio and video, audio later than video at 0us.
      */
     public void testTimestampsStartOffsetAudio() throws Exception {
+        if (!MediaUtils.check(mAndroid11, "test needs Android 11")) return;
+
         Vector<Integer> startOffsetUsVect = new Vector<Integer>();
         // Video starts at 0us.
         startOffsetUsVect.add(0);
@@ -496,6 +512,8 @@
      * audio and video, video later than audio at 0us.
      */
     public void testTimestampsStartOffsetVideo() throws Exception {
+        if (!MediaUtils.check(mAndroid11, "test needs Android 11")) return;
+
         Vector<Integer> startOffsetUsVect = new Vector<Integer>();
         // Video starts at 500000us.
         startOffsetUsVect.add(500000);
@@ -509,6 +527,8 @@
      * audio and video, audio later than video, positive offsets for both.
      */
     public void testTimestampsStartOffsetVideoAudio() throws Exception {
+        if (!MediaUtils.check(mAndroid11, "test needs Android 11")) return;
+
         Vector<Integer> startOffsetUsVect = new Vector<Integer>();
         // Video starts at 250000us.
         startOffsetUsVect.add(250000);
@@ -522,6 +542,8 @@
      * audio and video, video later than audio, positive offets for both.
      */
     public void testTimestampsStartOffsetAudioVideo() throws Exception {
+        if (!MediaUtils.check(mAndroid11, "test needs Android 11")) return;
+
         Vector<Integer> startOffsetUsVect = new Vector<Integer>();
         // Video starts at 500000us.
         startOffsetUsVect.add(500000);
@@ -535,6 +557,8 @@
      * audio and video, video later than audio, audio before zero.
      */
     public void testTimestampsStartOffsetNegativeAudioVideo() throws Exception {
+        if (!MediaUtils.check(mAndroid11, "test needs Android 11")) return;
+
         Vector<Integer> startOffsetUsVect = new Vector<Integer>();
         // Video starts at 50000us.
         startOffsetUsVect.add(50000);
diff --git a/tests/tests/net/src/android/net/cts/CaptivePortalTest.kt b/tests/tests/net/src/android/net/cts/CaptivePortalTest.kt
index 4418e17..0816aba 100644
--- a/tests/tests/net/src/android/net/cts/CaptivePortalTest.kt
+++ b/tests/tests/net/src/android/net/cts/CaptivePortalTest.kt
@@ -16,6 +16,7 @@
 
 package android.net.cts
 
+import android.Manifest.permission.CONNECTIVITY_INTERNAL
 import android.Manifest.permission.NETWORK_SETTINGS
 import android.Manifest.permission.READ_DEVICE_CONFIG
 import android.Manifest.permission.WRITE_DEVICE_CONFIG
@@ -31,6 +32,7 @@
 import android.net.Uri
 import android.net.cts.util.CtsNetUtils
 import android.net.wifi.WifiManager
+import android.os.Build
 import android.os.ConditionVariable
 import android.platform.test.annotations.AppModeFull
 import android.provider.DeviceConfig
@@ -164,7 +166,10 @@
                     "access."
             assertNotEquals(network, cm.activeNetwork, wifiDefaultMessage)
 
-            doAsShell(NETWORK_SETTINGS) { cm.startCaptivePortalApp(network) }
+            val startPortalAppPermission =
+                    if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) CONNECTIVITY_INTERNAL
+                    else NETWORK_SETTINGS
+            doAsShell(startPortalAppPermission) { cm.startCaptivePortalApp(network) }
             assertTrue(portalContentRequestCv.block(TEST_TIMEOUT_MS), "The captive portal login " +
                     "page was still not fetched ${TEST_TIMEOUT_MS}ms after startCaptivePortalApp.")
 
diff --git a/tests/tests/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/tests/net/src/android/net/cts/ConnectivityManagerTest.java
index baf5c2e..d498ed9 100644
--- a/tests/tests/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/tests/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -40,6 +40,16 @@
 import static android.system.OsConstants.AF_UNSPEC;
 
 import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
+import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
 
 import android.annotation.NonNull;
 import android.app.Instrumentation;
@@ -78,17 +88,22 @@
 import android.os.VintfRuntimeInfo;
 import android.platform.test.annotations.AppModeFull;
 import android.provider.Settings;
-import android.test.AndroidTestCase;
 import android.text.TextUtils;
 import android.util.Log;
 import android.util.Pair;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.util.ArrayUtils;
 
 import libcore.io.Streams;
 
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
 import java.io.FileDescriptor;
 import java.io.IOException;
 import java.io.InputStream;
@@ -114,7 +129,8 @@
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
-public class ConnectivityManagerTest extends AndroidTestCase {
+@RunWith(AndroidJUnit4.class)
+public class ConnectivityManagerTest {
 
     private static final String TAG = ConnectivityManagerTest.class.getSimpleName();
 
@@ -126,7 +142,10 @@
     private static final int INTERVAL_KEEPALIVE_RETRY_MS = 500;
     private static final int MAX_KEEPALIVE_RETRY_COUNT = 3;
     private static final int MIN_KEEPALIVE_INTERVAL = 10;
-    private static final int NETWORK_CHANGE_METEREDNESS_TIMEOUT = 5000;
+
+    // Changing meteredness on wifi involves reconnecting, which can take several seconds (involves
+    // re-associating, DHCP...)
+    private static final int NETWORK_CHANGE_METEREDNESS_TIMEOUT = 30_000;
     private static final int NUM_TRIES_MULTIPATH_PREF_CHECK = 20;
     private static final long INTERVAL_MULTIPATH_PREF_CHECK_MS = 500;
     // device could have only one interface: data, wifi.
@@ -150,22 +169,19 @@
     private PackageManager mPackageManager;
     private final HashMap<Integer, NetworkConfig> mNetworks =
             new HashMap<Integer, NetworkConfig>();
-    boolean mWifiConnectAttempted;
+    boolean mWifiWasDisabled;
     private UiAutomation mUiAutomation;
     private CtsNetUtils mCtsNetUtils;
-    private boolean mShellPermissionIdentityAdopted;
 
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-        Looper.prepare();
-        mContext = getContext();
+    @Before
+    public void setUp() throws Exception {
         mInstrumentation = InstrumentationRegistry.getInstrumentation();
+        mContext = mInstrumentation.getContext();
         mCm = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
         mWifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
         mPackageManager = mContext.getPackageManager();
         mCtsNetUtils = new CtsNetUtils(mContext);
-        mWifiConnectAttempted = false;
+        mWifiWasDisabled = false;
 
         // Get com.android.internal.R.array.networkAttributes
         int resId = mContext.getResources().getIdentifier("networkAttributes", "array", "android");
@@ -182,20 +198,17 @@
             } catch (Exception e) {}
         }
         mUiAutomation = mInstrumentation.getUiAutomation();
-        mShellPermissionIdentityAdopted = false;
     }
 
-    @Override
-    protected void tearDown() throws Exception {
+    @After
+    public void tearDown() throws Exception {
         // Return WiFi to its original disabled state after tests that explicitly connect.
-        if (mWifiConnectAttempted) {
+        if (mWifiWasDisabled) {
             mCtsNetUtils.disconnectFromWifi(null);
         }
         if (mCtsNetUtils.cellConnectAttempted()) {
             mCtsNetUtils.disconnectFromCell();
         }
-        dropShellPermissionIdentity();
-        super.tearDown();
     }
 
     /**
@@ -204,13 +217,12 @@
      * automatically in tearDown().
      */
     private Network ensureWifiConnected() {
-        if (mWifiManager.isWifiEnabled()) {
-            return mCtsNetUtils.getWifiNetwork();
-        }
-        mWifiConnectAttempted = true;
+        mWifiWasDisabled = !mWifiManager.isWifiEnabled();
+        // Even if wifi is enabled, the network may not be connected or ready yet
         return mCtsNetUtils.connectToWifi();
     }
 
+    @Test
     public void testIsNetworkTypeValid() {
         assertTrue(ConnectivityManager.isNetworkTypeValid(ConnectivityManager.TYPE_MOBILE));
         assertTrue(ConnectivityManager.isNetworkTypeValid(ConnectivityManager.TYPE_WIFI));
@@ -240,12 +252,14 @@
 
     }
 
+    @Test
     public void testSetNetworkPreference() {
         // getNetworkPreference() and setNetworkPreference() are both deprecated so they do
         // not preform any action.  Verify they are at least still callable.
         mCm.setNetworkPreference(mCm.getNetworkPreference());
     }
 
+    @Test
     public void testGetActiveNetworkInfo() {
         NetworkInfo ni = mCm.getActiveNetworkInfo();
 
@@ -254,6 +268,7 @@
         assertTrue(ni.getState() == State.CONNECTED);
     }
 
+    @Test
     public void testGetActiveNetwork() {
         Network network = mCm.getActiveNetwork();
         assertNotNull("You must have an active network connection to complete CTS", network);
@@ -266,6 +281,7 @@
         assertTrue(ni.getState() == State.CONNECTED);
     }
 
+    @Test
     public void testGetNetworkInfo() {
         for (int type = -1; type <= ConnectivityManager.MAX_NETWORK_TYPE+1; type++) {
             if (shouldBeSupported(type)) {
@@ -284,6 +300,7 @@
         }
     }
 
+    @Test
     public void testGetAllNetworkInfo() {
         NetworkInfo[] ni = mCm.getAllNetworkInfo();
         assertTrue(ni.length >= MIN_NUM_NETWORK_TYPES);
@@ -307,6 +324,7 @@
      * and that they are made from different IP addresses.
      */
     @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+    @Test
     public void testOpenConnection() throws Exception {
         boolean canRunTest = mPackageManager.hasSystemFeature(FEATURE_WIFI)
                 && mPackageManager.hasSystemFeature(FEATURE_TELEPHONY);
@@ -386,6 +404,7 @@
         } catch (UnsupportedOperationException expected) {}
     }
 
+    @Test
     public void testStartUsingNetworkFeature() {
 
         final String invalidateFeature = "invalidateFeature";
@@ -415,6 +434,7 @@
                (networkType == ConnectivityManager.TYPE_ETHERNET && shouldEthernetBeSupported());
     }
 
+    @Test
     public void testIsNetworkSupported() {
         for (int type = -1; type <= ConnectivityManager.MAX_NETWORK_TYPE; type++) {
             boolean supported = mCm.isNetworkSupported(type);
@@ -426,12 +446,14 @@
         }
     }
 
+    @Test
     public void testRequestRouteToHost() {
         for (int type = -1 ; type <= ConnectivityManager.MAX_NETWORK_TYPE; type++) {
             assertRequestRouteToHostUnsupported(type, HOST_ADDRESS);
         }
     }
 
+    @Test
     public void testTest() {
         mCm.getBackgroundDataSetting();
     }
@@ -452,6 +474,7 @@
      * that it would increase test coverage by much (how many devices have 3G radio but not Wifi?).
      */
     @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+    @Test
     public void testRegisterNetworkCallback() {
         if (!mPackageManager.hasSystemFeature(FEATURE_WIFI)) {
             Log.i(TAG, "testRegisterNetworkCallback cannot execute unless device supports WiFi");
@@ -493,6 +516,7 @@
      * of a {@code NetworkCallback}.
      */
     @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+    @Test
     public void testRegisterNetworkCallback_withPendingIntent() {
         if (!mPackageManager.hasSystemFeature(FEATURE_WIFI)) {
             Log.i(TAG, "testRegisterNetworkCallback cannot execute unless device supports WiFi");
@@ -538,6 +562,7 @@
      * see if we get a callback for an INTERNET request.
      */
     @AppModeFull(reason = "CHANGE_NETWORK_STATE permission can't be granted to instant apps")
+    @Test
     public void testRequestNetworkCallback() {
         final TestNetworkCallback callback = new TestNetworkCallback();
         mCm.requestNetwork(new NetworkRequest.Builder()
@@ -561,6 +586,7 @@
      * fail. Use WIFI and switch Wi-Fi off.
      */
     @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+    @Test
     public void testRequestNetworkCallback_onUnavailable() {
         final boolean previousWifiEnabledState = mWifiManager.isWifiEnabled();
         if (previousWifiEnabledState) {
@@ -598,6 +624,7 @@
 
     /** Verify restricted networks cannot be requested. */
     @AppModeFull(reason = "CHANGE_NETWORK_STATE permission can't be granted to instant apps")
+    @Test
     public void testRestrictedNetworks() {
         // Verify we can request unrestricted networks:
         NetworkRequest request = new NetworkRequest.Builder()
@@ -719,6 +746,7 @@
      * for metered and unmetered networks.
      */
     @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+    @Test
     public void testGetMultipathPreference() throws Exception {
         final ContentResolver resolver = mContext.getContentResolver();
         ensureWifiConnected();
@@ -887,18 +915,6 @@
                 keepalivesPerTransport, nc);
     }
 
-    private void adoptShellPermissionIdentity() {
-        mUiAutomation.adoptShellPermissionIdentity();
-        mShellPermissionIdentityAdopted = true;
-    }
-
-    private void dropShellPermissionIdentity() {
-        if (mShellPermissionIdentityAdopted) {
-            mUiAutomation.dropShellPermissionIdentity();
-            mShellPermissionIdentityAdopted = false;
-        }
-    }
-
     private static boolean isTcpKeepaliveSupportedByKernel() {
         final String kVersionString = VintfRuntimeInfo.getKernelVersion();
         return compareMajorMinorVersion(kVersionString, "4.8") >= 0;
@@ -933,6 +949,7 @@
      * Verifies that version string compare logic returns expected result for various cases.
      * Note that only major and minor number are compared.
      */
+    @Test
     public void testMajorMinorVersionCompare() {
         assertEquals(0, compareMajorMinorVersion("4.8.1", "4.8"));
         assertEquals(1, compareMajorMinorVersion("4.9", "4.8.1"));
@@ -952,6 +969,7 @@
      * keepalives is set to 0.
      */
     @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+    @Test
     public void testKeepaliveWifiUnsupported() throws Exception {
         if (!mPackageManager.hasSystemFeature(FEATURE_WIFI)) {
             Log.i(TAG, "testKeepaliveUnsupported cannot execute unless device"
@@ -961,32 +979,36 @@
 
         final Network network = ensureWifiConnected();
         if (getSupportedKeepalivesForNet(network) != 0) return;
+        final InetAddress srcAddr = getFirstV4Address(network);
+        assumeTrue("This test requires native IPv4", srcAddr != null);
 
-        adoptShellPermissionIdentity();
-
-        assertEquals(0, createConcurrentSocketKeepalives(network, 1, 0));
-        assertEquals(0, createConcurrentSocketKeepalives(network, 0, 1));
-
-        dropShellPermissionIdentity();
+        runWithShellPermissionIdentity(() -> {
+            assertEquals(0, createConcurrentSocketKeepalives(network, srcAddr, 1, 0));
+            assertEquals(0, createConcurrentSocketKeepalives(network, srcAddr, 0, 1));
+        });
     }
 
     @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+    @Test
     public void testCreateTcpKeepalive() throws Exception {
         if (!mPackageManager.hasSystemFeature(FEATURE_WIFI)) {
             Log.i(TAG, "testCreateTcpKeepalive cannot execute unless device supports WiFi");
             return;
         }
 
-        adoptShellPermissionIdentity();
-
         final Network network = ensureWifiConnected();
         if (getSupportedKeepalivesForNet(network) == 0) return;
+        final InetAddress srcAddr = getFirstV4Address(network);
+        assumeTrue("This test requires native IPv4", srcAddr != null);
+
         // If kernel < 4.8 then it doesn't support TCP keepalive, but it might still support
         // NAT-T keepalive. If keepalive limits from resource overlay is not zero, TCP keepalive
         // needs to be supported except if the kernel doesn't support it.
         if (!isTcpKeepaliveSupportedByKernel()) {
             // Sanity check to ensure the callback result is expected.
-            assertEquals(0, createConcurrentSocketKeepalives(network, 0, 1));
+            runWithShellPermissionIdentity(() -> {
+                assertEquals(0, createConcurrentSocketKeepalives(network, srcAddr, 0, 1));
+            });
             Log.i(TAG, "testCreateTcpKeepalive is skipped for kernel "
                     + VintfRuntimeInfo.getKernelVersion());
             return;
@@ -1000,6 +1022,8 @@
             // Should able to start keep alive offload when socket is idle.
             final Executor executor = mContext.getMainExecutor();
             final TestSocketKeepaliveCallback callback = new TestSocketKeepaliveCallback();
+
+            mUiAutomation.adoptShellPermissionIdentity();
             try (SocketKeepalive sk = mCm.createSocketKeepalive(network, s, executor, callback)) {
                 sk.start(MIN_KEEPALIVE_INTERVAL);
                 callback.expectStarted();
@@ -1021,6 +1045,8 @@
                 // Stop.
                 sk.stop();
                 callback.expectStopped();
+            } finally {
+                mUiAutomation.dropShellPermissionIdentity();
             }
 
             // Ensure socket is still connected.
@@ -1049,9 +1075,12 @@
 
             // Should get ERROR_SOCKET_NOT_IDLE because there is still data in the receive queue
             // that has not been read.
+            mUiAutomation.adoptShellPermissionIdentity();
             try (SocketKeepalive sk = mCm.createSocketKeepalive(network, s, executor, callback)) {
                 sk.start(MIN_KEEPALIVE_INTERVAL);
                 callback.expectError(SocketKeepalive.ERROR_SOCKET_NOT_IDLE);
+            } finally {
+                mUiAutomation.dropShellPermissionIdentity();
             }
         }
     }
@@ -1096,7 +1125,7 @@
     }
 
     private @NonNull ArrayList<SocketKeepalive> createConcurrentNattSocketKeepalives(
-            @NonNull Network network, int requestCount,
+            @NonNull Network network, @NonNull InetAddress srcAddr, int requestCount,
             @NonNull TestSocketKeepaliveCallback callback)  throws Exception {
 
         final Executor executor = mContext.getMainExecutor();
@@ -1104,7 +1133,6 @@
         // Initialize a real NaT-T socket.
         final IpSecManager mIpSec = (IpSecManager) mContext.getSystemService(Context.IPSEC_SERVICE);
         final UdpEncapsulationSocket nattSocket = mIpSec.openUdpEncapsulationSocket();
-        final InetAddress srcAddr = getFirstV4Address(network);
         final InetAddress dstAddr = getAddrByName(TEST_HOST, AF_INET);
         assertNotNull(srcAddr);
         assertNotNull(dstAddr);
@@ -1145,11 +1173,12 @@
      * @return the total number of keepalives created.
      */
     private int createConcurrentSocketKeepalives(
-            @NonNull Network network, int nattCount, int tcpCount) throws Exception {
+            @NonNull Network network, @NonNull InetAddress srcAddr, int nattCount, int tcpCount)
+            throws Exception {
         final ArrayList<SocketKeepalive> kalist = new ArrayList<>();
         final TestSocketKeepaliveCallback callback = new TestSocketKeepaliveCallback();
 
-        kalist.addAll(createConcurrentNattSocketKeepalives(network, nattCount, callback));
+        kalist.addAll(createConcurrentNattSocketKeepalives(network, srcAddr, nattCount, callback));
         kalist.addAll(createConcurrentTcpSocketKeepalives(network, tcpCount, callback));
 
         final int ret = kalist.size();
@@ -1169,6 +1198,7 @@
      * get leaked after iterations.
      */
     @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+    @Test
     public void testSocketKeepaliveLimitWifi() throws Exception {
         if (!mPackageManager.hasSystemFeature(FEATURE_WIFI)) {
             Log.i(TAG, "testSocketKeepaliveLimitWifi cannot execute unless device"
@@ -1181,33 +1211,39 @@
         if (supported == 0) {
             return;
         }
+        final InetAddress srcAddr = getFirstV4Address(network);
+        assumeTrue("This test requires native IPv4", srcAddr != null);
 
-        adoptShellPermissionIdentity();
+        runWithShellPermissionIdentity(() -> {
+            // Verifies that the supported keepalive slots meet MIN_SUPPORTED_KEEPALIVE_COUNT.
+            assertGreaterOrEqual(supported, MIN_SUPPORTED_WIFI_KEEPALIVE_COUNT);
 
-        // Verifies that the supported keepalive slots meet MIN_SUPPORTED_KEEPALIVE_COUNT.
-        assertGreaterOrEqual(supported, MIN_SUPPORTED_WIFI_KEEPALIVE_COUNT);
-
-        // Verifies that Nat-T keepalives can be established.
-        assertEquals(supported, createConcurrentSocketKeepalives(network, supported + 1, 0));
-        // Verifies that keepalives don't get leaked in second round.
-        assertEquals(supported, createConcurrentSocketKeepalives(network, supported, 0));
+            // Verifies that Nat-T keepalives can be established.
+            assertEquals(supported, createConcurrentSocketKeepalives(network, srcAddr,
+                    supported + 1, 0));
+            // Verifies that keepalives don't get leaked in second round.
+            assertEquals(supported, createConcurrentSocketKeepalives(network, srcAddr, supported,
+                    0));
+        });
 
         // If kernel < 4.8 then it doesn't support TCP keepalive, but it might still support
         // NAT-T keepalive. Test below cases only if TCP keepalive is supported by kernel.
-        if (isTcpKeepaliveSupportedByKernel()) {
-            assertEquals(supported, createConcurrentSocketKeepalives(network, 0, supported + 1));
+        if (!isTcpKeepaliveSupportedByKernel()) return;
+
+        runWithShellPermissionIdentity(() -> {
+            assertEquals(supported, createConcurrentSocketKeepalives(network, srcAddr, 0,
+                    supported + 1));
 
             // Verifies that different types can be established at the same time.
-            assertEquals(supported, createConcurrentSocketKeepalives(network,
+            assertEquals(supported, createConcurrentSocketKeepalives(network, srcAddr,
                     supported / 2, supported - supported / 2));
 
             // Verifies that keepalives don't get leaked in second round.
-            assertEquals(supported, createConcurrentSocketKeepalives(network, 0, supported));
-            assertEquals(supported, createConcurrentSocketKeepalives(network,
+            assertEquals(supported, createConcurrentSocketKeepalives(network, srcAddr, 0,
+                    supported));
+            assertEquals(supported, createConcurrentSocketKeepalives(network, srcAddr,
                     supported / 2, supported - supported / 2));
-        }
-
-        dropShellPermissionIdentity();
+        });
     }
 
     /**
@@ -1215,6 +1251,7 @@
      * don't get leaked after iterations.
      */
     @AppModeFull(reason = "Cannot request network in instant app mode")
+    @Test
     public void testSocketKeepaliveLimitTelephony() throws Exception {
         if (!mPackageManager.hasSystemFeature(FEATURE_TELEPHONY)) {
             Log.i(TAG, "testSocketKeepaliveLimitTelephony cannot execute unless device"
@@ -1231,18 +1268,19 @@
 
         final Network network = mCtsNetUtils.connectToCell();
         final int supported = getSupportedKeepalivesForNet(network);
+        final InetAddress srcAddr = getFirstV4Address(network);
+        assumeTrue("This test requires native IPv4", srcAddr != null);
 
-        adoptShellPermissionIdentity();
-
-        // Verifies that the supported keepalive slots meet minimum requirement.
-        assertGreaterOrEqual(supported, MIN_SUPPORTED_CELLULAR_KEEPALIVE_COUNT);
-
-        // Verifies that Nat-T keepalives can be established.
-        assertEquals(supported, createConcurrentSocketKeepalives(network, supported + 1, 0));
-        // Verifies that keepalives don't get leaked in second round.
-        assertEquals(supported, createConcurrentSocketKeepalives(network, supported, 0));
-
-        dropShellPermissionIdentity();
+        runWithShellPermissionIdentity(() -> {
+            // Verifies that the supported keepalive slots meet minimum requirement.
+            assertGreaterOrEqual(supported, MIN_SUPPORTED_CELLULAR_KEEPALIVE_COUNT);
+            // Verifies that Nat-T keepalives can be established.
+            assertEquals(supported, createConcurrentSocketKeepalives(network, srcAddr,
+                    supported + 1, 0));
+            // Verifies that keepalives don't get leaked in second round.
+            assertEquals(supported, createConcurrentSocketKeepalives(network, srcAddr, supported,
+                    0));
+        });
     }
 
     private int getIntResourceForName(@NonNull String resName) {
@@ -1255,6 +1293,7 @@
      * Verifies that the keepalive slots are limited as customized for unprivileged requests.
      */
     @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+    @Test
     public void testSocketKeepaliveUnprivileged() throws Exception {
         if (!mPackageManager.hasSystemFeature(FEATURE_WIFI)) {
             Log.i(TAG, "testSocketKeepaliveUnprivileged cannot execute unless device"
@@ -1267,6 +1306,8 @@
         if (supported == 0) {
             return;
         }
+        final InetAddress srcAddr = getFirstV4Address(network);
+        assumeTrue("This test requires native IPv4", srcAddr != null);
 
         // Resource ID might be shifted on devices that compiled with different symbols.
         // Thus, resolve ID at runtime is needed.
@@ -1282,7 +1323,7 @@
         final int expectedUnprivileged =
                 Math.min(allowedUnprivilegedPerUid, supported - reservedPrivilegedSlots);
         assertEquals(expectedUnprivileged,
-                createConcurrentSocketKeepalives(network, supported + 1, 0));
+                createConcurrentSocketKeepalives(network, srcAddr, supported + 1, 0));
     }
 
     private static void assertGreaterOrEqual(long greater, long lesser) {
@@ -1296,6 +1337,7 @@
      * See. b/144679405.
      */
     @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+    @Test
     public void testRestrictedNetworkPermission() throws Exception {
         // Ensure that CONNECTIVITY_USE_RESTRICTED_NETWORKS isn't granted to this package.
         final PackageInfo app = mPackageManager.getPackageInfo(mContext.getPackageName(),
diff --git a/tests/tests/net/src/android/net/cts/DnsResolverTest.java b/tests/tests/net/src/android/net/cts/DnsResolverTest.java
index 1cc49f9..28753ff 100644
--- a/tests/tests/net/src/android/net/cts/DnsResolverTest.java
+++ b/tests/tests/net/src/android/net/cts/DnsResolverTest.java
@@ -86,7 +86,6 @@
     static final int CANCEL_RETRY_TIMES = 5;
     static final int QUERY_TIMES = 10;
     static final int NXDOMAIN = 3;
-    static final int PRIVATE_DNS_SETTING_TIMEOUT_MS = 6_000;
 
     private ContentResolver mCR;
     private ConnectivityManager mCM;
@@ -107,32 +106,15 @@
         mExecutorInline = (Runnable r) -> r.run();
         mCR = getContext().getContentResolver();
         mCtsNetUtils = new CtsNetUtils(getContext());
-        storePrivateDnsSetting();
+        mCtsNetUtils.storePrivateDnsSetting();
     }
 
     @Override
     protected void tearDown() throws Exception {
-        restorePrivateDnsSetting();
+        mCtsNetUtils.restorePrivateDnsSetting();
         super.tearDown();
     }
 
-    private void storePrivateDnsSetting() {
-        // Store private DNS setting
-        mOldMode = Settings.Global.getString(mCR, Settings.Global.PRIVATE_DNS_MODE);
-        mOldDnsSpecifier = Settings.Global.getString(mCR, Settings.Global.PRIVATE_DNS_SPECIFIER);
-    }
-
-    private void restorePrivateDnsSetting() throws InterruptedException {
-        // restore private DNS setting
-        Settings.Global.putString(mCR, Settings.Global.PRIVATE_DNS_MODE, mOldMode);
-        if ("hostname".equals(mOldMode)) {
-            Settings.Global.putString(
-                mCR, Settings.Global.PRIVATE_DNS_SPECIFIER, mOldDnsSpecifier);
-            mCtsNetUtils.awaitPrivateDnsSetting("restorePrivateDnsSetting timeout",
-                    mCM.getActiveNetwork(), mOldDnsSpecifier, PRIVATE_DNS_SETTING_TIMEOUT_MS, true);
-        }
-    }
-
     private static String byteArrayToHexString(byte[] bytes) {
         char[] hexChars = new char[bytes.length * 2];
         for (int i = 0; i < bytes.length; ++i) {
@@ -416,16 +398,13 @@
         final String msg = "RawQuery " + TEST_NX_DOMAIN + " with private DNS";
         // Enable private DNS strict mode and set server to dns.google before doing NxDomain test.
         // b/144521720
-        Settings.Global.putString(mCR, Settings.Global.PRIVATE_DNS_MODE, "hostname");
-        Settings.Global.putString(mCR,
-                Settings.Global.PRIVATE_DNS_SPECIFIER, GOOGLE_PRIVATE_DNS_SERVER);
+        mCtsNetUtils.setPrivateDnsStrictMode(GOOGLE_PRIVATE_DNS_SERVER);
         for (Network network :  getTestableNetworks()) {
             final Network networkForPrivateDns =
                     (network != null) ? network : mCM.getActiveNetwork();
             assertNotNull("Can't find network to await private DNS on", networkForPrivateDns);
             mCtsNetUtils.awaitPrivateDnsSetting(msg + " wait private DNS setting timeout",
-                    networkForPrivateDns, GOOGLE_PRIVATE_DNS_SERVER,
-                    PRIVATE_DNS_SETTING_TIMEOUT_MS, true);
+                    networkForPrivateDns, GOOGLE_PRIVATE_DNS_SERVER, true);
             final VerifyCancelCallback callback = new VerifyCancelCallback(msg);
             mDns.rawQuery(network, TEST_NX_DOMAIN, CLASS_IN, TYPE_AAAA, FLAG_NO_CACHE_LOOKUP,
                     executor, null, callback);
@@ -688,9 +667,7 @@
         final Network[] testNetworks = getTestableNetworks();
 
         // Set an invalid private DNS server
-        Settings.Global.putString(mCR, Settings.Global.PRIVATE_DNS_MODE, "hostname");
-        Settings.Global.putString(mCR,
-                Settings.Global.PRIVATE_DNS_SPECIFIER, INVALID_PRIVATE_DNS_SERVER);
+        mCtsNetUtils.setPrivateDnsStrictMode(INVALID_PRIVATE_DNS_SERVER);
         final String msg = "Test PrivateDnsBypass " + TEST_DOMAIN;
         for (Network network : testNetworks) {
             // This test cannot be ran with null network because we need to explicitly pass a
@@ -699,7 +676,7 @@
 
             // wait for private DNS setting propagating
             mCtsNetUtils.awaitPrivateDnsSetting(msg + " wait private DNS setting timeout",
-                    network, INVALID_PRIVATE_DNS_SERVER, PRIVATE_DNS_SETTING_TIMEOUT_MS, false);
+                    network, INVALID_PRIVATE_DNS_SERVER, false);
 
             final CountDownLatch latch = new CountDownLatch(1);
             final DnsResolver.Callback<List<InetAddress>> errorCallback =
diff --git a/tests/tests/net/src/android/net/cts/MultinetworkApiTest.java b/tests/tests/net/src/android/net/cts/MultinetworkApiTest.java
index f123187..985e313 100644
--- a/tests/tests/net/src/android/net/cts/MultinetworkApiTest.java
+++ b/tests/tests/net/src/android/net/cts/MultinetworkApiTest.java
@@ -41,7 +41,6 @@
 
     private static final String TAG = "MultinetworkNativeApiTest";
     static final String GOOGLE_PRIVATE_DNS_SERVER = "dns.google";
-    static final int PRIVATE_DNS_SETTING_TIMEOUT_MS = 2_000;
 
     /**
      * @return 0 on success
@@ -69,7 +68,7 @@
         mCM = (ConnectivityManager) getContext().getSystemService(Context.CONNECTIVITY_SERVICE);
         mCR = getContext().getContentResolver();
         mCtsNetUtils = new CtsNetUtils(getContext());
-        storePrivateDnsSetting();
+        mCtsNetUtils.storePrivateDnsSetting();
     }
 
     @Override
@@ -77,18 +76,6 @@
         super.tearDown();
     }
 
-    private void storePrivateDnsSetting() {
-        // Store private DNS setting
-        mOldMode = Settings.Global.getString(mCR, Settings.Global.PRIVATE_DNS_MODE);
-        mOldDnsSpecifier = Settings.Global.getString(mCR, Settings.Global.PRIVATE_DNS_SPECIFIER);
-    }
-
-    private void restorePrivateDnsSetting() {
-        // restore private DNS setting
-        Settings.Global.putString(mCR, Settings.Global.PRIVATE_DNS_MODE, mOldMode);
-        Settings.Global.putString(mCR, Settings.Global.PRIVATE_DNS_SPECIFIER, mOldDnsSpecifier);
-    }
-
     private Network[] getTestableNetworks() {
         final ArrayList<Network> testableNetworks = new ArrayList<Network>();
         for (Network network : mCM.getAllNetworks()) {
@@ -239,17 +226,15 @@
         // Enable private DNS strict mode and set server to dns.google before doing NxDomain test.
         // b/144521720
         try {
-            Settings.Global.putString(mCR, Settings.Global.PRIVATE_DNS_MODE, "hostname");
-            Settings.Global.putString(mCR,
-                    Settings.Global.PRIVATE_DNS_SPECIFIER, GOOGLE_PRIVATE_DNS_SERVER);
+            mCtsNetUtils.setPrivateDnsStrictMode(GOOGLE_PRIVATE_DNS_SERVER);
             for (Network network : getTestableNetworks()) {
               // Wait for private DNS setting to propagate.
               mCtsNetUtils.awaitPrivateDnsSetting("NxDomain test wait private DNS setting timeout",
-                        network, GOOGLE_PRIVATE_DNS_SERVER, PRIVATE_DNS_SETTING_TIMEOUT_MS, true);
+                        network, GOOGLE_PRIVATE_DNS_SERVER, true);
               runResNnxDomainCheck(network.getNetworkHandle());
             }
         } finally {
-            restorePrivateDnsSetting();
+            mCtsNetUtils.restorePrivateDnsSetting();
         }
     }
 }
diff --git a/tests/tests/net/src/android/net/cts/NetworkRequestTest.java b/tests/tests/net/src/android/net/cts/NetworkRequestTest.java
index e8af1b3..d118c8a 100644
--- a/tests/tests/net/src/android/net/cts/NetworkRequestTest.java
+++ b/tests/tests/net/src/android/net/cts/NetworkRequestTest.java
@@ -87,7 +87,7 @@
         verifyNoCapabilities(nr);
     }
 
-    @Test
+    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
     public void testTemporarilyNotMeteredCapability() {
         assertTrue(new NetworkRequest.Builder()
                 .addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED).build()
diff --git a/tests/tests/net/util/java/android/net/cts/util/CtsNetUtils.java b/tests/tests/net/util/java/android/net/cts/util/CtsNetUtils.java
index 824146f..f39b184 100644
--- a/tests/tests/net/util/java/android/net/cts/util/CtsNetUtils.java
+++ b/tests/tests/net/util/java/android/net/cts/util/CtsNetUtils.java
@@ -17,6 +17,7 @@
 package android.net.cts.util;
 
 import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.net.ConnectivityManager.PRIVATE_DNS_MODE_OPPORTUNISTIC;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 
@@ -28,6 +29,7 @@
 import android.annotation.NonNull;
 import android.content.BroadcastReceiver;
 import android.content.Context;
+import android.content.ContentResolver;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.net.ConnectivityManager;
@@ -39,6 +41,7 @@
 import android.net.NetworkInfo.State;
 import android.net.NetworkRequest;
 import android.net.wifi.WifiManager;
+import android.provider.Settings;
 import android.system.Os;
 import android.system.OsConstants;
 import android.util.Log;
@@ -59,6 +62,7 @@
     private static final int SOCKET_TIMEOUT_MS = 2000;
     private static final int PRIVATE_DNS_PROBE_MS = 1_000;
 
+    public static final int PRIVATE_DNS_SETTING_TIMEOUT_MS = 6_000;
     public static final int HTTP_PORT = 80;
     public static final String TEST_HOST = "connectivitycheck.gstatic.com";
     public static final String HTTP_REQUEST =
@@ -69,15 +73,19 @@
     public static final String NETWORK_CALLBACK_ACTION =
             "ConnectivityManagerTest.NetworkCallbackAction";
 
-    private Context mContext;
-    private ConnectivityManager mCm;
-    private WifiManager mWifiManager;
+    private final Context mContext;
+    private final ConnectivityManager mCm;
+    private final ContentResolver mCR;
+    private final WifiManager mWifiManager;
     private TestNetworkCallback mCellNetworkCallback;
+    private String mOldPrivateDnsMode;
+    private String mOldPrivateDnsSpecifier;
 
     public CtsNetUtils(Context context) {
         mContext = context;
         mCm = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
         mWifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
+        mCR = context.getContentResolver();
     }
 
     // Toggle WiFi twice, leaving it in the state it started in
@@ -249,9 +257,51 @@
         return s;
     }
 
+    public void storePrivateDnsSetting() {
+        // Store private DNS setting
+        mOldPrivateDnsMode = Settings.Global.getString(mCR, Settings.Global.PRIVATE_DNS_MODE);
+        mOldPrivateDnsSpecifier = Settings.Global.getString(mCR,
+                Settings.Global.PRIVATE_DNS_SPECIFIER);
+        // It's possible that there is no private DNS default value in Settings.
+        // Give it a proper default mode which is opportunistic mode.
+        if (mOldPrivateDnsMode == null) {
+            mOldPrivateDnsSpecifier = "";
+            mOldPrivateDnsMode = PRIVATE_DNS_MODE_OPPORTUNISTIC;
+            Settings.Global.putString(mCR,
+                    Settings.Global.PRIVATE_DNS_SPECIFIER, mOldPrivateDnsSpecifier);
+            Settings.Global.putString(mCR, Settings.Global.PRIVATE_DNS_MODE, mOldPrivateDnsMode);
+        }
+    }
+
+    public void restorePrivateDnsSetting() throws InterruptedException {
+        if (mOldPrivateDnsMode == null || mOldPrivateDnsSpecifier == null) {
+            return;
+        }
+        // restore private DNS setting
+        if ("hostname".equals(mOldPrivateDnsMode)) {
+            setPrivateDnsStrictMode(mOldPrivateDnsSpecifier);
+            awaitPrivateDnsSetting("restorePrivateDnsSetting timeout",
+                    mCm.getActiveNetwork(),
+                    mOldPrivateDnsSpecifier, true);
+        } else {
+            Settings.Global.putString(mCR, Settings.Global.PRIVATE_DNS_MODE, mOldPrivateDnsMode);
+        }
+    }
+
+    public void setPrivateDnsStrictMode(String server) {
+        // To reduce flake rate, set PRIVATE_DNS_SPECIFIER before PRIVATE_DNS_MODE. This ensures
+        // that if the previous private DNS mode was not "hostname", the system only sees one
+        // EVENT_PRIVATE_DNS_SETTINGS_CHANGED event instead of two.
+        Settings.Global.putString(mCR, Settings.Global.PRIVATE_DNS_SPECIFIER, server);
+        final String mode = Settings.Global.getString(mCR, Settings.Global.PRIVATE_DNS_MODE);
+        // If current private DNS mode is "hostname", we only need to set PRIVATE_DNS_SPECIFIER.
+        if (!"hostname".equals(mode)) {
+            Settings.Global.putString(mCR, Settings.Global.PRIVATE_DNS_MODE, "hostname");
+        }
+    }
+
     public void awaitPrivateDnsSetting(@NonNull String msg, @NonNull Network network,
-            @NonNull String server, int timeoutMs,
-            boolean requiresValidatedServers) throws InterruptedException {
+            @NonNull String server, boolean requiresValidatedServers) throws InterruptedException {
         CountDownLatch latch = new CountDownLatch(1);
         NetworkRequest request = new NetworkRequest.Builder().clearCapabilities().build();
         NetworkCallback callback = new NetworkCallback() {
@@ -266,7 +316,7 @@
             }
         };
         mCm.registerNetworkCallback(request, callback);
-        assertTrue(msg, latch.await(timeoutMs, TimeUnit.MILLISECONDS));
+        assertTrue(msg, latch.await(PRIVATE_DNS_SETTING_TIMEOUT_MS, TimeUnit.MILLISECONDS));
         mCm.unregisterNetworkCallback(callback);
         // Wait some time for NetworkMonitor's private DNS probe to complete. If we do not do
         // this, then the test could complete before the NetworkMonitor private DNS probe
diff --git a/tests/tests/os/src/android/os/cts/FileObserverTest.java b/tests/tests/os/src/android/os/cts/FileObserverTest.java
index e758dd4..b853f89 100644
--- a/tests/tests/os/src/android/os/cts/FileObserverTest.java
+++ b/tests/tests/os/src/android/os/cts/FileObserverTest.java
@@ -16,6 +16,7 @@
 
 package android.os.cts;
 
+import android.os.Environment;
 import android.os.FileObserver;
 import android.platform.test.annotations.AppModeFull;
 import android.platform.test.annotations.AppModeInstant;
@@ -58,6 +59,9 @@
         if (!InstrumentationRegistry.getTargetContext().getPackageManager().isInstantApp()) {
             dir = getContext().getExternalFilesDir(null);
             helpSetUp(dir);
+
+            dir = Environment.getExternalStorageDirectory();
+            helpSetUp(dir);
         }
     }
 
@@ -91,6 +95,9 @@
 
         dir = getContext().getExternalFilesDir(null);
         helpTearDown(dir);
+
+        dir = Environment.getExternalStorageDirectory();
+        helpTearDown(dir);
     }
 
     public void testConstructor() {
@@ -247,11 +254,17 @@
     }
 
     @AppModeFull(reason = "Instant apps cannot access external storage")
+    public void testFileObserverExternalStorageDirectory() throws Exception {
+        helpTestFileObserver(Environment.getExternalStorageDirectory(), false);
+    }
+
+    @AppModeFull(reason = "Instant apps cannot access external storage")
     public void testFileObserver_multipleFilesFull() throws Exception {
         verifyMultipleFiles(
                 Pair.create(getContext().getCacheDir(), false),
                 Pair.create(getContext().getFilesDir(), false),
-                Pair.create(getContext().getExternalFilesDir(null), true)
+                Pair.create(getContext().getExternalFilesDir(null), true),
+                Pair.create(Environment.getExternalStorageDirectory(), false)
         );
     }
 
diff --git a/tests/tests/permission2/res/raw/android_manifest.xml b/tests/tests/permission2/res/raw/android_manifest.xml
index dcc3aef..abc63a5 100644
--- a/tests/tests/permission2/res/raw/android_manifest.xml
+++ b/tests/tests/permission2/res/raw/android_manifest.xml
@@ -3778,6 +3778,11 @@
     <permission android:name="android.permission.OBSERVE_ROLE_HOLDERS"
                 android:protectionLevel="signature|installer" />
 
+    <!-- Allows an application to manage the companion devices.
+         @hide -->
+    <permission android:name="android.permission.MANAGE_COMPANION_DEVICES"
+                android:protectionLevel="signature" />
+
     <!-- @SystemApi Allows an application to use SurfaceFlinger's low level features.
          <p>Not for use by third-party applications.
          @hide
diff --git a/tests/tests/permission2/src/android/permission2/cts/RestrictedPermissionsTest.java b/tests/tests/permission2/src/android/permission2/cts/RestrictedPermissionsTest.java
index abbc029..b44cfcb 100644
--- a/tests/tests/permission2/src/android/permission2/cts/RestrictedPermissionsTest.java
+++ b/tests/tests/permission2/src/android/permission2/cts/RestrictedPermissionsTest.java
@@ -50,7 +50,7 @@
 import androidx.annotation.NonNull;
 import androidx.test.platform.app.InstrumentationRegistry;
 
-import com.android.compatibility.common.util.SystemUtil.ThrowingRunnable;
+import com.android.compatibility.common.util.ThrowingRunnable;
 
 import org.junit.After;
 import org.junit.AfterClass;
@@ -691,7 +691,7 @@
         InstrumentationRegistry.getInstrumentation().getUiAutomation()
                 .adoptShellPermissionIdentity();
         try {
-            command.runOrThrow();
+            command.run();
         } finally {
             InstrumentationRegistry.getInstrumentation().getUiAutomation()
                     .dropShellPermissionIdentity();
diff --git a/tests/tests/permission2/src/android/permission2/cts/RestrictedStoragePermissionTest.java b/tests/tests/permission2/src/android/permission2/cts/RestrictedStoragePermissionTest.java
index de7e36e..b73ae5d 100644
--- a/tests/tests/permission2/src/android/permission2/cts/RestrictedStoragePermissionTest.java
+++ b/tests/tests/permission2/src/android/permission2/cts/RestrictedStoragePermissionTest.java
@@ -39,7 +39,7 @@
 import androidx.annotation.NonNull;
 import androidx.test.platform.app.InstrumentationRegistry;
 
-import com.android.compatibility.common.util.SystemUtil.ThrowingRunnable;
+import com.android.compatibility.common.util.ThrowingRunnable;
 
 import org.junit.After;
 import org.junit.Test;
@@ -668,7 +668,7 @@
                 .getUiAutomation()
                 .adoptShellPermissionIdentity();
         try {
-            command.runOrThrow();
+            command.run();
         } finally {
             InstrumentationRegistry.getInstrumentation()
                     .getUiAutomation()
diff --git a/tests/tests/permission3/src/android/permission3/cts/BaseUsePermissionTest.kt b/tests/tests/permission3/src/android/permission3/cts/BaseUsePermissionTest.kt
index c5166db..d4f29c0 100644
--- a/tests/tests/permission3/src/android/permission3/cts/BaseUsePermissionTest.kt
+++ b/tests/tests/permission3/src/android/permission3/cts/BaseUsePermissionTest.kt
@@ -271,8 +271,10 @@
         click(By.res("com.android.permissioncontroller:id/permission_allow_button"))
 
     protected fun clickPermissionRequestSettingsLinkAndAllowAlways() {
-        clickPermissionRequestSettingsLink()
-        clickAllowAlwaysInSettings()
+        eventually({
+            clickPermissionRequestSettingsLink()
+            clickAllowAlwaysInSettings()
+        }, TIMEOUT_MILLIS * 2)
         pressBack()
     }
 
@@ -294,15 +296,17 @@
 
     protected fun clickPermissionRequestSettingsLink() {
         waitForIdle()
-        // UiObject2 doesn't expose CharSequence.
-        val node = uiAutomation.rootInActiveWindow.findAccessibilityNodeInfosByViewId(
-            "com.android.permissioncontroller:id/detail_message"
-        )[0]
-        assertTrue(node.isVisibleToUser)
-        val text = node.text as Spanned
-        val clickableSpan = text.getSpans(0, text.length, ClickableSpan::class.java)[0]
-        // We could pass in null here in Java, but we need an instance in Kotlin.
-        clickableSpan.onClick(View(context))
+        eventually {
+            // UiObject2 doesn't expose CharSequence.
+            val node = uiAutomation.rootInActiveWindow.findAccessibilityNodeInfosByViewId(
+                    "com.android.permissioncontroller:id/detail_message"
+            )[0]
+            assertTrue(node.isVisibleToUser)
+            val text = node.text as Spanned
+            val clickableSpan = text.getSpans(0, text.length, ClickableSpan::class.java)[0]
+            // We could pass in null here in Java, but we need an instance in Kotlin.
+            clickableSpan.onClick(View(context))
+        }
         waitForIdle()
     }
 
diff --git a/tests/tests/shortcutmanager/src/android/content/pm/cts/shortcutmanager/ShortcutManagerClientApiTest.java b/tests/tests/shortcutmanager/src/android/content/pm/cts/shortcutmanager/ShortcutManagerClientApiTest.java
index 4e3c1e8..56a2579 100644
--- a/tests/tests/shortcutmanager/src/android/content/pm/cts/shortcutmanager/ShortcutManagerClientApiTest.java
+++ b/tests/tests/shortcutmanager/src/android/content/pm/cts/shortcutmanager/ShortcutManagerClientApiTest.java
@@ -1903,6 +1903,79 @@
                 });
     }
 
+    public void testGetShortcuts() {
+        runWithCallerWithStrictMode(mPackageContext1, () -> {
+            enableManifestActivity("Launcher_manifest_2", true);
+            retryUntil(() -> getManager().getManifestShortcuts().size() == 2,
+                    "Manifest shortcuts didn't show up");
+
+            assertTrue(getManager().setDynamicShortcuts(list(
+                    makeShortcut("s1"), makeShortcut("s2"), makeShortcut("s3"))));
+        });
+
+        setDefaultLauncher(getInstrumentation(), mLauncherContext1);
+        runWithCallerWithStrictMode(mLauncherContext1, () -> {
+            getLauncherApps().pinShortcuts(mPackageContext1.getPackageName(),
+                    list("ms21", "s1", "s2"), getUserHandle());
+            // TODO: Cache a few shortcuts
+        });
+
+        runWithCallerWithStrictMode(mPackageContext1, () -> {
+            getManager().removeDynamicShortcuts((list("s2")));
+        });
+
+        runWithCallerWithStrictMode(mPackageContext1, () -> {
+            assertWith(getManager().getShortcuts(ShortcutManager.FLAG_MATCH_MANIFEST))
+                    .haveIds("ms21", "ms22")
+                    .areAllManifest();
+            assertWith(getManager().getShortcuts(ShortcutManager.FLAG_MATCH_DYNAMIC))
+                    .haveIds("s1", "s3")
+                    .areAllDynamic();
+            assertWith(getManager().getShortcuts(ShortcutManager.FLAG_MATCH_PINNED))
+                    .haveIds("ms21", "s1", "s2")
+                    .areAllPinned();
+            assertWith(getManager().getShortcuts(ShortcutManager.FLAG_MATCH_CACHED))
+                    .isEmpty();
+
+            assertWith(getManager().getShortcuts(
+                    ShortcutManager.FLAG_MATCH_MANIFEST | ShortcutManager.FLAG_MATCH_DYNAMIC))
+                    .haveIds("ms21", "ms22", "s1", "s3");
+            assertWith(getManager().getShortcuts(
+                    ShortcutManager.FLAG_MATCH_MANIFEST | ShortcutManager.FLAG_MATCH_PINNED))
+                    .haveIds("ms21", "ms22", "s1", "s2");
+            assertWith(getManager().getShortcuts(
+                    ShortcutManager.FLAG_MATCH_MANIFEST | ShortcutManager.FLAG_MATCH_CACHED))
+                    .haveIds("ms21", "ms22");
+            assertWith(getManager().getShortcuts(
+                    ShortcutManager.FLAG_MATCH_DYNAMIC | ShortcutManager.FLAG_MATCH_PINNED))
+                    .haveIds("ms21", "s1", "s2", "s3");
+            assertWith(getManager().getShortcuts(
+                    ShortcutManager.FLAG_MATCH_DYNAMIC | ShortcutManager.FLAG_MATCH_CACHED))
+                    .haveIds("s1", "s3");
+            assertWith(getManager().getShortcuts(
+                    ShortcutManager.FLAG_MATCH_PINNED | ShortcutManager.FLAG_MATCH_CACHED))
+                    .haveIds("ms21", "s1", "s2");
+
+            assertWith(getManager().getShortcuts(ShortcutManager.FLAG_MATCH_MANIFEST
+                    | ShortcutManager.FLAG_MATCH_DYNAMIC | ShortcutManager.FLAG_MATCH_PINNED))
+                    .haveIds("ms21", "ms22", "s1", "s2", "s3");
+            assertWith(getManager().getShortcuts(ShortcutManager.FLAG_MATCH_MANIFEST
+                    | ShortcutManager.FLAG_MATCH_DYNAMIC | ShortcutManager.FLAG_MATCH_CACHED))
+                    .haveIds("ms21", "ms22", "s1", "s3");
+            assertWith(getManager().getShortcuts(ShortcutManager.FLAG_MATCH_MANIFEST
+                    | ShortcutManager.FLAG_MATCH_CACHED | ShortcutManager.FLAG_MATCH_PINNED))
+                    .haveIds("ms21", "ms22", "s1", "s2");
+            assertWith(getManager().getShortcuts(ShortcutManager.FLAG_MATCH_DYNAMIC
+                    | ShortcutManager.FLAG_MATCH_CACHED | ShortcutManager.FLAG_MATCH_PINNED))
+                    .haveIds("ms21", "s1", "s2", "s3");
+
+            assertWith(getManager().getShortcuts(
+                    ShortcutManager.FLAG_MATCH_MANIFEST | ShortcutManager.FLAG_MATCH_DYNAMIC
+                    | ShortcutManager.FLAG_MATCH_PINNED | ShortcutManager.FLAG_MATCH_CACHED))
+                    .haveIds("ms21", "ms22", "s1", "s2", "s3");
+        });
+    }
+
     // TODO Test auto rank adjustment.
     // TODO Test save & load.
 }
diff --git a/tests/tests/systemui/src/android/systemui/cts/WindowInsetsBehaviorTests.java b/tests/tests/systemui/src/android/systemui/cts/WindowInsetsBehaviorTests.java
index cddf6df..87478d0 100644
--- a/tests/tests/systemui/src/android/systemui/cts/WindowInsetsBehaviorTests.java
+++ b/tests/tests/systemui/src/android/systemui/cts/WindowInsetsBehaviorTests.java
@@ -93,6 +93,8 @@
     // greater than that, so that we can test if the limit can be changed by DeviceConfig or not.
     private static final int EXCLUSION_LIMIT_DP = 210;
 
+    private static final int NAV_BAR_INTERACTION_MODE_GESTURAL = 2;
+
     private final boolean mForceEnableGestureNavigation;
     private final Map<String, Boolean> mSystemGestureOptionsMap;
     private float mPixelsPerDp;
@@ -787,7 +789,7 @@
         Resources res = mTargetContext.getResources();
         int naviMode = res.getIdentifier(NAV_BAR_INTERACTION_MODE_RES_NAME, "integer", "android");
 
-        assumeTrue("Gesture navigation required", naviMode == 2);
+        assumeTrue("Gesture navigation required", naviMode == NAV_BAR_INTERACTION_MODE_GESTURAL);
     }
 
     /**
diff --git a/tests/tests/tv/src/android/media/tv/cts/TvInputManagerTest.java b/tests/tests/tv/src/android/media/tv/cts/TvInputManagerTest.java
index e1c165b..6649ca6 100644
--- a/tests/tests/tv/src/android/media/tv/cts/TvInputManagerTest.java
+++ b/tests/tests/tv/src/android/media/tv/cts/TvInputManagerTest.java
@@ -19,17 +19,25 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.pm.PackageManager;
+import android.media.AudioManager;
 import android.media.tv.TvContentRating;
+import android.media.tv.TvInputHardwareInfo;
 import android.media.tv.TvInputInfo;
 import android.media.tv.TvInputManager;
+import android.media.tv.TvInputManager.Hardware;
+import android.media.tv.TvInputManager.HardwareCallback;
 import android.media.tv.TvInputService;
+import android.media.tv.TvStreamConfig;
 import android.os.Handler;
 import android.test.ActivityInstrumentationTestCase2;
 
 import com.android.compatibility.common.util.PollingCheck;
 
+import androidx.test.InstrumentationRegistry;
+
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.concurrent.Executor;
 import java.util.List;
 
 import org.xmlpull.v1.XmlPullParserException;
@@ -53,6 +61,7 @@
     private String mStubId;
     private TvInputManager mManager;
     private LoggingCallback mCallback = new LoggingCallback();
+    private TvInputInfo mStubTvInputInfo;
 
     private static TvInputInfo getInfoForClassName(List<TvInputInfo> list, String name) {
         for (TvInputInfo info : list) {
@@ -75,6 +84,8 @@
         mManager = (TvInputManager) getActivity().getSystemService(Context.TV_INPUT_SERVICE);
         mStubId = getInfoForClassName(
                 mManager.getTvInputList(), StubTvInputService2.class.getName()).getId();
+        mStubTvInputInfo = getInfoForClassName(
+                mManager.getTvInputList(), StubTvInputService2.class.getName());
     }
 
     public void testGetInputState() throws Exception {
@@ -245,6 +256,56 @@
         getInstrumentation().waitForIdleSync();
     }
 
+    public void testAcquireTvInputHardware() {
+        InstrumentationRegistry.getInstrumentation().getUiAutomation()
+            .adoptShellPermissionIdentity("android.permission.TV_INPUT_HARDWARE",
+                    "android.permission.TUNER_RESOURCE_ACCESS");
+        if (mManager == null) {
+            return;
+        }
+        // Update hardware device list
+        int deviceId = 0;
+        boolean hardwareDeviceAdded = false;
+        List<TvInputHardwareInfo> hardwareList = mManager.getHardwareList();
+        if (hardwareList == null || hardwareList.isEmpty()) {
+            // Use the test api to add an HDMI hardware device
+            mManager.addHardwareDevice(deviceId);
+            hardwareDeviceAdded = true;
+        } else {
+            deviceId = hardwareList.get(0).getDeviceId();
+        }
+
+        // Acquire Hardware with a record client
+        HardwareCallback callback = new HardwareCallback() {
+            @Override
+            public void onReleased() {}
+
+            @Override
+            public void onStreamConfigChanged(TvStreamConfig[] configs) {}
+        };
+        CallbackExecutor executor = new CallbackExecutor();
+        Hardware hardware = mManager.acquireTvInputHardware(
+                deviceId, mStubTvInputInfo, null /*tvInputSessionId*/,
+                TvInputService.PRIORITY_HINT_USE_CASE_TYPE_PLAYBACK,
+                executor, callback);
+        assertNotNull(hardware);
+
+        // Acquire the same device with a LIVE client
+        Hardware hardwareAcquired = mManager.acquireTvInputHardware(
+                deviceId, mStubTvInputInfo, null /*tvInputSessionId*/,
+                TvInputService.PRIORITY_HINT_USE_CASE_TYPE_LIVE,
+                executor, callback);
+
+        assertNotNull(hardwareAcquired);
+
+        // Clean up
+        if (hardwareDeviceAdded) {
+            mManager.removeHardwareDevice(deviceId);
+        }
+        InstrumentationRegistry.getInstrumentation().getUiAutomation()
+            .dropShellPermissionIdentity();
+    }
+
     private static class LoggingCallback extends TvInputManager.TvInputCallback {
         private final List<String> mAddedInputs = new ArrayList<>();
         private final List<String> mRemovedInputs = new ArrayList<>();
@@ -292,4 +353,11 @@
             return null;
         }
     }
+
+    public class CallbackExecutor implements Executor {
+        @Override
+        public void execute(Runnable r) {
+            r.run();
+        }
+    }
 }
diff --git a/tests/tests/tv/src/android/media/tv/tuner/cts/TunerDvrTest.java b/tests/tests/tv/src/android/media/tv/tuner/cts/TunerDvrTest.java
new file mode 100644
index 0000000..e90be21
--- /dev/null
+++ b/tests/tests/tv/src/android/media/tv/tuner/cts/TunerDvrTest.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright 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.
+ */
+
+package android.media.tv.tuner.cts;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+import android.media.tv.tuner.Tuner;
+import android.media.tv.tuner.dvr.DvrPlayback;
+import android.media.tv.tuner.dvr.DvrRecorder;
+import android.media.tv.tuner.dvr.DvrSettings;
+import android.media.tv.tuner.dvr.OnPlaybackStatusChangedListener;
+import android.media.tv.tuner.dvr.OnRecordStatusChangedListener;
+import android.media.tv.tuner.filter.FilterCallback;
+import android.media.tv.tuner.filter.FilterEvent;
+import android.media.tv.tuner.filter.Filter;
+import android.media.tv.tuner.filter.FilterConfiguration;
+import android.media.tv.tuner.filter.RecordSettings;
+import android.media.tv.tuner.filter.Settings;
+import android.media.tv.tuner.filter.TsFilterConfiguration;
+import android.os.ParcelFileDescriptor;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import java.io.File;
+import java.io.RandomAccessFile;
+import java.util.concurrent.Executor;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class TunerDvrTest {
+    private static final String TAG = "MediaTunerDvrTest";
+
+    private Context mContext;
+    private Tuner mTuner;
+
+    @Before
+    public void setUp() throws Exception {
+        mContext = InstrumentationRegistry.getTargetContext();
+        InstrumentationRegistry
+                .getInstrumentation().getUiAutomation().adoptShellPermissionIdentity();
+        if (!hasTuner()) return;
+        mTuner = new Tuner(mContext, null, 100);
+    }
+
+    @After
+    public void tearDown() {
+        if (mTuner != null) {
+          mTuner.close();
+          mTuner = null;
+        }
+    }
+
+    @Test
+    public void testDvrSettings() throws Exception {
+        if (!hasTuner()) return;
+        DvrSettings settings = getDvrSettings();
+
+        assertEquals(Filter.STATUS_DATA_READY, settings.getStatusMask());
+        assertEquals(200L, settings.getLowThreshold());
+        assertEquals(800L, settings.getHighThreshold());
+        assertEquals(188L, settings.getPacketSize());
+        assertEquals(DvrSettings.DATA_FORMAT_TS, settings.getDataFormat());
+    }
+
+    @Test
+    public void testDvrRecorder() throws Exception {
+        if (!hasTuner()) return;
+        DvrRecorder d = mTuner.openDvrRecorder(1000, getExecutor(), getRecordListener());
+        assertNotNull(d);
+        d.configure(getDvrSettings());
+
+        File tmpFile = File.createTempFile("cts_tuner", "dvr_test");
+        d.setFileDescriptor(
+                ParcelFileDescriptor.open(tmpFile, ParcelFileDescriptor.MODE_READ_WRITE));
+
+        Filter filter = mTuner.openFilter(
+                Filter.TYPE_TS, Filter.SUBTYPE_RECORD, 1000, getExecutor(), getFilterCallback());
+        if (filter != null) {
+            Settings settings = RecordSettings
+                    .builder(Filter.TYPE_TS)
+                    .setTsIndexMask(RecordSettings.TS_INDEX_FIRST_PACKET)
+                    .build();
+            FilterConfiguration config = TsFilterConfiguration
+                    .builder()
+                    .setTpid(10)
+                    .setSettings(settings)
+                    .build();
+            filter.configure(config);
+            d.attachFilter(filter);
+        }
+        d.start();
+        d.flush();
+        if (filter != null) {
+            filter.start();
+            filter.flush();
+        }
+        d.write(10);
+        d.write(new byte[3], 0, 3);
+        d.stop();
+        d.close();
+        if (filter != null) {
+            d.detachFilter(filter);
+            filter.stop();
+            filter.close();
+        }
+
+        tmpFile.delete();
+    }
+
+    @Test
+    public void testDvrPlayback() throws Exception {
+        if (!hasTuner()) return;
+        DvrPlayback d = mTuner.openDvrPlayback(1000, getExecutor(), getPlaybackListener());
+        assertNotNull(d);
+        d.configure(getDvrSettings());
+
+        File tmpFile = File.createTempFile("cts_tuner", "dvr_test");
+        try (RandomAccessFile raf = new RandomAccessFile(tmpFile, "rw")) {
+            byte[] bytes = new byte[] {3, 5, 10, 22, 73, 33, 19};
+            raf.write(bytes);
+        }
+        d.setFileDescriptor(
+                ParcelFileDescriptor.open(tmpFile, ParcelFileDescriptor.MODE_READ_WRITE));
+
+        d.start();
+        d.flush();
+        assertEquals(3, d.read(3));
+        assertEquals(3, d.read(new byte[3], 0, 3));
+        d.stop();
+        d.close();
+
+        tmpFile.delete();
+    }
+
+    private OnRecordStatusChangedListener getRecordListener() {
+        return new OnRecordStatusChangedListener() {
+            @Override
+            public void onRecordStatusChanged(int status) {}
+        };
+    }
+
+    private OnPlaybackStatusChangedListener getPlaybackListener() {
+        return new OnPlaybackStatusChangedListener() {
+            @Override
+            public void onPlaybackStatusChanged(int status) {}
+        };
+    }
+
+    private Executor getExecutor() {
+        return Runnable::run;
+    }
+
+    private DvrSettings getDvrSettings() {
+        return DvrSettings
+                .builder()
+                .setStatusMask(Filter.STATUS_DATA_READY)
+                .setLowThreshold(200L)
+                .setHighThreshold(800L)
+                .setPacketSize(188L)
+                .setDataFormat(DvrSettings.DATA_FORMAT_TS)
+                .build();
+    }
+
+    private FilterCallback getFilterCallback() {
+        return new FilterCallback() {
+            @Override
+            public void onFilterEvent(Filter filter, FilterEvent[] events) {}
+            @Override
+            public void onFilterStatusChanged(Filter filter, int status) {}
+        };
+    }
+
+    private boolean hasTuner() {
+        // TODO: move to a Utils class.
+        return mContext.getPackageManager().hasSystemFeature("android.hardware.tv.tuner");
+    }
+}
diff --git a/tests/tests/tv/src/android/media/tv/tuner/cts/TunerFilterTest.java b/tests/tests/tv/src/android/media/tv/tuner/cts/TunerFilterTest.java
index a82e8c1..8c0472b 100644
--- a/tests/tests/tv/src/android/media/tv/tuner/cts/TunerFilterTest.java
+++ b/tests/tests/tv/src/android/media/tv/tuner/cts/TunerFilterTest.java
@@ -179,12 +179,14 @@
                         .builder()
                         .setPacketType(AlpFilterConfiguration.PACKET_TYPE_COMPRESSED)
                         .setLengthType(AlpFilterConfiguration.LENGTH_TYPE_WITH_ADDITIONAL_HEADER)
+                        .setSettings(null)
                         .build();
 
         assertEquals(Filter.TYPE_ALP, config.getType());
         assertEquals(AlpFilterConfiguration.PACKET_TYPE_COMPRESSED, config.getPacketType());
         assertEquals(
                 AlpFilterConfiguration.LENGTH_TYPE_WITH_ADDITIONAL_HEADER, config.getLengthType());
+        assertEquals(null, config.getSettings());
     }
 
     @Test
@@ -198,6 +200,7 @@
                         .setSrcPort(33)
                         .setDstPort(23)
                         .setPassthrough(false)
+                        .setSettings(null)
                         .build();
 
         assertEquals(Filter.TYPE_IP, config.getType());
@@ -208,6 +211,7 @@
         assertEquals(33, config.getSrcPort());
         assertEquals(23, config.getDstPort());
         assertFalse(config.isPassthrough());
+        assertEquals(null, config.getSettings());
     }
 
     @Test
@@ -217,10 +221,12 @@
                 MmtpFilterConfiguration
                         .builder()
                         .setMmtpPacketId(3)
+                        .setSettings(null)
                         .build();
 
         assertEquals(Filter.TYPE_MMTP, config.getType());
         assertEquals(3, config.getMmtpPacketId());
+        assertEquals(null, config.getSettings());
     }
 
     @Test
@@ -232,12 +238,14 @@
                         .setPacketType(TlvFilterConfiguration.PACKET_TYPE_IPV4)
                         .setCompressedIpPacket(true)
                         .setPassthrough(false)
+                        .setSettings(null)
                         .build();
 
         assertEquals(Filter.TYPE_TLV, config.getType());
         assertEquals(TlvFilterConfiguration.PACKET_TYPE_IPV4, config.getPacketType());
         assertTrue(config.isCompressedIpPacket());
         assertFalse(config.isPassthrough());
+        assertEquals(null, config.getSettings());
     }
 
     @Test
diff --git a/tests/tests/tv/src/android/media/tv/tuner/cts/TunerTest.java b/tests/tests/tv/src/android/media/tv/tuner/cts/TunerTest.java
index a63d7e3..2012192 100644
--- a/tests/tests/tv/src/android/media/tv/tuner/cts/TunerTest.java
+++ b/tests/tests/tv/src/android/media/tv/tuner/cts/TunerTest.java
@@ -24,6 +24,7 @@
 
 import android.content.Context;
 
+import android.media.tv.tuner.DemuxCapabilities;
 import android.media.tv.tuner.Descrambler;
 import android.media.tv.tuner.LnbCallback;
 import android.media.tv.tuner.Lnb;
@@ -36,6 +37,7 @@
 import android.media.tv.tuner.filter.AudioDescriptor;
 import android.media.tv.tuner.filter.DownloadEvent;
 import android.media.tv.tuner.filter.FilterCallback;
+import android.media.tv.tuner.filter.FilterConfiguration;
 import android.media.tv.tuner.filter.FilterEvent;
 import android.media.tv.tuner.filter.Filter;
 import android.media.tv.tuner.filter.IpPayloadEvent;
@@ -43,6 +45,9 @@
 import android.media.tv.tuner.filter.MmtpRecordEvent;
 import android.media.tv.tuner.filter.PesEvent;
 import android.media.tv.tuner.filter.SectionEvent;
+import android.media.tv.tuner.filter.SectionSettingsWithTableInfo;
+import android.media.tv.tuner.filter.Settings;
+import android.media.tv.tuner.filter.TsFilterConfiguration;
 import android.media.tv.tuner.filter.TemiEvent;
 import android.media.tv.tuner.filter.TimeFilter;
 import android.media.tv.tuner.filter.TsRecordEvent;
@@ -73,6 +78,7 @@
 import android.media.tv.tuner.frontend.IsdbsFrontendSettings;
 import android.media.tv.tuner.frontend.IsdbtFrontendCapabilities;
 import android.media.tv.tuner.frontend.IsdbtFrontendSettings;
+import android.media.tv.tuner.frontend.OnTuneEventListener;
 import android.media.tv.tuner.frontend.ScanCallback;
 
 import androidx.test.InstrumentationRegistry;
@@ -132,6 +138,7 @@
         FrontendInfo info = mTuner.getFrontendInfoById(ids.get(0));
         int res = mTuner.tune(createFrontendSettings(info));
         assertEquals(Tuner.RESULT_SUCCESS, res);
+        assertEquals(Tuner.RESULT_SUCCESS, mTuner.setLnaEnabled(false));
         res = mTuner.cancelTuning();
         assertEquals(Tuner.RESULT_SUCCESS, res);
     }
@@ -226,55 +233,104 @@
     }
 
     @Test
-    public void testOpenLnb() throws Exception {
+    public void testLnb() throws Exception {
         if (!hasTuner()) return;
         Lnb lnb = mTuner.openLnb(getExecutor(), getLnbCallback());
-        assertNotNull(lnb);
-    }
-
-    @Test
-    public void testLnbSetVoltage() throws Exception {
-        // TODO: move lnb-related tests to a separate file.
-        if (!hasTuner()) return;
-        Lnb lnb = mTuner.openLnb(getExecutor(), getLnbCallback());
+        if (lnb == null) return;
         assertEquals(lnb.setVoltage(Lnb.VOLTAGE_5V), Tuner.RESULT_SUCCESS);
-    }
-
-    @Test
-    public void testLnbSetTone() throws Exception {
-        if (!hasTuner()) return;
-        Lnb lnb = mTuner.openLnb(getExecutor(), getLnbCallback());
         assertEquals(lnb.setTone(Lnb.TONE_NONE), Tuner.RESULT_SUCCESS);
-    }
-
-    @Test
-    public void testLnbSetPosistion() throws Exception {
-        if (!hasTuner()) return;
-        Lnb lnb = mTuner.openLnb(getExecutor(), getLnbCallback());
         assertEquals(
                 lnb.setSatellitePosition(Lnb.POSITION_A), Tuner.RESULT_SUCCESS);
+        lnb.sendDiseqcMessage(new byte[] {1, 2});
+        lnb.close();
     }
 
     @Test
-    public void testOpenFilter() throws Exception {
+    public void testOpenLnbByname() throws Exception {
+        if (!hasTuner()) return;
+        Lnb lnb = mTuner.openLnbByName("default", getExecutor(), getLnbCallback());
+        if (lnb != null) {
+            lnb.close();
+        }
+    }
+
+    @Test
+    public void testCiCam() throws Exception {
+        if (!hasTuner()) return;
+        // open filter to get demux resource
+        mTuner.openFilter(
+                Filter.TYPE_TS, Filter.SUBTYPE_SECTION, 1000, getExecutor(), getFilterCallback());
+
+        mTuner.connectCiCam(1);
+        mTuner.disconnectCiCam();
+    }
+
+    @Test
+    public void testAvSyncId() throws Exception {
+        if (!hasTuner()) return;
+        // open filter to get demux resource
+        Filter f = mTuner.openFilter(
+                Filter.TYPE_TS, Filter.SUBTYPE_AUDIO, 1000, getExecutor(), getFilterCallback());
+        int id = mTuner.getAvSyncHwId(f);
+        if (id != Tuner.INVALID_AV_SYNC_ID) {
+            assertNotEquals(Tuner.INVALID_TIMESTAMP, mTuner.getAvSyncTime(id));
+        }
+    }
+
+    @Test
+    public void testReadFilter() throws Exception {
         if (!hasTuner()) return;
         Filter f = mTuner.openFilter(
                 Filter.TYPE_TS, Filter.SUBTYPE_SECTION, 1000, getExecutor(), getFilterCallback());
         assertNotNull(f);
+        assertNotEquals(Tuner.INVALID_FILTER_ID, f.getId());
+
+        Settings settings = SectionSettingsWithTableInfo
+                .builder(Filter.TYPE_TS)
+                .setTableId(2)
+                .setVersion(1)
+                .setCrcEnabled(true)
+                .setRaw(false)
+                .setRepeat(false)
+                .build();
+        FilterConfiguration config = TsFilterConfiguration
+                .builder()
+                .setTpid(10)
+                .setSettings(settings)
+                .build();
+        f.configure(config);
+        f.start();
+        f.flush();
+        f.read(new byte[3], 0, 3);
+        f.stop();
+        f.close();
     }
 
     @Test
-    public void testOpenTimeFilter() throws Exception {
+    public void testTimeFilter() throws Exception {
         if (!hasTuner()) return;
+        if (!mTuner.getDemuxCapabilities().isTimeFilterSupported()) return;
         TimeFilter f = mTuner.openTimeFilter();
         assertNotNull(f);
+        f.setCurrentTimestamp(0);
+        assertNotEquals(Tuner.INVALID_TIMESTAMP, f.getTimeStamp());
+        assertNotEquals(Tuner.INVALID_TIMESTAMP, f.getSourceTime());
+        f.clearTimestamp();
+        f.close();
     }
 
     @Test
-    public void testOpenDescrambler() throws Exception {
+    public void testDescrambler() throws Exception {
         if (!hasTuner()) return;
         Descrambler d = mTuner.openDescrambler();
         assertNotNull(d);
+        Filter f = mTuner.openFilter(
+                Filter.TYPE_TS, Filter.SUBTYPE_SECTION, 1000, getExecutor(), getFilterCallback());
+        d.setKeyToken(new byte[] {1, 3, 2});
+        d.addPid(Descrambler.PID_TYPE_T, 1, f);
+        d.removePid(Descrambler.PID_TYPE_T, 1, f);
+        f.close();
+        d.close();
     }
 
     @Test
@@ -291,6 +347,69 @@
         assertNotNull(d);
     }
 
+    @Test
+    public void testDemuxCapabilities() throws Exception {
+        if (!hasTuner()) return;
+        DemuxCapabilities d = mTuner.getDemuxCapabilities();
+        assertNotNull(d);
+
+        d.getDemuxCount();
+        d.getRecordCount();
+        d.getPlaybackCount();
+        d.getTsFilterCount();
+        d.getSectionFilterCount();
+        d.getAudioFilterCount();
+        d.getVideoFilterCount();
+        d.getPesFilterCount();
+        d.getPcrFilterCount();
+        d.getSectionFilterLength();
+        d.getFilterCapabilities();
+        d.getLinkCapabilities();
+        d.isTimeFilterSupported();
+    }
+
+    @Test
+    public void testResourceLostListener() throws Exception {
+        if (!hasTuner()) return;
+        mTuner.setResourceLostListener(getExecutor(), new Tuner.OnResourceLostListener() {
+            @Override
+            public void onResourceLost(Tuner tuner) {}
+        });
+        mTuner.clearResourceLostListener();
+    }
+
+    @Test
+    public void testOnTuneEventListener() throws Exception {
+        if (!hasTuner()) return;
+        mTuner.setOnTuneEventListener(getExecutor(), new OnTuneEventListener() {
+            @Override
+            public void onTuneEvent(int tuneEvent) {}
+        });
+        mTuner.clearOnTuneEventListener();
+    }
+
+    @Test
+    public void testUpdateResourcePriority() throws Exception {
+        if (!hasTuner()) return;
+        mTuner.updateResourcePriority(100, 20);
+    }
+
+    @Test
+    public void testShareFrontendFromTuner() throws Exception {
+        if (!hasTuner()) return;
+        Tuner other = new Tuner(mContext, null, 100);
+        List<Integer> ids = other.getFrontendIds();
+        assertFalse(ids.isEmpty());
+        FrontendInfo info = other.getFrontendInfoById(ids.get(0));
+
+	// call tune() to open frontend resource
+        int res = other.tune(createFrontendSettings(info));
+        assertEquals(Tuner.RESULT_SUCCESS, res);
+        assertNotNull(other.getFrontendInfo());
+        mTuner.shareFrontendFromTuner(other);
+        other.close();
+    }
+
     private boolean hasTuner() {
         return mContext.getPackageManager().hasSystemFeature("android.hardware.tv.tuner");
     }
diff --git a/tests/tests/wifi/src/android/net/wifi/cts/ConnectedNetworkScorerTest.java b/tests/tests/wifi/src/android/net/wifi/cts/ConnectedNetworkScorerTest.java
index 9c3b9b6..d5a0f3a 100644
--- a/tests/tests/wifi/src/android/net/wifi/cts/ConnectedNetworkScorerTest.java
+++ b/tests/tests/wifi/src/android/net/wifi/cts/ConnectedNetworkScorerTest.java
@@ -302,6 +302,10 @@
                 new TestUsabilityStatsListener(countDownLatchUsabilityStats);
         try {
             uiAutomation.adoptShellPermissionIdentity();
+            // Clear any external scorer already active on the device.
+            mWifiManager.clearWifiConnectedNetworkScorer();
+            Thread.sleep(500);
+
             mWifiManager.setWifiConnectedNetworkScorer(
                     Executors.newSingleThreadExecutor(), connectedNetworkScorer);
             // Since we're already connected, wait for onStart to be invoked.
diff --git a/tests/tests/wifi/src/android/net/wifi/cts/WifiNetworkSpecifierTest.java b/tests/tests/wifi/src/android/net/wifi/cts/WifiNetworkSpecifierTest.java
index e17d022..ba55513 100644
--- a/tests/tests/wifi/src/android/net/wifi/cts/WifiNetworkSpecifierTest.java
+++ b/tests/tests/wifi/src/android/net/wifi/cts/WifiNetworkSpecifierTest.java
@@ -137,7 +137,7 @@
             mConnectivityManager.unregisterNetworkCallback(mNetworkCallback);
         }
         ShellIdentityUtils.invokeWithShellPermissions(
-                () -> mWifiManager.enableNetwork(mTestNetwork.networkId, false));
+                () -> mWifiManager.enableNetwork(mTestNetwork.networkId, true));
         ShellIdentityUtils.invokeWithShellPermissions(
                 () -> mWifiManager.setScanThrottleEnabled(mWasScanThrottleEnabled));
         ShellIdentityUtils.invokeWithShellPermissions(