Merge "Add signing command for testing."
diff --git a/apex/product_packages.mk b/apex/product_packages.mk
index ef99f0f..fef6316 100644
--- a/apex/product_packages.mk
+++ b/apex/product_packages.mk
@@ -19,8 +19,12 @@
 # To include the APEX in your build, insert this in your device.mk:
 #   $(call inherit-product, packages/modules/Virtualization/apex/product_packages.mk)
 
-PRODUCT_PACKAGES += com.android.virt
+PRODUCT_PACKAGES += \
+    com.android.compos \
+    com.android.virt
+
 PRODUCT_ARTIFACT_PATH_REQUIREMENT_ALLOWED_LIST += \
+    system/apex/com.android.compos.apex \
     system/apex/com.android.virt.apex \
     system/bin/crosvm \
     system/lib64/%.dylib.so \
diff --git a/authfs/tests/Android.bp b/authfs/tests/Android.bp
deleted file mode 100644
index 8061c56..0000000
--- a/authfs/tests/Android.bp
+++ /dev/null
@@ -1,21 +0,0 @@
-package {
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-java_test_host {
-    name: "AuthFsHostTest",
-    srcs: ["java/**/*.java"],
-    libs: [
-        "tradefed",
-        "compatibility-tradefed",
-        "compatibility-host-util",
-    ],
-    static_libs: [
-        "VirtualizationTestHelper",
-    ],
-    test_suites: ["general-tests"],
-    data: [
-        ":authfs_test_files",
-        ":MicrodroidTestApp.signed",
-    ],
-}
diff --git a/authfs/tests/AndroidTest.xml b/authfs/tests/AndroidTest.xml
deleted file mode 100644
index 8f940f6..0000000
--- a/authfs/tests/AndroidTest.xml
+++ /dev/null
@@ -1,64 +0,0 @@
-<?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 authfs tests">
-    <!-- Need root to start virtualizationservice -->
-    <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
-
-    <!-- virtualizationservice doesn't have access to shell_data_file. Instead of giving it
-          a test-only permission, run it without selinux -->
-    <target_preparer class="com.android.tradefed.targetprep.DisableSELinuxTargetPreparer"/>
-
-    <!-- Basic checks that the device has all the prerequisites. -->
-    <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
-        <option name="throw-if-cmd-fail" value="true" />
-        <!-- Make sure kernel has FUSE enabled. -->
-        <option name="run-command" value="ls /dev/fuse" />
-        <!-- Make sure necessary executables are installed. -->
-        <option name="run-command" value="ls /apex/com.android.virt/bin/fd_server" />
-        <option name="run-command" value="ls /apex/com.android.virt/bin/authfs" />
-        <!-- Prepare test directory. -->
-        <option name="run-command" value="mkdir -p /data/local/tmp/authfs/mnt" />
-        <option name="teardown-command" value="rm -rf /data/local/tmp/authfs" />
-    </target_preparer>
-
-    <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
-        <option name="cleanup" value="true" />
-        <option name="abort-on-push-failure" value="true" />
-        <option name="push-file" key="cert.der" value="/data/local/tmp/authfs/cert.der" />
-        <option name="push-file" key="input.4m" value="/data/local/tmp/authfs/input.4m" />
-        <option name="push-file" key="input.4k1" value="/data/local/tmp/authfs/input.4k1" />
-        <option name="push-file" key="input.4k" value="/data/local/tmp/authfs/input.4k" />
-        <option name="push-file" key="input.4m.fsv_sig"
-            value="/data/local/tmp/authfs/input.4m.fsv_sig" />
-        <option name="push-file" key="input.4k1.fsv_sig"
-            value="/data/local/tmp/authfs/input.4k1.fsv_sig" />
-        <option name="push-file" key="input.4k.fsv_sig"
-            value="/data/local/tmp/authfs/input.4k.fsv_sig" />
-        <option name="push-file" key="input.4m.merkle_dump"
-            value="/data/local/tmp/authfs/input.4m.merkle_dump" />
-        <option name="push-file" key="input.4m.merkle_dump.bad"
-            value="/data/local/tmp/authfs/input.4m.merkle_dump.bad" />
-        <option name="push-file" key="input.4k1.merkle_dump"
-            value="/data/local/tmp/authfs/input.4k1.merkle_dump" />
-        <option name="push-file" key="input.4k.merkle_dump"
-            value="/data/local/tmp/authfs/input.4k.merkle_dump" />
-    </target_preparer>
-
-    <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
-        <option name="jar" value="AuthFsHostTest.jar" />
-    </test>
-</configuration>
diff --git a/demo/README.md b/demo/README.md
new file mode 100644
index 0000000..8d0550a
--- /dev/null
+++ b/demo/README.md
@@ -0,0 +1,28 @@
+# Microdroid demo app
+
+## Building
+
+```
+TARGET_BUILD_APPS=MicrodroidDemoApp m apps_only dist
+```
+
+## Installing
+
+```
+adb install out/dist/MicrodroidDemoApp.apk
+adb push out/dist/MicrodroidDemoApp.apk.idsig /data/local/tmp/virt
+```
+
+## Running
+
+Run these commands before running the app. Eventually, these won't be needed.
+
+```
+adb root
+adb setenforce 0
+adb start virtualizationservice
+```
+
+Run the app by touching the icon on the launcher. Press the `run` button to
+start a VM. You can see console output from the VM on the screen. You can stop
+the VM by pressing the `stop` button.
diff --git a/demo/java/com/android/microdroid/demo/MainActivity.java b/demo/java/com/android/microdroid/demo/MainActivity.java
index 6373b55..baf0242 100644
--- a/demo/java/com/android/microdroid/demo/MainActivity.java
+++ b/demo/java/com/android/microdroid/demo/MainActivity.java
@@ -119,7 +119,7 @@
                                 .debugMode(debug);
                 VirtualMachineConfig config = builder.build();
                 VirtualMachineManager vmm = VirtualMachineManager.getInstance(getApplication());
-                mVirtualMachine = vmm.create("demo_vm", config);
+                mVirtualMachine = vmm.getOrCreate("demo_vm", config);
                 mVirtualMachine.run();
                 mStatus.postValue(mVirtualMachine.getStatus());
             } catch (VirtualMachineException e) {
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachine.java b/javalib/src/android/system/virtualmachine/VirtualMachine.java
index 8089d85..53d6864 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachine.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachine.java
@@ -25,9 +25,11 @@
 
 import java.io.File;
 import java.io.FileInputStream;
+import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.nio.file.FileAlreadyExistsException;
 import java.nio.file.Files;
 import java.util.Optional;
 
@@ -105,16 +107,22 @@
     /* package */ static VirtualMachine create(
             Context context, String name, VirtualMachineConfig config)
             throws VirtualMachineException {
-        // TODO(jiyong): trigger an error if the VM having 'name' already exists.
         VirtualMachine vm = new VirtualMachine(context, name, config);
 
         try {
-            final File vmRoot = vm.mConfigFilePath.getParentFile();
-            Files.createDirectories(vmRoot.toPath());
+            final File thisVmDir = vm.mConfigFilePath.getParentFile();
+            Files.createDirectories(thisVmDir.getParentFile().toPath());
 
-            FileOutputStream output = new FileOutputStream(vm.mConfigFilePath);
-            vm.mConfig.serialize(output);
-            output.close();
+            // The checking of the existence of this directory and the creation of it is done
+            // atomically. If the directory already exists (i.e. the VM with the same name was
+            // already created), FileAlreadyExistsException is thrown
+            Files.createDirectory(thisVmDir.toPath());
+
+            try (FileOutputStream output = new FileOutputStream(vm.mConfigFilePath)) {
+                vm.mConfig.serialize(output);
+            }
+        } catch (FileAlreadyExistsException e) {
+            throw new VirtualMachineException("virtual machine already exists", e);
         } catch (IOException e) {
             throw new VirtualMachineException(e);
         }
@@ -126,7 +134,6 @@
     /** Loads a virtual machine that is already created before. */
     /* package */ static VirtualMachine load(Context context, String name)
             throws VirtualMachineException {
-        // TODO(jiyong): return null if the VM having the 'name' doesn't exist.
         VirtualMachine vm = new VirtualMachine(context, name, /* config */ null);
 
         try {
@@ -134,6 +141,9 @@
             VirtualMachineConfig config = VirtualMachineConfig.from(input);
             input.close();
             vm.mConfig = config;
+        } catch (FileNotFoundException e) {
+            // The VM doesn't exist.
+            return null;
         } catch (IOException e) {
             throw new VirtualMachineException(e);
         }
@@ -265,8 +275,21 @@
      */
     public VirtualMachineConfig setConfig(VirtualMachineConfig newConfig)
             throws VirtualMachineException {
-        // TODO(jiyong): implement this
-        throw new VirtualMachineException("Not implemented");
+        final VirtualMachineConfig oldConfig = getConfig();
+        if (!oldConfig.isCompatibleWith(newConfig)) {
+            throw new VirtualMachineException("incompatible config");
+        }
+
+        try {
+            FileOutputStream output = new FileOutputStream(mConfigFilePath);
+            newConfig.serialize(output);
+            output.close();
+        } catch (IOException e) {
+            throw new VirtualMachineException(e);
+        }
+        mConfig = newConfig;
+
+        return oldConfig;
     }
 
     @Override
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
index b5f04a2..f0e1ce6 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -19,6 +19,8 @@
 import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
 
 import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.Signature; // This actually is certificate!
 import android.os.ParcelFileDescriptor;
 import android.os.PersistableBundle;
 import android.system.virtualizationservice.VirtualMachineAppConfig;
@@ -28,6 +30,9 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
 
 /**
  * Represents a configuration of a virtual machine. A configuration consists of hardware
@@ -40,6 +45,7 @@
     // These defines the schema of the config file persisted on disk.
     private static final int VERSION = 1;
     private static final String KEY_VERSION = "version";
+    private static final String KEY_CERTS = "certs";
     private static final String KEY_APKPATH = "apkPath";
     private static final String KEY_IDSIGPATH = "idsigPath";
     private static final String KEY_PAYLOADCONFIGPATH = "payloadConfigPath";
@@ -47,6 +53,7 @@
 
     // Paths to the APK and its idsig file of this application.
     private final String mApkPath;
+    private final Signature[] mCerts;
     private final String mIdsigPath;
     private final boolean mDebugMode;
 
@@ -58,8 +65,13 @@
     // TODO(jiyong): add more items like # of cpu, size of ram, debuggability, etc.
 
     private VirtualMachineConfig(
-            String apkPath, String idsigPath, String payloadConfigPath, boolean debugMode) {
+            String apkPath,
+            Signature[] certs,
+            String idsigPath,
+            String payloadConfigPath,
+            boolean debugMode) {
         mApkPath = apkPath;
+        mCerts = certs;
         mIdsigPath = idsigPath;
         mPayloadConfigPath = payloadConfigPath;
         mDebugMode = debugMode;
@@ -77,6 +89,15 @@
         if (apkPath == null) {
             throw new VirtualMachineException("No apkPath");
         }
+        final String[] certStrings = b.getStringArray(KEY_CERTS);
+        if (certStrings == null || certStrings.length == 0) {
+            throw new VirtualMachineException("No certs");
+        }
+        List<Signature> certList = new ArrayList<>();
+        for (String s : certStrings) {
+            certList.add(new Signature(s));
+        }
+        Signature[] certs = certList.toArray(new Signature[0]);
         final String idsigPath = b.getString(KEY_IDSIGPATH);
         if (idsigPath == null) {
             throw new VirtualMachineException("No idsigPath");
@@ -86,7 +107,7 @@
             throw new VirtualMachineException("No payloadConfigPath");
         }
         final boolean debugMode = b.getBoolean(KEY_DEBUGMODE);
-        return new VirtualMachineConfig(apkPath, idsigPath, payloadConfigPath, debugMode);
+        return new VirtualMachineConfig(apkPath, certs, idsigPath, payloadConfigPath, debugMode);
     }
 
     /** Persists this config to a stream, for example a file. */
@@ -94,6 +115,12 @@
         PersistableBundle b = new PersistableBundle();
         b.putInt(KEY_VERSION, VERSION);
         b.putString(KEY_APKPATH, mApkPath);
+        List<String> certList = new ArrayList<>();
+        for (Signature cert : mCerts) {
+            certList.add(cert.toCharsString());
+        }
+        String[] certs = certList.toArray(new String[0]);
+        b.putStringArray(KEY_CERTS, certs);
         b.putString(KEY_IDSIGPATH, mIdsigPath);
         b.putString(KEY_PAYLOADCONFIGPATH, mPayloadConfigPath);
         b.putBoolean(KEY_DEBUGMODE, mDebugMode);
@@ -106,6 +133,23 @@
     }
 
     /**
+     * Tests if this config is compatible with other config. Being compatible means that the configs
+     * can be interchangeably used for the same virtual machine. Compatible changes includes the
+     * number of CPUs and the size of the RAM, and change of the payload as long as the payload is
+     * signed by the same signer. All other changes (e.g. using a payload from a different signer,
+     * change of the debug mode, etc.) are considered as incompatible.
+     */
+    public boolean isCompatibleWith(VirtualMachineConfig other) {
+        if (!Arrays.equals(this.mCerts, other.mCerts)) {
+            return false;
+        }
+        if (this.mDebugMode != other.mDebugMode) {
+            return false;
+        }
+        return true;
+    }
+
+    /**
      * Converts this config object into a parcel. Used when creating a VM via the virtualization
      * service. Notice that the files are not passed as paths, but as file descriptors because the
      * service doesn't accept paths as it might not have permission to open app-owned files and that
@@ -152,7 +196,22 @@
         /** Builds an immutable {@link VirtualMachineConfig} */
         public VirtualMachineConfig build() {
             final String apkPath = mContext.getPackageCodePath();
-            return new VirtualMachineConfig(apkPath, mIdsigPath, mPayloadConfigPath, mDebugMode);
+            final String packageName = mContext.getPackageName();
+            Signature[] certs;
+            try {
+                certs =
+                        mContext.getPackageManager()
+                                .getPackageInfo(
+                                        packageName, PackageManager.GET_SIGNING_CERTIFICATES)
+                                .signingInfo
+                                .getSigningCertificateHistory();
+            } catch (PackageManager.NameNotFoundException e) {
+                // This cannot happen as `packageName` is from this app.
+                throw new RuntimeException(e);
+            }
+
+            return new VirtualMachineConfig(
+                    apkPath, certs, mIdsigPath, mPayloadConfigPath, mDebugMode);
         }
     }
 }
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineManager.java b/javalib/src/android/system/virtualmachine/VirtualMachineManager.java
index dfa4f0b..317caee 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineManager.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineManager.java
@@ -74,7 +74,12 @@
         return VirtualMachine.load(mContext, name);
     }
 
