Merge "Add *.GTS variants of our tests" into main
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/PortNotifier.java b/android/TerminalApp/java/com/android/virtualization/terminal/PortNotifier.java
deleted file mode 100644
index 0d70ab9..0000000
--- a/android/TerminalApp/java/com/android/virtualization/terminal/PortNotifier.java
+++ /dev/null
@@ -1,146 +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.app.Notification;
-import android.app.Notification.Action;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.graphics.drawable.Icon;
-
-import java.util.HashSet;
-import java.util.Locale;
-import java.util.Set;
-
-/**
- * PortNotifier is responsible for posting a notification when a new open port is detected. User can
- * enable or disable forwarding of the port in notification panel.
- */
-class PortNotifier {
-    private static final String ACTION_PORT_FORWARDING = "android.virtualization.PORT_FORWARDING";
-    private static final String KEY_PORT = "port";
-    private static final String KEY_ENABLED = "enabled";
-
-    private final Context mContext;
-    private final NotificationManager mNotificationManager;
-    private final BroadcastReceiver mReceiver;
-    private final PortsStateManager mPortsStateManager;
-    private final PortsStateManager.Listener mPortsStateListener;
-
-    public PortNotifier(Context context) {
-        mContext = context;
-        mNotificationManager = mContext.getSystemService(NotificationManager.class);
-        mReceiver = new PortForwardingRequestReceiver();
-
-        mPortsStateManager = PortsStateManager.getInstance(mContext);
-        mPortsStateListener =
-                new PortsStateManager.Listener() {
-                    @Override
-                    public void onPortsStateUpdated(
-                            Set<Integer> oldActivePorts, Set<Integer> newActivePorts) {
-                        Set<Integer> union = new HashSet<>(oldActivePorts);
-                        union.addAll(newActivePorts);
-                        for (int port : union) {
-                            if (!oldActivePorts.contains(port)) {
-                                showNotificationFor(port);
-                            } else if (!newActivePorts.contains(port)) {
-                                discardNotificationFor(port);
-                            }
-                        }
-                    }
-                };
-        mPortsStateManager.registerListener(mPortsStateListener);
-
-        IntentFilter intentFilter = new IntentFilter(ACTION_PORT_FORWARDING);
-        mContext.registerReceiver(mReceiver, intentFilter, Context.RECEIVER_NOT_EXPORTED);
-    }
-
-    public void stop() {
-        mPortsStateManager.unregisterListener(mPortsStateListener);
-        mContext.unregisterReceiver(mReceiver);
-    }
-
-    private String getString(int resId) {
-        return mContext.getString(resId);
-    }
-
-    private PendingIntent getPendingIntentFor(int port, boolean enabled) {
-        Intent intent = new Intent(ACTION_PORT_FORWARDING);
-        intent.setPackage(mContext.getPackageName());
-        intent.setIdentifier(String.format(Locale.ROOT, "%d_%b", port, enabled));
-        intent.putExtra(KEY_PORT, port);
-        intent.putExtra(KEY_ENABLED, enabled);
-        return PendingIntent.getBroadcast(mContext, 0, intent, PendingIntent.FLAG_IMMUTABLE);
-    }
-
-    private void showNotificationFor(int port) {
-        Intent tapIntent = new Intent(mContext, SettingsPortForwardingActivity.class);
-        tapIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
-        PendingIntent tapPendingIntent =
-                PendingIntent.getActivity(mContext, 0, tapIntent, PendingIntent.FLAG_IMMUTABLE);
-
-        String title = getString(R.string.settings_port_forwarding_notification_title);
-        String content =
-                mContext.getString(R.string.settings_port_forwarding_notification_content, port);
-        String acceptText = getString(R.string.settings_port_forwarding_notification_accept);
-        String denyText = getString(R.string.settings_port_forwarding_notification_deny);
-        Icon icon = Icon.createWithResource(mContext, R.drawable.ic_launcher_foreground);
-
-        Action acceptAction =
-                new Action.Builder(icon, acceptText, getPendingIntentFor(port, true /* enabled */))
-                        .build();
-        Action denyAction =
-                new Action.Builder(icon, denyText, getPendingIntentFor(port, false /* enabled */))
-                        .build();
-        Notification notification =
-                new Notification.Builder(mContext, mContext.getPackageName())
-                        .setSmallIcon(R.drawable.ic_launcher_foreground)
-                        .setContentTitle(title)
-                        .setContentText(content)
-                        .setContentIntent(tapPendingIntent)
-                        .addAction(acceptAction)
-                        .addAction(denyAction)
-                        .build();
-        mNotificationManager.notify(TAG, port, notification);
-    }
-
-    private void discardNotificationFor(int port) {
-        mNotificationManager.cancel(TAG, port);
-    }
-
-    private final class PortForwardingRequestReceiver extends BroadcastReceiver {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            if (ACTION_PORT_FORWARDING.equals(intent.getAction())) {
-                performActionPortForwarding(context, intent);
-            }
-        }
-
-        private void performActionPortForwarding(Context context, Intent intent) {
-            int port = intent.getIntExtra(KEY_PORT, 0);
-            boolean enabled = intent.getBooleanExtra(KEY_ENABLED, false);
-            mPortsStateManager.updateEnabledPort(port, enabled);
-            discardNotificationFor(port);
-        }
-    }
-}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/PortNotifier.kt b/android/TerminalApp/java/com/android/virtualization/terminal/PortNotifier.kt
new file mode 100644
index 0000000..dd58e0a
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/PortNotifier.kt
@@ -0,0 +1,134 @@
+/*
+ * 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.app.Notification
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.graphics.drawable.Icon
+import com.android.virtualization.terminal.MainActivity.TAG
+import java.util.Locale
+
+/**
+ * PortNotifier is responsible for posting a notification when a new open port is detected. User can
+ * enable or disable forwarding of the port in notification panel.
+ */
+internal class PortNotifier(val context: Context) {
+    private val notificationManager: NotificationManager =
+        context.getSystemService<NotificationManager>(NotificationManager::class.java)
+    private val receiver: BroadcastReceiver =
+        PortForwardingRequestReceiver().also {
+            val intentFilter = IntentFilter(ACTION_PORT_FORWARDING)
+            context.registerReceiver(it, intentFilter, Context.RECEIVER_NOT_EXPORTED)
+        }
+    private val portsStateListener: PortsStateManager.Listener =
+        object : PortsStateManager.Listener {
+            override fun onPortsStateUpdated(oldActivePorts: Set<Int>, newActivePorts: Set<Int>) {
+                // added active ports
+                (newActivePorts - oldActivePorts).forEach { showNotificationFor(it) }
+                // removed active ports
+                (oldActivePorts - newActivePorts).forEach { discardNotificationFor(it) }
+            }
+        }
+    private val portsStateManager: PortsStateManager =
+        PortsStateManager.getInstance(context).also { it.registerListener(portsStateListener) }
+
+    fun stop() {
+        portsStateManager.unregisterListener(portsStateListener)
+        context.unregisterReceiver(receiver)
+    }
+
+    private fun getString(resId: Int): String {
+        return context.getString(resId)
+    }
+
+    private fun getPendingIntentFor(port: Int, enabled: Boolean): PendingIntent {
+        val intent = Intent(ACTION_PORT_FORWARDING)
+        intent.setPackage(context.getPackageName())
+        intent.setIdentifier(String.format(Locale.ROOT, "%d_%b", port, enabled))
+        intent.putExtra(KEY_PORT, port)
+        intent.putExtra(KEY_ENABLED, enabled)
+        return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
+    }
+
+    private fun showNotificationFor(port: Int) {
+        val tapIntent = Intent(context, SettingsPortForwardingActivity::class.java)
+        tapIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP)
+        val tapPendingIntent =
+            PendingIntent.getActivity(context, 0, tapIntent, PendingIntent.FLAG_IMMUTABLE)
+
+        val title = getString(R.string.settings_port_forwarding_notification_title)
+        val content =
+            context.getString(R.string.settings_port_forwarding_notification_content, port)
+        val acceptText = getString(R.string.settings_port_forwarding_notification_accept)
+        val denyText = getString(R.string.settings_port_forwarding_notification_deny)
+        val icon = Icon.createWithResource(context, R.drawable.ic_launcher_foreground)
+
+        val acceptAction: Notification.Action =
+            Notification.Action.Builder(
+                    icon,
+                    acceptText,
+                    getPendingIntentFor(port, true /* enabled */),
+                )
+                .build()
+        val denyAction: Notification.Action =
+            Notification.Action.Builder(
+                    icon,
+                    denyText,
+                    getPendingIntentFor(port, false /* enabled */),
+                )
+                .build()
+        val notification: Notification =
+            Notification.Builder(context, context.getPackageName())
+                .setSmallIcon(R.drawable.ic_launcher_foreground)
+                .setContentTitle(title)
+                .setContentText(content)
+                .setContentIntent(tapPendingIntent)
+                .addAction(acceptAction)
+                .addAction(denyAction)
+                .build()
+        notificationManager.notify(TAG, port, notification)
+    }
+
+    private fun discardNotificationFor(port: Int) {
+        notificationManager.cancel(TAG, port)
+    }
+
+    private inner class PortForwardingRequestReceiver : BroadcastReceiver() {
+        override fun onReceive(context: Context?, intent: Intent) {
+            if (ACTION_PORT_FORWARDING == intent.action) {
+                performActionPortForwarding(intent)
+            }
+        }
+
+        fun performActionPortForwarding(intent: Intent) {
+            val port = intent.getIntExtra(KEY_PORT, 0)
+            val enabled = intent.getBooleanExtra(KEY_ENABLED, false)
+            portsStateManager.updateEnabledPort(port, enabled)
+            discardNotificationFor(port)
+        }
+    }
+
+    companion object {
+        private const val ACTION_PORT_FORWARDING = "android.virtualization.PORT_FORWARDING"
+        private const val KEY_PORT = "port"
+        private const val KEY_ENABLED = "enabled"
+    }
+}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/PortsStateManager.java b/android/TerminalApp/java/com/android/virtualization/terminal/PortsStateManager.java
deleted file mode 100644
index 5321d89..0000000
--- a/android/TerminalApp/java/com/android/virtualization/terminal/PortsStateManager.java
+++ /dev/null
@@ -1,158 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.virtualization.terminal;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-
-import com.android.internal.annotations.GuardedBy;
-
-import java.util.HashSet;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-/**
- * PortsStateManager is responsible for communicating with shared preferences and managing state of
- * ports.
- */
-public class PortsStateManager {
-    private static final String PREFS_NAME = ".PORTS";
-    private static final int FLAG_ENABLED = 1;
-
-    private static PortsStateManager mInstance;
-    private final Object mLock = new Object();
-
-    private final SharedPreferences mSharedPref;
-
-    @GuardedBy("mLock")
-    private Set<Integer> mActivePorts;
-
-    @GuardedBy("mLock")
-    private final Set<Integer> mEnabledPorts;
-
-    @GuardedBy("mLock")
-    private final Set<Listener> mListeners;
-
-    private PortsStateManager(SharedPreferences sharedPref) {
-        mSharedPref = sharedPref;
-        mEnabledPorts =
-                mSharedPref.getAll().entrySet().stream()
-                        .filter(entry -> entry.getValue() instanceof Integer)
-                        .filter(entry -> ((int) entry.getValue() & FLAG_ENABLED) == FLAG_ENABLED)
-                        .map(entry -> entry.getKey())
-                        .filter(
-                                key -> {
-                                    try {
-                                        Integer.parseInt(key);
-                                        return true;
-                                    } catch (NumberFormatException e) {
-                                        return false;
-                                    }
-                                })
-                        .map(Integer::parseInt)
-                        .collect(Collectors.toSet());
-        mActivePorts = new HashSet<>();
-        mListeners = new HashSet<>();
-    }
-
-    static synchronized PortsStateManager getInstance(Context context) {
-        if (mInstance == null) {
-            SharedPreferences sharedPref =
-                    context.getSharedPreferences(
-                            context.getPackageName() + PREFS_NAME, Context.MODE_PRIVATE);
-            mInstance = new PortsStateManager(sharedPref);
-        }
-        return mInstance;
-    }
-
-    Set<Integer> getActivePorts() {
-        synchronized (mLock) {
-            return new HashSet<>(mActivePorts);
-        }
-    }
-
-    Set<Integer> getEnabledPorts() {
-        synchronized (mLock) {
-            return new HashSet<>(mEnabledPorts);
-        }
-    }
-
-    void updateActivePorts(Set<Integer> ports) {
-        Set<Integer> oldPorts;
-        synchronized (mLock) {
-            oldPorts = mActivePorts;
-            mActivePorts = ports;
-        }
-        notifyPortsStateUpdated(oldPorts, ports);
-    }
-
-    void updateEnabledPort(int port, boolean enabled) {
-        Set<Integer> activePorts;
-        synchronized (mLock) {
-            SharedPreferences.Editor editor = mSharedPref.edit();
-            editor.putInt(String.valueOf(port), enabled ? FLAG_ENABLED : 0);
-            editor.apply();
-            if (enabled) {
-                mEnabledPorts.add(port);
-            } else {
-                mEnabledPorts.remove(port);
-            }
-            activePorts = mActivePorts;
-        }
-        notifyPortsStateUpdated(activePorts, activePorts);
-    }
-
-    void clearEnabledPorts() {
-        Set<Integer> activePorts;
-        synchronized (mLock) {
-            SharedPreferences.Editor editor = mSharedPref.edit();
-            editor.clear();
-            editor.apply();
-            mEnabledPorts.clear();
-            activePorts = mActivePorts;
-        }
-        notifyPortsStateUpdated(activePorts, activePorts);
-    }
-
-    void registerListener(Listener listener) {
-        synchronized (mLock) {
-            mListeners.add(listener);
-        }
-    }
-
-    void unregisterListener(Listener listener) {
-        synchronized (mLock) {
-            mListeners.remove(listener);
-        }
-    }
-
-    private void notifyPortsStateUpdated(Set<Integer> oldActivePorts, Set<Integer> newActivePorts) {
-        Set<Listener> listeners;
-        synchronized (mLock) {
-            listeners = new HashSet<>(mListeners);
-        }
-        for (Listener listener : listeners) {
-            listener.onPortsStateUpdated(
-                    new HashSet<>(oldActivePorts), new HashSet<>(newActivePorts));
-        }
-    }
-
-    interface Listener {
-        default void onPortsStateUpdated(
-                Set<Integer> oldActivePorts, Set<Integer> newActivePorts) {}
-    }
-}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/PortsStateManager.kt b/android/TerminalApp/java/com/android/virtualization/terminal/PortsStateManager.kt
new file mode 100644
index 0000000..7e53cce
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/PortsStateManager.kt
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.virtualization.terminal
+
+import android.content.Context
+import android.content.SharedPreferences
+import com.android.internal.annotations.GuardedBy
+import java.util.HashSet
+
+/**
+ * PortsStateManager is responsible for communicating with shared preferences and managing state of
+ * ports.
+ */
+class PortsStateManager private constructor(private val sharedPref: SharedPreferences) {
+    private val lock = Any()
+
+    @GuardedBy("lock") private val activePorts: MutableSet<Int> = hashSetOf()
+
+    @GuardedBy("lock")
+    private val enabledPorts: MutableSet<Int> =
+        sharedPref
+            .getAll()
+            .entries
+            .filterIsInstance<MutableMap.MutableEntry<String, Int>>()
+            .filter { it.value and FLAG_ENABLED == FLAG_ENABLED }
+            .map { it.key.toIntOrNull() }
+            .filterNotNull()
+            .toMutableSet()
+
+    @GuardedBy("lock") private val listeners: MutableSet<Listener> = hashSetOf()
+
+    fun getActivePorts(): Set<Int> {
+        synchronized(lock) {
+            return HashSet<Int>(activePorts)
+        }
+    }
+
+    fun getEnabledPorts(): Set<Int> {
+        synchronized(lock) {
+            return HashSet<Int>(enabledPorts)
+        }
+    }
+
+    fun updateActivePorts(ports: Set<Int>) {
+        synchronized(lock) {
+            val oldPorts = getActivePorts()
+            activePorts.clear()
+            activePorts.addAll(ports)
+            notifyPortsStateUpdated(oldPorts, getActivePorts())
+        }
+    }
+
+    fun updateEnabledPort(port: Int, enabled: Boolean) {
+        synchronized(lock) {
+            val editor = sharedPref.edit()
+            editor.putInt(port.toString(), if (enabled) FLAG_ENABLED else 0)
+            editor.apply()
+            if (enabled) {
+                enabledPorts.add(port)
+            } else {
+                enabledPorts.remove(port)
+            }
+        }
+        notifyPortsStateUpdated(getActivePorts(), getActivePorts())
+    }
+
+    fun clearEnabledPorts() {
+        synchronized(lock) {
+            val editor = sharedPref.edit()
+            editor.clear()
+            editor.apply()
+            enabledPorts.clear()
+        }
+        notifyPortsStateUpdated(getActivePorts(), getActivePorts())
+    }
+
+    fun registerListener(listener: Listener) {
+        synchronized(lock) { listeners.add(listener) }
+    }
+
+    fun unregisterListener(listener: Listener) {
+        synchronized(lock) { listeners.remove(listener) }
+    }
+
+    // TODO: it notifies when both enabledPort and activePort are changed, but doesn't provide
+    // enabledPort's value change. Make this callback provide that information as well.
+    private fun notifyPortsStateUpdated(oldActivePorts: Set<Int>, newActivePorts: Set<Int>) {
+        synchronized(lock) { HashSet<Listener>(this@PortsStateManager.listeners) }
+            .forEach {
+                it.onPortsStateUpdated(HashSet<Int>(oldActivePorts), HashSet<Int>(newActivePorts))
+            }
+    }
+
+    interface Listener {
+        fun onPortsStateUpdated(oldActivePorts: Set<Int>, newActivePorts: Set<Int>) {}
+    }
+
+    companion object {
+        private const val PREFS_NAME = ".PORTS"
+        private const val FLAG_ENABLED = 1
+
+        private var instance: PortsStateManager? = null
+
+        @JvmStatic
+        @Synchronized
+        fun getInstance(context: Context): PortsStateManager {
+            if (instance == null) {
+                val sharedPref =
+                    context.getSharedPreferences(
+                        context.getPackageName() + PREFS_NAME,
+                        Context.MODE_PRIVATE,
+                    )
+                instance = PortsStateManager(sharedPref)
+            }
+            return instance!!
+        }
+    }
+}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/TerminalView.java b/android/TerminalApp/java/com/android/virtualization/terminal/TerminalView.java
deleted file mode 100644
index 0ffc093..0000000
--- a/android/TerminalApp/java/com/android/virtualization/terminal/TerminalView.java
+++ /dev/null
@@ -1,313 +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.graphics.Rect;
-import android.os.Bundle;
-import android.text.InputType;
-import android.text.TextUtils;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.view.View;
-import android.view.View.AccessibilityDelegate;
-import android.view.ViewGroup;
-import android.view.accessibility.AccessibilityEvent;
-import android.view.accessibility.AccessibilityManager;
-import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener;
-import android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener;
-import android.view.accessibility.AccessibilityNodeInfo;
-import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
-import android.view.accessibility.AccessibilityNodeProvider;
-import android.view.inputmethod.EditorInfo;
-import android.view.inputmethod.InputConnection;
-import android.webkit.WebView;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.List;
-
-public class TerminalView extends WebView
-        implements AccessibilityStateChangeListener, TouchExplorationStateChangeListener {
-    // Maximum length of texts the talk back announcements can be. This value is somewhat
-    // arbitrarily set. We may want to adjust this in the future.
-    private static final int TEXT_TOO_LONG_TO_ANNOUNCE = 200;
-
-    private final String CTRL_KEY_HANDLER;
-    private final String ENABLE_CTRL_KEY;
-    private final String TOUCH_TO_MOUSE_HANDLER;
-
-    private final AccessibilityManager mA11yManager;
-
-    public TerminalView(Context context, AttributeSet attrs) {
-        super(context, attrs);
-
-        mA11yManager = context.getSystemService(AccessibilityManager.class);
-        mA11yManager.addTouchExplorationStateChangeListener(this);
-        mA11yManager.addAccessibilityStateChangeListener(this);
-        adjustToA11yStateChange();
-        try {
-            CTRL_KEY_HANDLER = readAssetAsString(context, "js/ctrl_key_handler.js");
-            ENABLE_CTRL_KEY = readAssetAsString(context, "js/enable_ctrl_key.js");
-            TOUCH_TO_MOUSE_HANDLER = readAssetAsString(context, "js/touch_to_mouse_handler.js");
-        } catch (IOException e) {
-            // It cannot happen
-            throw new IllegalArgumentException("cannot read code from asset", e);
-        }
-    }
-
-    private String readAssetAsString(Context context, String filePath) throws IOException {
-        try (InputStream is = context.getAssets().open(filePath)) {
-            return new String(is.readAllBytes());
-        }
-    }
-
-    public void mapTouchToMouseEvent() {
-        this.evaluateJavascript(TOUCH_TO_MOUSE_HANDLER, null);
-    }
-
-    public void mapCtrlKey() {
-        this.evaluateJavascript(CTRL_KEY_HANDLER, null);
-    }
-
-    public void enableCtrlKey() {
-        this.evaluateJavascript(ENABLE_CTRL_KEY, null);
-    }
-
-    @Override
-    public void onAccessibilityStateChanged(boolean enabled) {
-        Log.d(TAG, "accessibility " + enabled);
-        adjustToA11yStateChange();
-    }
-
-    @Override
-    public void onTouchExplorationStateChanged(boolean enabled) {
-        Log.d(TAG, "touch exploration " + enabled);
-        adjustToA11yStateChange();
-    }
-
-    private void adjustToA11yStateChange() {
-        if (!mA11yManager.isEnabled()) {
-            setFocusable(true);
-            return;
-        }
-
-        // When accessibility is on, the webview itself doesn't have to be focusable. The (virtual)
-        // edittext will be focusable to accept inputs. However, the webview has to be focusable for
-        // an accessibility purpose so that users can read the contents in it or scroll the view.
-        setFocusable(false);
-        setFocusableInTouchMode(true);
-    }
-
-    // AccessibilityEvents for WebView are sent directly from WebContentsAccessibilityImpl to the
-    // parent of WebView, without going through WebView. So, there's no WebView methods we can
-    // override to intercept the event handling process. To work around this, we attach an
-    // AccessibilityDelegate to the parent view where the events are sent to. And to guarantee that
-    // the parent view exists, wait until the WebView is attached to the window by when the parent
-    // must exist.
-    private final AccessibilityDelegate mA11yEventFilter =
-            new AccessibilityDelegate() {
-                @Override
-                public boolean onRequestSendAccessibilityEvent(
-                        ViewGroup host, View child, AccessibilityEvent e) {
-                    // We filter only the a11y events from the WebView
-                    if (child != TerminalView.this) {
-                        return super.onRequestSendAccessibilityEvent(host, child, e);
-                    }
-                    final int eventType = e.getEventType();
-                    switch (e.getEventType()) {
-                            // Skip reading texts that are too long. Right now, ttyd emits entire
-                            // text on the terminal to the live region, which is very annoying to
-                            // screen reader users.
-                        case AccessibilityEvent.TYPE_ANNOUNCEMENT:
-                            CharSequence text = e.getText().get(0); // there always is a text
-                            if (text.length() >= TEXT_TOO_LONG_TO_ANNOUNCE) {
-                                Log.i(TAG, "Announcement skipped because it's too long: " + text);
-                                return false;
-                            }
-                            break;
-                    }
-                    return super.onRequestSendAccessibilityEvent(host, child, e);
-                }
-            };
-
-    @Override
-    protected void onAttachedToWindow() {
-        super.onAttachedToWindow();
-        if (mA11yManager.isEnabled()) {
-            View parent = (View) getParent();
-            parent.setAccessibilityDelegate(mA11yEventFilter);
-        }
-    }
-
-    private final AccessibilityNodeProvider mA11yNodeProvider =
-            new AccessibilityNodeProvider() {
-
-                /** Returns the original NodeProvider that WebView implements. */
-                private AccessibilityNodeProvider getParent() {
-                    return TerminalView.super.getAccessibilityNodeProvider();
-                }
-
-                /** Convenience method for reading a string resource. */
-                private String getString(int resId) {
-                    return TerminalView.this.getContext().getResources().getString(resId);
-                }
-
-                /** Checks if NodeInfo renders an empty line in the terminal. */
-                private boolean isEmptyLine(AccessibilityNodeInfo info) {
-                    final CharSequence text = info.getText();
-                    // Node with no text is not consiered a line. ttyd emits at least one character,
-                    // which usually is NBSP.
-                    if (text == null) {
-                        return false;
-                    }
-                    for (int i = 0; i < text.length(); i++) {
-                        char c = text.charAt(i);
-                        // Note: don't use Characters.isWhitespace as it doesn't recognize NBSP as a
-                        // whitespace.
-                        if (!TextUtils.isWhitespace(c)) {
-                            return false;
-                        }
-                    }
-                    return true;
-                }
-
-                @Override
-                public AccessibilityNodeInfo createAccessibilityNodeInfo(int id) {
-                    AccessibilityNodeInfo info = getParent().createAccessibilityNodeInfo(id);
-                    if (info == null) {
-                        return null;
-                    }
-
-                    final String className = info.getClassName().toString();
-
-                    // By default all views except the cursor is not click-able. Other views are
-                    // read-only. This ensures that user is not navigated to non-clickable elements
-                    // when using switches.
-                    if (!"android.widget.EditText".equals(className)) {
-                        info.removeAction(AccessibilityAction.ACTION_CLICK);
-                    }
-
-                    switch (className) {
-                        case "android.webkit.WebView":
-                            // There are two NodeInfo objects of class name WebView. The one is the
-                            // real WebView whose ID is View.NO_ID as it's at the root of the
-                            // virtual view hierarchy. The second one is a virtual view for the
-                            // iframe. The latter one's text is set to the command that we give to
-                            // ttyd, which is "login -f droid ...". This is an impl detail which
-                            // doesn't have to be announced.  Replace the text with "Terminal
-                            // display".
-                            if (id != View.NO_ID) {
-                                info.setText(null);
-                                info.setContentDescription(getString(R.string.terminal_display));
-                                // b/376827536
-                                info.setHintText(getString(R.string.double_tap_to_edit_text));
-                            }
-
-                            // These two lines below are to prevent this WebView element from being
-                            // fousable by the screen reader, while allowing any other element in
-                            // the WebView to be focusable by the reader. In our case, the EditText
-                            // is a117_focusable.
-                            info.setScreenReaderFocusable(false);
-                            info.addAction(AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS);
-                            break;
-                        case "android.view.View":
-                            // Empty line was announced as "space" (via the NBSP character).
-                            // Localize the spoken text.
-                            if (isEmptyLine(info)) {
-                                info.setContentDescription(getString(R.string.empty_line));
-                                // b/376827536
-                                info.setHintText(getString(R.string.double_tap_to_edit_text));
-                            }
-                            break;
-                        case "android.widget.TextView":
-                            // There are several TextViews in the terminal, and one of them is an
-                            // invisible TextView which seems to be from the <div
-                            // class="live-region"> tag. Interestingly, its text is often populated
-                            // with the entire text on the screen. Silence this by forcibly setting
-                            // the text to null. Note that this TextView is identified by having a
-                            // zero width. This certainly is not elegant, but I couldn't find other
-                            // options.
-                            Rect rect = new Rect();
-                            info.getBoundsInScreen(rect);
-                            if (rect.width() == 0) {
-                                info.setText(null);
-                                info.setContentDescription(getString(R.string.empty_line));
-                            }
-                            info.setScreenReaderFocusable(false);
-                            break;
-                        case "android.widget.EditText":
-                            // This EditText is for the <textarea> accepting user input; the cursor.
-                            // ttyd name it as "Terminal input" but it's not i18n'ed. Override it
-                            // here for better i18n.
-                            info.setText(null);
-                            info.setHintText(getString(R.string.double_tap_to_edit_text));
-                            info.setContentDescription(getString(R.string.terminal_input));
-                            info.setScreenReaderFocusable(true);
-                            info.addAction(AccessibilityAction.ACTION_FOCUS);
-                            break;
-                    }
-                    return info;
-                }
-
-                @Override
-                public boolean performAction(int id, int action, Bundle arguments) {
-                    return getParent().performAction(id, action, arguments);
-                }
-
-                @Override
-                public void addExtraDataToAccessibilityNodeInfo(
-                        int virtualViewId,
-                        AccessibilityNodeInfo info,
-                        String extraDataKey,
-                        Bundle arguments) {
-                    getParent()
-                            .addExtraDataToAccessibilityNodeInfo(
-                                    virtualViewId, info, extraDataKey, arguments);
-                }
-
-                @Override
-                public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(
-                        String text, int virtualViewId) {
-                    return getParent().findAccessibilityNodeInfosByText(text, virtualViewId);
-                }
-
-                @Override
-                public AccessibilityNodeInfo findFocus(int focus) {
-                    return getParent().findFocus(focus);
-                }
-            };
-
-    @Override
-    public AccessibilityNodeProvider getAccessibilityNodeProvider() {
-        AccessibilityNodeProvider p = super.getAccessibilityNodeProvider();
-        if (p != null && mA11yManager.isEnabled()) {
-            return mA11yNodeProvider;
-        }
-        return p;
-    }
-
-    @Override
-    public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
-        InputConnection inputConnection = super.onCreateInputConnection(outAttrs);
-        if (outAttrs != null) {
-            outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
-        }
-        return inputConnection;
-    }
-}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/TerminalView.kt b/android/TerminalApp/java/com/android/virtualization/terminal/TerminalView.kt
new file mode 100644
index 0000000..18a39fa
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/TerminalView.kt
@@ -0,0 +1,283 @@
+/*
+ * 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.graphics.Rect
+import android.os.Bundle
+import android.text.InputType
+import android.text.TextUtils
+import android.util.AttributeSet
+import android.util.Log
+import android.view.View
+import android.view.ViewGroup
+import android.view.accessibility.AccessibilityEvent
+import android.view.accessibility.AccessibilityManager
+import android.view.accessibility.AccessibilityNodeInfo
+import android.view.accessibility.AccessibilityNodeProvider
+import android.view.inputmethod.EditorInfo
+import android.view.inputmethod.InputConnection
+import android.webkit.WebView
+import com.android.virtualization.terminal.MainActivity.TAG
+import java.io.IOException
+
+class TerminalView(context: Context, attrs: AttributeSet?) :
+    WebView(context, attrs),
+    AccessibilityManager.AccessibilityStateChangeListener,
+    AccessibilityManager.TouchExplorationStateChangeListener {
+    private val ctrlKeyHandler: String = readAssetAsString(context, "js/ctrl_key_handler.js")
+    private val enableCtrlKey: String = readAssetAsString(context, "js/enable_ctrl_key.js")
+    private val touchToMouseHandler: String =
+        readAssetAsString(context, "js/touch_to_mouse_handler.js")
+    private val a11yManager =
+        context.getSystemService<AccessibilityManager>(AccessibilityManager::class.java).also {
+            it.addTouchExplorationStateChangeListener(this)
+            it.addAccessibilityStateChangeListener(this)
+        }
+
+    @Throws(IOException::class)
+    private fun readAssetAsString(context: Context, filePath: String): String {
+        return String(context.assets.open(filePath).readAllBytes())
+    }
+
+    fun mapTouchToMouseEvent() {
+        this.evaluateJavascript(touchToMouseHandler, null)
+    }
+
+    fun mapCtrlKey() {
+        this.evaluateJavascript(ctrlKeyHandler, null)
+    }
+
+    fun enableCtrlKey() {
+        this.evaluateJavascript(enableCtrlKey, null)
+    }
+
+    override fun onAccessibilityStateChanged(enabled: Boolean) {
+        Log.d(TAG, "accessibility $enabled")
+        adjustToA11yStateChange()
+    }
+
+    override fun onTouchExplorationStateChanged(enabled: Boolean) {
+        Log.d(TAG, "touch exploration $enabled")
+        adjustToA11yStateChange()
+    }
+
+    private fun adjustToA11yStateChange() {
+        if (!a11yManager.isEnabled) {
+            setFocusable(true)
+            return
+        }
+
+        // When accessibility is on, the webview itself doesn't have to be focusable. The (virtual)
+        // edittext will be focusable to accept inputs. However, the webview has to be focusable for
+        // an accessibility purpose so that users can read the contents in it or scroll the view.
+        setFocusable(false)
+        setFocusableInTouchMode(true)
+    }
+
+    // AccessibilityEvents for WebView are sent directly from WebContentsAccessibilityImpl to the
+    // parent of WebView, without going through WebView. So, there's no WebView methods we can
+    // override to intercept the event handling process. To work around this, we attach an
+    // AccessibilityDelegate to the parent view where the events are sent to. And to guarantee that
+    // the parent view exists, wait until the WebView is attached to the window by when the parent
+    // must exist.
+    private val mA11yEventFilter: AccessibilityDelegate =
+        object : AccessibilityDelegate() {
+            override fun onRequestSendAccessibilityEvent(
+                host: ViewGroup,
+                child: View,
+                e: AccessibilityEvent,
+            ): Boolean {
+                // We filter only the a11y events from the WebView
+                if (child !== this@TerminalView) {
+                    return super.onRequestSendAccessibilityEvent(host, child, e)
+                }
+                when (e.eventType) {
+                    AccessibilityEvent.TYPE_ANNOUNCEMENT -> {
+                        val text = e.text[0] // there always is a text
+                        if (text.length >= TEXT_TOO_LONG_TO_ANNOUNCE) {
+                            Log.i(TAG, "Announcement skipped because it's too long: $text")
+                            return false
+                        }
+                    }
+                }
+                return super.onRequestSendAccessibilityEvent(host, child, e)
+            }
+        }
+
+    override fun onAttachedToWindow() {
+        super.onAttachedToWindow()
+        if (a11yManager.isEnabled) {
+            val parent = getParent() as View
+            parent.setAccessibilityDelegate(mA11yEventFilter)
+        }
+    }
+
+    private val mA11yNodeProvider: AccessibilityNodeProvider =
+        object : AccessibilityNodeProvider() {
+            /** Returns the original NodeProvider that WebView implements. */
+            private fun getParent(): AccessibilityNodeProvider? {
+                return super@TerminalView.getAccessibilityNodeProvider()
+            }
+
+            /** Convenience method for reading a string resource. */
+            private fun getString(resId: Int): String {
+                return this@TerminalView.context.getResources().getString(resId)
+            }
+
+            /** Checks if NodeInfo renders an empty line in the terminal. */
+            private fun isEmptyLine(info: AccessibilityNodeInfo): Boolean {
+                // Node with no text is not considered a line. ttyd emits at least one character,
+                // which usually is NBSP.
+                // Note: don't use Characters.isWhitespace as it doesn't recognize NBSP as a
+                // whitespace.
+                return (info.getText()?.all { TextUtils.isWhitespace(it.code) }) == true
+            }
+
+            override fun createAccessibilityNodeInfo(id: Int): AccessibilityNodeInfo? {
+                val info: AccessibilityNodeInfo? = getParent()?.createAccessibilityNodeInfo(id)
+                if (info == null) {
+                    return null
+                }
+
+                val className = info.className.toString()
+
+                // By default all views except the cursor is not click-able. Other views are
+                // read-only. This ensures that user is not navigated to non-clickable elements
+                // when using switches.
+                if ("android.widget.EditText" != className) {
+                    info.removeAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK)
+                }
+
+                when (className) {
+                    "android.webkit.WebView" -> {
+                        // There are two NodeInfo objects of class name WebView. The one is the
+                        // real WebView whose ID is View.NO_ID as it's at the root of the
+                        // virtual view hierarchy. The second one is a virtual view for the
+                        // iframe. The latter one's text is set to the command that we give to
+                        // ttyd, which is "login -f droid ...". This is an impl detail which
+                        // doesn't have to be announced.  Replace the text with "Terminal
+                        // display".
+                        if (id != NO_ID) {
+                            info.setText(null)
+                            info.setContentDescription(getString(R.string.terminal_display))
+                            // b/376827536
+                            info.setHintText(getString(R.string.double_tap_to_edit_text))
+                        }
+
+                        // These two lines below are to prevent this WebView element from being
+                        // focusable by the screen reader, while allowing any other element in
+                        // the WebView to be focusable by the reader. In our case, the EditText
+                        // is a117_focusable.
+                        info.isScreenReaderFocusable = false
+                        info.addAction(
+                            AccessibilityNodeInfo.AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS
+                        )
+                    }
+
+                    "android.view.View" ->
+                        // Empty line was announced as "space" (via the NBSP character).
+                        // Localize the spoken text.
+                        if (isEmptyLine(info)) {
+                            info.setContentDescription(getString(R.string.empty_line))
+                            // b/376827536
+                            info.setHintText(getString(R.string.double_tap_to_edit_text))
+                        }
+
+                    "android.widget.TextView" -> {
+                        // There are several TextViews in the terminal, and one of them is an
+                        // invisible TextView which seems to be from the <div
+                        // class="live-region"> tag. Interestingly, its text is often populated
+                        // with the entire text on the screen. Silence this by forcibly setting
+                        // the text to null. Note that this TextView is identified by having a
+                        // zero width. This certainly is not elegant, but I couldn't find other
+                        // options.
+                        val rect = Rect()
+                        info.getBoundsInScreen(rect)
+                        if (rect.width() == 0) {
+                            info.setText(null)
+                            info.setContentDescription(getString(R.string.empty_line))
+                        }
+                        info.isScreenReaderFocusable = false
+                    }
+
+                    "android.widget.EditText" -> {
+                        // This EditText is for the <textarea> accepting user input; the cursor.
+                        // ttyd name it as "Terminal input" but it's not i18n'ed. Override it
+                        // here for better i18n.
+                        info.setText(null)
+                        info.setHintText(getString(R.string.double_tap_to_edit_text))
+                        info.setContentDescription(getString(R.string.terminal_input))
+                        info.isScreenReaderFocusable = true
+                        info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_FOCUS)
+                    }
+                }
+                return info
+            }
+
+            override fun performAction(id: Int, action: Int, arguments: Bundle?): Boolean {
+                return getParent()?.performAction(id, action, arguments) == true
+            }
+
+            override fun addExtraDataToAccessibilityNodeInfo(
+                virtualViewId: Int,
+                info: AccessibilityNodeInfo?,
+                extraDataKey: String?,
+                arguments: Bundle?,
+            ) {
+                getParent()
+                    ?.addExtraDataToAccessibilityNodeInfo(
+                        virtualViewId,
+                        info,
+                        extraDataKey,
+                        arguments,
+                    )
+            }
+
+            override fun findAccessibilityNodeInfosByText(
+                text: String?,
+                virtualViewId: Int,
+            ): MutableList<AccessibilityNodeInfo?>? {
+                return getParent()?.findAccessibilityNodeInfosByText(text, virtualViewId)
+            }
+
+            override fun findFocus(focus: Int): AccessibilityNodeInfo? {
+                return getParent()?.findFocus(focus)
+            }
+        }
+
+    override fun getAccessibilityNodeProvider(): AccessibilityNodeProvider? {
+        val p = super.getAccessibilityNodeProvider()
+        if (p != null && a11yManager.isEnabled) {
+            return mA11yNodeProvider
+        }
+        return p
+    }
+
+    override fun onCreateInputConnection(outAttrs: EditorInfo?): InputConnection? {
+        val inputConnection = super.onCreateInputConnection(outAttrs)
+        if (outAttrs != null) {
+            outAttrs.inputType = outAttrs.inputType or InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
+        }
+        return inputConnection
+    }
+
+    companion object {
+        // Maximum length of texts the talk back announcements can be. This value is somewhat
+        // arbitrarily set. We may want to adjust this in the future.
+        private const val TEXT_TOO_LONG_TO_ANNOUNCE = 200
+    }
+}
diff --git a/android/virtmgr/src/crosvm.rs b/android/virtmgr/src/crosvm.rs
index a90c1ff..096d3b5 100644
--- a/android/virtmgr/src/crosvm.rs
+++ b/android/virtmgr/src/crosvm.rs
@@ -324,7 +324,7 @@
             let tap =
                 if let Some(tap_file) = &config.tap { Some(tap_file.try_clone()?) } else { None };
 
