Add test API for extra APKs

Bug: 303201498
Test: atest MicrodroidTests
Change-Id: Iaae9274d704c6cbf47be31902820872c996a701e
diff --git a/javalib/api/test-current.txt b/javalib/api/test-current.txt
index 958005f..5aff93f 100644
--- a/javalib/api/test-current.txt
+++ b/javalib/api/test-current.txt
@@ -7,12 +7,14 @@
   }
 
   public final class VirtualMachineConfig {
+    method @FlaggedApi("RELEASE_AVF_ENABLE_MULTI_TENANT_MICRODROID_VM") @NonNull public java.util.List<java.lang.String> getExtraApks();
     method @FlaggedApi("RELEASE_AVF_ENABLE_VENDOR_MODULES") @Nullable public String getOs();
     method @Nullable public String getPayloadConfigPath();
     method public boolean isVmConsoleInputSupported();
   }
 
   public static final class VirtualMachineConfig.Builder {
+    method @FlaggedApi("RELEASE_AVF_ENABLE_MULTI_TENANT_MICRODROID_VM") @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder addExtraApk(@NonNull String);
     method @FlaggedApi("RELEASE_AVF_ENABLE_VENDOR_MODULES") @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setOs(@NonNull String);
     method @NonNull @RequiresPermission(android.system.virtualmachine.VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION) public android.system.virtualmachine.VirtualMachineConfig.Builder setPayloadConfigPath(@NonNull String);
     method @FlaggedApi("RELEASE_AVF_ENABLE_VENDOR_MODULES") @NonNull @RequiresPermission(android.system.virtualmachine.VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION) public android.system.virtualmachine.VirtualMachineConfig.Builder setVendorDiskImage(@NonNull java.io.File);
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachine.java b/javalib/src/android/system/virtualmachine/VirtualMachine.java
index 16f9631..5025e88 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachine.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachine.java
@@ -54,6 +54,8 @@
 import android.annotation.WorkerThread;
 import android.content.ComponentCallbacks2;
 import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
 import android.content.res.Configuration;
 import android.os.Binder;
 import android.os.IBinder;
@@ -76,7 +78,6 @@
 
 import java.io.File;
 import java.io.FileInputStream;
-import java.io.FileOutputStream;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.IOException;
@@ -807,10 +808,30 @@
                     createVmInputPipes();
                 }
 
+                VirtualMachineConfig vmConfig = getConfig();
                 VirtualMachineAppConfig appConfig =
-                        getConfig().toVsConfig(mContext.getPackageManager());
+                        vmConfig.toVsConfig(mContext.getPackageManager());
                 appConfig.name = mName;
 
+                if (!vmConfig.getExtraApks().isEmpty()) {
+                    // Extra APKs were specified directly, rather than via config file.
+                    // We've already populated the file names for the extra APKs and IDSigs
+                    // (via setupExtraApks). But we also need to open the APK files and add
+                    // fds for them to the payload config.
+                    // This isn't needed when the extra APKs are specified in a config file - then
+                    // Virtualization Manager opens them itself.
+                    List<ParcelFileDescriptor> extraApkFiles = new ArrayList<>(mExtraApks.size());
+                    for (ExtraApkSpec extraApk : mExtraApks) {
+                        try {
+                            extraApkFiles.add(
+                                    ParcelFileDescriptor.open(extraApk.apk, MODE_READ_ONLY));
+                        } catch (FileNotFoundException e) {
+                            throw new VirtualMachineException("Failed to open extra APK", e);
+                        }
+                    }
+                    appConfig.payload.getPayloadConfig().extraApks = extraApkFiles;
+                }
+
                 try {
                     createIdSigs(service, appConfig);
                 } catch (FileNotFoundException e) {
@@ -1239,6 +1260,46 @@
         return result.toString();
     }
 