-    /** Returns an existing {@link VirtualMachine} if it exists, or create a new one. */
+    /**
+     * Returns an existing {@link VirtualMachine} if it exists, or create a new one. If the virtual
+     * machine exists, and config is not null, the virtual machine is re-configured with the new
+     * config. However, if the config is not compatible with the original config of the virtual
+     * machine, exception is thrown.
+     */
     public VirtualMachine getOrCreate(String name, VirtualMachineConfig config)
             throws VirtualMachineException {
         VirtualMachine vm;
@@ -85,10 +90,11 @@
             }
         }
 
-        if (vm.getConfig().equals(config)) {
-            return vm;
-        } else {
-            throw new VirtualMachineException("Incompatible config");
+        if (config != null) {
+            // Can throw VirtualMachineException is the new config is not compatible with the
+            // old config.
+            vm.setConfig(config);
         }
+        return vm;
     }
 }
diff --git a/microdroid/init.rc b/microdroid/init.rc
index b683230..36cddbb 100644
--- a/microdroid/init.rc
+++ b/microdroid/init.rc
@@ -29,7 +29,6 @@
     exec - root system -- /system/bin/apexd --vm
 
     perform_apex_config
-    exec_start derive_sdk
 
     exec - root system -- /system/bin/apkdmverity /dev/block/by-name/microdroid-apk /dev/block/by-name/microdroid-apk-idsig microdroid-apk
     mkdir /mnt/apk 0755 system system
