Implement missing APIs

Bug: 183496040
Test: atest MicrodroidHostTest
Test: run MicrodroidDemoApp
Change-Id: I1ee057726b0c83f8fa24bc6fabaa4c7b6ae851d2
diff --git a/demo/java/com/android/microdroid/demo/MainActivity.java b/demo/java/com/android/microdroid/demo/MainActivity.java
index 6373b55..baf0242 100644
--- a/demo/java/com/android/microdroid/demo/MainActivity.java
+++ b/demo/java/com/android/microdroid/demo/MainActivity.java
@@ -119,7 +119,7 @@
                                 .debugMode(debug);
                 VirtualMachineConfig config = builder.build();
                 VirtualMachineManager vmm = VirtualMachineManager.getInstance(getApplication());
-                mVirtualMachine = vmm.create("demo_vm", config);
+                mVirtualMachine = vmm.getOrCreate("demo_vm", config);
                 mVirtualMachine.run();
                 mStatus.postValue(mVirtualMachine.getStatus());
             } catch (VirtualMachineException e) {
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachine.java b/javalib/src/android/system/virtualmachine/VirtualMachine.java
index 8089d85..53d6864 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachine.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachine.java
@@ -25,9 +25,11 @@
 
 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.nio.file.FileAlreadyExistsException;
 import java.nio.file.Files;
 import java.util.Optional;
 
@@ -105,16 +107,22 @@
     /* 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());
+            final File thisVmDir = vm.mConfigFilePath.getParentFile();
+            Files.createDirectories(thisVmDir.getParentFile().toPath());
 
-            FileOutputStream output = new FileOutputStream(vm.mConfigFilePath);
-            vm.mConfig.serialize(output);
-            output.close();
+            // 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(thisVmDir.toPath());
+
+            try (FileOutputStream output = new FileOutputStream(vm.mConfigFilePath)) {
+                vm.mConfig.serialize(output);
+            }
+        } catch (FileAlreadyExistsException e) {
+            throw new VirtualMachineException("virtual machine already exists", e);
         } catch (IOException e) {
             throw new VirtualMachineException(e);
         }
@@ -126,7 +134,6 @@
     /** 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 {
@@ -134,6 +141,9 @@
             VirtualMachineConfig config = VirtualMachineConfig.from(input);
             input.close();
             vm.mConfig = config;
+        } catch (FileNotFoundException e) {
+            // The VM doesn't exist.
+            return null;
         } catch (IOException e) {
             throw new VirtualMachineException(e);
         }
@@ -265,8 +275,21 @@
      */
     public VirtualMachineConfig setConfig(VirtualMachineConfig newConfig)
             throws VirtualMachineException {
-        // TODO(jiyong): implement this
-        throw new VirtualMachineException("Not implemented");
+        final VirtualMachineConfig oldConfig = getConfig();
+        if (!oldConfig.isCompatibleWith(newConfig)) {
+            throw new VirtualMachineException("incompatible config");
+        }
+
+        try {
+            FileOutputStream output = new FileOutputStream(mConfigFilePath);
+            newConfig.serialize(output);
+            output.close();
+        } catch (IOException e) {
+            throw new VirtualMachineException(e);
+        }
+        mConfig = newConfig;
+
+        return oldConfig;
     }
 
     @Override
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
index b5f04a2..f0e1ce6 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -19,6 +19,8 @@
 import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
 
 import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.Signature; // This actually is certificate!
 import android.os.ParcelFileDescriptor;
 import android.os.PersistableBundle;
 import android.system.virtualizationservice.VirtualMachineAppConfig;
@@ -28,6 +30,9 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
 
 /**
  * Represents a configuration of a virtual machine. A configuration consists of hardware
@@ -40,6 +45,7 @@
     // 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_CERTS = "certs";
     private static final String KEY_APKPATH = "apkPath";
     private static final String KEY_IDSIGPATH = "idsigPath";
     private static final String KEY_PAYLOADCONFIGPATH = "payloadConfigPath";
@@ -47,6 +53,7 @@
 
     // Paths to the APK and its idsig file of this application.
     private final String mApkPath;
+    private final Signature[] mCerts;
     private final String mIdsigPath;
     private final boolean mDebugMode;
 
@@ -58,8 +65,13 @@
     // TODO(jiyong): add more items like # of cpu, size of ram, debuggability, etc.
 
     private VirtualMachineConfig(
-            String apkPath, String idsigPath, String payloadConfigPath, boolean debugMode) {
+            String apkPath,
+            Signature[] certs,
+            String idsigPath,
+            String payloadConfigPath,
+            boolean debugMode) {
         mApkPath = apkPath;
+        mCerts = certs;
         mIdsigPath = idsigPath;
         mPayloadConfigPath = payloadConfigPath;
         mDebugMode = debugMode;
@@ -77,6 +89,15 @@
         if (apkPath == null) {
             throw new VirtualMachineException("No apkPath");
         }
+        final String[] certStrings = b.getStringArray(KEY_CERTS);
+        if (certStrings == null || certStrings.length == 0) {
+            throw new VirtualMachineException("No certs");
+        }
+        List<Signature> certList = new ArrayList<>();
+        for (String s : certStrings) {
+            certList.add(new Signature(s));
+        }
+        Signature[] certs = certList.toArray(new Signature[0]);
         final String idsigPath = b.getString(KEY_IDSIGPATH);
         if (idsigPath == null) {
             throw new VirtualMachineException("No idsigPath");
@@ -86,7 +107,7 @@
             throw new VirtualMachineException("No payloadConfigPath");
         }
         final boolean debugMode = b.getBoolean(KEY_DEBUGMODE);
-        return new VirtualMachineConfig(apkPath, idsigPath, payloadConfigPath, debugMode);
+        return new VirtualMachineConfig(apkPath, certs, idsigPath, payloadConfigPath, debugMode);
     }
 
     /** Persists this config to a stream, for example a file. */
@@ -94,6 +115,12 @@
         PersistableBundle b = new PersistableBundle();
         b.putInt(KEY_VERSION, VERSION);
         b.putString(KEY_APKPATH, mApkPath);
+        List<String> certList = new ArrayList<>();
+        for (Signature cert : mCerts) {
+            certList.add(cert.toCharsString());
+        }
+        String[] certs = certList.toArray(new String[0]);
+        b.putStringArray(KEY_CERTS, certs);
         b.putString(KEY_IDSIGPATH, mIdsigPath);
         b.putString(KEY_PAYLOADCONFIGPATH, mPayloadConfigPath);
         b.putBoolean(KEY_DEBUGMODE, mDebugMode);
@@ -106,6 +133,23 @@
     }
 
     /**
+     * Tests if this config is compatible with other config. Being compatible means that the configs
+     * can be interchangeably used for the same virtual machine. Compatible changes includes the
+     * number of CPUs and the size of the RAM, and change of the payload as long as the payload is
+     * signed by the same signer. All other changes (e.g. using a payload from a different signer,
+     * change of the debug mode, etc.) are considered as incompatible.
+     */
+    public boolean isCompatibleWith(VirtualMachineConfig other) {
+        if (!Arrays.equals(this.mCerts, other.mCerts)) {
+            return false;
+        }
+        if (this.mDebugMode != other.mDebugMode) {
+            return false;
+        }
+        return true;
+    }
+
+    /**
      * 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
@@ -152,7 +196,22 @@
         /** Builds an immutable {@link VirtualMachineConfig} */
         public VirtualMachineConfig build() {
             final String apkPath = mContext.getPackageCodePath();
-            return new VirtualMachineConfig(apkPath, mIdsigPath, mPayloadConfigPath, mDebugMode);
+            final String packageName = mContext.getPackageName();
+            Signature[] certs;
+            try {
+                certs =
+                        mContext.getPackageManager()
+                                .getPackageInfo(
+                                        packageName, PackageManager.GET_SIGNING_CERTIFICATES)
+                                .signingInfo
+                                .getSigningCertificateHistory();
+            } catch (PackageManager.NameNotFoundException e) {
+                // This cannot happen as `packageName` is from this app.
+                throw new RuntimeException(e);
+            }
+
+            return new VirtualMachineConfig(
+                    apkPath, certs, mIdsigPath, mPayloadConfigPath, mDebugMode);
         }
     }
 }
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineManager.java b/javalib/src/android/system/virtualmachine/VirtualMachineManager.java
index dfa4f0b..317caee 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineManager.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineManager.java
@@ -74,7 +74,12 @@
         return VirtualMachine.load(mContext, name);
     }
 
-    /** Returns an existing {@link VirtualMachine} if it exists, or create a new one. */
+    /**
+     * Returns an existing {@link VirtualMachine} if it exists, or create a new one. If the virtual
+     * machine exists, and config is not null, the virtual machine is re-configured with the new
+     * config. However, if the config is not compatible with the original config of the virtual
+     * machine, exception is thrown.
+     */
     public VirtualMachine getOrCreate(String name, VirtualMachineConfig config)
             throws VirtualMachineException {
         VirtualMachine vm;
@@ -85,10 +90,11 @@
             }
         }
 
-        if (vm.getConfig().equals(config)) {
-            return vm;
-        } else {
-            throw new VirtualMachineException("Incompatible config");
+        if (config != null) {
+            // Can throw VirtualMachineException is the new config is not compatible with the
+            // old config.
+            vm.setConfig(config);
         }
+        return vm;
     }
 }