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<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;
+ }
+}