diff --git a/microdroid/sepolicy/system/private/derive_classpath.te b/microdroid/sepolicy/system/private/derive_classpath.te
deleted file mode 100644
index 2299ba0..0000000
--- a/microdroid/sepolicy/system/private/derive_classpath.te
+++ /dev/null
@@ -1,25 +0,0 @@
-
-# Domain for derive_classpath
-type derive_classpath, domain, coredomain;
-type derive_classpath_exec, system_file_type, exec_type, file_type;
-init_daemon_domain(derive_classpath)
-
-# Read /apex
-allow derive_classpath apex_mnt_dir:dir r_dir_perms;
-
-# Create /data/system/environ/classpath file
-allow derive_classpath environ_system_data_file:dir rw_dir_perms;
-allow derive_classpath environ_system_data_file:file create_file_perms;
-
-# b/183079517 fails on gphone targets otherwise
-allow derive_classpath unlabeled:dir search;
-
-# Allow derive_classpath to write the classpath into ota dexopt
-# - Read the ota's apex dir
-allow derive_classpath postinstall_apex_mnt_dir:dir r_dir_perms;
-# - Report the BCP to the ota's dexopt
-allow derive_classpath postinstall_dexopt:dir search;
-allow derive_classpath postinstall_dexopt:fd use;
-allow derive_classpath postinstall_dexopt:file read;
-allow derive_classpath postinstall_dexopt:lnk_file read;
-allow derive_classpath postinstall_dexopt_tmpfs:file rw_file_perms;
diff --git a/microdroid/sepolicy/system/private/derive_sdk.te b/microdroid/sepolicy/system/private/derive_sdk.te
deleted file mode 100644
index 1f60e34..0000000
--- a/microdroid/sepolicy/system/private/derive_sdk.te
+++ /dev/null
@@ -1,12 +0,0 @@
-
-# Domain for derive_sdk
-type derive_sdk, domain, coredomain;
-type derive_sdk_exec, system_file_type, exec_type, file_type;
-init_daemon_domain(derive_sdk)
-
-# Read /apex
-allow derive_sdk apex_mnt_dir:dir r_dir_perms;
-
-# Prop rules: writable by derive_sdk, readable by bootclasspath (apps)
-set_prop(derive_sdk, module_sdkextensions_prop)
-neverallow { domain -init -derive_sdk } module_sdkextensions_prop:property_service set;
diff --git a/microdroid/sepolicy/system/private/postinstall_dexopt.te b/microdroid/sepolicy/system/private/postinstall_dexopt.te
index 94af043..14e7854 100644
--- a/microdroid/sepolicy/system/private/postinstall_dexopt.te
+++ b/microdroid/sepolicy/system/private/postinstall_dexopt.te
@@ -16,10 +16,6 @@
 #   with the `postinstall_file` type by update_engine.
 domain_auto_trans(postinstall_dexopt, postinstall_file, dex2oat)
 
