Merge "VirtualMachine: Ignore RemoteException when death of VM isn't an issue" into main
diff --git a/android/TerminalApp/assets/js/terminal_disconnect.js b/android/TerminalApp/assets/js/terminal_disconnect.js
new file mode 100644
index 0000000..1c89a13
--- /dev/null
+++ b/android/TerminalApp/assets/js/terminal_disconnect.js
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2025 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.
+ */
+
+(function() {
+  var originalLog = console.log;
+  console.log = function() {
+    console.log.history = console.log.history || [];
+    console.log.history.push(arguments);
+    originalLog.apply(console, arguments);
+    if (typeof arguments[0] === 'string' && arguments[0].startsWith("[ttyd] websocket connection closed with code: ")) {
+        TerminalApp.closeTab()
+    }
+  };
+})();
\ No newline at end of file
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/Logger.kt b/android/TerminalApp/java/com/android/virtualization/terminal/Logger.kt
index ba03716..088744e 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/Logger.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/Logger.kt
@@ -17,9 +17,7 @@
 
 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
@@ -56,19 +54,29 @@
             val logPath = dir.resolve(LocalDateTime.now().toString() + ".txt")
             val console = vm.getConsoleOutput()
             val file = Files.newOutputStream(logPath, StandardOpenOption.CREATE)
-            executor.submit<Int?> {
-                console.use { console ->
-                    LineBufferedOutputStream(file).use { fileOutput ->
-                        Streams.copy(console, fileOutput)
+            executor.execute({
+                try {
+                    console.use { console ->
+                        LineBufferedOutputStream(file).use { fileOutput ->
+                            Streams.copy(console, fileOutput)
+                        }
                     }
+                } catch (e: Exception) {
+                    Log.w(tag, "Failed to log console output. VM may be shutting down", e)
                 }
-            }
+            })
 
             val log = vm.getLogOutput()
-            executor.submit<Unit> { log.use { writeToLogd(it, tag) } }
-        } catch (e: VirtualMachineException) {
-            throw RuntimeException(e)
-        } catch (e: IOException) {
+            executor.execute({
+                log.use {
+                    try {
+                        writeToLogd(it, tag)
+                    } catch (e: Exception) {
+                        Log.w(tag, "Failed to log VM log output. VM may be shutting down", e)
+                    }
+                }
+            })
+        } catch (e: Exception) {
             throw RuntimeException(e)
         }
     }
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.kt b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.kt
index 8d74bad..33522c0 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.kt
@@ -185,20 +185,20 @@
         terminalViewModel.terminalTabs[tabId] = tab
         tab.customView!!
             .findViewById<Button>(R.id.tab_close_button)
-            .setOnClickListener(
-                View.OnClickListener { _: View? ->
-                    if (terminalTabAdapter.tabs.size == 1) {
-                        finishAndRemoveTask()
-                    }
-                    viewPager.offscreenPageLimit -= 1
-                    terminalTabAdapter.deleteTab(tab.position)
-                    tabLayout.removeTab(tab)
-                }
-            )
+            .setOnClickListener(View.OnClickListener { _: View? -> closeTab(tab) })
         // Add and select the tab
         tabLayout.addTab(tab, true)
     }
 