-            run_virtiofs(&config)?;
+            let vhost_fs_devices = run_virtiofs(&config)?;
 
             // If this fails and returns an error, `self` will be left in the `Failed` state.
             let child =
@@ -339,7 +339,13 @@
             let child_clone = child.clone();
             let instance_clone = instance.clone();
             let monitor_vm_exit_thread = Some(thread::spawn(move || {
-                instance_clone.monitor_vm_exit(child_clone, failure_pipe_read, vfio_devices, tap);
+                instance_clone.monitor_vm_exit(
+                    child_clone,
+                    failure_pipe_read,
+                    vfio_devices,
+                    tap,
+                    vhost_fs_devices,
+                );
             }));
 
             if detect_hangup {
@@ -486,6 +492,7 @@
         failure_pipe_read: File,
         vfio_devices: Vec<VfioDevice>,
         tap: Option<File>,
+        vhost_user_devices: Vec<SharedChild>,
     ) {
         let failure_reason_thread = std::thread::spawn(move || {
             // Read the pipe to see if any failure reason is written
@@ -513,6 +520,34 @@
             }
         }
 
+        // In crosvm, when vhost_user frontend is dead, vhost_user backend device will detect and
+        // exit. We can safely wait() for vhost user device after waiting crosvm main
+        // process.
+        for device in vhost_user_devices {
+            match device.wait() {
+                Ok(status) => {
+                    info!("Vhost user device({}) exited with status {}", device.id(), status);
+                    if !status.success() {
+                        if let Some(code) = status.code() {
+                            // vhost_user backend device exit with error code
+                            error!(
+                                "vhost user device({}) exited with error code: {}",
+                                device.id(),
+                                code
+                            );
+                        } else {
+                            // The spawned child process of vhost_user backend device is
+                            // killed by signal
+                            error!("vhost user device({}) killed by signal", device.id());
+                        }
+                    }
+                }
+                Err(e) => {
+                    error!("Error waiting for vhost user device({}) to die: {}", device.id(), e);
+                }
+            }
+        }
+
         let failure_reason = failure_reason_thread.join().expect("failure_reason_thread panic'd");
 
         let mut vm_state = self.vm_state.lock().unwrap();
@@ -915,7 +950,8 @@
     }
 }
 
