Introduce VirtualMachineCustomImageConfig instead of rawConfigPath

```
VirtualMachineCustomImageConfig.Builder builder = ...;
builder.setKernel(path);
builder.addParam("console=...");
builder.addDisk(Disk.RODisk(diskPath));
config.setCustomImageConfig(builder.build());
```

Bug: 330256602
Test: run vmlauncher
Change-Id: I14dedf15edc2be8ae047ebbc37f10d5c74adba76
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachine.java b/java/framework/src/android/system/virtualmachine/VirtualMachine.java
index f9ca2f2..dd2719c 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachine.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachine.java
@@ -813,7 +813,7 @@
 
     private android.system.virtualizationservice.VirtualMachineConfig
             createVirtualMachineConfigForRawFrom(VirtualMachineConfig vmConfig)
-                    throws IllegalStateException {
+                    throws IllegalStateException, IOException {
         VirtualMachineRawConfig rawConfig = vmConfig.toVsRawConfig();
         return android.system.virtualizationservice.VirtualMachineConfig.rawConfig(rawConfig);
     }
@@ -904,7 +904,7 @@
 
                 VirtualMachineConfig vmConfig = getConfig();
                 android.system.virtualizationservice.VirtualMachineConfig vmConfigParcel =
-                        vmConfig.getRawConfigPath() != null
+                        vmConfig.getCustomImageConfig() != null
                                 ? createVirtualMachineConfigForRawFrom(vmConfig)
                                 : createVirtualMachineConfigForAppFrom(vmConfig, service);
 
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java b/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java
index 99c3d05..be80db8 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -43,13 +43,11 @@
 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 org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
 
 import java.io.File;
 import java.io.FileInputStream;
@@ -60,13 +58,12 @@
 import java.io.OutputStream;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
-import java.nio.file.Files;
-import java.nio.file.Path;
 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;
 
 /**
@@ -88,7 +85,7 @@
     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_RAWCONFIGPATH = "rawConfigPath";
+    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";
@@ -184,8 +181,8 @@
     /** Name of the payload binary file within the APK that will be executed within the VM. */
     @Nullable private final String mPayloadBinaryName;
 
-    /** Path within the raw config file to launch the VM. */
-    @Nullable private final String mRawConfigPath;
+    /** 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;
@@ -224,7 +221,7 @@
             List<String> extraApks,
             @Nullable String payloadConfigPath,
             @Nullable String payloadBinaryName,
-            @Nullable String rawConfigPath,
+            @Nullable VirtualMachineCustomImageConfig customImageConfig,
             @DebugLevel int debugLevel,
             boolean protectedVm,
             long memoryBytes,
@@ -244,7 +241,7 @@
                                 Arrays.asList(extraApks.toArray(new String[0])));
         mPayloadConfigPath = payloadConfigPath;
         mPayloadBinaryName = payloadBinaryName;
-        mRawConfigPath = rawConfigPath;
+        mCustomImageConfig = customImageConfig;
         mDebugLevel = debugLevel;
         mProtectedVm = protectedVm;
         mMemoryBytes = memoryBytes;
@@ -307,9 +304,10 @@
 
         String payloadConfigPath = b.getString(KEY_PAYLOADCONFIGPATH);
         String payloadBinaryName = b.getString(KEY_PAYLOADBINARYNAME);
-        String rawConfigPath = b.getString(KEY_RAWCONFIGPATH);
-        if (rawConfigPath != null) {
-            builder.setRawConfigPath(rawConfigPath);
+        PersistableBundle customImageConfigBundle = b.getPersistableBundle(KEY_CUSTOMIMAGECONFIG);
+        if (customImageConfigBundle != null) {
+            builder.setCustomImageConfig(
+                    VirtualMachineCustomImageConfig.from(customImageConfigBundle));
         } else if (payloadConfigPath != null) {
             builder.setPayloadConfigPath(payloadConfigPath);
         } else {
@@ -372,7 +370,9 @@
         }
         b.putString(KEY_PAYLOADCONFIGPATH, mPayloadConfigPath);
         b.putString(KEY_PAYLOADBINARYNAME, mPayloadBinaryName);
-        b.putString(KEY_RAWCONFIGPATH, mRawConfigPath);
+        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);
@@ -433,13 +433,13 @@
     }
 
     /**
-     * Returns the path within the raw config file to launch the VM.
+     * Returns the custom image config to launch the custom VM.
      *
      * @hide
      */
     @Nullable
-    public String getRawConfigPath() {
-        return mRawConfigPath;
+    public VirtualMachineCustomImageConfig getCustomImageConfig() {
+        return mCustomImageConfig;
     }
 
     /**
@@ -585,96 +585,67 @@
                 && Objects.equals(this.mExtraApks, other.mExtraApks);
     }
 
-    // Needs to sync with packages/modules/Virtualization/libs/vmconfig/src/lib.rs
-    VirtualMachineRawConfig toVsRawConfig() throws IllegalStateException {
-        VirtualMachineRawConfig config = new VirtualMachineRawConfig();
-
+    private ParcelFileDescriptor openOrNull(File file, int mode) {
         try {
-            String rawJson = new String(Files.readAllBytes(Path.of(mRawConfigPath)));
-            JSONObject json = new JSONObject(rawJson);
-            config.name = json.optString("name", "");
-            config.instanceId = new byte[64];
-            if (json.has("kernel")) {
-                config.kernel =
-                        ParcelFileDescriptor.open(
-                                new File(json.getString("kernel")), MODE_READ_ONLY);
-            }
-            if (json.has("initrd")) {
-                config.initrd =
-                        ParcelFileDescriptor.open(
-                                new File(json.getString("initrd")), MODE_READ_ONLY);
-            }
-            if (json.has("params")) {
-                config.params = json.getString("params");
-            }
-            if (json.has("bootloader")) {
-                config.bootloader =
-                        ParcelFileDescriptor.open(
-                                new File(json.getString("bootloader")), MODE_READ_ONLY);
-            }
-            if (json.has("disks")) {
-                JSONArray diskArr = json.getJSONArray("disks");
-                config.disks = new DiskImage[diskArr.length()];
-                for (int i = 0; i < diskArr.length(); i++) {
-                    config.disks[i] = new DiskImage();
-                    JSONObject item = diskArr.getJSONObject(i);
-                    config.disks[i].writable = item.optBoolean("writable", false);
-                    if (item.has("image")) {
-                        config.disks[i].image =
-                                ParcelFileDescriptor.open(
-                                        new File(item.getString("image")),
-                                        config.disks[i].writable
-                                                ? MODE_READ_WRITE
-                                                : MODE_READ_ONLY);
-                    }
-                    if (item.has("partition")) {
-                        JSONArray partitionArr = json.getJSONArray("partition");
-                        config.disks[i].partitions = new Partition[partitionArr.length()];
-                        for (int j = 0; j < partitionArr.length(); j++) {
-                            config.disks[i].partitions[j] = new Partition();
-                            JSONObject partitionItem = partitionArr.getJSONObject(j);
-                            config.disks[i].partitions[j].writable =
-                                    partitionItem.optBoolean("writable", false);
-                            config.disks[i].partitions[j].label = partitionItem.getString("label");
-                            config.disks[i].partitions[j].image =
-                                    ParcelFileDescriptor.open(
-                                            new File(partitionItem.getString("image")),
-                                            config.disks[i].partitions[j].writable
-                                                    ? MODE_READ_WRITE
-                                                    : MODE_READ_ONLY);
-                        }
-                    } else {
-                        config.disks[i].partitions = new Partition[0];
-                    }
-                }
-            } else {
-                config.disks = new DiskImage[0];
-            }
-            // The value which is set by setProtectedVm is used.
-            if (json.has("protected")) {
-                Log.d(TAG, "'protected' field is ignored, the value from setProtectedVm is used");
-            }
-            config.protectedVm = this.mProtectedVm;
-            config.memoryMib = json.optInt("memory_mib", 0);
-            if (json.optString("cpu_topology", "one_cpu").equals("match_host")) {
-                config.cpuTopology = CPU_TOPOLOGY_MATCH_HOST;
-            } else {
-                config.cpuTopology = CPU_TOPOLOGY_ONE_CPU;
-            }
-            config.platformVersion = json.getString("platform_version");
-            if (json.has("devices")) {
-                JSONArray arr = json.getJSONArray("devices");
-                config.devices = new String[arr.length()];
-                for (int i = 0; i < arr.length(); i++) {
-                    config.devices[i] = arr.getString(i);
-                }
-            } else {
-                config.devices = new String[0];
-            }
-
-        } catch (JSONException | IOException e) {
-            throw new IllegalStateException("malformed input", e);
+            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.of(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);
+        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();
+
+            config.disks[i].image =
+                    ParcelFileDescriptor.open(
+                            new File(customImageConfig.getDisks()[i].getImagePath()),
+                            config.disks[i].writable ? MODE_READ_WRITE : MODE_READ_ONLY);
+            config.disks[i].partitions = new Partition[0];
+        }
+
+        config.protectedVm = this.mProtectedVm;
+        config.memoryMib = bytesToMebiBytes(mMemoryBytes);
+        config.cpuTopology = (byte) this.mCpuTopology;
+        config.devices = EMPTY_STRING_ARRAY;
+        config.platformVersion = "~1.0";
         return config;
     }
 
@@ -804,7 +775,7 @@
         @Nullable private String mApkPath;
         private final List<String> mExtraApks = new ArrayList<>();
         @Nullable private String mPayloadConfigPath;
-        @Nullable private String mRawConfigPath;
+        @Nullable private VirtualMachineCustomImageConfig mCustomImageConfig;
         @Nullable private String mPayloadBinaryName;
         @DebugLevel private int mDebugLevel = DEBUG_LEVEL_NONE;
         private boolean mProtectedVm;
@@ -854,11 +825,11 @@
                 // This should never happen, unless we're deserializing a bad config
                 throw new IllegalStateException("apkPath or packageName must be specified");
             }
-            if (mRawConfigPath != null) {
+            if (mCustomImageConfig != null) {
                 if (mPayloadBinaryName != null || mPayloadConfigPath != null) {
                     throw new IllegalStateException(
-                            "setRawConfigPath and (setPayloadBinaryName or setPayloadConfigPath)"
-                                    + " may not both be called");
+                            "setCustomImageConfig and (setPayloadBinaryName or"
+                                    + " setPayloadConfigPath) may not both be called");
                 }
             } else if (mPayloadBinaryName == null) {
                 if (mPayloadConfigPath == null) {
@@ -893,7 +864,7 @@
                     mExtraApks,
                     mPayloadConfigPath,
                     mPayloadBinaryName,
-                    mRawConfigPath,
+                    mCustomImageConfig,
                     mDebugLevel,
                     mProtectedVm,
                     mMemoryBytes,
@@ -955,25 +926,15 @@
         }
 
         /**
-         * Sets the path within the raw config file to launch the VM. The file is a JSON file; see
-         * packages/modules/Virtualization/libs/vmconfig/src/lib.rs for the format.
+         * Sets the custom config file to launch the custom VM.
          *
          * @hide
          */
         @RequiresPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION)
         @NonNull
-        public Builder setRawConfigPath(@NonNull String rawConfigPath) {
-            // TODO: This method will be removed when the builder support more structured methods
-            // for that like below.
-            //    builder
-            //      .setLinuxKernelConfig(new LinuxKernelConfig()
-            //        .setKernelPath(...)
-            //        .setInitRamdiskPath(...)
-            //        .setKernelCommandLine(...))
-            //      .setDiskConfig(new DiskConfig()
-            //        .addDisk(...)
-            //        .addDisk(...))
-            mRawConfigPath = requireNonNull(rawConfigPath, "rawConfigPath must not be null");
+        public Builder setCustomImageConfig(
+                @NonNull VirtualMachineCustomImageConfig customImageConfig) {
+            this.mCustomImageConfig = customImageConfig;
             return this;
         }
 
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java b/java/framework/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java
new file mode 100644
index 0000000..219fdca
--- /dev/null
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java
@@ -0,0 +1,224 @@
+/*
+ * 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.NonNull;
+import android.annotation.Nullable;
+import android.os.PersistableBundle;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** @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";
+    @Nullable private final String name;
+    @NonNull 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
+    public Disk[] getDisks() {
+        return disks;
+    }
+
+    @Nullable
+    public String getBootloaderPath() {
+        return bootloaderPath;
+    }
+
+    @Nullable
+    public String getInitrdPath() {
+        return initrdPath;
+    }
+
+    @NonNull
+    public String getKernelPath() {
+        return kernelPath;
+    }
+
+    @Nullable
+    public String getName() {
+        return name;
+    }
+
+    @Nullable
+    public String[] getParams() {
+        return params;
+    }
+
+    /** @hide */
+    public VirtualMachineCustomImageConfig(
+            String name,
+            String kernelPath,
+            String initrdPath,
+            String bootloaderPath,
+            String[] params,
+            Disk[] disks) {
+        this.name = name;
+        this.kernelPath = kernelPath;
+        this.initrdPath = initrdPath;
+        this.bootloaderPath = bootloaderPath;
+        this.params = params;
+        this.disks = disks;
+    }
+
+    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++) {
+                    builder.addDisk(
+                            writables[i] ? Disk.RWDisk(diskImages[i]) : Disk.RODisk(diskImages[i]));
+                }
+            }
+        }
+        return null;
+    }
+
+    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;
+                images[i] = disks[i].imagePath;
+            }
+            pb.putBooleanArray(KEY_DISK_WRITABLES, writables);
+            pb.putStringArray(KEY_DISK_IMAGES, images);
+        }
+        return pb;
+    }
+
+    /** @hide */
+    public static final class Disk {
+        private final boolean writable;
+        private final String imagePath;
+
+        private Disk(boolean writable, String imagePath) {
+            this.writable = writable;
+            this.imagePath = imagePath;
+        }
+
+        /** @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 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<>();
+
+        /** @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 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]));
+        }
+    }
+}
diff --git a/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java b/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
index 90d7fcc..7c927c9 100644
--- a/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
+++ b/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
@@ -16,8 +16,11 @@
 
 package com.android.virtualization.vmlauncher;
 
+import static android.system.virtualmachine.VirtualMachineConfig.CPU_TOPOLOGY_MATCH_HOST;
+
 import android.app.Activity;
 import android.os.Bundle;
+import android.system.virtualmachine.VirtualMachineCustomImageConfig;
 import android.util.Log;
 import android.system.virtualmachine.VirtualMachine;
 import android.system.virtualmachine.VirtualMachineCallback;
@@ -25,10 +28,17 @@
 import android.system.virtualmachine.VirtualMachineException;
 import android.system.virtualmachine.VirtualMachineManager;
 
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 
@@ -39,6 +49,62 @@
     private final ExecutorService mExecutorService = Executors.newFixedThreadPool(4);
     private VirtualMachine mVirtualMachine;
 
+    private VirtualMachineConfig createVirtualMachineConfig(String jsonPath) {
+        VirtualMachineConfig.Builder configBuilder =
+                new VirtualMachineConfig.Builder(getApplication());
+        configBuilder.setCpuTopology(CPU_TOPOLOGY_MATCH_HOST);
+
+        configBuilder.setProtectedVm(false);
+        if (DEBUG) {
+            configBuilder.setDebugLevel(VirtualMachineConfig.DEBUG_LEVEL_FULL);
+            configBuilder.setVmOutputCaptured(true);
+        }
+        VirtualMachineCustomImageConfig.Builder customImageConfigBuilder =
+                new VirtualMachineCustomImageConfig.Builder();
+        try {
+            String rawJson = new String(Files.readAllBytes(Path.of(jsonPath)));
+            JSONObject json = new JSONObject(rawJson);
+            customImageConfigBuilder.setName(json.optString("name", ""));
+            if (json.has("kernel")) {
+                customImageConfigBuilder.setKernelPath(json.getString("kernel"));
+            }
+            if (json.has("initrd")) {
+                customImageConfigBuilder.setInitrdPath(json.getString("initrd"));
+            }
+            if (json.has("params")) {
+                Arrays.stream(json.getString("params").split(" "))
+                        .forEach(customImageConfigBuilder::addParam);
+            }
+            if (json.has("bootloader")) {
+                customImageConfigBuilder.setInitrdPath(json.getString("bootloader"));
+            }
+            if (json.has("disks")) {
+                JSONArray diskArr = json.getJSONArray("disks");
+                for (int i = 0; i < diskArr.length(); i++) {
+                    JSONObject item = diskArr.getJSONObject(i);
+                    if (item.has("image")) {
+                        if (item.optBoolean("writable", false)) {
+                            customImageConfigBuilder.addDisk(
+                                    VirtualMachineCustomImageConfig.Disk.RWDisk(
+                                            item.getString("image")));
+                        } else {
+                            customImageConfigBuilder.addDisk(
+                                    VirtualMachineCustomImageConfig.Disk.RODisk(
+                                            item.getString("image")));
+                        }
+                    }
+                }
+            }
+
+            configBuilder.setMemoryBytes(8L * 1024 * 1024 * 1024 /* 8 GB */);
+            configBuilder.setCustomImageConfig(customImageConfigBuilder.build());
+
+        } catch (JSONException | IOException e) {
+            throw new IllegalStateException("malformed input", e);
+        }
+        return configBuilder.build();
+    }
+
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -89,15 +155,8 @@
                 };
 
         try {
-            VirtualMachineConfig.Builder builder =
-                    new VirtualMachineConfig.Builder(getApplication());
-            builder.setRawConfigPath("/data/local/tmp/vm_config.json");
-            builder.setProtectedVm(false);
-            if (DEBUG) {
-                builder.setDebugLevel(VirtualMachineConfig.DEBUG_LEVEL_FULL);
-                builder.setVmOutputCaptured(true);
-            }
-            VirtualMachineConfig config = builder.build();
+            VirtualMachineConfig config =
+                    createVirtualMachineConfig("/data/local/tmp/vm_config.json");
             VirtualMachineManager vmm =
                     getApplication().getSystemService(VirtualMachineManager.class);
             if (vmm == null) {