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