Merge "Retrieve terminal info in a more robust way" into main
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.kt b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.kt
index f6eeff9..662fef5 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.kt
@@ -24,8 +24,6 @@
 import android.graphics.drawable.Icon
 import android.graphics.fonts.FontStyle
 import android.net.Uri
-import android.net.nsd.NsdManager
-import android.net.nsd.NsdServiceInfo
 import android.os.Build
 import android.os.Bundle
 import android.os.ConditionVariable
@@ -62,6 +60,7 @@
 import java.io.IOException
 import java.net.MalformedURLException
 import java.net.URL
+import java.util.concurrent.CompletableFuture
 import java.util.concurrent.ExecutorService
 import java.util.concurrent.Executors
 
@@ -78,12 +77,11 @@
     private lateinit var image: InstalledImage
     private lateinit var accessibilityManager: AccessibilityManager
     private lateinit var manageExternalStorageActivityResultLauncher: ActivityResultLauncher<Intent>
-    private var ipAddress: String? = null
-    private var port: Int? = null
     private lateinit var terminalViewModel: TerminalViewModel
     private lateinit var viewPager: ViewPager2
     private lateinit var tabLayout: TabLayout
     private lateinit var terminalTabAdapter: TerminalTabAdapter
+    private val terminalInfo = CompletableFuture<TerminalInfo>()
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
@@ -243,40 +241,12 @@
     }
 
     fun connectToTerminalService(terminalView: TerminalView) {
-        if (ipAddress != null && port != null) {
-            val url = getTerminalServiceUrl(ipAddress, port!!)
-            terminalView.loadUrl(url.toString())
-            return
-        }
-        // TODO: refactor this block as a method
-        val nsdManager = getSystemService<NsdManager>(NsdManager::class.java)
-        val info = NsdServiceInfo()
-        info.serviceType = "_http._tcp"
-        info.serviceName = "ttyd"
-        nsdManager.registerServiceInfoCallback(
-            info,
-            executorService,
-            object : NsdManager.ServiceInfoCallback {
-                var loaded: Boolean = false
-
-                override fun onServiceInfoCallbackRegistrationFailed(errorCode: Int) {}
-
-                override fun onServiceInfoCallbackUnregistered() {}
-
-                override fun onServiceLost() {}
-
-                override fun onServiceUpdated(info: NsdServiceInfo) {
-                    Log.i(TAG, "Service found: $info")
-                    if (!loaded) {
-                        ipAddress = info.hostAddresses[0].hostAddress
-                        port = info.port
-                        val url = getTerminalServiceUrl(ipAddress, port!!)
-                        loaded = true
-                        nsdManager.unregisterServiceInfoCallback(this)
-                        runOnUiThread(Runnable { terminalView.loadUrl(url.toString()) })
-                    }
-                }
+        terminalInfo.thenAcceptAsync(
+            { info ->
+                val url = getTerminalServiceUrl(info.ipAddress, info.port)
+                runOnUiThread({ terminalView.loadUrl(url.toString()) })
             },
+            executorService,
         )
     }
 
@@ -292,6 +262,10 @@
         Log.i(TAG, "onVmStart()")
     }
 
+    override fun onTerminalAvailable(info: TerminalInfo) {
+        terminalInfo.complete(info)
+    }
+
     override fun onVmStop() {
         Log.i(TAG, "onVmStop()")
         finish()
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
index 5f3fd49..345e8dd 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
@@ -60,6 +60,7 @@
 import java.net.InetSocketAddress
 import java.net.SocketAddress
 import java.nio.file.Files
+import java.util.concurrent.CompletableFuture
 import java.util.concurrent.ExecutorService
 import java.util.concurrent.Executors
 
@@ -117,6 +118,8 @@
     interface VmLauncherServiceCallback {
         fun onVmStart()
 
+        fun onTerminalAvailable(info: TerminalInfo)
+
         fun onVmStop()
 
         fun onVmError()
@@ -230,35 +233,56 @@
 
         portNotifier = PortNotifier(this)
 
-        // TODO: dedup this part
+        getTerminalServiceInfo()
+            .thenAcceptAsync(
+                { info ->
+                    val ipAddress = info.hostAddresses[0].hostAddress
+                    val port = info.port
+                    val bundle = Bundle()
+                    bundle.putString(KEY_TERMINAL_IPADDRESS, ipAddress)
+                    bundle.putInt(KEY_TERMINAL_PORT, port)
+                    resultReceiver!!.send(RESULT_TERMINAL_AVAIL, bundle)
+                    startDebianServer(ipAddress)
+                },
+                executorService,
+            )
+
+        return START_NOT_STICKY
+    }
+
+    private fun getTerminalServiceInfo(): CompletableFuture<NsdServiceInfo> {
+        val executor = Executors.newSingleThreadExecutor(TerminalThreadFactory(applicationContext))
         val nsdManager = getSystemService<NsdManager?>(NsdManager::class.java)
-        val info = NsdServiceInfo()
-        info.serviceType = "_http._tcp"
-        info.serviceName = "ttyd"
+        val queryInfo = NsdServiceInfo()
+        queryInfo.serviceType = "_http._tcp"
+        queryInfo.serviceName = "ttyd"
+        var resolvedInfo = CompletableFuture<NsdServiceInfo>()
+
         nsdManager.registerServiceInfoCallback(
-            info,
-            executorService!!,
+            queryInfo,
+            executor,
             object : NsdManager.ServiceInfoCallback {
-                var started: Boolean = false
+                var found: Boolean = false
 
                 override fun onServiceInfoCallbackRegistrationFailed(errorCode: Int) {}
 
-                override fun onServiceInfoCallbackUnregistered() {}
+                override fun onServiceInfoCallbackUnregistered() {
+                    executor.shutdown()
+                }
 
                 override fun onServiceLost() {}
 
                 override fun onServiceUpdated(info: NsdServiceInfo) {
                     Log.i(TAG, "Service found: $info")
-                    if (!started) {
-                        started = true
+                    if (!found) {
+                        found = true
                         nsdManager.unregisterServiceInfoCallback(this)
-                        startDebianServer(info.hostAddresses[0].hostAddress)
+                        resolvedInfo.complete(info)
                     }
                 }
             },
         )
-
-        return START_NOT_STICKY
+        return resolvedInfo
     }
 
     private fun createNotificationForTerminalClose(): Notification {
@@ -438,6 +462,10 @@
         private const val RESULT_START = 0
         private const val RESULT_STOP = 1
         private const val RESULT_ERROR = 2
+        private const val RESULT_TERMINAL_AVAIL = 3
+
+        private const val KEY_TERMINAL_IPADDRESS = "address"
+        private const val KEY_TERMINAL_PORT = "port"
 
         private const val INITIAL_MEM_BALLOON_PERCENT = 10
         private const val MAX_MEM_BALLOON_PERCENT = 50
@@ -463,6 +491,11 @@
                         }
                         when (resultCode) {
                             RESULT_START -> callback.onVmStart()
+                            RESULT_TERMINAL_AVAIL -> {
+                                val ipAddress = resultData!!.getString(KEY_TERMINAL_IPADDRESS)
+                                val port = resultData!!.getInt(KEY_TERMINAL_PORT)
+                                callback.onTerminalAvailable(TerminalInfo(ipAddress!!, port))
+                            }
                             RESULT_STOP -> callback.onVmStop()
                             RESULT_ERROR -> callback.onVmError()
                         }
@@ -489,6 +522,8 @@
     }
 }
 
+data class TerminalInfo(val ipAddress: String, val port: Int)
+
 data class DisplayInfo(val width: Int, val height: Int, val dpi: Int, val refreshRate: Int) :
     Parcelable {
     constructor(