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
+ }
+ }
+}