Close tab when the ttyd session is closed

Test: Manually
Bug: 404153212
Change-Id: I085fedf12618ee4cd3af2f755a1e89ac1f7dc7ec
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/MainActivity.kt b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.kt
index 52afef4..b311dca 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.kt
@@ -179,20 +179,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/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()