Merge "Fix NPE when multi-window not supported" into tm-dev
diff --git a/tests/PhotoPicker/Android.bp b/tests/PhotoPicker/Android.bp
index 5a9fc33..45ecfe2 100644
--- a/tests/PhotoPicker/Android.bp
+++ b/tests/PhotoPicker/Android.bp
@@ -25,6 +25,7 @@
test_suites: ["general-tests", "mts-mediaprovider", "cts"],
sdk_version: "core_current",
min_sdk_version: "30",
+ target_sdk_version: "33",
libs: [
"android.test.base",
"android.test.runner",
diff --git a/tests/signature/api-check/shared-libs-api/Android.bp b/tests/signature/api-check/shared-libs-api/Android.bp
index afab2f3..607df48 100644
--- a/tests/signature/api-check/shared-libs-api/Android.bp
+++ b/tests/signature/api-check/shared-libs-api/Android.bp
@@ -24,6 +24,7 @@
static_libs: [
"cts-api-signature-test",
"compatibility-device-util-axt",
+ "junit-params",
],
}
@@ -36,6 +37,8 @@
jarjar_rules: ":cts-android-test-jarjar-rules",
java_resources: [
+ ":cts-current-api-gz",
+ ":cts-shared-libs-names.txt",
":CtsSharedLibsApiSignatureTestCases_cts-shared-libs-all-current.api",
":CtsSharedLibsApiSignatureTestCases_cts-shared-libs-all-previous.api",
],
@@ -60,12 +63,29 @@
srcs: ["src/**/*.java"],
}
+genrule {
+ name: "cts-shared-libs-names.txt",
+ srcs: [
+ "AndroidManifest.xml",
+ ],
+ out: [
+ "shared-libs-names.txt",
+ ],
+ cmd: "grep 'uses-library' $(in) | cut -f2 -d\\\" | sort > $(out)",
+}
+
// Generates a zip file containing the current public and system API files for shared libraries.
genrule {
name: "CtsSharedLibsApiSignatureTestCases_cts-shared-libs-all-current.api",
srcs: [
":android.net.ipsec.ike{.public.api.txt}",
":android.net.ipsec.ike{.system.api.txt}",
+ ":android.test.base{.public.api.txt}",
+ ":android.test.base{.system.api.txt}",
+ ":android.test.runner{.public.api.txt}",
+ ":android.test.runner{.system.api.txt}",
+ ":android.test.mock{.public.api.txt}",
+ ":android.test.mock{.system.api.txt}",
":com.android.future.usb.accessory{.public.api.txt}",
":com.android.future.usb.accessory{.system.api.txt}",
":com.android.libraries.tv.tvsystem{.public.api.txt}",
@@ -82,6 +102,8 @@
":com.android.nfc_extras{.system.api.txt}",
":javax.obex{.public.api.txt}",
":javax.obex{.system.api.txt}",
+ ":org.apache.http.legacy{.public.api.txt}",
+ ":org.apache.http.legacy{.system.api.txt}",
],
tools: [
"soong_zip",
diff --git a/tests/signature/api-check/shared-libs-api/AndroidTest.xml b/tests/signature/api-check/shared-libs-api/AndroidTest.xml
index 14a54a2..9bd8233 100644
--- a/tests/signature/api-check/shared-libs-api/AndroidTest.xml
+++ b/tests/signature/api-check/shared-libs-api/AndroidTest.xml
@@ -28,6 +28,7 @@
<option name="package" value="android.signature.cts.api.shared_libs" />
<option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
<option name="class" value="android.signature.cts.api.SignatureMultiLibsTest" />
+ <option name="instrumentation-arg" key="base-api-files" value="current.api.gz" />
<option name="instrumentation-arg" key="expected-api-files" value="shared-libs-all-current.api.zip" />
<option name="instrumentation-arg" key="previous-api-files" value="shared-libs-all-previous.api.zip" />
<option name="runtime-hint" value="30s" />
diff --git a/tests/signature/api-check/shared-libs-api/src/android/signature/cts/api/SignatureMultiLibsTest.java b/tests/signature/api-check/shared-libs-api/src/android/signature/cts/api/SignatureMultiLibsTest.java
index 2bc63f7..2d0f719 100644
--- a/tests/signature/api-check/shared-libs-api/src/android/signature/cts/api/SignatureMultiLibsTest.java
+++ b/tests/signature/api-check/shared-libs-api/src/android/signature/cts/api/SignatureMultiLibsTest.java
@@ -17,20 +17,33 @@
package android.signature.cts.api;
import android.app.Instrumentation;
+import android.content.Context;
+import android.content.pm.SharedLibraryInfo;
import android.signature.cts.ApiComplianceChecker;
import android.signature.cts.ApiDocumentParser;
import android.signature.cts.JDiffClassDescription;
import android.signature.cts.VirtualPath;
+import android.util.Log;
import androidx.test.platform.app.InstrumentationRegistry;
-import java.util.Arrays;
+import com.google.common.base.Suppliers;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.List;
import java.util.Set;
import java.util.TreeSet;
+import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
-import org.junit.BeforeClass;
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+import junitparams.naming.TestCaseName;
+import org.junit.Assume;
+import org.junit.FixMethodOrder;
import org.junit.Test;
-
-import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
+import org.junit.runner.RunWith;
+import org.junit.runners.MethodSorters;
/**
* Verifies that any shared library provided by this device and for which this test has a
@@ -40,23 +53,103 @@
* {@code <uses-library>} entry for every shared library that provides an API that is contained
* within the shared-libs-all.api.zip supplied to this test.
*/
-public class SignatureMultiLibsTest extends SignatureTest {
+@RunWith(JUnitParamsRunner.class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class SignatureMultiLibsTest extends AbstractSignatureTest {
+
+ protected static final Supplier<String[]> EXPECTED_API_FILES =
+ getSupplierOfAMandatoryCommaSeparatedListArgument(EXPECTED_API_FILES_ARG);
+
+ protected static final Supplier<String[]> PREVIOUS_API_FILES =
+ getSupplierOfAMandatoryCommaSeparatedListArgument(PREVIOUS_API_FILES_ARG);
private static final String TAG = SignatureMultiLibsTest.class.getSimpleName();
- private static Set<String> libraries;
+ /**
+ * A memoized supplier of the list of shared libraries on the device.
+ */
+ protected static final Supplier<Set<String>> AVAILABLE_SHARED_LIBRARIES =
+ Suppliers.memoize(SignatureMultiLibsTest::retrieveActiveSharedLibraries)::get;
+
+ private static final String SHARED_LIBRARY_LIST_RESOURCE_NAME = "shared-libs-names.txt";
/**
- * Obtain a list of shared libraries from the device.
+ * Retrieve the names of the shared libraries that are active on the device.
+ *
+ * @return The set of shared library names.
*/
- @BeforeClass
- public static void retrieveListOfSharedLibrariesOnDevice() throws Exception {
+ private static Set<String> retrieveActiveSharedLibraries() {
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
- String result = runShellCommand(instrumentation, "cmd package list libraries");
- libraries = Arrays.stream(result.split("\n")).map(line -> line.split(":")[1])
- .peek(library -> System.out.printf("%s: Found library: %s%n",
- SignatureMultiLibsTest.class.getSimpleName(), library))
- .collect(Collectors.toCollection(TreeSet::new));
+ Context context = instrumentation.getTargetContext();
+
+ List<SharedLibraryInfo> sharedLibraries =
+ context.getPackageManager().getSharedLibraries(0);
+
+ Set<String> sharedLibraryNames = new TreeSet<>();
+ for (SharedLibraryInfo sharedLibrary : sharedLibraries) {
+ String name = sharedLibrary.getName();
+ sharedLibraryNames.add(name);
+ Log.d(TAG, String.format("Found library: %s%n", name));
+ }
+
+ return sharedLibraryNames;
+ }
+
+ /**
+ * A memoized supplier of the list of shared libraries that this can test.
+ */
+ protected static final Supplier<List<String>> TESTABLE_SHARED_LIBRARIES =
+ Suppliers.memoize(SignatureMultiLibsTest::retrieveTestableSharedLibraries)::get;
+
+ /**
+ * Retrieve the names of the shared libraries that are testable by this test.
+ *
+ * @return The set of shared library names.
+ */
+ private static List<String> retrieveTestableSharedLibraries() {
+ ClassLoader classLoader = SignatureMultiLibsTest.class.getClassLoader();
+ try (InputStream is = classLoader.getResourceAsStream(SHARED_LIBRARY_LIST_RESOURCE_NAME)) {
+ if (is == null) {
+ throw new RuntimeException(
+ "Resource " + SHARED_LIBRARY_LIST_RESOURCE_NAME + " could not be found");
+ }
+
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(is))) {
+ return reader.lines()
+ .filter(line -> !line.isEmpty())
+ .sorted()
+ .collect(Collectors.toList());
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("Could not retrieve testable shared libraries", e);
+ }
+ }
+
+ /**
+ * Convert the list of testable shared libraries into a form suitable for parameterizing a test.
+ */
+ public Object[][] getTestableSharedLibraryParameters() {
+ List<String> libraries = TESTABLE_SHARED_LIBRARIES.get();
+ Object[][] params = new Object[libraries.size()][1];
+ for (int i = 0; i < libraries.size(); i++) {
+ String name = libraries.get(i);
+ TestableLibraryParameter param = new TestableLibraryParameter(name);
+ params[i][0] = param;
+ }
+ return params;
+ }
+
+ /**
+ * Skips the test if the supplied library is unavailable on the device.
+ *
+ * <p>If the library is unavailable then this throws an
+ * {@link org.junit.AssumptionViolatedException}.</p>
+ *
+ * @param library the name of the library that needs to be available.
+ */
+ private void skipTestIfLibraryIsUnavailable(String library) {
+ Assume.assumeTrue("Shared library " + library + " is not available on this device",
+ AVAILABLE_SHARED_LIBRARIES.get().contains(library));
}
/**
@@ -65,29 +158,45 @@
*
* @param apiDocumentParser the parser to use.
* @param apiResources the list of API resource files.
+ * @param library the name of the library whose APIs should be parsed.
* @return a stream of {@link JDiffClassDescription}.
*/
private Stream<JDiffClassDescription> parseActiveSharedLibraryApis(
- ApiDocumentParser apiDocumentParser, String[] apiResources) {
+ ApiDocumentParser apiDocumentParser, String[] apiResources, String library) {
+
return retrieveApiResourcesAsStream(getClass().getClassLoader(), apiResources)
- .filter(this::checkLibrary)
+ .filter(path -> {
+ String apiLibraryName = getLibraryNameFromPath(path);
+ return apiLibraryName.equals(library);
+ })
.flatMap(apiDocumentParser::parseAsStream);
}
/**
- * Tests that the device's API matches the expected set defined in xml.
- * <p/>
- * Will check the entire API, and then report the complete list of failures
+ * Tests that each shared library's API matches its current API.
+ *
+ * <p>One test per shared library, checks the entire API, and then reports the complete list of
+ * failures.</p>
*/
@Test
- public void testSignature() {
+ // Parameterize this method with the set of testable shared libraries.
+ @Parameters(method = "getTestableSharedLibraryParameters")
+ // The test name is the method name followed by and _ and the shared library name, with .s
+ // replaced with _. e.g. testRuntimeCompatibilityWithCurrentApi_android_test_base.
+ @TestCaseName("{method}_{0}")
+ public void testRuntimeCompatibilityWithCurrentApi(TestableLibraryParameter parameter) {
+ String library = parameter.getName();
+ skipTestIfLibraryIsUnavailable(library);
runWithTestResultObserver(mResultObserver -> {
ApiComplianceChecker complianceChecker =
new ApiComplianceChecker(mResultObserver, mClassProvider);
+ // Load classes from any API files that form the base which the expected APIs extend.
+ loadBaseClasses(complianceChecker);
+
ApiDocumentParser apiDocumentParser = new ApiDocumentParser(TAG);
- parseActiveSharedLibraryApis(apiDocumentParser, expectedApiFiles)
+ parseActiveSharedLibraryApis(apiDocumentParser, EXPECTED_API_FILES.get(), library)
.forEach(complianceChecker::checkSignatureCompliance);
// After done parsing all expected API files, perform any deferred checks.
@@ -96,17 +205,27 @@
}
/**
- * Tests that the device's API matches the previous APIs defined in xml.
+ * Tests that each shared library's API matches its previous APIs.
+ *
+ * <p>One test per shared library, checks the entire API, and then reports the complete list of
+ * failures.</p>
*/
@Test
- public void testPreviousSignatures() {
+ // Parameterize this method with the set of testable shared libraries.
+ @Parameters(method = "getTestableSharedLibraryParameters")
+ // The test name is the method name followed by and _ and the shared library name, with .s
+ // replaced with _. e.g. testRuntimeCompatibilityWithPreviousApis_android_test_base.
+ @TestCaseName("{method}_{0}")
+ public void testRuntimeCompatibilityWithPreviousApis(TestableLibraryParameter parameter) {
+ String library = parameter.getName();
+ skipTestIfLibraryIsUnavailable(library);
runWithTestResultObserver(mResultObserver -> {
ApiComplianceChecker complianceChecker =
new ApiComplianceChecker(mResultObserver, mClassProvider);
ApiDocumentParser apiDocumentParser = new ApiDocumentParser(TAG);
- parseActiveSharedLibraryApis(apiDocumentParser, previousApiFiles)
+ parseActiveSharedLibraryApis(apiDocumentParser, PREVIOUS_API_FILES.get(), library)
.map(clazz -> clazz.setPreviousApiFlag(true))
.forEach(complianceChecker::checkSignatureCompliance);
@@ -116,25 +235,37 @@
}
/**
- * Check to see if the supplied name is an API file for a shared library that is available on
- * this device.
+ * Get the library name from the API file's path.
*
* @param path the path of the API file.
- * @return true if the API corresponds to a shared library on the device, false otherwise.
+ * @return the library name for the API file.
*/
- private boolean checkLibrary (VirtualPath path) {
+ private String getLibraryNameFromPath(VirtualPath path) {
String name = path.toString();
- String libraryName = name.substring(name.lastIndexOf('/') + 1).split("-")[0];
- boolean matched = libraries.contains(libraryName);
- if (matched) {
- System.out.printf("%s: Processing API file %s, from library %s as it does match a"
- + " shared library on this device%n",
- getClass().getSimpleName(), name, libraryName);
- } else {
- System.out.printf("%s: Ignoring API file %s, from library %s as it does not match a"
- + " shared library on this device%n",
- getClass().getSimpleName(), name, libraryName);
+ return name.substring(name.lastIndexOf('/') + 1).split("-")[0];
+ }
+
+ /**
+ * A wrapper around a shared library name to ensure that its string representation is suitable
+ * for use in a parameterized test name, i.e. does not contain any characters that are not
+ * allowed in a test name by CTS/AndroidJUnitRunner.
+ */
+ public static class TestableLibraryParameter {
+ private final String name;
+ private final String parameter;
+
+ public TestableLibraryParameter(String name) {
+ this.name = name;
+ this.parameter = name.replace('.', '_');
}
- return matched;
+
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public String toString() {
+ return parameter;
+ }
}
}
diff --git a/tests/signature/api-check/src/java/android/signature/cts/api/AbstractApiTest.java b/tests/signature/api-check/src/java/android/signature/cts/api/AbstractApiTest.java
index f4d364b..b1dd014 100644
--- a/tests/signature/api-check/src/java/android/signature/cts/api/AbstractApiTest.java
+++ b/tests/signature/api-check/src/java/android/signature/cts/api/AbstractApiTest.java
@@ -26,24 +26,16 @@
import android.signature.cts.JDiffClassDescription;
import android.signature.cts.ResultObserver;
import android.signature.cts.VirtualPath;
-import android.signature.cts.VirtualPath.LocalFilePath;
-import android.signature.cts.VirtualPath.ResourcePath;
import android.util.Log;
-
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
-
import com.android.compatibility.common.util.DynamicConfigDeviceSide;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.nio.file.Files;
-import java.nio.file.Path;
+import com.google.common.base.Suppliers;
import java.util.Collection;
import java.util.Collections;
-import java.util.function.Predicate;
+import java.util.function.Supplier;
import java.util.stream.Stream;
-import java.util.zip.ZipFile;
+import org.junit.AfterClass;
import org.junit.Before;
import org.junit.runner.RunWith;
@@ -62,8 +54,6 @@
*/
private static final String DYNAMIC_CONFIG_NAME_OPTION = "dynamic-config-name";
- private static final String TAG = "SignatureTest";
-
private TestResultObserver mResultObserver;
ClassProvider mClassProvider;
@@ -73,6 +63,11 @@
*/
private Collection<String> expectedFailures = Collections.emptyList();
+ @AfterClass
+ public static void closeResourceStore() {
+ ResourceStore.close();
+ }
+
public Instrumentation getInstrumentation() {
return InstrumentationRegistry.getInstrumentation();
}
@@ -176,6 +171,13 @@
mResultObserver.onTestComplete(); // Will throw is there are failures
}
+ static Supplier<String[]> getSupplierOfAnOptionalCommaSeparatedListArgument(String key) {
+ return Suppliers.memoize(() -> {
+ Bundle arguments = InstrumentationRegistry.getArguments();
+ return getCommaSeparatedListOptional(arguments, key);
+ })::get;
+ }
+
static String[] getCommaSeparatedListOptional(Bundle instrumentationArgs, String key) {
String argument = instrumentationArgs.getString(key);
if (argument == null) {
@@ -184,6 +186,13 @@
return argument.split(",");
}
+ static Supplier<String[]> getSupplierOfAMandatoryCommaSeparatedListArgument(String key) {
+ return Suppliers.memoize(() -> {
+ Bundle arguments = InstrumentationRegistry.getArguments();
+ return getCommaSeparatedListRequired(arguments, key);
+ })::get;
+ }
+
static String[] getCommaSeparatedListRequired(Bundle instrumentationArgs, String key) {
String argument = instrumentationArgs.getString(key);
if (argument == null) {
@@ -192,47 +201,6 @@
return argument.split(",");
}
- private static Stream<VirtualPath> readResource(ClassLoader classLoader, String resourceName) {
- try {
- ResourcePath resourcePath =
- VirtualPath.get(classLoader, resourceName);
- if (resourceName.endsWith(".zip")) {
- // Extract to a temporary file and read from there.
- Path file = extractResourceToFile(resourceName, resourcePath.newInputStream());
- return flattenPaths(VirtualPath.get(file.toString()));
- } else {
- return Stream.of(resourcePath);
- }
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
-
- private static Path extractResourceToFile(String resourceName, InputStream is) throws IOException {
- Path tempDirectory = Files.createTempDirectory("signature");
- Path file = tempDirectory.resolve(resourceName);
- Log.i(TAG, "extractResourceToFile: extracting " + resourceName + " to " + file);
- Files.copy(is, file);
- is.close();
- return file;
- }
-
- /**
- * Given a path in the local file system (possibly of a zip file) flatten it into a stream of
- * virtual paths.
- */
- private static Stream<VirtualPath> flattenPaths(LocalFilePath path) {
- try {
- if (path.toString().endsWith(".zip")) {
- return getZipEntryFiles(path);
- } else {
- return Stream.of(path);
- }
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
-
/**
* Create a stream of {@link JDiffClassDescription} by parsing a set of API resource files.
*
@@ -263,18 +231,6 @@
ClassLoader classLoader,
String[] apiResources) {
return Stream.of(apiResources)
- .flatMap(resourceName -> readResource(classLoader, resourceName));
- }
-
- /**
- * Get the zip entries that are files.
- *
- * @param path the path to the zip file.
- * @return paths to zip entries
- */
- private static Stream<VirtualPath> getZipEntryFiles(LocalFilePath path) throws IOException {
- @SuppressWarnings("resource")
- ZipFile zip = new ZipFile(path.toFile());
- return zip.stream().map(entry -> VirtualPath.get(zip, entry));
+ .flatMap(resourceName -> ResourceStore.readResource(classLoader, resourceName));
}
}
diff --git a/tests/signature/api-check/src/java/android/signature/cts/api/AbstractSignatureTest.java b/tests/signature/api-check/src/java/android/signature/cts/api/AbstractSignatureTest.java
new file mode 100644
index 0000000..6df224f
--- /dev/null
+++ b/tests/signature/api-check/src/java/android/signature/cts/api/AbstractSignatureTest.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2022 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.signature.cts.api;
+
+import android.signature.cts.ApiComplianceChecker;
+import android.signature.cts.ApiDocumentParser;
+import java.util.function.Supplier;
+
+/**
+ * Base class of the tests that check accessibility of API signatures at runtime.
+ */
+public class AbstractSignatureTest extends AbstractApiTest {
+
+ private static final String TAG = AbstractSignatureTest.class.getSimpleName();
+
+ /**
+ * The name of the instrumentation option that contains the list of the current API signatures
+ * that are expected to be accessible.
+ */
+ protected static final String EXPECTED_API_FILES_ARG = "expected-api-files";
+
+ /**
+ * The name of the instrumentation option that contains the list of the previous API signatures
+ * that are expected to be accessible.
+ */
+ protected static final String PREVIOUS_API_FILES_ARG = "previous-api-files";
+
+ /**
+ * Supplier of the list of files specified in the instrumentation argument "base-api-files".
+ */
+ private static final Supplier<String[]> BASE_API_FILES =
+ getSupplierOfAnOptionalCommaSeparatedListArgument("base-api-files");
+
+ /**
+ * Load the base API files into the supplied compliance checker.
+ *
+ * <p>Base API files are not checked by the compliance checker but may be extended by classes
+ * which are checked.</p>
+ *
+ * @param complianceChecker the {@link ApiComplianceChecker} into which the base API will be
+ * loaded.
+ */
+ protected void loadBaseClasses(ApiComplianceChecker complianceChecker) {
+ ApiDocumentParser apiDocumentParser = new ApiDocumentParser(TAG);
+ parseApiResourcesAsStream(apiDocumentParser, BASE_API_FILES.get())
+ .forEach(complianceChecker::addBaseClass);
+ }
+}
diff --git a/tests/signature/api-check/src/java/android/signature/cts/api/ResourceStore.java b/tests/signature/api-check/src/java/android/signature/cts/api/ResourceStore.java
new file mode 100644
index 0000000..722ea63
--- /dev/null
+++ b/tests/signature/api-check/src/java/android/signature/cts/api/ResourceStore.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2022 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.signature.cts.api;
+
+import android.signature.cts.VirtualPath;
+import android.util.Log;
+import com.google.common.base.Suppliers;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Supplier;
+import java.util.stream.Stream;
+import java.util.zip.ZipFile;
+
+/**
+ * Manages local storage of resources that need to be extracted from the Jar into the local
+ * filesystem before use.
+ */
+public class ResourceStore {
+
+ private static final String TAG = ResourceStore.class.getSimpleName();
+
+ /**
+ * Supplier for the temporary directory.
+ */
+ private static final Supplier<Path> TEMPORARY_DIRECTORY = Suppliers.memoize(() -> {
+ try {
+ return Files.createTempDirectory("signature");
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ })::get;
+
+ /**
+ * A map from a {@link VirtualPath} to a {@link ZipFile} for zip file resources that
+ * have been extracted from the jar into the local file system.
+ */
+ private static final Map<VirtualPath, ZipFile> extractedZipFiles = new HashMap<>();
+
+ public static Stream<VirtualPath> readResource(ClassLoader classLoader, String resourceName) {
+ try {
+ VirtualPath resourcePath = VirtualPath.get(classLoader, resourceName);
+ if (resourceName.endsWith(".zip")) {
+ // Extract to a temporary file and then read from there. If the resource has already
+ // been extracted before then reuse the previous file.
+ @SuppressWarnings("resource")
+ ZipFile zip = extractedZipFiles.computeIfAbsent(resourcePath, virtualPath -> {
+ try {
+ Path path = extractResourceToFile(resourceName, resourcePath);
+ return new ZipFile(path.toFile());
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ return zip.stream().map(entry -> VirtualPath.get(zip, entry));
+ } else {
+ return Stream.of(resourcePath);
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Close any previously opened {@link ZipFile} instances.
+ */
+ public static void close() {
+ for (ZipFile zipfile: extractedZipFiles.values()) {
+ // If an error occurred when closing the ZipFile log the failure and continue.
+ try {
+ zipfile.close();
+ } catch (IOException e) {
+ Log.e(TAG, "Could not close ZipFile " + zipfile, e);
+ }
+ }
+ }
+
+ private static Path extractResourceToFile(String resourceName, VirtualPath resourcePath)
+ throws IOException {
+ Path tempDirectory = TEMPORARY_DIRECTORY.get();
+ Path file = tempDirectory.resolve(resourceName);
+ try (InputStream is = resourcePath.newInputStream()) {
+ Log.i(TAG, "extractResourceToFile: extracting " + resourceName + " to " + file);
+ Files.copy(is, file);
+ }
+ return file;
+ }
+}
diff --git a/tests/signature/api-check/src/java/android/signature/cts/api/SignatureTest.java b/tests/signature/api-check/src/java/android/signature/cts/api/SignatureTest.java
index 1be75ca..136211f 100644
--- a/tests/signature/api-check/src/java/android/signature/cts/api/SignatureTest.java
+++ b/tests/signature/api-check/src/java/android/signature/cts/api/SignatureTest.java
@@ -23,48 +23,55 @@
import android.signature.cts.FailureType;
import android.signature.cts.JDiffClassDescription;
import android.signature.cts.ReflectionHelper;
+import com.google.common.base.Suppliers;
import java.util.Comparator;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Predicate;
+import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.junit.Test;
/**
* Performs the signature check via a JUnit test.
*/
-public class SignatureTest extends AbstractApiTest {
+public class SignatureTest extends AbstractSignatureTest {
private static final String TAG = SignatureTest.class.getSimpleName();
- protected String[] expectedApiFiles;
- protected String[] previousApiFiles;
- protected String[] baseApiFiles;
- private String[] unexpectedApiFiles;
+ private static final String UNEXPECTED_API_FILES_ARG = "unexpected-api-files";
+
+ private static final Supplier<String[]> EXPECTED_API_FILES =
+ getSupplierOfAnOptionalCommaSeparatedListArgument(EXPECTED_API_FILES_ARG);
+ private static final Supplier<String[]> UNEXPECTED_API_FILES =
+ getSupplierOfAnOptionalCommaSeparatedListArgument(UNEXPECTED_API_FILES_ARG);
+ private static final Supplier<String[]> PREVIOUS_API_FILES =
+ getSupplierOfAnOptionalCommaSeparatedListArgument(PREVIOUS_API_FILES_ARG);
+
+ private static final Supplier<Set<JDiffClassDescription>> UNEXPECTED_CLASSES =
+ Suppliers.memoize(SignatureTest::loadUnexpectedClasses)::get;
@Override
- protected void initializeFromArgs(Bundle instrumentationArgs) throws Exception {
- expectedApiFiles = getCommaSeparatedListOptional(instrumentationArgs, "expected-api-files");
- baseApiFiles = getCommaSeparatedListOptional(instrumentationArgs, "base-api-files");
- unexpectedApiFiles = getCommaSeparatedListOptional(instrumentationArgs, "unexpected-api-files");
- previousApiFiles = getCommaSeparatedListOptional(instrumentationArgs, "previous-api-files");
+ protected void initializeFromArgs(Bundle instrumentationArgs) {
+ String[] expectedApiFiles = EXPECTED_API_FILES.get();
+ String[] unexpectedApiFiles = UNEXPECTED_API_FILES.get();
if (expectedApiFiles.length + unexpectedApiFiles.length == 0) {
throw new IllegalStateException(
- "Expected at least one file to be specified in"
- + " 'expected-api-files' or 'unexpected-api-files'");
+ String.format("Expected at least one file to be specified in '%s' or '%s'",
+ EXPECTED_API_FILES_ARG, UNEXPECTED_API_FILES_ARG));
}
}
/**
- * Tests that the device's API matches the expected set defined in xml.
- * <p/>
- * Will check the entire API, and then report the complete list of failures
+ * Make sure that this APK cannot access any unexpected classes.
+ *
+ * <p>The set of unexpected classes may be empty, in which case this test does nothing.</p>
*/
@Test
- public void testSignature() {
+ public void testCannotAccessUnexpectedClasses() {
runWithTestResultObserver(mResultObserver -> {
- Set<JDiffClassDescription> unexpectedClasses = loadUnexpectedClasses();
+ Set<JDiffClassDescription> unexpectedClasses = UNEXPECTED_CLASSES.get();
for (JDiffClassDescription classDescription : unexpectedClasses) {
Class<?> unexpectedClass = findUnexpectedClass(classDescription, mClassProvider);
if (unexpectedClass != null) {
@@ -74,16 +81,52 @@
"Class should not be accessible to this APK");
}
}
+ });
+ }
+ /**
+ * Tests that the device's API matches the expected set defined in xml.
+ *
+ * <p>Will check the entire API, and then report the complete list of failures</p>
+ */
+ @Test
+ public void testRuntimeCompatibilityWithCurrentApi() {
+ runWithTestResultObserver(mResultObserver -> {
ApiComplianceChecker complianceChecker =
new ApiComplianceChecker(mResultObserver, mClassProvider);
// Load classes from any API files that form the base which the expected APIs extend.
loadBaseClasses(complianceChecker);
+
// Load classes from system API files and check for signature compliance.
+ String[] expectedApiFiles = EXPECTED_API_FILES.get();
+ Set<JDiffClassDescription> unexpectedClasses = UNEXPECTED_CLASSES.get();
checkClassesSignatureCompliance(complianceChecker, expectedApiFiles, unexpectedClasses,
false /* isPreviousApi */);
+
+ // After done parsing all expected API files, perform any deferred checks.
+ complianceChecker.checkDeferred();
+ });
+ }
+
+ /**
+ * Tests that the device's API matches the last few previously released api files.
+ *
+ * <p>Will check all the recently released api files, and then report the complete list of
+ * failures.</p>
+ */
+ @Test
+ public void testRuntimeCompatibilityWithPreviousApis() {
+ runWithTestResultObserver(mResultObserver -> {
+ ApiComplianceChecker complianceChecker =
+ new ApiComplianceChecker(mResultObserver, mClassProvider);
+
+ // Load classes from any API files that form the base which the expected APIs extend.
+ loadBaseClasses(complianceChecker);
+
// Load classes from previous API files and check for signature compliance.
+ String[] previousApiFiles = PREVIOUS_API_FILES.get();
+ Set<JDiffClassDescription> unexpectedClasses = UNEXPECTED_CLASSES.get();
checkClassesSignatureCompliance(complianceChecker, previousApiFiles, unexpectedClasses,
true /* isPreviousApi */);
@@ -105,9 +148,10 @@
}
}
- private Set<JDiffClassDescription> loadUnexpectedClasses() {
+ private static Set<JDiffClassDescription> loadUnexpectedClasses() {
ApiDocumentParser apiDocumentParser = new ApiDocumentParser(TAG);
- return parseApiResourcesAsStream(apiDocumentParser, unexpectedApiFiles)
+ return retrieveApiResourcesAsStream(SignatureTest.class.getClassLoader(), UNEXPECTED_API_FILES.get())
+ .flatMap(apiDocumentParser::parseAsStream)
.collect(Collectors.toCollection(SignatureTest::newSetOfClassDescriptions));
}
@@ -115,12 +159,6 @@
return new TreeSet<>(Comparator.comparing(JDiffClassDescription::getAbsoluteClassName));
}
- private void loadBaseClasses(ApiComplianceChecker complianceChecker) {
- ApiDocumentParser apiDocumentParser = new ApiDocumentParser(TAG);
- parseApiResourcesAsStream(apiDocumentParser, baseApiFiles)
- .forEach(complianceChecker::addBaseClass);
- }
-
private void checkClassesSignatureCompliance(ApiComplianceChecker complianceChecker,
String[] classes, Set<JDiffClassDescription> unexpectedClasses, boolean isPreviousApi) {
ApiDocumentParser apiDocumentParser = new ApiDocumentParser(TAG);
@@ -129,5 +167,4 @@
.map(clazz -> clazz.setPreviousApiFlag(isPreviousApi))
.forEach(complianceChecker::checkSignatureCompliance);
}
-
}
diff --git a/tests/signature/lib/android/src/android/signature/cts/VirtualPath.java b/tests/signature/lib/android/src/android/signature/cts/VirtualPath.java
index 140dd6d..51b2ff0 100644
--- a/tests/signature/lib/android/src/android/signature/cts/VirtualPath.java
+++ b/tests/signature/lib/android/src/android/signature/cts/VirtualPath.java
@@ -20,6 +20,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
+import java.util.Objects;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
@@ -55,6 +56,24 @@
public abstract InputStream newInputStream() throws IOException;
+ /**
+ * Override as abstract to force sub-classes to implement it.
+ */
+ @Override
+ public abstract int hashCode();
+
+ /**
+ * Override as abstract to force sub-classes to implement it.
+ */
+ @Override
+ public abstract boolean equals(Object obj);
+
+ /**
+ * Override as abstract to force sub-classes to implement it.
+ */
+ @Override
+ public abstract String toString();
+
public static class LocalFilePath extends VirtualPath {
private final String path;
@@ -76,6 +95,23 @@
}
@Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ LocalFilePath that = (LocalFilePath) o;
+ return path.equals(that.path);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(path);
+ }
+
+ @Override
public String toString() {
return path;
}
@@ -98,6 +134,24 @@
}
@Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ ZipEntryPath that = (ZipEntryPath) o;
+ return zip.getName().equals(that.zip.getName())
+ && entry.getName().equals(that.entry.getName());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(zip.getName(), entry.getName());
+ }
+
+ @Override
public String toString() {
return "zip:file:" + zip.getName() + "!/" + entry.getName();
}
@@ -119,6 +173,23 @@
}
@Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ ResourcePath that = (ResourcePath) o;
+ return url.equals(that.url);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(url);
+ }
+
+ @Override
public String toString() {
return url.toExternalForm();
}
diff --git a/tests/tests/car_builtin/src/android/car/cts/builtin/app/ActivityManagerHelperTest.java b/tests/tests/car_builtin/src/android/car/cts/builtin/app/ActivityManagerHelperTest.java
index 7c23b4d..aaba29f 100644
--- a/tests/tests/car_builtin/src/android/car/cts/builtin/app/ActivityManagerHelperTest.java
+++ b/tests/tests/car_builtin/src/android/car/cts/builtin/app/ActivityManagerHelperTest.java
@@ -44,12 +44,11 @@
import androidx.test.runner.AndroidJUnit4;
import com.android.compatibility.common.util.PollingCheck;
-import com.android.compatibility.common.util.SystemUtil;
+import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
-import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -69,14 +68,11 @@
private static final String GRANTED_PERMISSION_INTERACT_ACROSS_USERS =
"android.permission.INTERACT_ACROSS_USERS";
private static final String PERMISSION_REMOVE_TASKS = "android.permission.REMOVE_TASKS";
- private static final String PERMISSION_GET_TASKS = "android.permission.GET_TASKS";
private static final String PERMISSION_MANAGE_ACTIVITY_TASKS =
"android.permission.MANAGE_ACTIVITY_TASKS";
private static final String SIMPLE_APP_PACKAGE_NAME = "android.car.cts.builtin.apps.simple";
- private static final String SIMPLE_ACTIVITY_NAME = "SimpleActivity";
- private static final String START_SIMPLE_ACTIVITY_COMMAND = "am start -W -n "
- + SIMPLE_APP_PACKAGE_NAME + "/." + SIMPLE_ACTIVITY_NAME;
+ private static final String SIMPLE_ACTIVITY_NAME = SIMPLE_APP_PACKAGE_NAME + ".SimpleActivity";
private static final int OWNING_UID = UserHandle.ALL.getIdentifier();
private static final int MAX_NUM_TASKS = 1_000;
@@ -100,6 +96,13 @@
private final Instrumentation mInstrumentation = InstrumentationRegistry.getInstrumentation();
private final Context mContext = mInstrumentation.getContext();
+ @Before
+ public void setUp() throws Exception {
+ // Home was launched in ActivityManagerTestBase#setUp, wait until it is stable,
+ // in order not to mix the event of its TaskView Activity with the TestActivity.
+ mWmState.waitForHomeActivityVisible();
+ }
+
@Test
public void testCheckComponentPermission() throws Exception {
// not requested from Manifest
@@ -183,20 +186,18 @@
public void testProcessObserverCallback() throws Exception {
// setup
ProcessObserverCallbackTestImpl callbackImpl = new ProcessObserverCallbackTestImpl();
- launchSimpleActivity();
// execute
try {
mInstrumentation.getUiAutomation().adoptShellPermissionIdentity(
- PERMISSION_SET_ACTIVITY_WATCHER);
+ PERMISSION_SET_ACTIVITY_WATCHER); // for registerProcessObserverCallback
ActivityManagerHelper.registerProcessObserverCallback(callbackImpl);
- ArrayList<Integer> appTasks = getAppTasks(SIMPLE_APP_PACKAGE_NAME);
- appTasks.forEach((taskId) -> ActivityManagerHelper.removeTask(taskId));
+ launchSimpleActivity();
// assert
- assertThat(callbackImpl.isProcessDiedObserved()).isTrue();
+ assertThat(callbackImpl.waitForForegroundActivitiesChanged()).isTrue();
} finally {
// teardown
ActivityManagerHelper.unregisterProcessObserverCallback(callbackImpl);
@@ -249,45 +250,27 @@
private static final class ProcessObserverCallbackTestImpl extends ProcessObserverCallback {
private final CountDownLatch mLatch = new CountDownLatch(1);
- private int mObservedPid = -1;
- private int mObservedUid = -1;
- private boolean mProcessDiedObserved;
-
+ // Use onForegroundActivitiesChanged(), because onProcessDied() can be called
+ // in very long time later even if the task was removed.
@Override
- public void onProcessDied(int pid, int uid) {
- mProcessDiedObserved = true;
- Log.d(TAG, "ProcessDied: pid " + pid + " uid " + uid);
+ public void onForegroundActivitiesChanged(int pid, int uid, boolean foregroundActivities) {
+ Log.d(TAG, "onForegroundActivitiesChanged: pid " + pid + " uid " + uid);
mLatch.countDown();
}
- public boolean isProcessDiedObserved() throws Exception {
- if (!mLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
- throw new Exception("process died is not observed in " + TIMEOUT_MS + " ms)");
- }
- return mProcessDiedObserved;
+ public boolean waitForForegroundActivitiesChanged() throws Exception {
+ return mLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS);
}
}
private void launchSimpleActivity() {
- SystemUtil.runShellCommand(START_SIMPLE_ACTIVITY_COMMAND);
- }
-
- private ArrayList<Integer> getAppTasks(String pkgName) {
- ActivityManager am = mContext.getSystemService(ActivityManager.class);
-
- List<ActivityManager.RunningTaskInfo> runningTasks = am.getRunningTasks(MAX_NUM_TASKS);
- ArrayList<Integer> appTasks = new ArrayList<>();
- for (ActivityManager.RunningTaskInfo taskInfo : runningTasks) {
- String taskPackageName = taskInfo.baseActivity.getPackageName();
- int taskId = taskInfo.taskId;
- Log.d(TAG, "tasks package name: " + taskPackageName);
- if (taskPackageName.equals(pkgName)) {
- Log.d(TAG, "getAppTask(): adding " + SIMPLE_APP_PACKAGE_NAME + " task " + taskId);
- appTasks.add(taskId);
- }
- }
-
- return appTasks;
+ ComponentName simpleActivity = new ComponentName(
+ SIMPLE_APP_PACKAGE_NAME, SIMPLE_ACTIVITY_NAME);
+ Intent intent = new Intent()
+ .setComponent(simpleActivity)
+ .addFlags(FLAG_ACTIVITY_NEW_TASK);
+ mContext.startActivity(intent, /* options = */ null);
+ waitAndAssertTopResumedActivity(simpleActivity, DEFAULT_DISPLAY, "Activity isn't resumed");
}
private <T> T launchTestActivity(Class<T> type) {