Merge "Improve error reporting and logging."
diff --git a/Android.bp b/Android.bp
new file mode 100644
index 0000000..b655551
--- /dev/null
+++ b/Android.bp
@@ -0,0 +1,29 @@
+//
+// 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 {
+    default_applicable_licenses: ["packages_modules_Virtualization_license"],
+}
+
+// Added automatically by a large-scale-change
+// See: http://go/android-license-faq
+license {
+    name: "packages_modules_Virtualization_license",
+    visibility: [":__subpackages__"],
+    license_kinds: [
+        "legacy_unencumbered",
+    ],
+    // large-scale-change unable to identify any license_text files
+}
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 15099da..13c68d7 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -2,6 +2,9 @@
   "postsubmit": [
     {
       "name": "MicrodroidHostTestCases"
+    },
+    {
+      "name": "VirtualizationTestCases"
     }
   ],
   "imports": [
diff --git a/apex/Android.bp b/apex/Android.bp
index bcc7099..146e4eb 100644
--- a/apex/Android.bp
+++ b/apex/Android.bp
@@ -52,16 +52,10 @@
         "mk_cdisk",
         "mk_payload",
     ],
-    apps: [
-        // TODO(jiyong): remove this when microdroid_payload.json is created by
-        // VirtualizationService.
-        "MicrodroidTestApp",
-    ],
     prebuilts: [
         "com.android.virt.init.rc",
         "microdroid_cdisk.json",
         "microdroid_cdisk_env.json",
-        "microdroid_payload.json",
         "microdroid_uboot_env",
         "microdroid_bootloader",
     ],
diff --git a/compos/Android.bp b/compos/Android.bp
index fc0517f..1611b68 100644
--- a/compos/Android.bp
+++ b/compos/Android.bp
@@ -55,3 +55,10 @@
         "com.android.compos",
     ],
 }
+
+// TODO(b/190503456) Remove this when vm/virtualizationservice generates payload.img from vm_config
+prebuilt_etc {
+    name: "compos_payload_config",
+    src: "payload_config.json",
+    filename: "payload_config.json",
+}
diff --git a/compos/apex/Android.bp b/compos/apex/Android.bp
index 7ced384..3a8f601 100644
--- a/compos/apex/Android.bp
+++ b/compos/apex/Android.bp
@@ -40,4 +40,12 @@
         "compsvc_worker",
         "pvm_exec",
     ],
+
+    apps: [
+        "CompOSPayloadApp",
+    ],
+
+    prebuilts: [
+        "compos_payload_config",
+    ],
 }
