Merge "Convert Logger to kotlin" into main
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/ErrorActivity.java b/android/TerminalApp/java/com/android/virtualization/terminal/ErrorActivity.java
deleted file mode 100644
index 7099f22..0000000
--- a/android/TerminalApp/java/com/android/virtualization/terminal/ErrorActivity.java
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * Copyright 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.Intent;
-import android.os.Bundle;
-import android.view.View;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import java.io.IOException;
-import java.io.PrintWriter;
-import java.io.StringWriter;
-
-public class ErrorActivity extends BaseActivity {
-    private static final String EXTRA_CAUSE = "cause";
-
-    public static void start(Context context, Exception e) {
-        Intent intent = new Intent(context, ErrorActivity.class);
-        intent.putExtra(EXTRA_CAUSE, e);
-        intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
-        context.startActivity(intent);
-    }
-
-    @Override
-    protected void onCreate(@Nullable Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        setContentView(R.layout.activity_error);
-
-        View button = findViewById(R.id.recovery);
-        button.setOnClickListener((event) -> launchRecoveryActivity());
-    }
-
-    @Override
-    protected void onNewIntent(@NonNull Intent intent) {
-        super.onNewIntent(intent);
-        setIntent(intent);
-    }
-
-    @Override
-    public void onResume() {
-        super.onResume();
-
-        Intent intent = getIntent();
-        Exception e = intent.getParcelableExtra(EXTRA_CAUSE, Exception.class);
-        TextView cause = findViewById(R.id.cause);
-        if (e != null) {
-            String stackTrace = getStackTrace(e);
-            cause.setText(getString(R.string.error_code, stackTrace));
-        } else {
-            cause.setText(null);
-        }
-    }
-
-    private void launchRecoveryActivity() {
-        Intent intent = new Intent(this, SettingsRecoveryActivity.class);
-        startActivity(intent);
-    }
-
-    private static String getStackTrace(Exception e) {
-        try (StringWriter sWriter = new StringWriter();
-                PrintWriter pWriter = new PrintWriter(sWriter)) {
-            e.printStackTrace(pWriter);
-            return sWriter.toString();
-        } catch (IOException ex) {
-            // This shall never happen
-            throw new RuntimeException(ex);
-        }
-    }
-}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/ErrorActivity.kt b/android/TerminalApp/java/com/android/virtualization/terminal/ErrorActivity.kt
new file mode 100644
index 0000000..f253f86
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/ErrorActivity.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright 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.Intent
+import android.os.Bundle
+import android.view.View
+import android.widget.TextView
+import java.io.IOException
+import java.io.PrintWriter
+import java.io.StringWriter
+import java.lang.Exception
+import java.lang.RuntimeException
+
+class ErrorActivity : BaseActivity() {
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        setContentView(R.layout.activity_error)
+
+        val button = findViewById<View>(R.id.recovery)
+        button.setOnClickListener(View.OnClickListener { _ -> launchRecoveryActivity() })
+    }
+
+    override fun onNewIntent(intent: Intent) {
+        super.onNewIntent(intent)
+        setIntent(intent)
+    }
+
+    override fun onResume() {
+        super.onResume()
+
+        val intent = getIntent()
+        val e = intent.getParcelableExtra<Exception?>(EXTRA_CAUSE, Exception::class.java)
+        val cause = findViewById<TextView>(R.id.cause)
+        cause.text = e?.let { getString(R.string.error_code, getStackTrace(it)) }
+    }
+
+    private fun launchRecoveryActivity() {
+        val intent = Intent(this, SettingsRecoveryActivity::class.java)
+        startActivity(intent)
+    }
+
+    companion object {
+        private const val EXTRA_CAUSE = "cause"
+
+        @JvmStatic
+        fun start(context: Context, e: Exception) {
+            val intent = Intent(context, ErrorActivity::class.java)
+            intent.putExtra(EXTRA_CAUSE, e)
+            intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
+            context.startActivity(intent)
+        }
+
+        private fun getStackTrace(e: Exception): String? {
+            try {
+                StringWriter().use { sWriter ->
+                    PrintWriter(sWriter).use { pWriter ->
+                        e.printStackTrace(pWriter)
+                        return sWriter.toString()
+                    }
+                }
+            } catch (ex: IOException) {
+                // This shall never happen
+                throw RuntimeException(ex)
+            }
+        }
+    }
+}
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
+        }
+    }
+}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/TerminalExceptionHandler.java b/android/TerminalApp/java/com/android/virtualization/terminal/TerminalExceptionHandler.java
deleted file mode 100644
index 4ab2b77..0000000
--- a/android/TerminalApp/java/com/android/virtualization/terminal/TerminalExceptionHandler.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright 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.util.Log;
-
-public class TerminalExceptionHandler implements Thread.UncaughtExceptionHandler {
-    private static final String TAG = "TerminalExceptionHandler";
-
-    private final Context mContext;
-
-    public TerminalExceptionHandler(Context context) {
-        mContext = context;
-    }
-
-    @Override
-    public void uncaughtException(Thread thread, Throwable throwable) {
-        Exception exception;
-        if (throwable instanceof Exception) {
-            exception = (Exception) throwable;
-        } else {
-            exception = new Exception(throwable);
-        }
-        try {
-            ErrorActivity.start(mContext, exception);
-        } catch (Exception ex) {
-            Log.wtf(TAG, "Failed to launch error activity for an exception", exception);
-        }
-
-        thread.getDefaultUncaughtExceptionHandler().uncaughtException(thread, throwable);
-    }
-}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/TerminalExceptionHandler.kt b/android/TerminalApp/java/com/android/virtualization/terminal/TerminalExceptionHandler.kt
new file mode 100644
index 0000000..3a8c444
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/TerminalExceptionHandler.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 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.util.Log
+import java.lang.Exception
+
+class TerminalExceptionHandler(private val context: Context) : Thread.UncaughtExceptionHandler {
+
+    override fun uncaughtException(thread: Thread, throwable: Throwable) {
+        val exception = (throwable as? Exception) ?: Exception(throwable)
+        try {
+            ErrorActivity.start(context, exception)
+        } catch (_: Exception) {
+            Log.wtf(TAG, "Failed to launch error activity for an exception", exception)
+        }
+        Thread.getDefaultUncaughtExceptionHandler()?.uncaughtException(thread, throwable)
+    }
+
+    companion object {
+        private const val TAG = "TerminalExceptionHandler"
+    }
+}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/TerminalThreadFactory.java b/android/TerminalApp/java/com/android/virtualization/terminal/TerminalThreadFactory.java
deleted file mode 100644
index 5ee535d..0000000
--- a/android/TerminalApp/java/com/android/virtualization/terminal/TerminalThreadFactory.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright 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 java.util.concurrent.Executors;
-import java.util.concurrent.ThreadFactory;
-
-public class TerminalThreadFactory implements ThreadFactory {
-    private final Context mContext;
-
-    public TerminalThreadFactory(Context context) {
-        mContext = context;
-    }
-
-    @Override
-    public Thread newThread(Runnable r) {
-        Thread thread = Executors.defaultThreadFactory().newThread(r);
-        thread.setUncaughtExceptionHandler(new TerminalExceptionHandler(mContext));
-        return thread;
-    }
-}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/TerminalThreadFactory.kt b/android/TerminalApp/java/com/android/virtualization/terminal/TerminalThreadFactory.kt
new file mode 100644
index 0000000..f8e909d
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/TerminalThreadFactory.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 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 java.util.concurrent.Executors
+import java.util.concurrent.ThreadFactory
+
+class TerminalThreadFactory(private val context: Context) : ThreadFactory {
+    override fun newThread(r: Runnable): Thread {
+        return Executors.defaultThreadFactory().newThread(r).also {
+            it.uncaughtExceptionHandler = TerminalExceptionHandler(context)
+        }
+    }
+}
diff --git a/build/debian/build.sh b/build/debian/build.sh
index 3db6a40..613f7d2 100755
--- a/build/debian/build.sh
+++ b/build/debian/build.sh
@@ -57,18 +57,16 @@
 			;;
 	esac
 	if [[ "${*:$OPTIND:1}" ]]; then