-# Run derive_classpath to get the current BCP.
-domain_auto_trans(postinstall_dexopt, derive_classpath_exec, derive_classpath)
-# Allow postinstall_dexopt to make a tempfile for derive_classpath to write into
-tmpfs_domain(postinstall_dexopt);
 allow postinstall_dexopt postinstall_dexopt_tmpfs:file open;
 
 allow postinstall_dexopt self:global_capability_class_set { chown dac_override dac_read_search fowner fsetid setgid setuid };
diff --git a/microdroid/sepolicy/system/private/property_contexts b/microdroid/sepolicy/system/private/property_contexts
index c2a3a62..deeb840 100644
--- a/microdroid/sepolicy/system/private/property_contexts
+++ b/microdroid/sepolicy/system/private/property_contexts
@@ -32,7 +32,6 @@
 ro.logd.kernel u:object_r:logd_prop:s0 exact bool
 
 ro.boottime.adbd                      u:object_r:boottime_prop:s0 exact int
-ro.boottime.derive_sdk                u:object_r:boottime_prop:s0 exact int
 ro.boottime.hwservicemanager          u:object_r:boottime_prop:s0 exact int
 ro.boottime.init                      u:object_r:boottime_prop:s0 exact int
 ro.boottime.init.cold_boot_wait       u:object_r:boottime_prop:s0 exact int
@@ -59,7 +58,6 @@
 
 sys.usb.controller u:object_r:usb_control_prop:s0 exact string
 
-init.svc.derive_sdk                u:object_r:init_service_status_private_prop:s0 exact string
 init.svc.hwservicemanager          u:object_r:init_service_status_private_prop:s0 exact string
 init.svc.keystore2                 u:object_r:init_service_status_private_prop:s0 exact string
 init.svc.logd                      u:object_r:init_service_status_private_prop:s0 exact string
diff --git a/tests/AndroidTest.xml b/tests/AndroidTest.xml
index b56c0e8..5e7faf9 100644
--- a/tests/AndroidTest.xml
+++ b/tests/AndroidTest.xml
@@ -15,10 +15,6 @@
 -->
 
 <configuration description="Config for Virtualization tests">
-    <!-- virtualizationservice doesn't have access to shell_data_file. Instead of giving it
-      a test-only permission, run it without selinux -->
-    <target_preparer class="com.android.tradefed.targetprep.DisableSELinuxTargetPreparer"/>
-
     <!-- Push test binaries to the device. -->
     <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
         <option name="cleanup" value="true" />
@@ -28,6 +24,10 @@
         <option name="push-file" key="virt_test_initramfs.img" value="/data/local/tmp/virt-test/initramfs" />
     </target_preparer>
 
+    <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer">
+        <option name="force-root" value="true" />
+    </target_preparer>
+
     <test class="com.android.tradefed.testtype.GTest" >
         <option name="native-test-device-path" value="/data/local/tmp/virt-test" />
         <option name="module-name" value="VirtualizationTestCases" />
diff --git a/tests/hostside/Android.bp b/tests/hostside/Android.bp
index 968c991..4d70c70 100644
--- a/tests/hostside/Android.bp
+++ b/tests/hostside/Android.bp
@@ -8,9 +8,14 @@
     test_suites: ["device-tests"],
     libs: [
         "tradefed",
+        "compatibility-tradefed",
+        "compatibility-host-util",
     ],
     static_libs: [
         "VirtualizationTestHelper",
     ],
-    data: [":MicrodroidTestApp.signed"],
+    data: [
+        ":authfs_test_files",
+        ":MicrodroidTestApp.signed",
+    ],
 }
diff --git a/tests/hostside/AndroidTest.xml b/tests/hostside/AndroidTest.xml
index eda733a..889fc4e 100644
--- a/tests/hostside/AndroidTest.xml
+++ b/tests/hostside/AndroidTest.xml
@@ -22,6 +22,38 @@
       a test-only permission, run it without selinux -->
     <target_preparer class="com.android.tradefed.targetprep.DisableSELinuxTargetPreparer"/>
 
+    <!-- Prepare test directory for AuthFsTestCase. -->
+    <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+        <option name="throw-if-cmd-fail" value="true" />
+        <!-- Prepare test directory. -->
+        <option name="run-command" value="mkdir -p /data/local/tmp/authfs/mnt" />
+        <option name="teardown-command" value="rm -rf /data/local/tmp/authfs" />
+    </target_preparer>
+
+    <!-- Prepare files for AuthFsTestCase. -->
+    <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
+        <option name="cleanup" value="true" />
+        <option name="abort-on-push-failure" value="true" />
+        <option name="push-file" key="cert.der" value="/data/local/tmp/authfs/cert.der" />
+        <option name="push-file" key="input.4m" value="/data/local/tmp/authfs/input.4m" />
+        <option name="push-file" key="input.4k1" value="/data/local/tmp/authfs/input.4k1" />
+        <option name="push-file" key="input.4k" value="/data/local/tmp/authfs/input.4k" />
+        <option name="push-file" key="input.4m.fsv_sig"
+            value="/data/local/tmp/authfs/input.4m.fsv_sig" />
+        <option name="push-file" key="input.4k1.fsv_sig"
+            value="/data/local/tmp/authfs/input.4k1.fsv_sig" />
+        <option name="push-file" key="input.4k.fsv_sig"
+            value="/data/local/tmp/authfs/input.4k.fsv_sig" />
+        <option name="push-file" key="input.4m.merkle_dump"
+            value="/data/local/tmp/authfs/input.4m.merkle_dump" />
+        <option name="push-file" key="input.4m.merkle_dump.bad"
+            value="/data/local/tmp/authfs/input.4m.merkle_dump.bad" />
+        <option name="push-file" key="input.4k1.merkle_dump"
+            value="/data/local/tmp/authfs/input.4k1.merkle_dump" />
+        <option name="push-file" key="input.4k.merkle_dump"
+            value="/data/local/tmp/authfs/input.4k.merkle_dump" />
+    </target_preparer>
+
     <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
         <option name="jar" value="MicrodroidHostTestCases.jar" />
     </test>
