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) {