Convert InstalledImage to kotlin

Bug: 383243644
Test: install vm image
Change-Id: Idc2271d0739a36638a8a33aa33fdedb07336cc54
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/InstalledImage.java b/android/TerminalApp/java/com/android/virtualization/terminal/InstalledImage.java
deleted file mode 100644
index 318f49a..0000000
--- a/android/TerminalApp/java/com/android/virtualization/terminal/InstalledImage.java
+++ /dev/null
@@ -1,212 +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 static com.android.virtualization.terminal.MainActivity.TAG;
-
-import android.content.Context;
-import android.os.FileUtils;
-import android.system.ErrnoException;
-import android.system.Os;
-import android.util.Log;
-
-import java.io.BufferedReader;
-import java.io.FileDescriptor;
-import java.io.FileReader;
-import java.io.IOException;
-import java.io.RandomAccessFile;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.StandardCopyOption;
-
-/** Collection of files that consist of a VM image. */
-class InstalledImage {
-    private static final String INSTALL_DIRNAME = "linux";
-    private static final String ROOTFS_FILENAME = "root_part";
-    private static final String BACKUP_FILENAME = "root_part_backup";
-    private static final String CONFIG_FILENAME = "vm_config.json";
-    private static final String BUILD_ID_FILENAME = "build_id";
-    static final String MARKER_FILENAME = "completed";
-
-    public static final long RESIZE_STEP_BYTES = 4 << 20; // 4 MiB
-
-    private final Path mDir;
-    private final Path mRootPartition;
-    private final Path mBackup;
-    private final Path mConfig;
-    private final Path mMarker;
-    private String mBuildId;
-
-    /** Returns InstalledImage for a given app context */
-    public static InstalledImage getDefault(Context context) {
-        Path installDir = context.getFilesDir().toPath().resolve(INSTALL_DIRNAME);
-        return new InstalledImage(installDir);
-    }
-
-    private InstalledImage(Path dir) {
-        mDir = dir;
-        mRootPartition = dir.resolve(ROOTFS_FILENAME);
-        mBackup = dir.resolve(BACKUP_FILENAME);
-        mConfig = dir.resolve(CONFIG_FILENAME);
-        mMarker = dir.resolve(MARKER_FILENAME);
-    }
-
-    public Path getInstallDir() {
-        return mDir;
-    }
-
-    /** Tests if this InstalledImage is actually installed. */
-    public boolean isInstalled() {
-        return Files.exists(mMarker);
-    }
-
-    /** Fully understalls this InstalledImage by deleting everything. */
-    public void uninstallFully() throws IOException {
-        FileUtils.deleteContentsAndDir(mDir.toFile());
-    }
-
-    /** Returns the path to the VM config file. */
-    public Path getConfigPath() {
-        return mConfig;
-    }
-
-    /** Returns the build ID of the installed image */
-    public String getBuildId() {
-        if (mBuildId == null) {
-            mBuildId = readBuildId();
-        }
-        return mBuildId;
-    }
-
-    private String readBuildId() {
-        Path file = mDir.resolve(BUILD_ID_FILENAME);
-        if (!Files.exists(file)) {
-            return "<no build id>";
-        }
-        try (BufferedReader r = new BufferedReader(new FileReader(file.toFile()))) {
-            return r.readLine();
-        } catch (IOException e) {
-            throw new RuntimeException("Failed to read build ID", e);
-        }
-    }
-
-    public Path uninstallAndBackup() throws IOException {
-        Files.delete(mMarker);
-        Files.move(mRootPartition, mBackup, StandardCopyOption.REPLACE_EXISTING);
-        return mBackup;
-    }
-
-    public Path getBackupFile() {
-        return mBackup;
-    }
-
-    public boolean hasBackup() {
-        return Files.exists(mBackup);
-    }
-
-    public void deleteBackup() throws IOException {
-        Files.deleteIfExists(mBackup);
-    }
-
-    public long getSize() throws IOException {
-        return Files.size(mRootPartition);
-    }
-
-    public long getSmallestSizePossible() throws IOException {
-        runE2fsck(mRootPartition);
-        String p = mRootPartition.toAbsolutePath().toString();
-        String result = runCommand("/system/bin/resize2fs", "-P", p);
-        // The return value is the number of 4k block
-        try {
-            long minSize =
-                    Long.parseLong(result.lines().toArray(String[]::new)[1].substring(42))
-                            * 4
-                            * 1024;
-            return roundUp(minSize);
-        } catch (NumberFormatException e) {
-            Log.e(TAG, "Failed to parse min size, p=" + p + ", result=" + result);
-            throw new IOException(e);
-        }
-    }
-
-    public long resize(long desiredSize) throws IOException {
-        desiredSize = roundUp(desiredSize);
-        final long curSize = getSize();
-
-        if (desiredSize == curSize) {
-            return desiredSize;
-        }
-
-        runE2fsck(mRootPartition);
-        if (desiredSize > curSize) {
-            allocateSpace(mRootPartition, desiredSize);
-        }
-        resizeFilesystem(mRootPartition, desiredSize);
-        return getSize();
-    }
-
-    private static void allocateSpace(Path path, long sizeInBytes) throws IOException {
-        try {
-            RandomAccessFile raf = new RandomAccessFile(path.toFile(), "rw");
-            FileDescriptor fd = raf.getFD();
-            Os.posix_fallocate(fd, 0, sizeInBytes);
-            raf.close();
-            Log.d(TAG, "Allocated space to: " + sizeInBytes + " bytes");
-        } catch (ErrnoException e) {
-            Log.e(TAG, "Failed to allocate space", e);
-            throw new IOException("Failed to allocate space", e);
-        }
-    }
-
-    private static void runE2fsck(Path path) throws IOException {
-        String p = path.toAbsolutePath().toString();
-        runCommand("/system/bin/e2fsck", "-y", "-f", p);
-        Log.d(TAG, "e2fsck completed: " + path);
-    }
-
-    private static void resizeFilesystem(Path path, long sizeInBytes) throws IOException {
-        long sizeInMB = sizeInBytes / (1024 * 1024);
-        if (sizeInMB == 0) {
-            Log.e(TAG, "Invalid size: " + sizeInBytes + " bytes");
-            throw new IllegalArgumentException("Size cannot be zero MB");
-        }
-        String sizeArg = sizeInMB + "M";
-        String p = path.toAbsolutePath().toString();
-        runCommand("/system/bin/resize2fs", p, sizeArg);
-        Log.d(TAG, "resize2fs completed: " + path + ", size: " + sizeArg);
-    }
-
-    private static String runCommand(String... command) throws IOException {
-        try {
-            Process process = new ProcessBuilder(command).redirectErrorStream(true).start();
-            process.waitFor();
-            String result = new String(process.getInputStream().readAllBytes());
-            if (process.exitValue() != 0) {
-                Log.w(TAG, "Process returned with error, command=" + String.join(" ", command)
-                    + ", exitValue=" + process.exitValue() + ", result=" + result);
-            }
-            return result;
-        } catch (InterruptedException e) {
-            Thread.currentThread().interrupt();
-            throw new IOException("Command interrupted", e);
-        }
-    }
-
-    private static long roundUp(long bytes) {
-        // Round up every diskSizeStep MB
-        return (long) Math.ceil(((double) bytes) / RESIZE_STEP_BYTES) * RESIZE_STEP_BYTES;
-    }
-}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/InstalledImage.kt b/android/TerminalApp/java/com/android/virtualization/terminal/InstalledImage.kt
new file mode 100644
index 0000000..e52f996
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/InstalledImage.kt
@@ -0,0 +1,200 @@
+/*
+ * 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.os.FileUtils
+import android.system.ErrnoException
+import android.system.Os
+import android.util.Log
+import com.android.virtualization.terminal.MainActivity.TAG
+import java.io.BufferedReader
+import java.io.FileReader
+import java.io.IOException
+import java.io.RandomAccessFile
+import java.lang.IllegalArgumentException
+import java.lang.NumberFormatException
+import java.lang.RuntimeException
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.StandardCopyOption
+import kotlin.math.ceil
+
+/** Collection of files that consist of a VM image. */
+internal class InstalledImage private constructor(val installDir: Path) {
+    private val rootPartition: Path = installDir.resolve(ROOTFS_FILENAME)
+    val backupFile: Path = installDir.resolve(BACKUP_FILENAME)
+
+    /** The path to the VM config file. */
+    val configPath: Path = installDir.resolve(CONFIG_FILENAME)
+    private val marker: Path = installDir.resolve(MARKER_FILENAME)
+    /** The build ID of the installed image */
+    val buildId: String by lazy { readBuildId() }
+
+    /** Tests if this InstalledImage is actually installed. */
+    fun isInstalled(): Boolean {
+        return Files.exists(marker)
+    }
+
+    /** Fully uninstall this InstalledImage by deleting everything. */
+    @Throws(IOException::class)
+    fun uninstallFully() {
+        FileUtils.deleteContentsAndDir(installDir.toFile())
+    }
+
+    private fun readBuildId(): String {
+        val file = installDir.resolve(BUILD_ID_FILENAME)
+        if (!Files.exists(file)) {
+            return "<no build id>"
+        }
+        try {
+            BufferedReader(FileReader(file.toFile())).use { r ->
+                return r.readLine()
+            }
+        } catch (e: IOException) {
+            throw RuntimeException("Failed to read build ID", e)
+        }
+    }
+
+    @Throws(IOException::class)
+    fun uninstallAndBackup(): Path {
+        Files.delete(marker)
+        Files.move(rootPartition, backupFile, StandardCopyOption.REPLACE_EXISTING)
+        return backupFile
+    }
+
+    fun hasBackup(): Boolean {
+        return Files.exists(backupFile)
+    }
+
+    @Throws(IOException::class)
+    fun deleteBackup() {
+        Files.deleteIfExists(backupFile)
+    }
+
+    @Throws(IOException::class)
+    fun getSize(): Long {
+        return Files.size(rootPartition)
+    }
+
+    @Throws(IOException::class)
+    fun getSmallestSizePossible(): Long {
+        runE2fsck(rootPartition)
+        val p: String = rootPartition.toAbsolutePath().toString()
+        val result = runCommand("/system/bin/resize2fs", "-P", p)
+        // The return value is the number of 4k block
+        return try {
+            roundUp(result.lines().first().substring(42).toLong() * 4 * 1024)
+        } catch (e: NumberFormatException) {
+            Log.e(TAG, "Failed to parse min size, p=$p, result=$result")
+            throw IOException(e)
+        }
+    }
+
+    @Throws(IOException::class)
+    fun resize(desiredSize: Long): Long {
+        val roundedUpDesiredSize = roundUp(desiredSize)
+        val curSize = getSize()
+
+        if (roundedUpDesiredSize == curSize) {
+            return roundedUpDesiredSize
+        }
+
+        runE2fsck(rootPartition)
+        if (roundedUpDesiredSize > curSize) {
+            allocateSpace(rootPartition, roundedUpDesiredSize)
+        }
+        resizeFilesystem(rootPartition, roundedUpDesiredSize)
+        return getSize()
+    }
+
+    companion object {
+        private const val INSTALL_DIRNAME = "linux"
+        private const val ROOTFS_FILENAME = "root_part"
+        private const val BACKUP_FILENAME = "root_part_backup"
+        private const val CONFIG_FILENAME = "vm_config.json"
+        private const val BUILD_ID_FILENAME = "build_id"
+        const val MARKER_FILENAME: String = "completed"
+
+        const val RESIZE_STEP_BYTES: Long = 4 shl 20 // 4 MiB
+
+        /** Returns InstalledImage for a given app context */
+        @JvmStatic
+        fun getDefault(context: Context): InstalledImage {
+            val installDir = context.getFilesDir().toPath().resolve(INSTALL_DIRNAME)
+            return InstalledImage(installDir)
+        }
+
+        @Throws(IOException::class)
+        private fun allocateSpace(path: Path, sizeInBytes: Long) {
+            try {
+                val raf = RandomAccessFile(path.toFile(), "rw")
+                val fd = raf.fd
+                Os.posix_fallocate(fd, 0, sizeInBytes)
+                raf.close()
+                Log.d(TAG, "Allocated space to: $sizeInBytes bytes")
+            } catch (e: ErrnoException) {
+                Log.e(TAG, "Failed to allocate space", e)
+                throw IOException("Failed to allocate space", e)
+            }
+        }
+
+        @Throws(IOException::class)
+        private fun runE2fsck(path: Path) {
+            val p: String = path.toAbsolutePath().toString()
+            runCommand("/system/bin/e2fsck", "-y", "-f", p)
+            Log.d(TAG, "e2fsck completed: $path")
+        }
+
+        @Throws(IOException::class)
+        private fun resizeFilesystem(path: Path, sizeInBytes: Long) {
+            val sizeInMB = sizeInBytes / (1024 * 1024)
+            if (sizeInMB == 0L) {
+                Log.e(TAG, "Invalid size: $sizeInBytes bytes")
+                throw IllegalArgumentException("Size cannot be zero MB")
+            }
+            val sizeArg = sizeInMB.toString() + "M"
+            val p: String = path.toAbsolutePath().toString()
+            runCommand("/system/bin/resize2fs", p, sizeArg)
+            Log.d(TAG, "resize2fs completed: $path, size: $sizeArg")
+        }
+
+        @Throws(IOException::class)
+        private fun runCommand(vararg command: String): String {
+            try {
+                val process = ProcessBuilder(*command).redirectErrorStream(true).start()
+                process.waitFor()
+                val result = String(process.inputStream.readAllBytes())
+                if (process.exitValue() != 0) {
+                    Log.w(
+                        TAG,
+                        "Process returned with error, command=${listOf(*command).joinToString(" ")}," +
+                            "exitValue=${process.exitValue()}, result=$result",
+                    )
+                }
+                return result
+            } catch (e: InterruptedException) {
+                Thread.currentThread().interrupt()
+                throw IOException("Command interrupted", e)
+            }
+        }
+
+        private fun roundUp(bytes: Long): Long {
+            // Round up every diskSizeStep MB
+            return ceil((bytes.toDouble()) / RESIZE_STEP_BYTES).toLong() * RESIZE_STEP_BYTES
+        }
+    }
+}