Move java/framework to libs/framework-virtualization

and run formatter as requested

Bug: 352458998
Test: pass TH
Change-Id: I606f8c173728315c4fc88866809af20900520838
diff --git a/libs/framework-virtualization/Android.bp b/libs/framework-virtualization/Android.bp
new file mode 100644
index 0000000..d3a2b54
--- /dev/null
+++ b/libs/framework-virtualization/Android.bp
@@ -0,0 +1,55 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_sdk_library {
+    name: "framework-virtualization",
+
+    defaults: ["non-updatable-framework-module-defaults"],
+
+    jarjar_rules: "jarjar-rules.txt",
+
+    srcs: ["src/**/*.java"],
+    static_libs: [
+        "android.system.virtualizationservice-java",
+        "avf_aconfig_flags_java",
+        // For android.sysprop.HypervisorProperties
+        "PlatformProperties",
+    ],
+
+    apex_available: ["com.android.virt"],
+
+    permitted_packages: [
+        "android.system.virtualmachine",
+        "android.system.virtualizationservice",
+        // android.sysprop.*, renamed by jarjar
+        "com.android.system.virtualmachine.sysprop",
+    ],
+    errorprone: {
+        enabled: true,
+        javacflags: [
+            // We use @GuardedBy and we want a test failure if our locking isn't consistent with it.
+            "-Xep:GuardedBy:ERROR",
+        ],
+    },
+
+    sdk_version: "core_platform",
+    stub_only_libs: [
+        "android_module_lib_stubs_current",
+    ],
+    impl_only_libs: [
+        "framework",
+    ],
+    impl_library_visibility: [
+        "//packages/modules/Virtualization:__subpackages__",
+    ],
+    aconfig_declarations: [
+        "avf_aconfig_flags",
+    ],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+        warning_checks: [
+            "FlaggedApi",
+        ],
+    },
+}
diff --git a/libs/framework-virtualization/README.md b/libs/framework-virtualization/README.md
new file mode 100644
index 0000000..0dd7e64
--- /dev/null
+++ b/libs/framework-virtualization/README.md
@@ -0,0 +1,377 @@
+# Android Virtualization Framework API
+
+These Java APIs allow an app to configure and run a Virtual Machine running
+[Microdroid](../build/microdroid/README.md) and execute native code from the app (the
+payload) within it.
+
+There is more information on AVF [here](../README.md). To see how to package the
+payload code that is to run inside a VM, and the native API available to it, see
+the [VM Payload API](../libs/libvm_payload/README.md)
+
+The API classes are all in the
+[`android.system.virtualmachine`](src/android/system/virtualmachine) package.
+
+All of these APIs were introduced in API level 34 (Android 14). The classes may
+not exist in devices running an earlier version.
+
+Note that they are all `@SystemApi` and require the restricted
+`android.permission.MANAGE_VIRTUAL_MACHINE` permission, so they are not
+available to third party apps. In Android 14 the permission was available only to
+privileged apps; in Android 15 it is available to all preinstalled apps. On both
+versions it can also be granted to other apps via `adb shell pm grant` for
+development purposes.
+
+
+## Detecting AVF Support
+
+The simplest way to detect whether a device has support for AVF is to retrieve
+an instance of the
+[`VirtualMachineManager`](src/android/system/virtualmachine/VirtualMachineManager.java)
+class; if the result is not `null` then the device has support. You can then
+find out whether protected, non-protected VMs, or both are supported using the
+`getCapabilities()` method. Note that this code requires API level 34 or higher:
+
+```Java
+VirtualMachineManager vmm = context.getSystemService(VirtualMachineManager.class);
+if (vmm == null) {
+    // AVF is not supported.
+} else {
+    // AVF is supported.
+    int capabilities = vmm.getCapabilities();
+    if ((capabilties & CAPABILITY_PROTECTED_VM) != 0) {
+        // Protected VMs supported.
+    }
+    if ((capabilties & CAPABILITY_NON_PROTECTED_VM) != 0) {
+        // Non-Protected VMs supported.
+    }
+}
+```
+
+An alternative for detecting AVF support is to query support for the
+`android.software.virtualization_framework` system feature. This method will
+work on any API level, and return false if it is below 34:
+
+```Java
+if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_VIRTUALIZATION_FRAMEWORK)) {
+    // AVF is supported.
+}
+```
+
+You can also express a dependency on this system feature in your app's manifest
+with a
+[`<uses-feature>`](https://developer.android.com/guide/topics/manifest/uses-feature-element)
+element.
+
+
+## Starting a VM
+
+Once you have an instance of the
+[`VirtualMachineManager`](src/android/system/virtualmachine/VirtualMachineManager.java),
+a VM can be started by:
+- Specifying the desired VM configuration, using a
+  [`VirtualMachineConfig`](src/android/system/virtualmachine/VirtualMachineConfig.java)
+  builder;
+- Creating a new
+  [`VirtualMachine`](src/android/system/virtualmachine/VirtualMachine.java)
+  instance (or retrieving an existing one);
+- Registering to retrieve events from the VM by providing a
+  [`VirtualMachineCallback`](src/android/system/virtualmachine/VirtualMachineCallback.java)
+  (optional, but recommended);
+- Running the VM.
+
+A minimal example might look like this:
+
+```Java
+VirtualMachineConfig config =
+        new VirtualMachineConfig.Builder(this)
+            .setProtectedVm(true)
+            .setPayloadBinaryName("my_payload.so")
+            .build();
+
+VirtualMachine vm = vmm.getOrCreate("my vm", config);
+
+vm.setCallback(executor, new VirtualMachineCallback() {...});
+
+vm.run();
+```
+
+Here we are running a protected VM, which will execute the code in the
+`my_payload.so` file included in your APK.
+
+Information about the VM, including its configuration, is stored in files in
+your app's private data directory. The file names are based on the VM name you
+supply. So once an instance of a VM has been created it can be retrieved by name
+even if the app is restarted or the device is rebooted. Directly inspecting or
+modifying these files is not recommended.
+
+The `getOrCreate()` call will retrieve an existing VM instance if it exists (in
+which case the `config` parameter is ignored), or create a new one
+otherwise. There are also separate `get()` and `create()` methods.
+
+The `run()` method is asynchronous; it returns successfully once the VM is
+starting. You can find out when the VM is ready, or if it fails, via your
+`VirtualMachineCallback` implementation.
+
+## VM Configuration
+
+There are other things that you can specify as part of the
+[`VirtualMachineConfig`](src/android/system/virtualmachine/VirtualMachineConfig.java):
+- Whether the VM should be debuggable. A debuggable VM is not secure, but it
+  does allow access to logs from inside the VM, which can be useful for
+  troubleshooting.
+- How much memory should be available to the VM. (This is an upper bound;
+  typically memory is allocated to the VM as it is needed until the limit is
+  reached - but there is some overhead proportional to the maximum size.)
+- How many virtual CPUs the VM has.
+- How much encrypted storage the VM has.
+- The path to the installed APK containing the code to run as the VM
+  payload. (Normally you don't need this; the APK path is determined from the
+  context passed to the config builder.)
+
+## VM Life-cycle
+
+To find out the progress of the Virtual Machine once it is started you should
+implement the methods defined by
+[`VirtualMachineCallback`](src/android/system/virtualmachine/VirtualMachineCallback.java). These
+are called when the following events happen:
+- `onPayloadStarted()`: The VM payload is about to be run.
+- `onPayloadReady()`: The VM payload is running and ready to accept
+  connections. (This notification is triggered by the payload code, using the
+  [`AVmPayload_notifyPayloadReady()`](../libs/libvm_payload/include/vm_payload.h)
+  function.)
+- `onPayloadFinished()`: The VM payload has exited normally. The exit code of
+  the VM (the value returned by [`AVmPayload_main()`](../libs/libvm_payload/README.md))
+  is supplied as a parameter.
+- `onError()`: The VM failed; something went wrong. An error code and
+  human-readable message are provided which may help diagnosing the problem.
+- `onStopped()`: The VM is no longer running. This is the final notification
+  from any VM run, whether or not it was successful. You can run the VM again
+  when you want to. A reason code indicating why the VM stopped is supplied as a
+  parameter.
+
+You can also query the status of a VM at any point by calling `getStatus()` on
+the `VirtualMachine` object. This will return one of the following values:
+- `STATUS_STOPPED`: The VM is not running - either it has not yet been started,
+  or it stopped after running.
+- `STATUS_RUNNING`: The VM is running. Your payload inside the VM may not be
+  running, since the VM may be in the process of starting or stopping.
+- `STATUS_DELETED`: The VM has been deleted, e.g. by calling the `delete()`
+  method on
+  [`VirtualMachineManager`](src/android/system/virtualmachine/VirtualMachineManager.java). This
+  is irreversible - once a VM is in this state it will never leave it.
+
+Some methods on
+[`VirtualMachine`](src/android/system/virtualmachine/VirtualMachine.java) can
+only be called when the VM status is `STATUS_RUNNING` (e.g. `stop()`), and some
+can only be called when the it is `STATUS_STOPPED` (e.g. `run()`).
+
+## VM Identity and Secrets
+
+Every VM has a 32-byte secret unique to it, which is not available to the
+host. We refer to this as the VM identity.  The secret, and thus the identity,
+doesn’t normally change if the same VM is stopped and started, even after a
+reboot.
+
+In Android 14 the secret is derived, using the [Open Profile for
+DICE](https://pigweed.googlesource.com/open-dice/+/refs/heads/main/docs/android.md),
+from:
+- A device-specific randomly generated value;
+- The complete system image;
+- A per-instance salt;
+- The code running in the VM, including the bootloader, kernel, Microdroid and
+  payload;
+- Significant VM configuration options, e.g.  whether the VM is debuggable.
+
+Any change to any of these will mean a different secret is generated.  So while
+an attacker could start a similar VM with maliciously altered code, that VM will
+not have access to the same secret. An attempt to start an existing VM instance
+which doesn't derive the same secret will fail.
+
+However, this also means that if the payload code changes - for example, your
+app is updated - then this also changes the identity. An existing VM instance
+will no longer be runnable, and you will have to delete it and create a new
+instance with a new secret.
+
+The payload code is not given direct access to the VM secret, but an API is
+provided to allow deterministically deriving further secrets from it,
+e.g. encryption or signing keys. See
+[`AVmPayload_getVmInstanceSecret()`](../libs/libvm_payload/include/vm_payload.h).
+
+Some VM configuration changes are allowed that don’t affect the identity -
+e.g. changing the number of CPUs or the amount of memory allocated to the
+VM. This can be done using the `setConfig()` method on
+[`VirtualMachine`](src/android/system/virtualmachine/VirtualMachine.java).
+
+Deleting a VM (using the `delete()` method on
+[`VirtualMachineManager`](src/android/system/virtualmachine/VirtualMachineManager.java))
+and recreating it will generate a new salt, so the new VM will have a different
+secret, even if it is otherwise identical.
+
+## Communicating with a VM
+
+Once the VM payload has successfully started you will probably want to establish
+communication between it and your app.
+
+Only the app that started a VM can connect to it. The VM can accept connections
+from the app, but cannot initiate connections to other VMs or other processes in
+the host Android.
+
+### Vsock
+
+The simplest form of communication is using a socket running over the
+[vsock](https://man7.org/linux/man-pages/man7/vsock.7.html) protocol.
+
+We suggest that the VM payload should create a listening socket (using the
+standard socket API) and then trigger the `onPayloadReady()` callback; the app
+can then connect to the socket. This helps to avoid a race condition where the
+app tries to connect before the VM is listening, necessitating a retry
+mechanism.
+
+In the payload this might look like this:
+
+```C++
+#include "vm_payload.h"
+
+extern "C" int AVmPayload_main() {
+  int fd = socket(AF_VSOCK, SOCK_STREAM, 0);
+  // bind, listen
+  AVmPayload_notifyPayloadReady();
+  // accept, read/write, ...
+}
+```
+
+And, in the app, like this:
+
+```Java
+void onPayloadReady(VirtualMachine vm) {
+  ParcelFileDescriptor pfd = vm.connectVsock(port);
+  // ...
+}
+```
+
+Vsock is useful for simple communication, or transferring of bulk data. For a
+richer RPC style of communication we suggest using Binder.
+
+### Binder
+
+The use of AIDL interfaces between the VM and app is supported via Binder RPC,
+which transmits messages over an underlying vsock socket.
+
+Note that Binder RPC has some limitations compared to the kernel Binder used in
+Android - for example file descriptors can't be sent. It also isn't possible to
+send a kernel Binder interface over Binder RPC, or vice versa.
+
+There is a payload API to allow an AIDL interface to be served over a specific
+vsock port, and the VirtualMachine class provides a way to connect to the VM and
+retrieve an instance of the interface.
+
+The payload code to serve a hypothetical `IPayload` interface might look like
+this:
+
+```C++
+class PayloadImpl : public BnPayload { ... };
+
+
+extern "C" int AVmPayload_main() {
+  auto service = ndk::SharedRefBase::make<PayloadImpl>();
+  auto callback = [](void*) {
+    AVmPayload_notifyPayloadReady();
+  };
+  AVmPayload_runVsockRpcServer(service->asBinder().get(),
+    port, callback, nullptr);
+}
+
+```
+
+And then the app code to connect to it could look like this:
+
+```Java
+void onPayloadReady(VirtualMachine vm) {
+  IPayload payload =
+    Payload.Stub.asInterface(vm.connectToVsockServer(port));
+  // ...
+}
+```
+
+## Stopping a VM
+
+You can stop a VM abruptly by calling the `stop()` method on the
+[`VirtualMachine`](src/android/system/virtualmachine/VirtualMachine.java)
+instance. This is equivalent to turning off the power; the VM gets no
+opportunity to clean up at all. Any unwritten data might be lost.
+
+A better strategy might be to wait for the VM to exit cleanly (e.g. waiting for
+the `onStopped()` callback).
+
+Then you can arrange for your VM payload code to exit when it has finished its
+task (by returning from [`AVmPayload_main()`](../libs/libvm_payload/README.md), or
+calling `exit()`). Alternatively you could exit when you receive a request to do
+so from the app, e.g. via binder.
+
+When the VM payload does this you will receive an `onPayloadFinished()`
+callback, if you have installed a
+[`VirtualMachineCallback`](src/android/system/virtualmachine/VirtualMachineCallback.java),
+which includes the payload's exit code.
+
+Use of `stop()` should be reserved as a recovery mechanism - for example if the
+VM has not stopped within a reasonable time (a few seconds, say) after being
+requested to.
+
+The status of a VM will be `STATUS_STOPPED` if your `onStopped()` callback is
+invoked, or after a successful call to `stop()`. Note that your `onStopped()`
+will be called on the VM even if it ended as a result of a call to `stop()`.
+
+# Encrypted Storage
+
+When configuring a VM you can specify that it should have access to an encrypted
+storage filesystem of up to a specified size, using the
+`setEncryptedStorageBytes()` method on a
+[`VirtualMachineConfig`](src/android/system/virtualmachine/VirtualMachineConfig.java)
+builder.
+
+Inside the VM this storage is mounted at a path that can be retrieved via the
+[`AVmPayload_getEncryptedStoragePath()`](../libs/libvm_payload/include/vm_payload.h)
+function. The VM can create sub-directories and read and write files here. Any
+data written is persisted and should be available next time the VM is run. (An
+automatic sync is done when the payload exits normally.)
+
+Outside the VM the storage is persisted as a file in the app’s private data
+directory. The data is encrypted using a key derived from the VM secret, which
+is not made available outside the VM.
+
+So an attacker should not be able to decrypt the data; however, a sufficiently
+powerful attacker could delete it, wholly or partially roll it back to an
+earlier version, or modify it, corrupting the data.
+
+For more info see [README](https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Virtualization/libs/framework-virtualization/README.md)
+
+# Transferring a VM
+
+It is possible to make a copy of a VM instance. This can be used to transfer a
+VM from one app to another, which can be useful in some circumstances.
+
+This should only be done while the VM is stopped. The first step is to call
+`toDescriptor()` on the
+[`VirtualMachine`](src/android/system/virtualmachine/VirtualMachine.java)
+instance, which returns a
+[`VirtualMachineDescriptor`](src/android/system/virtualmachine/VirtualMachineDescriptor.java)
+object. This object internally contains open file descriptors to the files that
+hold the VM's state (its instance data, configuration, and encrypted storage).
+
+A `VirtualMachineDescriptor` is
+[`Parcelable`](https://developer.android.com/reference/android/os/Parcelable),
+so it can be passed to another app via a Binder call.  Any app with a
+`VirtualMachineDescriptor` can pass it, along with a new VM name, to the
+`importFromDescriptor()` method on
+[`VirtualMachineManager`](src/android/system/virtualmachine/VirtualMachineManager.java). This
+is equivalent to calling `create()` with the same name and configuration, except
+that the new VM is the same instance as the original, with the same VM secret,
+and has access to a copy of the original's encrypted storage.
+
+Once the transfer has been completed it would be reasonable to delete the
+original VM, using the `delete()` method on `VirtualMachineManager`.
+
+
+
+
+
diff --git a/libs/framework-virtualization/api/current.txt b/libs/framework-virtualization/api/current.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/libs/framework-virtualization/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/libs/framework-virtualization/api/module-lib-current.txt b/libs/framework-virtualization/api/module-lib-current.txt
new file mode 100644
index 0000000..4d59764
--- /dev/null
+++ b/libs/framework-virtualization/api/module-lib-current.txt
@@ -0,0 +1,9 @@
+// Signature format: 2.0
+package android.system.virtualmachine {
+
+  public class VirtualizationFrameworkInitializer {
+    method public static void registerServiceWrappers();
+  }
+
+}
+
diff --git a/libs/framework-virtualization/api/module-lib-removed.txt b/libs/framework-virtualization/api/module-lib-removed.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/libs/framework-virtualization/api/module-lib-removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/libs/framework-virtualization/api/removed.txt b/libs/framework-virtualization/api/removed.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/libs/framework-virtualization/api/removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/libs/framework-virtualization/api/system-current.txt b/libs/framework-virtualization/api/system-current.txt
new file mode 100644
index 0000000..d9bafa1
--- /dev/null
+++ b/libs/framework-virtualization/api/system-current.txt
@@ -0,0 +1,110 @@
+// Signature format: 2.0
+package android.system.virtualmachine {
+
+  public class VirtualMachine implements java.lang.AutoCloseable {
+    method public void clearCallback();
+    method @WorkerThread public void close();
+    method @NonNull @WorkerThread public android.os.IBinder connectToVsockServer(@IntRange(from=android.system.virtualmachine.VirtualMachine.MIN_VSOCK_PORT, to=android.system.virtualmachine.VirtualMachine.MAX_VSOCK_PORT) long) throws android.system.virtualmachine.VirtualMachineException;
+    method @NonNull @WorkerThread public android.os.ParcelFileDescriptor connectVsock(@IntRange(from=android.system.virtualmachine.VirtualMachine.MIN_VSOCK_PORT, to=android.system.virtualmachine.VirtualMachine.MAX_VSOCK_PORT) long) throws android.system.virtualmachine.VirtualMachineException;
+    method @NonNull @WorkerThread public android.system.virtualmachine.VirtualMachineConfig getConfig();
+    method @NonNull @WorkerThread public java.io.InputStream getConsoleOutput() throws android.system.virtualmachine.VirtualMachineException;
+    method @NonNull @WorkerThread public java.io.InputStream getLogOutput() throws android.system.virtualmachine.VirtualMachineException;
+    method @NonNull public String getName();
+    method @WorkerThread public int getStatus();
+    method @RequiresPermission(android.system.virtualmachine.VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION) @WorkerThread public void run() throws android.system.virtualmachine.VirtualMachineException;
+    method public void setCallback(@NonNull java.util.concurrent.Executor, @NonNull android.system.virtualmachine.VirtualMachineCallback);
+    method @NonNull @WorkerThread public android.system.virtualmachine.VirtualMachineConfig setConfig(@NonNull android.system.virtualmachine.VirtualMachineConfig) throws android.system.virtualmachine.VirtualMachineException;
+    method @WorkerThread public void stop() throws android.system.virtualmachine.VirtualMachineException;
+    method @NonNull @WorkerThread public android.system.virtualmachine.VirtualMachineDescriptor toDescriptor() throws android.system.virtualmachine.VirtualMachineException;
+    field public static final String MANAGE_VIRTUAL_MACHINE_PERMISSION = "android.permission.MANAGE_VIRTUAL_MACHINE";
+    field public static final long MAX_VSOCK_PORT = 4294967295L; // 0xffffffffL
+    field public static final long MIN_VSOCK_PORT = 1024L; // 0x400L
+    field public static final int STATUS_DELETED = 2; // 0x2
+    field public static final int STATUS_RUNNING = 1; // 0x1
+    field public static final int STATUS_STOPPED = 0; // 0x0
+    field public static final String USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION = "android.permission.USE_CUSTOM_VIRTUAL_MACHINE";
+  }
+
+  public interface VirtualMachineCallback {
+    method public void onError(@NonNull android.system.virtualmachine.VirtualMachine, int, @NonNull String);
+    method public void onPayloadFinished(@NonNull android.system.virtualmachine.VirtualMachine, int);
+    method public void onPayloadReady(@NonNull android.system.virtualmachine.VirtualMachine);
+    method public void onPayloadStarted(@NonNull android.system.virtualmachine.VirtualMachine);
+    method public void onStopped(@NonNull android.system.virtualmachine.VirtualMachine, int);
+    field public static final int ERROR_PAYLOAD_CHANGED = 2; // 0x2
+    field public static final int ERROR_PAYLOAD_INVALID_CONFIG = 3; // 0x3
+    field public static final int ERROR_PAYLOAD_VERIFICATION_FAILED = 1; // 0x1
+    field public static final int ERROR_UNKNOWN = 0; // 0x0
+    field public static final int STOP_REASON_BOOTLOADER_INSTANCE_IMAGE_CHANGED = 10; // 0xa
+    field public static final int STOP_REASON_BOOTLOADER_PUBLIC_KEY_MISMATCH = 9; // 0x9
+    field public static final int STOP_REASON_CRASH = 6; // 0x6
+    field public static final int STOP_REASON_HANGUP = 16; // 0x10
+    field public static final int STOP_REASON_INFRASTRUCTURE_ERROR = 0; // 0x0
+    field public static final int STOP_REASON_KILLED = 1; // 0x1
+    field public static final int STOP_REASON_MICRODROID_FAILED_TO_CONNECT_TO_VIRTUALIZATION_SERVICE = 11; // 0xb
+    field public static final int STOP_REASON_MICRODROID_INVALID_PAYLOAD_CONFIG = 14; // 0xe
+    field public static final int STOP_REASON_MICRODROID_PAYLOAD_HAS_CHANGED = 12; // 0xc
+    field public static final int STOP_REASON_MICRODROID_PAYLOAD_VERIFICATION_FAILED = 13; // 0xd
+    field public static final int STOP_REASON_MICRODROID_UNKNOWN_RUNTIME_ERROR = 15; // 0xf
+    field public static final int STOP_REASON_PVM_FIRMWARE_INSTANCE_IMAGE_CHANGED = 8; // 0x8
+    field public static final int STOP_REASON_PVM_FIRMWARE_PUBLIC_KEY_MISMATCH = 7; // 0x7
+    field public static final int STOP_REASON_REBOOT = 5; // 0x5
+    field public static final int STOP_REASON_SHUTDOWN = 3; // 0x3
+    field public static final int STOP_REASON_START_FAILED = 4; // 0x4
+    field public static final int STOP_REASON_UNKNOWN = 2; // 0x2
+    field public static final int STOP_REASON_VIRTUALIZATION_SERVICE_DIED = -1; // 0xffffffff
+  }
+
+  public final class VirtualMachineConfig {
+    method @Nullable public String getApkPath();
+    method public int getCpuTopology();
+    method public int getDebugLevel();
+    method @IntRange(from=0) public long getEncryptedStorageBytes();
+    method @IntRange(from=0) public long getMemoryBytes();
+    method @Nullable public String getPayloadBinaryName();
+    method public boolean isCompatibleWith(@NonNull android.system.virtualmachine.VirtualMachineConfig);
+    method public boolean isEncryptedStorageEnabled();
+    method public boolean isProtectedVm();
+    method public boolean isVmOutputCaptured();
+    field public static final int CPU_TOPOLOGY_MATCH_HOST = 1; // 0x1
+    field public static final int CPU_TOPOLOGY_ONE_CPU = 0; // 0x0
+    field public static final int DEBUG_LEVEL_FULL = 1; // 0x1
+    field public static final int DEBUG_LEVEL_NONE = 0; // 0x0
+  }
+
+  public static final class VirtualMachineConfig.Builder {
+    ctor public VirtualMachineConfig.Builder(@NonNull android.content.Context);
+    method @NonNull public android.system.virtualmachine.VirtualMachineConfig build();
+    method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setApkPath(@NonNull String);
+    method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setCpuTopology(int);
+    method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setDebugLevel(int);
+    method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setEncryptedStorageBytes(@IntRange(from=1) long);
+    method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setMemoryBytes(@IntRange(from=1) long);
+    method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setPayloadBinaryName(@NonNull String);
+    method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setProtectedVm(boolean);
+    method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setVmOutputCaptured(boolean);
+  }
+
+  public final class VirtualMachineDescriptor implements java.lang.AutoCloseable android.os.Parcelable {
+    method public void close();
+    method public int describeContents();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.system.virtualmachine.VirtualMachineDescriptor> CREATOR;
+  }
+
+  public class VirtualMachineException extends java.lang.Exception {
+  }
+
+  public class VirtualMachineManager {
+    method @NonNull @RequiresPermission(android.system.virtualmachine.VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION) @WorkerThread public android.system.virtualmachine.VirtualMachine create(@NonNull String, @NonNull android.system.virtualmachine.VirtualMachineConfig) throws android.system.virtualmachine.VirtualMachineException;
+    method @WorkerThread public void delete(@NonNull String) throws android.system.virtualmachine.VirtualMachineException;
+    method @Nullable @WorkerThread public android.system.virtualmachine.VirtualMachine get(@NonNull String) throws android.system.virtualmachine.VirtualMachineException;
+    method public int getCapabilities();
+    method @NonNull @WorkerThread public android.system.virtualmachine.VirtualMachine getOrCreate(@NonNull String, @NonNull android.system.virtualmachine.VirtualMachineConfig) throws android.system.virtualmachine.VirtualMachineException;
+    method @NonNull @WorkerThread public android.system.virtualmachine.VirtualMachine importFromDescriptor(@NonNull String, @NonNull android.system.virtualmachine.VirtualMachineDescriptor) throws android.system.virtualmachine.VirtualMachineException;
+    field public static final int CAPABILITY_NON_PROTECTED_VM = 2; // 0x2
+    field public static final int CAPABILITY_PROTECTED_VM = 1; // 0x1
+  }
+
+}
+
diff --git a/libs/framework-virtualization/api/system-removed.txt b/libs/framework-virtualization/api/system-removed.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/libs/framework-virtualization/api/system-removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/libs/framework-virtualization/api/test-current.txt b/libs/framework-virtualization/api/test-current.txt
new file mode 100644
index 0000000..7e8da26
--- /dev/null
+++ b/libs/framework-virtualization/api/test-current.txt
@@ -0,0 +1,40 @@
+// Signature format: 2.0
+package android.system.virtualmachine {
+
+  public class VirtualMachine implements java.lang.AutoCloseable {
+    method @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") @RequiresPermission(android.system.virtualmachine.VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION) public void enableTestAttestation() throws android.system.virtualmachine.VirtualMachineException;
+    method @NonNull @WorkerThread public java.io.OutputStream getConsoleInput() throws android.system.virtualmachine.VirtualMachineException;
+    method @NonNull public java.io.File getRootDir();
+  }
+
+  public final class VirtualMachineConfig {
+    method @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") @NonNull public java.util.List<java.lang.String> getExtraApks();
+    method @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") @NonNull public String getOs();
+    method @Nullable public String getPayloadConfigPath();
+    method public boolean isVmConsoleInputSupported();
+    field @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") public static final String MICRODROID = "microdroid";
+  }
+
+  public static final class VirtualMachineConfig.Builder {
+    method @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder addExtraApk(@NonNull String);
+    method @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") @NonNull @RequiresPermission(android.system.virtualmachine.VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION) 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("com.android.system.virtualmachine.flags.avf_v_test_apis") @NonNull @RequiresPermission(android.system.virtualmachine.VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION) public android.system.virtualmachine.VirtualMachineConfig.Builder setVendorDiskImage(@NonNull java.io.File);
+    method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setVmConsoleInputSupported(boolean);
+  }
+
+  public class VirtualMachineManager {
+    method @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") @NonNull public java.util.List<java.lang.String> getSupportedOSList() throws android.system.virtualmachine.VirtualMachineException;
+    method @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") @RequiresPermission(android.system.virtualmachine.VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION) public boolean isFeatureEnabled(String) throws android.system.virtualmachine.VirtualMachineException;
+    method @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") @RequiresPermission(android.system.virtualmachine.VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION) public boolean isRemoteAttestationSupported() throws android.system.virtualmachine.VirtualMachineException;
+    method @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") @RequiresPermission(android.system.virtualmachine.VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION) public boolean isUpdatableVmSupported() throws android.system.virtualmachine.VirtualMachineException;
+    field @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") public static final String FEATURE_DICE_CHANGES = "com.android.kvm.DICE_CHANGES";
+    field @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") public static final String FEATURE_LLPVM_CHANGES = "com.android.kvm.LLPVM_CHANGES";
+    field @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") public static final String FEATURE_MULTI_TENANT = "com.android.kvm.MULTI_TENANT";
+    field public static final String FEATURE_NETWORK = "com.android.kvm.NETWORK";
+    field @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") public static final String FEATURE_REMOTE_ATTESTATION = "com.android.kvm.REMOTE_ATTESTATION";
+    field @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") public static final String FEATURE_VENDOR_MODULES = "com.android.kvm.VENDOR_MODULES";
+  }
+
+}
+
diff --git a/libs/framework-virtualization/api/test-removed.txt b/libs/framework-virtualization/api/test-removed.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/libs/framework-virtualization/api/test-removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/libs/framework-virtualization/jarjar-rules.txt b/libs/framework-virtualization/jarjar-rules.txt
new file mode 100644
index 0000000..726f9aa
--- /dev/null
+++ b/libs/framework-virtualization/jarjar-rules.txt
@@ -0,0 +1,10 @@
+# Rules for the android.system.virtualmachine java_sdk_library.
+
+# Keep the API surface, most of it is accessible from VirtualMachineManager
+keep android.system.virtualmachine.VirtualMachineManager
+# VirtualizationModuleFrameworkInitializer is not accessible from
+# VirtualMachineManager, we need to explicitly keep it.
+keep android.system.virtualmachine.VirtualizationFrameworkInitializer
+
+# We statically link PlatformProperties, rename to avoid clashes.
+rule android.sysprop.** com.android.system.virtualmachine.sysprop.@1
diff --git a/libs/framework-virtualization/lint-baseline.xml b/libs/framework-virtualization/lint-baseline.xml
new file mode 100644
index 0000000..a77a80b
--- /dev/null
+++ b/libs/framework-virtualization/lint-baseline.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.4.0-alpha08" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha08">
+
+    <issue
+        id="FlaggedApi"
+        message="Method `getExtraApks()` is a flagged API and should be inside an `if (Flags.avfVTestApis())` check (or annotate the surrounding method `createVirtualMachineConfigForAppFrom` with `@FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS) to transfer requirement to caller`)"
+        errorLine1="        if (!vmConfig.getExtraApks().isEmpty()) {"
+        errorLine2="             ~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Virtualization/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachine.java"
+            line="1105"
+            column="14"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Method `getExtraApks()` is a flagged API and should be inside an `if (Flags.avfVTestApis())` check (or annotate the surrounding method `setupExtraApks` with `@FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS) to transfer requirement to caller`)"
+        errorLine1="        List&lt;String> extraApks = config.getExtraApks();"
+        errorLine2="                                 ~~~~~~~~~~~~~~~~~~~~~">
+        <location
+		file="packages/modules/Virtualization/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachine.java"
+            line="1730"
+            column="34"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Method `setVendorDiskImage()` is a flagged API and should be inside an `if (Flags.avfVTestApis())` check (or annotate the surrounding method `fromPersistableBundle` with `@FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS) to transfer requirement to caller`)"
+        errorLine1="            builder.setVendorDiskImage(new File(vendorDiskImagePath));"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+		file="packages/modules/Virtualization/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineConfig.java"
+            line="367"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Method `setOs()` is a flagged API and should be inside an `if (Flags.avfVTestApis())` check (or annotate the surrounding method `fromPersistableBundle` with `@FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS) to transfer requirement to caller`)"
+        errorLine1="        builder.setOs(b.getString(KEY_OS));"
+        errorLine2="        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+		file="packages/modules/Virtualization/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineConfig.java"
+            line="370"
+            column="9"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Method `addExtraApk()` is a flagged API and should be inside an `if (Flags.avfVTestApis())` check (or annotate the surrounding method `fromPersistableBundle` with `@FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS) to transfer requirement to caller`)"
+        errorLine1="                builder.addExtraApk(extraApk);"
+        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+		file="packages/modules/Virtualization/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineConfig.java"
+            line="375"
+            column="17"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Field `MICRODROID` is a flagged API and should be inside an `if (Flags.avfVTestApis())` check (or annotate the surrounding method `?` with `@FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS) to transfer requirement to caller`)"
+        errorLine1="        @OsName private final String DEFAULT_OS = MICRODROID;"
+        errorLine2="                                                  ~~~~~~~~~~">
+        <location
+		file="packages/modules/Virtualization/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineConfig.java"
+            line="855"
+            column="51"/>
+    </issue>
+
+</issues>
diff --git a/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachine.java b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachine.java
new file mode 100644
index 0000000..3b16a8a
--- /dev/null
+++ b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachine.java
@@ -0,0 +1,2407 @@
+/*
+ * 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.system.virtualmachine;
+
+import static android.os.ParcelFileDescriptor.AutoCloseInputStream;
+import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
+import static android.os.ParcelFileDescriptor.MODE_READ_WRITE;
+import static android.system.virtualmachine.VirtualMachineCallback.ERROR_PAYLOAD_CHANGED;
+import static android.system.virtualmachine.VirtualMachineCallback.ERROR_PAYLOAD_INVALID_CONFIG;
+import static android.system.virtualmachine.VirtualMachineCallback.ERROR_PAYLOAD_VERIFICATION_FAILED;
+import static android.system.virtualmachine.VirtualMachineCallback.ERROR_UNKNOWN;
+import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_CRASH;
+import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_HANGUP;
+import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_INFRASTRUCTURE_ERROR;
+import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_KILLED;
+import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_MICRODROID_FAILED_TO_CONNECT_TO_VIRTUALIZATION_SERVICE;
+import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_MICRODROID_INVALID_PAYLOAD_CONFIG;
+import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_MICRODROID_PAYLOAD_HAS_CHANGED;
+import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_MICRODROID_PAYLOAD_VERIFICATION_FAILED;
+import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_MICRODROID_UNKNOWN_RUNTIME_ERROR;
+import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_PVM_FIRMWARE_INSTANCE_IMAGE_CHANGED;
+import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_PVM_FIRMWARE_PUBLIC_KEY_MISMATCH;
+import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_REBOOT;
+import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_SHUTDOWN;
+import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_START_FAILED;
+import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_UNKNOWN;
+import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_VIRTUALIZATION_SERVICE_DIED;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+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;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.system.ErrnoException;
+import android.system.OsConstants;
+import android.system.virtualizationcommon.DeathReason;
+import android.system.virtualizationcommon.ErrorCode;
+import android.system.virtualizationservice.IVirtualMachine;
+import android.system.virtualizationservice.IVirtualMachineCallback;
+import android.system.virtualizationservice.IVirtualizationService;
+import android.system.virtualizationservice.InputDevice;
+import android.system.virtualizationservice.PartitionType;
+import android.system.virtualizationservice.VirtualMachineAppConfig;
+import android.system.virtualizationservice.VirtualMachineRawConfig;
+import android.system.virtualizationservice.VirtualMachineState;
+import android.util.JsonReader;
+import android.util.Log;
+import android.util.Pair;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.system.virtualmachine.flags.Flags;
+
+import libcore.io.IoBridge;
+import libcore.io.IoUtils;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.channels.FileChannel;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.FileAlreadyExistsException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Consumer;
+import java.util.zip.ZipFile;
+
+/**
+ * Represents an VM instance, with its own configuration and state. Instances are persistent and are
+ * created or retrieved via {@link VirtualMachineManager}.
+ *
+ * <p>The {@link #run} method actually starts up the VM and allows the payload code to execute. It
+ * will continue until it exits or {@link #stop} is called. Updates on the state of the VM can be
+ * received using {@link #setCallback}. The app can communicate with the VM using {@link
+ * #connectToVsockServer} or {@link #connectVsock}.
+ *
+ * <p>The payload code running inside the VM has access to a set of native APIs; see the <a
+ * href="https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Virtualization/libs/libvm_payload/README.md">README
+ * file</a> for details.
+ *
+ * <p>Each VM has a unique secret, computed from the APK that contains the code running in it, the
+ * VM configuration, and a random per-instance salt. The secret can be accessed by the payload code
+ * running inside the VM (using {@code AVmPayload_getVmInstanceSecret}) but is not made available
+ * outside it.
+ *
+ * @hide
+ */
+@SystemApi
+public class VirtualMachine implements AutoCloseable {
+    /** The permission needed to create or run a virtual machine. */
+    public static final String MANAGE_VIRTUAL_MACHINE_PERMISSION =
+            "android.permission.MANAGE_VIRTUAL_MACHINE";
+
+    /**
+     * The permission needed to create a virtual machine with more advanced configuration options.
+     */
+    public static final String USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION =
+            "android.permission.USE_CUSTOM_VIRTUAL_MACHINE";
+
+    /**
+     * The lowest port number that can be used to communicate with the virtual machine payload.
+     *
+     * @see #connectToVsockServer
+     * @see #connectVsock
+     */
+    @SuppressLint("MinMaxConstant") // Won't change: see man 7 vsock.
+    public static final long MIN_VSOCK_PORT = 1024;
+
+    /**
+     * The highest port number that can be used to communicate with the virtual machine payload.
+     *
+     * @see #connectToVsockServer
+     * @see #connectVsock
+     */
+    @SuppressLint("MinMaxConstant") // Won't change: see man 7 vsock.
+    public static final long MAX_VSOCK_PORT = (1L << 32) - 1;
+
+    private ParcelFileDescriptor mTouchSock;
+    private ParcelFileDescriptor mKeySock;
+    private ParcelFileDescriptor mMouseSock;
+    private ParcelFileDescriptor mSwitchesSock;
+    private ParcelFileDescriptor mTrackpadSock;
+
+    private enum InputEventType {
+        TOUCH,
+        MOUSE,
+        TRACKPAD
+    }
+
+    private BlockingQueue<Pair<InputEventType, MotionEvent>> mInputEventQueue =
+            new LinkedBlockingQueue<>();
+
+    /**
+     * Status of a virtual machine
+     *
+     * @hide
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(
+            prefix = "STATUS_",
+            value = {STATUS_STOPPED, STATUS_RUNNING, STATUS_DELETED})
+    public @interface Status {}
+
+    /** The virtual machine has just been created, or {@link #stop} was called on it. */
+    public static final int STATUS_STOPPED = 0;
+
+    /** The virtual machine is running. */
+    public static final int STATUS_RUNNING = 1;
+
+    /**
+     * The virtual machine has been deleted. This is an irreversible state. Once a virtual machine
+     * is deleted all its secrets are permanently lost, and it cannot be run. A new virtual machine
+     * with the same name and config may be created, with new and different secrets.
+     */
+    public static final int STATUS_DELETED = 2;
+
+    private static final String TAG = "VirtualMachine";
+
+    /** Name of the directory under the files directory where all VMs created for the app exist. */
+    private static final String VM_DIR = "vm";
+
+    /** Name of the persisted config file for a VM. */
+    private static final String CONFIG_FILE = "config.xml";
+
+    /** Name of the instance image file for a VM. (Not implemented) */
+    private static final String INSTANCE_IMAGE_FILE = "instance.img";
+
+    /** Name of the file for a VM containing Id. */
+    private static final String INSTANCE_ID_FILE = "instance_id";
+
+    /** Name of the idsig file for a VM */
+    private static final String IDSIG_FILE = "idsig";
+
+    /** Name of the idsig files for extra APKs. */
+    private static final String EXTRA_IDSIG_FILE_PREFIX = "extra_idsig_";
+
+    /** Size of the instance image. 10 MB. */
+    private static final long INSTANCE_FILE_SIZE = 10 * 1024 * 1024;
+
+    /** Name of the file backing the encrypted storage */
+    private static final String ENCRYPTED_STORE_FILE = "storage.img";
+
+    /** The package which owns this VM. */
+    @NonNull private final String mPackageName;
+
+    /** Name of this VM within the package. The name should be unique in the package. */
+    @NonNull private final String mName;
+
+    /** Path to the directory containing all the files related to this VM. */
+    @NonNull private final File mVmRootPath;
+
+    /**
+     * Path to the config file for this VM. The config file is where the configuration is persisted.
+     */
+    @NonNull private final File mConfigFilePath;
+
+    /** Path to the instance image file for this VM. */
+    @NonNull private final File mInstanceFilePath;
+
+    /** Path to the idsig file for this VM. */
+    @NonNull private final File mIdsigFilePath;
+
+    /** File that backs the encrypted storage - Will be null if not enabled. */
+    @Nullable private final File mEncryptedStoreFilePath;
+
+    /** File that contains the Id. This is NULL iff FEATURE_LLPVM is disabled */
+    @Nullable private final File mInstanceIdPath;
+
+    /**
+     * Unmodifiable list of extra apks. Apks are specified by the vm config, and corresponding
+     * idsigs are to be generated.
+     */
+    @NonNull private final List<ExtraApkSpec> mExtraApks;
+
+    private class MemoryManagementCallbacks implements ComponentCallbacks2 {
+        @Override
+        public void onConfigurationChanged(@NonNull Configuration newConfig) {}
+
+        @Override
+        public void onLowMemory() {}
+
+        @Override
+        public void onTrimMemory(int level) {
+            int percent;
+
+            switch (level) {
+                case ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL:
+                    percent = 50;
+                    break;
+                case ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW:
+                    percent = 30;
+                    break;
+                case ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE:
+                    percent = 10;
+                    break;
+                case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND:
+                case ComponentCallbacks2.TRIM_MEMORY_MODERATE:
+                case ComponentCallbacks2.TRIM_MEMORY_COMPLETE:
+                    /* Release as much memory as we can. The app is on the LMKD LRU kill list. */
+                    percent = 50;
+                    break;
+                default:
+                    /* Treat unrecognised messages as generic low-memory warnings. */
+                    percent = 30;
+                    break;
+            }
+
+            synchronized (mLock) {
+                try {
+                    if (mVirtualMachine != null) {
+                        long bytes = mConfig.getMemoryBytes();
+                        mVirtualMachine.setMemoryBalloon(bytes * percent / 100);
+                    }
+                } catch (Exception e) {
+                    /* Caller doesn't want our exceptions. Log them instead. */
+                    Log.w(TAG, "TrimMemory failed: ", e);
+                }
+            }
+        }
+    }
+
+    /** Running instance of virtmgr that hosts VirtualizationService for this VM. */
+    @NonNull private final VirtualizationService mVirtualizationService;
+
+    private final MemoryManagementCallbacks mMemoryManagementCallbacks;
+
+    @NonNull private final Context mContext;
+
+    // A note on lock ordering:
+    // You can take mLock while holding VirtualMachineManager.sCreateLock, but not vice versa.
+    // We never take any other lock while holding mCallbackLock; therefore you can
+    // take mCallbackLock while holding any other lock.
+
+    /** Lock protecting our mutable state (other than callbacks). */
+    private final Object mLock = new Object();
+
+    /** Lock protecting callbacks. */
+    private final Object mCallbackLock = new Object();
+
+    private final boolean mVmOutputCaptured;
+
+    private final boolean mVmConsoleInputSupported;
+
+    private final boolean mConnectVmConsole;
+
+    private final Executor mConsoleExecutor = Executors.newSingleThreadExecutor();
+
+    private ExecutorService mInputEventExecutor;
+
+    /** The configuration that is currently associated with this VM. */
+    @GuardedBy("mLock")
+    @NonNull
+    private VirtualMachineConfig mConfig;
+
+    /** Handle to the "running" VM. */
+    @GuardedBy("mLock")
+    @Nullable
+    private IVirtualMachine mVirtualMachine;
+
+    @GuardedBy("mLock")
+    @Nullable
+    private ParcelFileDescriptor mConsoleOutReader;
+
+    @GuardedBy("mLock")
+    @Nullable
+    private ParcelFileDescriptor mConsoleOutWriter;
+
+    @GuardedBy("mLock")
+    @Nullable
+    private ParcelFileDescriptor mConsoleInReader;
+
+    @GuardedBy("mLock")
+    @Nullable
+    private ParcelFileDescriptor mConsoleInWriter;
+
+    @GuardedBy("mLock")
+    @Nullable
+    private ParcelFileDescriptor mTeeConsoleOutReader;
+
+    @GuardedBy("mLock")
+    @Nullable
+    private ParcelFileDescriptor mTeeConsoleOutWriter;
+
+    @GuardedBy("mLock")
+    @Nullable
+    private ParcelFileDescriptor mPtyFd;
+
+    @GuardedBy("mLock")
+    @Nullable
+    private ParcelFileDescriptor mPtsFd;
+
+    @GuardedBy("mLock")
+    @Nullable
+    private String mPtsName;
+
+    @GuardedBy("mLock")
+    @Nullable
+    private ParcelFileDescriptor mLogReader;
+
+    @GuardedBy("mLock")
+    @Nullable
+    private ParcelFileDescriptor mLogWriter;
+
+    @GuardedBy("mLock")
+    private boolean mWasDeleted = false;
+
+    /** The registered callback */
+    @GuardedBy("mCallbackLock")
+    @Nullable
+    private VirtualMachineCallback mCallback;
+
+    /** The executor on which the callback will be executed */
+    @GuardedBy("mCallbackLock")
+    @Nullable
+    private Executor mCallbackExecutor;
+
+    private static class ExtraApkSpec {
+        public final File apk;
+        public final File idsig;
+
+        ExtraApkSpec(File apk, File idsig) {
+            this.apk = apk;
+            this.idsig = idsig;
+        }
+    }
+
+    static {
+        System.loadLibrary("virtualmachine_jni");
+    }
+
+    private VirtualMachine(
+            @NonNull Context context,
+            @NonNull String name,
+            @NonNull VirtualMachineConfig config,
+            @NonNull VirtualizationService service)
+            throws VirtualMachineException {
+        mPackageName = context.getPackageName();
+        mName = requireNonNull(name, "Name must not be null");
+        mConfig = requireNonNull(config, "Config must not be null");
+        mVirtualizationService = service;
+
+        File thisVmDir = getVmDir(context, mName);
+        mVmRootPath = thisVmDir;
+        mConfigFilePath = new File(thisVmDir, CONFIG_FILE);
+        try {
+            mInstanceIdPath =
+                    (mVirtualizationService
+                                    .getBinder()
+                                    .isFeatureEnabled(IVirtualizationService.FEATURE_LLPVM_CHANGES))
+                            ? new File(thisVmDir, INSTANCE_ID_FILE)
+                            : null;
+        } catch (RemoteException e) {
+            throw e.rethrowAsRuntimeException();
+        }
+        mInstanceFilePath = new File(thisVmDir, INSTANCE_IMAGE_FILE);
+        mIdsigFilePath = new File(thisVmDir, IDSIG_FILE);
+        mExtraApks = setupExtraApks(context, config, thisVmDir);
+        mContext = context;
+        mEncryptedStoreFilePath =
+                (config.isEncryptedStorageEnabled())
+                        ? new File(thisVmDir, ENCRYPTED_STORE_FILE)
+                        : null;
+
+        mVmOutputCaptured = config.isVmOutputCaptured();
+        mVmConsoleInputSupported = config.isVmConsoleInputSupported();
+        mConnectVmConsole = config.isConnectVmConsole();
+
+        VirtualMachineCustomImageConfig customImageConfig;
+        customImageConfig = config.getCustomImageConfig();
+        if (customImageConfig == null || customImageConfig.useAutoMemoryBalloon()) {
+            mMemoryManagementCallbacks = new MemoryManagementCallbacks();
+        } else {
+            mMemoryManagementCallbacks = null;
+        }
+    }
+
+    /**
+     * Creates a virtual machine from an {@link VirtualMachineDescriptor} object and associates it
+     * with the given name.
+     *
+     * <p>The new virtual machine will be in the same state as the descriptor indicates.
+     *
+     * <p>Once a virtual machine is imported it is persisted until it is deleted by calling {@link
+     * #delete}. The imported virtual machine is in {@link #STATUS_STOPPED} state. To run the VM,
+     * call {@link #run}.
+     */
+    @GuardedBy("VirtualMachineManager.sCreateLock")
+    @NonNull
+    static VirtualMachine fromDescriptor(
+            @NonNull Context context,
+            @NonNull String name,
+            @NonNull VirtualMachineDescriptor vmDescriptor)
+            throws VirtualMachineException {
+        File vmDir = createVmDir(context, name);
+        try {
+            VirtualMachine vm;
+            try (vmDescriptor) {
+                VirtualMachineConfig config = VirtualMachineConfig.from(vmDescriptor.getConfigFd());
+                vm = new VirtualMachine(context, name, config, VirtualizationService.getInstance());
+                config.serialize(vm.mConfigFilePath);
+                try {
+                    vm.mInstanceFilePath.createNewFile();
+                } catch (IOException e) {
+                    throw new VirtualMachineException("failed to create instance image", e);
+                }
+                vm.importInstanceFrom(vmDescriptor.getInstanceImgFd());
+
+                if (vmDescriptor.getEncryptedStoreFd() != null) {
+                    try {
+                        vm.mEncryptedStoreFilePath.createNewFile();
+                    } catch (IOException e) {
+                        throw new VirtualMachineException(
+                                "failed to create encrypted storage image", e);
+                    }
+                    vm.importEncryptedStoreFrom(vmDescriptor.getEncryptedStoreFd());
+                }
+                if (vm.mInstanceIdPath != null) {
+                    vm.importInstanceIdFrom(vmDescriptor.getInstanceIdFd());
+                    vm.claimInstance();
+                }
+            }
+            return vm;
+        } catch (VirtualMachineException | RuntimeException e) {
+            // If anything goes wrong, delete any files created so far and the VM's directory
+            try {
+                deleteRecursively(vmDir);
+            } catch (Exception innerException) {
+                e.addSuppressed(innerException);
+            }
+            throw e;
+        }
+    }
+
+    /**
+     * Creates a virtual machine with the given name and config. Once a virtual machine is created
+     * it is persisted until it is deleted by calling {@link #delete}. The created virtual machine
+     * is in {@link #STATUS_STOPPED} state. To run the VM, call {@link #run}.
+     */
+    @GuardedBy("VirtualMachineManager.sCreateLock")
+    @NonNull
+    static VirtualMachine create(
+            @NonNull Context context, @NonNull String name, @NonNull VirtualMachineConfig config)
+            throws VirtualMachineException {
+        File vmDir = createVmDir(context, name);
+
+        try {
+            VirtualMachine vm =
+                    new VirtualMachine(context, name, config, VirtualizationService.getInstance());
+            config.serialize(vm.mConfigFilePath);
+            try {
+                vm.mInstanceFilePath.createNewFile();
+            } catch (IOException e) {
+                throw new VirtualMachineException("failed to create instance image", e);
+            }
+            if (config.isEncryptedStorageEnabled()) {
+                try {
+                    vm.mEncryptedStoreFilePath.createNewFile();
+                } catch (IOException e) {
+                    throw new VirtualMachineException(
+                            "failed to create encrypted storage image", e);
+                }
+            }
+
+            IVirtualizationService service = vm.mVirtualizationService.getBinder();
+
+            if (vm.mInstanceIdPath != null) {
+                try (FileOutputStream stream = new FileOutputStream(vm.mInstanceIdPath)) {
+                    byte[] id = service.allocateInstanceId();
+                    stream.write(id);
+                } catch (FileNotFoundException e) {
+                    throw new VirtualMachineException("instance_id file missing", e);
+                } catch (IOException e) {
+                    throw new VirtualMachineException("failed to persist instance_id", e);
+                } catch (RemoteException e) {
+                    throw e.rethrowAsRuntimeException();
+                } catch (ServiceSpecificException | IllegalArgumentException e) {
+                    throw new VirtualMachineException("failed to create instance_id", e);
+                }
+            }
+
+            try {
+                service.initializeWritablePartition(
+                        ParcelFileDescriptor.open(vm.mInstanceFilePath, MODE_READ_WRITE),
+                        INSTANCE_FILE_SIZE,
+                        PartitionType.ANDROID_VM_INSTANCE);
+            } catch (FileNotFoundException e) {
+                throw new VirtualMachineException("instance image missing", e);
+            } catch (RemoteException e) {
+                throw e.rethrowAsRuntimeException();
+            } catch (ServiceSpecificException | IllegalArgumentException e) {
+                throw new VirtualMachineException("failed to create instance partition", e);
+            }
+
+            if (config.isEncryptedStorageEnabled()) {
+                try {
+                    service.initializeWritablePartition(
+                            ParcelFileDescriptor.open(vm.mEncryptedStoreFilePath, MODE_READ_WRITE),
+                            config.getEncryptedStorageBytes(),
+                            PartitionType.ENCRYPTEDSTORE);
+                } catch (FileNotFoundException e) {
+                    throw new VirtualMachineException("encrypted storage image missing", e);
+                } catch (RemoteException e) {
+                    throw e.rethrowAsRuntimeException();
+                } catch (ServiceSpecificException | IllegalArgumentException e) {
+                    throw new VirtualMachineException(
+                            "failed to create encrypted storage partition", e);
+                }
+            }
+            return vm;
+        } catch (VirtualMachineException | RuntimeException e) {
+            // If anything goes wrong, delete any files created so far and the VM's directory
+            try {
+                vmInstanceCleanup(context, name);
+            } catch (Exception innerException) {
+                e.addSuppressed(innerException);
+            }
+            throw e;
+        }
+    }
+
+    /** Loads a virtual machine that is already created before. */
+    @GuardedBy("VirtualMachineManager.sCreateLock")
+    @Nullable
+    static VirtualMachine load(@NonNull Context context, @NonNull String name)
+            throws VirtualMachineException {
+        File thisVmDir = getVmDir(context, name);
+        if (!thisVmDir.exists()) {
+            // The VM doesn't exist.
+            return null;
+        }
+        File configFilePath = new File(thisVmDir, CONFIG_FILE);
+        VirtualMachineConfig config = VirtualMachineConfig.from(configFilePath);
+        VirtualMachine vm =
+                new VirtualMachine(context, name, config, VirtualizationService.getInstance());
+
+        if (vm.mInstanceIdPath != null && !vm.mInstanceIdPath.exists()) {
+            throw new VirtualMachineException("instance_id file missing");
+        }
+        if (!vm.mInstanceFilePath.exists()) {
+            throw new VirtualMachineException("instance image missing");
+        }
+        if (config.isEncryptedStorageEnabled() && !vm.mEncryptedStoreFilePath.exists()) {
+            throw new VirtualMachineException("Storage image missing");
+        }
+        return vm;
+    }
+
+    @GuardedBy("VirtualMachineManager.sCreateLock")
+    void delete(Context context, String name) throws VirtualMachineException {
+        synchronized (mLock) {
+            checkStopped();
+            // Once we explicitly delete a VM it must remain permanently in the deleted state;
+            // if a new VM is created with the same name (and files) that's unrelated.
+            mWasDeleted = true;
+        }
+        vmInstanceCleanup(context, name);
+    }
+
+    // Delete the full VM directory and notify VirtualizationService to remove this
+    // VM instance for housekeeping.
+    @GuardedBy("VirtualMachineManager.sCreateLock")
+    static void vmInstanceCleanup(Context context, String name) throws VirtualMachineException {
+        File vmDir = getVmDir(context, name);
+        notifyInstanceRemoval(vmDir, VirtualizationService.getInstance());
+        try {
+            deleteRecursively(vmDir);
+        } catch (IOException e) {
+            throw new VirtualMachineException(e);
+        }
+    }
+
+    private static void notifyInstanceRemoval(
+            File vmDirectory, @NonNull VirtualizationService service) {
+        File instanceIdFile = new File(vmDirectory, INSTANCE_ID_FILE);
+        try {
+            byte[] instanceId = Files.readAllBytes(instanceIdFile.toPath());
+            service.getBinder().removeVmInstance(instanceId);
+        } catch (Exception e) {
+            // Deliberately ignoring error in removing VM instance. This potentially leads to
+            // unaccounted instances in the VS' database. But, nothing much can be done by caller.
+            Log.w(TAG, "Failed to notify VS to remove the VM instance", e);
+        }
+    }
+
+    // Claim the instance. This notifies the global VS about the ownership of this
+    // instance_id for housekeeping purpose.
+    void claimInstance() throws VirtualMachineException {
+        if (mInstanceIdPath != null) {
+            IVirtualizationService service = mVirtualizationService.getBinder();
+            try {
+                byte[] instanceId = Files.readAllBytes(mInstanceIdPath.toPath());
+                service.claimVmInstance(instanceId);
+            } catch (IOException e) {
+                throw new VirtualMachineException("failed to read instance_id", e);
+            } catch (RemoteException e) {
+                throw e.rethrowAsRuntimeException();
+            }
+        }
+    }
+
+    @GuardedBy("VirtualMachineManager.sCreateLock")
+    @NonNull
+    private static File createVmDir(@NonNull Context context, @NonNull String name)
+            throws VirtualMachineException {
+        File vmDir = getVmDir(context, name);
+        try {
+            // We don't need to undo this even if VM creation fails.
+            Files.createDirectories(vmDir.getParentFile().toPath());
+
+            // 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(vmDir.toPath());
+        } catch (FileAlreadyExistsException e) {
+            throw new VirtualMachineException("virtual machine already exists", e);
+        } catch (IOException e) {
+            throw new VirtualMachineException("failed to create directory for VM", e);
+        }
+        return vmDir;
+    }
+
+    @NonNull
+    private static File getVmDir(@NonNull Context context, @NonNull String name) {
+        if (name.contains(File.separator) || name.equals(".") || name.equals("..")) {
+            throw new IllegalArgumentException("Invalid VM name: " + name);
+        }
+        File vmRoot = new File(context.getDataDir(), VM_DIR);
+        return new File(vmRoot, name);
+    }
+
+    /**
+     * Returns the name of this virtual machine. The name is unique in the package and can't be
+     * changed.
+     *
+     * @hide
+     */
+    @SystemApi
+    @NonNull
+    public String getName() {
+        return mName;
+    }
+
+    /**
+     * Returns the currently selected config of this virtual machine. There can be multiple virtual
+     * machines sharing the same config. Even in that case, the virtual machines are completely
+     * isolated from each other; they have different secrets. It is also possible that a virtual
+     * machine can change its config, which can be done by calling {@link #setConfig}.
+     *
+     * <p>NOTE: This method may block and should not be called on the main thread.
+     *
+     * @hide
+     */
+    @SystemApi
+    @WorkerThread
+    @NonNull
+    public VirtualMachineConfig getConfig() {
+        synchronized (mLock) {
+            return mConfig;
+        }
+    }
+
+    /**
+     * Returns the current status of this virtual machine.
+     *
+     * <p>NOTE: This method may block and should not be called on the main thread.
+     *
+     * @hide
+     */
+    @SystemApi
+    @WorkerThread
+    @Status
+    public int getStatus() {
+        IVirtualMachine virtualMachine;
+        synchronized (mLock) {
+            if (mWasDeleted) {
+                return STATUS_DELETED;
+            }
+            virtualMachine = mVirtualMachine;
+        }
+
+        int status;
+        if (virtualMachine == null) {
+            status = STATUS_STOPPED;
+        } else {
+            try {
+                status = stateToStatus(virtualMachine.getState());
+            } catch (RemoteException e) {
+                throw e.rethrowAsRuntimeException();
+            }
+        }
+        if (status == STATUS_STOPPED && !mVmRootPath.exists()) {
+            // A VM can quite happily keep running if its backing files have been deleted.
+            // But once it stops, it's gone forever.
+            synchronized (mLock) {
+                dropVm();
+            }
+            return STATUS_DELETED;
+        }
+        return status;
+    }
+
+    private int stateToStatus(@VirtualMachineState int state) {
+        switch (state) {
+            case VirtualMachineState.STARTING:
+            case VirtualMachineState.STARTED:
+            case VirtualMachineState.READY:
+            case VirtualMachineState.FINISHED:
+                return STATUS_RUNNING;
+            case VirtualMachineState.NOT_STARTED:
+            case VirtualMachineState.DEAD:
+            default:
+                return STATUS_STOPPED;
+        }
+    }
+
+    // Throw an appropriate exception if we have a running VM, or the VM has been deleted.
+    @GuardedBy("mLock")
+    private void checkStopped() throws VirtualMachineException {
+        if (mWasDeleted || !mVmRootPath.exists()) {
+            throw new VirtualMachineException("VM has been deleted");
+        }
+        if (mVirtualMachine == null) {
+            return;
+        }
+        try {
+            if (stateToStatus(mVirtualMachine.getState()) != STATUS_STOPPED) {
+                throw new VirtualMachineException("VM is not in stopped state");
+            }
+        } catch (RemoteException e) {
+            throw e.rethrowAsRuntimeException();
+        }
+        // It's stopped, but we still have a reference to it - we can fix that.
+        dropVm();
+    }
+
+    /**
+     * This should only be called when we know our VM has stopped; we no longer need to hold a
+     * reference to it (this allows resources to be GC'd) and we no longer need to be informed of
+     * memory pressure.
+     */
+    @GuardedBy("mLock")
+    private void dropVm() {
+        if (mMemoryManagementCallbacks != null) {
+            mContext.unregisterComponentCallbacks(mMemoryManagementCallbacks);
+        }
+        mVirtualMachine = null;
+    }
+
+    /** If we have an IVirtualMachine in the running state return it, otherwise throw. */
+    @GuardedBy("mLock")
+    private IVirtualMachine getRunningVm() throws VirtualMachineException {
+        try {
+            if (mVirtualMachine != null
+                    && stateToStatus(mVirtualMachine.getState()) == STATUS_RUNNING) {
+                return mVirtualMachine;
+            } else {
+                if (mWasDeleted || !mVmRootPath.exists()) {
+                    throw new VirtualMachineException("VM has been deleted");
+                } else {
+                    throw new VirtualMachineException("VM is not in running state");
+                }
+            }
+        } catch (RemoteException e) {
+            throw e.rethrowAsRuntimeException();
+        }
+    }
+
+    /**
+     * Registers the callback object to get events from the virtual machine. If a callback was
+     * already registered, it is replaced with the new one.
+     *
+     * @hide
+     */
+    @SystemApi
+    public void setCallback(
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull VirtualMachineCallback callback) {
+        synchronized (mCallbackLock) {
+            mCallback = callback;
+            mCallbackExecutor = executor;
+        }
+    }
+
+    /**
+     * Clears the currently registered callback.
+     *
+     * @hide
+     */
+    @SystemApi
+    public void clearCallback() {
+        synchronized (mCallbackLock) {
+            mCallback = null;
+            mCallbackExecutor = null;
+        }
+    }
+
+    /** Executes a callback on the callback executor. */
+    private void executeCallback(Consumer<VirtualMachineCallback> fn) {
+        final VirtualMachineCallback callback;
+        final Executor executor;
+        synchronized (mCallbackLock) {
+            callback = mCallback;
+            executor = mCallbackExecutor;
+        }
+        if (callback == null || executor == null) {
+            return;
+        }
+        final long restoreToken = Binder.clearCallingIdentity();
+        try {
+            executor.execute(() -> fn.accept(callback));
+        } finally {
+            Binder.restoreCallingIdentity(restoreToken);
+        }
+    }
+
+    private android.system.virtualizationservice.VirtualMachineConfig
+            createVirtualMachineConfigForRawFrom(VirtualMachineConfig vmConfig)
+                    throws IllegalStateException, IOException {
+        VirtualMachineRawConfig rawConfig = vmConfig.toVsRawConfig();
+
+        // Handle input devices here
+        List<InputDevice> inputDevices = new ArrayList<>();
+        if (vmConfig.getCustomImageConfig() != null && rawConfig.displayConfig != null) {
+            if (vmConfig.getCustomImageConfig().useTouch()) {
+                ParcelFileDescriptor[] pfds = ParcelFileDescriptor.createSocketPair();
+                mTouchSock = pfds[0];
+                InputDevice.MultiTouch t = new InputDevice.MultiTouch();
+                t.width = rawConfig.displayConfig.width;
+                t.height = rawConfig.displayConfig.height;
+                t.pfd = pfds[1];
+                inputDevices.add(InputDevice.multiTouch(t));
+            }
+            if (vmConfig.getCustomImageConfig().useKeyboard()) {
+                ParcelFileDescriptor[] pfds = ParcelFileDescriptor.createSocketPair();
+                mKeySock = pfds[0];
+                InputDevice.Keyboard k = new InputDevice.Keyboard();
+                k.pfd = pfds[1];
+                inputDevices.add(InputDevice.keyboard(k));
+            }
+            if (vmConfig.getCustomImageConfig().useMouse()) {
+                ParcelFileDescriptor[] pfds = ParcelFileDescriptor.createSocketPair();
+                mMouseSock = pfds[0];
+                InputDevice.Mouse m = new InputDevice.Mouse();
+                m.pfd = pfds[1];
+                inputDevices.add(InputDevice.mouse(m));
+            }
+            if (vmConfig.getCustomImageConfig().useSwitches()) {
+                ParcelFileDescriptor[] pfds = ParcelFileDescriptor.createSocketPair();
+                mSwitchesSock = pfds[0];
+                InputDevice.Switches s = new InputDevice.Switches();
+                s.pfd = pfds[1];
+                inputDevices.add(InputDevice.switches(s));
+            }
+            if (vmConfig.getCustomImageConfig().useTrackpad()) {
+                ParcelFileDescriptor[] pfds = ParcelFileDescriptor.createSocketPair();
+                mTrackpadSock = pfds[0];
+                InputDevice.Trackpad t = new InputDevice.Trackpad();
+                // TODO(b/347253952): make it configurable
+                t.width = 2380;
+                t.height = 1369;
+                t.pfd = pfds[1];
+                inputDevices.add(InputDevice.trackpad(t));
+            }
+        }
+        rawConfig.inputDevices = inputDevices.toArray(new InputDevice[0]);
+
+        // Handle network support
+        if (vmConfig.getCustomImageConfig() != null) {
+            rawConfig.networkSupported = vmConfig.getCustomImageConfig().useNetwork();
+        }
+
+        return android.system.virtualizationservice.VirtualMachineConfig.rawConfig(rawConfig);
+    }
+
+    private static record InputEvent(short type, short code, int value) {}
+
+    /** @hide */
+    public boolean sendKeyEvent(KeyEvent event) {
+        if (mKeySock == null) {
+            Log.d(TAG, "mKeySock == null");
+            return false;
+        }
+        // from include/uapi/linux/input-event-codes.h in the kernel.
+        short EV_SYN = 0x00;
+        short EV_KEY = 0x01;
+        short SYN_REPORT = 0x00;
+        boolean down = event.getAction() != MotionEvent.ACTION_UP;
+
+        return writeEventsToSock(
+                mKeySock,
+                Arrays.asList(
+                        new InputEvent(EV_KEY, (short) event.getScanCode(), down ? 1 : 0),
+                        new InputEvent(EV_SYN, SYN_REPORT, 0)));
+    }
+
+    /** @hide */
+    public boolean sendMouseEvent(MotionEvent event) {
+        try {
+            mInputEventQueue.add(
+                    Pair.create(InputEventType.MOUSE, MotionEvent.obtainNoHistory(event)));
+            return true;
+        } catch (Exception e) {
+            Log.e(TAG, e.toString());
+            return false;
+        }
+    }
+
+    /** @hide */
+    private boolean sendMouseEventInternal(MotionEvent event) {
+        if (mMouseSock == null) {
+            Log.d(TAG, "mMouseSock == null");
+            return false;
+        }
+        // from include/uapi/linux/input-event-codes.h in the kernel.
+        short EV_SYN = 0x00;
+        short EV_REL = 0x02;
+        short EV_KEY = 0x01;
+        short REL_X = 0x00;
+        short REL_Y = 0x01;
+        short SYN_REPORT = 0x00;
+        switch (event.getAction()) {
+            case MotionEvent.ACTION_MOVE:
+                int x = (int) event.getX();
+                int y = (int) event.getY();
+                return writeEventsToSock(
+                        mMouseSock,
+                        Arrays.asList(
+                                new InputEvent(EV_REL, REL_X, x),
+                                new InputEvent(EV_REL, REL_Y, y),
+                                new InputEvent(EV_SYN, SYN_REPORT, 0)));
+            case MotionEvent.ACTION_BUTTON_PRESS:
+            case MotionEvent.ACTION_BUTTON_RELEASE:
+                short BTN_LEFT = 0x110;
+                short BTN_RIGHT = 0x111;
+                short BTN_MIDDLE = 0x112;
+                short keyCode;
+                switch (event.getActionButton()) {
+                    case MotionEvent.BUTTON_PRIMARY:
+                        keyCode = BTN_LEFT;
+                        break;
+                    case MotionEvent.BUTTON_SECONDARY:
+                        keyCode = BTN_RIGHT;
+                        break;
+                    case MotionEvent.BUTTON_TERTIARY:
+                        keyCode = BTN_MIDDLE;
+                        break;
+                    default:
+                        Log.d(TAG, event.toString());
+                        return false;
+                }
+                return writeEventsToSock(
+                        mMouseSock,
+                        Arrays.asList(
+                                new InputEvent(
+                                        EV_KEY,
+                                        keyCode,
+                                        event.getAction() == MotionEvent.ACTION_BUTTON_PRESS
+                                                ? 1
+                                                : 0),
+                                new InputEvent(EV_SYN, SYN_REPORT, 0)));
+            case MotionEvent.ACTION_SCROLL:
+                short REL_HWHEEL = 0x06;
+                short REL_WHEEL = 0x08;
+                int scrollX = (int) event.getAxisValue(MotionEvent.AXIS_HSCROLL);
+                int scrollY = (int) event.getAxisValue(MotionEvent.AXIS_VSCROLL);
+                boolean status = true;
+                if (scrollX != 0) {
+                    status &=
+                            writeEventsToSock(
+                                    mMouseSock,
+                                    Arrays.asList(
+                                            new InputEvent(EV_REL, REL_HWHEEL, scrollX),
+                                            new InputEvent(EV_SYN, SYN_REPORT, 0)));
+                } else if (scrollY != 0) {
+                    status &=
+                            writeEventsToSock(
+                                    mMouseSock,
+                                    Arrays.asList(
+                                            new InputEvent(EV_REL, REL_WHEEL, scrollY),
+                                            new InputEvent(EV_SYN, SYN_REPORT, 0)));
+                } else {
+                    Log.d(TAG, event.toString());
+                    return false;
+                }
+                return status;
+            case MotionEvent.ACTION_UP:
+            case MotionEvent.ACTION_DOWN:
+                // Ignored because it's handled by ACTION_BUTTON_PRESS and ACTION_BUTTON_RELEASE
+                return true;
+            default:
+                Log.d(TAG, event.toString());
+                return false;
+        }
+    }
+
+    /** @hide */
+    public boolean sendMultiTouchEvent(MotionEvent event) {
+        try {
+            mInputEventQueue.add(
+                    Pair.create(InputEventType.TOUCH, MotionEvent.obtainNoHistory(event)));
+            return true;
+        } catch (Exception e) {
+            Log.e(TAG, e.toString());
+            return false;
+        }
+    }
+
+    /** @hide */
+    private boolean sendMultiTouchEventInternal(MotionEvent event) {
+        if (mTouchSock == null) {
+            Log.d(TAG, "mTouchSock == null");
+            return false;
+        }
+        // from include/uapi/linux/input-event-codes.h in the kernel.
+        short EV_SYN = 0x00;
+        short EV_ABS = 0x03;
+        short EV_KEY = 0x01;
+        short BTN_TOUCH = 0x14a;
+        short ABS_X = 0x00;
+        short ABS_Y = 0x01;
+        short SYN_REPORT = 0x00;
+        short ABS_MT_SLOT = 0x2f;
+        short ABS_MT_POSITION_X = 0x35;
+        short ABS_MT_POSITION_Y = 0x36;
+        short ABS_MT_TRACKING_ID = 0x39;
+
+        switch (event.getActionMasked()) {
+            case MotionEvent.ACTION_MOVE:
+                List<InputEvent> events =
+                        new ArrayList<>(
+                                event.getPointerCount() * 6 /*InputEvent per a pointer*/
+                                        + 1 /*SYN*/);
+                for (int actionIdx = 0; actionIdx < event.getPointerCount(); actionIdx++) {
+                    int pointerId = event.getPointerId(actionIdx);
+                    int x = (int) event.getRawX(actionIdx);
+                    int y = (int) event.getRawY(actionIdx);
+                    events.add(new InputEvent(EV_ABS, ABS_MT_SLOT, pointerId));
+                    events.add(new InputEvent(EV_ABS, ABS_MT_TRACKING_ID, pointerId));
+                    events.add(new InputEvent(EV_ABS, ABS_MT_POSITION_X, x));
+                    events.add(new InputEvent(EV_ABS, ABS_MT_POSITION_Y, y));
+                    events.add(new InputEvent(EV_ABS, ABS_X, x));
+                    events.add(new InputEvent(EV_ABS, ABS_Y, y));
+                }
+                events.add(new InputEvent(EV_SYN, SYN_REPORT, 0));
+                return writeEventsToSock(mTouchSock, events);
+            case MotionEvent.ACTION_DOWN:
+            case MotionEvent.ACTION_POINTER_DOWN:
+            case MotionEvent.ACTION_UP:
+            case MotionEvent.ACTION_POINTER_UP:
+                break;
+            default:
+                return false;
+        }
+
+        boolean down =
+                event.getActionMasked() == MotionEvent.ACTION_DOWN
+                        || event.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN;
+        int actionIdx = event.getActionIndex();
+        int pointerId = event.getPointerId(actionIdx);
+        int x = (int) event.getRawX(actionIdx);
+        int y = (int) event.getRawY(actionIdx);
+        return writeEventsToSock(
+                mTouchSock,
+                Arrays.asList(
+                        new InputEvent(EV_KEY, BTN_TOUCH, down ? 1 : 0),
+                        new InputEvent(EV_ABS, ABS_MT_SLOT, pointerId),
+                        new InputEvent(EV_ABS, ABS_MT_TRACKING_ID, down ? pointerId : -1),
+                        new InputEvent(EV_ABS, ABS_MT_POSITION_X, x),
+                        new InputEvent(EV_ABS, ABS_MT_POSITION_Y, y),
+                        new InputEvent(EV_ABS, ABS_X, x),
+                        new InputEvent(EV_ABS, ABS_Y, y),
+                        new InputEvent(EV_SYN, SYN_REPORT, 0)));
+    }
+
+    /** @hide */
+    public boolean sendLidEvent(boolean close) {
+        if (mSwitchesSock == null) {
+            Log.d(TAG, "mSwitcheSock == null");
+            return false;
+        }
+
+        // from include/uapi/linux/input-event-codes.h in the kernel.
+        short EV_SYN = 0x00;
+        short EV_SW = 0x05;
+        short SW_LID = 0x00;
+        short SYN_REPORT = 0x00;
+        return writeEventsToSock(
+                mSwitchesSock,
+                Arrays.asList(
+                        new InputEvent(EV_SW, SW_LID, close ? 1 : 0),
+                        new InputEvent(EV_SYN, SYN_REPORT, 0)));
+    }
+
+    /** @hide */
+    public boolean sendTabletModeEvent(boolean tabletMode) {
+        if (mSwitchesSock == null) {
+            Log.d(TAG, "mSwitcheSock == null");
+            return false;
+        }
+
+        // from include/uapi/linux/input-event-codes.h in the kernel.
+        short EV_SYN = 0x00;
+        short EV_SW = 0x05;
+        short SW_TABLET_MODE = 0x01;
+        short SYN_REPORT = 0x00;
+        return writeEventsToSock(
+                mSwitchesSock,
+                Arrays.asList(
+                        new InputEvent(EV_SW, SW_TABLET_MODE, tabletMode ? 1 : 0),
+                        new InputEvent(EV_SYN, SYN_REPORT, 0)));
+    }
+
+    /** @hide */
+    public boolean sendTrackpadEvent(MotionEvent event) {
+        try {
+            mInputEventQueue.add(
+                    Pair.create(InputEventType.TRACKPAD, MotionEvent.obtainNoHistory(event)));
+            return true;
+        } catch (Exception e) {
+            Log.e(TAG, e.toString());
+            return false;
+        }
+    }
+
+    /** @hide */
+    private boolean sendTrackpadEventInternal(MotionEvent event) {
+        if (mTrackpadSock == null) {
+            Log.d(TAG, "mTrackpadSock == null");
+            return false;
+        }
+        // from include/uapi/linux/input-event-codes.h in the kernel.
+        short EV_SYN = 0x00;
+        short EV_ABS = 0x03;
+        short EV_KEY = 0x01;
+        short BTN_TOUCH = 0x14a;
+        short BTN_TOOL_FINGER = 0x145;
+        short BTN_TOOL_DOUBLETAP = 0x14d;
+        short BTN_TOOL_TRIPLETAP = 0x14e;
+        short BTN_TOOL_QUADTAP = 0x14f;
+        short ABS_X = 0x00;
+        short ABS_Y = 0x01;
+        short SYN_REPORT = 0x00;
+        short ABS_MT_SLOT = 0x2f;
+        short ABS_MT_TOUCH_MAJOR = 0x30;
+        short ABS_MT_TOUCH_MINOR = 0x31;
+        short ABS_MT_WIDTH_MAJOR = 0x32;
+        short ABS_MT_WIDTH_MINOR = 0x33;
+        short ABS_MT_ORIENTATION = 0x34;
+        short ABS_MT_POSITION_X = 0x35;
+        short ABS_MT_POSITION_Y = 0x36;
+        short ABS_MT_TOOL_TYPE = 0x37;
+        short ABS_MT_BLOB_ID = 0x38;
+        short ABS_MT_TRACKING_ID = 0x39;
+        short ABS_MT_PRESSURE = 0x3a;
+        short ABS_MT_DISTANCE = 0x3b;
+        short ABS_MT_TOOL_X = 0x3c;
+        short ABS_MT_TOOL_Y = 0x3d;
+        short ABS_PRESSURE = 0x18;
+        short ABS_TOOL_WIDTH = 0x1c;
+
+        switch (event.getActionMasked()) {
+            case MotionEvent.ACTION_BUTTON_PRESS:
+            case MotionEvent.ACTION_BUTTON_RELEASE:
+                short BTN_LEFT = 0x110;
+                short keyCode;
+                switch (event.getActionButton()) {
+                    case MotionEvent.BUTTON_PRIMARY:
+                        keyCode = BTN_LEFT;
+                        break;
+                    default:
+                        Log.d(TAG, event.toString());
+                        return false;
+                }
+                return writeEventsToSock(
+                        mMouseSock,
+                        Arrays.asList(
+                                new InputEvent(
+                                        EV_KEY,
+                                        keyCode,
+                                        event.getAction() == MotionEvent.ACTION_BUTTON_PRESS
+                                                ? 1
+                                                : 0),
+                                new InputEvent(EV_SYN, SYN_REPORT, 0)));
+            case MotionEvent.ACTION_MOVE:
+                List<InputEvent> events =
+                        new ArrayList<>(
+                                event.getPointerCount() * 10 /*InputEvent per a pointer*/
+                                        + 1 /*SYN*/);
+                for (int actionIdx = 0; actionIdx < event.getPointerCount(); actionIdx++) {
+                    int pointerId = event.getPointerId(actionIdx);
+                    int x = (int) event.getRawX(actionIdx);
+                    int y = (int) event.getRawY(actionIdx);
+                    events.add(new InputEvent(EV_ABS, ABS_MT_SLOT, pointerId));
+                    events.add(new InputEvent(EV_ABS, ABS_MT_TRACKING_ID, pointerId));
+                    events.add(new InputEvent(EV_ABS, ABS_MT_POSITION_X, x));
+                    events.add(new InputEvent(EV_ABS, ABS_MT_POSITION_Y, y));
+                    events.add(
+                            new InputEvent(
+                                    EV_ABS,
+                                    ABS_MT_TOUCH_MAJOR,
+                                    (short) event.getTouchMajor(actionIdx)));
+                    events.add(
+                            new InputEvent(
+                                    EV_ABS,
+                                    ABS_MT_TOUCH_MINOR,
+                                    (short) event.getTouchMinor(actionIdx)));
+                    events.add(new InputEvent(EV_ABS, ABS_X, x));
+                    events.add(new InputEvent(EV_ABS, ABS_Y, y));
+                    events.add(
+                            new InputEvent(
+                                    EV_ABS,
+                                    ABS_PRESSURE,
+                                    (short) (255 * event.getPressure(actionIdx))));
+                    events.add(
+                            new InputEvent(
+                                    EV_ABS,
+                                    ABS_MT_PRESSURE,
+                                    (short) (255 * event.getPressure(actionIdx))));
+                }
+                events.add(new InputEvent(EV_SYN, SYN_REPORT, 0));
+                return writeEventsToSock(mTrackpadSock, events);
+            case MotionEvent.ACTION_DOWN:
+            case MotionEvent.ACTION_POINTER_DOWN:
+            case MotionEvent.ACTION_UP:
+            case MotionEvent.ACTION_POINTER_UP:
+                break;
+            default:
+                return false;
+        }
+
+        boolean down =
+                event.getActionMasked() == MotionEvent.ACTION_DOWN
+                        || event.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN;
+        int actionIdx = event.getActionIndex();
+        int pointerId = event.getPointerId(actionIdx);
+        int x = (int) event.getRawX(actionIdx);
+        int y = (int) event.getRawY(actionIdx);
+        return writeEventsToSock(
+                mTrackpadSock,
+                Arrays.asList(
+                        new InputEvent(EV_KEY, BTN_TOUCH, down ? 1 : 0),
+                        new InputEvent(
+                                EV_KEY,
+                                BTN_TOOL_FINGER,
+                                down && event.getPointerCount() == 1 ? 1 : 0),
+                        new InputEvent(
+                                EV_KEY, BTN_TOOL_DOUBLETAP, event.getPointerCount() == 2 ? 1 : 0),
+                        new InputEvent(
+                                EV_KEY, BTN_TOOL_TRIPLETAP, event.getPointerCount() == 3 ? 1 : 0),
+                        new InputEvent(
+                                EV_KEY, BTN_TOOL_QUADTAP, event.getPointerCount() > 4 ? 1 : 0),
+                        new InputEvent(EV_ABS, ABS_MT_SLOT, pointerId),
+                        new InputEvent(EV_ABS, ABS_MT_TRACKING_ID, down ? pointerId : -1),
+                        new InputEvent(EV_ABS, ABS_MT_TOOL_TYPE, 0 /* MT_TOOL_FINGER */),
+                        new InputEvent(EV_ABS, ABS_MT_POSITION_X, x),
+                        new InputEvent(EV_ABS, ABS_MT_POSITION_Y, y),
+                        new InputEvent(
+                                EV_ABS, ABS_MT_TOUCH_MAJOR, (short) event.getTouchMajor(actionIdx)),
+                        new InputEvent(
+                                EV_ABS, ABS_MT_TOUCH_MINOR, (short) event.getTouchMinor(actionIdx)),
+                        new InputEvent(EV_ABS, ABS_X, x),
+                        new InputEvent(EV_ABS, ABS_Y, y),
+                        new InputEvent(
+                                EV_ABS, ABS_PRESSURE, (short) (255 * event.getPressure(actionIdx))),
+                        new InputEvent(
+                                EV_ABS,
+                                ABS_MT_PRESSURE,
+                                (short) (255 * event.getPressure(actionIdx))),
+                        new InputEvent(EV_SYN, SYN_REPORT, 0)));
+    }
+
+    /** @hide */
+    public long getMemoryBalloon() {
+        long bytes = 0;
+
+        if (mMemoryManagementCallbacks != null) {
+            Log.d(TAG, "Auto balloon enabled in getMemoryBalloon");
+            return bytes;
+        }
+
+        synchronized (mLock) {
+            try {
+                if (mVirtualMachine != null) {
+                    bytes = mVirtualMachine.getMemoryBalloon();
+                }
+            } catch (RemoteException e) {
+                Log.w(TAG, "Cannot getMemoryBalloon", e);
+            }
+        }
+
+        return bytes;
+    }
+
+    /** @hide */
+    public void setMemoryBalloon(long bytes) {
+        if (mMemoryManagementCallbacks != null) {
+            Log.d(TAG, "Auto balloon enabled in setMemoryBalloon");
+            return;
+        }
+
+        synchronized (mLock) {
+            try {
+                if (mVirtualMachine != null) {
+                    mVirtualMachine.setMemoryBalloon(bytes);
+                }
+            } catch (RemoteException e) {
+                Log.w(TAG, "Cannot setMemoryBalloon", e);
+            }
+        }
+    }
+
+    private boolean writeEventsToSock(ParcelFileDescriptor sock, List<InputEvent> evtList) {
+        ByteBuffer byteBuffer =
+                ByteBuffer.allocate(8 /* (type: u16 + code: u16 + value: i32) */ * evtList.size());
+        byteBuffer.clear();
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
+        for (InputEvent e : evtList) {
+            byteBuffer.putShort(e.type);
+            byteBuffer.putShort(e.code);
+            byteBuffer.putInt(e.value);
+        }
+        try {
+            IoBridge.write(
+                    sock.getFileDescriptor(), byteBuffer.array(), 0, byteBuffer.array().length);
+        } catch (IOException e) {
+            Log.d(TAG, "cannot send event", e);
+            return false;
+        }
+        return true;
+    }
+
+    private android.system.virtualizationservice.VirtualMachineConfig
+            createVirtualMachineConfigForAppFrom(
+                    VirtualMachineConfig vmConfig, IVirtualizationService service)
+                    throws RemoteException, IOException, VirtualMachineException {
+        VirtualMachineAppConfig appConfig = vmConfig.toVsConfig(mContext.getPackageManager());
+        appConfig.instanceImage = ParcelFileDescriptor.open(mInstanceFilePath, MODE_READ_WRITE);
+        appConfig.name = mName;
+        if (mInstanceIdPath != null) {
+            appConfig.instanceId = Files.readAllBytes(mInstanceIdPath.toPath());
+        } else {
+            // FEATURE_LLPVM_CHANGES is disabled, instance_id is not used.
+            appConfig.instanceId = new byte[64];
+        }
+        if (mEncryptedStoreFilePath != null) {
+            appConfig.encryptedStorageImage =
+                    ParcelFileDescriptor.open(mEncryptedStoreFilePath, MODE_READ_WRITE);
+        }
+
+        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 {
+            createIdSigsAndUpdateConfig(service, appConfig);
+        } catch (FileNotFoundException e) {
+            throw new VirtualMachineException("Failed to generate APK signature", e);
+        }
+        return android.system.virtualizationservice.VirtualMachineConfig.appConfig(appConfig);
+    }
+
+    /**
+     * Runs this virtual machine. The returning of this method however doesn't mean that the VM has
+     * actually started running or the OS has booted there. Such events can be notified by
+     * registering a callback using {@link #setCallback} before calling {@code run()}. There is no
+     * limit other than available memory that limits the number of virtual machines that can run at
+     * the same time.
+     *
+     * <p>NOTE: This method may block and should not be called on the main thread.
+     *
+     * @throws VirtualMachineException if the virtual machine is not stopped or could not be
+     *     started.
+     * @hide
+     */
+    @SystemApi
+    @WorkerThread
+    @RequiresPermission(MANAGE_VIRTUAL_MACHINE_PERMISSION)
+    public void run() throws VirtualMachineException {
+        synchronized (mLock) {
+            checkStopped();
+
+            try {
+                mIdsigFilePath.createNewFile();
+                for (ExtraApkSpec extraApk : mExtraApks) {
+                    extraApk.idsig.createNewFile();
+                }
+            } catch (IOException e) {
+                // If the file already exists, exception is not thrown.
+                throw new VirtualMachineException("Failed to create APK signature file", e);
+            }
+
+            IVirtualizationService service = mVirtualizationService.getBinder();
+
+            try {
+                if (mConnectVmConsole) {
+                    createPtyConsole();
+                }
+
+                if (mVmOutputCaptured) {
+                    createVmOutputPipes();
+                }
+
+                if (mVmConsoleInputSupported) {
+                    createVmInputPipes();
+                }
+
+                ParcelFileDescriptor consoleOutFd = null;
+                if (mConnectVmConsole && mVmOutputCaptured) {
+                    // If we are enabling output pipes AND the host console, then we tee the console
+                    // output to both.
+                    ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
+                    mTeeConsoleOutReader = pipe[0];
+                    mTeeConsoleOutWriter = pipe[1];
+                    consoleOutFd = mTeeConsoleOutWriter;
+                    TeeWorker tee =
+                            new TeeWorker(
+                                    mName + " console",
+                                    new FileInputStream(mTeeConsoleOutReader.getFileDescriptor()),
+                                    List.of(
+                                            new FileOutputStream(mPtyFd.getFileDescriptor()),
+                                            new FileOutputStream(
+                                                    mConsoleOutWriter.getFileDescriptor())));
+                    // If the VM is stopped then the tee worker thread would get an EOF or read()
+                    // error which would tear down itself.
+                    mConsoleExecutor.execute(tee);
+                } else if (mConnectVmConsole) {
+                    consoleOutFd = mPtyFd;
+                } else if (mVmOutputCaptured) {
+                    consoleOutFd = mConsoleOutWriter;
+                }
+                mInputEventExecutor = Executors.newSingleThreadExecutor();
+                mInputEventExecutor.execute(
+                        () -> {
+                            while (true) {
+                                try {
+                                    Pair<InputEventType, MotionEvent> event =
+                                            mInputEventQueue.take();
+                                    switch (event.first) {
+                                        case TOUCH:
+                                            sendMultiTouchEventInternal(event.second);
+                                            break;
+                                        case TRACKPAD:
+                                            sendTrackpadEventInternal(event.second);
+                                            break;
+                                        case MOUSE:
+                                            sendMouseEventInternal(event.second);
+                                            break;
+                                    }
+                                    event.second.recycle();
+                                } catch (Exception e) {
+                                    Log.e(TAG, e.toString());
+                                }
+                            }
+                        });
+                ParcelFileDescriptor consoleInFd = null;
+                if (mConnectVmConsole) {
+                    consoleInFd = mPtyFd;
+                } else if (mVmConsoleInputSupported) {
+                    consoleInFd = mConsoleInReader;
+                }
+
+                VirtualMachineConfig vmConfig = getConfig();
+                android.system.virtualizationservice.VirtualMachineConfig vmConfigParcel =
+                        vmConfig.getCustomImageConfig() != null
+                                ? createVirtualMachineConfigForRawFrom(vmConfig)
+                                : createVirtualMachineConfigForAppFrom(vmConfig, service);
+
+                mVirtualMachine =
+                        service.createVm(vmConfigParcel, consoleOutFd, consoleInFd, mLogWriter);
+                mVirtualMachine.registerCallback(new CallbackTranslator(service));
+                if (mMemoryManagementCallbacks != null) {
+                    mContext.registerComponentCallbacks(mMemoryManagementCallbacks);
+                }
+                if (mConnectVmConsole) {
+                    mVirtualMachine.setHostConsoleName(getHostConsoleName());
+                }
+                mVirtualMachine.start();
+            } catch (IOException e) {
+                throw new VirtualMachineException("failed to persist files", e);
+            } catch (IllegalStateException | ServiceSpecificException e) {
+                throw new VirtualMachineException(e);
+            } catch (RemoteException e) {
+                throw e.rethrowAsRuntimeException();
+            }
+        }
+    }
+
+    private void createIdSigsAndUpdateConfig(
+            IVirtualizationService service, VirtualMachineAppConfig appConfig)
+            throws RemoteException, FileNotFoundException {
+        // Fill the idsig file by hashing the apk
+        service.createOrUpdateIdsigFile(
+                appConfig.apk, ParcelFileDescriptor.open(mIdsigFilePath, MODE_READ_WRITE));
+
+        for (ExtraApkSpec extraApk : mExtraApks) {
+            service.createOrUpdateIdsigFile(
+                    ParcelFileDescriptor.open(extraApk.apk, MODE_READ_ONLY),
+                    ParcelFileDescriptor.open(extraApk.idsig, MODE_READ_WRITE));
+        }
+
+        // Re-open idsig files in read-only mode
+        appConfig.idsig = ParcelFileDescriptor.open(mIdsigFilePath, MODE_READ_ONLY);
+        List<ParcelFileDescriptor> extraIdsigs = new ArrayList<>();
+        for (ExtraApkSpec extraApk : mExtraApks) {
+            extraIdsigs.add(ParcelFileDescriptor.open(extraApk.idsig, MODE_READ_ONLY));
+        }
+        appConfig.extraIdsigs = extraIdsigs;
+    }
+
+    @GuardedBy("mLock")
+    private void createVmOutputPipes() throws VirtualMachineException {
+        try {
+            if (mConsoleOutReader == null || mConsoleOutWriter == null) {
+                ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
+                mConsoleOutReader = pipe[0];
+                mConsoleOutWriter = pipe[1];
+            }
+
+            if (mLogReader == null || mLogWriter == null) {
+                ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
+                mLogReader = pipe[0];
+                mLogWriter = pipe[1];
+            }
+        } catch (IOException e) {
+            throw new VirtualMachineException("Failed to create output stream for VM", e);
+        }
+    }
+
+    @GuardedBy("mLock")
+    private void createVmInputPipes() throws VirtualMachineException {
+        try {
+            if (mConsoleInReader == null || mConsoleInWriter == null) {
+                if (mConnectVmConsole) {
+                    // If we are enabling input pipes AND the host console, then we should just use
+                    // the host pty peer end as the console write end.
+                    createPtyConsole();
+                    mConsoleInReader = mPtyFd.dup();
+                    mConsoleInWriter = mPtsFd.dup();
+                } else {
+                    ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
+                    mConsoleInReader = pipe[0];
+                    mConsoleInWriter = pipe[1];
+                }
+            }
+        } catch (IOException e) {
+            throw new VirtualMachineException("Failed to create input stream for VM", e);
+        }
+    }
+
+    @FunctionalInterface
+    private static interface OpenPtyCallback {
+        public void apply(FileDescriptor mfd, FileDescriptor sfd, byte[] name);
+    }
+
+    // Opens a pty and set the master end to raw mode and O_NONBLOCK.
+    private static native void nativeOpenPtyRawNonblock(OpenPtyCallback resultCallback)
+            throws IOException;
+
+    @GuardedBy("mLock")
+    private void createPtyConsole() throws VirtualMachineException {
+        if (mPtyFd != null && mPtsFd != null) {
+            return;
+        }
+        List<FileDescriptor> fd = new ArrayList<>(2);
+        StringBuilder nameBuilder = new StringBuilder();
+        try {
+            try {
+                nativeOpenPtyRawNonblock(
+                        (FileDescriptor mfd, FileDescriptor sfd, byte[] ptsName) -> {
+                            fd.add(mfd);
+                            fd.add(sfd);
+                            nameBuilder.append(new String(ptsName, StandardCharsets.UTF_8));
+                        });
+            } catch (Exception e) {
+                fd.forEach(IoUtils::closeQuietly);
+                throw e;
+            }
+        } catch (IOException e) {
+            throw new VirtualMachineException(
+                    "Failed to create host console to connect to the VM console", e);
+        }
+        mPtyFd = new ParcelFileDescriptor(fd.get(0));
+        mPtsFd = new ParcelFileDescriptor(fd.get(1));
+        mPtsName = nameBuilder.toString();
+        Log.d(TAG, "Serial console device: " + mPtsName);
+    }
+
+    /**
+     * Returns the name of the peer end (ptsname) of the host console. The host console is only
+     * available if the {@link VirtualMachineConfig} specifies that a host console should
+     * {@linkplain VirtualMachineConfig#isConnectVmConsole connect} to the VM console.
+     *
+     * @throws VirtualMachineException if the host pseudoterminal could not be created, or
+     *     connecting to the VM console is not enabled.
+     * @hide
+     */
+    @NonNull
+    private String getHostConsoleName() throws VirtualMachineException {
+        if (!mConnectVmConsole) {
+            throw new VirtualMachineException("Host console is not enabled");
+        }
+        synchronized (mLock) {
+            createPtyConsole();
+            return mPtsName;
+        }
+    }
+
+    /**
+     * Returns the stream object representing the console output from the virtual machine. The
+     * console output is only available if the {@link VirtualMachineConfig} specifies that it should
+     * be {@linkplain VirtualMachineConfig#isVmOutputCaptured captured}.
+     *
+     * <p>If you turn on output capture, you must consume data from {@code getConsoleOutput} -
+     * because otherwise the code in the VM may get blocked when the pipe buffer fills up.
+     *
+     * <p>NOTE: This method may block and should not be called on the main thread.
+     *
+     * @throws VirtualMachineException if the stream could not be created, or capturing is turned
+     *     off.
+     * @hide
+     */
+    @SystemApi
+    @WorkerThread
+    @NonNull
+    public InputStream getConsoleOutput() throws VirtualMachineException {
+        if (!mVmOutputCaptured) {
+            throw new VirtualMachineException("Capturing vm outputs is turned off");
+        }
+        synchronized (mLock) {
+            createVmOutputPipes();
+            return new FileInputStream(mConsoleOutReader.getFileDescriptor());
+        }
+    }
+
+    /**
+     * Returns the stream object representing the console input to the virtual machine. The console
+     * input is only available if the {@link VirtualMachineConfig} specifies that it should be
+     * {@linkplain VirtualMachineConfig#isVmConsoleInputSupported supported}.
+     *
+     * <p>NOTE: This method may block and should not be called on the main thread.
+     *
+     * @throws VirtualMachineException if the stream could not be created, or console input is not
+     *     supported.
+     * @hide
+     */
+    @TestApi
+    @WorkerThread
+    @NonNull
+    public OutputStream getConsoleInput() throws VirtualMachineException {
+        if (!mVmConsoleInputSupported) {
+            throw new VirtualMachineException("VM console input is not supported");
+        }
+        synchronized (mLock) {
+            createVmInputPipes();
+            return new FileOutputStream(mConsoleInWriter.getFileDescriptor());
+        }
+    }
+
+    /**
+     * Returns the stream object representing the log output from the virtual machine. The log
+     * output is only available if the VirtualMachineConfig specifies that it should be {@linkplain
+     * VirtualMachineConfig#isVmOutputCaptured captured}.
+     *
+     * <p>If you turn on output capture, you must consume data from {@code getLogOutput} - because
+     * otherwise the code in the VM may get blocked when the pipe buffer fills up.
+     *
+     * <p>NOTE: This method may block and should not be called on the main thread.
+     *
+     * @throws VirtualMachineException if the stream could not be created, or capturing is turned
+     *     off.
+     * @hide
+     */
+    @SystemApi
+    @WorkerThread
+    @NonNull
+    public InputStream getLogOutput() throws VirtualMachineException {
+        if (!mVmOutputCaptured) {
+            throw new VirtualMachineException("Capturing vm outputs is turned off");
+        }
+        synchronized (mLock) {
+            createVmOutputPipes();
+            return new FileInputStream(mLogReader.getFileDescriptor());
+        }
+    }
+
+    /**
+     * Stops this virtual machine. Stopping a virtual machine is like pulling the plug on a real
+     * computer; the machine halts immediately. Software running on the virtual machine is not
+     * notified of the event. Writes to {@linkplain
+     * VirtualMachineConfig.Builder#setEncryptedStorageBytes encrypted storage} might not be
+     * persisted, and the instance might be left in an inconsistent state.
+     *
+     * <p>For a graceful shutdown, you could request the payload to call {@code exit()}, e.g. via a
+     * {@linkplain #connectToVsockServer binder request}, and wait for {@link
+     * VirtualMachineCallback#onPayloadFinished} to be called.
+     *
+     * <p>A stopped virtual machine can be re-started by calling {@link #run()}.
+     *
+     * <p>NOTE: This method may block and should not be called on the main thread.
+     *
+     * @throws VirtualMachineException if the virtual machine is not running or could not be
+     *     stopped.
+     * @hide
+     */
+    @SystemApi
+    @WorkerThread
+    public void stop() throws VirtualMachineException {
+        synchronized (mLock) {
+            if (mVirtualMachine == null) {
+                throw new VirtualMachineException("VM is not running");
+            }
+            try {
+                mVirtualMachine.stop();
+                dropVm();
+                if (mInputEventExecutor != null) {
+                    mInputEventExecutor.shutdownNow();
+                }
+            } catch (RemoteException e) {
+                throw e.rethrowAsRuntimeException();
+            } catch (ServiceSpecificException e) {
+                throw new VirtualMachineException(e);
+            }
+        }
+    }
+
+    /** @hide */
+    public void suspend() throws VirtualMachineException {
+        synchronized (mLock) {
+            if (mVirtualMachine == null) {
+                throw new VirtualMachineException("VM is not running");
+            }
+            try {
+                mVirtualMachine.suspend();
+            } catch (RemoteException e) {
+                throw e.rethrowAsRuntimeException();
+            } catch (ServiceSpecificException e) {
+                throw new VirtualMachineException(e);
+            }
+        }
+    }
+
+    /** @hide */
+    public void resume() throws VirtualMachineException {
+        synchronized (mLock) {
+            if (mVirtualMachine == null) {
+                throw new VirtualMachineException("VM is not running");
+            }
+            try {
+                mVirtualMachine.resume();
+            } catch (RemoteException e) {
+                throw e.rethrowAsRuntimeException();
+            } catch (ServiceSpecificException e) {
+                throw new VirtualMachineException(e);
+            }
+        }
+    }
+
+    /**
+     * Stops this virtual machine, if it is running.
+     *
+     * <p>NOTE: This method may block and should not be called on the main thread.
+     *
+     * @see #stop()
+     * @hide
+     */
+    @SystemApi
+    @WorkerThread
+    @Override
+    public void close() {
+        synchronized (mLock) {
+            if (mVirtualMachine == null) {
+                return;
+            }
+            try {
+                if (stateToStatus(mVirtualMachine.getState()) == STATUS_RUNNING) {
+                    mVirtualMachine.stop();
+                    dropVm();
+                }
+            } catch (RemoteException e) {
+                throw e.rethrowAsRuntimeException();
+            } catch (ServiceSpecificException e) {
+                // Deliberately ignored; this almost certainly means the VM exited just as
+                // we tried to stop it.
+                Log.i(TAG, "Ignoring error on close()", e);
+            }
+        }
+    }
+
+    private static void deleteRecursively(File dir) throws IOException {
+        // Note: This doesn't follow symlinks, which is important. Instead they are just deleted
+        // (and Files.delete deletes the link not the target).
+        Files.walkFileTree(
+                dir.toPath(),
+                new SimpleFileVisitor<>() {
+                    @Override
+                    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
+                            throws IOException {
+                        Files.delete(file);
+                        return FileVisitResult.CONTINUE;
+                    }
+
+                    @Override
+                    public FileVisitResult postVisitDirectory(Path dir, IOException e)
+                            throws IOException {
+                        // Directory is deleted after we've visited (deleted) all its contents, so
+                        // it
+                        // should be empty by now.
+                        Files.delete(dir);
+                        return FileVisitResult.CONTINUE;
+                    }
+                });
+    }
+
+    /**
+     * Changes the config of this virtual machine to a new one. This can be used to adjust things
+     * like the number of CPU and size of the RAM, depending on the situation (e.g. the size of the
+     * application to run on the virtual machine, etc.)
+     *
+     * <p>The new config must be {@linkplain VirtualMachineConfig#isCompatibleWith compatible with}
+     * the existing config.
+     *
+     * <p>NOTE: This method may block and should not be called on the main thread.
+     *
+     * @return the old config
+     * @throws VirtualMachineException if the virtual machine is not stopped, or the new config is
+     *     incompatible.
+     * @hide
+     */
+    @SystemApi
+    @WorkerThread
+    @NonNull
+    public VirtualMachineConfig setConfig(@NonNull VirtualMachineConfig newConfig)
+            throws VirtualMachineException {
+        synchronized (mLock) {
+            VirtualMachineConfig oldConfig = mConfig;
+            if (!oldConfig.isCompatibleWith(newConfig)) {
+                throw new VirtualMachineException("incompatible config");
+            }
+            checkStopped();
+
+            if (oldConfig != newConfig) {
+                // Delete any existing file before recreating; that ensures any
+                // VirtualMachineDescriptor that refers to the old file does not see the new config.
+                mConfigFilePath.delete();
+                newConfig.serialize(mConfigFilePath);
+                mConfig = newConfig;
+            }
+            return oldConfig;
+        }
+    }
+
+    @Nullable
+    private static native IBinder nativeConnectToVsockServer(IBinder vmBinder, int port);
+
+    /**
+     * Connect to a VM's binder service via vsock and return the root IBinder object. Guest VMs are
+     * expected to set up vsock servers in their payload. After the host app receives the {@link
+     * VirtualMachineCallback#onPayloadReady}, it can use this method to establish a connection to
+     * the guest VM.
+     *
+     * <p>NOTE: This method may block and should not be called on the main thread.
+     *
+     * @throws VirtualMachineException if the virtual machine is not running or the connection
+     *     failed.
+     * @hide
+     */
+    @SystemApi
+    @WorkerThread
+    @NonNull
+    public IBinder connectToVsockServer(
+            @IntRange(from = MIN_VSOCK_PORT, to = MAX_VSOCK_PORT) long port)
+            throws VirtualMachineException {
+
+        synchronized (mLock) {
+            IBinder iBinder =
+                    nativeConnectToVsockServer(getRunningVm().asBinder(), validatePort(port));
+            if (iBinder == null) {
+                throw new VirtualMachineException("Failed to connect to vsock server");
+            }
+            return iBinder;
+        }
+    }
+
+    /**
+     * Opens a vsock connection to the VM on the given port.
+     *
+     * <p>The caller is responsible for closing the returned {@code ParcelFileDescriptor}.
+     *
+     * <p>NOTE: This method may block and should not be called on the main thread.
+     *
+     * @throws VirtualMachineException if connecting fails.
+     * @hide
+     */
+    @SystemApi
+    @WorkerThread
+    @NonNull
+    public ParcelFileDescriptor connectVsock(
+            @IntRange(from = MIN_VSOCK_PORT, to = MAX_VSOCK_PORT) long port)
+            throws VirtualMachineException {
+        synchronized (mLock) {
+            try {
+                return getRunningVm().connectVsock(validatePort(port));
+            } catch (RemoteException e) {
+                throw e.rethrowAsRuntimeException();
+            } catch (ServiceSpecificException e) {
+                throw new VirtualMachineException(e);
+            }
+        }
+    }
+
+    private int validatePort(long port) {
+        // Ports below 1024 are "privileged" (payload code can't bind to these), and port numbers
+        // are 32-bit unsigned numbers at the OS level, even though we pass them as 32-bit signed
+        // numbers internally.
+        if (port < MIN_VSOCK_PORT || port > MAX_VSOCK_PORT) {
+            throw new IllegalArgumentException("Bad port " + port);
+        }
+        return (int) port;
+    }
+
+    /**
+     * Returns the root directory where all files related to this {@link VirtualMachine} (e.g.
+     * {@code instance.img}, {@code apk.idsig}, etc) are stored.
+     *
+     * @hide
+     */
+    @TestApi
+    @NonNull
+    public File getRootDir() {
+        return mVmRootPath;
+    }
+
+    /**
+     * Enables the VM to request attestation in testing mode.
+     *
+     * <p>This function provisions a key pair for the VM attestation testing, a fake certificate
+     * will be associated to the fake key pair when the VM requests attestation in testing mode.
+     *
+     * <p>The provisioned key pair can only be used in subsequent calls to {@link
+     * AVmPayload_requestAttestationForTesting} within a running VM.
+     *
+     * @hide
+     */
+    @TestApi
+    @RequiresPermission(USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION)
+    @FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS)
+    public void enableTestAttestation() throws VirtualMachineException {
+        try {
+            mVirtualizationService.getBinder().enableTestAttestation();
+        } catch (RemoteException e) {
+            throw e.rethrowAsRuntimeException();
+        }
+    }
+
+    /**
+     * Captures the current state of the VM in a {@link VirtualMachineDescriptor} instance. The VM
+     * needs to be stopped to avoid inconsistency in its state representation.
+     *
+     * <p>The state of the VM is not actually copied until {@link
+     * VirtualMachineManager#importFromDescriptor} is called. It is recommended that the VM not be
+     * started until that operation is complete.
+     *
+     * <p>NOTE: This method may block and should not be called on the main thread.
+     *
+     * @return a {@link VirtualMachineDescriptor} instance that represents the VM's state.
+     * @throws VirtualMachineException if the virtual machine is not stopped, or the state could not
+     *     be captured.
+     * @hide
+     */
+    @SystemApi
+    @WorkerThread
+    @NonNull
+    public VirtualMachineDescriptor toDescriptor() throws VirtualMachineException {
+        synchronized (mLock) {
+            checkStopped();
+            try {
+                return new VirtualMachineDescriptor(
+                        ParcelFileDescriptor.open(mConfigFilePath, MODE_READ_ONLY),
+                        mInstanceIdPath != null
+                                ? ParcelFileDescriptor.open(mInstanceIdPath, MODE_READ_ONLY)
+                                : null,
+                        ParcelFileDescriptor.open(mInstanceFilePath, MODE_READ_ONLY),
+                        mEncryptedStoreFilePath != null
+                                ? ParcelFileDescriptor.open(mEncryptedStoreFilePath, MODE_READ_ONLY)
+                                : null);
+            } catch (IOException e) {
+                throw new VirtualMachineException(e);
+            }
+        }
+    }
+
+    @Override
+    public String toString() {
+        VirtualMachineConfig config = getConfig();
+        String payloadConfigPath = config.getPayloadConfigPath();
+        String payloadBinaryName = config.getPayloadBinaryName();
+
+        StringBuilder result = new StringBuilder();
+        result.append("VirtualMachine(").append("name:").append(getName()).append(", ");
+        if (payloadBinaryName != null) {
+            result.append("payload:").append(payloadBinaryName).append(", ");
+        }
+        if (payloadConfigPath != null) {
+            result.append("config:").append(payloadConfigPath).append(", ");
+        }
+        result.append("package: ").append(mPackageName).append(")");
+        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 {
+        /*
+         * JSON schema from packages/modules/Virtualization/microdroid/libs/libmicrodroid_payload_metadata/config/src/lib.rs:
+         *
+         * <p>{ "extra_apks": [ { "path": "/system/app/foo.apk", }, ... ], ... }
+         */
+        try {
+            List<String> apks = new ArrayList<>();
+
+            reader.beginObject();
+            while (reader.hasNext()) {
+                if (reader.nextName().equals("extra_apks")) {
+                    reader.beginArray();
+                    while (reader.hasNext()) {
+                        reader.beginObject();
+                        String name = reader.nextName();
+                        if (name.equals("path")) {
+                            apks.add(reader.nextString());
+                        } else {
+                            reader.skipValue();
+                        }
+                        reader.endObject();
+                    }
+                    reader.endArray();
+                } else {
+                    reader.skipValue();
+                }
+            }
+            reader.endObject();
+            return apks;
+        } catch (IOException e) {
+            throw new VirtualMachineException(e);
+        }
+    }
+
+    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);
+            }
+
+            extraApks.add(
+                    new ExtraApkSpec(
+                            new File(appInfo.sourceDir),
+                            new File(vmDir, EXTRA_IDSIG_FILE_PREFIX + i)));
+        }
+        return extraApks;
+    }
+
+    private void importInstanceIdFrom(@NonNull ParcelFileDescriptor instanceIdFd)
+            throws VirtualMachineException {
+        try (FileChannel idOutput = new FileOutputStream(mInstanceIdPath).getChannel();
+                FileChannel idInput = new AutoCloseInputStream(instanceIdFd).getChannel()) {
+            idOutput.transferFrom(idInput, /* position= */ 0, idInput.size());
+        } catch (IOException e) {
+            throw new VirtualMachineException("failed to copy instance_id", e);
+        }
+    }
+
+    private void importInstanceFrom(@NonNull ParcelFileDescriptor instanceFd)
+            throws VirtualMachineException {
+        try (FileChannel instance = new FileOutputStream(mInstanceFilePath).getChannel();
+                FileChannel instanceInput = new AutoCloseInputStream(instanceFd).getChannel()) {
+            instance.transferFrom(instanceInput, /* position= */ 0, instanceInput.size());
+        } catch (IOException e) {
+            throw new VirtualMachineException("failed to transfer instance image", e);
+        }
+    }
+
+    private void importEncryptedStoreFrom(@NonNull ParcelFileDescriptor encryptedStoreFd)
+            throws VirtualMachineException {
+        try (FileChannel storeOutput = new FileOutputStream(mEncryptedStoreFilePath).getChannel();
+                FileChannel storeInput = new AutoCloseInputStream(encryptedStoreFd).getChannel()) {
+            storeOutput.transferFrom(storeInput, /* position= */ 0, storeInput.size());
+        } catch (IOException e) {
+            throw new VirtualMachineException("failed to transfer encryptedstore image", e);
+        }
+    }
+
+    /** Map the raw AIDL (& binder) callbacks to what the client expects. */
+    private class CallbackTranslator extends IVirtualMachineCallback.Stub {
+        private final IVirtualizationService mService;
+        private final DeathRecipient mDeathRecipient;
+
+        // The VM should only be observed to die once
+        private final AtomicBoolean mOnDiedCalled = new AtomicBoolean(false);
+
+        public CallbackTranslator(IVirtualizationService service) throws RemoteException {
+            this.mService = service;
+            this.mDeathRecipient = () -> reportStopped(STOP_REASON_VIRTUALIZATION_SERVICE_DIED);
+            service.asBinder().linkToDeath(mDeathRecipient, 0);
+        }
+
+        @Override
+        public void onPayloadStarted(int cid) {
+            executeCallback((cb) -> cb.onPayloadStarted(VirtualMachine.this));
+        }
+
+        @Override
+        public void onPayloadReady(int cid) {
+            executeCallback((cb) -> cb.onPayloadReady(VirtualMachine.this));
+        }
+
+        @Override
+        public void onPayloadFinished(int cid, int exitCode) {
+            executeCallback((cb) -> cb.onPayloadFinished(VirtualMachine.this, exitCode));
+        }
+
+        @Override
+        public void onError(int cid, int errorCode, String message) {
+            int translatedError = getTranslatedError(errorCode);
+            executeCallback((cb) -> cb.onError(VirtualMachine.this, translatedError, message));
+        }
+
+        @Override
+        public void onDied(int cid, int reason) {
+            int translatedReason = getTranslatedReason(reason);
+            reportStopped(translatedReason);
+            mService.asBinder().unlinkToDeath(mDeathRecipient, 0);
+        }
+
+        private void reportStopped(@VirtualMachineCallback.StopReason int reason) {
+            if (mOnDiedCalled.compareAndSet(false, true)) {
+                executeCallback((cb) -> cb.onStopped(VirtualMachine.this, reason));
+            }
+        }
+
+        @VirtualMachineCallback.ErrorCode
+        private int getTranslatedError(int reason) {
+            switch (reason) {
+                case ErrorCode.PAYLOAD_VERIFICATION_FAILED:
+                    return ERROR_PAYLOAD_VERIFICATION_FAILED;
+                case ErrorCode.PAYLOAD_CHANGED:
+                    return ERROR_PAYLOAD_CHANGED;
+                case ErrorCode.PAYLOAD_INVALID_CONFIG:
+                    return ERROR_PAYLOAD_INVALID_CONFIG;
+                default:
+                    return ERROR_UNKNOWN;
+            }
+        }
+
+        @VirtualMachineCallback.StopReason
+        private int getTranslatedReason(int reason) {
+            switch (reason) {
+                case DeathReason.INFRASTRUCTURE_ERROR:
+                    return STOP_REASON_INFRASTRUCTURE_ERROR;
+                case DeathReason.KILLED:
+                    return STOP_REASON_KILLED;
+                case DeathReason.SHUTDOWN:
+                    return STOP_REASON_SHUTDOWN;
+                case DeathReason.START_FAILED:
+                    return STOP_REASON_START_FAILED;
+                case DeathReason.REBOOT:
+                    return STOP_REASON_REBOOT;
+                case DeathReason.CRASH:
+                    return STOP_REASON_CRASH;
+                case DeathReason.PVM_FIRMWARE_PUBLIC_KEY_MISMATCH:
+                    return STOP_REASON_PVM_FIRMWARE_PUBLIC_KEY_MISMATCH;
+                case DeathReason.PVM_FIRMWARE_INSTANCE_IMAGE_CHANGED:
+                    return STOP_REASON_PVM_FIRMWARE_INSTANCE_IMAGE_CHANGED;
+                case DeathReason.MICRODROID_FAILED_TO_CONNECT_TO_VIRTUALIZATION_SERVICE:
+                    return STOP_REASON_MICRODROID_FAILED_TO_CONNECT_TO_VIRTUALIZATION_SERVICE;
+                case DeathReason.MICRODROID_PAYLOAD_HAS_CHANGED:
+                    return STOP_REASON_MICRODROID_PAYLOAD_HAS_CHANGED;
+                case DeathReason.MICRODROID_PAYLOAD_VERIFICATION_FAILED:
+                    return STOP_REASON_MICRODROID_PAYLOAD_VERIFICATION_FAILED;
+                case DeathReason.MICRODROID_INVALID_PAYLOAD_CONFIG:
+                    return STOP_REASON_MICRODROID_INVALID_PAYLOAD_CONFIG;
+                case DeathReason.MICRODROID_UNKNOWN_RUNTIME_ERROR:
+                    return STOP_REASON_MICRODROID_UNKNOWN_RUNTIME_ERROR;
+                case DeathReason.HANGUP:
+                    return STOP_REASON_HANGUP;
+                default:
+                    return STOP_REASON_UNKNOWN;
+            }
+        }
+    }
+
+    /**
+     * Duplicates {@code InputStream} data to multiple {@code OutputStream}. Like the "tee" command.
+     *
+     * <p>Supports non-blocking writes to the output streams by ignoring EAGAIN error.
+     */
+    private static class TeeWorker implements Runnable {
+        private final String mName;
+        private final InputStream mIn;
+        private final List<OutputStream> mOuts;
+
+        TeeWorker(String name, InputStream in, Collection<OutputStream> outs) {
+            mName = name;
+            mIn = in;
+            mOuts = new ArrayList<>(outs);
+        }
+
+        @Override
+        public void run() {
+            byte[] buffer = new byte[2048];
+            try {
+                while (!Thread.interrupted()) {
+                    int len = mIn.read(buffer);
+                    if (len < 0) {
+                        break;
+                    }
+                    for (OutputStream out : mOuts) {
+                        try {
+                            out.write(buffer, 0, len);
+                        } catch (IOException e) {
+                            // EAGAIN is expected because the file description has O_NONBLOCK flag.
+                            if (!isErrnoError(e, OsConstants.EAGAIN)) {
+                                throw e;
+                            }
+                        }
+                    }
+                }
+            } catch (Exception e) {
+                Log.e(TAG, "Tee " + mName, e);
+            }
+        }
+
+        private static ErrnoException asErrnoException(Throwable e) {
+            if (e instanceof ErrnoException) {
+                return (ErrnoException) e;
+            } else if (e instanceof IOException) {
+                // Try to unwrap ErrnoException#rethrowAsIOException()
+                return asErrnoException(e.getCause());
+            }
+            return null;
+        }
+
+        private static boolean isErrnoError(Exception e, int expectedValue) {
+            ErrnoException errno = asErrnoException(e);
+            return errno != null && errno.errno == expectedValue;
+        }
+    }
+}
diff --git a/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineCallback.java b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineCallback.java
new file mode 100644
index 0000000..b077826
--- /dev/null
+++ b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineCallback.java
@@ -0,0 +1,162 @@
+/*
+ * 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.system.virtualmachine;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Callback interface to get notified with the events from the virtual machine. The methods are
+ * executed on a binder thread. Implementations can make blocking calls in the methods.
+ *
+ * @hide
+ */
+@SystemApi
+@SuppressLint("CallbackInterface") // Guidance has changed, lint is out of date (b/245552641)
+public interface VirtualMachineCallback {
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(
+            prefix = "ERROR_",
+            value = {
+                ERROR_UNKNOWN,
+                ERROR_PAYLOAD_VERIFICATION_FAILED,
+                ERROR_PAYLOAD_CHANGED,
+                ERROR_PAYLOAD_INVALID_CONFIG
+            })
+    @interface ErrorCode {}
+
+    /** Error code for all other errors not listed below. */
+    int ERROR_UNKNOWN = 0;
+
+    /**
+     * Error code indicating that the payload can't be verified due to various reasons (e.g invalid
+     * merkle tree, invalid formats, etc).
+     */
+    int ERROR_PAYLOAD_VERIFICATION_FAILED = 1;
+
+    /** Error code indicating that the payload is verified, but has changed since the last boot. */
+    int ERROR_PAYLOAD_CHANGED = 2;
+
+    /** Error code indicating that the payload config is invalid. */
+    int ERROR_PAYLOAD_INVALID_CONFIG = 3;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(
+            prefix = "STOP_REASON_",
+            value = {
+                STOP_REASON_VIRTUALIZATION_SERVICE_DIED,
+                STOP_REASON_INFRASTRUCTURE_ERROR,
+                STOP_REASON_KILLED,
+                STOP_REASON_UNKNOWN,
+                STOP_REASON_SHUTDOWN,
+                STOP_REASON_START_FAILED,
+                STOP_REASON_REBOOT,
+                STOP_REASON_CRASH,
+                STOP_REASON_PVM_FIRMWARE_PUBLIC_KEY_MISMATCH,
+                STOP_REASON_PVM_FIRMWARE_INSTANCE_IMAGE_CHANGED,
+                STOP_REASON_BOOTLOADER_PUBLIC_KEY_MISMATCH,
+                STOP_REASON_BOOTLOADER_INSTANCE_IMAGE_CHANGED,
+                STOP_REASON_MICRODROID_FAILED_TO_CONNECT_TO_VIRTUALIZATION_SERVICE,
+                STOP_REASON_MICRODROID_PAYLOAD_HAS_CHANGED,
+                STOP_REASON_MICRODROID_PAYLOAD_VERIFICATION_FAILED,
+                STOP_REASON_MICRODROID_INVALID_PAYLOAD_CONFIG,
+                STOP_REASON_MICRODROID_UNKNOWN_RUNTIME_ERROR,
+                STOP_REASON_HANGUP,
+            })
+    @interface StopReason {}
+
+    /** The virtualization service itself died, taking the VM down with it. */
+    //  This is a negative number to avoid conflicting with the other death reasons which match
+    //  the ones in the AIDL interface.
+    int STOP_REASON_VIRTUALIZATION_SERVICE_DIED = -1;
+
+    /** There was an error waiting for the VM. */
+    int STOP_REASON_INFRASTRUCTURE_ERROR = 0;
+
+    /** The VM was killed. */
+    int STOP_REASON_KILLED = 1;
+
+    /** The VM died for an unknown reason. */
+    int STOP_REASON_UNKNOWN = 2;
+
+    /** The VM requested to shut down. */
+    int STOP_REASON_SHUTDOWN = 3;
+
+    /** crosvm had an error starting the VM. */
+    int STOP_REASON_START_FAILED = 4;
+
+    /** The VM requested to reboot, possibly as the result of a kernel panic. */
+    int STOP_REASON_REBOOT = 5;
+
+    /** The VM or crosvm crashed. */
+    int STOP_REASON_CRASH = 6;
+
+    /** The pVM firmware failed to verify the VM because the public key doesn't match. */
+    int STOP_REASON_PVM_FIRMWARE_PUBLIC_KEY_MISMATCH = 7;
+
+    /** The pVM firmware failed to verify the VM because the instance image changed. */
+    int STOP_REASON_PVM_FIRMWARE_INSTANCE_IMAGE_CHANGED = 8;
+
+    /** The bootloader failed to verify the VM because the public key doesn't match. */
+    int STOP_REASON_BOOTLOADER_PUBLIC_KEY_MISMATCH = 9;
+
+    /** The bootloader failed to verify the VM because the instance image changed. */
+    int STOP_REASON_BOOTLOADER_INSTANCE_IMAGE_CHANGED = 10;
+
+    /** The microdroid failed to connect to VirtualizationService's RPC server. */
+    int STOP_REASON_MICRODROID_FAILED_TO_CONNECT_TO_VIRTUALIZATION_SERVICE = 11;
+
+    /** The payload for microdroid is changed. */
+    int STOP_REASON_MICRODROID_PAYLOAD_HAS_CHANGED = 12;
+
+    /** The microdroid failed to verify given payload APK. */
+    int STOP_REASON_MICRODROID_PAYLOAD_VERIFICATION_FAILED = 13;
+
+    /** The VM config for microdroid is invalid (e.g. missing tasks). */
+    int STOP_REASON_MICRODROID_INVALID_PAYLOAD_CONFIG = 14;
+
+    /** There was a runtime error while running microdroid manager. */
+    int STOP_REASON_MICRODROID_UNKNOWN_RUNTIME_ERROR = 15;
+
+    /** The VM killed due to hangup */
+    int STOP_REASON_HANGUP = 16;
+
+    /** Called when the payload starts in the VM. */
+    void onPayloadStarted(@NonNull VirtualMachine vm);
+
+    /**
+     * Called when the payload in the VM is ready to serve. See {@link
+     * VirtualMachine#connectToVsockServer}.
+     */
+    void onPayloadReady(@NonNull VirtualMachine vm);
+
+    /** Called when the payload has finished in the VM. */
+    void onPayloadFinished(@NonNull VirtualMachine vm, int exitCode);
+
+    /** Called when an error occurs in the VM. */
+    void onError(@NonNull VirtualMachine vm, @ErrorCode int errorCode, @NonNull String message);
+
+    /** Called when the VM has stopped. */
+    void onStopped(@NonNull VirtualMachine vm, @StopReason int reason);
+}
diff --git a/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineConfig.java b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineConfig.java
new file mode 100644
index 0000000..7ae4a55
--- /dev/null
+++ b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -0,0 +1,1289 @@
+/*
+ * 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.system.virtualmachine;
+
+import static android.os.ParcelFileDescriptor.AutoCloseInputStream;
+import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
+import static android.os.ParcelFileDescriptor.MODE_READ_WRITE;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.StringDef;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.os.ParcelFileDescriptor;
+import android.os.PersistableBundle;
+import android.sysprop.HypervisorProperties;
+import android.system.virtualizationservice.DiskImage;
+import android.system.virtualizationservice.Partition;
+import android.system.virtualizationservice.VirtualMachineAppConfig;
+import android.system.virtualizationservice.VirtualMachinePayloadConfig;
+import android.system.virtualizationservice.VirtualMachineRawConfig;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.system.virtualmachine.flags.Flags;
+
+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.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.Optional;
+import java.util.zip.ZipFile;
+
+/**
+ * Represents a configuration of a virtual machine. A configuration consists of hardware
+ * configurations like the number of CPUs and the size of RAM, and software configurations like the
+ * payload to run on the virtual machine.
+ *
+ * @hide
+ */
+@SystemApi
+public final class VirtualMachineConfig {
+    private static final String TAG = "VirtualMachineConfig";
+
+    private static String[] EMPTY_STRING_ARRAY = {};
+    private static final String U_BOOT_PREBUILT_PATH = "/apex/com.android.virt/etc/u-boot.bin";
+
+    // These define the schema of the config file persisted on disk.
+    // Please bump up the version number when adding a new key.
+    private static final int VERSION = 10;
+    private static final String KEY_VERSION = "version";
+    private static final String KEY_PACKAGENAME = "packageName";
+    private static final String KEY_APKPATH = "apkPath";
+    private static final String KEY_PAYLOADCONFIGPATH = "payloadConfigPath";
+    private static final String KEY_CUSTOMIMAGECONFIG = "customImageConfig";
+    private static final String KEY_PAYLOADBINARYNAME = "payloadBinaryPath";
+    private static final String KEY_DEBUGLEVEL = "debugLevel";
+    private static final String KEY_PROTECTED_VM = "protectedVm";
+    private static final String KEY_MEMORY_BYTES = "memoryBytes";
+    private static final String KEY_CPU_TOPOLOGY = "cpuTopology";
+    private static final String KEY_CONSOLE_INPUT_DEVICE = "consoleInputDevice";
+    private static final String KEY_ENCRYPTED_STORAGE_BYTES = "encryptedStorageBytes";
+    private static final String KEY_VM_OUTPUT_CAPTURED = "vmOutputCaptured";
+    private static final String KEY_VM_CONSOLE_INPUT_SUPPORTED = "vmConsoleInputSupported";
+    private static final String KEY_CONNECT_VM_CONSOLE = "connectVmConsole";
+    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";
+    private static final String KEY_SHOULD_BOOST_UCLAMP = "shouldBoostUclamp";
+    private static final String KEY_SHOULD_USE_HUGEPAGES = "shouldUseHugepages";
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(
+            prefix = "DEBUG_LEVEL_",
+            value = {DEBUG_LEVEL_NONE, DEBUG_LEVEL_FULL})
+    public @interface DebugLevel {}
+
+    /**
+     * Not debuggable at all. No log is exported from the VM. Debugger can't be attached to the app
+     * process running in the VM. This is the default level.
+     *
+     * @hide
+     */
+    @SystemApi public static final int DEBUG_LEVEL_NONE = 0;
+
+    /**
+     * Fully debuggable. All logs (both logcat and kernel message) are exported. All processes
+     * running in the VM can be attached to the debugger. Rooting is possible.
+     *
+     * @hide
+     */
+    @SystemApi public static final int DEBUG_LEVEL_FULL = 1;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(
+            prefix = "CPU_TOPOLOGY_",
+            value = {
+                CPU_TOPOLOGY_ONE_CPU,
+                CPU_TOPOLOGY_MATCH_HOST,
+            })
+    public @interface CpuTopology {}
+
+    /**
+     * Run VM with 1 vCPU. This is the default option, usually the fastest to boot and consuming the
+     * least amount of resources. Typically the best option for small or ephemeral workloads.
+     *
+     * @hide
+     */
+    @SystemApi public static final int CPU_TOPOLOGY_ONE_CPU = 0;
+
+    /**
+     * Run VM with vCPU topology matching the physical CPU topology of the host. Usually takes
+     * longer to boot and consumes more resources compared to a single vCPU. Typically a good option
+     * for long-running workloads that benefit from parallel execution.
+     *
+     * @hide
+     */
+    @SystemApi public static final int CPU_TOPOLOGY_MATCH_HOST = 1;
+
+    /** Name of a package whose primary APK contains the VM payload. */
+    @Nullable private final String mPackageName;
+
+    /** 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;
+
+    /** Whether to run the VM in protected mode, so the host can't access its memory. */
+    private final boolean mProtectedVm;
+
+    /**
+     * The amount of RAM to give the VM, in bytes. If this is 0 or negative the default will be
+     * used.
+     */
+    private final long mMemoryBytes;
+
+    /** CPU topology configuration of the VM. */
+    @CpuTopology private final int mCpuTopology;
+
+    /** The serial device for VM console input. */
+    @Nullable private final String mConsoleInputDevice;
+
+    /** Path within the APK to the payload config file that defines software aspects of the VM. */
+    @Nullable private final String mPayloadConfigPath;
+
+    /** Name of the payload binary file within the APK that will be executed within the VM. */
+    @Nullable private final String mPayloadBinaryName;
+
+    /** The custom image config file to launch the custom VM. */
+    @Nullable private final VirtualMachineCustomImageConfig mCustomImageConfig;
+
+    /** The size of storage in bytes. 0 indicates that encryptedStorage is not required */
+    private final long mEncryptedStorageBytes;
+
+    /** Whether the app can read console and log output. */
+    private final boolean mVmOutputCaptured;
+
+    /** Whether the app can write console input to the VM */
+    private final boolean mVmConsoleInputSupported;
+
+    /** Whether to connect the VM console to a host console. */
+    private final boolean mConnectVmConsole;
+
+    @Nullable private final File mVendorDiskImage;
+
+    /** OS name of the VM using payload binaries. */
+    @NonNull @OsName private final String mOs;
+
+    private final boolean mShouldBoostUclamp;
+
+    private final boolean mShouldUseHugepages;
+
+    @Retention(RetentionPolicy.SOURCE)
+    @StringDef(
+            prefix = "MICRODROID",
+            value = {MICRODROID})
+    private @interface OsName {}
+
+    /**
+     * OS name of microdroid using microdroid kernel.
+     *
+     * @see Builder#setOs
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS)
+    @OsName
+    public static final String MICRODROID = "microdroid";
+
+    private VirtualMachineConfig(
+            @Nullable String packageName,
+            @Nullable String apkPath,
+            List<String> extraApks,
+            @Nullable String payloadConfigPath,
+            @Nullable String payloadBinaryName,
+            @Nullable VirtualMachineCustomImageConfig customImageConfig,
+            @DebugLevel int debugLevel,
+            boolean protectedVm,
+            long memoryBytes,
+            @CpuTopology int cpuTopology,
+            @Nullable String consoleInputDevice,
+            long encryptedStorageBytes,
+            boolean vmOutputCaptured,
+            boolean vmConsoleInputSupported,
+            boolean connectVmConsole,
+            @Nullable File vendorDiskImage,
+            @NonNull @OsName String os,
+            boolean shouldBoostUclamp,
+            boolean shouldUseHugepages) {
+        // 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;
+        mCustomImageConfig = customImageConfig;
+        mDebugLevel = debugLevel;
+        mProtectedVm = protectedVm;
+        mMemoryBytes = memoryBytes;
+        mCpuTopology = cpuTopology;
+        mConsoleInputDevice = consoleInputDevice;
+        mEncryptedStorageBytes = encryptedStorageBytes;
+        mVmOutputCaptured = vmOutputCaptured;
+        mVmConsoleInputSupported = vmConsoleInputSupported;
+        mConnectVmConsole = connectVmConsole;
+        mVendorDiskImage = vendorDiskImage;
+        mOs = os;
+        mShouldBoostUclamp = shouldBoostUclamp;
+        mShouldUseHugepages = shouldUseHugepages;
+    }
+
+    /** Loads a config from a file. */
+    @NonNull
+    static VirtualMachineConfig from(@NonNull File file) throws VirtualMachineException {
+        try (FileInputStream input = new FileInputStream(file)) {
+            return fromInputStream(input);
+        } catch (IOException e) {
+            throw new VirtualMachineException("Failed to read VM config from file", e);
+        }
+    }
+
+    /** Loads a config from a {@link ParcelFileDescriptor}. */
+    @NonNull
+    static VirtualMachineConfig from(@NonNull ParcelFileDescriptor fd)
+            throws VirtualMachineException {
+        try (AutoCloseInputStream input = new AutoCloseInputStream(fd)) {
+            return fromInputStream(input);
+        } catch (IOException e) {
+            throw new VirtualMachineException("failed to read VM config from file descriptor", e);
+        }
+    }
+
+    /** Loads a config from a stream, for example a file. */
+    @NonNull
+    private static VirtualMachineConfig fromInputStream(@NonNull InputStream input)
+            throws IOException, VirtualMachineException {
+        PersistableBundle b = PersistableBundle.readFromStream(input);
+        try {
+            return fromPersistableBundle(b);
+        } catch (NullPointerException | IllegalArgumentException | IllegalStateException e) {
+            throw new VirtualMachineException("Persisted VM config is invalid", e);
+        }
+    }
+
+    @NonNull
+    private static VirtualMachineConfig fromPersistableBundle(PersistableBundle b) {
+        int version = b.getInt(KEY_VERSION);
+        if (version > VERSION) {
+            throw new IllegalArgumentException(
+                    "Version " + version + " too high; current is " + VERSION);
+        }
+
+        String packageName = b.getString(KEY_PACKAGENAME);
+        Builder builder = new Builder(packageName);
+
+        String apkPath = b.getString(KEY_APKPATH);
+        if (apkPath != null) {
+            builder.setApkPath(apkPath);
+        }
+
+        String payloadConfigPath = b.getString(KEY_PAYLOADCONFIGPATH);
+        String payloadBinaryName = b.getString(KEY_PAYLOADBINARYNAME);
+        PersistableBundle customImageConfigBundle = b.getPersistableBundle(KEY_CUSTOMIMAGECONFIG);
+        if (customImageConfigBundle != null) {
+            builder.setCustomImageConfig(
+                    VirtualMachineCustomImageConfig.from(customImageConfigBundle));
+        } else if (payloadConfigPath != null) {
+            builder.setPayloadConfigPath(payloadConfigPath);
+        } else {
+            builder.setPayloadBinaryName(payloadBinaryName);
+        }
+
+        @DebugLevel int debugLevel = b.getInt(KEY_DEBUGLEVEL);
+        if (debugLevel != DEBUG_LEVEL_NONE && debugLevel != DEBUG_LEVEL_FULL) {
+            throw new IllegalArgumentException("Invalid debugLevel: " + debugLevel);
+        }
+        builder.setDebugLevel(debugLevel);
+        builder.setProtectedVm(b.getBoolean(KEY_PROTECTED_VM));
+        long memoryBytes = b.getLong(KEY_MEMORY_BYTES);
+        if (memoryBytes != 0) {
+            builder.setMemoryBytes(memoryBytes);
+        }
+        builder.setCpuTopology(b.getInt(KEY_CPU_TOPOLOGY));
+        String consoleInputDevice = b.getString(KEY_CONSOLE_INPUT_DEVICE);
+        if (consoleInputDevice != null) {
+            builder.setConsoleInputDevice(consoleInputDevice);
+        }
+        long encryptedStorageBytes = b.getLong(KEY_ENCRYPTED_STORAGE_BYTES);
+        if (encryptedStorageBytes != 0) {
+            builder.setEncryptedStorageBytes(encryptedStorageBytes);
+        }
+        builder.setVmOutputCaptured(b.getBoolean(KEY_VM_OUTPUT_CAPTURED));
+        builder.setVmConsoleInputSupported(b.getBoolean(KEY_VM_CONSOLE_INPUT_SUPPORTED));
+        builder.setConnectVmConsole(b.getBoolean(KEY_CONNECT_VM_CONSOLE));
+
+        String vendorDiskImagePath = b.getString(KEY_VENDOR_DISK_IMAGE_PATH);
+        if (vendorDiskImagePath != null) {
+            builder.setVendorDiskImage(new File(vendorDiskImagePath));
+        }
+
+        builder.setOs(b.getString(KEY_OS));
+
+        String[] extraApks = b.getStringArray(KEY_EXTRA_APKS);
+        if (extraApks != null) {
+            for (String extraApk : extraApks) {
+                builder.addExtraApk(extraApk);
+            }
+        }
+
+        builder.setShouldBoostUclamp(b.getBoolean(KEY_SHOULD_BOOST_UCLAMP));
+        builder.setShouldUseHugepages(b.getBoolean(KEY_SHOULD_USE_HUGEPAGES));
+
+        return builder.build();
+    }
+
+    /** Persists this config to a file. */
+    void serialize(@NonNull File file) throws VirtualMachineException {
+        try (FileOutputStream output = new FileOutputStream(file)) {
+            serializeOutputStream(output);
+        } catch (IOException e) {
+            throw new VirtualMachineException("failed to write VM config", e);
+        }
+    }
+
+    /** Persists this config to a stream, for example a file. */
+    private void serializeOutputStream(@NonNull OutputStream output) throws IOException {
+        PersistableBundle b = new PersistableBundle();
+        b.putInt(KEY_VERSION, VERSION);
+        if (mPackageName != null) {
+            b.putString(KEY_PACKAGENAME, mPackageName);
+        }
+        if (mApkPath != null) {
+            b.putString(KEY_APKPATH, mApkPath);
+        }
+        b.putString(KEY_PAYLOADCONFIGPATH, mPayloadConfigPath);
+        b.putString(KEY_PAYLOADBINARYNAME, mPayloadBinaryName);
+        if (mCustomImageConfig != null) {
+            b.putPersistableBundle(KEY_CUSTOMIMAGECONFIG, mCustomImageConfig.toPersistableBundle());
+        }
+        b.putInt(KEY_DEBUGLEVEL, mDebugLevel);
+        b.putBoolean(KEY_PROTECTED_VM, mProtectedVm);
+        b.putInt(KEY_CPU_TOPOLOGY, mCpuTopology);
+        if (mConsoleInputDevice != null) {
+            b.putString(KEY_CONSOLE_INPUT_DEVICE, mConsoleInputDevice);
+        }
+        if (mMemoryBytes > 0) {
+            b.putLong(KEY_MEMORY_BYTES, mMemoryBytes);
+        }
+        if (mEncryptedStorageBytes > 0) {
+            b.putLong(KEY_ENCRYPTED_STORAGE_BYTES, mEncryptedStorageBytes);
+        }
+        b.putBoolean(KEY_VM_OUTPUT_CAPTURED, mVmOutputCaptured);
+        b.putBoolean(KEY_VM_CONSOLE_INPUT_SUPPORTED, mVmConsoleInputSupported);
+        b.putBoolean(KEY_CONNECT_VM_CONSOLE, mConnectVmConsole);
+        if (mVendorDiskImage != null) {
+            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.putBoolean(KEY_SHOULD_BOOST_UCLAMP, mShouldBoostUclamp);
+        b.putBoolean(KEY_SHOULD_USE_HUGEPAGES, mShouldUseHugepages);
+        b.writeToStream(output);
+    }
+
+    /**
+     * Returns the absolute path of the APK which should contain the binary payload that will
+     * execute within the VM. Returns null if no specific path has been set.
+     *
+     * @hide
+     */
+    @SystemApi
+    @Nullable
+    public String getApkPath() {
+        return mApkPath;
+    }
+
+    /**
+     * 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(Flags.FLAG_AVF_V_TEST_APIS)
+    @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.
+     *
+     * @hide
+     */
+    @TestApi
+    @Nullable
+    public String getPayloadConfigPath() {
+        return mPayloadConfigPath;
+    }
+
+    /**
+     * Returns the custom image config to launch the custom VM.
+     *
+     * @hide
+     */
+    @Nullable
+    public VirtualMachineCustomImageConfig getCustomImageConfig() {
+        return mCustomImageConfig;
+    }
+
+    /**
+     * Returns the name of the payload binary file, in the {@code lib/<ABI>} directory of the APK,
+     * that will be executed within the VM.
+     *
+     * @hide
+     */
+    @SystemApi
+    @Nullable
+    public String getPayloadBinaryName() {
+        return mPayloadBinaryName;
+    }
+
+    /**
+     * Returns the debug level for the VM.
+     *
+     * @hide
+     */
+    @SystemApi
+    @DebugLevel
+    public int getDebugLevel() {
+        return mDebugLevel;
+    }
+
+    /**
+     * Returns whether the VM's memory will be protected from the host.
+     *
+     * @hide
+     */
+    @SystemApi
+    public boolean isProtectedVm() {
+        return mProtectedVm;
+    }
+
+    /**
+     * Returns the amount of RAM that will be made available to the VM, or 0 if the default size
+     * will be used.
+     *
+     * @hide
+     */
+    @SystemApi
+    @IntRange(from = 0)
+    public long getMemoryBytes() {
+        return mMemoryBytes;
+    }
+
+    /**
+     * Returns the CPU topology configuration of the VM.
+     *
+     * @hide
+     */
+    @SystemApi
+    @CpuTopology
+    public int getCpuTopology() {
+        return mCpuTopology;
+    }
+
+    /**
+     * Returns whether encrypted storage is enabled or not.
+     *
+     * @hide
+     */
+    @SystemApi
+    public boolean isEncryptedStorageEnabled() {
+        return mEncryptedStorageBytes > 0;
+    }
+
+    /**
+     * Returns the size of encrypted storage (in bytes) available in the VM, or 0 if encrypted
+     * storage is not enabled
+     *
+     * @hide
+     */
+    @SystemApi
+    @IntRange(from = 0)
+    public long getEncryptedStorageBytes() {
+        return mEncryptedStorageBytes;
+    }
+
+    /**
+     * Returns whether the app can read the VM console or log output. If not, the VM output is
+     * automatically forwarded to the host logcat.
+     *
+     * @see Builder#setVmOutputCaptured
+     * @hide
+     */
+    @SystemApi
+    public boolean isVmOutputCaptured() {
+        return mVmOutputCaptured;
+    }
+
+    /**
+     * Returns whether the app can write to the VM console.
+     *
+     * @see Builder#setVmConsoleInputSupported
+     * @hide
+     */
+    @TestApi
+    public boolean isVmConsoleInputSupported() {
+        return mVmConsoleInputSupported;
+    }
+
+    /**
+     * Returns whether to connect the VM console to a host console.
+     *
+     * @see Builder#setConnectVmConsole
+     * @hide
+     */
+    public boolean isConnectVmConsole() {
+        return mConnectVmConsole;
+    }
+
+    /**
+     * Returns the OS of the VM.
+     *
+     * @see Builder#setOs
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS)
+    @NonNull
+    @OsName
+    public String getOs() {
+        return mOs;
+    }
+
+    /**
+     * Tests if this config is compatible with other config. Being compatible means that the configs
+     * can be interchangeably used for the same virtual machine; they do not change the VM identity
+     * or secrets. Such changes include varying the number of CPUs or the size of the RAM. Changes
+     * that would alter the identity of the VM (e.g. using a different payload or changing the debug
+     * mode) are considered incompatible.
+     *
+     * @see VirtualMachine#setConfig
+     * @hide
+     */
+    @SystemApi
+    public boolean isCompatibleWith(@NonNull VirtualMachineConfig other) {
+        if (this == other) {
+            return true;
+        }
+        return this.mDebugLevel == other.mDebugLevel
+                && this.mProtectedVm == other.mProtectedVm
+                && this.mEncryptedStorageBytes == other.mEncryptedStorageBytes
+                && this.mVmOutputCaptured == other.mVmOutputCaptured
+                && this.mVmConsoleInputSupported == other.mVmConsoleInputSupported
+                && this.mConnectVmConsole == other.mConnectVmConsole
+                && this.mConsoleInputDevice == other.mConsoleInputDevice
+                && (this.mVendorDiskImage == null) == (other.mVendorDiskImage == null)
+                && 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.mExtraApks, other.mExtraApks);
+    }
+
+    private ParcelFileDescriptor openOrNull(File file, int mode) {
+        try {
+            return ParcelFileDescriptor.open(file, mode);
+        } catch (FileNotFoundException e) {
+            Log.d(TAG, "cannot open", e);
+            return null;
+        }
+    }
+
+    VirtualMachineRawConfig toVsRawConfig() throws IllegalStateException, IOException {
+        VirtualMachineRawConfig config = new VirtualMachineRawConfig();
+        VirtualMachineCustomImageConfig customImageConfig = getCustomImageConfig();
+        requireNonNull(customImageConfig);
+        config.name = Optional.ofNullable(customImageConfig.getName()).orElse("");
+        config.instanceId = new byte[64];
+        config.kernel =
+                Optional.ofNullable(customImageConfig.getKernelPath())
+                        .map(
+                                (path) -> {
+                                    try {
+                                        return ParcelFileDescriptor.open(
+                                                new File(path), MODE_READ_ONLY);
+                                    } catch (FileNotFoundException e) {
+                                        throw new RuntimeException(e);
+                                    }
+                                })
+                        .orElse(null);
+
+        config.initrd =
+                Optional.ofNullable(customImageConfig.getInitrdPath())
+                        .map((path) -> openOrNull(new File(path), MODE_READ_ONLY))
+                        .orElse(null);
+        config.bootloader =
+                Optional.ofNullable(customImageConfig.getBootloaderPath())
+                        .map((path) -> openOrNull(new File(path), MODE_READ_ONLY))
+                        .orElse(null);
+
+        if (config.kernel == null && config.bootloader == null) {
+            config.bootloader = openOrNull(new File(U_BOOT_PREBUILT_PATH), MODE_READ_ONLY);
+        }
+
+        config.params =
+                Optional.ofNullable(customImageConfig.getParams())
+                        .map((params) -> TextUtils.join(" ", params))
+                        .orElse("");
+        config.disks =
+                new DiskImage
+                        [Optional.ofNullable(customImageConfig.getDisks())
+                                .map(arr -> arr.length)
+                                .orElse(0)];
+        for (int i = 0; i < config.disks.length; i++) {
+            config.disks[i] = new DiskImage();
+            config.disks[i].writable = customImageConfig.getDisks()[i].isWritable();
+            String diskImagePath = customImageConfig.getDisks()[i].getImagePath();
+            if (diskImagePath != null) {
+                config.disks[i].image =
+                        ParcelFileDescriptor.open(
+                                new File(diskImagePath),
+                                config.disks[i].writable ? MODE_READ_WRITE : MODE_READ_ONLY);
+            }
+
+            List<Partition> partitions = new ArrayList<>();
+            for (VirtualMachineCustomImageConfig.Partition p :
+                    customImageConfig.getDisks()[i].getPartitions()) {
+                Partition part = new Partition();
+                part.label = p.name;
+                part.image =
+                        ParcelFileDescriptor.open(
+                                new File(p.imagePath),
+                                p.writable ? MODE_READ_WRITE : MODE_READ_ONLY);
+                part.writable = p.writable;
+                part.guid = TextUtils.isEmpty(p.guid) ? null : p.guid;
+                partitions.add(part);
+            }
+            config.disks[i].partitions = partitions.toArray(new Partition[0]);
+        }
+
+        config.displayConfig =
+                Optional.ofNullable(customImageConfig.getDisplayConfig())
+                        .map(dc -> dc.toParcelable())
+                        .orElse(null);
+        config.gpuConfig =
+                Optional.ofNullable(customImageConfig.getGpuConfig())
+                        .map(dc -> dc.toParcelable())
+                        .orElse(null);
+        config.protectedVm = this.mProtectedVm;
+        config.memoryMib = bytesToMebiBytes(mMemoryBytes);
+        config.cpuTopology = (byte) this.mCpuTopology;
+        config.consoleInputDevice = mConsoleInputDevice;
+        config.devices = EMPTY_STRING_ARRAY;
+        config.platformVersion = "~1.0";
+        config.audioConfig =
+                Optional.ofNullable(customImageConfig.getAudioConfig())
+                        .map(ac -> ac.toParcelable())
+                        .orElse(null);
+        return config;
+    }
+
+    /**
+     * Converts this config object into the parcelable type 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 could be abused to run a VM with software that the calling
+     * application doesn't own.
+     */
+    VirtualMachineAppConfig toVsConfig(@NonNull PackageManager packageManager)
+            throws VirtualMachineException {
+        VirtualMachineAppConfig vsConfig = new VirtualMachineAppConfig();
+
+        String apkPath = (mApkPath != null) ? mApkPath : findPayloadApk(packageManager);
+
+        try {
+            vsConfig.apk = ParcelFileDescriptor.open(new File(apkPath), MODE_READ_ONLY);
+        } catch (FileNotFoundException e) {
+            throw new VirtualMachineException("Failed to open APK", e);
+        }
+        if (mPayloadBinaryName != null) {
+            VirtualMachinePayloadConfig payloadConfig = new VirtualMachinePayloadConfig();
+            payloadConfig.payloadBinaryName = mPayloadBinaryName;
+            payloadConfig.extraApks = Collections.emptyList();
+            vsConfig.payload = VirtualMachineAppConfig.Payload.payloadConfig(payloadConfig);
+        } else {
+            vsConfig.payload = VirtualMachineAppConfig.Payload.configPath(mPayloadConfigPath);
+        }
+        vsConfig.osName = mOs;
+        switch (mDebugLevel) {
+            case DEBUG_LEVEL_FULL:
+                vsConfig.debugLevel = VirtualMachineAppConfig.DebugLevel.FULL;
+                break;
+            default:
+                vsConfig.debugLevel = VirtualMachineAppConfig.DebugLevel.NONE;
+                break;
+        }
+        vsConfig.protectedVm = mProtectedVm;
+        vsConfig.memoryMib = bytesToMebiBytes(mMemoryBytes);
+        switch (mCpuTopology) {
+            case CPU_TOPOLOGY_MATCH_HOST:
+                vsConfig.cpuTopology = android.system.virtualizationservice.CpuTopology.MATCH_HOST;
+                break;
+            default:
+                vsConfig.cpuTopology = android.system.virtualizationservice.CpuTopology.ONE_CPU;
+                break;
+        }
+
+        if (mVendorDiskImage != null) {
+            VirtualMachineAppConfig.CustomConfig customConfig =
+                    new VirtualMachineAppConfig.CustomConfig();
+            customConfig.devices = EMPTY_STRING_ARRAY;
+            try {
+                customConfig.vendorImage =
+                        ParcelFileDescriptor.open(mVendorDiskImage, MODE_READ_ONLY);
+            } catch (FileNotFoundException e) {
+                throw new VirtualMachineException(
+                        "Failed to open vendor disk image " + mVendorDiskImage.getAbsolutePath(),
+                        e);
+            }
+            vsConfig.customConfig = customConfig;
+        }
+
+        vsConfig.boostUclamp = mShouldBoostUclamp;
+        vsConfig.hugePages = mShouldUseHugepages;
+
+        return vsConfig;
+    }
+
+    private String findPayloadApk(PackageManager packageManager) throws VirtualMachineException {
+        ApplicationInfo appInfo;
+        try {
+            appInfo =
+                    packageManager.getApplicationInfo(
+                            mPackageName, PackageManager.ApplicationInfoFlags.of(0));
+        } catch (PackageManager.NameNotFoundException e) {
+            throw new VirtualMachineException("Package not found", e);
+        }
+
+        String[] splitApkPaths = appInfo.splitSourceDirs;
+        String[] abis = Build.SUPPORTED_64_BIT_ABIS;
+
+        // If there are split APKs, and we know the payload binary name, see if we can find a
+        // split APK containing the binary.
+        if (mPayloadBinaryName != null && splitApkPaths != null && abis.length != 0) {
+            String[] libraryNames = new String[abis.length];
+            for (int i = 0; i < abis.length; i++) {
+                libraryNames[i] = "lib/" + abis[i] + "/" + mPayloadBinaryName;
+            }
+
+            for (String path : splitApkPaths) {
+                try (ZipFile zip = new ZipFile(path)) {
+                    for (String name : libraryNames) {
+                        if (zip.getEntry(name) != null) {
+                            Log.i(TAG, "Found payload in " + path);
+                            return path;
+                        }
+                    }
+                } catch (IOException e) {
+                    Log.w(TAG, "Failed to scan split APK: " + path, e);
+                }
+            }
+        }
+
+        // This really is the path to the APK, not a directory.
+        return appInfo.sourceDir;
+    }
+
+    private int bytesToMebiBytes(long mMemoryBytes) {
+        long oneMebi = 1024 * 1024;
+        // We can't express requests for more than 2 exabytes, but then they're not going to succeed
+        // anyway.
+        if (mMemoryBytes > (Integer.MAX_VALUE - 1) * oneMebi) {
+            return Integer.MAX_VALUE;
+        }
+        return (int) ((mMemoryBytes + oneMebi - 1) / oneMebi);
+    }
+
+    /**
+     * A builder used to create a {@link VirtualMachineConfig}.
+     *
+     * @hide
+     */
+    @SystemApi
+    public static final class Builder {
+        @OsName private final String DEFAULT_OS = MICRODROID;
+
+        @Nullable private final String mPackageName;
+        @Nullable private String mApkPath;
+        private final List<String> mExtraApks = new ArrayList<>();
+        @Nullable private String mPayloadConfigPath;
+        @Nullable private VirtualMachineCustomImageConfig mCustomImageConfig;
+        @Nullable private String mPayloadBinaryName;
+        @DebugLevel private int mDebugLevel = DEBUG_LEVEL_NONE;
+        private boolean mProtectedVm;
+        private boolean mProtectedVmSet;
+        private long mMemoryBytes;
+        @CpuTopology private int mCpuTopology = CPU_TOPOLOGY_ONE_CPU;
+        @Nullable private String mConsoleInputDevice;
+        private long mEncryptedStorageBytes;
+        private boolean mVmOutputCaptured = false;
+        private boolean mVmConsoleInputSupported = false;
+        private boolean mConnectVmConsole = false;
+        @Nullable private File mVendorDiskImage;
+        @NonNull @OsName private String mOs = DEFAULT_OS;
+        private boolean mShouldBoostUclamp = false;
+        private boolean mShouldUseHugepages = false;
+
+        /**
+         * Creates a builder for the given context.
+         *
+         * @hide
+         */
+        @SystemApi
+        public Builder(@NonNull Context context) {
+            mPackageName = requireNonNull(context, "context must not be null").getPackageName();
+        }
+
+        /**
+         * Creates a builder for a specific package. If packageName is null, {@link #setApkPath}
+         * must be called to specify the APK containing the payload.
+         */
+        private Builder(@Nullable String packageName) {
+            mPackageName = packageName;
+        }
+
+        /**
+         * Builds an immutable {@link VirtualMachineConfig}
+         *
+         * @hide
+         */
+        @SystemApi
+        @NonNull
+        public VirtualMachineConfig build() {
+            String apkPath = null;
+            String packageName = null;
+
+            if (mApkPath != null) {
+                apkPath = mApkPath;
+            } else if (mPackageName != null) {
+                packageName = mPackageName;
+            } else {
+                // This should never happen, unless we're deserializing a bad config
+                throw new IllegalStateException("apkPath or packageName must be specified");
+            }
+            if (mCustomImageConfig != null) {
+                if (mPayloadBinaryName != null || mPayloadConfigPath != null) {
+                    throw new IllegalStateException(
+                            "setCustomImageConfig and (setPayloadBinaryName or"
+                                    + " setPayloadConfigPath) may not both be called");
+                }
+            } else if (mPayloadBinaryName == null) {
+                if (mPayloadConfigPath == null) {
+                    throw new IllegalStateException("setPayloadBinaryName must be called");
+                }
+                if (!mExtraApks.isEmpty()) {
+                    throw new IllegalStateException(
+                            "setPayloadConfigPath and addExtraApk may not both be called");
+                }
+            } else {
+                if (mPayloadConfigPath != null) {
+                    throw new IllegalStateException(
+                            "setPayloadBinaryName and setPayloadConfigPath may not both be called");
+                }
+            }
+
+            if (!mProtectedVmSet) {
+                throw new IllegalStateException("setProtectedVm must be called explicitly");
+            }
+
+            if (mVmOutputCaptured && mDebugLevel != DEBUG_LEVEL_FULL) {
+                throw new IllegalStateException("debug level must be FULL to capture output");
+            }
+
+            if (mVmConsoleInputSupported && mDebugLevel != DEBUG_LEVEL_FULL) {
+                throw new IllegalStateException("debug level must be FULL to use console input");
+            }
+
+            if (mConnectVmConsole && mDebugLevel != DEBUG_LEVEL_FULL) {
+                throw new IllegalStateException(
+                        "debug level must be FULL to connect to the console");
+            }
+
+            return new VirtualMachineConfig(
+                    packageName,
+                    apkPath,
+                    mExtraApks,
+                    mPayloadConfigPath,
+                    mPayloadBinaryName,
+                    mCustomImageConfig,
+                    mDebugLevel,
+                    mProtectedVm,
+                    mMemoryBytes,
+                    mCpuTopology,
+                    mConsoleInputDevice,
+                    mEncryptedStorageBytes,
+                    mVmOutputCaptured,
+                    mVmConsoleInputSupported,
+                    mConnectVmConsole,
+                    mVendorDiskImage,
+                    mOs,
+                    mShouldBoostUclamp,
+                    mShouldUseHugepages);
+        }
+
+        /**
+         * Sets the absolute path of the APK containing the binary payload that will execute within
+         * the VM. If not set explicitly, defaults to the split APK containing the payload, if there
+         * is one, and otherwise the primary APK of the context.
+         *
+         * @hide
+         */
+        @SystemApi
+        @NonNull
+        public Builder setApkPath(@NonNull String apkPath) {
+            requireNonNull(apkPath, "apkPath must not be null");
+            if (!apkPath.startsWith("/")) {
+                throw new IllegalArgumentException("APK path must be an absolute path");
+            }
+            mApkPath = apkPath;
+            return this;
+        }
+
+        /**
+         * 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(Flags.FLAG_AVF_V_TEST_APIS)
+        @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/libs/libmicrodroid_payload_metadata/config/src/lib.rs for
+         * the format.
+         *
+         * @hide
+         */
+        @RequiresPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION)
+        @TestApi
+        @NonNull
+        public Builder setPayloadConfigPath(@NonNull String payloadConfigPath) {
+            mPayloadConfigPath =
+                    requireNonNull(payloadConfigPath, "payloadConfigPath must not be null");
+            return this;
+        }
+
+        /**
+         * Sets the custom config file to launch the custom VM.
+         *
+         * @hide
+         */
+        @RequiresPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION)
+        @NonNull
+        public Builder setCustomImageConfig(
+                @NonNull VirtualMachineCustomImageConfig customImageConfig) {
+            this.mCustomImageConfig = customImageConfig;
+            return this;
+        }
+
+        /**
+         * Sets the name of the payload binary file that will be executed within the VM, e.g.
+         * "payload.so". The file must reside in the {@code lib/<ABI>} directory of the APK.
+         *
+         * <p>Note that VMs only support 64-bit code, even if the owning app is running as a 32-bit
+         * process.
+         *
+         * @hide
+         */
+        @SystemApi
+        @NonNull
+        public Builder setPayloadBinaryName(@NonNull String payloadBinaryName) {
+            requireNonNull(payloadBinaryName, "payloadBinaryName must not be null");
+            if (payloadBinaryName.contains(File.separator)) {
+                throw new IllegalArgumentException(
+                        "Invalid binary file name: " + payloadBinaryName);
+            }
+            mPayloadBinaryName = payloadBinaryName;
+            return this;
+        }
+
+        /**
+         * Sets the debug level. Defaults to {@link #DEBUG_LEVEL_NONE}.
+         *
+         * <p>If {@link #DEBUG_LEVEL_FULL} is set then logs from inside the VM are exported to the
+         * host and adb connections from the host are possible. This is convenient for debugging but
+         * may compromise the integrity of the VM - including bypassing the protections offered by a
+         * {@linkplain #setProtectedVm protected VM}.
+         *
+         * <p>Note that it isn't possible to {@linkplain #isCompatibleWith change} the debug level
+         * of a VM instance; debug and non-debug VMs always have different secrets.
+         *
+         * @hide
+         */
+        @SystemApi
+        @NonNull
+        public Builder setDebugLevel(@DebugLevel int debugLevel) {
+            if (debugLevel != DEBUG_LEVEL_NONE && debugLevel != DEBUG_LEVEL_FULL) {
+                throw new IllegalArgumentException("Invalid debugLevel: " + debugLevel);
+            }
+            mDebugLevel = debugLevel;
+            return this;
+        }
+
+        /**
+         * Sets whether to protect the VM memory from the host. No default is provided, this must be
+         * set explicitly.
+         *
+         * <p>Note that if debugging is {@linkplain #setDebugLevel enabled} for a protected VM, the
+         * VM is not truly protected - direct memory access by the host is prevented, but e.g. the
+         * debugger can be used to access the VM's internals.
+         *
+         * <p>It isn't possible to {@linkplain #isCompatibleWith change} the protected status of a
+         * VM instance; protected and non-protected VMs always have different secrets.
+         *
+         * @see VirtualMachineManager#getCapabilities
+         * @hide
+         */
+        @SystemApi
+        @NonNull
+        public Builder setProtectedVm(boolean protectedVm) {
+            if (protectedVm) {
+                if (!HypervisorProperties.hypervisor_protected_vm_supported().orElse(false)) {
+                    throw new UnsupportedOperationException(
+                            "Protected VMs are not supported on this device.");
+                }
+            } else {
+                if (!HypervisorProperties.hypervisor_vm_supported().orElse(false)) {
+                    throw new UnsupportedOperationException(
+                            "Non-protected VMs are not supported on this device.");
+                }
+            }
+            mProtectedVm = protectedVm;
+            mProtectedVmSet = true;
+            return this;
+        }
+
+        /**
+         * Sets the amount of RAM to give the VM, in bytes. If not explicitly set then a default
+         * size will be used.
+         *
+         * @hide
+         */
+        @SystemApi
+        @NonNull
+        public Builder setMemoryBytes(@IntRange(from = 1) long memoryBytes) {
+            if (memoryBytes <= 0) {
+                throw new IllegalArgumentException("Memory size must be positive");
+            }
+            mMemoryBytes = memoryBytes;
+            return this;
+        }
+
+        /**
+         * Sets the CPU topology configuration of the VM. Defaults to {@link #CPU_TOPOLOGY_ONE_CPU}.
+         *
+         * <p>This determines how many virtual CPUs will be created, and their performance and
+         * scheduling characteristics, such as affinity masks. Topology also has an effect on memory
+         * usage as each vCPU requires additional memory to keep its state.
+         *
+         * @hide
+         */
+        @SystemApi
+        @NonNull
+        public Builder setCpuTopology(@CpuTopology int cpuTopology) {
+            if (cpuTopology != CPU_TOPOLOGY_ONE_CPU && cpuTopology != CPU_TOPOLOGY_MATCH_HOST) {
+                throw new IllegalArgumentException("Invalid cpuTopology: " + cpuTopology);
+            }
+            mCpuTopology = cpuTopology;
+            return this;
+        }
+
+        /**
+         * Sets the serial device for VM console input.
+         *
+         * @see android.system.virtualizationservice.ConsoleInputDevice
+         * @hide
+         */
+        public Builder setConsoleInputDevice(@Nullable String consoleInputDevice) {
+            mConsoleInputDevice = consoleInputDevice;
+            return this;
+        }
+
+        /**
+         * Sets the size (in bytes) of encrypted storage available to the VM. If not set, no
+         * encrypted storage is provided.
+         *
+         * <p>The storage is encrypted with a key deterministically derived from the VM identity
+         *
+         * <p>The encrypted storage is persistent across VM reboots as well as device reboots. The
+         * backing file (containing encrypted data) is stored in the app's private data directory.
+         *
+         * <p>Note - There is no integrity guarantee or rollback protection on the storage in case
+         * the encrypted data is modified.
+         *
+         * <p>Deleting the VM will delete the encrypted data - there is no way to recover that data.
+         *
+         * @hide
+         */
+        @SystemApi
+        @NonNull
+        public Builder setEncryptedStorageBytes(@IntRange(from = 1) long encryptedStorageBytes) {
+            if (encryptedStorageBytes <= 0) {
+                throw new IllegalArgumentException("Encrypted Storage size must be positive");
+            }
+            mEncryptedStorageBytes = encryptedStorageBytes;
+            return this;
+        }
+
+        /**
+         * Sets whether to allow the app to read the VM outputs (console / log). Default is {@code
+         * false}.
+         *
+         * <p>By default, console and log outputs of a {@linkplain #setDebugLevel debuggable} VM are
+         * automatically forwarded to the host logcat. Setting this as {@code true} will allow the
+         * app to directly read {@linkplain VirtualMachine#getConsoleOutput console output} and
+         * {@linkplain VirtualMachine#getLogOutput log output}, instead of forwarding them to the
+         * host logcat.
+         *
+         * <p>If you turn on output capture, you must consume data from {@link
+         * VirtualMachine#getConsoleOutput} and {@link VirtualMachine#getLogOutput} - because
+         * otherwise the code in the VM may get blocked when the pipe buffer fills up.
+         *
+         * <p>The {@linkplain #setDebugLevel debug level} must be {@link #DEBUG_LEVEL_FULL} to be
+         * set as true.
+         *
+         * @hide
+         */
+        @SystemApi
+        @NonNull
+        public Builder setVmOutputCaptured(boolean captured) {
+            mVmOutputCaptured = captured;
+            return this;
+        }
+
+        /**
+         * Sets whether to allow the app to write to the VM console. Default is {@code false}.
+         *
+         * <p>Setting this as {@code true} will allow the app to directly write into {@linkplain
+         * VirtualMachine#getConsoleInput console input}.
+         *
+         * <p>The {@linkplain #setDebugLevel debug level} must be {@link #DEBUG_LEVEL_FULL} to be
+         * set as true.
+         *
+         * @hide
+         */
+        @TestApi
+        @NonNull
+        public Builder setVmConsoleInputSupported(boolean supported) {
+            mVmConsoleInputSupported = supported;
+            return this;
+        }
+
+        /**
+         * Sets whether to connect the VM console to a host console. Default is {@code false}.
+         *
+         * <p>Setting this as {@code true} will allow the shell to directly communicate with the VM
+         * console through the connected host console.
+         *
+         * <p>The {@linkplain #setDebugLevel debug level} must be {@link #DEBUG_LEVEL_FULL} to be
+         * set as true.
+         *
+         * @hide
+         */
+        @NonNull
+        public Builder setConnectVmConsole(boolean supported) {
+            mConnectVmConsole = supported;
+            return this;
+        }
+
+        /**
+         * Sets the path to the disk image with vendor-specific modules.
+         *
+         * @hide
+         */
+        @TestApi
+        @FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS)
+        @RequiresPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION)
+        @NonNull
+        public Builder setVendorDiskImage(@NonNull File vendorDiskImage) {
+            mVendorDiskImage =
+                    requireNonNull(vendorDiskImage, "vendor disk image must not be null");
+            return this;
+        }
+
+        /**
+         * Sets an OS for the VM. Defaults to {@code "microdroid"}.
+         *
+         * <p>See {@link VirtualMachineManager#getSupportedOSList} for available OS names.
+         *
+         * @hide
+         */
+        @TestApi
+        @FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS)
+        @RequiresPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION)
+        @NonNull
+        public Builder setOs(@NonNull @OsName String os) {
+            mOs = requireNonNull(os, "os must not be null");
+            return this;
+        }
+
+        /** @hide */
+        public Builder setShouldBoostUclamp(boolean shouldBoostUclamp) {
+            mShouldBoostUclamp = shouldBoostUclamp;
+            return this;
+        }
+
+        /** @hide */
+        public Builder setShouldUseHugepages(boolean shouldUseHugepages) {
+            mShouldUseHugepages = shouldUseHugepages;
+            return this;
+        }
+    }
+}
diff --git a/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java
new file mode 100644
index 0000000..37dc8fa
--- /dev/null
+++ b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java
@@ -0,0 +1,913 @@
+/*
+ * Copyright (C) 2024 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.system.virtualmachine;
+
+import android.annotation.Nullable;
+import android.os.PersistableBundle;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+/** @hide */
+public class VirtualMachineCustomImageConfig {
+    private static final String KEY_NAME = "name";
+    private static final String KEY_KERNEL = "kernel";
+    private static final String KEY_INITRD = "initrd";
+    private static final String KEY_BOOTLOADER = "bootloader";
+    private static final String KEY_PARAMS = "params";
+    private static final String KEY_DISK_WRITABLES = "disk_writables";
+    private static final String KEY_DISK_IMAGES = "disk_images";
+    private static final String KEY_PARTITION_LABELS = "partition_labels_";
+    private static final String KEY_PARTITION_IMAGES = "partition_images_";
+    private static final String KEY_PARTITION_WRITABLES = "partition_writables_";
+    private static final String KEY_PARTITION_GUIDS = "partition_guids_";
+    private static final String KEY_DISPLAY_CONFIG = "display_config";
+    private static final String KEY_TOUCH = "touch";
+    private static final String KEY_KEYBOARD = "keyboard";
+    private static final String KEY_MOUSE = "mouse";
+    private static final String KEY_SWITCHES = "switches";
+    private static final String KEY_NETWORK = "network";
+    private static final String KEY_GPU = "gpu";
+    private static final String KEY_AUDIO_CONFIG = "audio_config";
+    private static final String KEY_TRACKPAD = "trackpad";
+    private static final String KEY_AUTO_MEMORY_BALLOON = "auto_memory_balloon";
+
+    @Nullable private final String name;
+    @Nullable private final String kernelPath;
+    @Nullable private final String initrdPath;
+    @Nullable private final String bootloaderPath;
+    @Nullable private final String[] params;
+    @Nullable private final Disk[] disks;
+    @Nullable private final DisplayConfig displayConfig;
+    @Nullable private final AudioConfig audioConfig;
+    private final boolean touch;
+    private final boolean keyboard;
+    private final boolean mouse;
+    private final boolean switches;
+    private final boolean network;
+    @Nullable private final GpuConfig gpuConfig;
+    private final boolean trackpad;
+    private final boolean autoMemoryBalloon;
+
+    @Nullable
+    public Disk[] getDisks() {
+        return disks;
+    }
+
+    @Nullable
+    public String getBootloaderPath() {
+        return bootloaderPath;
+    }
+
+    @Nullable
+    public String getInitrdPath() {
+        return initrdPath;
+    }
+
+    @Nullable
+    public String getKernelPath() {
+        return kernelPath;
+    }
+
+    @Nullable
+    public String getName() {
+        return name;
+    }
+
+    @Nullable
+    public String[] getParams() {
+        return params;
+    }
+
+    public boolean useTouch() {
+        return touch;
+    }
+
+    public boolean useKeyboard() {
+        return keyboard;
+    }
+
+    public boolean useMouse() {
+        return mouse;
+    }
+
+    public boolean useSwitches() {
+        return switches;
+    }
+
+    public boolean useTrackpad() {
+        return mouse;
+    }
+
+    public boolean useAutoMemoryBalloon() {
+        return autoMemoryBalloon;
+    }
+
+    public boolean useNetwork() {
+        return network;
+    }
+
+    /** @hide */
+    public VirtualMachineCustomImageConfig(
+            String name,
+            String kernelPath,
+            String initrdPath,
+            String bootloaderPath,
+            String[] params,
+            Disk[] disks,
+            DisplayConfig displayConfig,
+            boolean touch,
+            boolean keyboard,
+            boolean mouse,
+            boolean switches,
+            boolean network,
+            GpuConfig gpuConfig,
+            AudioConfig audioConfig,
+            boolean trackpad,
+            boolean autoMemoryBalloon) {
+        this.name = name;
+        this.kernelPath = kernelPath;
+        this.initrdPath = initrdPath;
+        this.bootloaderPath = bootloaderPath;
+        this.params = params;
+        this.disks = disks;
+        this.displayConfig = displayConfig;
+        this.touch = touch;
+        this.keyboard = keyboard;
+        this.mouse = mouse;
+        this.switches = switches;
+        this.network = network;
+        this.gpuConfig = gpuConfig;
+        this.audioConfig = audioConfig;
+        this.trackpad = trackpad;
+        this.autoMemoryBalloon = autoMemoryBalloon;
+    }
+
+    static VirtualMachineCustomImageConfig from(PersistableBundle customImageConfigBundle) {
+        Builder builder = new Builder();
+        builder.setName(customImageConfigBundle.getString(KEY_NAME));
+        builder.setKernelPath(customImageConfigBundle.getString(KEY_KERNEL));
+        builder.setInitrdPath(customImageConfigBundle.getString(KEY_INITRD));
+        builder.setBootloaderPath(customImageConfigBundle.getString(KEY_BOOTLOADER));
+        String[] params = customImageConfigBundle.getStringArray(KEY_PARAMS);
+        if (params != null) {
+            for (String param : params) {
+                builder.addParam(param);
+            }
+        }
+        boolean[] writables = customImageConfigBundle.getBooleanArray(KEY_DISK_WRITABLES);
+        String[] diskImages = customImageConfigBundle.getStringArray(KEY_DISK_IMAGES);
+        if (writables != null && diskImages != null) {
+            if (writables.length == diskImages.length) {
+                for (int i = 0; i < writables.length; i++) {
+                    String diskImage = diskImages[i];
+                    diskImage = diskImage.equals("") ? null : diskImage;
+                    Disk disk = writables[i] ? Disk.RWDisk(diskImage) : Disk.RODisk(diskImage);
+                    String[] labels =
+                            customImageConfigBundle.getStringArray(KEY_PARTITION_LABELS + i);
+                    String[] images =
+                            customImageConfigBundle.getStringArray(KEY_PARTITION_IMAGES + i);
+                    boolean[] partitionWritables =
+                            customImageConfigBundle.getBooleanArray(KEY_PARTITION_WRITABLES + i);
+                    String[] guids =
+                            customImageConfigBundle.getStringArray(KEY_PARTITION_GUIDS + i);
+                    for (int j = 0; j < labels.length; j++) {
+                        disk.addPartition(
+                                new Partition(
+                                        labels[j], images[j], partitionWritables[j], guids[j]));
+                    }
+                    builder.addDisk(disk);
+                }
+            }
+        }
+        PersistableBundle displayConfigPb =
+                customImageConfigBundle.getPersistableBundle(KEY_DISPLAY_CONFIG);
+        builder.setDisplayConfig(DisplayConfig.from(displayConfigPb));
+        builder.useTouch(customImageConfigBundle.getBoolean(KEY_TOUCH));
+        builder.useKeyboard(customImageConfigBundle.getBoolean(KEY_KEYBOARD));
+        builder.useMouse(customImageConfigBundle.getBoolean(KEY_MOUSE));
+        builder.useNetwork(customImageConfigBundle.getBoolean(KEY_NETWORK));
+        builder.setGpuConfig(GpuConfig.from(customImageConfigBundle.getPersistableBundle(KEY_GPU)));
+        PersistableBundle audioConfigPb =
+                customImageConfigBundle.getPersistableBundle(KEY_AUDIO_CONFIG);
+        builder.setAudioConfig(AudioConfig.from(audioConfigPb));
+        builder.useTrackpad(customImageConfigBundle.getBoolean(KEY_TRACKPAD));
+        builder.useAutoMemoryBalloon(customImageConfigBundle.getBoolean(KEY_AUTO_MEMORY_BALLOON));
+        return builder.build();
+    }
+
+    PersistableBundle toPersistableBundle() {
+        PersistableBundle pb = new PersistableBundle();
+        pb.putString(KEY_NAME, this.name);
+        pb.putString(KEY_KERNEL, this.kernelPath);
+        pb.putString(KEY_BOOTLOADER, this.bootloaderPath);
+        pb.putString(KEY_INITRD, this.initrdPath);
+        pb.putStringArray(KEY_PARAMS, this.params);
+
+        if (disks != null) {
+            boolean[] writables = new boolean[disks.length];
+            String[] images = new String[disks.length];
+            for (int i = 0; i < disks.length; i++) {
+                writables[i] = disks[i].writable;
+                String imagePath = disks[i].imagePath;
+                images[i] = imagePath == null ? "" : imagePath;
+
+                int numPartitions = disks[i].getPartitions().size();
+                String[] partitionLabels = new String[numPartitions];
+                String[] partitionImages = new String[numPartitions];
+                boolean[] partitionWritables = new boolean[numPartitions];
+                String[] partitionGuids = new String[numPartitions];
+
+                for (int j = 0; j < numPartitions; j++) {
+                    Partition p = disks[i].getPartitions().get(j);
+                    partitionLabels[j] = p.name;
+                    partitionImages[j] = p.imagePath;
+                    partitionWritables[j] = p.writable;
+                    partitionGuids[j] = p.guid == null ? "" : p.guid;
+                }
+                pb.putStringArray(KEY_PARTITION_LABELS + i, partitionLabels);
+                pb.putStringArray(KEY_PARTITION_IMAGES + i, partitionImages);
+                pb.putBooleanArray(KEY_PARTITION_WRITABLES + i, partitionWritables);
+                pb.putStringArray(KEY_PARTITION_GUIDS + i, partitionGuids);
+            }
+            pb.putBooleanArray(KEY_DISK_WRITABLES, writables);
+            pb.putStringArray(KEY_DISK_IMAGES, images);
+        }
+        pb.putPersistableBundle(
+                KEY_DISPLAY_CONFIG,
+                Optional.ofNullable(displayConfig)
+                        .map(dc -> dc.toPersistableBundle())
+                        .orElse(null));
+        pb.putBoolean(KEY_TOUCH, touch);
+        pb.putBoolean(KEY_KEYBOARD, keyboard);
+        pb.putBoolean(KEY_MOUSE, mouse);
+        pb.putBoolean(KEY_SWITCHES, switches);
+        pb.putBoolean(KEY_NETWORK, network);
+        pb.putPersistableBundle(
+                KEY_GPU,
+                Optional.ofNullable(gpuConfig).map(gc -> gc.toPersistableBundle()).orElse(null));
+        pb.putPersistableBundle(
+                KEY_AUDIO_CONFIG,
+                Optional.ofNullable(audioConfig).map(ac -> ac.toPersistableBundle()).orElse(null));
+        pb.putBoolean(KEY_TRACKPAD, trackpad);
+        pb.putBoolean(KEY_AUTO_MEMORY_BALLOON, autoMemoryBalloon);
+        return pb;
+    }
+
+    @Nullable
+    public AudioConfig getAudioConfig() {
+        return audioConfig;
+    }
+
+    @Nullable
+    public DisplayConfig getDisplayConfig() {
+        return displayConfig;
+    }
+
+    @Nullable
+    public GpuConfig getGpuConfig() {
+        return gpuConfig;
+    }
+
+    /** @hide */
+    public static final class Disk {
+        private final boolean writable;
+        private final String imagePath;
+        private final List<Partition> partitions;
+
+        private Disk(boolean writable, String imagePath) {
+            this.writable = writable;
+            this.imagePath = imagePath;
+            this.partitions = new ArrayList<>();
+        }
+
+        /** @hide */
+        public static Disk RWDisk(String imagePath) {
+            return new Disk(true, imagePath);
+        }
+
+        /** @hide */
+        public static Disk RODisk(String imagePath) {
+            return new Disk(false, imagePath);
+        }
+
+        /** @hide */
+        public boolean isWritable() {
+            return writable;
+        }
+
+        /** @hide */
+        public String getImagePath() {
+            return imagePath;
+        }
+
+        /** @hide */
+        public Disk addPartition(Partition p) {
+            this.partitions.add(p);
+            return this;
+        }
+
+        /** @hide */
+        public List<Partition> getPartitions() {
+            return partitions;
+        }
+    }
+
+    /** @hide */
+    public static final class Partition {
+        public final String name;
+        public final String imagePath;
+        public final boolean writable;
+        public final String guid;
+
+        public Partition(String name, String imagePath, boolean writable, String guid) {
+            this.name = name;
+            this.imagePath = imagePath;
+            this.writable = writable;
+            this.guid = guid;
+        }
+    }
+
+    /** @hide */
+    public static final class Builder {
+        private String name;
+        private String kernelPath;
+        private String initrdPath;
+        private String bootloaderPath;
+        private List<String> params = new ArrayList<>();
+        private List<Disk> disks = new ArrayList<>();
+        private AudioConfig audioConfig;
+        private DisplayConfig displayConfig;
+        private boolean touch;
+        private boolean keyboard;
+        private boolean mouse;
+        private boolean switches;
+        private boolean network;
+        private GpuConfig gpuConfig;
+        private boolean trackpad;
+        private boolean autoMemoryBalloon = true;
+
+        /** @hide */
+        public Builder() {}
+
+        /** @hide */
+        public Builder setName(String name) {
+            this.name = name;
+            return this;
+        }
+
+        /** @hide */
+        public Builder setKernelPath(String kernelPath) {
+            this.kernelPath = kernelPath;
+            return this;
+        }
+
+        /** @hide */
+        public Builder setBootloaderPath(String bootloaderPath) {
+            this.bootloaderPath = bootloaderPath;
+            return this;
+        }
+
+        /** @hide */
+        public Builder setInitrdPath(String initrdPath) {
+            this.initrdPath = initrdPath;
+            return this;
+        }
+
+        /** @hide */
+        public Builder addDisk(Disk disk) {
+            this.disks.add(disk);
+            return this;
+        }
+
+        /** @hide */
+        public Builder addParam(String param) {
+            this.params.add(param);
+            return this;
+        }
+
+        /** @hide */
+        public Builder setDisplayConfig(DisplayConfig displayConfig) {
+            this.displayConfig = displayConfig;
+            return this;
+        }
+
+        /** @hide */
+        public Builder setGpuConfig(GpuConfig gpuConfig) {
+            this.gpuConfig = gpuConfig;
+            return this;
+        }
+
+        /** @hide */
+        public Builder useTouch(boolean touch) {
+            this.touch = touch;
+            return this;
+        }
+
+        /** @hide */
+        public Builder useKeyboard(boolean keyboard) {
+            this.keyboard = keyboard;
+            return this;
+        }
+
+        /** @hide */
+        public Builder useMouse(boolean mouse) {
+            this.mouse = mouse;
+            return this;
+        }
+
+        /** @hide */
+        public Builder useSwitches(boolean switches) {
+            this.switches = switches;
+            return this;
+        }
+
+        /** @hide */
+        public Builder useTrackpad(boolean trackpad) {
+            this.trackpad = trackpad;
+            return this;
+        }
+
+        /** @hide */
+        public Builder useAutoMemoryBalloon(boolean autoMemoryBalloon) {
+            this.autoMemoryBalloon = autoMemoryBalloon;
+            return this;
+        }
+
+        /** @hide */
+        public Builder useNetwork(boolean network) {
+            this.network = network;
+            return this;
+        }
+
+        /** @hide */
+        public Builder setAudioConfig(AudioConfig audioConfig) {
+            this.audioConfig = audioConfig;
+            return this;
+        }
+
+        /** @hide */
+        public VirtualMachineCustomImageConfig build() {
+            return new VirtualMachineCustomImageConfig(
+                    this.name,
+                    this.kernelPath,
+                    this.initrdPath,
+                    this.bootloaderPath,
+                    this.params.toArray(new String[0]),
+                    this.disks.toArray(new Disk[0]),
+                    displayConfig,
+                    touch,
+                    keyboard,
+                    mouse,
+                    switches,
+                    network,
+                    gpuConfig,
+                    audioConfig,
+                    trackpad,
+                    autoMemoryBalloon);
+        }
+    }
+
+    /** @hide */
+    public static final class AudioConfig {
+        private static final String KEY_USE_MICROPHONE = "use_microphone";
+        private static final String KEY_USE_SPEAKER = "use_speaker";
+        private final boolean useMicrophone;
+        private final boolean useSpeaker;
+
+        private AudioConfig(boolean useMicrophone, boolean useSpeaker) {
+            this.useMicrophone = useMicrophone;
+            this.useSpeaker = useSpeaker;
+        }
+
+        /** @hide */
+        public boolean useMicrophone() {
+            return useMicrophone;
+        }
+
+        /** @hide */
+        public boolean useSpeaker() {
+            return useSpeaker;
+        }
+
+        android.system.virtualizationservice.AudioConfig toParcelable() {
+            android.system.virtualizationservice.AudioConfig parcelable =
+                    new android.system.virtualizationservice.AudioConfig();
+            parcelable.useSpeaker = this.useSpeaker;
+            parcelable.useMicrophone = this.useMicrophone;
+
+            return parcelable;
+        }
+
+        private static AudioConfig from(PersistableBundle pb) {
+            if (pb == null) {
+                return null;
+            }
+            Builder builder = new Builder();
+            builder.setUseMicrophone(pb.getBoolean(KEY_USE_MICROPHONE));
+            builder.setUseSpeaker(pb.getBoolean(KEY_USE_SPEAKER));
+            return builder.build();
+        }
+
+        private PersistableBundle toPersistableBundle() {
+            PersistableBundle pb = new PersistableBundle();
+            pb.putBoolean(KEY_USE_MICROPHONE, this.useMicrophone);
+            pb.putBoolean(KEY_USE_SPEAKER, this.useSpeaker);
+            return pb;
+        }
+
+        /** @hide */
+        public static class Builder {
+            private boolean useMicrophone = false;
+            private boolean useSpeaker = false;
+
+            /** @hide */
+            public Builder() {}
+
+            /** @hide */
+            public Builder setUseMicrophone(boolean useMicrophone) {
+                this.useMicrophone = useMicrophone;
+                return this;
+            }
+
+            /** @hide */
+            public Builder setUseSpeaker(boolean useSpeaker) {
+                this.useSpeaker = useSpeaker;
+                return this;
+            }
+
+            /** @hide */
+            public AudioConfig build() {
+                return new AudioConfig(useMicrophone, useSpeaker);
+            }
+        }
+    }
+
+    /** @hide */
+    public static final class DisplayConfig {
+        private static final String KEY_WIDTH = "width";
+        private static final String KEY_HEIGHT = "height";
+        private static final String KEY_HORIZONTAL_DPI = "horizontal_dpi";
+        private static final String KEY_VERTICAL_DPI = "vertical_dpi";
+        private static final String KEY_REFRESH_RATE = "refresh_rate";
+        private final int width;
+        private final int height;
+        private final int horizontalDpi;
+        private final int verticalDpi;
+        private final int refreshRate;
+
+        private DisplayConfig(
+                int width, int height, int horizontalDpi, int verticalDpi, int refreshRate) {
+            this.width = width;
+            this.height = height;
+            this.horizontalDpi = horizontalDpi;
+            this.verticalDpi = verticalDpi;
+            this.refreshRate = refreshRate;
+        }
+
+        /** @hide */
+        public int getWidth() {
+            return width;
+        }
+
+        /** @hide */
+        public int getHeight() {
+            return height;
+        }
+
+        /** @hide */
+        public int getHorizontalDpi() {
+            return horizontalDpi;
+        }
+
+        /** @hide */
+        public int getVerticalDpi() {
+            return verticalDpi;
+        }
+
+        /** @hide */
+        public int getRefreshRate() {
+            return refreshRate;
+        }
+
+        android.system.virtualizationservice.DisplayConfig toParcelable() {
+            android.system.virtualizationservice.DisplayConfig parcelable =
+                    new android.system.virtualizationservice.DisplayConfig();
+            parcelable.width = this.width;
+            parcelable.height = this.height;
+            parcelable.horizontalDpi = this.horizontalDpi;
+            parcelable.verticalDpi = this.verticalDpi;
+            parcelable.refreshRate = this.refreshRate;
+
+            return parcelable;
+        }
+
+        private static DisplayConfig from(PersistableBundle pb) {
+            if (pb == null) {
+                return null;
+            }
+            Builder builder = new Builder();
+            builder.setWidth(pb.getInt(KEY_WIDTH));
+            builder.setHeight(pb.getInt(KEY_HEIGHT));
+            builder.setHorizontalDpi(pb.getInt(KEY_HORIZONTAL_DPI));
+            builder.setVerticalDpi(pb.getInt(KEY_VERTICAL_DPI));
+            builder.setRefreshRate(pb.getInt(KEY_REFRESH_RATE));
+            return builder.build();
+        }
+
+        private PersistableBundle toPersistableBundle() {
+            PersistableBundle pb = new PersistableBundle();
+            pb.putInt(KEY_WIDTH, this.width);
+            pb.putInt(KEY_HEIGHT, this.height);
+            pb.putInt(KEY_HORIZONTAL_DPI, this.horizontalDpi);
+            pb.putInt(KEY_VERTICAL_DPI, this.verticalDpi);
+            pb.putInt(KEY_REFRESH_RATE, this.refreshRate);
+            return pb;
+        }
+
+        /** @hide */
+        public static class Builder {
+            // Default values come from external/crosvm/vm_control/src/gpu.rs
+            private int width;
+            private int height;
+            private int horizontalDpi = 320;
+            private int verticalDpi = 320;
+            private int refreshRate = 60;
+
+            /** @hide */
+            public Builder() {}
+
+            /** @hide */
+            public Builder setWidth(int width) {
+                this.width = width;
+                return this;
+            }
+
+            /** @hide */
+            public Builder setHeight(int height) {
+                this.height = height;
+                return this;
+            }
+
+            /** @hide */
+            public Builder setHorizontalDpi(int horizontalDpi) {
+                this.horizontalDpi = horizontalDpi;
+                return this;
+            }
+
+            /** @hide */
+            public Builder setVerticalDpi(int verticalDpi) {
+                this.verticalDpi = verticalDpi;
+                return this;
+            }
+
+            /** @hide */
+            public Builder setRefreshRate(int refreshRate) {
+                this.refreshRate = refreshRate;
+                return this;
+            }
+
+            /** @hide */
+            public DisplayConfig build() {
+                if (this.width == 0 || this.height == 0) {
+                    throw new IllegalStateException("width and height must be specified");
+                }
+                return new DisplayConfig(width, height, horizontalDpi, verticalDpi, refreshRate);
+            }
+        }
+    }
+
+    /** @hide */
+    public static final class GpuConfig {
+        private static final String KEY_BACKEND = "backend";
+        private static final String KEY_CONTEXT_TYPES = "context_types";
+        private static final String KEY_PCI_ADDRESS = "pci_address";
+        private static final String KEY_RENDERER_FEATURES = "renderer_features";
+        private static final String KEY_RENDERER_USE_EGL = "renderer_use_egl";
+        private static final String KEY_RENDERER_USE_GLES = "renderer_use_gles";
+        private static final String KEY_RENDERER_USE_GLX = "renderer_use_glx";
+        private static final String KEY_RENDERER_USE_SURFACELESS = "renderer_use_surfaceless";
+        private static final String KEY_RENDERER_USE_VULKAN = "renderer_use_vulkan";
+
+        private final String backend;
+        private final String[] contextTypes;
+        private final String pciAddress;
+        private final String rendererFeatures;
+        private final boolean rendererUseEgl;
+        private final boolean rendererUseGles;
+        private final boolean rendererUseGlx;
+        private final boolean rendererUseSurfaceless;
+        private final boolean rendererUseVulkan;
+
+        private GpuConfig(
+                String backend,
+                String[] contextTypes,
+                String pciAddress,
+                String rendererFeatures,
+                boolean rendererUseEgl,
+                boolean rendererUseGles,
+                boolean rendererUseGlx,
+                boolean rendererUseSurfaceless,
+                boolean rendererUseVulkan) {
+            this.backend = backend;
+            this.contextTypes = contextTypes;
+            this.pciAddress = pciAddress;
+            this.rendererFeatures = rendererFeatures;
+            this.rendererUseEgl = rendererUseEgl;
+            this.rendererUseGles = rendererUseGles;
+            this.rendererUseGlx = rendererUseGlx;
+            this.rendererUseSurfaceless = rendererUseSurfaceless;
+            this.rendererUseVulkan = rendererUseVulkan;
+        }
+
+        /** @hide */
+        public String getBackend() {
+            return backend;
+        }
+
+        /** @hide */
+        public String[] getContextTypes() {
+            return contextTypes;
+        }
+
+        /** @hide */
+        public String getPciAddress() {
+            return pciAddress;
+        }
+
+        /** @hide */
+        public String getRendererFeatures() {
+            return rendererFeatures;
+        }
+
+        /** @hide */
+        public boolean getRendererUseEgl() {
+            return rendererUseEgl;
+        }
+
+        /** @hide */
+        public boolean getRendererUseGles() {
+            return rendererUseGles;
+        }
+
+        /** @hide */
+        public boolean getRendererUseGlx() {
+            return rendererUseGlx;
+        }
+
+        /** @hide */
+        public boolean getRendererUseSurfaceless() {
+            return rendererUseSurfaceless;
+        }
+
+        /** @hide */
+        public boolean getRendererUseVulkan() {
+            return rendererUseVulkan;
+        }
+
+        android.system.virtualizationservice.GpuConfig toParcelable() {
+            android.system.virtualizationservice.GpuConfig parcelable =
+                    new android.system.virtualizationservice.GpuConfig();
+            parcelable.backend = this.backend;
+            parcelable.contextTypes = this.contextTypes;
+            parcelable.pciAddress = this.pciAddress;
+            parcelable.rendererFeatures = this.rendererFeatures;
+            parcelable.rendererUseEgl = this.rendererUseEgl;
+            parcelable.rendererUseGles = this.rendererUseGles;
+            parcelable.rendererUseGlx = this.rendererUseGlx;
+            parcelable.rendererUseSurfaceless = this.rendererUseSurfaceless;
+            parcelable.rendererUseVulkan = this.rendererUseVulkan;
+            return parcelable;
+        }
+
+        private static GpuConfig from(PersistableBundle pb) {
+            if (pb == null) {
+                return null;
+            }
+            Builder builder = new Builder();
+            builder.setBackend(pb.getString(KEY_BACKEND));
+            builder.setContextTypes(pb.getStringArray(KEY_CONTEXT_TYPES));
+            builder.setPciAddress(pb.getString(KEY_PCI_ADDRESS));
+            builder.setRendererFeatures(pb.getString(KEY_RENDERER_FEATURES));
+            builder.setRendererUseEgl(pb.getBoolean(KEY_RENDERER_USE_EGL));
+            builder.setRendererUseGles(pb.getBoolean(KEY_RENDERER_USE_GLES));
+            builder.setRendererUseGlx(pb.getBoolean(KEY_RENDERER_USE_GLX));
+            builder.setRendererUseSurfaceless(pb.getBoolean(KEY_RENDERER_USE_SURFACELESS));
+            builder.setRendererUseVulkan(pb.getBoolean(KEY_RENDERER_USE_VULKAN));
+            return builder.build();
+        }
+
+        private PersistableBundle toPersistableBundle() {
+            PersistableBundle pb = new PersistableBundle();
+            pb.putString(KEY_BACKEND, this.backend);
+            pb.putStringArray(KEY_CONTEXT_TYPES, this.contextTypes);
+            pb.putString(KEY_PCI_ADDRESS, this.pciAddress);
+            pb.putString(KEY_RENDERER_FEATURES, this.rendererFeatures);
+            pb.putBoolean(KEY_RENDERER_USE_EGL, this.rendererUseEgl);
+            pb.putBoolean(KEY_RENDERER_USE_GLES, this.rendererUseGles);
+            pb.putBoolean(KEY_RENDERER_USE_GLX, this.rendererUseGlx);
+            pb.putBoolean(KEY_RENDERER_USE_SURFACELESS, this.rendererUseSurfaceless);
+            pb.putBoolean(KEY_RENDERER_USE_VULKAN, this.rendererUseVulkan);
+            return pb;
+        }
+
+        /** @hide */
+        public static class Builder {
+            private String backend;
+            private String[] contextTypes;
+            private String pciAddress;
+            private String rendererFeatures;
+            private boolean rendererUseEgl = true;
+            private boolean rendererUseGles = true;
+            private boolean rendererUseGlx = false;
+            private boolean rendererUseSurfaceless = true;
+            private boolean rendererUseVulkan = false;
+
+            /** @hide */
+            public Builder() {}
+
+            /** @hide */
+            public Builder setBackend(String backend) {
+                this.backend = backend;
+                return this;
+            }
+
+            /** @hide */
+            public Builder setContextTypes(String[] contextTypes) {
+                this.contextTypes = contextTypes;
+                return this;
+            }
+
+            /** @hide */
+            public Builder setPciAddress(String pciAddress) {
+                this.pciAddress = pciAddress;
+                return this;
+            }
+
+            /** @hide */
+            public Builder setRendererFeatures(String rendererFeatures) {
+                this.rendererFeatures = rendererFeatures;
+                return this;
+            }
+
+            /** @hide */
+            public Builder setRendererUseEgl(Boolean rendererUseEgl) {
+                this.rendererUseEgl = rendererUseEgl;
+                return this;
+            }
+
+            /** @hide */
+            public Builder setRendererUseGles(Boolean rendererUseGles) {
+                this.rendererUseGles = rendererUseGles;
+                return this;
+            }
+
+            /** @hide */
+            public Builder setRendererUseGlx(Boolean rendererUseGlx) {
+                this.rendererUseGlx = rendererUseGlx;
+                return this;
+            }
+
+            /** @hide */
+            public Builder setRendererUseSurfaceless(Boolean rendererUseSurfaceless) {
+                this.rendererUseSurfaceless = rendererUseSurfaceless;
+                return this;
+            }
+
+            /** @hide */
+            public Builder setRendererUseVulkan(Boolean rendererUseVulkan) {
+                this.rendererUseVulkan = rendererUseVulkan;
+                return this;
+            }
+
+            /** @hide */
+            public GpuConfig build() {
+                return new GpuConfig(
+                        backend,
+                        contextTypes,
+                        pciAddress,
+                        rendererFeatures,
+                        rendererUseEgl,
+                        rendererUseGles,
+                        rendererUseGlx,
+                        rendererUseSurfaceless,
+                        rendererUseVulkan);
+            }
+        }
+    }
+}
diff --git a/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineDescriptor.java b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineDescriptor.java
new file mode 100644
index 0000000..ee79256
--- /dev/null
+++ b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineDescriptor.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.system.virtualmachine;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.ParcelFileDescriptor;
+import android.os.Parcelable;
+
+import java.io.IOException;
+
+/**
+ * A VM descriptor that captures the state of a Virtual Machine.
+ *
+ * <p>You can capture the current state of VM by creating an instance of this class with {@link
+ * VirtualMachine#toDescriptor}, optionally pass it to another App, and then build an identical VM
+ * with the descriptor received.
+ *
+ * @hide
+ */
+@SystemApi
+public final class VirtualMachineDescriptor implements Parcelable, AutoCloseable {
+    private volatile boolean mClosed = false;
+    @NonNull private final ParcelFileDescriptor mConfigFd;
+    // File descriptor of the file containing the instance id - will be null iff
+    // FEATURE_LLPVM_CHANGES is disabled.
+    @Nullable private final ParcelFileDescriptor mInstanceIdFd;
+    @NonNull private final ParcelFileDescriptor mInstanceImgFd;
+    // File descriptor of the image backing the encrypted storage - Will be null if encrypted
+    // storage is not enabled. */
+    @Nullable private final ParcelFileDescriptor mEncryptedStoreFd;
+
+    @Override
+    public int describeContents() {
+        return CONTENTS_FILE_DESCRIPTOR;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel out, int flags) {
+        checkNotClosed();
+        out.writeParcelable(mConfigFd, flags);
+        out.writeParcelable(mInstanceIdFd, flags);
+        out.writeParcelable(mInstanceImgFd, flags);
+        out.writeParcelable(mEncryptedStoreFd, flags);
+    }
+
+    @NonNull
+    public static final Parcelable.Creator<VirtualMachineDescriptor> CREATOR =
+            new Parcelable.Creator<>() {
+                public VirtualMachineDescriptor createFromParcel(Parcel in) {
+                    return new VirtualMachineDescriptor(in);
+                }
+
+                public VirtualMachineDescriptor[] newArray(int size) {
+                    return new VirtualMachineDescriptor[size];
+                }
+            };
+
+    /**
+     * @return File descriptor of the VM configuration file config.xml.
+     */
+    @NonNull
+    ParcelFileDescriptor getConfigFd() {
+        checkNotClosed();
+        return mConfigFd;
+    }
+
+    /**
+     * @return File descriptor of the file containing instance_id of the VM.
+     */
+    @Nullable
+    ParcelFileDescriptor getInstanceIdFd() {
+        checkNotClosed();
+        return mInstanceIdFd;
+    }
+
+    /**
+     * @return File descriptor of the instance.img of the VM.
+     */
+    @NonNull
+    ParcelFileDescriptor getInstanceImgFd() {
+        checkNotClosed();
+        return mInstanceImgFd;
+    }
+
+    /**
+     * @return File descriptor of image backing the encrypted storage.
+     *     <p>This method will return null if encrypted storage is not enabled.
+     */
+    @Nullable
+    ParcelFileDescriptor getEncryptedStoreFd() {
+        checkNotClosed();
+        return mEncryptedStoreFd;
+    }
+
+    VirtualMachineDescriptor(
+            @NonNull ParcelFileDescriptor configFd,
+            @Nullable ParcelFileDescriptor instanceIdFd,
+            @NonNull ParcelFileDescriptor instanceImgFd,
+            @Nullable ParcelFileDescriptor encryptedStoreFd) {
+        mConfigFd = requireNonNull(configFd);
+        mInstanceIdFd = instanceIdFd;
+        mInstanceImgFd = requireNonNull(instanceImgFd);
+        mEncryptedStoreFd = encryptedStoreFd;
+    }
+
+    private VirtualMachineDescriptor(Parcel in) {
+        mConfigFd = requireNonNull(readParcelFileDescriptor(in));
+        mInstanceIdFd = readParcelFileDescriptor(in);
+        mInstanceImgFd = requireNonNull(readParcelFileDescriptor(in));
+        mEncryptedStoreFd = readParcelFileDescriptor(in);
+    }
+
+    private ParcelFileDescriptor readParcelFileDescriptor(Parcel in) {
+        return in.readParcelable(
+                ParcelFileDescriptor.class.getClassLoader(), ParcelFileDescriptor.class);
+    }
+
+    /**
+     * Release any resources held by this descriptor. Calling {@code close} on an already-closed
+     * descriptor has no effect.
+     */
+    @Override
+    public void close() {
+        mClosed = true;
+        // Let the compiler do the work: close everything, throw if any of them fail, skipping null.
+        try (mConfigFd;
+                mInstanceIdFd;
+                mInstanceImgFd;
+                mEncryptedStoreFd) {
+        } catch (IOException ignored) {
+            // PFD already swallows exceptions from closing the fd. There's no reason to propagate
+            // this to the caller.
+        }
+    }
+
+    private void checkNotClosed() {
+        if (mClosed) {
+            throw new IllegalStateException("Descriptor has been closed");
+        }
+    }
+}
diff --git a/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineException.java b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineException.java
new file mode 100644
index 0000000..9948fda
--- /dev/null
+++ b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineException.java
@@ -0,0 +1,40 @@
+/*
+ * 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.system.virtualmachine;
+
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+
+/**
+ * Exception thrown when operations on virtual machines fail.
+ *
+ * @hide
+ */
+@SystemApi
+public class VirtualMachineException extends Exception {
+    VirtualMachineException(@Nullable String message) {
+        super(message);
+    }
+
+    VirtualMachineException(@Nullable String message, @Nullable Throwable cause) {
+        super(message, cause);
+    }
+
+    VirtualMachineException(@Nullable Throwable cause) {
+        super(cause);
+    }
+}
diff --git a/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineManager.java b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineManager.java
new file mode 100644
index 0000000..242dc91
--- /dev/null
+++ b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineManager.java
@@ -0,0 +1,469 @@
+/*
+ * 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.system.virtualmachine;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresFeature;
+import android.annotation.RequiresPermission;
+import android.annotation.StringDef;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.annotation.WorkerThread;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.RemoteException;
+import android.sysprop.HypervisorProperties;
+import android.system.virtualizationservice.IVirtualizationService;
+import android.util.ArrayMap;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.system.virtualmachine.flags.Flags;
+
+import java.io.File;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Manages {@linkplain VirtualMachine virtual machine} instances created by an app. Each instance is
+ * created from a {@linkplain VirtualMachineConfig configuration} that defines the shape of the VM
+ * (RAM, CPUs), the code to execute within it, etc.
+ *
+ * <p>Each virtual machine instance is named; the configuration and related state of each is
+ * persisted in the app's private data directory and an instance can be retrieved given the name.
+ * The name must be a valid directory name and must not contain '/'.
+ *
+ * <p>The app can then start, stop and otherwise interact with the VM.
+ *
+ * <p>An instance of {@link VirtualMachineManager} can be obtained by calling {@link
+ * Context#getSystemService(Class)}.
+ *
+ * @hide
+ */
+@SystemApi
+@RequiresFeature(PackageManager.FEATURE_VIRTUALIZATION_FRAMEWORK)
+public class VirtualMachineManager {
+    /**
+     * A lock used to synchronize the creation of virtual machines. It protects {@link #mVmsByName},
+     * but is also held throughout VM creation / retrieval / deletion, to prevent these actions
+     * racing with each other.
+     */
+    private static final Object sCreateLock = new Object();
+
+    @NonNull private final Context mContext;
+
+    /** @hide */
+    public VirtualMachineManager(@NonNull Context context) {
+        mContext = requireNonNull(context);
+    }
+
+    @GuardedBy("sCreateLock")
+    private final Map<String, WeakReference<VirtualMachine>> mVmsByName = new ArrayMap<>();
+
+    /**
+     * Capabilities of the virtual machine implementation.
+     *
+     * @hide
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(
+            prefix = "CAPABILITY_",
+            flag = true,
+            value = {CAPABILITY_PROTECTED_VM, CAPABILITY_NON_PROTECTED_VM})
+    public @interface Capability {}
+
+    /**
+     * The implementation supports creating protected VMs, whose memory is inaccessible to the host
+     * OS.
+     *
+     * @see VirtualMachineConfig.Builder#setProtectedVm
+     */
+    public static final int CAPABILITY_PROTECTED_VM = 1;
+
+    /**
+     * The implementation supports creating non-protected VMs, whose memory is accessible to the
+     * host OS.
+     *
+     * @see VirtualMachineConfig.Builder#setProtectedVm
+     */
+    public static final int CAPABILITY_NON_PROTECTED_VM = 2;
+
+    /**
+     * Features provided by {@link VirtualMachineManager}.
+     *
+     * @hide
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    @StringDef(
+            prefix = "FEATURE_",
+            value = {
+                FEATURE_DICE_CHANGES,
+                FEATURE_LLPVM_CHANGES,
+                FEATURE_MULTI_TENANT,
+                FEATURE_NETWORK,
+                FEATURE_REMOTE_ATTESTATION,
+                FEATURE_VENDOR_MODULES,
+            })
+    public @interface Features {}
+
+    /**
+     * Feature to include new data in the VM DICE chain.
+     *
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS)
+    public static final String FEATURE_DICE_CHANGES = IVirtualizationService.FEATURE_DICE_CHANGES;
+
+    /**
+     * Feature to run payload as non-root user.
+     *
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS)
+    public static final String FEATURE_MULTI_TENANT = IVirtualizationService.FEATURE_MULTI_TENANT;
+
+    /**
+     * Feature to allow network features in VM.
+     *
+     * @hide
+     */
+    @TestApi public static final String FEATURE_NETWORK = IVirtualizationService.FEATURE_NETWORK;
+
+    /**
+     * Feature to allow remote attestation in Microdroid.
+     *
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS)
+    public static final String FEATURE_REMOTE_ATTESTATION =
+            IVirtualizationService.FEATURE_REMOTE_ATTESTATION;
+
+    /**
+     * Feature to allow vendor modules in Microdroid.
+     *
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS)
+    public static final String FEATURE_VENDOR_MODULES =
+            IVirtualizationService.FEATURE_VENDOR_MODULES;
+
+    /**
+     * Feature to enable Secretkeeper protected secrets in Microdroid based pVMs.
+     *
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS)
+    public static final String FEATURE_LLPVM_CHANGES = IVirtualizationService.FEATURE_LLPVM_CHANGES;
+
+    /**
+     * Returns a set of flags indicating what this implementation of virtualization is capable of.
+     *
+     * @see #CAPABILITY_PROTECTED_VM
+     * @see #CAPABILITY_NON_PROTECTED_VM
+     * @hide
+     */
+    @SystemApi
+    @Capability
+    public int getCapabilities() {
+        @Capability int result = 0;
+        if (HypervisorProperties.hypervisor_protected_vm_supported().orElse(false)) {
+            result |= CAPABILITY_PROTECTED_VM;
+        }
+        if (HypervisorProperties.hypervisor_vm_supported().orElse(false)) {
+            result |= CAPABILITY_NON_PROTECTED_VM;
+        }
+        return result;
+    }
+
+    /**
+     * Creates a new {@link VirtualMachine} with the given name and config. Creating a virtual
+     * machine with the same name as an existing virtual machine is an error. The existing virtual
+     * machine has to be deleted before its name can be reused.
+     *
+     * <p>Each successful call to this method creates a new (and different) virtual machine even if
+     * the name and the config are the same as a deleted one. The new virtual machine will initially
+     * be stopped.
+     *
+     * <p>NOTE: This method may block and should not be called on the main thread.
+     *
+     * @throws VirtualMachineException if the VM cannot be created, or there is an existing VM with
+     *     the given name.
+     * @hide
+     */
+    @SystemApi
+    @NonNull
+    @WorkerThread
+    @RequiresPermission(VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION)
+    public VirtualMachine create(@NonNull String name, @NonNull VirtualMachineConfig config)
+            throws VirtualMachineException {
+        synchronized (sCreateLock) {
+            return createLocked(name, config);
+        }
+    }
+
+    @NonNull
+    @GuardedBy("sCreateLock")
+    private VirtualMachine createLocked(@NonNull String name, @NonNull VirtualMachineConfig config)
+            throws VirtualMachineException {
+        VirtualMachine vm = VirtualMachine.create(mContext, name, config);
+        mVmsByName.put(name, new WeakReference<>(vm));
+        return vm;
+    }
+
+    /**
+     * Returns an existing {@link VirtualMachine} with the given name. Returns null if there is no
+     * such virtual machine.
+     *
+     * <p>There is at most one {@code VirtualMachine} object corresponding to a given virtual
+     * machine instance. Multiple calls to get() passing the same name will get the same object
+     * returned, until the virtual machine is deleted (via {@link #delete}) and then recreated.
+     *
+     * <p>NOTE: This method may block and should not be called on the main thread.
+     *
+     * @see #getOrCreate
+     * @throws VirtualMachineException if the virtual machine exists but could not be successfully
+     *     retrieved. This can be resolved by calling {@link #delete} on the VM.
+     * @hide
+     */
+    @SystemApi
+    @WorkerThread
+    @Nullable
+    public VirtualMachine get(@NonNull String name) throws VirtualMachineException {
+        synchronized (sCreateLock) {
+            return getLocked(name);
+        }
+    }
+
+    @Nullable
+    @GuardedBy("sCreateLock")
+    private VirtualMachine getLocked(@NonNull String name) throws VirtualMachineException {
+        VirtualMachine vm = getVmByName(name);
+        if (vm != null) return vm;
+
+        vm = VirtualMachine.load(mContext, name);
+        if (vm != null) {
+            mVmsByName.put(name, new WeakReference<>(vm));
+        }
+        return vm;
+    }
+
+    /**
+     * Imports a virtual machine from an {@link VirtualMachineDescriptor} object and associates it
+     * with the given name.
+     *
+     * <p>The new virtual machine will be in the same state as the descriptor indicates. The
+     * descriptor is automatically closed and cannot be used again.
+     *
+     * <p>NOTE: This method may block and should not be called on the main thread.
+     *
+     * @throws VirtualMachineException if the VM cannot be imported or the {@code
+     *     VirtualMachineDescriptor} has already been closed.
+     * @hide
+     */
+    @NonNull
+    @SystemApi
+    @WorkerThread
+    public VirtualMachine importFromDescriptor(
+            @NonNull String name, @NonNull VirtualMachineDescriptor vmDescriptor)
+            throws VirtualMachineException {
+        VirtualMachine vm;
+        synchronized (sCreateLock) {
+            vm = VirtualMachine.fromDescriptor(mContext, name, vmDescriptor);
+            mVmsByName.put(name, new WeakReference<>(vm));
+        }
+        return vm;
+    }
+
+    /**
+     * Returns an existing {@link VirtualMachine} if it exists, or create a new one. The config
+     * parameter is used only when a new virtual machine is created.
+     *
+     * <p>NOTE: This method may block and should not be called on the main thread.
+     *
+     * @throws VirtualMachineException if the virtual machine could not be created or retrieved.
+     * @hide
+     */
+    @SystemApi
+    @WorkerThread
+    @NonNull
+    public VirtualMachine getOrCreate(@NonNull String name, @NonNull VirtualMachineConfig config)
+            throws VirtualMachineException {
+        synchronized (sCreateLock) {
+            VirtualMachine vm = getLocked(name);
+            if (vm != null) {
+                return vm;
+            } else {
+                return createLocked(name, config);
+            }
+        }
+    }
+
+    /**
+     * Deletes an existing {@link VirtualMachine}. Deleting a virtual machine means deleting any
+     * persisted data associated with it including the per-VM secret. This is an irreversible
+     * action. A virtual machine once deleted can never be restored. A new virtual machine created
+     * with the same name is different from an already deleted virtual machine even if it has the
+     * same config.
+     *
+     * <p>NOTE: This method may block and should not be called on the main thread.
+     *
+     * @throws VirtualMachineException if the virtual machine does not exist, is not stopped, or
+     *     cannot be deleted.
+     * @hide
+     */
+    @SystemApi
+    @WorkerThread
+    public void delete(@NonNull String name) throws VirtualMachineException {
+        synchronized (sCreateLock) {
+            VirtualMachine vm = getVmByName(name);
+            if (vm == null) {
+                VirtualMachine.vmInstanceCleanup(mContext, name);
+            } else {
+                vm.delete(mContext, name);
+            }
+            mVmsByName.remove(name);
+        }
+    }
+
+    @Nullable
+    @GuardedBy("sCreateLock")
+    private VirtualMachine getVmByName(@NonNull String name) {
+        requireNonNull(name);
+        WeakReference<VirtualMachine> weakReference = mVmsByName.get(name);
+        if (weakReference != null) {
+            VirtualMachine vm = weakReference.get();
+            if (vm != null && vm.getStatus() != VirtualMachine.STATUS_DELETED) {
+                return vm;
+            }
+        }
+        return null;
+    }
+
+    private static final String JSON_SUFFIX = ".json";
+    private static final List<String> SUPPORTED_OS_LIST_FROM_CFG =
+            extractSupportedOSListFromConfig();
+
+    private boolean isVendorModuleEnabled() {
+        return VirtualizationService.nativeIsVendorModulesFlagEnabled();
+    }
+
+    private static List<String> extractSupportedOSListFromConfig() {
+        List<String> supportedOsList = new ArrayList<>();
+        File directory = new File("/apex/com.android.virt/etc");
+        File[] files = directory.listFiles();
+        if (files != null) {
+            for (File file : files) {
+                String fileName = file.getName();
+                if (fileName.endsWith(JSON_SUFFIX)) {
+                    supportedOsList.add(
+                            fileName.substring(0, fileName.length() - JSON_SUFFIX.length()));
+                }
+            }
+        }
+        return supportedOsList;
+    }
+
+    /**
+     * Returns a list of supported OS names.
+     *
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS)
+    @NonNull
+    public List<String> getSupportedOSList() throws VirtualMachineException {
+        if (isVendorModuleEnabled()) {
+            return SUPPORTED_OS_LIST_FROM_CFG;
+        } else {
+            return Arrays.asList("microdroid");
+        }
+    }
+
+    /**
+     * Returns {@code true} if given {@code featureName} is enabled.
+     *
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS)
+    @RequiresPermission(VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION)
+    public boolean isFeatureEnabled(@Features String featureName) throws VirtualMachineException {
+        synchronized (sCreateLock) {
+            VirtualizationService service = VirtualizationService.getInstance();
+            try {
+                return service.getBinder().isFeatureEnabled(featureName);
+            } catch (RemoteException e) {
+                throw e.rethrowAsRuntimeException();
+            }
+        }
+    }
+
+    /**
+     * Returns {@code true} if the pVM remote attestation feature is supported. Remote attestation
+     * allows a protected VM to attest its authenticity to a remote server.
+     *
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS)
+    @RequiresPermission(VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION)
+    public boolean isRemoteAttestationSupported() throws VirtualMachineException {
+        synchronized (sCreateLock) {
+            VirtualizationService service = VirtualizationService.getInstance();
+            try {
+                return service.getBinder().isRemoteAttestationSupported();
+            } catch (RemoteException e) {
+                throw e.rethrowAsRuntimeException();
+            }
+        }
+    }
+
+    /**
+     * Returns {@code true} if Updatable VM feature is supported by AVF. Updatable VM allow secrets
+     * and data to be accessible even after updates of boot images and apks. For more info see
+     * packages/modules/Virtualization/docs/updatable_vm.md
+     *
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS)
+    @RequiresPermission(VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION)
+    public boolean isUpdatableVmSupported() throws VirtualMachineException {
+        synchronized (sCreateLock) {
+            VirtualizationService service = VirtualizationService.getInstance();
+            try {
+                return service.getBinder().isUpdatableVmSupported();
+            } catch (RemoteException e) {
+                throw e.rethrowAsRuntimeException();
+            }
+        }
+    }
+}
diff --git a/libs/framework-virtualization/src/android/system/virtualmachine/VirtualizationFrameworkInitializer.java b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualizationFrameworkInitializer.java
new file mode 100644
index 0000000..30ac425
--- /dev/null
+++ b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualizationFrameworkInitializer.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.system.virtualmachine;
+
+import static android.content.pm.PackageManager.FEATURE_VIRTUALIZATION_FRAMEWORK;
+
+import android.annotation.SystemApi;
+import android.app.SystemServiceRegistry;
+import android.content.Context;
+
+/**
+ * Holds initialization code for virtualization module
+ *
+ * @hide
+ */
+@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+public class VirtualizationFrameworkInitializer {
+
+    private VirtualizationFrameworkInitializer() {}
+
+    /**
+     * Called by the static initializer in the {@link SystemServiceRegistry}, and registers {@link
+     * VirtualMachineManager} to the {@link Context}. so that it's accessible from {@link
+     * Context#getSystemService(String)}.
+     */
+    public static void registerServiceWrappers() {
+        // Note: it's important that the getPackageManager().hasSystemFeature() check is executed
+        // in the lambda, and not directly in the registerServiceWrappers method. The
+        // registerServiceWrappers is called during Zygote static initialization, and at that
+        // point the PackageManager is not available yet.
+        //
+        // On the other hand, the lambda is executed after the app calls Context.getSystemService
+        // (VirtualMachineManager.class), at which point the PackageManager is available. The
+        // result of the lambda is cached on per-context basis.
+        SystemServiceRegistry.registerContextAwareService(
+                Context.VIRTUALIZATION_SERVICE,
+                VirtualMachineManager.class,
+                ctx ->
+                        ctx.getPackageManager().hasSystemFeature(FEATURE_VIRTUALIZATION_FRAMEWORK)
+                                ? new VirtualMachineManager(ctx)
+                                : null);
+    }
+}
diff --git a/libs/framework-virtualization/src/android/system/virtualmachine/VirtualizationService.java b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualizationService.java
new file mode 100644
index 0000000..83b64ee
--- /dev/null
+++ b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualizationService.java
@@ -0,0 +1,105 @@
+/*
+ * 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.system.virtualmachine;
+
+import android.annotation.NonNull;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.system.virtualizationservice.IVirtualizationService;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.lang.ref.WeakReference;
+
+/** A running instance of virtmgr that is hosting a VirtualizationService AIDL service. */
+class VirtualizationService {
+    static {
+        System.loadLibrary("virtualizationservice_jni");
+    }
+
+    /* Soft reference caching the last created instance of this class. */
+    @GuardedBy("VirtualMachineManager.sCreateLock")
+    private static WeakReference<VirtualizationService> sInstance;
+
+    /*
+     * Client FD for UDS connection to virtmgr's RpcBinder server. Closing it
+     * will make virtmgr shut down.
+     */
+    private final ParcelFileDescriptor mClientFd;
+
+    /* Persistent connection to IVirtualizationService. */
+    private final IVirtualizationService mBinder;
+
+    private static native int nativeSpawn();
+
+    private native IBinder nativeConnect(int clientFd);
+
+    private native boolean nativeIsOk(int clientFd);
+
+    /*
+     * Retrieve boolean value whether RELEASE_AVF_ENABLE_VENDOR_MODULES build flag is enabled or
+     * not.
+     */
+    static native boolean nativeIsVendorModulesFlagEnabled();
+
+    /*
+     * Spawns a new virtmgr subprocess that will host a VirtualizationService
+     * AIDL service.
+     */
+    private VirtualizationService() throws VirtualMachineException {
+        int clientFd = nativeSpawn();
+        if (clientFd < 0) {
+            throw new VirtualMachineException("Could not spawn Virtualization Manager");
+        }
+        mClientFd = ParcelFileDescriptor.adoptFd(clientFd);
+
+        IBinder binder = nativeConnect(mClientFd.getFd());
+        if (binder == null) {
+            throw new VirtualMachineException("Could not connect to Virtualization Manager");
+        }
+        mBinder = IVirtualizationService.Stub.asInterface(binder);
+    }
+
+    /* Returns the IVirtualizationService binder. */
+    @NonNull
+    IVirtualizationService getBinder() {
+        return mBinder;
+    }
+
+    /*
+     * Checks the state of the client FD. Returns false if the FD is in erroneous state
+     * or if the other endpoint had closed its FD.
+     */
+    private boolean isOk() {
+        return nativeIsOk(mClientFd.getFd());
+    }
+
+    /*
+     * Returns an instance of this class. Might spawn a new instance if one doesn't exist, or
+     * if the previous instance had crashed.
+     */
+    @GuardedBy("VirtualMachineManager.sCreateLock")
+    @NonNull
+    static VirtualizationService getInstance() throws VirtualMachineException {
+        VirtualizationService service = (sInstance == null) ? null : sInstance.get();
+        if (service == null || !service.isOk()) {
+            service = new VirtualizationService();
+            sInstance = new WeakReference<>(service);
+        }
+        return service;
+    }
+}