Merge "Rename payload metadata partition"
diff --git a/demo/Android.bp b/demo/Android.bp
new file mode 100644
index 0000000..77049de
--- /dev/null
+++ b/demo/Android.bp
@@ -0,0 +1,21 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_app {
+    name: "MicrodroidDemoApp",
+    srcs: ["java/**/*.java"],
+    resource_dirs: ["res"],
+    static_libs: [
+        "androidx-constraintlayout_constraintlayout",
+        "androidx.appcompat_appcompat",
+        "com.google.android.material_material",
+    ],
+    libs: [
+        "android.system.virtualmachine",
+    ],
+    jni_libs: ["MicrodroidTestNativeLib"],
+    platform_apis: true,
+    use_embedded_native_libs: true,
+    v4_signature: true,
+}
diff --git a/demo/AndroidManifest.xml b/demo/AndroidManifest.xml
new file mode 100644
index 0000000..ae4f734
--- /dev/null
+++ b/demo/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.microdroid.demo">
+
+    <application
+        android:label="MicrodroidDemo"
+        android:theme="@style/Theme.MicrodroidDemo">
+        <uses-library android:name="android.system.virtualmachine" android:required="true" />
+        <activity android:name=".MainActivity" android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+
+</manifest>
diff --git a/demo/assets/vm_config.json b/demo/assets/vm_config.json
new file mode 100644
index 0000000..b814394
--- /dev/null
+++ b/demo/assets/vm_config.json
@@ -0,0 +1,13 @@
+{
+  "os": {
+    "name": "microdroid"
+  },
+  "task": {
+    "type": "microdroid_launcher",
+    "command": "MicrodroidTestNativeLib.so",
+    "args": [
+      "hello",
+      "microdroid"
+    ]
+  }
+}
diff --git a/demo/java/com/android/microdroid/demo/MainActivity.java b/demo/java/com/android/microdroid/demo/MainActivity.java
new file mode 100644
index 0000000..976e37e
--- /dev/null
+++ b/demo/java/com/android/microdroid/demo/MainActivity.java
@@ -0,0 +1,112 @@
+/*
+ * 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 com.android.microdroid.demo;
+
+import android.app.Application;
+import android.os.Bundle;
+import android.system.virtualmachine.VirtualMachine;
+import android.system.virtualmachine.VirtualMachineConfig;
+import android.system.virtualmachine.VirtualMachineException;
+import android.system.virtualmachine.VirtualMachineManager;
+import android.widget.TextView;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.Observer;
+import androidx.lifecycle.ViewModelProvider;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * This app is to demonstrate the use of APIs in the android.system.virtualmachine library.
+ * Currently, this app starts a virtual machine running Microdroid and shows the console output from
+ * the virtual machine to the UI.
+ */
+public class MainActivity extends AppCompatActivity {
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_main);
+
+        // Whenthe console model is updated, append the new line to the text view.
+        TextView view = (TextView) findViewById(R.id.textview);
+        VirtualMachineModel model = new ViewModelProvider(this).get(VirtualMachineModel.class);
+        model.getConsoleOutput()
+                .observeForever(
+                        new Observer<String>() {
+                            @Override
+                            public void onChanged(String line) {
+                                view.append(line + "\n");
+                            }
+                        });
+    }
+
+    /** Models a virtual machine and console output from it. */
+    public static class VirtualMachineModel extends AndroidViewModel {
+        private final VirtualMachine mVirtualMachine;
+        private final MutableLiveData<String> mConsoleOutput = new MutableLiveData<>();
+
+        public VirtualMachineModel(Application app) {
+            super(app);
+
+            // Create a VM and run it.
+            // TODO(jiyong): remove the call to idsigPath
+            try {
+                VirtualMachineConfig config =
+                        new VirtualMachineConfig.Builder(getApplication(), "assets/vm_config.json")
+                                .idsigPath("/data/local/tmp/virt/MicrodroidDemoApp.apk.idsig")
+                                .build();
+                VirtualMachineManager vmm = VirtualMachineManager.getInstance(getApplication());
+                mVirtualMachine = vmm.create("demo_vm", config);
+                mVirtualMachine.run();
+            } catch (VirtualMachineException e) {
+                throw new RuntimeException(e);
+            }
+
+            // Read console output from the VM in the background
+            ExecutorService executorService = Executors.newFixedThreadPool(1);
+            executorService.execute(
+                    new Runnable() {
+                        @Override
+                        public void run() {
+                            try {
+                                BufferedReader reader =
+                                        new BufferedReader(
+                                                new InputStreamReader(
+                                                        mVirtualMachine.getConsoleOutputStream()));
+                                while (true) {
+                                    String line = reader.readLine();
+                                    mConsoleOutput.postValue(line);
+                                }
+                            } catch (IOException | VirtualMachineException e) {
+                                // Consume
+                            }
+                        }
+                    });
+        }
+
+        public LiveData<String> getConsoleOutput() {
+            return mConsoleOutput;
+        }
+    }
+}
diff --git a/demo/res/layout/activity_main.xml b/demo/res/layout/activity_main.xml
new file mode 100644
index 0000000..026382f
--- /dev/null
+++ b/demo/res/layout/activity_main.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="#FFC107"
+    android:scrollbars="horizontal|vertical"
+    android:textAlignment="textStart"
+    tools:context=".MainActivity">
+
+    <ScrollView
+        android:id="@+id/scrollview"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+        <TextView
+            android:id="@+id/textview"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:background="#FFEB3B"
+            android:fontFamily="monospace"
+            android:textColor="#000000" />
+    </ScrollView>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/demo/res/values/colors.xml b/demo/res/values/colors.xml
new file mode 100644
index 0000000..f8c6127
--- /dev/null
+++ b/demo/res/values/colors.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="purple_200">#FFBB86FC</color>
+    <color name="purple_500">#FF6200EE</color>
+    <color name="purple_700">#FF3700B3</color>
+    <color name="teal_200">#FF03DAC5</color>
+    <color name="teal_700">#FF018786</color>
+    <color name="black">#FF000000</color>
+    <color name="white">#FFFFFFFF</color>
+</resources>
\ No newline at end of file
diff --git a/demo/res/values/themes.xml b/demo/res/values/themes.xml
new file mode 100644
index 0000000..16b8ab3
--- /dev/null
+++ b/demo/res/values/themes.xml
@@ -0,0 +1,16 @@
+<resources xmlns:tools="http://schemas.android.com/tools">
+    <!-- Base application theme. -->
+    <style name="Theme.MicrodroidDemo" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
+        <!-- Primary brand color. -->
+        <item name="colorPrimary">@color/purple_500</item>
+        <item name="colorPrimaryVariant">@color/purple_700</item>
+        <item name="colorOnPrimary">@color/white</item>
+        <!-- Secondary brand color. -->
+        <item name="colorSecondary">@color/teal_200</item>
+        <item name="colorSecondaryVariant">@color/teal_700</item>
+        <item name="colorOnSecondary">@color/black</item>
+        <!-- Status bar color. -->
+        <item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
+        <!-- Customize your theme here. -->
+    </style>
+</resources>
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachine.java b/javalib/src/android/system/virtualmachine/VirtualMachine.java
index 504bc03..8089d85 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachine.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachine.java
@@ -16,5 +16,267 @@
 
 package android.system.virtualmachine;
 