diff --git a/compos/apk/Android.bp b/compos/apk/Android.bp
new file mode 100644
index 0000000..c6192b9
--- /dev/null
+++ b/compos/apk/Android.bp
@@ -0,0 +1,9 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_app {
+    name: "CompOSPayloadApp",
+    sdk_version: "current",
+    apex_available: ["com.android.compos"],
+}
diff --git a/compos/apk/AndroidManifest.xml b/compos/apk/AndroidManifest.xml
new file mode 100644
index 0000000..1e9352b
--- /dev/null
+++ b/compos/apk/AndroidManifest.xml
@@ -0,0 +1,18 @@
+<?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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+      package="com.android.compos.payload">
+    <application android:label="CompOS" />
+</manifest>
diff --git a/compos/apk/assets/vm_config.json b/compos/apk/assets/vm_config.json
new file mode 100644
index 0000000..a8dca71
--- /dev/null
+++ b/compos/apk/assets/vm_config.json
@@ -0,0 +1,34 @@
+{
+  "version": 1,
+  "os": {
+    "name": "microdroid"
+  },
+  "task": {
+    "type": "executable",
+    "command": "/apex/com.android.compos/bin/compsvc",
+    "args": [
+      "--rpc-binder",
+      "/apex/com.android.art/bin/dex2oat64"
+    ]
+  },
+  "apexes": [
+    {
+      "name": "com.android.adbd"
+    },
+    {
+      "name": "com.android.art"
+    },
+    {
+      "name": "com.android.compos"
+    },
+    {
+      "name": "com.android.i18n"
+    },
+    {
+      "name": "com.android.os.statsd"
+    },
+    {
+      "name": "com.android.sdkext"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/compos/payload_config.json b/compos/payload_config.json
new file mode 100644
index 0000000..588ccca
--- /dev/null
+++ b/compos/payload_config.json
@@ -0,0 +1,15 @@
+{
+  "apk": {
+    "path": "/apex/com.android.compos/app/CompOSPayloadApp/CompOSPayloadApp.apk",
+    "name": "com.android.compos.payload"
+  },
+  "system_apexes": [
+    "com.android.adbd",
+    "com.android.art",
+    "com.android.compos",
+    "com.android.i18n",
+    "com.android.os.statsd",
+    "com.android.sdkext"
+  ],
+  "payload_config_path": "/mnt/apk/assets/vm_config.json"
+}
\ No newline at end of file
diff --git a/microdroid/Android.bp b/microdroid/Android.bp
index b454ea2..6f731bb 100644
--- a/microdroid/Android.bp
+++ b/microdroid/Android.bp
@@ -398,11 +398,6 @@
 }
 
 prebuilt_etc {
-    name: "microdroid_payload.json",
-    src: "microdroid_payload.json",
-}
-
-prebuilt_etc {
     name: "microdroid_vendor_manifest",
     src: "microdroid_vendor_manifest.xml",
     filename: "manifest.xml",
diff --git a/microdroid/README.md b/microdroid/README.md
index 6b9f4b1..aae6e66 100644
--- a/microdroid/README.md
+++ b/microdroid/README.md
@@ -42,51 +42,33 @@
 
 ## Running
 
-Create a config file, `microdroid.json`:
+Create two config files, `microdroid.json` and `payload.json`:
 
 ```json
+microdroid.json:
+
 {
   "bootloader": "/apex/com.android.virt/etc/microdroid_bootloader",
   "disks": [
     {
       "partitions": [
         {
-          "label": "misc",
-          "path": "/data/local/tmp/microdroid/misc.img"
-        },
-        {
           "label": "boot_a",
           "path": "/apex/com.android.virt/etc/fs/microdroid_boot-5.10.img"
         },
         {
-          "label": "boot_b",
-          "path": "/apex/com.android.virt/etc/fs/microdroid_boot-5.10.img"
-        },
-        {
           "label": "vendor_boot_a",
           "path": "/apex/com.android.virt/etc/fs/microdroid_vendor_boot-5.10.img"
         },
         {
-          "label": "vendor_boot_b",
-          "path": "/apex/com.android.virt/etc/fs/microdroid_vendor_boot-5.10.img"
-        },
-        {
           "label": "vbmeta_a",
           "path": "/apex/com.android.virt/etc/fs/microdroid_vbmeta.img"
         },
         {
-          "label": "vbmeta_b",
-          "path": "/apex/com.android.virt/etc/fs/microdroid_vbmeta.img"
-        },
-        {
           "label": "vbmeta_system_a",
           "path": "/apex/com.android.virt/etc/fs/microdroid_vbmeta_system.img"
         },
         {
-          "label": "vbmeta_system_b",
-          "path": "/apex/com.android.virt/etc/fs/microdroid_vbmeta_system.img"
-        },
-        {
           "label": "super",
           "path": "/apex/com.android.virt/etc/fs/microdroid_super.img"
         }
@@ -108,9 +90,20 @@
     }
   ]
 }
+
+payload.json:
+
+{
+  "system_apexes" : [
+    "com.android.adbd",
+    "com.android.i18n",
+    "com.android.os.statsd",
+    "com.android.sdkext"
+  ]
+}
 ```
 
-Copy the artifacts to the temp directory, create the composite image using
+Copy the these files to the temp directory, create the composite image using
 `mk_cdisk` and copy the VM config file. For now, some other files have to be
 manually created. In the future, you won't need these, and this shall be done
 via [`virtualizationservice`](../virtualizationservice/).
@@ -118,8 +111,8 @@
 ```sh
 $ adb root
 $ adb shell 'mkdir /data/local/tmp/microdroid'
-$ adb shell 'dd if=/dev/zero of=/data/local/tmp/microdroid/misc.img bs=4k count=256'
-$ adb shell 'cd /data/local/tmp/microdroid; /apex/com.android.virt/bin/mk_payload /apex/com.android.virt/etc/microdroid_payload.json payload.img'
+$ adb push payload.json /data/local/tmp/microdroid/payload.json
+$ adb shell 'cd /data/local/tmp/microdroid; /apex/com.android.virt/bin/mk_payload payload.json payload.img'
 $ adb shell 'chmod go+r /data/local/tmp/microdroid/payload*'
 $ adb push microdroid.json /data/local/tmp/microdroid/microdroid.json
 ```
diff --git a/microdroid/microdroid_cdisk.json b/microdroid/microdroid_cdisk.json
index 5721775..d0a5025 100644
--- a/microdroid/microdroid_cdisk.json
+++ b/microdroid/microdroid_cdisk.json
@@ -1,44 +1,24 @@
 {
   "partitions": [
     {
-      "label": "misc",
-      "path": "misc.img"
-    },
-    {
       "label": "boot_a",
-      "path": "microdroid_boot-5.10.img"
-    },
-    {
-      "label": "boot_b",
-      "path": "microdroid_boot-5.10.img"
+      "path": "/apex/com.android.virt/etc/fs/microdroid_boot-5.10.img"
     },
     {
       "label": "vendor_boot_a",
-      "path": "microdroid_vendor_boot-5.10.img"
-    },
-    {
-      "label": "vendor_boot_b",
-      "path": "microdroid_vendor_boot-5.10.img"
+      "path": "/apex/com.android.virt/etc/fs/microdroid_vendor_boot-5.10.img"
     },
     {
       "label": "vbmeta_a",
-      "path": "microdroid_vbmeta.img"
-    },
-    {
-      "label": "vbmeta_b",
-      "path": "microdroid_vbmeta.img"
+      "path": "/apex/com.android.virt/etc/fs/microdroid_vbmeta.img"
     },
     {
       "label": "vbmeta_system_a",
-      "path": "microdroid_vbmeta_system.img"
-    },
-    {
-      "label": "vbmeta_system_b",
-      "path": "microdroid_vbmeta_system.img"
+      "path": "/apex/com.android.virt/etc/fs/microdroid_vbmeta_system.img"
     },
     {
       "label": "super",
-      "path": "microdroid_super.img"
+      "path": "/apex/com.android.virt/etc/fs/microdroid_super.img"
     }
   ]
 }
diff --git a/microdroid/microdroid_cdisk_env.json b/microdroid/microdroid_cdisk_env.json
index b0fbe44..8c84ed5 100644
--- a/microdroid/microdroid_cdisk_env.json
+++ b/microdroid/microdroid_cdisk_env.json
@@ -2,7 +2,7 @@
   "partitions": [
     {
       "label": "uboot_env",
-      "path": "uboot_env.img"
+      "path": "/apex/com.android.virt/etc/uboot_env.img"
     }
   ]
 }
diff --git a/microdroid/microdroid_payload.json b/microdroid/microdroid_payload.json
deleted file mode 100644
index 7af0452..0000000
--- a/microdroid/microdroid_payload.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
-  "system_apexes": [
-    "com.android.adbd",
-    "com.android.i18n",
-    "com.android.os.statsd",
-    "com.android.sdkext"
-  ],
-  "apk": {
-    "name": "com.android.microdroid.test",
-    "path": "/apex/com.android.virt/app/MicrodroidTestApp/MicrodroidTestApp.apk"
-  },
-  "payload_config_path": "/mnt/apk/assets/vm_config.json"
-}
diff --git a/microdroid/sepolicy/Android.bp b/microdroid/sepolicy/Android.bp
index 9bb6408..6488153 100644
--- a/microdroid/sepolicy/Android.bp
+++ b/microdroid/sepolicy/Android.bp
@@ -1,3 +1,12 @@
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "packages_modules_Virtualization_license"
+    // to get the below license kinds:
+    //   legacy_unencumbered
+    default_applicable_licenses: ["packages_modules_Virtualization_license"],
+}
+
 system_policy_files = [
     "system/private/security_classes",
     "system/private/initial_sids",
diff --git a/microdroid/sepolicy/system/private/microdroid_launcher.te b/microdroid/sepolicy/system/private/microdroid_launcher.te
index e68b687..5a313b6 100644
--- a/microdroid/sepolicy/system/private/microdroid_launcher.te
+++ b/microdroid/sepolicy/system/private/microdroid_launcher.te
@@ -21,3 +21,6 @@
 
 # Allow to use terminal
 allow microdroid_launcher devpts:chr_file rw_file_perms;
+
+# Allow to set debug prop
+set_prop(microdroid_launcher, debug_prop)
diff --git a/microdroid/sepolicy/system/private/microdroid_manager.te b/microdroid/sepolicy/system/private/microdroid_manager.te
index f2feca2..deb969c 100644
--- a/microdroid/sepolicy/system/private/microdroid_manager.te
+++ b/microdroid/sepolicy/system/private/microdroid_manager.te
@@ -21,6 +21,12 @@
 # Until then, allow microdroid_manager to execute the shell or other system executables.
 allow microdroid_manager {shell_exec toolbox_exec}:file rx_file_perms;
 
+# Let microdroid_manager kernel-log.
+# TODO(b/189805435) when ready this should be kmsg_device rather than kmsg_debug_device
+userdebug_or_eng(`
+  allow microdroid_manager kmsg_debug_device:chr_file write;
+')
+
 # Let microdroid_manager read a config file from /mnt/apk (fusefs)
 # TODO(b/188400186) remove the below two rules
 userdebug_or_eng(`
diff --git a/microdroid/uboot-env-x86_64.txt b/microdroid/uboot-env-x86_64.txt
index ffc0462..1abafa6 100644
--- a/microdroid/uboot-env-x86_64.txt
+++ b/microdroid/uboot-env-x86_64.txt
@@ -1,7 +1,9 @@
 # Static u-boot environment variables for microdroid. See b/180481192
 
 # Boot the device following the Android boot procedure
-bootcmd=avb init virtio 0 && avb verify _a && boot_android virtio 0#misc
+# `0` is the disk number of os_composite.img
+# `a` and `_a` are the slot index for A/B
+bootcmd=avb init virtio 0 && avb verify _a && boot_android virtio 0 a
 
 bootdelay=0
 
diff --git a/microdroid/uboot-env.txt b/microdroid/uboot-env.txt
index 0bdc591..585702e 100644
--- a/microdroid/uboot-env.txt
+++ b/microdroid/uboot-env.txt
@@ -1,7 +1,9 @@
 # Static u-boot environment variables for microdroid. See b/180481192
 
 # Boot the device following the Android boot procedure
-bootcmd=avb init virtio 0 && avb verify _a && boot_android virtio 0#misc
+# `0` is the disk number of os_composite.img
+# `a` and `_a` are the slot index for A/B
+bootcmd=avb init virtio 0 && avb verify _a && boot_android virtio 0 a
 
 bootdelay=0
 fdtaddr=0x80000000
diff --git a/microdroid_manager/Android.bp b/microdroid_manager/Android.bp
index cb628b1..30f8481 100644
--- a/microdroid_manager/Android.bp
+++ b/microdroid_manager/Android.bp
@@ -9,7 +9,7 @@
     edition: "2018",
     prefer_rlib: true,
     rustlibs: [
-        "libandroid_logger",
+        "libenv_logger",
         "libanyhow",
         "libkeystore2_system_property-rust",
         "liblog_rust",
diff --git a/microdroid_manager/microdroid_manager.rc b/microdroid_manager/microdroid_manager.rc
index c800002..4f194a3 100644
--- a/microdroid_manager/microdroid_manager.rc
+++ b/microdroid_manager/microdroid_manager.rc
@@ -1,4 +1,7 @@
 service microdroid_manager /system/bin/microdroid_manager
     disabled
+    # TODO(b/189805435) for now redirect stdio to kmsg
+    stdio_to_kmsg
+    setenv RUST_LOG info
     # TODO(jooyung) remove this when microdroid_manager becomes a daemon
     oneshot
\ No newline at end of file
diff --git a/microdroid_manager/src/main.rs b/microdroid_manager/src/main.rs
index ae72a59..10731c5 100644
--- a/microdroid_manager/src/main.rs
+++ b/microdroid_manager/src/main.rs
@@ -17,10 +17,9 @@
 mod ioutil;
 mod metadata;
 
-use android_logger::Config;
 use anyhow::{anyhow, bail, Result};
 use keystore2_system_property::PropertyWatcher;
-use log::{info, Level};
+use log::info;
 use microdroid_payload_config::{Task, TaskType, VmPayloadConfig};
 use std::fs;
 use std::path::Path;
@@ -29,10 +28,9 @@
 
 const WAIT_TIMEOUT: Duration = Duration::from_secs(10);
 
-const LOG_TAG: &str = "MicrodroidManager";
-
 fn main() -> Result<()> {
-    android_logger::init_once(Config::default().with_tag(LOG_TAG).with_min_level(Level::Debug));
+    // TODO(b/189805435) use kernlog
+    env_logger::init();
 
     info!("started.");
 
@@ -57,8 +55,15 @@
 
 fn exec_task(task: &Task) -> Result<()> {
     info!("executing main task {:?}...", task);
-    build_command(task)?.spawn()?;
-    Ok(())
+    let exit_status = build_command(task)?.spawn()?.wait()?;
+    if exit_status.success() {
+        Ok(())
+    } else {
+        match exit_status.code() {
+            Some(code) => bail!("task exited with exit code: {}", code),
+            None => bail!("task terminated by signal"),
+        }
+    }
 }
 
 fn build_command(task: &Task) -> Result<Command> {
diff --git a/tests/hostside/Android.bp b/tests/hostside/Android.bp
index 429b737..8edd65d 100644
--- a/tests/hostside/Android.bp
+++ b/tests/hostside/Android.bp
@@ -6,5 +6,9 @@
     name: "MicrodroidHostTestCases",
     srcs: ["java/**/*.java"],
     test_suites: ["device-tests"],
-    libs: ["tradefed"],
+    libs: [
+        "tradefed",
+        "compatibility-tradefed",
+    ],
+    data: [":MicrodroidTestApp.signed"],
 }
diff --git a/tests/hostside/AndroidTest.xml b/tests/hostside/AndroidTest.xml
index da24b71..247923d 100644
--- a/tests/hostside/AndroidTest.xml
+++ b/tests/hostside/AndroidTest.xml
@@ -18,6 +18,11 @@
         <option name="force-root" value="true" />
     </target_preparer>
 
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="MicrodroidTestApp.apk" />
+    </target_preparer>
+
     <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
         <option name="jar" value="MicrodroidHostTestCases.jar" />
     </test>
diff --git a/tests/hostside/java/android/virt/test/MicrodroidTestCase.java b/tests/hostside/java/android/virt/test/MicrodroidTestCase.java
index a1043f7..7f9b8de 100644
--- a/tests/hostside/java/android/virt/test/MicrodroidTestCase.java
+++ b/tests/hostside/java/android/virt/test/MicrodroidTestCase.java
@@ -20,28 +20,36 @@
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.CoreMatchers.not;
 import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeThat;
 
+import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
 import com.android.tradefed.util.CommandResult;
 import com.android.tradefed.util.CommandStatus;
-import com.android.tradefed.util.FileUtil;
 import com.android.tradefed.util.RunUtil;
 
+import org.json.JSONArray;
+import org.json.JSONObject;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.io.BufferedReader;
 import java.io.File;
 import java.io.FileWriter;
+import java.io.InputStream;
+import java.io.InputStreamReader;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import java.util.zip.ZipFile;
 
 @RunWith(DeviceJUnit4ClassRunner.class)
 public class MicrodroidTestCase extends BaseHostJUnit4Test {
@@ -50,134 +58,201 @@
     private static final int TEST_VM_CID = 10;
     private static final int TEST_VM_ADB_PORT = 8000;
     private static final String MICRODROID_SERIAL = "localhost:" + TEST_VM_ADB_PORT;
+
     // This is really slow on GCE (2m 40s) but fast on localhost or actual Android phones (< 10s)
     // Set the maximum timeout value big enough.
     private static final long MICRODROID_BOOT_TIMEOUT_MINUTES = 5;
 
-    private String executeCommand(String cmd) {
-        final long defaultCommandTimeoutMillis = 3000; // 3 sec. Can be slow on GCE
-        return executeCommand(defaultCommandTimeoutMillis, cmd);
-    }
-
-    private String executeCommand(long timeout, String cmd) {
-        CommandResult result = RunUtil.getDefault().runTimedCmd(timeout, cmd.split(" "));
-        return result.getStdout().trim(); // remove the trailing whitespace including newline
-    }
-
-    private String executeCommandOnMicrodroid(String cmd) {
-        cmd = "adb -s " + MICRODROID_SERIAL + " " + cmd;
-        return executeCommand(cmd);
-    }
-
     @Test
     public void testMicrodroidBoots() throws Exception {
-        // Prepare input files
-        String prepareImagesCmd =
-                String.format(
-                        "mkdir -p %s; cd %s; "
-                                + "cp %setc/microdroid_bootloader bootloader && "
-                                + "cp %setc/fs/*.img . && "
-                                + "cp %setc/uboot_env.img . && "
-                                + "dd if=/dev/zero of=misc.img bs=4k count=256",
-                        TEST_ROOT, TEST_ROOT, VIRT_APEX, VIRT_APEX, VIRT_APEX);
-        getDevice().executeShellCommand(prepareImagesCmd);
+        final String apkName = "MicrodroidTestApp.apk";
+        final String packageName = "com.android.microdroid.test";
+        final String configPath = "assets/vm_config.json"; // path inside the APK
+        startMicrodroid(apkName, packageName, configPath);
+        waitForMicrodroidBoot(MICRODROID_BOOT_TIMEOUT_MINUTES);
+        adbConnectToMicrodroid();
 
-        // Create os_composite.img, env_composite.img, and payload.img
-        String makeOsCompositeCmd =
-                String.format(
-                        "cd %s; %sbin/mk_cdisk %setc/microdroid_cdisk.json os_composite.img",
-                        TEST_ROOT, VIRT_APEX, VIRT_APEX);
-        getDevice().executeShellCommand(makeOsCompositeCmd);
-        String makeEnvCompositeCmd =
-                String.format(
-                        "cd %s; %sbin/mk_cdisk %setc/microdroid_cdisk_env.json env_composite.img",
-                        TEST_ROOT, VIRT_APEX, VIRT_APEX);
-        getDevice().executeShellCommand(makeEnvCompositeCmd);
-        String makePayloadCompositeCmd =
-                String.format(
-                        "cd %s; %sbin/mk_payload %setc/microdroid_payload.json payload.img",
-                        TEST_ROOT, VIRT_APEX, VIRT_APEX);
-        getDevice().executeShellCommand(makePayloadCompositeCmd);
+        // Check if it actually booted by reading a sysprop.
+        assertThat(runOnMicrodroid("getprop", "ro.hardware"), is("microdroid"));
 
-        // Make sure that the composite images are created
-        final List<String> compositeImages =
-                new ArrayList<>(
-                        Arrays.asList(
-                                TEST_ROOT + "/os_composite.img",
-                                TEST_ROOT + "/env_composite.img",
-                                TEST_ROOT + "/payload.img"));
+        // Test writing to /data partition
+        runOnMicrodroid("echo MicrodroidTest > /data/local/tmp/test.txt");
+        assertThat(runOnMicrodroid("cat /data/local/tmp/test.txt"), is("MicrodroidTest"));
+
+        // Check if the APK partition exists
+        final String apkPartition = "/dev/block/by-name/microdroid-apk";
+        assertThat(runOnMicrodroid("ls", apkPartition), is(apkPartition));
+
+        // Check if the APK is mounted using zipfuse
+        final String mountEntry = "zipfuse on /mnt/apk type fuse.zipfuse";
+        assertThat(runOnMicrodroid("mount"), containsString(mountEntry));
+
+        // Check if the native library in the APK is has correct filesystem info
+        final String[] abis = runOnMicrodroid("getprop", "ro.product.cpu.abilist").split(",");
+        assertThat(abis.length, is(1));
+        final String testLib = "/mnt/apk/lib/" + abis[0] + "/MicrodroidTestNativeLib.so";
+        final String label = "u:object_r:system_file:s0";
+        assertThat(runOnMicrodroid("ls", "-Z", testLib), is(label + " " + testLib));
+
+        // Check if the command in vm_config.json was executed by examining the side effect of the
+        // command
+        assertThat(runOnMicrodroid("getprop", "debug.microdroid.app.run"), is("true"));
+
+        // Manually execute the library and check the output
+        final String microdroidLauncher = "system/bin/microdroid_launcher";
+        assertThat(
+                runOnMicrodroid(microdroidLauncher, testLib, "arg1", "arg2"),
+                is("Hello Microdroid " + testLib + " arg1 arg2"));
+
+        // Shutdown microdroid
+        runOnMicrodroid("reboot");
+    }
+
+    // Run an arbitrary command in the host side and returns the result
+    private String runOnHost(String... cmd) {
+        final long timeout = 10000;
+        CommandResult result = RunUtil.getDefault().runTimedCmd(timeout, cmd);
+        assertThat(result.getStatus(), is(CommandStatus.SUCCESS));
+        return result.getStdout().trim();
+    }
+
+    // Same as runOnHost, but failure is not an error
+    private String tryRunOnHost(String... cmd) {
+        final long timeout = 10000;
+        CommandResult result = RunUtil.getDefault().runTimedCmd(timeout, cmd);
+        return result.getStdout().trim();
+    }
+
+    // Run a shell command on Android
+    private String runOnAndroid(String... cmd) throws Exception {
+        CommandResult result = getDevice().executeShellV2Command(join(cmd));
+        assertThat(result.getStatus(), is(CommandStatus.SUCCESS));
+        return result.getStdout().trim();
+    }
+
+    // Same as runOnAndroid, but failutre is not an error
+    private String tryRunOnAndroid(String... cmd) throws Exception {
+        CommandResult result = getDevice().executeShellV2Command(join(cmd));
+        return result.getStdout().trim();
+    }
+
+    // Run a shell command on Microdroid
+    private String runOnMicrodroid(String... cmd) {
+        final long timeout = 30000; // 30 sec. Microdroid is extremely slow on GCE-on-CF.
         CommandResult result =
-                getDevice().executeShellV2Command("du -b " + String.join(" ", compositeImages));
-        assertThat(result.getExitCode(), is(0));
-        assertThat(result.getStdout(), is(not("")));
+                RunUtil.getDefault()
+                        .runTimedCmd(timeout, "adb", "-s", MICRODROID_SERIAL, "shell", join(cmd));
+        assertThat(result.getStatus(), is(CommandStatus.SUCCESS));
+        return result.getStdout().trim();
+    }
+
+    private String join(String... strs) {
+        return String.join(" ", Arrays.asList(strs));
+    }
+
+    private String createPayloadImage(String apkName, String packageName, String configPath)
+            throws Exception {
+        File apkFile = findTestFile(apkName);
+        getDevice().installPackage(apkFile, /* reinstall */ true);
+
+        // Read the config file from the apk and parse it to know the list of APEXes needed
+        ZipFile apkAsZip = new ZipFile(apkFile);
+        InputStream is = apkAsZip.getInputStream(apkAsZip.getEntry(configPath));
+        String configString =
+                new BufferedReader(new InputStreamReader(is))
+                        .lines()
+                        .collect(Collectors.joining("\n"));
+        JSONObject configObject = new JSONObject(configString);
+        JSONArray apexes = configObject.getJSONArray("apexes");
+        List<String> apexNames = new ArrayList<>();
+        for (int i = 0; i < apexes.length(); i++) {
+            JSONObject anApex = apexes.getJSONObject(i);
+            apexNames.add(anApex.getString("name"));
+        }
+
+        // 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);
+        assertTrue(apkPath.startsWith("package:"));
+        apkPath = apkPath.substring("package:".length());
+
+        // Create payload.json from the gathered data
+        JSONObject payloadObject = new JSONObject();
+        payloadObject.put("system_apexes", new JSONArray(apexNames));
+        payloadObject.put("payload_config_path", "/mnt/apk/" + configPath);
+        JSONObject apkObject = new JSONObject();
+        apkObject.put("path", apkPath);
+        apkObject.put("name", packageName);
+        payloadObject.put("apk", apkObject);
+
+        // Push the idsig file to the device
+        // TODO(b/190343842): pass path to this file to payloadObject
+        // File idsigOnHost = findTestFile(apkFile + ".idsig");
+        // final String testApkIdsig = TEST_ROOT + apkFile + ".idsig";
+        // getDevice().pushFile(idsigOnHost, testApkIdsig);
+
+        // Copy the json file to Android
+        File payloadJsonOnHost = File.createTempFile("payload", "json");
+        FileWriter writer = new FileWriter(payloadJsonOnHost);
+        writer.write(payloadObject.toString());
+        writer.close();
+        final String payloadJson = TEST_ROOT + "payload.json";
+        getDevice().pushFile(payloadJsonOnHost, payloadJson);
+
+        // Finally run mk_payload to create payload.img
+        final String mkPayload = VIRT_APEX + "bin/mk_payload";
+        final String payloadImg = TEST_ROOT + "payload.img";
+        runOnAndroid(mkPayload, payloadJson, payloadImg);
+        assertThat(runOnAndroid("du", "-b", payloadImg), is(not("")));
+
+        return payloadImg;
+    }
+
+    private File findTestFile(String name) throws Exception {
+        return (new CompatibilityBuildHelper(getBuild())).getTestFile(name);
+    }
+
+    private void startMicrodroid(String apkName, String packageName, String configPath)
+            throws Exception {
+        // Create payload.img
+        final String payloadImg = createPayloadImage(apkName, packageName, configPath);
+
+        // Tools and executables
+        final String mkCdisk = VIRT_APEX + "bin/mk_cdisk";
+        final String crosvm = VIRT_APEX + "bin/crosvm";
+
+        // Create os_composisite.img and env_composite.img
+        // TODO(jiyong): remove this when running a VM is done by `vm`
+        final String cdiskJson = VIRT_APEX + "etc/microdroid_cdisk.json";
+        final String cdiskEnvJson = VIRT_APEX + "etc/microdroid_cdisk_env.json";
+        final String osImg = TEST_ROOT + "os_composite.img";
+        final String envImg = TEST_ROOT + "env_composite.img";
+        final String bootloader = VIRT_APEX + "etc/microdroid_bootloader";
+        runOnAndroid(mkCdisk, cdiskJson, osImg);
+        runOnAndroid(mkCdisk, cdiskEnvJson, envImg);
 
         // Start microdroid using crosvm
+        // TODO(jiyong): do this via the `vm` command
         ExecutorService executor = Executors.newFixedThreadPool(1);
-        String runMicrodroidCmd =
-                String.format(
-                        "cd %s; %sbin/crosvm run --cid=%d --disable-sandbox --bios=bootloader"
-                                + " --serial=type=syslog --disk=os_composite.img"
-                                + " --disk=env_composite.img --disk=payload.img &",
-                        TEST_ROOT, VIRT_APEX, TEST_VM_CID);
         executor.execute(
                 () -> {
                     try {
-                        getDevice().executeShellV2Command(runMicrodroidCmd);
+                        runOnAndroid(
+                                crosvm,
+                                "run",
+                                "--cid=" + TEST_VM_CID,
+                                "--disable-sandbox",
+                                "--bios=" + bootloader,
+                                "--serial=type=syslog",
+                                "--disk=" + osImg,
+                                "--disk=" + envImg,
+                                "--disk=" + payloadImg,
+                                "&");
                     } catch (Exception e) {
                         throw new RuntimeException(e);
                     }
                 });
-        waitForMicrodroidBoot(MICRODROID_BOOT_TIMEOUT_MINUTES);
-
-        // Connect to microdroid and read a system property from there
-        executeCommand(
-                "adb -s "
-                        + getDevice().getSerialNumber()
-                        + " forward tcp:"
-                        + TEST_VM_ADB_PORT
-                        + " vsock:"
-                        + TEST_VM_CID
-                        + ":5555");
-        executeCommand("adb connect " + MICRODROID_SERIAL);
-        String prop = executeCommandOnMicrodroid("shell getprop ro.hardware");
-        assertThat(prop, is("microdroid"));
-
-        // Test writing to /data partition
-        File tmpFile = FileUtil.createTempFile("test", ".txt");
-        tmpFile.deleteOnExit();
-        FileWriter writer = new FileWriter(tmpFile);
-        writer.write("MicrodroidTest");
-        writer.close();
-
-        executeCommandOnMicrodroid("push " + tmpFile.getPath() + " /data/local/tmp/test.txt");
-        assertThat(
-                executeCommandOnMicrodroid("shell cat /data/local/tmp/test.txt"),
-                is("MicrodroidTest"));
-
-        assertThat(
-                executeCommandOnMicrodroid("shell ls /dev/block/by-name/microdroid-apk"),
-                is("/dev/block/by-name/microdroid-apk"));
-
-        assertThat(
-                executeCommandOnMicrodroid("shell mount"),
-                containsString("zipfuse on /mnt/apk type fuse.zipfuse"));
-
-        final String[] abiList =
-                executeCommandOnMicrodroid("shell getprop ro.product.cpu.abilist").split(",");
-        assertThat(abiList.length, is(1));
-
-        final String libPath = "/mnt/apk/lib/" + abiList[0] + "/MicrodroidTestNativeLib.so";
-        assertThat(
-                executeCommandOnMicrodroid("shell ls -Z " + libPath),
-                is("u:object_r:system_file:s0 " + libPath));
-
-        assertThat(
-                executeCommandOnMicrodroid(
-                        "shell /system/bin/microdroid_launcher " + libPath + " arg1 arg2"),
-                is("Hello Microdroid " + libPath + " arg1 arg2"));
-
-        // Shutdown microdroid
-        executeCommand("adb -s localhost:" + TEST_VM_ADB_PORT + " shell reboot");
     }
 
     private void waitForMicrodroidBoot(long timeoutMinutes) throws Exception {
@@ -191,9 +266,19 @@
                         TimeUnit.MINUTES);
     }
 
+    // Establish an adb connection to microdroid by letting Android forward the connection to
+    // microdroid.
+    private void adbConnectToMicrodroid() {
+        final String serial = getDevice().getSerialNumber();
+        final String from = "tcp:" + TEST_VM_ADB_PORT;
+        final String to = "vsock:" + TEST_VM_CID + ":5555";
+        runOnHost("adb", "-s", serial, "forward", from, to);
+        runOnHost("adb", "connect", MICRODROID_SERIAL);
+    }
+
     private void skipIfFail(String command) throws Exception {
-        assumeThat(
-                getDevice().executeShellV2Command(command).getStatus(), is(CommandStatus.SUCCESS));
+        CommandResult result = getDevice().executeShellV2Command(command);
+        assumeThat(result.getStatus(), is(CommandStatus.SUCCESS));
     }
 
     @Before
@@ -208,24 +293,25 @@
     @Before
     public void setUp() throws Exception {
         // kill stale crosvm processes
-        getDevice().executeShellV2Command("killall crosvm");
+        tryRunOnAndroid("killall", "crosvm");
 
-        // delete the test root
-        getDevice().executeShellCommand("rm -rf " + TEST_ROOT);
+        // Prepare the test root
+        tryRunOnAndroid("rm", "-rf", TEST_ROOT);
+        tryRunOnAndroid("mkdir", "-p", TEST_ROOT);
 
         // disconnect from microdroid
-        executeCommand("adb disconnect " + MICRODROID_SERIAL);
+        tryRunOnHost("adb", "disconnect", MICRODROID_SERIAL);
 
         // clear the log
-        getDevice().executeShellV2Command("logcat -c");
+        tryRunOnAndroid("logcat", "-c");
     }
 
     @After
     public void shutdown() throws Exception {
         // disconnect from microdroid
-        executeCommand("adb disconnect " + MICRODROID_SERIAL);
+        tryRunOnHost("adb", "disconnect", MICRODROID_SERIAL);
 
         // kill stale crosvm processes
-        getDevice().executeShellV2Command("killall crosvm");
+        tryRunOnAndroid("killall", "crosvm");
     }
 }
diff --git a/tests/testapk/Android.bp b/tests/testapk/Android.bp
index 277ccc8..35f2f08 100644
--- a/tests/testapk/Android.bp
+++ b/tests/testapk/Android.bp
@@ -7,7 +7,7 @@
     srcs: ["src/java/**/*.java"],
     jni_libs: ["MicrodroidTestNativeLib"],
     sdk_version: "current",
-    apex_available: ["com.android.virt"], // TODO(jiyong): remove this from virt APEX
+    use_embedded_native_libs: true,
 }
 
 // TODO(jiyong): make this a binary, not a shared library
@@ -15,5 +15,21 @@
     name: "MicrodroidTestNativeLib",
     srcs: ["src/native/*.cpp"],
     sdk_version: "current",
-    apex_available: ["com.android.virt"], // TODO(jiyong): remove this from virt APEX
+}
+
+genrule {
+    name: "MicrodroidTestApp.signed",
+    out: [
+        "MicrodroidTestApp.apk",
+        "MicrodroidTestApp.apk.idsig",
+    ],
+    srcs: [":MicrodroidTestApp"],
+    tools:["apksigner"],
+    tool_files: ["test.keystore"],
+    cmd: "$(location apksigner) sign " +
+         "--ks $(location test.keystore) " +
+         "--ks-pass=pass:testkey --key-pass=pass:testkey " +
+         "--in $(in) " +
+         "--out $(genDir)/MicrodroidTestApp.apk",
+         // $(genDir)/MicrodroidTestApp.apk.idsig is generated implicitly
 }
diff --git a/tests/testapk/assets/vm_config.json b/tests/testapk/assets/vm_config.json
index 7a3df7a..8312f4d 100644
--- a/tests/testapk/assets/vm_config.json
+++ b/tests/testapk/assets/vm_config.json
@@ -9,5 +9,19 @@
       "hello",
       "microdroid"
     ]
-  }
-}
\ No newline at end of file
+  },
+  "apexes": [
+    {
+      "name": "com.android.adbd"
+    },
+    {
+      "name": "com.android.i18n"
+    },
+    {
+      "name": "com.android.os.statsd"
+    },
+    {
+      "name": "com.android.sdkext"
+    }
+  ]
+}
diff --git a/tests/testapk/src/native/testbinary.cpp b/tests/testapk/src/native/testbinary.cpp
index c317cd2..c3eefc4 100644
--- a/tests/testapk/src/native/testbinary.cpp
+++ b/tests/testapk/src/native/testbinary.cpp
@@ -14,6 +14,7 @@
  * limitations under the License.
  */
 #include <stdio.h>
+#include <sys/system_properties.h>
 
 extern "C" int android_native_main(int argc, char* argv[]) {
     printf("Hello Microdroid ");
@@ -25,5 +26,7 @@
         }
     }
     printf("\n");
+
+    __system_property_set("debug.microdroid.app.run", "true");
     return 0;
 }
diff --git a/tests/testapk/test.keystore b/tests/testapk/test.keystore
new file mode 100644
index 0000000..2f024d8
--- /dev/null
+++ b/tests/testapk/test.keystore
Binary files differ