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(