-/** @hide */
-public class VirtualMachine {}
+import android.content.Context;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.system.virtualizationservice.IVirtualMachine;
+import android.system.virtualizationservice.IVirtualizationService;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.util.Optional;
+
+/**
+ * A handle to the virtual machine. The virtual machine is local to the app which created the
+ * virtual machine.
+ *
+ * @hide
+ */
+public class 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 virtualization service. */
+    private static final String SERVICE_NAME = "android.system.virtualizationservice";
+
+    /** Status of a virtual machine */
+    public enum Status {
+        /** The virtual machine has just been created, or {@link #stop()} was called on it. */
+        STOPPED,
+        /** The virtual machine is running. */
+        RUNNING,
+        /**
+         * The virtual machine is deleted. This is a irreversable state. Once a virtual machine is
+         * deleted, it can never be undone, which means all its secrets are permanently lost.
+         */
+        DELETED,
+    }
+
+    /** The package which owns this VM. */
+    private final String mPackageName;
+
+    /** Name of this VM within the package. The name should be unique in the package. */
+    private final String mName;
+
+    /**
+     * Path to the config file for this VM. The config file is where the configuration is persisted.
+     */
+    private final File mConfigFilePath;
+
+    /** Path to the instance image file for this VM. (Not implemented) */
+    private final File mInstanceFilePath;
+
+    /** The configuration that is currently associated with this VM. */
+    private VirtualMachineConfig mConfig;
+
+    /** Handle to the "running" VM. */
+    private IVirtualMachine mVirtualMachine;
+
+    private ParcelFileDescriptor mConsoleReader;
+    private ParcelFileDescriptor mConsoleWriter;
+
+    private VirtualMachine(Context context, String name, VirtualMachineConfig config) {
+        mPackageName = context.getPackageName();
+        mName = name;
+        mConfig = config;
+
+        final File vmRoot = new File(context.getFilesDir(), VM_DIR);
+        final File thisVmDir = new File(vmRoot, mName);
+        mConfigFilePath = new File(thisVmDir, CONFIG_FILE);
+        mInstanceFilePath = new File(thisVmDir, INSTANCE_IMAGE_FILE);
+    }
+
+    /**
+     * 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 #STOPPED} state. To run the VM, call {@link #run()}.
+     */
+    /* package */ static VirtualMachine create(
+            Context context, String name, VirtualMachineConfig config)
+            throws VirtualMachineException {
+        // TODO(jiyong): trigger an error if the VM having 'name' already exists.
+        VirtualMachine vm = new VirtualMachine(context, name, config);
+
+        try {
+            final File vmRoot = vm.mConfigFilePath.getParentFile();
+            Files.createDirectories(vmRoot.toPath());
+
+            FileOutputStream output = new FileOutputStream(vm.mConfigFilePath);
+            vm.mConfig.serialize(output);
+            output.close();
+        } catch (IOException e) {
+            throw new VirtualMachineException(e);
+        }
+
+        // TODO(jiyong): create the instance image file
+        return vm;
+    }
+
+    /** Loads a virtual machine that is already created before. */
+    /* package */ static VirtualMachine load(Context context, String name)
+            throws VirtualMachineException {
+        // TODO(jiyong): return null if the VM having the 'name' doesn't exist.
+        VirtualMachine vm = new VirtualMachine(context, name, /* config */ null);
+
+        try {
+            FileInputStream input = new FileInputStream(vm.mConfigFilePath);
+            VirtualMachineConfig config = VirtualMachineConfig.from(input);
+            input.close();
+            vm.mConfig = config;
+        } catch (IOException e) {
+            throw new VirtualMachineException(e);
+        }
+
+        return vm;
+    }
+
+    /**
+     * Returns the name of this virtual machine. The name is unique in the package and can't be
+     * changed.
+     */
+    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; one cannot share its secret to another virtual machine even if they
+     * share the same config. It is also possible that a virtual machine can switch its config,
+     * which can be done by calling {@link #setConfig(VirtualMachineCOnfig)}.
+     */
+    public VirtualMachineConfig getConfig() {
+        return mConfig;
+    }
+
+    /** Returns the current status of this virtual machine. */
+    public Status getStatus() throws VirtualMachineException {
+        try {
+            if (mVirtualMachine != null && mVirtualMachine.isRunning()) {
+                return Status.RUNNING;
+            }
+        } catch (RemoteException e) {
+            throw new VirtualMachineException(e);
+        }
+        if (!mConfigFilePath.exists()) {
+            return Status.DELETED;
+        }
+        return Status.STOPPED;
+    }
+
+    /**
+     * 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 object (not implemented currently).
+     */
+    public void run() throws VirtualMachineException {
+        if (getStatus() != Status.STOPPED) {
+            throw new VirtualMachineException(this + " is not in stopped state");
+        }
+        IVirtualizationService service =
+                IVirtualizationService.Stub.asInterface(ServiceManager.getService(SERVICE_NAME));
+
+        try {
+            if (mConsoleReader == null && mConsoleWriter == null) {
+                ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
+                mConsoleReader = pipe[0];
+                mConsoleWriter = pipe[1];
+            }
+            mVirtualMachine =
+                    service.startVm(
+                            android.system.virtualizationservice.VirtualMachineConfig.appConfig(
+                                    getConfig().toParcel()),
+                            mConsoleWriter);
+        } catch (IOException e) {
+            throw new VirtualMachineException(e);
+        } catch (RemoteException e) {
+            throw new VirtualMachineException(e);
+        }
+    }
+
+    /** Returns the stream object representing the console output from the virtual machine. */
+    public InputStream getConsoleOutputStream() throws VirtualMachineException {
+        if (mConsoleReader == null) {
+            throw new VirtualMachineException("Console output not available");
+        }
+        return new FileInputStream(mConsoleReader.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 with the event. A stopped virtual machine can be re-started by calling {@link
+     * #run()}.
+     */
+    public void stop() throws VirtualMachineException {
+        // Dropping the IVirtualMachine handle stops the VM
+        mVirtualMachine = null;
+    }
+
+    /**
+     * Deletes this virtual machine. Deleting a virtual machine means deleting any persisted data
+     * associated with it including the per-VM secret. This is an irreversable action. A virtual
+     * machine once deleted can never be restored. A new virtual machine created with the same name
+     * and the same config is different from an already deleted virtual machine.
+     */
+    public void delete() throws VirtualMachineException {
+        if (getStatus() != Status.STOPPED) {
+            throw new VirtualMachineException("Virtual machine is not stopped");
+        }
+        final File vmRootDir = mConfigFilePath.getParentFile();
+        mConfigFilePath.delete();
+        mInstanceFilePath.delete();
+        vmRootDir.delete();
+    }
+
+    /** Returns the CID of this virtual machine, if it is running. */
+    public Optional<Integer> getCid() throws VirtualMachineException {
+        if (getStatus() != Status.RUNNING) {
+            return Optional.empty();
+        }
+        try {
+            return Optional.of(mVirtualMachine.getCid());
+        } catch (RemoteException e) {
+            throw new VirtualMachineException(e);
+        }
+    }
+
+    /**
+     * 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.) However, changing a config might make the
+     * virtual machine un-bootable if the new config is not compatible with the existing one. For
+     * example, if the signer of the app payload in the new config is different from that of the old
+     * config, the virtual machine won't boot. To prevent such cases, this method returns exception
+     * when an incompatible config is attempted.
+     *
+     * @return the old config
+     */
+    public VirtualMachineConfig setConfig(VirtualMachineConfig newConfig)
+            throws VirtualMachineException {
+        // TODO(jiyong): implement this
+        throw new VirtualMachineException("Not implemented");
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("VirtualMachine(");
+        sb.append("name:" + getName() + ", ");
+        sb.append("config:" + getConfig().getPayloadConfigPath() + ", ");
+        sb.append("package: " + mPackageName);
+        sb.append(")");
+        return sb.toString();
+    }
+}
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
new file mode 100644
index 0000000..062cd04
--- /dev/null
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -0,0 +1,143 @@
+/*
+ * 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.MODE_READ_ONLY;
+
+import android.content.Context;
+import android.os.ParcelFileDescriptor;
+import android.os.PersistableBundle;
+import android.system.virtualizationservice.VirtualMachineAppConfig;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * 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
+ * OS and application to run on the virtual machine.
+ *
+ * @hide
+ */
+public final class VirtualMachineConfig {
+    // These defines the schema of the config file persisted on disk.
+    private static final int VERSION = 1;
+    private static final String KEY_VERSION = "version";
+    private static final String KEY_APKPATH = "apkPath";
+    private static final String KEY_IDSIGPATH = "idsigPath";
+    private static final String KEY_PAYLOADCONFIGPATH = "payloadConfigPath";
+
+    // Paths to the APK and its idsig file of this application.
+    private final String mApkPath;
+    private final String mIdsigPath;
+
+    /**
+     * Path within the APK to the payload config file that defines software aspects of this config.
+     */
+    private final String mPayloadConfigPath;
+
+    // TODO(jiyong): add more items like # of cpu, size of ram, debuggability, etc.
+
+    private VirtualMachineConfig(String apkPath, String idsigPath, String payloadConfigPath) {
+        mApkPath = apkPath;
+        mIdsigPath = idsigPath;
+        mPayloadConfigPath = payloadConfigPath;
+    }
+
+    /** Loads a config from a stream, for example a file. */
+    /* package */ static VirtualMachineConfig from(InputStream input)
+            throws IOException, VirtualMachineException {
+        PersistableBundle b = PersistableBundle.readFromStream(input);
+        final int version = b.getInt(KEY_VERSION);
+        if (version > VERSION) {
+            throw new VirtualMachineException("Version too high");
+        }
+        final String apkPath = b.getString(KEY_APKPATH);
+        if (apkPath == null) {
+            throw new VirtualMachineException("No apkPath");
+        }
+        final String idsigPath = b.getString(KEY_IDSIGPATH);
+        if (idsigPath == null) {
+            throw new VirtualMachineException("No idsigPath");
+        }
+        final String payloadConfigPath = b.getString(KEY_PAYLOADCONFIGPATH);
+        if (payloadConfigPath == null) {
+            throw new VirtualMachineException("No payloadConfigPath");
+        }
+        return new VirtualMachineConfig(apkPath, idsigPath, payloadConfigPath);
+    }
+
+    /** Persists this config to a stream, for example a file. */
+    /* package */ void serialize(OutputStream output) throws IOException {
+        PersistableBundle b = new PersistableBundle();
+        b.putInt(KEY_VERSION, VERSION);
+        b.putString(KEY_APKPATH, mApkPath);
+        b.putString(KEY_IDSIGPATH, mIdsigPath);
+        b.putString(KEY_PAYLOADCONFIGPATH, mPayloadConfigPath);
+        b.writeToStream(output);
+    }
+
+    /** Returns the path to the payload config within the owning application. */
+    public String getPayloadConfigPath() {
+        return mPayloadConfigPath;
+    }
+
+    /**
+     * Converts this config object into a parcel. 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.
+     */
+    /* package */ VirtualMachineAppConfig toParcel() throws FileNotFoundException {
+        VirtualMachineAppConfig parcel = new VirtualMachineAppConfig();
+        parcel.apk = ParcelFileDescriptor.open(new File(mApkPath), MODE_READ_ONLY);
+        parcel.idsig = ParcelFileDescriptor.open(new File(mIdsigPath), MODE_READ_ONLY);
+        parcel.configPath = mPayloadConfigPath;
+        return parcel;
+    }
+
+    /** A builder used to create a {@link VirtualMachineConfig}. */
+    public static class Builder {
+        private Context mContext;
+        private String mPayloadConfigPath;
+        private String mIdsigPath; // TODO(jiyong): remove this
+        // TODO(jiyong): add more items like # of cpu, size of ram, debuggability, etc.
+
+        /** Creates a builder for the given context (APK), and the payload config file in APK. */
+        public Builder(Context context, String payloadConfigPath) {
+            mContext = context;
+            mPayloadConfigPath = payloadConfigPath;
+        }
+
+        // TODO(jiyong): remove this. Apps shouldn't need to set the path to the idsig file. It
+        // should be automatically found or created on demand.
+        /** Set the path to the idsig file for the current application. */
+        public Builder idsigPath(String idsigPath) {
+            mIdsigPath = idsigPath;
+            return this;
+        }
+
+        /** Builds an immutable {@link VirtualMachineConfig} */
+        public VirtualMachineConfig build() {
+            final String apkPath = mContext.getPackageCodePath();
+            return new VirtualMachineConfig(apkPath, mIdsigPath, mPayloadConfigPath);
+        }
+    }
+}
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineException.java b/javalib/src/android/system/virtualmachine/VirtualMachineException.java
new file mode 100644
index 0000000..d6aeab3
--- /dev/null
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineException.java
@@ -0,0 +1,36 @@
+/*
+ * 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;
+
+/** @hide */
+public class VirtualMachineException extends Exception {
+    public VirtualMachineException() {
+        super();
+    }
+
+    public VirtualMachineException(String message) {
+        super(message);
+    }
+
+    public VirtualMachineException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public VirtualMachineException(Throwable cause) {
+        super(cause);
+    }
+}
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineManager.java b/javalib/src/android/system/virtualmachine/VirtualMachineManager.java
new file mode 100644
index 0000000..dfa4f0b
--- /dev/null
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineManager.java
@@ -0,0 +1,94 @@
+/*
+ * 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.content.Context;
+
+import java.lang.ref.WeakReference;
+import java.util.Map;
+import java.util.WeakHashMap;
+
+/**
+ * Manages {@link VirtualMachine} objects created for an application.
+ *
+ * @hide
+ */
+public class VirtualMachineManager {
+    private final Context mContext;
+
+    private VirtualMachineManager(Context context) {
+        mContext = context;
+    }
+
+    static Map<Context, WeakReference<VirtualMachineManager>> sInstances = new WeakHashMap<>();
+
+    /** Returns the per-context instance. */
+    public static VirtualMachineManager getInstance(Context context) {
+        synchronized (sInstances) {
+            VirtualMachineManager vmm =
+                    sInstances.containsKey(context) ? sInstances.get(context).get() : null;
+            if (vmm == null) {
+                vmm = new VirtualMachineManager(context);
+                sInstances.put(context, new WeakReference(vmm));
+            }
+            return vmm;
+        }
+    }
+
+    /** A lock used to synchronize the creation of virtual machines */
+    private static final Object sCreateLock = new Object();
+
+    /**
+     * 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. Every call to this methods creates a
+     * new (and different) virtual machine even if the name and the config are the same as the
+     * deleted one.
+     */
+    public VirtualMachine create(String name, VirtualMachineConfig config)
+            throws VirtualMachineException {
+        synchronized (sCreateLock) {
+            return VirtualMachine.create(mContext, name, config);
+        }
+    }
+
+    /**
+     * Returns an existing {@link VirtualMachine} with the given name. Returns null if there is no
+     * such virtual machine.
+     */
+    public VirtualMachine get(String name) throws VirtualMachineException {
+        return VirtualMachine.load(mContext, name);
+    }
+
+    /** Returns an existing {@link VirtualMachine} if it exists, or create a new one. */
+    public VirtualMachine getOrCreate(String name, VirtualMachineConfig config)
+            throws VirtualMachineException {
+        VirtualMachine vm;
+        synchronized (sCreateLock) {
+            vm = get(name);
+            if (vm == null) {
+                return create(name, config);
+            }
+        }
+
+        if (vm.getConfig().equals(config)) {
+            return vm;
+        } else {
+            throw new VirtualMachineException("Incompatible config");
+        }
+    }
+}
diff --git a/tests/testapk/src/java/com/android/microdroid/test/TestActivity.java b/tests/testapk/src/java/com/android/microdroid/test/TestActivity.java
index b25869b..f73772e 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/TestActivity.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/TestActivity.java
@@ -17,8 +17,35 @@
 
 import android.app.Activity;
 import android.os.Bundle;
+import android.system.virtualmachine.VirtualMachine;
+import android.system.virtualmachine.VirtualMachineConfig;
+import android.system.virtualmachine.VirtualMachineException;
+import android.system.virtualmachine.VirtualMachineManager;
 
 public class TestActivity extends Activity {
+
     @Override
-    public void onCreate(Bundle savedInstanceState) {}
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        VirtualMachine vm1 = createAndRunVirtualMachine("vm1");
+        VirtualMachine vm2 = createAndRunVirtualMachine("vm2");
+    }
+
+    private VirtualMachine createAndRunVirtualMachine(String name) {
+        VirtualMachine vm;
+        try {
+            VirtualMachineConfig config =
+                    new VirtualMachineConfig.Builder(this, "assets/vm_config.json")
+                            .idsigPath("/data/local/tmp/virt/MicrodroidTestApp.apk.idsig")
+                            .build();
+
+            VirtualMachineManager vmm = VirtualMachineManager.getInstance(this);
+            vm = vmm.create(name, config);
+            vm.run();
+        } catch (VirtualMachineException e) {
+            throw new RuntimeException(e);
+        }
+        return vm;
+    }
 }
diff --git a/virtualizationservice/aidl/Android.bp b/virtualizationservice/aidl/Android.bp
index a3311f2..f7cb339 100644
--- a/virtualizationservice/aidl/Android.bp
+++ b/virtualizationservice/aidl/Android.bp
@@ -5,8 +5,8 @@
 aidl_interface {
     name: "android.system.virtualizationservice",
     srcs: ["**/*.aidl"],
-    // This is never accessed directly. Apps are expected to use this indirectly via the java wrapper
-    // android.system.virtualmachine.
+    // This is never accessed directly. Apps are expected to use this indirectly via the Java
+    // wrapper android.system.virtualmachine.
     unstable: true,
     backend: {
         java: {