Refactor vm_config.json parsing routine

The routine is separate into a dedicated class. The parsing is now done
structurally using Gson.

Bug: N/A
Test: run Ferrochrome

Change-Id: I25cac5e8a7a12a7a0e6493a9362a239e43a00274
diff --git a/android/VmLauncherApp/Android.bp b/android/VmLauncherApp/Android.bp
index 7103d53..7dd2473 100644
--- a/android/VmLauncherApp/Android.bp
+++ b/android/VmLauncherApp/Android.bp
@@ -11,6 +11,7 @@
         "android.system.virtualizationservice_internal-java",
         // TODO(b/331708504): will be removed when AVF framework handles surface
         "libcrosvm_android_display_service-java",
+        "gson",
     ],
     libs: [
         "framework-virtualization.impl",
@@ -22,7 +23,7 @@
         "com.android.virt",
     ],
     optimize: {
-        optimize: true,
+        proguard_flags_files: ["proguard.flags"],
         shrink_resources: true,
     },
 }
diff --git a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/MainActivity.java b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/MainActivity.java
index d837c04..160140a 100644
--- a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/MainActivity.java
+++ b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/MainActivity.java
@@ -17,7 +17,6 @@
 package com.android.virtualization.vmlauncher;
 
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
-import static android.system.virtualmachine.VirtualMachineConfig.CPU_TOPOLOGY_MATCH_HOST;
 
 import android.Manifest.permission;
 import android.app.Activity;
@@ -26,7 +25,6 @@
 import android.content.Intent;
 import android.crosvm.ICrosvmAndroidDisplayService;
 import android.graphics.PixelFormat;
-import android.graphics.Rect;
 import android.hardware.input.InputManager;
 import android.os.Bundle;
 import android.os.ParcelFileDescriptor;
@@ -36,13 +34,8 @@
 import android.system.virtualmachine.VirtualMachine;
 import android.system.virtualmachine.VirtualMachineCallback;
 import android.system.virtualmachine.VirtualMachineConfig;
-import android.system.virtualmachine.VirtualMachineCustomImageConfig;
-import android.system.virtualmachine.VirtualMachineCustomImageConfig.AudioConfig;
-import android.system.virtualmachine.VirtualMachineCustomImageConfig.DisplayConfig;
-import android.system.virtualmachine.VirtualMachineCustomImageConfig.GpuConfig;
 import android.system.virtualmachine.VirtualMachineException;
 import android.system.virtualmachine.VirtualMachineManager;
-import android.util.DisplayMetrics;
 import android.util.Log;
 import android.view.InputDevice;
 import android.view.KeyEvent;
@@ -53,14 +46,9 @@
 import android.view.WindowInsets;
 import android.view.WindowInsetsController;
 import android.view.WindowManager;
-import android.view.WindowMetrics;
 
 import libcore.io.IoBridge;
 
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-
 import java.io.BufferedOutputStream;
 import java.io.BufferedReader;
 import java.io.FileInputStream;
@@ -72,10 +60,6 @@
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
 import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 
@@ -94,164 +78,6 @@
     private CursorHandler mCursorHandler;
     private ClipboardManager mClipboardManager;
 
