fix(android): tighten pairing retry behavior

This commit is contained in:
Ayaan Zaidi
2026-04-08 21:23:16 +05:30
parent 6090afa0e5
commit 1f899f8442
3 changed files with 57 additions and 29 deletions

View File

@@ -56,15 +56,12 @@ import androidx.compose.ui.unit.dp
import ai.openclaw.app.MainViewModel
import ai.openclaw.app.gateway.GatewayEndpoint
import ai.openclaw.app.ui.mobileCardSurface
import kotlinx.coroutines.delay
private enum class ConnectInputMode {
SetupCode,
Manual,
}
private const val PAIRING_AUTO_RETRY_MS = 6_000L
@Composable
fun ConnectTabScreen(viewModel: MainViewModel) {
val context = LocalContext.current
@@ -147,14 +144,8 @@ fun ConnectTabScreen(viewModel: MainViewModel) {
val pairingRequired = !isConnected && gatewayStatusLooksLikePairing(statusText)
val statusLabel = gatewayStatusForDisplay(statusText)
LaunchedEffect(pairingRequired) {
if (!pairingRequired) {
return@LaunchedEffect
}
while (true) {
delay(PAIRING_AUTO_RETRY_MS)
viewModel.refreshGatewayConnection()
}
PairingAutoRetryEffect(enabled = pairingRequired) {
viewModel.refreshGatewayConnection()
}
Column(

View File

@@ -0,0 +1,45 @@
package ai.openclaw.app.ui
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import kotlinx.coroutines.delay
internal const val PAIRING_AUTO_RETRY_MS = 6_000L
@Composable
internal fun PairingAutoRetryEffect(enabled: Boolean, onRetry: () -> Unit) {
val lifecycleOwner = LocalLifecycleOwner.current
var lifecycleStarted by
remember(lifecycleOwner) {
mutableStateOf(lifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED))
}
DisposableEffect(lifecycleOwner) {
val observer =
LifecycleEventObserver { _, _ ->
lifecycleStarted = lifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
LaunchedEffect(enabled, lifecycleStarted) {
if (!enabled || !lifecycleStarted) {
return@LaunchedEffect
}
while (true) {
delay(PAIRING_AUTO_RETRY_MS)
onRetry()
}
}
}

View File

@@ -71,7 +71,6 @@ import androidx.compose.material.icons.filled.Wifi
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -102,7 +101,6 @@ import ai.openclaw.app.node.DeviceNotificationListenerService
import com.google.mlkit.vision.barcode.common.Barcode
import com.google.mlkit.vision.codescanner.GmsBarcodeScannerOptions
import com.google.mlkit.vision.codescanner.GmsBarcodeScanning
import kotlinx.coroutines.delay
private enum class OnboardingStep(val index: Int, val label: String) {
Welcome(1, "Welcome"),
@@ -134,8 +132,6 @@ private enum class SpecialAccessToggle {
NotificationListener,
}
private const val PAIRING_AUTO_RETRY_MS = 6_000L
private val onboardingBackgroundGradient: Brush
@Composable get() = mobileBackgroundGradient
@@ -894,6 +890,12 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) {
}
val token = persistedGatewayToken.trim()
val password = gatewayPassword.trim()
val bootstrapToken =
if (gatewayInputMode == GatewayInputMode.SetupCode) {
decodeGatewaySetupCode(setupCode)?.bootstrapToken?.trim()?.ifEmpty { null }
} else {
null
}
attemptedConnect = true
viewModel.setManualEnabled(true)
viewModel.setManualHost(parsed.config.host)
@@ -903,6 +905,7 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) {
viewModel.setGatewayBootstrapToken("")
} else {
viewModel.resetGatewaySetupAuth()
viewModel.setGatewayBootstrapToken(bootstrapToken.orEmpty())
}
if (token.isNotEmpty()) {
viewModel.setGatewayToken(token)
@@ -913,12 +916,7 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) {
viewModel.connect(
GatewayEndpoint.manual(host = parsed.config.host, port = parsed.config.port),
token = token.ifEmpty { null },
bootstrapToken =
if (gatewayInputMode == GatewayInputMode.SetupCode) {
decodeGatewaySetupCode(setupCode)?.bootstrapToken?.trim()?.ifEmpty { null }
} else {
null
},
bootstrapToken = bootstrapToken,
password = password.ifEmpty { null },
)
},
@@ -1591,14 +1589,8 @@ private fun FinalStep(
val showDiagnostics = gatewayStatusHasDiagnostics(statusText)
val pairingRequired = gatewayStatusLooksLikePairing(statusText)
LaunchedEffect(pairingRequired, attemptedConnect) {
if (!pairingRequired || !attemptedConnect) {
return@LaunchedEffect
}
while (true) {
delay(PAIRING_AUTO_RETRY_MS)
viewModel.refreshGatewayConnection()
}
PairingAutoRetryEffect(enabled = pairingRequired && attemptedConnect) {
viewModel.refreshGatewayConnection()
}
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {