Move existing library loading tests to a TF hostside test.

Replace the manual runtest.sh script with something that tradefed can
run. It requires root and will remount, push .so libraries and
public.libraries*txt files to any path they need to go to, and then do
a soft reboot.

Since libraries are pushed to the test locations rather than being
installed through LOCAL_MODULE_PATH, the shared libraries in the build
don't matter, and the same one is now used for all locations. That
means it no longer exercises the `product_specific` Soong property for
the two product libraries.

Test: atest libnativeloader_e2e_tests
Bug: 137356719
Change-Id: Ida1c70cb282946d72736da5cab0c751b60f0b03a
diff --git a/libnativeloader/test/Android.bp b/libnativeloader/test/Android.bp
index fb9ae0d..6f5e49f 100644
--- a/libnativeloader/test/Android.bp
+++ b/libnativeloader/test/Android.bp
@@ -24,57 +24,56 @@
 }
 
 cc_library {
-    name: "libfoo.oem1",
-    srcs: ["test.cpp"],
-    cflags: ["-DLIBNAME=\"libfoo.oem1.so\""],
-    shared_libs: [
-        "libbase",
+    name: "libnativeloader_testlib",
+    srcs: [],
+    stl: "none",
+}
+
+// This app is just an intermediate container to be able to include the .so
+// library as a java resource in the host test. It's not actually installed or
+// started.
+android_test_helper_app {
+    name: "library_container_app",
+    defaults: ["art_module_source_build_java_defaults"],
+    manifest: "library_container_app_manifest.xml",
+    compile_multilib: "both",
+    jni_libs: ["libnativeloader_testlib"],
+}
+
+android_test_helper_app {
+    name: "loadlibrarytest_system_app",
+    defaults: ["art_module_source_build_java_defaults"],
+    manifest: "loadlibrarytest_system_app_manifest.xml",
+    srcs: ["src/android/test/app/SystemAppTest.java"],
+    static_libs: [
+        "androidx.test.ext.junit",
+        "androidx.test.ext.truth",
+        "androidx.test.rules",
     ],
 }
 
-cc_library {
-    name: "libbar.oem1",
-    srcs: ["test.cpp"],
-    cflags: ["-DLIBNAME=\"libbar.oem1.so\""],
-    shared_libs: [
-        "libbase",
+android_test_helper_app {
+    name: "loadlibrarytest_vendor_app",
+    defaults: ["art_module_source_build_java_defaults"],
+    manifest: "loadlibrarytest_vendor_app_manifest.xml",
+    srcs: ["src/android/test/app/VendorAppTest.java"],
+    static_libs: [
+        "androidx.test.ext.junit",
+        "androidx.test.ext.truth",
+        "androidx.test.rules",
     ],
 }
 
-cc_library {
-    name: "libfoo.oem2",
-    srcs: ["test.cpp"],
-    cflags: ["-DLIBNAME=\"libfoo.oem2.so\""],
-    shared_libs: [
-        "libbase",
+java_test_host {
+    name: "libnativeloader_e2e_tests",
+    defaults: ["art_module_source_build_java_defaults"],
+    srcs: ["src/android/test/hostside/*.java"],
+    libs: ["tradefed"],
+    java_resources: [
+        ":library_container_app",
+        ":loadlibrarytest_system_app",
+        ":loadlibrarytest_vendor_app",
     ],
-}
-
-cc_library {
-    name: "libbar.oem2",
-    srcs: ["test.cpp"],
-    cflags: ["-DLIBNAME=\"libbar.oem2.so\""],
-    shared_libs: [
-        "libbase",
-    ],
-}
-
-cc_library {
-    name: "libfoo.product1",
-    srcs: ["test.cpp"],
-    cflags: ["-DLIBNAME=\"libfoo.product1.so\""],
-    product_specific: true,
-    shared_libs: [
-        "libbase",
-    ],
-}
-
-cc_library {
-    name: "libbar.product1",
-    srcs: ["test.cpp"],
-    cflags: ["-DLIBNAME=\"libbar.product1.so\""],
-    product_specific: true,
-    shared_libs: [
-        "libbase",
-    ],
+    test_config: "libnativeloader_e2e_tests.xml",
+    test_suites: ["generic-tests"],
 }
diff --git a/libnativeloader/test/Android.mk b/libnativeloader/test/Android.mk
deleted file mode 100644
index 95fa68a..0000000
--- a/libnativeloader/test/Android.mk
+++ /dev/null
@@ -1,72 +0,0 @@
-#
-# Copyright (C) 2017 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.
-#
-LOCAL_PATH:= $(call my-dir)
-
-include $(CLEAR_VARS)
-LOCAL_MODULE := public.libraries-oem1.txt
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
-LOCAL_LICENSE_CONDITIONS := notice
-LOCAL_NOTICE_FILE := $(LOCAL_PATH)/../../NOTICE
-LOCAL_SRC_FILES:= $(LOCAL_MODULE)
-LOCAL_MODULE_CLASS := ETC
-LOCAL_MODULE_PATH := $(TARGET_OUT_ETC)
-include $(BUILD_PREBUILT)
-
-include $(CLEAR_VARS)
-LOCAL_MODULE := public.libraries-oem2.txt
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
-LOCAL_LICENSE_CONDITIONS := notice
-LOCAL_NOTICE_FILE := $(LOCAL_PATH)/../../NOTICE
-LOCAL_SRC_FILES:= $(LOCAL_MODULE)
-LOCAL_MODULE_CLASS := ETC
-LOCAL_MODULE_PATH := $(TARGET_OUT_ETC)
-include $(BUILD_PREBUILT)
-
-include $(CLEAR_VARS)
-LOCAL_MODULE := public.libraries-product1.txt
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
-LOCAL_LICENSE_CONDITIONS := notice
-LOCAL_NOTICE_FILE := $(LOCAL_PATH)/../../NOTICE
-LOCAL_SRC_FILES:= $(LOCAL_MODULE)
-LOCAL_MODULE_CLASS := ETC
-LOCAL_MODULE_PATH := $(TARGET_OUT_PRODUCT_ETC)
-include $(BUILD_PREBUILT)
-
-include $(CLEAR_VARS)
-LOCAL_PACKAGE_NAME := oemlibrarytest-system
-LOCAL_MODULE_TAGS := tests
-LOCAL_MANIFEST_FILE := system/AndroidManifest.xml
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
-LOCAL_SDK_VERSION := current
-LOCAL_PROGUARD_ENABLED := disabled
-LOCAL_MODULE_PATH := $(TARGET_OUT_APPS)
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
-LOCAL_LICENSE_CONDITIONS := notice
-LOCAL_NOTICE_FILE := $(LOCAL_PATH)/../../NOTICE
-include $(BUILD_PACKAGE)
-
-include $(CLEAR_VARS)
-LOCAL_PACKAGE_NAME := oemlibrarytest-vendor
-LOCAL_MODULE_TAGS := tests
-LOCAL_MANIFEST_FILE := vendor/AndroidManifest.xml
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
-LOCAL_SDK_VERSION := current
-LOCAL_PROGUARD_ENABLED := disabled
-LOCAL_MODULE_PATH := $(TARGET_OUT_VENDOR_APPS)
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
-LOCAL_LICENSE_CONDITIONS := notice
-LOCAL_NOTICE_FILE := $(LOCAL_PATH)/../../NOTICE
-include $(BUILD_PACKAGE)
diff --git a/libnativeloader/test/libnativeloader_e2e_tests.xml b/libnativeloader/test/libnativeloader_e2e_tests.xml
new file mode 100644
index 0000000..b1333b0
--- /dev/null
+++ b/libnativeloader/test/libnativeloader_e2e_tests.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 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="Config for libnativeloader e2e test cases">
+    <option name="test-suite-tag" value="libnativeloader_e2e_tests" />
+    <option name="test-suite-tag" value="apct" />
+
+    <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" />
+
+    <test class="com.android.tradefed.testtype.HostTest" >
+        <option name="jar" value="libnativeloader_e2e_tests.jar" />
+    </test>
+
+    <!-- Only run tests if the device under test is SDK version 31 (Android 12) or above. -->
+    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.Sdk31ModuleController" />
+</configuration>
diff --git a/libnativeloader/test/test.cpp b/libnativeloader/test/library_container_app_manifest.xml
similarity index 71%
rename from libnativeloader/test/test.cpp
rename to libnativeloader/test/library_container_app_manifest.xml
index b166928..20030de 100644
--- a/libnativeloader/test/test.cpp
+++ b/libnativeloader/test/library_container_app_manifest.xml
@@ -1,5 +1,6 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * 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.
@@ -12,10 +13,8 @@
  * 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.
- */
-#define LOG_TAG "oemlib"
-#include <android-base/logging.h>
+ -->
 
-static __attribute__((constructor)) void test_lib_init() {
-  LOG(DEBUG) << LIBNAME << " loaded";
-}
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.test.app.container">
+</manifest>
diff --git a/libnativeloader/test/system/AndroidManifest.xml b/libnativeloader/test/loadlibrarytest_system_app_manifest.xml
similarity index 70%
rename from libnativeloader/test/system/AndroidManifest.xml
rename to libnativeloader/test/loadlibrarytest_system_app_manifest.xml
index c304889..afb6061 100644
--- a/libnativeloader/test/system/AndroidManifest.xml
+++ b/libnativeloader/test/loadlibrarytest_system_app_manifest.xml
@@ -17,15 +17,7 @@
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="android.test.app.system">
-
-    <application>
-        <activity android:name="android.test.app.TestActivity" >
-            <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
-            </intent-filter>
-        </activity>
-    </application>
-
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="android.test.app.system" />
 </manifest>
 
diff --git a/libnativeloader/test/vendor/AndroidManifest.xml b/libnativeloader/test/loadlibrarytest_vendor_app_manifest.xml
similarity index 70%
rename from libnativeloader/test/vendor/AndroidManifest.xml
rename to libnativeloader/test/loadlibrarytest_vendor_app_manifest.xml
index c4c1a9c..578de8d 100644
--- a/libnativeloader/test/vendor/AndroidManifest.xml
+++ b/libnativeloader/test/loadlibrarytest_vendor_app_manifest.xml
@@ -17,15 +17,7 @@
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="android.test.app.vendor">
-
-    <application>
-        <activity android:name="android.test.app.TestActivity" >
-            <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
-            </intent-filter>
-        </activity>
-    </application>
-
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="android.test.app.vendor" />
 </manifest>
 
diff --git a/libnativeloader/test/public.libraries-oem1.txt b/libnativeloader/test/public.libraries-oem1.txt
deleted file mode 100644
index f9433e2..0000000
--- a/libnativeloader/test/public.libraries-oem1.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-libfoo.oem1.so
-libbar.oem1.so
diff --git a/libnativeloader/test/public.libraries-oem2.txt b/libnativeloader/test/public.libraries-oem2.txt
deleted file mode 100644
index de6bdb0..0000000
--- a/libnativeloader/test/public.libraries-oem2.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-libfoo.oem2.so
-libbar.oem2.so
diff --git a/libnativeloader/test/public.libraries-product1.txt b/libnativeloader/test/public.libraries-product1.txt
deleted file mode 100644
index 358154c..0000000
--- a/libnativeloader/test/public.libraries-product1.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-libfoo.product1.so
-libbar.product1.so
diff --git a/libnativeloader/test/runtest.sh b/libnativeloader/test/runtest.sh
deleted file mode 100755
index 40beb5b..0000000
--- a/libnativeloader/test/runtest.sh
+++ /dev/null
@@ -1,11 +0,0 @@
-#!/bin/bash
-adb root
-adb remount
-adb sync
-adb shell stop
-adb shell start
-sleep 5 # wait until device reboots
-adb logcat -c;
-adb shell am start -n android.test.app.system/android.test.app.TestActivity
-adb shell am start -n android.test.app.vendor/android.test.app.TestActivity
-adb logcat | grep android.test.app
diff --git a/libnativeloader/test/src/android/test/app/SystemAppTest.java b/libnativeloader/test/src/android/test/app/SystemAppTest.java
new file mode 100644
index 0000000..ea4db17
--- /dev/null
+++ b/libnativeloader/test/src/android/test/app/SystemAppTest.java
@@ -0,0 +1,45 @@
+/*
+ * 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.test.app;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class SystemAppTest {
+    @Test
+    public void testLoadLibraries() {
+        System.loadLibrary("foo.oem1");
+        System.loadLibrary("bar.oem1");
+        System.loadLibrary("foo.oem2");
+        System.loadLibrary("bar.oem2");
+        assertLibraryNotFoundError("foo.product1");
+        assertLibraryNotFoundError("bar.product1");
+    }
+
+    private void assertLibraryNotFoundError(String libraryName) {
+        Throwable t =
+                assertThrows(UnsatisfiedLinkError.class, () -> System.loadLibrary(libraryName));
+        assertThat(t.getMessage()).containsMatch("dlopen failed: library .* not found");
+    }
+}
diff --git a/libnativeloader/test/src/android/test/app/TestActivity.java b/libnativeloader/test/src/android/test/app/TestActivity.java
deleted file mode 100644
index a7a455d..0000000
--- a/libnativeloader/test/src/android/test/app/TestActivity.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright (C) 2018 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.test.app;
-
-import android.app.Activity;
-import android.os.Bundle;
-import android.util.Log;
-
-public class TestActivity extends Activity {
-
-    @Override
-    public void onCreate(Bundle icicle) {
-         super.onCreate(icicle);
-         tryLoadingLib("foo.oem1");
-         tryLoadingLib("bar.oem1");
-         tryLoadingLib("foo.oem2");
-         tryLoadingLib("bar.oem2");
-         tryLoadingLib("foo.product1");
-         tryLoadingLib("bar.product1");
-    }
-
-    private void tryLoadingLib(String name) {
-        try {
-            System.loadLibrary(name);
-            Log.d(getPackageName(), "library " + name + " is successfully loaded");
-        } catch (UnsatisfiedLinkError e) {
-            Log.d(getPackageName(), "failed to load libarary " + name, e);
-        }
-    }
-}
diff --git a/libnativeloader/test/src/android/test/app/VendorAppTest.java b/libnativeloader/test/src/android/test/app/VendorAppTest.java
new file mode 100644
index 0000000..ea8fdd4
--- /dev/null
+++ b/libnativeloader/test/src/android/test/app/VendorAppTest.java
@@ -0,0 +1,52 @@
+/*
+ * 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.test.app;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class VendorAppTest {
+    @Test
+    public void testLoadLibraries() {
+        assertLinkerNamespaceError("foo.oem1");
+        assertLinkerNamespaceError("bar.oem1");
+        assertLinkerNamespaceError("foo.oem2");
+        assertLinkerNamespaceError("bar.oem2");
+        assertLibraryNotFoundError("foo.product1");
+        assertLibraryNotFoundError("bar.product1");
+    }
+
+    private void assertLinkerNamespaceError(String libraryName) {
+        Throwable t =
+                assertThrows(UnsatisfiedLinkError.class, () -> System.loadLibrary(libraryName));
+        assertThat(t.getMessage())
+                .containsMatch("dlopen failed: .* is not accessible for the namespace");
+    }
+
+    private void assertLibraryNotFoundError(String libraryName) {
+        Throwable t =
+                assertThrows(UnsatisfiedLinkError.class, () -> System.loadLibrary(libraryName));
+        assertThat(t.getMessage()).containsMatch("dlopen failed: library .* not found");
+    }
+}
diff --git a/libnativeloader/test/src/android/test/hostside/LibnativeloaderTest.java b/libnativeloader/test/src/android/test/hostside/LibnativeloaderTest.java
new file mode 100644
index 0000000..7465868
--- /dev/null
+++ b/libnativeloader/test/src/android/test/hostside/LibnativeloaderTest.java
@@ -0,0 +1,258 @@
+/*
+ * 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.test.hostside;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.junit4.AfterClassWithInfo;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+import com.android.tradefed.testtype.junit4.BeforeClassWithInfo;
+import com.android.tradefed.util.CommandResult;
+import com.google.common.io.ByteStreams;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test libnativeloader behavior for apps and libs in various partitions by overlaying them over
+ * the system partitions. Requires root.
+ */
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class LibnativeloaderTest extends BaseHostJUnit4Test {
+    private static final String TAG = "LibnativeloaderTest";
+    private static final String CLEANUP_PATHS_KEY = TAG + ":CLEANUP_PATHS";
+    private static final String LOG_FILE_NAME = "TestActivity.log";
+
+    @BeforeClassWithInfo
+    public static void beforeClassWithDevice(TestInformation testInfo) throws Exception {
+        DeviceContext ctx = new DeviceContext(testInfo.getDevice());
+
+        // A soft reboot is slow, so do setup for all tests and reboot once.
+
+        ctx.mDevice.remountSystemWritable();
+        try (ZipFile libApk = openLibContainerApk()) {
+            ctx.pushSystemOemLibs(libApk);
+            ctx.pushProductLibs(libApk);
+        }
+
+        // "Install" apps in various partitions through plain adb push. We need them in these
+        // locations to test library loading restrictions, so we cannot use
+        // ITestDevice.installPackage for it since it only installs in /data.
+
+        // For testSystemApp
+        ctx.pushResource("/loadlibrarytest_system_app.apk",
+                "/system/app/loadlibrarytest_system_app/loadlibrarytest_system_app.apk");
+
+        // For testVendorApp
+        ctx.pushResource("/loadlibrarytest_vendor_app.apk",
+                "/vendor/app/loadlibrarytest_vendor_app/loadlibrarytest_vendor_app.apk");
+
+        ctx.softReboot();
+
+        testInfo.properties().put(CLEANUP_PATHS_KEY, ctx.mCleanup.getPathList());
+    }
+
+    @AfterClassWithInfo
+    public static void afterClassWithDevice(TestInformation testInfo) throws Exception {
+        String cleanupPathList = testInfo.properties().get(CLEANUP_PATHS_KEY);
+        CleanupPaths cleanup = new CleanupPaths(testInfo.getDevice(), cleanupPathList);
+        cleanup.cleanup();
+    }
+
+    @Test
+    public void testSystemApp() throws Exception {
+        runDeviceTests("android.test.app.system", "android.test.app.SystemAppTest");
+    }
+
+    @Test
+    public void testVendorApp() throws Exception {
+        runDeviceTests("android.test.app.vendor", "android.test.app.VendorAppTest");
+    }
+
+    // Utility class that keeps track of a set of paths the need to be deleted after testing.
+    private static class CleanupPaths {
+        private ITestDevice mDevice;
+        private List<String> mCleanupPaths;
+
+        CleanupPaths(ITestDevice device) {
+            mDevice = device;
+            mCleanupPaths = new ArrayList<String>();
+        }
+
+        CleanupPaths(ITestDevice device, String pathList) {
+            mDevice = device;
+            mCleanupPaths = Arrays.asList(pathList.split(":"));
+        }
+
+        String getPathList() { return String.join(":", mCleanupPaths); }
+
+        // Adds the given path, or its topmost nonexisting parent directory, to the list of paths to
+        // clean up.
+        void addPath(String devicePath) throws DeviceNotAvailableException {
+            File path = new File(devicePath);
+            while (true) {
+                File parentPath = path.getParentFile();
+                if (parentPath == null || mDevice.doesFileExist(parentPath.toString())) {
+                    break;
+                }
+                path = parentPath;
+            }
+            String nonExistingPath = path.toString();
+            if (!mCleanupPaths.contains(nonExistingPath)) {
+                mCleanupPaths.add(nonExistingPath);
+            }
+        }
+
+        void cleanup() throws DeviceNotAvailableException {
+            // Clean up in reverse order in case several pushed files were in the same nonexisting
+            // directory.
+            for (int i = mCleanupPaths.size() - 1; i >= 0; --i) {
+                mDevice.deleteFile(mCleanupPaths.get(i));
+            }
+        }
+    }
+
+    // Class for code that needs an ITestDevice. It is instantiated both in tests and in
+    // (Before|After)ClassWithInfo.
+    private static class DeviceContext implements AutoCloseable {
+        ITestDevice mDevice;
+        CleanupPaths mCleanup;
+        private String mPrimaryArch;
+
+        DeviceContext(ITestDevice device) {
+            mDevice = device;
+            mCleanup = new CleanupPaths(mDevice);
+        }
+
+        public void close() throws DeviceNotAvailableException { mCleanup.cleanup(); }
+
+        void pushSystemOemLibs(ZipFile libApk) throws Exception {
+            pushNativeTestLib(libApk, "/system/${LIB}/libfoo.oem1.so");
+            pushNativeTestLib(libApk, "/system/${LIB}/libbar.oem1.so");
+            pushString("libfoo.oem1.so\n"
+                            + "libbar.oem1.so\n",
+                    "/system/etc/public.libraries-oem1.txt");
+
+            pushNativeTestLib(libApk, "/system/${LIB}/libfoo.oem2.so");
+            pushNativeTestLib(libApk, "/system/${LIB}/libbar.oem2.so");
+            pushString("libfoo.oem2.so\n"
+                            + "libbar.oem2.so\n",
+                    "/system/etc/public.libraries-oem2.txt");
+        }
+
+        void pushProductLibs(ZipFile libApk) throws Exception {
+            pushNativeTestLib(libApk, "/product/${LIB}/libfoo.product1.so");
+            pushNativeTestLib(libApk, "/product/${LIB}/libbar.product1.so");
+            pushString("libfoo.product1.so\n"
+                            + "libbar.product1.so\n",
+                    "/product/etc/public.libraries-product1.txt");
+        }
+
+        void softReboot() throws DeviceNotAvailableException {
+            assertCommandSucceeds("setprop dev.bootcomplete 0");
+            assertCommandSucceeds("stop");
+            assertCommandSucceeds("start");
+            mDevice.waitForDeviceAvailable();
+        }
+
+        String getPrimaryArch() throws DeviceNotAvailableException {
+            if (mPrimaryArch == null) {
+                mPrimaryArch = assertCommandSucceeds("getprop ro.bionic.arch");
+            }
+            return mPrimaryArch;
+        }
+
+        // Pushes the given file contents to the device at the given destination path. destPath is
+        // assumed to have no risk of overlapping with existing files, and is deleted in tearDown(),
+        // along with any directory levels that had to be created.
+        void pushString(String fileContents, String destPath) throws DeviceNotAvailableException {
+            mCleanup.addPath(destPath);
+            assertThat(mDevice.pushString(fileContents, destPath)).isTrue();
+        }
+
+        // Like pushString, but extracts a Java resource and pushes that.
+        void pushResource(String resourceName, String destPath) throws Exception {
+            File hostTempFile = extractResourceToTempFile(resourceName);
+            mCleanup.addPath(destPath);
+            assertThat(mDevice.pushFile(hostTempFile, destPath)).isTrue();
+        }
+
+        // Like pushString, but extracts libnativeloader_testlib.so from the library_container_app
+        // APK and pushes it to destPath. "${LIB}" is replaced with "lib" or "lib64" as appropriate.
+        void pushNativeTestLib(ZipFile libApk, String destPath) throws Exception {
+            String libApkPath = "lib/" + getPrimaryArch() + "/libnativeloader_testlib.so";
+            ZipEntry entry = libApk.getEntry(libApkPath);
+            assertWithMessage("Failed to find " + libApkPath + " in library_container_app.apk")
+                    .that(entry)
+                    .isNotNull();
+
+            File libraryTempFile;
+            try (InputStream inStream = libApk.getInputStream(entry)) {
+                libraryTempFile = writeStreamToTempFile("libnativeloader_testlib.so", inStream);
+            }
+
+            String libDir = getPrimaryArch().contains("64") ? "lib64" : "lib";
+            destPath = destPath.replace("${LIB}", libDir);
+
+            mCleanup.addPath(destPath);
+            assertThat(mDevice.pushFile(libraryTempFile, destPath)).isTrue();
+        }
+
+        String assertCommandSucceeds(String command) throws DeviceNotAvailableException {
+            CommandResult result = mDevice.executeShellV2Command(command);
+            assertWithMessage(result.toString()).that(result.getExitCode()).isEqualTo(0);
+            // Remove trailing \n's.
+            return result.getStdout().trim();
+        }
+    }
+
+    static private ZipFile openLibContainerApk() throws Exception {
+        return new ZipFile(extractResourceToTempFile("/library_container_app.apk"));
+    }
+
+    static private File extractResourceToTempFile(String resourceName) throws Exception {
+        assertThat(resourceName).startsWith("/");
+        try (InputStream inStream = LibnativeloaderTest.class.getResourceAsStream(resourceName)) {
+            assertWithMessage("Failed to extract resource " + resourceName)
+                    .that(inStream)
+                    .isNotNull();
+            return writeStreamToTempFile(resourceName.substring(1), inStream);
+        }
+    }
+
+    static private File writeStreamToTempFile(String tempFileBaseName, InputStream inStream)
+            throws Exception {
+        File hostTempFile = File.createTempFile(tempFileBaseName, null);
+        try (FileOutputStream outStream = new FileOutputStream(hostTempFile)) {
+            ByteStreams.copy(inStream, outStream);
+        }
+        return hostTempFile;
+    }
+}