-    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);
-            configBuilder.setConnectVmConsole(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.setBootloaderPath(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")));
-                        }
-                    } else if (item.has("partitions")) {
-                        boolean diskWritable = item.optBoolean("writable", false);
-                        VirtualMachineCustomImageConfig.Disk disk =
-                                diskWritable
-                                        ? VirtualMachineCustomImageConfig.Disk.RWDisk(null)
-                                        : VirtualMachineCustomImageConfig.Disk.RODisk(null);
-                        JSONArray partitions = item.getJSONArray("partitions");
-                        for (int j = 0; j < partitions.length(); j++) {
-                            JSONObject partition = partitions.getJSONObject(j);
-                            String label = partition.getString("label");
-                            String path = partition.getString("path");
-                            boolean partitionWritable =
-                                    diskWritable && partition.optBoolean("writable", false);
-                            String guid = partition.optString("guid");
-                            VirtualMachineCustomImageConfig.Partition p =
-                                    new VirtualMachineCustomImageConfig.Partition(
-                                            label, path, partitionWritable, guid);
-                            disk.addPartition(p);
-                        }
-                        customImageConfigBuilder.addDisk(disk);
-                    }
-                }
-            }
-            if (json.has("console_input_device")) {
-                configBuilder.setConsoleInputDevice(json.getString("console_input_device"));
-            }
-            if (json.has("gpu")) {
-                JSONObject gpuJson = json.getJSONObject("gpu");
-
-                GpuConfig.Builder gpuConfigBuilder = new GpuConfig.Builder();
-
-                if (gpuJson.has("backend")) {
-                    gpuConfigBuilder.setBackend(gpuJson.getString("backend"));
-                }
-                if (gpuJson.has("context_types")) {
-                    ArrayList<String> contextTypes = new ArrayList<String>();
-                    JSONArray contextTypesJson = gpuJson.getJSONArray("context_types");
-                    for (int i = 0; i < contextTypesJson.length(); i++) {
-                        contextTypes.add(contextTypesJson.getString(i));
-                    }
-                    gpuConfigBuilder.setContextTypes(contextTypes.toArray(new String[0]));
-                }
-                if (gpuJson.has("pci_address")) {
-                    gpuConfigBuilder.setPciAddress(gpuJson.getString("pci_address"));
-                }
-                if (gpuJson.has("renderer_features")) {
-                    gpuConfigBuilder.setRendererFeatures(gpuJson.getString("renderer_features"));
-                }
-                if (gpuJson.has("renderer_use_egl")) {
-                    gpuConfigBuilder.setRendererUseEgl(gpuJson.getBoolean("renderer_use_egl"));
-                }
-                if (gpuJson.has("renderer_use_gles")) {
-                    gpuConfigBuilder.setRendererUseGles(gpuJson.getBoolean("renderer_use_gles"));
-                }
-                if (gpuJson.has("renderer_use_glx")) {
-                    gpuConfigBuilder.setRendererUseGlx(gpuJson.getBoolean("renderer_use_glx"));
-                }
-                if (gpuJson.has("renderer_use_surfaceless")) {
-                    gpuConfigBuilder.setRendererUseSurfaceless(
-                            gpuJson.getBoolean("renderer_use_surfaceless"));
-                }
-                if (gpuJson.has("renderer_use_vulkan")) {
-                    gpuConfigBuilder.setRendererUseVulkan(
-                            gpuJson.getBoolean("renderer_use_vulkan"));
-                }
-                customImageConfigBuilder.setGpuConfig(gpuConfigBuilder.build());
-            }
-
-            long memoryMib = 1024; // 1GB by default
-            if (json.has("memory_mib")) {
-                memoryMib = json.getLong("memory_mib");
-            }
-            configBuilder.setMemoryBytes(memoryMib * 1024 * 1024);
-
-            WindowMetrics windowMetrics = getWindowManager().getCurrentWindowMetrics();
-            float dpi = DisplayMetrics.DENSITY_DEFAULT * windowMetrics.getDensity();
-            int refreshRate = (int) getDisplay().getRefreshRate();
-            if (json.has("display")) {
-                JSONObject display = json.getJSONObject("display");
-                if (display.has("scale")) {
-                    dpi *= (float) display.getDouble("scale");
-                }
-                if (display.has("refresh_rate")) {
-                    refreshRate = display.getInt("refresh_rate");
-                }
-            }
-            int dpiInt = (int) dpi;
-            DisplayConfig.Builder displayConfigBuilder = new DisplayConfig.Builder();
-            Rect windowSize = windowMetrics.getBounds();
-            displayConfigBuilder.setWidth(windowSize.right);
-            displayConfigBuilder.setHeight(windowSize.bottom);
-            displayConfigBuilder.setHorizontalDpi(dpiInt);
-            displayConfigBuilder.setVerticalDpi(dpiInt);
-            displayConfigBuilder.setRefreshRate(refreshRate);
-
-            customImageConfigBuilder.setDisplayConfig(displayConfigBuilder.build());
-            customImageConfigBuilder.useTouch(true);
-            customImageConfigBuilder.useKeyboard(true);
-            customImageConfigBuilder.useMouse(true);
-            customImageConfigBuilder.useSwitches(true);
-            customImageConfigBuilder.useTrackpad(true);
-            customImageConfigBuilder.useNetwork(true);
-
-            AudioConfig.Builder audioConfigBuilder = new AudioConfig.Builder();
-            audioConfigBuilder.setUseMicrophone(true);
-            audioConfigBuilder.setUseSpeaker(true);
-            customImageConfigBuilder.setAudioConfig(audioConfigBuilder.build());
-            configBuilder.setCustomImageConfig(customImageConfigBuilder.build());
-
-        } catch (JSONException | IOException e) {
-            throw new IllegalStateException("malformed input", e);
-        }
-        return configBuilder.build();
-    }
 
     private static boolean isVolumeKey(int keyCode) {
         return keyCode == KeyEvent.KEYCODE_VOLUME_UP
@@ -386,7 +212,7 @@
 
         try {
             VirtualMachineConfig config =
-                    createVirtualMachineConfig("/data/local/tmp/vm_config.json");
+                    VmConfigJson.from("/data/local/tmp/vm_config.json").toConfig(this);
             VirtualMachineManager vmm =
                     getApplication().getSystemService(VirtualMachineManager.class);
             if (vmm == null) {
diff --git a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/VmConfigJson.java b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/VmConfigJson.java
new file mode 100644
index 0000000..332b9f5
--- /dev/null
+++ b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/VmConfigJson.java
@@ -0,0 +1,220 @@
+/*
+ * 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 com.android.virtualization.vmlauncher;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.system.virtualmachine.VirtualMachineConfig;
+import android.system.virtualmachine.VirtualMachineCustomImageConfig;
+import android.system.virtualmachine.VirtualMachineCustomImageConfig.AudioConfig;
+import android.system.virtualmachine.VirtualMachineCustomImageConfig.Disk;
+import android.system.virtualmachine.VirtualMachineCustomImageConfig.DisplayConfig;
+import android.system.virtualmachine.VirtualMachineCustomImageConfig.GpuConfig;
+import android.system.virtualmachine.VirtualMachineCustomImageConfig.Partition;
+import android.util.DisplayMetrics;
+import android.view.WindowManager;
+import android.view.WindowMetrics;
+
+import com.google.gson.Gson;
+import com.google.gson.annotations.SerializedName;
+
+import java.io.FileReader;
+import java.util.Arrays;
+
+/** This class and its inner classes model vm_config.json. */
+class VmConfigJson {
+    private static final boolean DEBUG = true;
+
+    private VmConfigJson() {}
+
+    @SerializedName("protected")
+    private boolean isProtected;
+
+    private String name;
+    private String cpu_topology;
+    private String platform_version;
+    private int memory_mib = 1024;
+    private String console_input_device;
+    private String bootloader;
+    private String kernel;
+    private String initrd;
+    private String params;
+    private DiskJson[] disks;
+    private DisplayJson display;
+    private GpuJson gpu;
+
+    /** Parses JSON file at jsonPath */
+    static VmConfigJson from(String jsonPath) {
+        try (FileReader r = new FileReader(jsonPath)) {
+            return new Gson().fromJson(r, VmConfigJson.class);
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to parse " + jsonPath, e);
+        }
+    }
+
+    private int getCpuTopology() {
+        switch (cpu_topology) {
+            case "one_cpu":
+                return VirtualMachineConfig.CPU_TOPOLOGY_ONE_CPU;
+            case "match_host":
+                return VirtualMachineConfig.CPU_TOPOLOGY_MATCH_HOST;
+            default:
+                throw new RuntimeException("invalid cpu topology: " + cpu_topology);
+        }
+    }
+
+    /** Converts this parsed JSON into VirtualMachieConfig */
+    VirtualMachineConfig toConfig(Context context) {
+        VirtualMachineConfig.Builder builder = new VirtualMachineConfig.Builder(context);
+        builder.setProtectedVm(isProtected)
+                .setMemoryBytes((long) memory_mib * 1024 * 1024)
+                .setConsoleInputDevice(console_input_device)
+                .setCpuTopology(getCpuTopology())
+                .setCustomImageConfig(toCustomImageConfig(context));
+
+        // TODO: make these configurable via json
+        if (DEBUG) {
+            builder.setDebugLevel(VirtualMachineConfig.DEBUG_LEVEL_FULL)
+                    .setVmOutputCaptured(true)
+                    .setConnectVmConsole(true);
+        }
+
+        return builder.build();
+    }
+
+    private VirtualMachineCustomImageConfig toCustomImageConfig(Context context) {
+        VirtualMachineCustomImageConfig.Builder builder =
+                new VirtualMachineCustomImageConfig.Builder();
+
+        builder.setName(name)
+                .setBootloaderPath(bootloader)
+                .setKernelPath(kernel)
+                .setInitrdPath(initrd);
+        if (params != null) {
+            Arrays.stream(params.split(" ")).forEach(builder::addParam);
+        }
+
+        // TODO: make these configurable via json
+        builder.useTouch(true)
+                .useKeyboard(true)
+                .useMouse(true)
+                .useSwitches(true)
+                .useTrackpad(true)
+                .useNetwork(true)
+                .setAudioConfig(
+                        new AudioConfig.Builder()
+                                .setUseMicrophone(true)
+                                .setUseSpeaker(true)
+                                .build());
+
+        for (DiskJson d : disks) {
+            builder.addDisk(d.toConfig());
+        }
+        builder.setDisplayConfig(display.toConfig(context)).setGpuConfig(gpu.toConfig());
+
+        return builder.build();
+    }
+
+    private static class DiskJson {
+        private DiskJson() {}
+
+        private boolean writable;
+        private String image;
+        private PartitionJson[] partitions;
+
+        private Disk toConfig() {
+            Disk d = writable ? Disk.RWDisk(image) : Disk.RODisk(image);
+            for (PartitionJson pj : partitions) {
+                boolean writable = this.writable && pj.writable;
+                d.addPartition(new Partition(pj.label, pj.path, writable, pj.guid));
+            }
+            return d;
+        }
+    }
+
+    private static class PartitionJson {
+        private PartitionJson() {}
+
+        private boolean writable;
+        private String label;
+        private String path;
+        private String guid;
+    }
+
+    private static class DisplayJson {
+        private DisplayJson() {}
+
+        private float scale;
+        private int refresh_rate;
+
+        private DisplayConfig toConfig(Context context) {
+            WindowManager wm = context.getSystemService(WindowManager.class);
+            WindowMetrics metrics = wm.getCurrentWindowMetrics();
+            Rect dispBounds = metrics.getBounds();
+
+            // TODO: make this overridable by json
+            int width = dispBounds.right;
+            int height = dispBounds.bottom;
+
+            int dpi = (int) (DisplayMetrics.DENSITY_DEFAULT * metrics.getDensity());
+            if (scale > 0.0f) {
+                dpi = (int) (dpi * scale);
+            }
+
+            int refreshRate = (int) context.getDisplay().getRefreshRate();
+            if (this.refresh_rate != 0) {
+                refreshRate = this.refresh_rate;
+            }
+
+            return new DisplayConfig.Builder()
+                    .setWidth(width)
+                    .setHeight(height)
+                    .setHorizontalDpi(dpi)
+                    .setVerticalDpi(dpi)
+                    .setRefreshRate(refreshRate)
+                    .build();
+        }
+    }
+
+    private static class GpuJson {
+        private GpuJson() {}
+
+        private String backend;
+        private String pci_address;
+        private String renderer_features;
+        private boolean renderer_use_egl = true;
+        private boolean renderer_use_gles = true;
+        private boolean renderer_use_glx = false;
+        private boolean renderer_use_surfaceless = true;
+        private boolean renderer_use_vulkan = false;
+        private String[] context_types;
+
+        private GpuConfig toConfig() {
+            return new GpuConfig.Builder()
+                    .setBackend(backend)
+                    .setPciAddress(pci_address)
+                    .setRendererFeatures(renderer_features)
+                    .setRendererUseEgl(renderer_use_egl)
+                    .setRendererUseGles(renderer_use_gles)
+                    .setRendererUseGlx(renderer_use_glx)
+                    .setRendererUseSurfaceless(renderer_use_surfaceless)
+                    .setRendererUseVulkan(renderer_use_vulkan)
+                    .setContextTypes(context_types)
+                    .build();
+        }
+    }
+}
diff --git a/android/VmLauncherApp/proguard.flags b/android/VmLauncherApp/proguard.flags
new file mode 100644
index 0000000..5e05ecf
--- /dev/null
+++ b/android/VmLauncherApp/proguard.flags
@@ -0,0 +1,7 @@
+# Keep the no-args constructor of the deserialized class
+-keepclassmembers class com.android.virtualization.vmlauncher.VmConfigJson {
+  <init>();
+}
+-keepclassmembers class com.android.virtualization.vmlauncher.VmConfigJson$* {
+  <init>();
+}