diff --git a/tests/hostside/helper/java/android/virt/test/CommandRunner.java b/tests/hostside/helper/java/android/virt/test/CommandRunner.java
new file mode 100644
index 0000000..696c89a
--- /dev/null
+++ b/tests/hostside/helper/java/android/virt/test/CommandRunner.java
@@ -0,0 +1,89 @@
+/*
+ * 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.
+ */
+
+package android.virt.test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeThat;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+
+import java.util.Arrays;
+
+import javax.annotation.Nonnull;
+
+/** A helper class to provide easy way to run commands on a test device. */
+public class CommandRunner {
+
+    /** Default timeout. 30 sec because Microdroid is extremely slow on GCE-on-CF. */
+    private static final long DEFAULT_TIMEOUT = 30000;
+
+    private ITestDevice mDevice;
+
+    public CommandRunner(@Nonnull ITestDevice device) {
+        mDevice = device;
+    }
+
+    public ITestDevice getDevice() {
+        return mDevice;
+    }
+
+    public String run(String... cmd) throws DeviceNotAvailableException {
+        CommandResult result = runForResult(cmd);
+        if (result.getStatus() != CommandStatus.SUCCESS) {
+            fail(join(cmd) + " has failed: " + result);
+        }
+        return result.getStdout().trim();
+    }
+
+    public String tryRun(String... cmd) throws DeviceNotAvailableException {
+        CommandResult result = runForResult(cmd);
+        if (result.getStatus() == CommandStatus.SUCCESS) {
+            return result.getStdout().trim();
+        } else {
+            CLog.d(join(cmd) + " has failed (but ok): " + result);
+            return null;
+        }
+    }
+
+    public String runWithTimeout(long timeoutMillis, String... cmd)
+            throws DeviceNotAvailableException {
+        CommandResult result =
+                mDevice.executeShellV2Command(
+                        join(cmd), timeoutMillis, java.util.concurrent.TimeUnit.MILLISECONDS);
+        if (result.getStatus() != CommandStatus.SUCCESS) {
+            fail(join(cmd) + " has failed: " + result);
+        }
+        return result.getStdout().trim();
+    }
+
+    public CommandResult runForResult(String... cmd) throws DeviceNotAvailableException {
+        return mDevice.executeShellV2Command(join(cmd));
+    }
+
+    public void assumeSuccess(String... cmd) throws DeviceNotAvailableException {
+        assumeThat(runForResult(cmd).getStatus(), is(CommandStatus.SUCCESS));
+    }
+
+    private static String join(String... strs) {
+        return String.join(" ", Arrays.asList(strs));
+    }
+}
diff --git a/tests/hostside/helper/java/android/virt/test/VirtualizationTestCaseBase.java b/tests/hostside/helper/java/android/virt/test/VirtualizationTestCaseBase.java
index 6d43760..7a17619 100644
--- a/tests/hostside/helper/java/android/virt/test/VirtualizationTestCaseBase.java
+++ b/tests/hostside/helper/java/android/virt/test/VirtualizationTestCaseBase.java
@@ -20,10 +20,11 @@
 import static org.junit.Assert.assertThat;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
-import static org.junit.Assume.assumeThat;
 
 import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
+import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
 import com.android.tradefed.util.CommandResult;
@@ -50,92 +51,63 @@
 
     private static final long MICRODROID_ADB_CONNECT_TIMEOUT_MINUTES = 5;
 