-		built_image="${*:$OPTIND:1}"
+		output="${*:$OPTIND:1}"
 	fi
 }
 
 prepare_build_id() {
-	local filename=build_id
 	if [ -z "${KOKORO_BUILD_NUMBER}" ]; then
-		echo eng-$(hostname)-$(date --utc) > ${filename}
+		echo eng-$(hostname)-$(date --utc)
 	else
-		echo ${KOKORO_BUILD_NUMBER} > ${filename}
+		echo ${KOKORO_BUILD_NUMBER}
 	fi
-	echo ${filename}
 }
 
 install_prerequisites() {
@@ -302,17 +300,22 @@
 }
 
 run_fai() {
-	local out="${built_image}"
+	local out="${raw_disk_image}"
 	make -C "${debian_cloud_image}" "image_bookworm_nocloud_${debian_arch}"
 	mv "${debian_cloud_image}/image_bookworm_nocloud_${debian_arch}.raw" "${out}"
 }
 
-extract_partitions() {
+generate_output_package() {
+	fdisk -l "${raw_disk_image}"
 	root_partition_num=1
 	bios_partition_num=14
 	efi_partition_num=15
 
-	loop=$(losetup -f --show --partscan $built_image)
+	pushd ${workdir} > /dev/null
+
+	echo ${build_id} > build_id
+
+	loop=$(losetup -f --show --partscan $raw_disk_image)
 	dd if="${loop}p$root_partition_num" of=root_part
 	if [[ "$arch" == "x86_64" ]]; then
 		dd if="${loop}p$bios_partition_num" of=bios_part
@@ -320,11 +323,38 @@
 	dd if="${loop}p$efi_partition_num" of=efi_part
 	losetup -d "${loop}"
 
-	sed -i "s/{root_part_guid}/$(sfdisk --part-uuid $built_image $root_partition_num)/g" vm_config.json
+	cp ${vm_config} vm_config.json
+	sed -i "s/{root_part_guid}/$(sfdisk --part-uuid $raw_disk_image $root_partition_num)/g" vm_config.json
 	if [[ "$arch" == "x86_64" ]]; then
-		sed -i "s/{bios_part_guid}/$(sfdisk --part-uuid $built_image $bios_partition_num)/g" vm_config.json
+		sed -i "s/{bios_part_guid}/$(sfdisk --part-uuid $raw_disk_image $bios_partition_num)/g" vm_config.json
 	fi
-	sed -i "s/{efi_part_guid}/$(sfdisk --part-uuid $built_image $efi_partition_num)/g" vm_config.json
+	sed -i "s/{efi_part_guid}/$(sfdisk --part-uuid $raw_disk_image $efi_partition_num)/g" vm_config.json
+
+	images=()
+	if [[ "$arch" == "aarch64" ]]; then
+		images+=(
+			root_part
+			efi_part
+		)
+	# TODO(b/365955006): remove these lines when uboot supports x86_64 EFI application
+	elif [[ "$arch" == "x86_64" ]]; then
+		rm -f vmlinuz initrd.img
+		virt-get-kernel -a "${raw_disk_image}"
+		mv vmlinuz* vmlinuz
+		mv initrd.img* initrd.img
+		images+=(
+			bios_part
+			root_part
+			efi_part
+			vmlinuz
+			initrd.img
+		)
+	fi
+
+	popd > /dev/null
+
+	# --sparse option isn't supported in apache-commons-compress
+	tar czv -f ${output} -C ${workdir} build_id "${images[@]}" vm_config.json
 }
 
 clean_up() {
@@ -334,14 +364,16 @@
 set -e
 trap clean_up EXIT
 
-built_image=image.raw
+output=images.tar.gz
 workdir=$(mktemp -d)
+raw_disk_image=${workdir}/image.raw
 build_id=$(prepare_build_id)
 debian_cloud_image=${workdir}/debian_cloud_image
 debian_version=bookworm
 config_space=${debian_cloud_image}/config_space/${debian_version}
 resources_dir=${debian_cloud_image}/src/debian_cloud_images/resources
 arch="$(uname -m)"
+vm_config="$(realpath $(dirname "$0"))/vm_config.json.${arch}"
 mode=debug
 save_workdir=0
 use_custom_kernel=0
@@ -353,32 +385,4 @@
 copy_android_config
 package_custom_kernel
 run_fai
-fdisk -l "${built_image}"
-images=()
-
-cp "$(dirname "$0")/vm_config.json.${arch}" vm_config.json
-
-extract_partitions
-
-if [[ "$arch" == "aarch64" ]]; then
-	images+=(
-		root_part
-		efi_part
-	)
-# TODO(b/365955006): remove these lines when uboot supports x86_64 EFI application
-elif [[ "$arch" == "x86_64" ]]; then
-	rm -f vmlinuz initrd.img
-	virt-get-kernel -a "${built_image}"
-	mv vmlinuz* vmlinuz
-	mv initrd.img* initrd.img
-	images+=(
-		bios_part
-		root_part
-		efi_part
-		vmlinuz
-		initrd.img
-	)
-fi
-
-# --sparse option isn't supported in apache-commons-compress
-tar czv -f images.tar.gz ${build_id} "${images[@]}" vm_config.json
+generate_output_package
diff --git a/libs/libavf/include/android/virtualization.h b/libs/libavf/include/android/virtualization.h
index 6b54bf7..ef57325 100644
--- a/libs/libavf/include/android/virtualization.h
+++ b/libs/libavf/include/android/virtualization.h
@@ -70,7 +70,13 @@
                                      const char* _Nonnull name) __INTRODUCED_IN(36);
 
 /**
- * Set an instance ID of a virtual machine.
+ * Set an instance ID of a virtual machine. Every virtual machine is identified by a unique
+ * `instanceId` which the virtual machine uses as its persistent identity while performing stateful
+ * operations that are expected to outlast single boot of the VM. For example, some virtual machines
+ * use it as a `Id` for storing secrets in Secretkeeper, which are retrieved on next boot of th VM.
+ *
+ * The `instanceId` is expected to be re-used for the VM instance with an associated state (secret,
+ * encrypted storage) - i.e., rebooting the VM must not change the instanceId.
  *
  * \param config a virtual machine config object.
  * \param instanceId a pointer to a 64-byte buffer for the instance ID.