Merge changes I935e2faf,Ia12df7e6 into main

* changes:
  Convert ImageArchive to kotlin
  Remove TODO to avoid conflicts
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/CertificateUtils.java b/android/TerminalApp/java/com/android/virtualization/terminal/CertificateUtils.java
deleted file mode 100644
index e3d1a67..0000000
--- a/android/TerminalApp/java/com/android/virtualization/terminal/CertificateUtils.java
+++ /dev/null
@@ -1,100 +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 static com.android.virtualization.terminal.MainActivity.TAG;
-
-import android.content.Context;
-import android.security.keystore.KeyGenParameterSpec;
-import android.security.keystore.KeyProperties;
-import android.util.Base64;
-import android.util.Log;
-
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.security.InvalidAlgorithmParameterException;
-import java.security.KeyPairGenerator;
-import java.security.KeyStore;
-import java.security.NoSuchAlgorithmException;
-import java.security.NoSuchProviderException;
-import java.security.cert.Certificate;
-import java.security.cert.CertificateEncodingException;
-import java.security.cert.CertificateExpiredException;
-import java.security.cert.CertificateNotYetValidException;
-import java.security.cert.X509Certificate;
-
-public class CertificateUtils {
-    private static final String ALIAS = "ttyd";
-
-    public static KeyStore.PrivateKeyEntry createOrGetKey() {
-        try {
-            KeyStore ks = KeyStore.getInstance("AndroidKeyStore");
-            ks.load(null);
-
-            if (!ks.containsAlias(ALIAS)) {
-                Log.d(TAG, "there is no keypair, will generate it");
-                createKey();
-            } else if (!(ks.getCertificate(ALIAS) instanceof X509Certificate)) {
-                Log.d(TAG, "certificate isn't X509Certificate or it is invalid");
-                createKey();
-            } else {
-                try {
-                    ((X509Certificate) ks.getCertificate(ALIAS)).checkValidity();
-                } catch (CertificateExpiredException | CertificateNotYetValidException e) {
-                    Log.d(TAG, "certificate is invalid", e);
-                    createKey();
-                }
-            }
-            return ((KeyStore.PrivateKeyEntry) ks.getEntry(ALIAS, null));
-        } catch (Exception e) {
-            throw new RuntimeException("cannot generate or get key", e);
-        }
-    }
-
-    private static void createKey()
-            throws NoSuchAlgorithmException,
-                    NoSuchProviderException,
-                    InvalidAlgorithmParameterException {
-        KeyPairGenerator kpg =
-                KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore");
-        kpg.initialize(
-                new KeyGenParameterSpec.Builder(
-                                ALIAS, KeyProperties.PURPOSE_SIGN | KeyProperties.PURPOSE_VERIFY)
-                        .setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512)
-                        .build());
-
-        kpg.generateKeyPair();
-    }
-
-    public static void writeCertificateToFile(Context context, Certificate cert) {
-        String certFileName = "ca.crt";
-        File certFile = new File(context.getFilesDir(), certFileName);
-        try (FileOutputStream writer = new FileOutputStream(certFile)) {
-            String cert_begin = "-----BEGIN CERTIFICATE-----\n";
-            String end_cert = "-----END CERTIFICATE-----\n";
-            String output =
-                    cert_begin
-                            + Base64.encodeToString(cert.getEncoded(), Base64.DEFAULT)
-                                    .replaceAll("(.{64})", "$1\n")
-                            + end_cert;
-            writer.write(output.getBytes());
-        } catch (IOException | CertificateEncodingException e) {
-            throw new RuntimeException("cannot write certs", e);
-        }
-    }
-}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/CertificateUtils.kt b/android/TerminalApp/java/com/android/virtualization/terminal/CertificateUtils.kt
new file mode 100644
index 0000000..53809e5
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/CertificateUtils.kt
@@ -0,0 +1,110 @@
+/*
+ * 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.security.keystore.KeyGenParameterSpec
+import android.security.keystore.KeyProperties
+import android.util.Base64
+import android.util.Log
+import java.io.File
+import java.io.FileOutputStream
+import java.io.IOException
+import java.lang.Exception
+import java.lang.RuntimeException
+import java.security.InvalidAlgorithmParameterException
+import java.security.KeyPairGenerator
+import java.security.KeyStore
+import java.security.NoSuchAlgorithmException
+import java.security.NoSuchProviderException
+import java.security.cert.Certificate
+import java.security.cert.CertificateEncodingException
+import java.security.cert.CertificateExpiredException
+import java.security.cert.CertificateNotYetValidException
+import java.security.cert.X509Certificate
+
+object CertificateUtils {
+    private const val ALIAS = "ttyd"
+
+    @JvmStatic
+    fun createOrGetKey(): KeyStore.PrivateKeyEntry {
+        try {
+            val ks = KeyStore.getInstance("AndroidKeyStore")
+            ks.load(null)
+
+            if (!ks.containsAlias(ALIAS)) {
+                Log.d(MainActivity.TAG, "there is no keypair, will generate it")
+                createKey()
+            } else if (ks.getCertificate(ALIAS) !is X509Certificate) {
+                Log.d(MainActivity.TAG, "certificate isn't X509Certificate or it is invalid")
+                createKey()
+            } else {
+                try {
+                    (ks.getCertificate(ALIAS) as X509Certificate).checkValidity()
+                } catch (e: CertificateExpiredException) {
+                    Log.d(MainActivity.TAG, "certificate is invalid", e)
+                    createKey()
+                } catch (e: CertificateNotYetValidException) {
+                    Log.d(MainActivity.TAG, "certificate is invalid", e)
+                    createKey()
+                }
+            }
+            return ks.getEntry(ALIAS, null) as KeyStore.PrivateKeyEntry
+        } catch (e: Exception) {
+            throw RuntimeException("cannot generate or get key", e)
+        }
+    }
+
+    @Throws(
+        NoSuchAlgorithmException::class,
+        NoSuchProviderException::class,
+        InvalidAlgorithmParameterException::class,
+    )
+    private fun createKey() {
+        val kpg = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore")
+        kpg.initialize(
+            KeyGenParameterSpec.Builder(
+                    ALIAS,
+                    KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY,
+                )
+                .setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512)
+                .build()
+        )
+
+        kpg.generateKeyPair()
+    }
+
+    @JvmStatic
+    fun writeCertificateToFile(context: Context, cert: Certificate) {
+        val certFile = File(context.getFilesDir(), "ca.crt")
+        try {
+            FileOutputStream(certFile).use { writer ->
+                val certBegin = "-----BEGIN CERTIFICATE-----\n"
+                val certEnd = "-----END CERTIFICATE-----\n"
+                val output =
+                    (certBegin +
+                        Base64.encodeToString(cert.encoded, Base64.DEFAULT)
+                            .replace("(.{64})".toRegex(), "$1\n") +
+                        certEnd)
+                writer.write(output.toByteArray())
+            }
+        } catch (e: IOException) {
+            throw RuntimeException("cannot write certs", e)
+        } catch (e: CertificateEncodingException) {
+            throw RuntimeException("cannot write certs", e)
+        }
+    }
+}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/Logger.java b/android/TerminalApp/java/com/android/virtualization/terminal/Logger.java
deleted file mode 100644
index 2c0149e..0000000
--- a/android/TerminalApp/java/com/android/virtualization/terminal/Logger.java
+++ /dev/null
@@ -1,87 +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.system.virtualmachine.VirtualMachine;
-import android.system.virtualmachine.VirtualMachineConfig;
-import android.system.virtualmachine.VirtualMachineException;
-import android.util.Log;
-
-import libcore.io.Streams;
-
-import java.io.BufferedOutputStream;
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.StandardOpenOption;
-import java.util.concurrent.ExecutorService;
-
-/**
- * Forwards VM's console output to a file on the Android side, and VM's log output to Android logd.
- */
-class Logger {
-    private Logger() {}
-
-    static void setup(VirtualMachine vm, Path path, ExecutorService executor) {
-        if (vm.getConfig().getDebugLevel() != VirtualMachineConfig.DEBUG_LEVEL_FULL) {
-            return;
-        }
-
-        try {
-            InputStream console = vm.getConsoleOutput();
-            OutputStream file = Files.newOutputStream(path, StandardOpenOption.CREATE);
-            executor.submit(() -> Streams.copy(console, new LineBufferedOutputStream(file)));
-
-            InputStream log = vm.getLogOutput();
-            executor.submit(() -> writeToLogd(log, vm.getName()));
-        } catch (VirtualMachineException | IOException e) {
-            throw new RuntimeException(e);
-        }
-    }
-
-    private static boolean writeToLogd(InputStream input, String vmName) throws IOException {
-        BufferedReader reader = new BufferedReader(new InputStreamReader(input));
-        String line;
-        while ((line = reader.readLine()) != null && !Thread.interrupted()) {
-            Log.d(vmName, line);
-        }
-        // TODO: find out why javac complains when the return type of this method is void. It
-        // (incorrectly?) thinks that IOException should be caught inside the lambda.
-        return true;
-    }
-
-    private static class LineBufferedOutputStream extends BufferedOutputStream {
-        LineBufferedOutputStream(OutputStream out) {
-            super(out);
-        }
-
-        @Override
-        public void write(byte[] buf, int off, int len) throws IOException {
-            super.write(buf, off, len);
-            for (int i = 0; i < len; ++i) {
-                if (buf[off + i] == '\n') {
-                    flush();
-                    break;
-                }
-            }
-        }
-    }
-}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/Logger.kt b/android/TerminalApp/java/com/android/virtualization/terminal/Logger.kt
new file mode 100644
index 0000000..3a273ec
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/Logger.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.system.virtualmachine.VirtualMachine
+import android.system.virtualmachine.VirtualMachineConfig
+import android.system.virtualmachine.VirtualMachineException
+import android.util.Log
+import com.android.virtualization.terminal.Logger.LineBufferedOutputStream
+import java.io.BufferedOutputStream
+import java.io.BufferedReader
+import java.io.IOException
+import java.io.InputStream
+import java.io.InputStreamReader
+import java.io.OutputStream
+import java.lang.RuntimeException
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.StandardOpenOption
+import java.util.concurrent.ExecutorService
+import libcore.io.Streams
+
+/**
+ * Forwards VM's console output to a file on the Android side, and VM's log output to Android logd.
+ */
+internal object Logger {
+    @JvmStatic
+    fun setup(vm: VirtualMachine, path: Path, executor: ExecutorService) {
+        if (vm.config.debugLevel != VirtualMachineConfig.DEBUG_LEVEL_FULL) {
+            return
+        }
+
+        try {
+            val console = vm.getConsoleOutput()
+            val file = Files.newOutputStream(path, StandardOpenOption.CREATE)
+            executor.submit<Int?> {
+                console.use { console ->
+                    LineBufferedOutputStream(file).use { fileOutput ->
+                        Streams.copy(console, fileOutput)
+                    }
+                }
+            }
+
+            val log = vm.getLogOutput()
+            executor.submit<Unit> { log.use { writeToLogd(it, vm.name) } }
+        } catch (e: VirtualMachineException) {
+            throw RuntimeException(e)
+        } catch (e: IOException) {
+            throw RuntimeException(e)
+        }
+    }
+
+    @Throws(IOException::class)
+    private fun writeToLogd(input: InputStream?, vmName: String?) {
+        val reader = BufferedReader(InputStreamReader(input))
+        reader
+            .useLines { lines -> lines.takeWhile { !Thread.interrupted() } }
+            .forEach { Log.d(vmName, it) }
+    }
+
+    private class LineBufferedOutputStream(out: OutputStream?) : BufferedOutputStream(out) {
+        @Throws(IOException::class)
+        override fun write(buf: ByteArray, off: Int, len: Int) {
+            super.write(buf, off, len)
+            (0 until len).firstOrNull { buf[off + it] == '\n'.code.toByte() }?.let { flush() }
+        }
+    }
+}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/Runner.java b/android/TerminalApp/java/com/android/virtualization/terminal/Runner.java
deleted file mode 100644
index 4094025..0000000
--- a/android/TerminalApp/java/com/android/virtualization/terminal/Runner.java
+++ /dev/null
@@ -1,115 +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.system.virtualmachine.VirtualMachine;
-import android.system.virtualmachine.VirtualMachineCallback;
-import android.system.virtualmachine.VirtualMachineConfig;
-import android.system.virtualmachine.VirtualMachineCustomImageConfig;
-import android.system.virtualmachine.VirtualMachineException;
-import android.system.virtualmachine.VirtualMachineManager;
-import android.util.Log;
-
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ForkJoinPool;
-
-/** Utility class for creating a VM and waiting for it to finish. */
-class Runner {
-    private final VirtualMachine mVirtualMachine;
-    private final Callback mCallback;
-
-    private Runner(VirtualMachine vm, Callback cb) {
-        mVirtualMachine = vm;
-        mCallback = cb;
-    }
-
-    /** Create a virtual machine of the given config, under the given context. */
-    static Runner create(Context context, VirtualMachineConfig config)
-            throws VirtualMachineException {
-        // context may already be the app context, but calling this again is not harmful.
-        // See b/359439878 on why vmm should be obtained from the app context.
-        Context appContext = context.getApplicationContext();
-        VirtualMachineManager vmm = appContext.getSystemService(VirtualMachineManager.class);
-        VirtualMachineCustomImageConfig customConfig = config.getCustomImageConfig();
-        if (customConfig == null) {
-            throw new RuntimeException("CustomImageConfig is missing");
-        }
-
-        String name = customConfig.getName();
-        if (name == null || name.isEmpty()) {
-            throw new RuntimeException("Virtual machine's name is missing in the config");
-        }
-
-        VirtualMachine vm = vmm.getOrCreate(name, config);
-        try {
-            vm.setConfig(config);
-        } catch (VirtualMachineException e) {
-            vmm.delete(name);
-            vm = vmm.create(name, config);
-            Log.w(TAG, "Re-creating virtual machine (" + name + ")", e);
-        }
-
-        Callback cb = new Callback();
-        vm.setCallback(ForkJoinPool.commonPool(), cb);
-        vm.run();
-        return new Runner(vm, cb);
-    }
-
-    /** Give access to the underlying VirtualMachine object. */
-    VirtualMachine getVm() {
-        return mVirtualMachine;
-    }
-
-    /** Get future about VM's exit status. */
-    CompletableFuture<Boolean> getExitStatus() {
-        return mCallback.mFinishedSuccessfully;
-    }
-
-    private static class Callback implements VirtualMachineCallback {
-        final CompletableFuture<Boolean> mFinishedSuccessfully = new CompletableFuture<>();
-
-        @Override
-        public void onPayloadStarted(VirtualMachine vm) {
-            // This event is only from Microdroid-based VM. Custom VM shouldn't emit this.
-        }
-
-        @Override
-        public void onPayloadReady(VirtualMachine vm) {
-            // This event is only from Microdroid-based VM. Custom VM shouldn't emit this.
-        }
-
-        @Override
-        public void onPayloadFinished(VirtualMachine vm, int exitCode) {
-            // This event is only from Microdroid-based VM. Custom VM shouldn't emit this.
-        }
-
-        @Override
-        public void onError(VirtualMachine vm, int errorCode, String message) {
-            Log.e(TAG, "Error from VM. code: " + errorCode + " (" + message + ")");
-            mFinishedSuccessfully.complete(false);
-        }
-
-        @Override
-        public void onStopped(VirtualMachine vm, int reason) {
-            Log.d(TAG, "VM stopped. Reason: " + reason);
-            mFinishedSuccessfully.complete(true);
-        }
-    }
-}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/Runner.kt b/android/TerminalApp/java/com/android/virtualization/terminal/Runner.kt
new file mode 100644
index 0000000..86dadbe
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/Runner.kt
@@ -0,0 +1,93 @@
+/*
+ * 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.system.virtualmachine.VirtualMachine
+import android.system.virtualmachine.VirtualMachineCallback
+import android.system.virtualmachine.VirtualMachineConfig
+import android.system.virtualmachine.VirtualMachineException
+import android.system.virtualmachine.VirtualMachineManager
+import android.util.Log
+import com.android.virtualization.terminal.MainActivity.TAG
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.ForkJoinPool
+
+/** Utility class for creating a VM and waiting for it to finish. */
+internal class Runner private constructor(val vm: VirtualMachine?, callback: Callback) {
+    /** Get future about VM's exit status. */
+    val exitStatus = callback.finishedSuccessfully
+
+    private class Callback : VirtualMachineCallback {
+        val finishedSuccessfully: CompletableFuture<Boolean?> = CompletableFuture<Boolean?>()
+
+        override fun onPayloadStarted(vm: VirtualMachine) {
+            // This event is only from Microdroid-based VM. Custom VM shouldn't emit this.
+        }
+
+        override fun onPayloadReady(vm: VirtualMachine) {
+            // This event is only from Microdroid-based VM. Custom VM shouldn't emit this.
+        }
+
+        override fun onPayloadFinished(vm: VirtualMachine, exitCode: Int) {
+            // This event is only from Microdroid-based VM. Custom VM shouldn't emit this.
+        }
+
+        override fun onError(vm: VirtualMachine, errorCode: Int, message: String) {
+            Log.e(TAG, "Error from VM. code: $errorCode ($message)")
+            finishedSuccessfully.complete(false)
+        }
+
+        override fun onStopped(vm: VirtualMachine, reason: Int) {
+            Log.d(TAG, "VM stopped. Reason: $reason")
+            finishedSuccessfully.complete(true)
+        }
+    }
+
+    companion object {
+        /** Create a virtual machine of the given config, under the given context. */
+        @JvmStatic
+        @Throws(VirtualMachineException::class)
+        fun create(context: Context, config: VirtualMachineConfig): Runner {
+            // context may already be the app context, but calling this again is not harmful.
+            // See b/359439878 on why vmm should be obtained from the app context.
+            val appContext = context.getApplicationContext()
+            val vmm =
+                appContext.getSystemService<VirtualMachineManager>(
+                    VirtualMachineManager::class.java
+                )
+            val customConfig = config.customImageConfig
+            requireNotNull(customConfig) { "CustomImageConfig is missing" }
+
+            val name = customConfig.name
+            require(!name.isNullOrEmpty()) { "Virtual machine's name is missing in the config" }
+
+            var vm = vmm.getOrCreate(name, config)
+            try {
+                vm.config = config
+            } catch (e: VirtualMachineException) {
+                vmm.delete(name)
+                vm = vmm.create(name, config)
+                Log.w(TAG, "Re-creating virtual machine ($name)", e)
+            }
+
+            val cb = Callback()
+            vm.setCallback(ForkJoinPool.commonPool(), cb)
+            vm.run()
+            return Runner(vm, cb)
+        }
+    }
+}
diff --git a/libs/libavf/Android.bp b/libs/libavf/Android.bp
index 079f4ae..b583e21 100644
--- a/libs/libavf/Android.bp
+++ b/libs/libavf/Android.bp
@@ -10,6 +10,7 @@
     source_stem: "bindings",
     bindgen_flags: ["--default-enum-style rust"],
     apex_available: ["com.android.virt"],
