Merge "Convert ConfigJson to kotlin" into main
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/ConfigJson.java b/android/TerminalApp/java/com/android/virtualization/terminal/ConfigJson.java
deleted file mode 100644
index a0fca82..0000000
--- a/android/TerminalApp/java/com/android/virtualization/terminal/ConfigJson.java
+++ /dev/null
@@ -1,354 +0,0 @@
-/*
- * 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.terminal;
-
-
-import android.content.Context;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.graphics.Rect;
-import android.os.Environment;
-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.system.virtualmachine.VirtualMachineCustomImageConfig.SharedPath;
-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.BufferedReader;
-import java.io.FileReader;
-import java.io.IOException;
-import java.io.Reader;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Objects;
-import java.util.stream.Collectors;
-
-/** This class and its inner classes model vm_config.json. */
-class ConfigJson {
-    private static final boolean DEBUG = true;
-
-    private ConfigJson() {}
-
-    @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 boolean debuggable;
-    private boolean console_out;
-    private boolean connect_console;
-    private boolean network;
-    private InputJson input;
-    private AudioJson audio;
-    private DiskJson[] disks;
-    private SharedPathJson[] sharedPath;
-    private DisplayJson display;
-    private GpuJson gpu;
-    private boolean auto_memory_balloon;
-
-    /** Parses JSON file at jsonPath */
-    static ConfigJson from(Context context, Path jsonPath) {
-        try (FileReader fileReader = new FileReader(jsonPath.toFile())) {
-            String content = replaceKeywords(fileReader, context);
-            return new Gson().fromJson(content, ConfigJson.class);
-        } catch (Exception e) {
-            throw new RuntimeException("Failed to parse " + jsonPath, e);
-        }
-    }
-
-    private static String replaceKeywords(Reader r, Context context) throws IOException {
-        Map<String, String> rules = new HashMap<>();
-        rules.put("\\$PAYLOAD_DIR", InstalledImage.getDefault(context).getInstallDir().toString());
-        rules.put("\\$USER_ID", String.valueOf(context.getUserId()));
-        rules.put("\\$PACKAGE_NAME", context.getPackageName());
-        rules.put("\\$APP_DATA_DIR", context.getDataDir().toString());
-
-        try (BufferedReader br = new BufferedReader(r)) {
-            return br.lines()
-                    .map(
-                            line -> {
-                                for (Map.Entry<String, String> rule : rules.entrySet()) {
-                                    line = line.replaceAll(rule.getKey(), rule.getValue());
-                                }
-                                return line;
-                            })
-                    .collect(Collectors.joining("\n"));
-        }
-    }
-
-    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);
-        }
-    }
-
-    private int getDebugLevel() {
-        return debuggable
-                ? VirtualMachineConfig.DEBUG_LEVEL_FULL
-                : VirtualMachineConfig.DEBUG_LEVEL_NONE;
-    }
-
-    /** Converts this parsed JSON into VirtualMachieConfig Builder */
-    VirtualMachineConfig.Builder toConfigBuilder(Context context) {
-        return new VirtualMachineConfig.Builder(context)
-                .setProtectedVm(isProtected)
-                .setMemoryBytes((long) memory_mib * 1024 * 1024)
-                .setConsoleInputDevice(console_input_device)
-                .setCpuTopology(getCpuTopology())
-                .setCustomImageConfig(toCustomImageConfigBuilder(context).build())
-                .setDebugLevel(getDebugLevel())
-                .setVmOutputCaptured(console_out)
-                .setConnectVmConsole(connect_console);
-    }
-
-    VirtualMachineCustomImageConfig.Builder toCustomImageConfigBuilder(Context context) {
-        VirtualMachineCustomImageConfig.Builder builder =
-                new VirtualMachineCustomImageConfig.Builder();
-
-        builder.setName(name)
-                .setBootloaderPath(bootloader)
-                .setKernelPath(kernel)
-                .setInitrdPath(initrd)
-                .useNetwork(network)
-                .useAutoMemoryBalloon(auto_memory_balloon);
-
-        if (input != null) {
-            builder.useTouch(input.touchscreen)
-                    .useKeyboard(input.keyboard)
-                    .useMouse(input.mouse)
-                    .useTrackpad(input.trackpad)
-                    .useSwitches(input.switches);
-        }
-
-        if (audio != null) {
-            builder.setAudioConfig(audio.toConfig());
-        }
-
-        if (display != null) {
-            builder.setDisplayConfig(display.toConfig(context));
-        }
-
-        if (gpu != null) {
-            builder.setGpuConfig(gpu.toConfig());
-        }
-
-        if (params != null) {
-            Arrays.stream(params.split(" ")).forEach(builder::addParam);
-        }
-
-        if (disks != null) {
-            Arrays.stream(disks).map(d -> d.toConfig()).forEach(builder::addDisk);
-        }
-
-        if (sharedPath != null) {
-            Arrays.stream(sharedPath)
-                    .map(d -> d.toConfig(context))
-                    .filter(Objects::nonNull)
-                    .forEach(builder::addSharedPath);
-        }
-        return builder;
-    }
-
-    private static class SharedPathJson {
-        private SharedPathJson() {}
-
-        private String sharedPath;
-        private static final int GUEST_UID = 1000;
-        private static final int GUEST_GID = 100;
-
-        private SharedPath toConfig(Context context) {
-            try {
-                int terminalUid = getTerminalUid(context);
-                if (sharedPath.contains("emulated")) {
-                    if (Environment.isExternalStorageManager()) {
-                        int currentUserId = context.getUserId();
-                        String path = sharedPath + "/" + currentUserId + "/Download";
-                        return new SharedPath(
-                                path,
-                                terminalUid,
-                                terminalUid,
-                                GUEST_UID,
-                                GUEST_GID,
-                                0007,
-                                "android",
-                                "android",
-                                false, /* app domain is set to false so that crosvm is spin up as child of virtmgr */
-                                "");
-                    }
-                    return null;
-                }
-                Path socketPath = context.getFilesDir().toPath().resolve("internal.virtiofs");
-                Files.deleteIfExists(socketPath);
-                return new SharedPath(
-                        sharedPath,
-                        terminalUid,
-                        terminalUid,
-                        0,
-                        0,
-                        0007,
-                        "internal",
-                        "internal",
-                        true, /* app domain is set to true so that crosvm is spin up from app context */
-                        socketPath.toString());
-            } catch (NameNotFoundException | IOException e) {
-                return null;
-            }
-        }
-
-        private int getTerminalUid(Context context) throws NameNotFoundException {
-            return context.getPackageManager()
-                    .getPackageUidAsUser(context.getPackageName(), context.getUserId());
-        }
-    }
-
-    private static class InputJson {
-        private InputJson() {}
-
-        private boolean touchscreen;
-        private boolean keyboard;
-        private boolean mouse;
-        private boolean switches;
-        private boolean trackpad;
-    }
-
-    private static class AudioJson {
-        private AudioJson() {}
-
-        private boolean microphone;
-        private boolean speaker;
-
-        private AudioConfig toConfig() {
-            return new AudioConfig.Builder()
-                    .setUseMicrophone(microphone)
-                    .setUseSpeaker(speaker)
-                    .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 int width_pixels;
-        private int height_pixels;
-
-        private DisplayConfig toConfig(Context context) {
-            WindowManager wm = context.getSystemService(WindowManager.class);
-            WindowMetrics metrics = wm.getCurrentWindowMetrics();
-            Rect dispBounds = metrics.getBounds();
-
-            int width = width_pixels > 0 ? width_pixels : dispBounds.right;
-            int height = height_pixels > 0 ? height_pixels : 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/TerminalApp/java/com/android/virtualization/terminal/ConfigJson.kt b/android/TerminalApp/java/com/android/virtualization/terminal/ConfigJson.kt
new file mode 100644
index 0000000..1fd58cd
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/ConfigJson.kt
@@ -0,0 +1,333 @@
+/*
+ * 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.terminal
+
+import android.content.Context
+import android.content.pm.PackageManager
+import android.os.Environment
+import android.system.virtualmachine.VirtualMachineConfig
+import android.system.virtualmachine.VirtualMachineCustomImageConfig
+import android.util.DisplayMetrics
+import android.view.WindowManager
+import com.android.virtualization.terminal.ConfigJson.AudioJson
+import com.android.virtualization.terminal.ConfigJson.DiskJson
+import com.android.virtualization.terminal.ConfigJson.DisplayJson
+import com.android.virtualization.terminal.ConfigJson.GpuJson
+import com.android.virtualization.terminal.ConfigJson.InputJson
+import com.android.virtualization.terminal.ConfigJson.PartitionJson
+import com.android.virtualization.terminal.ConfigJson.SharedPathJson
+import com.android.virtualization.terminal.InstalledImage.Companion.getDefault
+import com.google.gson.Gson
+import com.google.gson.annotations.SerializedName
+import java.io.BufferedReader
+import java.io.FileReader
+import java.io.IOException
+import java.io.Reader
+import java.lang.Exception
+import java.lang.RuntimeException
+import java.nio.file.Files
+import java.nio.file.Path
+
+/** This class and its inner classes model vm_config.json. */
+internal data class ConfigJson(
+    @SerializedName("protected") private val isProtected: Boolean,
+    private val name: String?,
+    private val cpu_topology: String?,
+    private val platform_version: String?,
+    private val memory_mib: Int = 1024,
+    private val console_input_device: String?,
+    private val bootloader: String?,
+    private val kernel: String?,
+    private val initrd: String?,
+    private val params: String?,
+    private val debuggable: Boolean,
+    private val console_out: Boolean,
+    private val connect_console: Boolean,
+    private val network: Boolean,
+    private val input: InputJson?,
+    private val audio: AudioJson?,
+    private val disks: Array<DiskJson>?,
+    private val sharedPath: Array<SharedPathJson>?,
+    private val display: DisplayJson?,
+    private val gpu: GpuJson?,
+    private val auto_memory_balloon: Boolean,
+) {
+    private fun getCpuTopology(): Int {
+        return when (cpu_topology) {
+            "one_cpu" -> VirtualMachineConfig.CPU_TOPOLOGY_ONE_CPU
+            "match_host" -> VirtualMachineConfig.CPU_TOPOLOGY_MATCH_HOST
+            else -> throw RuntimeException("invalid cpu topology: $cpu_topology")
+        }
+    }
+
+    private fun getDebugLevel(): Int {
+        return if (debuggable) VirtualMachineConfig.DEBUG_LEVEL_FULL
+        else VirtualMachineConfig.DEBUG_LEVEL_NONE
+    }
+
+    /** Converts this parsed JSON into VirtualMachineConfig Builder */
+    fun toConfigBuilder(context: Context): VirtualMachineConfig.Builder {
+        return VirtualMachineConfig.Builder(context)
+            .setProtectedVm(isProtected)
+            .setMemoryBytes(memory_mib.toLong() * 1024 * 1024)
+            .setConsoleInputDevice(console_input_device)
+            .setCpuTopology(getCpuTopology())
+            .setCustomImageConfig(toCustomImageConfigBuilder(context).build())
+            .setDebugLevel(getDebugLevel())
+            .setVmOutputCaptured(console_out)
+            .setConnectVmConsole(connect_console)
+    }
+
+    fun toCustomImageConfigBuilder(context: Context): VirtualMachineCustomImageConfig.Builder {
+        val builder = VirtualMachineCustomImageConfig.Builder()
+
+        builder
+            .setName(name)
+            .setBootloaderPath(bootloader)
+            .setKernelPath(kernel)
+            .setInitrdPath(initrd)
+            .useNetwork(network)
+            .useAutoMemoryBalloon(auto_memory_balloon)
+
+        if (input != null) {
+            builder
+                .useTouch(input.touchscreen)
+                .useKeyboard(input.keyboard)
+                .useMouse(input.mouse)
+                .useTrackpad(input.trackpad)
+                .useSwitches(input.switches)
+        }
+
+        if (audio != null) {
+            builder.setAudioConfig(audio.toConfig())
+        }
+
+        if (display != null) {
+            builder.setDisplayConfig(display.toConfig(context))
+        }
+
+        if (gpu != null) {
+            builder.setGpuConfig(gpu.toConfig())
+        }
+
+        params?.split(" ".toRegex())?.filter { it.isNotEmpty() }?.forEach { builder.addParam(it) }
+
+        disks?.forEach { builder.addDisk(it.toConfig()) }
+
+        sharedPath?.mapNotNull { it.toConfig(context) }?.forEach { builder.addSharedPath(it) }
+
+        return builder
+    }
+
+    internal data class SharedPathJson(private val sharedPath: String?) {
+        fun toConfig(context: Context): VirtualMachineCustomImageConfig.SharedPath? {
+            try {
+                val terminalUid = getTerminalUid(context)
+                if (sharedPath?.contains("emulated") == true) {
+                    if (Environment.isExternalStorageManager()) {
+                        val currentUserId = context.userId
+                        val path = "$sharedPath/$currentUserId/Download"
+                        return VirtualMachineCustomImageConfig.SharedPath(
+                            path,
+                            terminalUid,
+                            terminalUid,
+                            GUEST_UID,
+                            GUEST_GID,
+                            7,
+                            "android",
+                            "android",
+                            false, /* app domain is set to false so that crosvm is spin up as child of virtmgr */
+                            "",
+                        )
+                    }
+                    return null
+                }
+                val socketPath = context.getFilesDir().toPath().resolve("internal.virtiofs")
+                Files.deleteIfExists(socketPath)
+                return VirtualMachineCustomImageConfig.SharedPath(
+                    sharedPath,
+                    terminalUid,
+                    terminalUid,
+                    0,
+                    0,
+                    7,
+                    "internal",
+                    "internal",
+                    true, /* app domain is set to true so that crosvm is spin up from app context */
+                    socketPath.toString(),
+                )
+            } catch (e: PackageManager.NameNotFoundException) {
+                return null
+            } catch (e: IOException) {
+                return null
+            }
+        }
+
+        @Throws(PackageManager.NameNotFoundException::class)
+        fun getTerminalUid(context: Context): Int {
+            return context
+                .getPackageManager()
+                .getPackageUidAsUser(context.getPackageName(), context.userId)
+        }
+
+        companion object {
+            private const val GUEST_UID = 1000
+            private const val GUEST_GID = 100
+        }
+    }
+
+    internal data class InputJson(
+        val touchscreen: Boolean,
+        val keyboard: Boolean,
+        val mouse: Boolean,
+        val switches: Boolean,
+        val trackpad: Boolean,
+    )
+
+    internal data class AudioJson(private val microphone: Boolean, private val speaker: Boolean) {
+
+        fun toConfig(): VirtualMachineCustomImageConfig.AudioConfig {
+            return VirtualMachineCustomImageConfig.AudioConfig.Builder()
+                .setUseMicrophone(microphone)
+                .setUseSpeaker(speaker)
+                .build()
+        }
+    }
+
+    internal data class DiskJson(
+        private val writable: Boolean,
+        private val image: String?,
+        private val partitions: Array<PartitionJson>?,
+    ) {
+        fun toConfig(): VirtualMachineCustomImageConfig.Disk {
+            val d =
+                if (writable) VirtualMachineCustomImageConfig.Disk.RWDisk(image)
+                else VirtualMachineCustomImageConfig.Disk.RODisk(image)
+            partitions?.forEach {
+                val writable = this.writable && it.writable
+                d.addPartition(
+                    VirtualMachineCustomImageConfig.Partition(it.label, it.path, writable, it.guid)
+                )
+            }
+            return d
+        }
+    }
+
+    internal data class PartitionJson(
+        val writable: Boolean,
+        val label: String?,
+        val path: String?,
+        val guid: String?,
+    )
+
+    internal data class DisplayJson(
+        private val scale: Float = 0f,
+        private val refresh_rate: Int = 0,
+        private val width_pixels: Int = 0,
+        private val height_pixels: Int = 0,
+    ) {
+        fun toConfig(context: Context): VirtualMachineCustomImageConfig.DisplayConfig {
+            val wm = context.getSystemService<WindowManager>(WindowManager::class.java)
+            val metrics = wm.currentWindowMetrics
+            val dispBounds = metrics.bounds
+
+            val width = if (width_pixels > 0) width_pixels else dispBounds.right
+            val height = if (height_pixels > 0) height_pixels else dispBounds.bottom
+
+            var dpi = (DisplayMetrics.DENSITY_DEFAULT * metrics.density).toInt()
+            if (scale > 0.0f) {
+                dpi = (dpi * scale).toInt()
+            }
+
+            var refreshRate = context.display.refreshRate.toInt()
+            if (this.refresh_rate != 0) {
+                refreshRate = this.refresh_rate
+            }
+
+            return VirtualMachineCustomImageConfig.DisplayConfig.Builder()
+                .setWidth(width)
+                .setHeight(height)
+                .setHorizontalDpi(dpi)
+                .setVerticalDpi(dpi)
+                .setRefreshRate(refreshRate)
+                .build()
+        }
+    }
+
+    internal data class GpuJson(
+        private val backend: String?,
+        private val pci_address: String?,
+        private val renderer_features: String?,
+        // TODO: GSON actaully ignores the default values
+        private val renderer_use_egl: Boolean = true,
+        private val renderer_use_gles: Boolean = true,
+        private val renderer_use_glx: Boolean = false,
+        private val renderer_use_surfaceless: Boolean = true,
+        private val renderer_use_vulkan: Boolean = false,
+        private val context_types: Array<String>?,
+    ) {
+        fun toConfig(): VirtualMachineCustomImageConfig.GpuConfig {
+            return VirtualMachineCustomImageConfig.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()
+        }
+    }
+
+    companion object {
+        private const val DEBUG = true
+
+        /** Parses JSON file at jsonPath */
+        fun from(context: Context, jsonPath: Path): ConfigJson {
+            try {
+                FileReader(jsonPath.toFile()).use { fileReader ->
+                    val content = replaceKeywords(fileReader, context)
+                    return Gson().fromJson<ConfigJson?>(content, ConfigJson::class.java)
+                }
+            } catch (e: Exception) {
+                throw RuntimeException("Failed to parse $jsonPath", e)
+            }
+        }
+
+        @Throws(IOException::class)
+        private fun replaceKeywords(r: Reader, context: Context): String {
+            val rules: Map<String, String> =
+                mapOf(
+                    "\\\$PAYLOAD_DIR" to getDefault(context).installDir.toString(),
+                    "\\\$USER_ID" to context.userId.toString(),
+                    "\\\$PACKAGE_NAME" to context.getPackageName(),
+                    "\\\$APP_DATA_DIR" to context.getDataDir().toString(),
+                )
+
+            return BufferedReader(r).useLines { lines ->
+                lines
+                    .map { line ->
+                        rules.entries.fold(line) { acc, rule ->
+                            acc.replace(rule.key.toRegex(), rule.value)
+                        }
+                    }
+                    .joinToString("\n")
+            }
+        }
+    }
+}