fix(android): stabilize gateway operator reconnect

This commit is contained in:
Ayaan Zaidi
2026-02-25 16:25:02 +05:30
committed by Ayaan Zaidi
parent 3607b733cb
commit 90ddb3f271
4 changed files with 31 additions and 43 deletions

View File

@@ -144,10 +144,6 @@ class MainViewModel(app: Application) : AndroidViewModel(app) {
runtime.setTalkEnabled(enabled)
}
fun logGatewayDebugSnapshot(source: String = "manual") {
runtime.logGatewayDebugSnapshot(source)
}
fun refreshGatewayConnection() {
runtime.refreshGatewayConnection()
}

View File

@@ -328,13 +328,20 @@ class NodeRuntime(context: Context) {
private fun updateStatus() {
_isConnected.value = operatorConnected
val operator = operatorStatusText.trim()
val node = nodeStatusText.trim()
_statusText.value =
when {
operatorConnected && _nodeConnected.value -> "Connected"
operatorConnected && !_nodeConnected.value -> "Connected (node offline)"
!operatorConnected && _nodeConnected.value -> "Connected (operator offline)"
operatorStatusText.isNotBlank() && operatorStatusText != "Offline" -> operatorStatusText
else -> nodeStatusText
!operatorConnected && _nodeConnected.value ->
if (operator.isNotEmpty() && operator != "Offline") {
"Connected (operator: $operator)"
} else {
"Connected (operator offline)"
}
operator.isNotBlank() && operator != "Offline" -> operator
else -> node
}
}
@@ -614,17 +621,14 @@ class NodeRuntime(context: Context) {
prefs.setTalkEnabled(value)
}
fun logGatewayDebugSnapshot(source: String = "manual") {
val flowToken = gatewayToken.value.trim()
val loadedToken = prefs.loadGatewayToken().orEmpty()
Log.i(
"OpenClawGatewayDebug",
"source=$source manualEnabled=${manualEnabled.value} host=${manualHost.value} port=${manualPort.value} tls=${manualTls.value} flowTokenLen=${flowToken.length} loadTokenLen=${loadedToken.length} connected=${isConnected.value} status=${statusText.value}",
)
}
fun refreshGatewayConnection() {
val endpoint = connectedEndpoint ?: return
val endpoint =
connectedEndpoint ?: run {
_statusText.value = "Failed: no cached gateway endpoint"
return
}
operatorStatusText = "Connecting…"
updateStatus()
val token = prefs.loadGatewayToken()
val password = prefs.loadGatewayPassword()
val tls = connectionManager.resolveTlsParams(endpoint)

View File

@@ -62,6 +62,11 @@ class GatewaySession(
private val onInvoke: (suspend (InvokeRequest) -> InvokeResult)? = null,
private val onTlsFingerprint: ((stableId: String, fingerprint: String) -> Unit)? = null,
) {
private companion object {
// Keep connect timeout above observed gateway unauthorized close on lower-end devices.
private const val CONNECT_RPC_TIMEOUT_MS = 12_000L
}
data class InvokeRequest(
val id: String,
val nodeId: String,
@@ -302,26 +307,13 @@ class GatewaySession(
val identity = identityStore.loadOrCreate()
val storedToken = deviceAuthStore.loadToken(identity.deviceId, options.role)
val trimmedToken = token?.trim().orEmpty()
val authToken = if (storedToken.isNullOrBlank()) trimmedToken else storedToken
// QR/setup/manual shared token must take precedence; stale role tokens can survive re-onboarding.
val authToken = if (trimmedToken.isNotBlank()) trimmedToken else storedToken.orEmpty()
val payload = buildConnectParams(identity, connectNonce, authToken, password?.trim())
var res = request("connect", payload, timeoutMs = 8_000)
val res = request("connect", payload, timeoutMs = CONNECT_RPC_TIMEOUT_MS)
if (!res.ok) {
val msg = res.error?.message ?: "connect failed"
val hasStoredToken = !storedToken.isNullOrBlank()
val canRetryWithShared = hasStoredToken && trimmedToken.isNotBlank()
if (canRetryWithShared) {
val sharedPayload = buildConnectParams(identity, connectNonce, trimmedToken, password?.trim())
val sharedRes = request("connect", sharedPayload, timeoutMs = 8_000)
if (!sharedRes.ok) {
val retryMsg = sharedRes.error?.message ?: msg
throw IllegalStateException(retryMsg)
}
// Stored device token was bypassed successfully; clear stale token for future connects.
deviceAuthStore.clearToken(identity.deviceId, options.role)
res = sharedRes
} else {
throw IllegalStateException(msg)
}
throw IllegalStateException(msg)
}
handleConnectSuccess(res, identity.deviceId)
connectDeferred.complete(Unit)

View File

@@ -168,6 +168,11 @@ fun ConnectTabScreen(viewModel: MainViewModel) {
validationText = null
return@Button
}
if (statusText.contains("operator offline", ignoreCase = true)) {
validationText = null
viewModel.refreshGatewayConnection()
return@Button
}
val config =
resolveGatewayConnectConfig(
@@ -397,15 +402,6 @@ fun ConnectTabScreen(viewModel: MainViewModel) {
HorizontalDivider(color = mobileBorder)
Text(
"Debug snapshot: mode=${if (inputMode == ConnectInputMode.SetupCode) "setup" else "manual"}, manualEnabled=$manualEnabled, tokenLen=${gatewayToken.trim().length}",
style = mobileCaption1,
color = mobileTextSecondary,
)
TextButton(onClick = { viewModel.logGatewayDebugSnapshot(source = "connect_tab") }) {
Text("Log gateway debug snapshot", style = mobileCallout.copy(fontWeight = FontWeight.SemiBold), color = mobileAccent)
}
TextButton(onClick = { viewModel.setOnboardingCompleted(false) }) {
Text("Run onboarding again", style = mobileCallout.copy(fontWeight = FontWeight.SemiBold), color = mobileAccent)
}