-fn run_virtiofs(config: &CrosvmConfig) -> io::Result<()> {
+fn run_virtiofs(config: &CrosvmConfig) -> io::Result<Vec<SharedChild>> {
+    let mut devices: Vec<SharedChild> = Vec::new();
     for shared_path in &config.shared_paths {
         if shared_path.app_domain {
             continue;
@@ -947,9 +983,10 @@
 
         let result = SharedChild::spawn(&mut command)?;
         info!("Spawned virtiofs crosvm({})", result.id());
+        devices.push(result);
     }
 
-    Ok(())
+    Ok(devices)
 }
 
 /// Starts an instance of `crosvm` to manage a new VM.
diff --git a/build/debian/build.sh b/build/debian/build.sh
index 613f7d2..3f33ec8 100755
--- a/build/debian/build.sh
+++ b/build/debian/build.sh
@@ -307,9 +307,10 @@
 
 generate_output_package() {
 	fdisk -l "${raw_disk_image}"
-	root_partition_num=1
-	bios_partition_num=14
-	efi_partition_num=15
+	local vm_config="$(realpath $(dirname "$0"))/vm_config.json.${arch}"
+	local root_partition_num=1
+	local bios_partition_num=14
+	local efi_partition_num=15
 
 	pushd ${workdir} > /dev/null
 
@@ -373,7 +374,6 @@
 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
diff --git a/docs/getting_started.md b/docs/getting_started.md
index 0a7cca6..03657ed 100644
--- a/docs/getting_started.md
+++ b/docs/getting_started.md
@@ -9,7 +9,7 @@
 * aosp\_oriole (Pixel 6)
 * aosp\_raven (Pixel 6 Pro)
 * aosp\_felix (Pixel Fold)
-* aosp\_tangopro (Pixel Tablet)
+* aosp\_tangorpro (Pixel Tablet)
 * aosp\_cf\_x86\_64\_phone (Cuttlefish a.k.a. Cloud Android). Follow [this
   instruction](https://source.android.com/docs/setup/create/cuttlefish-use) to
   use.
diff --git a/guest/pvmfw/Android.bp b/guest/pvmfw/Android.bp
index 51f7802..793204d 100644
--- a/guest/pvmfw/Android.bp
+++ b/guest/pvmfw/Android.bp
@@ -384,6 +384,8 @@
         "-E",
         "-P",
         "-xassembler-with-cpp", // allow C preprocessor directives
+        // Suppress an error about the unused -c that precedes -S.
+        "-Wno-unused-command-line-argument",
     ],
     visibility: ["//visibility:private"],
 }
diff --git a/libs/cstr/rules.mk b/libs/cstr/rules.mk
new file mode 100644
index 0000000..2309c30
--- /dev/null
+++ b/libs/cstr/rules.mk
@@ -0,0 +1,28 @@
+# 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.
+#
+
+LOCAL_DIR := $(GET_LOCAL_DIR)
+
+MODULE := $(LOCAL_DIR)
+
+SRC_DIR := packages/modules/Virtualization/libs/cstr
+
+MODULE_SRCS := $(SRC_DIR)/src/lib.rs
+
+MODULE_CRATE_NAME := cstr
+
+MODULE_RUST_EDITION := 2021
+
+include make/library.mk
diff --git a/libs/libfdt/bindgen/rules.mk b/libs/libfdt/bindgen/rules.mk
new file mode 100644
index 0000000..130a317
--- /dev/null
+++ b/libs/libfdt/bindgen/rules.mk
@@ -0,0 +1,38 @@
+# 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.
+#
+
+LOCAL_DIR := $(GET_LOCAL_DIR)
+
+MODULE := $(LOCAL_DIR)
+
+MODULE_SRCS := $(LOCAL_DIR)/src/lib.rs
+
+MODULE_CRATE_NAME := libfdt_bindgen
+
+MODULE_DEPS += \
+	external/dtc/libfdt \
+
+MODULE_BINDGEN_ALLOW_FUNCTIONS := \
+	fdt_.* \
+
+MODULE_BINDGEN_ALLOW_VARS := \
+	FDT_.* \
+
+MODULE_BINDGEN_ALLOW_TYPES := \
+	fdt_.* \
+
+MODULE_BINDGEN_SRC_HEADER := $(LOCAL_DIR)/fdt.h
+
+include make/library.mk
diff --git a/libs/libfdt/bindgen/src/lib.rs b/libs/libfdt/bindgen/src/lib.rs
new file mode 100644
index 0000000..015132b
--- /dev/null
+++ b/libs/libfdt/bindgen/src/lib.rs
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+
+//! # Interface library for libfdt.
+
+#![no_std]
+#![allow(non_upper_case_globals)]
+#![allow(non_camel_case_types)]
+#![allow(non_snake_case)]
+
+include!(env!("BINDGEN_INC_FILE"));
diff --git a/libs/libfdt/rules.mk b/libs/libfdt/rules.mk
new file mode 100644
index 0000000..2b4e470
--- /dev/null
+++ b/libs/libfdt/rules.mk
@@ -0,0 +1,37 @@
+# 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.
+#
+
+LOCAL_DIR := $(GET_LOCAL_DIR)
+
+MODULE := $(LOCAL_DIR)
+
+SRC_DIR := packages/modules/Virtualization/libs/libfdt
+
+MODULE_SRCS := $(SRC_DIR)/src/lib.rs
+
+MODULE_CRATE_NAME := libfdt
+
+MODULE_RUST_EDITION := 2021
+
+MODULE_LIBRARY_DEPS += \
+	external/dtc/libfdt \
+	packages/modules/Virtualization/libs/cstr \
+	packages/modules/Virtualization/libs/libfdt/bindgen \
+	$(call FIND_CRATE,zerocopy) \
+	$(call FIND_CRATE,static_assertions) \
+
+MODULE_RUST_USE_CLIPPY := true
+
+include make/library.mk
diff --git a/tests/vts/Android.bp b/tests/vts/Android.bp
index 35fbcdc..c8e2523 100644
--- a/tests/vts/Android.bp
+++ b/tests/vts/Android.bp
@@ -20,6 +20,7 @@
         "libavf_bindgen",
         "libciborium",
         "liblog_rust",
+        "libhypervisor_props",
         "libscopeguard",
         "libservice_vm_comm",
         "libvsock",
diff --git a/tests/vts/src/vts_libavf_test.rs b/tests/vts/src/vts_libavf_test.rs
index ba38a2e..e30c175 100644
--- a/tests/vts/src/vts_libavf_test.rs
+++ b/tests/vts/src/vts_libavf_test.rs
@@ -177,10 +177,20 @@
 
 #[test]
 fn test_run_rialto_protected() -> Result<()> {
-    run_rialto(true /* protected_vm */)
+    if hypervisor_props::is_protected_vm_supported()? {
+        run_rialto(true /* protected_vm */)
+    } else {
+        info!("pVMs are not supported on device. skipping test");
+        Ok(())
+    }
 }
 
 #[test]
 fn test_run_rialto_non_protected() -> Result<()> {
-    run_rialto(false /* protected_vm */)
+    if hypervisor_props::is_vm_supported()? {
+        run_rialto(false /* protected_vm */)
+    } else {
+        info!("non-pVMs are not supported on device. skipping test");
+        Ok(())
+    }
 }