-    public void prepareVirtualizationTestSetup() throws DeviceNotAvailableException {
-        // kill stale crosvm processes
-        tryRunOnAndroid("killall", "crosvm");
+    public static void prepareVirtualizationTestSetup(ITestDevice androidDevice)
+            throws DeviceNotAvailableException {
+        CommandRunner android = new CommandRunner(androidDevice);
 
-        // Prepare the test root
-        tryRunOnAndroid("rm", "-rf", TEST_ROOT);
-        tryRunOnAndroid("mkdir", "-p", TEST_ROOT);
+        // kill stale crosvm processes
+        android.tryRun("killall", "crosvm");
 
         // disconnect from microdroid
         tryRunOnHost("adb", "disconnect", MICRODROID_SERIAL);
     }
 
-    public void cleanUpVirtualizationTestSetup() throws DeviceNotAvailableException {
+    public static void cleanUpVirtualizationTestSetup(ITestDevice androidDevice)
+            throws DeviceNotAvailableException {
+        CommandRunner android = new CommandRunner(androidDevice);
+
         // disconnect from microdroid
         tryRunOnHost("adb", "disconnect", MICRODROID_SERIAL);
 
         // kill stale VMs and directories
-        tryRunOnAndroid("killall", "crosvm");
-        tryRunOnAndroid("rm", "-rf", "/data/misc/virtualizationservice/*");
-        tryRunOnAndroid("stop", "virtualizationservice");
+        android.tryRun("killall", "crosvm");
+        android.tryRun("rm", "-rf", "/data/misc/virtualizationservice/*");
+        android.tryRun("stop", "virtualizationservice");
     }
 
-    public void testIfDeviceIsCapable() throws DeviceNotAvailableException {
+    public static void testIfDeviceIsCapable(ITestDevice androidDevice)
+            throws DeviceNotAvailableException {
+        CommandRunner android = new CommandRunner(androidDevice);
+
         // Checks the preconditions to run microdroid. If the condition is not satisfied
         // don't run the test (instead of failing)
-        skipIfFail("ls /dev/kvm");
-        skipIfFail("ls /dev/vhost-vsock");
-        skipIfFail("ls /apex/com.android.virt/bin/crosvm");
+        android.assumeSuccess("ls /dev/kvm");
+        android.assumeSuccess("ls /dev/vhost-vsock");
+        android.assumeSuccess("ls /apex/com.android.virt/bin/crosvm");
     }
 
     // Run an arbitrary command in the host side and returns the result
-    private String runOnHost(String... cmd) {
+    private static String runOnHost(String... cmd) {
         return runOnHostWithTimeout(10000, cmd);
     }
 
     // Same as runOnHost, but failure is not an error
-    private String tryRunOnHost(String... cmd) {
+    private static String tryRunOnHost(String... cmd) {
         final long timeout = 10000;
         CommandResult result = RunUtil.getDefault().runTimedCmd(timeout, cmd);
         return result.getStdout().trim();
     }
 
     // Same as runOnHost, but with custom timeout
-    private String runOnHostWithTimeout(long timeoutMillis, String... cmd) {
+    private static String runOnHostWithTimeout(long timeoutMillis, String... cmd) {
         assertTrue(timeoutMillis >= 0);
         CommandResult result = RunUtil.getDefault().runTimedCmd(timeoutMillis, cmd);
         assertThat(result.getStatus(), is(CommandStatus.SUCCESS));
         return result.getStdout().trim();
     }
 
-    // Run a shell command on Android. the default timeout is 2 min by tradefed
-    public String runOnAndroid(String... cmd) throws DeviceNotAvailableException {
-        CommandResult result = getDevice().executeShellV2Command(join(cmd));
-        if (result.getStatus() != CommandStatus.SUCCESS) {
-            fail(join(cmd) + " has failed: " + result);
-        }
-        return result.getStdout().trim();
-    }
-
-    // Same as runOnAndroid, but returns null on error.
-    public String tryRunOnAndroid(String... cmd) throws DeviceNotAvailableException {
-        CommandResult result = getDevice().executeShellV2Command(join(cmd));
-        if (result.getStatus() == CommandStatus.SUCCESS) {
-            return result.getStdout().trim();
-        } else {
-            CLog.d(join(cmd) + " has failed (but ok): " + result);
-            return null;
-        }
-    }
-
-    private String runOnAndroidWithTimeout(long timeoutMillis, String... cmd)
-            throws DeviceNotAvailableException {
-        CommandResult result =
-                getDevice()
-                        .executeShellV2Command(
-                                join(cmd),
-                                timeoutMillis,
-                                java.util.concurrent.TimeUnit.MILLISECONDS);
-        if (result.getStatus() != CommandStatus.SUCCESS) {
-            fail(join(cmd) + " has failed: " + result);
-        }
-        return result.getStdout().trim();
-    }
-
     // Run a shell command on Microdroid
-    public String runOnMicrodroid(String... cmd) {
+    public static String runOnMicrodroid(String... cmd) {
         CommandResult result = runOnMicrodroidForResult(cmd);
         if (result.getStatus() != CommandStatus.SUCCESS) {
             fail(join(cmd) + " has failed: " + result);
@@ -144,7 +116,7 @@
     }
 
     // Same as runOnMicrodroid, but returns null on error.
-    public String tryRunOnMicrodroid(String... cmd) {
+    public static String tryRunOnMicrodroid(String... cmd) {
         CommandResult result = runOnMicrodroidForResult(cmd);
         if (result.getStatus() == CommandStatus.SUCCESS) {
             return result.getStdout().trim();
@@ -154,51 +126,62 @@
         }
     }
 
-    public CommandResult runOnMicrodroidForResult(String... cmd) {
+    public static CommandResult runOnMicrodroidForResult(String... cmd) {
         final long timeout = 30000; // 30 sec. Microdroid is extremely slow on GCE-on-CF.
         return RunUtil.getDefault()
                 .runTimedCmd(timeout, "adb", "-s", MICRODROID_SERIAL, "shell", join(cmd));
     }
 
-    private String join(String... strs) {
+    private static String join(String... strs) {
         return String.join(" ", Arrays.asList(strs));
     }
 
     public File findTestFile(String name) {
+        return findTestFile(getBuild(), name);
+    }
+
+    private static File findTestFile(IBuildInfo buildInfo, String name) {
         try {
-            return (new CompatibilityBuildHelper(getBuild())).getTestFile(name);
+            return (new CompatibilityBuildHelper(buildInfo)).getTestFile(name);
         } catch (FileNotFoundException e) {
             fail("Missing test file: " + name);
             return null;
         }
     }
 
-    public String startMicrodroid(
-            String apkName, String packageName, String configPath, boolean debug)
+    public static String startMicrodroid(
+            ITestDevice androidDevice,
+            IBuildInfo buildInfo,
+            String apkName,
+            String packageName,
+            String configPath,
+            boolean debug)
             throws DeviceNotAvailableException {
+        CommandRunner android = new CommandRunner(androidDevice);
+
         // Install APK
-        File apkFile = findTestFile(apkName);
-        getDevice().installPackage(apkFile, /* reinstall */ true);
+        File apkFile = findTestFile(buildInfo, apkName);
+        androidDevice.installPackage(apkFile, /* reinstall */ true);
 
         // Get the path to the installed apk. Note that
         // getDevice().getAppPackageInfo(...).getCodePath() doesn't work due to the incorrect
         // parsing of the "=" character. (b/190975227). So we use the `pm path` command directly.
-        String apkPath = runOnAndroid("pm", "path", packageName);
+        String apkPath = android.run("pm", "path", packageName);
         assertTrue(apkPath.startsWith("package:"));
         apkPath = apkPath.substring("package:".length());
 
         // Push the idsig file to the device
-        File idsigOnHost = findTestFile(apkName + ".idsig");
+        File idsigOnHost = findTestFile(buildInfo, apkName + ".idsig");
         final String apkIdsigPath = TEST_ROOT + apkName + ".idsig";
-        getDevice().pushFile(idsigOnHost, apkIdsigPath);
+        androidDevice.pushFile(idsigOnHost, apkIdsigPath);
 
         final String logPath = TEST_ROOT + "log.txt";
         final String debugFlag = debug ? "--debug " : "";
 
         // Run the VM
-        runOnAndroid("start", "virtualizationservice");
+        android.run("start", "virtualizationservice");
         String ret =
-                runOnAndroid(
+                android.run(
                         VIRT_APEX + "bin/vm",
                         "run-app",
                         "--daemonize",
@@ -214,7 +197,7 @@
                 () -> {
                     try {
                         // Keep redirecting sufficiently long enough
-                        runOnAndroidWithTimeout(
+                        android.runWithTimeout(
                                 MICRODROID_BOOT_TIMEOUT_MINUTES * 60 * 1000,
                                 "logwrapper",
                                 "tail",
@@ -233,21 +216,20 @@
         return matcher.group(1);
     }
 
-    public void shutdownMicrodroid(String cid) throws DeviceNotAvailableException {
-        // Shutdown microdroid
-        runOnAndroid(VIRT_APEX + "bin/vm", "stop", cid);
+    public static void shutdownMicrodroid(ITestDevice androidDevice, String cid)
+            throws DeviceNotAvailableException {
+        CommandRunner android = new CommandRunner(androidDevice);
 
-        // TODO(192660485): Figure out why shutting down the VM disconnects adb on cuttlefish
-        // temporarily. Without this wait, the rest of `runOnAndroid/skipIfFail` fails due to the
-        // connection loss, and results in assumption error exception for the rest of the tests.
-        try {
-            Thread.sleep(1000);
-        } catch (InterruptedException e) {
-            Thread.currentThread().interrupt();
-        }
+        // Close the connection before shutting the VM down. Otherwise, b/192660485.
+        tryRunOnHost("adb", "disconnect", MICRODROID_SERIAL);
+        final String serial = androidDevice.getSerialNumber();
+        tryRunOnHost("adb", "-s", serial, "forward", "--remove", "tcp:" + TEST_VM_ADB_PORT);
+
+        // Shutdown the VM
+        android.run(VIRT_APEX + "bin/vm", "stop", cid);
     }
 
-    public void rootMicrodroid() throws DeviceNotAvailableException {
+    public static void rootMicrodroid() throws DeviceNotAvailableException {
         runOnHost("adb", "-s", MICRODROID_SERIAL, "root");
 
         // TODO(192660959): Figure out the root cause and remove the sleep. For unknown reason,
@@ -255,6 +237,12 @@
         // `adb -s $MICRODROID_SERIAL shell ...` often fails with "adb: device offline".
         try {
             Thread.sleep(1000);
+            runOnHostWithTimeout(
+                    MICRODROID_ADB_CONNECT_TIMEOUT_MINUTES * 60 * 1000,
+                    "adb",
+                    "-s",
+                    MICRODROID_SERIAL,
+                    "wait-for-device");
         } catch (InterruptedException e) {
             Thread.currentThread().interrupt();
         }
@@ -262,12 +250,13 @@
 
     // Establish an adb connection to microdroid by letting Android forward the connection to
     // microdroid. Wait until the connection is established and microdroid is booted.
-    public void adbConnectToMicrodroid(String cid) throws DeviceNotAvailableException {
+    public static void adbConnectToMicrodroid(ITestDevice androidDevice, String cid)
+            throws DeviceNotAvailableException {
         long start = System.currentTimeMillis();
         long timeoutMillis = MICRODROID_ADB_CONNECT_TIMEOUT_MINUTES * 60 * 1000;
         long elapsed = 0;
 
-        final String serial = getDevice().getSerialNumber();
+        final String serial = androidDevice.getSerialNumber();
         final String from = "tcp:" + TEST_VM_ADB_PORT;
         final String to = "vsock:" + cid + ":5555";
         runOnHost("adb", "-s", serial, "forward", from, to);
@@ -301,9 +290,4 @@
         // Check if it actually booted by reading a sysprop.
         assertThat(runOnMicrodroid("getprop", "ro.hardware"), is("microdroid"));
     }
-
-    private void skipIfFail(String command) throws DeviceNotAvailableException {
-        CommandResult result = getDevice().executeShellV2Command(command);
-        assumeThat(result.getStatus(), is(CommandStatus.SUCCESS));
-    }
 }
diff --git a/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java b/tests/hostside/java/android/virt/test/AuthFsTestCase.java
similarity index 79%
rename from authfs/tests/java/src/com/android/fs/AuthFsHostTest.java
rename to tests/hostside/java/android/virt/test/AuthFsTestCase.java
index 426b333..ae29a09 100644
--- a/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java
+++ b/tests/hostside/java/android/virt/test/AuthFsTestCase.java
@@ -14,22 +14,28 @@
  * limitations under the License.
  */
 
-package com.android.virt.fs;
+package android.virt.test;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeFalse;
 
 import android.platform.test.annotations.RootPermissionTest;
-import android.virt.test.VirtualizationTestCaseBase;
 
 import com.android.compatibility.common.util.PollingCheck;
 import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.invoker.TestInformation;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.junit4.AfterClassWithInfo;
+import com.android.tradefed.testtype.junit4.BeforeClassWithInfo;
 import com.android.tradefed.util.CommandResult;
 
 import org.junit.After;
+import org.junit.AssumptionViolatedException;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -37,10 +43,9 @@
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 
-// TODO move to Virtualization/tests/hostside/
 @RootPermissionTest
 @RunWith(DeviceJUnit4ClassRunner.class)
-public final class AuthFsHostTest extends VirtualizationTestCaseBase {
+public final class AuthFsTestCase extends VirtualizationTestCaseBase {
 
     /** Test directory on Android where data are located */
     private static final String TEST_DIR = "/data/local/tmp/authfs";
@@ -55,50 +60,87 @@
     private static final String AUTHFS_BIN = "/system/bin/authfs";
 
     /** Plenty of time for authfs to get ready */
-    private static final int AUTHFS_INIT_TIMEOUT_MS = 1500;
+    private static final int AUTHFS_INIT_TIMEOUT_MS = 3000;
 
     /** FUSE's magic from statfs(2) */
     private static final String FUSE_SUPER_MAGIC_HEX = "65735546";
 
-    private ExecutorService mThreadPool;
-    private String mCid;
+    private static CommandRunner sAndroid;
+    private static String sCid;
+    private static boolean sAssumptionFailed;
 
-    @Before
-    public void setUp() throws DeviceNotAvailableException {
-        testIfDeviceIsCapable();
+    private ExecutorService mThreadPool = Executors.newCachedThreadPool();
 
-        cleanUpTestFiles();
+    @BeforeClassWithInfo
+    public static void beforeClassWithDevice(TestInformation testInfo)
+            throws DeviceNotAvailableException {
+        assertNotNull(testInfo.getDevice());
+        ITestDevice androidDevice = testInfo.getDevice();
+        sAndroid = new CommandRunner(androidDevice);
 
-        prepareVirtualizationTestSetup();
+        try {
+            testIfDeviceIsCapable(androidDevice);
+        } catch (AssumptionViolatedException e) {
+            // NB: The assumption exception is NOT handled by the test infra when it is thrown from
+            // a class method (see b/37502066). This has not only caused the loss of log, but also
+            // prevented the test cases to be reported at all and thus confused the test infra.
+            //
+            // Since we want to avoid the big overhead to start the VM repeatedly on CF, let's catch
+            // AssumptionViolatedException and emulate it artifitially.
+            CLog.e("Assumption failed: " + e);
+            sAssumptionFailed = true;
+            return;
+        }
 
-        mThreadPool = Executors.newCachedThreadPool();
+        prepareVirtualizationTestSetup(androidDevice);
 
         // For each test case, boot and adb connect to a new Microdroid
+        CLog.i("Starting the shared VM");
         final String apkName = "MicrodroidTestApp.apk";
         final String packageName = "com.android.microdroid.test";
         final String configPath = "assets/vm_config.json"; // path inside the APK
-        mCid = startMicrodroid(apkName, packageName, configPath, /* debug */ false);
-        adbConnectToMicrodroid(mCid);
+        sCid =
+                startMicrodroid(
+                        androidDevice,
+                        testInfo.getBuildInfo(),
+                        apkName,
+                        packageName,
+                        configPath,
+                        /* debug */ false);
+        adbConnectToMicrodroid(androidDevice, sCid);
 
         // Root because authfs (started from shell in this test) currently require root to open
         // /dev/fuse and mount the FUSE.
         rootMicrodroid();
     }
 
-    @After
-    public void tearDown() throws DeviceNotAvailableException {
-        if (mCid != null) {
-            shutdownMicrodroid(mCid);
-            mCid = null;
+    @AfterClassWithInfo
+    public static void afterClassWithDevice(TestInformation testInfo)
+            throws DeviceNotAvailableException {
+        assertNotNull(sAndroid);
+
+        if (sCid != null) {
+            CLog.i("Shutting down shared VM");
+            shutdownMicrodroid(sAndroid.getDevice(), sCid);
+            sCid = null;
         }
 
-        tryRunOnAndroid("killall fd_server");
-        cleanUpTestFiles();
-        cleanUpVirtualizationTestSetup();
+        cleanUpVirtualizationTestSetup(sAndroid.getDevice());
+        sAndroid = null;
     }
 
-    private void cleanUpTestFiles() throws DeviceNotAvailableException {
-        tryRunOnAndroid("rm -f " + TEST_DIR + "/output");
+    @Before
+    public void setUp() {
+        assumeFalse(sAssumptionFailed);
+    }
+
+    @After
+    public void tearDown() throws DeviceNotAvailableException {
+        sAndroid.tryRun("killall fd_server");
+        sAndroid.tryRun("rm -f " + TEST_DIR + "/output");
+
+        tryRunOnMicrodroid("killall authfs");
+        tryRunOnMicrodroid("umount " + MOUNT_DIR);
     }
 
     @Test
@@ -194,7 +236,7 @@
 
         // Action
         // Tampering with the first 2 4K block of the backing file.
-        runOnAndroid("dd if=/dev/zero of=" + backendPath + " bs=1 count=8192");
+        sAndroid.run("dd if=/dev/zero of=" + backendPath + " bs=1 count=8192");
 
         // Verify
         // Write to a block partially requires a read back to calculate the new hash. It should fail
@@ -274,7 +316,7 @@
     }
 
     private String computeFileHashOnAndroid(String path) throws DeviceNotAvailableException {
-        String result = runOnAndroid("sha256sum " + path);
+        String result = sAndroid.run("sha256sum " + path);
         String[] tokens = result.split("\\s");
         if (tokens.length > 0) {
             return tokens[0];
@@ -320,7 +362,7 @@
                 () -> {
                     try {
                         CLog.i("Starting fd_server");
-                        CommandResult result = getDevice().executeShellV2Command(cmd);
+                        CommandResult result = sAndroid.runForResult(cmd);
                         CLog.w("fd_server has stopped: " + result);
                     } catch (DeviceNotAvailableException e) {
                         CLog.e("Error running fd_server", e);
diff --git a/tests/hostside/java/android/virt/test/MicrodroidTestCase.java b/tests/hostside/java/android/virt/test/MicrodroidTestCase.java
index 02fb7e5..d48028e 100644
--- a/tests/hostside/java/android/virt/test/MicrodroidTestCase.java
+++ b/tests/hostside/java/android/virt/test/MicrodroidTestCase.java
@@ -35,8 +35,15 @@
     @Test
     public void testMicrodroidBoots() throws Exception {
         final String configPath = "assets/vm_config.json"; // path inside the APK
-        final String cid = startMicrodroid(APK_NAME, PACKAGE_NAME, configPath, /* debug */ false);
-        adbConnectToMicrodroid(cid);
+        final String cid =
+                startMicrodroid(
+                        getDevice(),
+                        getBuild(),
+                        APK_NAME,
+                        PACKAGE_NAME,
+                        configPath,
+                        /* debug */ false);
+        adbConnectToMicrodroid(getDevice(), cid);
 
         // Test writing to /data partition
         runOnMicrodroid("echo MicrodroidTest > /data/local/tmp/test.txt");
@@ -73,26 +80,27 @@
         // Check that keystore was found by the payload
         assertThat(runOnMicrodroid("getprop", "debug.microdroid.test.keystore"), is("PASS"));
 
-        shutdownMicrodroid(cid);
+        shutdownMicrodroid(getDevice(), cid);
     }
 
     @Test
     public void testDebugMode() throws Exception {
         final String configPath = "assets/vm_config.json"; // path inside the APK
         final boolean debug = true;
-        final String cid = startMicrodroid(APK_NAME, PACKAGE_NAME, configPath, debug);
-        adbConnectToMicrodroid(cid);
+        final String cid =
+                startMicrodroid(getDevice(), getBuild(), APK_NAME, PACKAGE_NAME, configPath, debug);
+        adbConnectToMicrodroid(getDevice(), cid);
 
         assertThat(runOnMicrodroid("getenforce"), is("Permissive"));
 
-        shutdownMicrodroid(cid);
+        shutdownMicrodroid(getDevice(), cid);
     }
 
     @Before
     public void setUp() throws Exception {
-        testIfDeviceIsCapable();
+        testIfDeviceIsCapable(getDevice());
 
-        prepareVirtualizationTestSetup();
+        prepareVirtualizationTestSetup(getDevice());
 
         getDevice().installPackage(findTestFile(APK_NAME), /* reinstall */ false);
 
@@ -102,7 +110,7 @@
 
     @After
     public void shutdown() throws Exception {
-        cleanUpVirtualizationTestSetup();
+        cleanUpVirtualizationTestSetup(getDevice());
 
         getDevice().uninstallPackage(PACKAGE_NAME);
     }
diff --git a/tests/testapk/Android.bp b/tests/testapk/Android.bp
index f14fca8..7332149 100644
--- a/tests/testapk/Android.bp
+++ b/tests/testapk/Android.bp
@@ -2,7 +2,7 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
-android_app {
+android_test_helper_app {
     name: "MicrodroidTestApp",
     srcs: ["src/java/**/*.java"],
     libs: [
diff --git a/virtualizationservice/src/aidl.rs b/virtualizationservice/src/aidl.rs
index bc19109..8bdfa9d 100644
--- a/virtualizationservice/src/aidl.rs
+++ b/virtualizationservice/src/aidl.rs
@@ -59,8 +59,8 @@
 
 /// The list of APEXes which microdroid requires.
 /// TODO(b/192200378) move this to microdroid.json?
-const MICRODROID_REQUIRED_APEXES: [&str; 4] =
-    ["com.android.adbd", "com.android.i18n", "com.android.os.statsd", "com.android.sdkext"];
+const MICRODROID_REQUIRED_APEXES: [&str; 3] =
+    ["com.android.adbd", "com.android.i18n", "com.android.os.statsd"];
 
 /// Implementation of `IVirtualizationService`, the entry point of the AIDL service.
 #[derive(Debug, Default)]