+    fun closeTab(tab: TabLayout.Tab) {
+        if (terminalTabAdapter.tabs.size == 1) {
+            finishAndRemoveTask()
+        }
+        viewPager.offscreenPageLimit -= 1
+        terminalTabAdapter.deleteTab(tab.position)
+        tabLayout.removeTab(tab)
+    }
+
     private fun lockOrientationIfNecessary() {
         val hasHwQwertyKeyboard = resources.configuration.keyboard == Configuration.KEYBOARD_QWERTY
         if (hasHwQwertyKeyboard) {
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/MemBalloonController.kt b/android/TerminalApp/java/com/android/virtualization/terminal/MemBalloonController.kt
index 7647d9b..2ed7217 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/MemBalloonController.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/MemBalloonController.kt
@@ -58,7 +58,7 @@
             // available memory to the virtual machine
             override fun onResume(owner: LifecycleOwner) {
                 ongoingInflation?.cancel(false)
-                executor.submit({
+                executor.execute({
                     Log.v(TAG, "app resumed. deflating mem balloon to the minimum")
                     vm.setMemoryBalloonByPercent(0)
                 })
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsRecoveryActivity.kt b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsRecoveryActivity.kt
index 319a53b..654cb57 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsRecoveryActivity.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsRecoveryActivity.kt
@@ -21,19 +21,23 @@
 import android.view.View
 import androidx.appcompat.app.AppCompatActivity
 import androidx.core.view.isVisible
-import androidx.lifecycle.lifecycleScope
 import com.android.virtualization.terminal.MainActivity.Companion.TAG
 import com.google.android.material.card.MaterialCardView
 import com.google.android.material.dialog.MaterialAlertDialogBuilder
 import com.google.android.material.snackbar.Snackbar
 import java.io.IOException
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
 
 class SettingsRecoveryActivity : AppCompatActivity() {
+    private lateinit var executorService: ExecutorService
+
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
+
+        executorService =
+            Executors.newSingleThreadExecutor(TerminalThreadFactory(applicationContext))
+
         setContentView(R.layout.settings_recovery)
 
         val resetCard = findViewById<MaterialCardView>(R.id.settings_recovery_reset_card)
@@ -81,6 +85,12 @@
         }
     }
 
+    override fun onDestroy() {
+        super.onDestroy()
+
+        executorService.shutdown()
+    }
+
     private fun removeBackup(): Unit {
         try {
             InstalledImage.getDefault(this).deleteBackup()
@@ -116,27 +126,22 @@
         }
     }
 
-    private fun runInBackgroundAndRestartApp(
-        backgroundWork: suspend CoroutineScope.() -> Unit
-    ): Unit {
+    private fun runInBackgroundAndRestartApp(backgroundWork: Runnable) {
         findViewById<View>(R.id.setting_recovery_card_container).visibility = View.INVISIBLE
         findViewById<View>(R.id.recovery_boot_progress).visibility = View.VISIBLE
-        lifecycleScope
-            .launch(Dispatchers.IO) { backgroundWork() }
-            .invokeOnCompletion {
-                runOnUiThread {
-                    findViewById<View>(R.id.setting_recovery_card_container).visibility =
-                        View.VISIBLE
-                    findViewById<View>(R.id.recovery_boot_progress).visibility = View.INVISIBLE
-                    // Restart terminal
-                    val intent =
-                        baseContext.packageManager.getLaunchIntentForPackage(
-                            baseContext.packageName
-                        )
-                    intent?.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
-                    finish()
-                    startActivity(intent)
-                }
+        executorService.execute({
+            backgroundWork.run()
+
+            runOnUiThread {
+                findViewById<View>(R.id.setting_recovery_card_container).visibility = View.VISIBLE
+                findViewById<View>(R.id.recovery_boot_progress).visibility = View.INVISIBLE
+                // Restart terminal
+                val intent =
+                    baseContext.packageManager.getLaunchIntentForPackage(baseContext.packageName)
+                intent?.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
+                finish()
+                startActivity(intent)
             }
+        })
     }
 }
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/TerminalTabFragment.kt b/android/TerminalApp/java/com/android/virtualization/terminal/TerminalTabFragment.kt
index a0c6e4e..72dea5c 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/TerminalTabFragment.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/TerminalTabFragment.kt
@@ -24,6 +24,7 @@
 import android.view.View
 import android.view.ViewGroup
 import android.webkit.ClientCertRequest
+import android.webkit.JavascriptInterface
 import android.webkit.SslErrorHandler
 import android.webkit.WebChromeClient
 import android.webkit.WebResourceError
@@ -92,6 +93,7 @@
 
         terminalView.webChromeClient = TerminalWebChromeClient()
         terminalView.webViewClient = TerminalWebViewClient()
+        terminalView.addJavascriptInterface(TerminalViewInterface(context!!), "TerminalApp")
 
         (activity as MainActivity).modifierKeysController.addTerminalView(terminalView)
         terminalViewModel.terminalViews.add(terminalView)
@@ -119,6 +121,18 @@
         }
     }
 
+    inner class TerminalViewInterface(private val mContext: android.content.Context) {
+        @JavascriptInterface
+        fun closeTab() {
+            if (activity != null) {
+                activity?.runOnUiThread {
+                    val mainActivity = (activity as MainActivity)
+                    mainActivity.closeTab(terminalViewModel.terminalTabs[id]!!)
+                }
+            }
+        }
+    }
+
     private inner class TerminalWebViewClient : WebViewClient() {
         private var loadFailed = false
         private var requestId: Long = 0
@@ -174,6 +188,7 @@
                             bootProgressView.visibility = View.GONE
                             terminalView.visibility = View.VISIBLE
                             terminalView.mapTouchToMouseEvent()
+                            terminalView.applyTerminalDisconnectCallback()
                             updateMainActivity()
                             updateFocus()
                         }
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/TerminalView.kt b/android/TerminalApp/java/com/android/virtualization/terminal/TerminalView.kt
index 4b11c1d..9c83d8b 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/TerminalView.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/TerminalView.kt
@@ -41,6 +41,8 @@
     private val ctrlKeyHandler: String = readAssetAsString(context, "js/ctrl_key_handler.js")
     private val enableCtrlKey: String = readAssetAsString(context, "js/enable_ctrl_key.js")
     private val disableCtrlKey: String = readAssetAsString(context, "js/disable_ctrl_key.js")
+    private val terminalDisconnectCallback: String =
+        readAssetAsString(context, "js/terminal_disconnect.js")
     private val touchToMouseHandler: String =
         readAssetAsString(context, "js/touch_to_mouse_handler.js")
     private val a11yManager =
@@ -70,6 +72,10 @@
         this.evaluateJavascript(disableCtrlKey, null)
     }
 
+    fun applyTerminalDisconnectCallback() {
+        this.evaluateJavascript(terminalDisconnectCallback, null)
+    }
+
     override fun onAccessibilityStateChanged(enabled: Boolean) {
         Log.d(TAG, "accessibility $enabled")
         adjustToA11yStateChange()
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
index 84168e5..0b34a8d 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
@@ -94,7 +94,7 @@
 
     override fun onCreate() {
         super.onCreate()
-        val threadFactory = TerminalThreadFactory(getApplicationContext())
+        val threadFactory = TerminalThreadFactory(applicationContext)
         bgThreads = Executors.newCachedThreadPool(threadFactory)
         mainWorkerThread = Executors.newSingleThreadExecutor(threadFactory)
         image = InstalledImage.getDefault(this)
@@ -123,7 +123,7 @@
                 // done.
                 val diskSize = intent.getLongExtra(EXTRA_DISK_SIZE, image.getApparentSize())
 
-                mainWorkerThread.submit({
+                mainWorkerThread.execute({
                     doStart(notification, displayInfo, diskSize, resultReceiver)
                 })
 
@@ -131,7 +131,7 @@
                 // ForegroundServiceDidNotStartInTimeException
                 startForeground(this.hashCode(), notification)
             }
-            ACTION_SHUTDOWN_VM -> mainWorkerThread.submit({ doShutdown(resultReceiver) })
+            ACTION_SHUTDOWN_VM -> mainWorkerThread.execute({ doShutdown(resultReceiver) })
             else -> {
                 Log.e(TAG, "Unknown command " + intent.action)
                 stopSelf()
@@ -505,7 +505,7 @@
     }
 
     override fun onDestroy() {
-        mainWorkerThread.submit({
+        mainWorkerThread.execute({
             if (runner?.vm?.getStatus() == VirtualMachine.STATUS_RUNNING) {
                 doShutdown(null)
             }
diff --git a/build/apex/manifest.json b/build/apex/manifest.json
index e596ce1..9be2aa6 100644
--- a/build/apex/manifest.json
+++ b/build/apex/manifest.json
@@ -1,6 +1,6 @@
 {
   "name": "com.android.virt",
-  "version": 2,
+  "version": 3,
   "requireNativeLibs": [
     "libEGL.so",
     "libGLESv2.so",
diff --git a/build/compos/manifest.json b/build/compos/manifest.json
index 7a07b1b..6d1f816 100644
--- a/build/compos/manifest.json
+++ b/build/compos/manifest.json
@@ -1,4 +1,4 @@
 {
   "name": "com.android.compos",
-  "version": 2
+  "version": 3
 }
diff --git a/build/microdroid/Android.bp b/build/microdroid/Android.bp
index 10b492b..9152091 100644
--- a/build/microdroid/Android.bp
+++ b/build/microdroid/Android.bp
@@ -506,7 +506,7 @@
     },
 }
 
-MICRODROID_GKI_ROLLBACK_INDEX = 1
+MICRODROID_GKI_ROLLBACK_INDEX = 2
 
 flag_aware_avb_add_hash_footer_defaults {
     name: "microdroid_kernel_cap_defaults",
diff --git a/guest/pvmfw/avb/tests/utils.rs b/guest/pvmfw/avb/tests/utils.rs
index 38541c5..843cca9 100644
--- a/guest/pvmfw/avb/tests/utils.rs
+++ b/guest/pvmfw/avb/tests/utils.rs
@@ -133,7 +133,9 @@
         initrd_digest,
         public_key: &public_key,
         capabilities,
-        rollback_index: 1,
+        // TODO(b/392081737): Capture expected rollback_index from build variables as we
+        // intend on auto-syncing rollback_index with security patch timestamps
+        rollback_index: 2,
         page_size,
         name: None,
     };
diff --git a/guest/rialto/Android.bp b/guest/rialto/Android.bp
index 35ede7a..a49f11f 100644
--- a/guest/rialto/Android.bp
+++ b/guest/rialto/Android.bp
@@ -63,8 +63,8 @@
 
 // Both SERVICE_VM_VERSION and SERVICE_VM_VERSION_STRING should represent the
 // same version number for the service VM.
-SERVICE_VM_VERSION = 1
-SERVICE_VM_VERSION_STRING = "1"
+SERVICE_VM_VERSION = 2
+SERVICE_VM_VERSION_STRING = "2"
 
 genrule {
     name: "service_vm_version_rs",
diff --git a/tests/old_images_avf_test/src/main.rs b/tests/old_images_avf_test/src/main.rs
index 018a80e..b72c706 100644
--- a/tests/old_images_avf_test/src/main.rs
+++ b/tests/old_images_avf_test/src/main.rs
@@ -173,20 +173,6 @@
 }
 
 #[test]
-fn test_run_rialto_protected() -> Result<()> {
-    if hypervisor_props::is_protected_vm_supported()? {
-        run_vm(
-            "/data/local/tmp/rialto.bin", /* image_path */
-            c"test_rialto",               /* test_name */
-            true,                         /* protected_vm */
-        )
-    } else {
-        info!("pVMs are not supported on device. skipping test");
-        Ok(())
-    }
-}
-
-#[test]
 fn test_run_rialto_non_protected() -> Result<()> {
     if hypervisor_props::is_vm_supported()? {
         run_vm(
@@ -201,20 +187,6 @@
 }
 
 #[test]
-fn test_run_android16_rialto_protected() -> Result<()> {
-    if hypervisor_props::is_protected_vm_supported()? {
-        run_vm(
-            "/data/local/tmp/android16_rialto.bin", /* image_path */
-            c"android16_test_rialto",               /* test_name */
-            true,                                   /* protected_vm */
-        )
-    } else {
-        info!("pVMs are not supported on device. skipping test");
-        Ok(())
-    }
-}
-
-#[test]
 fn test_run_android16_rialto_non_protected() -> Result<()> {
     if hypervisor_props::is_vm_supported()? {
         run_vm(