Add APIs for creating and running a virtual machine

Bug: 183496040
Test: atest MicrodroidHostTestCase
Test: TARGET_BUILD_APPS=MicrodroidTestApp.signed
Push the build MicrodroidTestApp.apk.idsig to /data/local/tmp/virt
Install the APK and run it.

Change-Id: I1614f8c83605a32ef34b62216c13cb4e4f7fcf99
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: {