Retrieve terminal info in a more robust way
Previously, it was possible for onServiceUpdated is called "after" the
service is destroyed due to an early exit of the application. When that
happened, we got RejectedExecutionException because the executor service
given to the NsdManager is shutdown as part of the service destroyal.
This CL fixes that by having a dedicated executor service for NsdManager
and shut it down upon onServiceInfoCallbackUnregistered, which means
there will be no more callbacks.
This CL also does some pending refactorings;
* Retrival of the terminal service info (ip addr & port) is deduped
across MainActivity and VmLauncherService
* Uses CompletableFuture to abstract the retrival of the terminal
service info.
Bug: 401041913
Test: try to kill the app before the terminal is connected
Change-Id: Ie268601317f040c2da9d33d31b2b9d1f9e475b61
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(