+    /**
+     * Reads the payload config inside the application, parses extra APK information, and then
+     * creates corresponding idsig file paths.
+     */
+    private static List<ExtraApkSpec> setupExtraApks(
+            @NonNull Context context, @NonNull VirtualMachineConfig config, @NonNull File vmDir)
+            throws VirtualMachineException {
+        String configPath = config.getPayloadConfigPath();
+        List<String> extraApks = config.getExtraApks();
+        if (configPath != null) {
+            return setupExtraApksFromConfigFile(context, vmDir, configPath);
+        } else if (!extraApks.isEmpty()) {
+            return setupExtraApksFromList(context, vmDir, extraApks);
+        } else {
+            return Collections.emptyList();
+        }
+    }
+
+    private static List<ExtraApkSpec> setupExtraApksFromConfigFile(
+            Context context, File vmDir, String configPath) throws VirtualMachineException {
+        try (ZipFile zipFile = new ZipFile(context.getPackageCodePath())) {
+            InputStream inputStream = zipFile.getInputStream(zipFile.getEntry(configPath));
+            List<String> apkList =
+                    parseExtraApkListFromPayloadConfig(
+                            new JsonReader(new InputStreamReader(inputStream)));
+
+            List<ExtraApkSpec> extraApks = new ArrayList<>(apkList.size());
+            for (int i = 0; i < apkList.size(); ++i) {
+                extraApks.add(
+                        new ExtraApkSpec(
+                                new File(apkList.get(i)),
+                                new File(vmDir, EXTRA_IDSIG_FILE_PREFIX + i)));
+            }
+
+            return extraApks;
+        } catch (IOException e) {
+            throw new VirtualMachineException("Couldn't parse extra apks from the vm config", e);
+        }
+    }
+
     private static List<String> parseExtraApkListFromPayloadConfig(JsonReader reader)
             throws VirtualMachineException {
         /*
@@ -1275,36 +1336,28 @@
         }
     }
 
-    /**
-     * Reads the payload config inside the application, parses extra APK information, and then
-     * creates corresponding idsig file paths.
-     */
-    private static List<ExtraApkSpec> setupExtraApks(
-            @NonNull Context context, @NonNull VirtualMachineConfig config, @NonNull File vmDir)
-            throws VirtualMachineException {
-        String configPath = config.getPayloadConfigPath();
-        if (configPath == null) {
-            return Collections.emptyList();
-        }
-        try (ZipFile zipFile = new ZipFile(context.getPackageCodePath())) {
-            InputStream inputStream =
-                    zipFile.getInputStream(zipFile.getEntry(configPath));
-            List<String> apkList =
-                    parseExtraApkListFromPayloadConfig(
-                            new JsonReader(new InputStreamReader(inputStream)));
-
-            List<ExtraApkSpec> extraApks = new ArrayList<>();
-            for (int i = 0; i < apkList.size(); ++i) {
-                extraApks.add(
-                        new ExtraApkSpec(
-                                new File(apkList.get(i)),
-                                new File(vmDir, EXTRA_IDSIG_FILE_PREFIX + i)));
+    private static List<ExtraApkSpec> setupExtraApksFromList(
+            Context context, File vmDir, List<String> extraApkInfo) throws VirtualMachineException {
+        int count = extraApkInfo.size();
+        List<ExtraApkSpec> extraApks = new ArrayList<>(count);
+        for (int i = 0; i < count; i++) {
+            String packageName = extraApkInfo.get(i);
+            ApplicationInfo appInfo;
+            try {
+                appInfo =
+                        context.getPackageManager()
+                                .getApplicationInfo(
+                                        packageName, PackageManager.ApplicationInfoFlags.of(0));
+            } catch (PackageManager.NameNotFoundException e) {
+                throw new VirtualMachineException("Extra APK package not found", e);
             }
 
-            return Collections.unmodifiableList(extraApks);
-        } catch (IOException e) {
-            throw new VirtualMachineException("Couldn't parse extra apks from the vm config", e);
+            extraApks.add(
+                    new ExtraApkSpec(
+                            new File(appInfo.sourceDir),
+                            new File(vmDir, EXTRA_IDSIG_FILE_PREFIX + i)));
         }
+        return extraApks;
     }
 
     private void importInstanceFrom(@NonNull ParcelFileDescriptor instanceFd)
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
index e8ef195..9688789 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -49,7 +49,10 @@
 import java.io.OutputStream;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
+import java.util.List;
 import java.util.Objects;
 import java.util.zip.ZipFile;
 
@@ -67,7 +70,7 @@
     private static String[] EMPTY_STRING_ARRAY = {};
 
     // These define the schema of the config file persisted on disk.
-    private static final int VERSION = 7;
+    private static final int VERSION = 8;
     private static final String KEY_VERSION = "version";
     private static final String KEY_PACKAGENAME = "packageName";
     private static final String KEY_APKPATH = "apkPath";
@@ -82,6 +85,7 @@
     private static final String KEY_VM_CONSOLE_INPUT_SUPPORTED = "vmConsoleInputSupported";
     private static final String KEY_VENDOR_DISK_IMAGE_PATH = "vendorDiskImagePath";
     private static final String KEY_OS = "os";
+    private static final String KEY_EXTRA_APKS = "extraApks";
 
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
@@ -140,6 +144,8 @@
     /** Absolute path to the APK file containing the VM payload. */
     @Nullable private final String mApkPath;
 
+    private final List<String> mExtraApks;
+
     @DebugLevel private final int mDebugLevel;
 
     /**
@@ -181,6 +187,7 @@
     private VirtualMachineConfig(
             @Nullable String packageName,
             @Nullable String apkPath,
+            List<String> extraApks,
             @Nullable String payloadConfigPath,
             @Nullable String payloadBinaryName,
             @DebugLevel int debugLevel,
@@ -195,6 +202,11 @@
         // This is only called from Builder.build(); the builder handles parameter validation.
         mPackageName = packageName;
         mApkPath = apkPath;
+        mExtraApks =
+                extraApks.isEmpty()
+                        ? Collections.emptyList()
+                        : Collections.unmodifiableList(
+                                Arrays.asList(extraApks.toArray(new String[0])));
         mPayloadConfigPath = payloadConfigPath;
         mPayloadBinaryName = payloadBinaryName;
         mDebugLevel = debugLevel;
@@ -292,6 +304,13 @@
             builder.setOs(os);
         }
 
+        String[] extraApks = b.getStringArray(KEY_EXTRA_APKS);
+        if (extraApks != null) {
+            for (String extraApk : extraApks) {
+                builder.addExtraApk(extraApk);
+            }
+        }
+
         return builder.build();
     }
 
@@ -331,6 +350,10 @@
             b.putString(KEY_VENDOR_DISK_IMAGE_PATH, mVendorDiskImage.getAbsolutePath());
         }
         b.putString(KEY_OS, mOs);
+        if (!mExtraApks.isEmpty()) {
+            String[] extraApks = mExtraApks.toArray(new String[0]);
+            b.putStringArray(KEY_EXTRA_APKS, extraApks);
+        }
         b.writeToStream(output);
     }
 
@@ -347,6 +370,19 @@
     }
 
     /**
+     * Returns the package names of any extra APKs that have been requested for the VM. They are
+     * returned in the order in which they were added via {@link Builder#addExtraApk}.
+     *
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi("RELEASE_AVF_ENABLE_MULTI_TENANT_MICRODROID_VM")
+    @NonNull
+    public List<String> getExtraApks() {
+        return mExtraApks;
+    }
+
+    /**
      * Returns the path within the APK to the payload config file that defines software aspects of
      * the VM.
      *
@@ -495,7 +531,8 @@
                 && Objects.equals(this.mPayloadConfigPath, other.mPayloadConfigPath)
                 && Objects.equals(this.mPayloadBinaryName, other.mPayloadBinaryName)
                 && Objects.equals(this.mPackageName, other.mPackageName)
-                && Objects.equals(this.mOs, other.mOs);
+                && Objects.equals(this.mOs, other.mOs)
+                && Objects.equals(this.mExtraApks, other.mExtraApks);
     }
 
     /**
@@ -623,6 +660,7 @@
 
         @Nullable private final String mPackageName;
         @Nullable private String mApkPath;
+        private final List<String> mExtraApks = new ArrayList<>();
         @Nullable private String mPayloadConfigPath;
         @Nullable private String mPayloadBinaryName;
         @DebugLevel private int mDebugLevel = DEBUG_LEVEL_NONE;
@@ -683,6 +721,10 @@
                     throw new IllegalStateException(
                             "setPayloadConfigPath and setOs may not both be called");
                 }
+                if (!mExtraApks.isEmpty()) {
+                    throw new IllegalStateException(
+                            "setPayloadConfigPath and addExtraApk may not both be called");
+                }
             } else {
                 if (mPayloadConfigPath != null) {
                     throw new IllegalStateException(
@@ -710,6 +752,7 @@
             return new VirtualMachineConfig(
                     packageName,
                     apkPath,
+                    mExtraApks,
                     mPayloadConfigPath,
                     mPayloadBinaryName,
                     mDebugLevel,
@@ -742,6 +785,21 @@
         }
 
         /**
+         * Specify the package name of an extra APK to be included in the VM. Each extra APK is
+         * mounted, in unzipped form, inside the VM, allowing access to the code and/or data within
+         * it. The VM entry point must be in the main APK.
+         *
+         * @hide
+         */
+        @TestApi
+        @FlaggedApi("RELEASE_AVF_ENABLE_MULTI_TENANT_MICRODROID_VM")
+        @NonNull
+        public Builder addExtraApk(@NonNull String packageName) {
+            mExtraApks.add(requireNonNull(packageName, "extra APK package name must not be null"));
+            return this;
+        }
+
+        /**
          * Sets the path within the APK to the payload config file that defines software aspects of
          * the VM. The file is a JSON file; see
          * packages/modules/Virtualization/microdroid/payload/config/src/lib.rs for the format.
diff --git a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
index df6280d..695e638 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -481,29 +481,32 @@
         // Minimal has as little as specified as possible; everything that can be is defaulted.
         VirtualMachineConfig.Builder minimalBuilder =
                 new VirtualMachineConfig.Builder(getContext())
-                        .setPayloadBinaryName("binary.so")
+                        .setPayloadConfigPath("config/path")
                         .setProtectedVm(isProtectedVm());
         VirtualMachineConfig minimal = minimalBuilder.build();
 
         assertThat(minimal.getApkPath()).isNull();
+        assertThat(minimal.getExtraApks()).isEmpty();
         assertThat(minimal.getDebugLevel()).isEqualTo(DEBUG_LEVEL_NONE);
         assertThat(minimal.getMemoryBytes()).isEqualTo(0);
         assertThat(minimal.getCpuTopology()).isEqualTo(CPU_TOPOLOGY_ONE_CPU);
-        assertThat(minimal.getPayloadBinaryName()).isEqualTo("binary.so");
-        assertThat(minimal.getPayloadConfigPath()).isNull();
+        assertThat(minimal.getPayloadBinaryName()).isNull();
+        assertThat(minimal.getPayloadConfigPath()).isEqualTo("config/path");
         assertThat(minimal.isProtectedVm()).isEqualTo(isProtectedVm());
         assertThat(minimal.isEncryptedStorageEnabled()).isFalse();
         assertThat(minimal.getEncryptedStorageBytes()).isEqualTo(0);
         assertThat(minimal.isVmOutputCaptured()).isEqualTo(false);
-        assertThat(minimal.getOs()).isEqualTo("microdroid");
+        assertThat(minimal.getOs()).isNull();
 
         // Maximal has everything that can be set to some non-default value. (And has different
         // values than minimal for the required fields.)
         VirtualMachineConfig.Builder maximalBuilder =
                 new VirtualMachineConfig.Builder(getContext())
                         .setProtectedVm(mProtectedVm)
-                        .setPayloadConfigPath("config/path")
+                        .setPayloadBinaryName("binary.so")
                         .setApkPath("/apk/path")
+                        .addExtraApk("package.name1:split")
+                        .addExtraApk("package.name2")
                         .setDebugLevel(DEBUG_LEVEL_FULL)
                         .setMemoryBytes(42)
                         .setCpuTopology(CPU_TOPOLOGY_MATCH_HOST)
@@ -512,25 +515,28 @@
         VirtualMachineConfig maximal = maximalBuilder.build();
 
         assertThat(maximal.getApkPath()).isEqualTo("/apk/path");
+        assertThat(maximal.getExtraApks())
+                .containsExactly("package.name1:split", "package.name2")
+                .inOrder();
         assertThat(maximal.getDebugLevel()).isEqualTo(DEBUG_LEVEL_FULL);
         assertThat(maximal.getMemoryBytes()).isEqualTo(42);
         assertThat(maximal.getCpuTopology()).isEqualTo(CPU_TOPOLOGY_MATCH_HOST);
-        assertThat(maximal.getPayloadBinaryName()).isNull();
-        assertThat(maximal.getPayloadConfigPath()).isEqualTo("config/path");
+        assertThat(maximal.getPayloadBinaryName()).isEqualTo("binary.so");
+        assertThat(maximal.getPayloadConfigPath()).isNull();
         assertThat(maximal.isProtectedVm()).isEqualTo(isProtectedVm());
         assertThat(maximal.isEncryptedStorageEnabled()).isTrue();
         assertThat(maximal.getEncryptedStorageBytes()).isEqualTo(1_000_000);
         assertThat(maximal.isVmOutputCaptured()).isEqualTo(true);
-        assertThat(maximal.getOs()).isNull();
+        assertThat(maximal.getOs()).isEqualTo("microdroid");
 
         assertThat(minimal.isCompatibleWith(maximal)).isFalse();
         assertThat(minimal.isCompatibleWith(minimal)).isTrue();
         assertThat(maximal.isCompatibleWith(maximal)).isTrue();
 
-        VirtualMachineConfig os = minimalBuilder.setOs("microdroid_gki-android14-6.1").build();
+        VirtualMachineConfig os = maximalBuilder.setOs("microdroid_gki-android14-6.1").build();
         assertThat(os.getPayloadBinaryName()).isEqualTo("binary.so");
         assertThat(os.getOs()).isEqualTo("microdroid_gki-android14-6.1");
-        assertThat(os.isCompatibleWith(minimal)).isFalse();
+        assertThat(os.isCompatibleWith(maximal)).isFalse();
     }
 
     @Test
@@ -542,6 +548,7 @@
         // All your null are belong to me.
         assertThrows(NullPointerException.class, () -> new VirtualMachineConfig.Builder(null));
         assertThrows(NullPointerException.class, () -> builder.setApkPath(null));
+        assertThrows(NullPointerException.class, () -> builder.addExtraApk(null));
         assertThrows(NullPointerException.class, () -> builder.setPayloadConfigPath(null));
         assertThrows(NullPointerException.class, () -> builder.setPayloadBinaryName(null));
         assertThrows(NullPointerException.class, () -> builder.setVendorDiskImage(null));
@@ -607,6 +614,7 @@
                 .isTrue();
 
         // Changes that must be incompatible, since they must change the VM identity.
+        assertConfigCompatible(baseline, newBaselineBuilder().addExtraApk("foo")).isFalse();
         assertConfigCompatible(baseline, newBaselineBuilder().setDebugLevel(DEBUG_LEVEL_FULL))
                 .isFalse();
         assertConfigCompatible(baseline, newBaselineBuilder().setPayloadBinaryName("different"))
@@ -931,7 +939,34 @@
                         vm,
                         (ts, tr) -> {
                             tr.mExtraApkTestProp =
-                                    ts.readProperty("debug.microdroid.test.extra_apk");
+                                    ts.readProperty(
+                                            "debug.microdroid.test.extra_apk_build_manifest");
+                        });
+        assertThat(testResults.mExtraApkTestProp).isEqualTo("PASS");
+    }
+
+    @Test
+    @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"})
+    public void extraApkInVmConfig() throws Exception {
+        assumeSupportedDevice();
+        assumeFeatureEnabled(VirtualMachineManager.FEATURE_MULTI_TENANT);
+
+        grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
+        VirtualMachineConfig config =
+                newVmConfigBuilderWithPayloadBinary("MicrodroidTestNativeLib.so")
+                        .setMemoryBytes(minMemoryRequired())
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .addExtraApk(VM_SHARE_APP_PACKAGE_NAME)
+                        .build();
+        VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_extra_apk", config);
+
+        TestResults testResults =
+                runVmTestService(
+                        TAG,
+                        vm,
+                        (ts, tr) -> {
+                            tr.mExtraApkTestProp =
+                                    ts.readProperty("debug.microdroid.test.extra_apk_vm_share");
                         });
         assertThat(testResults.mExtraApkTestProp).isEqualTo("PASS");
     }
diff --git a/tests/testapk/src/native/testbinary.cpp b/tests/testapk/src/native/testbinary.cpp
index c9b5e3a..1a75102 100644
--- a/tests/testapk/src/native/testbinary.cpp
+++ b/tests/testapk/src/native/testbinary.cpp
@@ -349,7 +349,7 @@
     return {};
 }
 
-Result<void> verify_apk() {
+Result<void> verify_build_manifest() {
     const char* path = "/mnt/extra-apk/0/assets/build_manifest.pb";
 
     std::string str;
@@ -364,6 +364,17 @@
     return {};
 }
 
+Result<void> verify_vm_share() {
+    const char* path = "/mnt/extra-apk/0/assets/vmshareapp.txt";
+
+    std::string str;
+    if (!android::base::ReadFileToString(path, &str)) {
+        return ErrnoError() << "failed to read vmshareapp.txt";
+    }
+
+    return {};
+}
+
 } // Anonymous namespace
 
 extern "C" int AVmPayload_main() {
@@ -372,8 +383,10 @@
     // Make sure we can call into other shared libraries.
     testlib_sub();
 
-    // Extra apks may be missing; this is not a fatal error
-    report_test("extra_apk", verify_apk());
+    // Report various things that aren't always fatal - these are checked in MicrodroidTests as
+    // appropriate.
+    report_test("extra_apk_build_manifest", verify_build_manifest());
+    report_test("extra_apk_vm_share", verify_vm_share());
 
     __system_property_set("debug.microdroid.app.run", "true");
 
diff --git a/tests/vmshareapp/assets/vmshareapp.txt b/tests/vmshareapp/assets/vmshareapp.txt
new file mode 100644
index 0000000..02fdd71
--- /dev/null
+++ b/tests/vmshareapp/assets/vmshareapp.txt
@@ -0,0 +1 @@
+Marker file for the vmshareapp APK