+    visibility: ["//packages/modules/Virtualization/tests/vts"],
 }
 
 rust_defaults {
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.
diff --git a/tests/vts/Android.bp b/tests/vts/Android.bp
new file mode 100644
index 0000000..35fbcdc
--- /dev/null
+++ b/tests/vts/Android.bp
@@ -0,0 +1,35 @@
+prebuilt_etc {
+    name: "vts_libavf_test_kernel",
+    filename: "rialto.bin",
+    src: ":empty_file",
+    target: {
+        android_arm64: {
+            src: ":rialto_signed",
+        },
+    },
+    installable: false,
+    visibility: ["//visibility:private"],
+}
+
+rust_test {
+    name: "vts_libavf_test",
+    crate_name: "vts_libavf_test",
+    srcs: ["src/vts_libavf_test.rs"],
+    rustlibs: [
+        "libanyhow",
+        "libavf_bindgen",
+        "libciborium",
+        "liblog_rust",
+        "libscopeguard",
+        "libservice_vm_comm",
+        "libvsock",
+    ],
+    shared_libs: ["libavf"],
+    test_suites: [
+        "general-tests",
+        "vts",
+    ],
+    data: [":vts_libavf_test_kernel"],
+    test_config: "AndroidTest.xml",
+    compile_multilib: "first",
+}
diff --git a/tests/vts/AndroidTest.xml b/tests/vts/AndroidTest.xml
new file mode 100644
index 0000000..75c8d31
--- /dev/null
+++ b/tests/vts/AndroidTest.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<configuration description="Runs vts_libavf_test.">
+    <option name="test-suite-tag" value="vts" />
+
+    <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" />
+
+    <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
+        <option name="cleanup" value="true" />
+        <option name="push" value="vts_libavf_test->/data/local/tmp/vts_libavf_test" />
+        <option name="push" value="rialto.bin->/data/local/tmp/rialto.bin" />
+    </target_preparer>
+
+    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.ArchModuleController">
+        <option name="arch" value="arm64" />
+    </object>
+
+    <test class="com.android.tradefed.testtype.rust.RustBinaryTest" >
+        <option name="test-device-path" value="/data/local/tmp" />
+        <option name="module-name" value="vts_libavf_test" />
+        <!-- rialto uses a fixed port number for the host, can't run two tests at the same time -->
+        <option name="native-test-flag" value="--test-threads=1" />
+    </test>
+</configuration>
diff --git a/tests/vts/src/vts_libavf_test.rs b/tests/vts/src/vts_libavf_test.rs
new file mode 100644
index 0000000..ba38a2e
--- /dev/null
+++ b/tests/vts/src/vts_libavf_test.rs
@@ -0,0 +1,186 @@
+// 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.
+
+//! Tests running a VM with LLNDK
+
+use anyhow::{bail, ensure, Context, Result};
+use log::info;
+use std::ffi::CStr;
+use std::fs::File;
+use std::io::{self, BufWriter, Write};
+use std::os::fd::IntoRawFd;
+use std::time::{Duration, Instant};
+use vsock::{VsockListener, VsockStream, VMADDR_CID_HOST};
+
+use avf_bindgen::*;
+use service_vm_comm::{Request, Response, ServiceVmRequest, VmType};
+
+const VM_MEMORY_MB: i32 = 16;
+const WRITE_BUFFER_CAPACITY: usize = 512;
+
+const LISTEN_TIMEOUT: Duration = Duration::from_secs(10);
+const READ_TIMEOUT: Duration = Duration::from_secs(10);
+const WRITE_TIMEOUT: Duration = Duration::from_secs(10);
+const STOP_TIMEOUT: timespec = timespec { tv_sec: 10, tv_nsec: 0 };
+
+/// Processes the request in the service VM.
+fn process_request(vsock_stream: &mut VsockStream, request: Request) -> Result<Response> {
+    write_request(vsock_stream, &ServiceVmRequest::Process(request))?;
+    read_response(vsock_stream)
+}
+
+/// Sends the request to the service VM.
+fn write_request(vsock_stream: &mut VsockStream, request: &ServiceVmRequest) -> Result<()> {
+    let mut buffer = BufWriter::with_capacity(WRITE_BUFFER_CAPACITY, vsock_stream);
+    ciborium::into_writer(request, &mut buffer)?;
+    buffer.flush().context("Failed to flush the buffer")?;
+    Ok(())
+}
+
+/// Reads the response from the service VM.
+fn read_response(vsock_stream: &mut VsockStream) -> Result<Response> {
+    let response: Response = ciborium::from_reader(vsock_stream)
+        .context("Failed to read the response from the service VM")?;
+    Ok(response)
+}
+
+fn listen_from_guest(port: u32) -> Result<VsockStream> {
+    let vsock_listener =
+        VsockListener::bind_with_cid_port(VMADDR_CID_HOST, port).context("Failed to bind vsock")?;
+    vsock_listener.set_nonblocking(true).context("Failed to set nonblocking")?;
+    let start_time = Instant::now();
+    loop {
+        if start_time.elapsed() >= LISTEN_TIMEOUT {
+            bail!("Timeout while listening");
+        }
+        match vsock_listener.accept() {
+            Ok((vsock_stream, _peer_addr)) => return Ok(vsock_stream),
+            Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
+                std::thread::sleep(Duration::from_millis(100));
+            }
+            Err(e) => bail!("Failed to listen: {e:?}"),
+        }
+    }
+}
+
+fn run_rialto(protected_vm: bool) -> Result<()> {
+    let kernel_file =
+        File::open("/data/local/tmp/rialto.bin").context("Failed to open kernel file")?;
+    let kernel_fd = kernel_file.into_raw_fd();
+
+    // SAFETY: AVirtualMachineRawConfig_create() isn't unsafe but rust_bindgen forces it to be seen
+    // as unsafe
+    let config = unsafe { AVirtualMachineRawConfig_create() };
+
+    info!("raw config created");
+
+    // SAFETY: config is the only reference to a valid object
+    unsafe {
+        AVirtualMachineRawConfig_setName(
+            config,
+            CStr::from_bytes_with_nul(b"vts_libavf_test_rialto\0").unwrap().as_ptr(),
+        );
+        AVirtualMachineRawConfig_setKernel(config, kernel_fd);
+        AVirtualMachineRawConfig_setProtectedVm(config, protected_vm);
+        AVirtualMachineRawConfig_setMemoryMiB(config, VM_MEMORY_MB);
+    }
+
+    let mut vm = std::ptr::null_mut();
+    let mut service = std::ptr::null_mut();
+
+    ensure!(
+        // SAFETY: &mut service is a valid pointer to *AVirtualizationService
+        unsafe { AVirtualizationService_create(&mut service, false) } == 0,
+        "AVirtualizationService_create failed"
+    );
+
+    scopeguard::defer! {
+        // SAFETY: service is a valid pointer to AVirtualizationService
+        unsafe { AVirtualizationService_destroy(service); }
+    }
+
+    ensure!(
+        // SAFETY: &mut vm is a valid pointer to *AVirtualMachine
+        unsafe {
+            AVirtualMachine_createRaw(
+                service, config, -1, // console_in
+                -1, // console_out
+                -1, // log
+                &mut vm,
+            )
+        } == 0,
+        "AVirtualMachine_createRaw failed"
+    );
+
+    scopeguard::defer! {
+        // SAFETY: vm is a valid pointer to AVirtualMachine
+        unsafe { AVirtualMachine_destroy(vm); }
+    }
+
+    info!("vm created");
+
+    let vm_type = if protected_vm { VmType::ProtectedVm } else { VmType::NonProtectedVm };
+
+    let listener_thread = std::thread::spawn(move || listen_from_guest(vm_type.port()));
+
+    // SAFETY: vm is the only reference to a valid object
+    unsafe {
+        AVirtualMachine_start(vm);
+    }
+
+    info!("VM started");
+
+    let mut vsock_stream = listener_thread.join().unwrap()?;
+    vsock_stream.set_read_timeout(Some(READ_TIMEOUT))?;
+    vsock_stream.set_write_timeout(Some(WRITE_TIMEOUT))?;
+
+    info!("client connected");
+
+    let request_data = vec![1, 2, 3, 4, 5];
+    let expected_data = vec![5, 4, 3, 2, 1];
+    let response = process_request(&mut vsock_stream, Request::Reverse(request_data))
+        .context("Failed to process request")?;
+    let Response::Reverse(reversed_data) = response else {
+        bail!("Expected Response::Reverse but was {response:?}");
+    };
+    ensure!(reversed_data == expected_data, "Expected {expected_data:?} but was {reversed_data:?}");
+
+    info!("request processed");
+
+    write_request(&mut vsock_stream, &ServiceVmRequest::Shutdown)
+        .context("Failed to send shutdown")?;
+
+    info!("shutdown sent");
+
+    let mut stop_reason = AVirtualMachineStopReason::AVIRTUAL_MACHINE_UNRECOGNISED;
+    ensure!(
+        // SAFETY: vm is the only reference to a valid object
+        unsafe { AVirtualMachine_waitForStop(vm, &STOP_TIMEOUT, &mut stop_reason) },
+        "AVirtualMachine_waitForStop failed"
+    );
+
+    info!("stopped");
+
+    Ok(())
+}
+
+#[test]
+fn test_run_rialto_protected() -> Result<()> {
+    run_rialto(true /* protected_vm */)
+}
+
+#[test]
+fn test_run_rialto_non_protected() -> Result<()> {
+    run_rialto(false /* protected_vm */)
+}