


















@@ -23,6 +23,7 @@ import android.content.pm.PackageManager
2323import android.hardware.Sensor
2424import android.hardware.SensorManager
2525import android.os.Build
26+import android.os.SystemClock
2627import android.provider.Settings
2728import android.widget.Toast
2829import androidx.activity.compose.rememberLauncherForActivityResult
@@ -79,6 +80,7 @@ import androidx.compose.runtime.DisposableEffect
7980import androidx.compose.runtime.LaunchedEffect
8081import androidx.compose.runtime.collectAsState
8182import androidx.compose.runtime.getValue
83+import androidx.compose.runtime.mutableLongStateOf
8284import androidx.compose.runtime.mutableStateOf
8385import androidx.compose.runtime.remember
8486import androidx.compose.runtime.saveable.rememberSaveable
@@ -100,6 +102,7 @@ import androidx.lifecycle.compose.LocalLifecycleOwner
100102import com.google.mlkit.vision.barcode.common.Barcode
101103import com.google.mlkit.vision.codescanner.GmsBarcodeScannerOptions
102104import com.google.mlkit.vision.codescanner.GmsBarcodeScanning
105+import kotlinx.coroutines.delay
103106104107private enum class OnboardingStep {
105108Welcome,
@@ -108,6 +111,8 @@ private enum class OnboardingStep {
108111Permissions,
109112}
110113114+private const val GATEWAY_CONNECT_SETTLING_MS = 2_500L
115+111116@Composable
112117fun OnboardingFlow(
113118viewModel: MainViewModel,
@@ -134,6 +139,8 @@ fun OnboardingFlow(
134139var password by rememberSaveable { mutableStateOf("") }
135140var setupError by rememberSaveable { mutableStateOf<String?>(null) }
136141var attemptedConnect by rememberSaveable { mutableStateOf(false) }
142+var connectAttemptStartedAtMs by rememberSaveable { mutableLongStateOf(0L) }
143+var recoveryNowMs by remember { mutableLongStateOf(SystemClock.elapsedRealtime()) }
137144138145val qrScannerOptions =
139146 remember {
@@ -152,6 +159,13 @@ fun OnboardingFlow(
152159 }
153160 }
154161162+LaunchedEffect(step, connectAttemptStartedAtMs) {
163+if (step != OnboardingStep.Recovery || connectAttemptStartedAtMs <= 0L) return@LaunchedEffect
164+ recoveryNowMs = SystemClock.elapsedRealtime()
165+ delay(GATEWAY_CONNECT_SETTLING_MS)
166+ recoveryNowMs = SystemClock.elapsedRealtime()
167+ }
168+155169 pendingTrust?.let { prompt ->
156170AlertDialog(
157171 onDismissRequest = viewModel::declineGatewayTrustPrompt,
@@ -250,6 +264,7 @@ fun OnboardingFlow(
250264251265 setupError = null
252266 attemptedConnect = true
267+ connectAttemptStartedAtMs = SystemClock.elapsedRealtime()
253268 viewModel.resetGatewaySetupAuth()
254269 viewModel.setManualEnabled(true)
255270 viewModel.setManualHost(config.host)
@@ -275,10 +290,12 @@ fun OnboardingFlow(
275290 remoteAddress = remoteAddress,
276291 ready = ready,
277292 attemptedConnect = attemptedConnect,
293+ connectSettling = recoveryNowMs - connectAttemptStartedAtMs < GATEWAY_CONNECT_SETTLING_MS,
278294 onAutoRetry = viewModel::refreshGatewayConnection,
279295 onBack = { step = OnboardingStep.Gateway },
280296 onRetry = {
281297 attemptedConnect = true
298+ connectAttemptStartedAtMs = SystemClock.elapsedRealtime()
282299val config =
283300 resolveGatewayConfig(
284301 setupCode = setupCode,
@@ -496,31 +513,44 @@ private fun GatewayRecoveryScreen(
496513remoteAddress: String?,
497514ready: Boolean,
498515attemptedConnect: Boolean,
516+connectSettling: Boolean,
499517onAutoRetry: () -> Unit,
500518onBack: () -> Unit,
501519onRetry: () -> Unit,
502520onEdit: () -> Unit,
503521onContinue: () -> Unit,
504522modifier: Modifier = Modifier,
505523) {
506-val pairingRequired = gatewayStatusLooksLikePairing(statusText)
524+val recoveryState = gatewayRecoveryUiState(ready = ready, statusText = statusText, connectSettling = connectSettling)
507525val context = LocalContext.current
508-PairingAutoRetryEffect(enabled = pairingRequired && attemptedConnect && !ready, onRetry = onAutoRetry)
526+PairingAutoRetryEffect(enabled = recoveryState.canAutoRetry && attemptedConnect, onRetry = onAutoRetry)
509527510528ClawScaffold(modifier = modifier, contentPadding = PaddingValues(horizontal = 18.dp, vertical = 16.dp)) {
511529Column(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(18.dp)) {
512530OnboardingHeader(title = "Gateway Recovery", onBack = onBack)
513531Spacer(modifier = Modifier.height(12.dp))
514532Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(12.dp)) {
515533Icon(
516- imageVector = if (ready) Icons.Default.CheckCircle else Icons.Default.ErrorOutline,
534+ imageVector =
535+when (recoveryState) {
536+GatewayRecoveryUiState.Connected -> Icons.Default.CheckCircle
537+GatewayRecoveryUiState.Pairing -> Icons.Default.WifiTethering
538+GatewayRecoveryUiState.Finishing -> Icons.Default.WifiTethering
539+GatewayRecoveryUiState.Failed -> Icons.Default.ErrorOutline
540+ },
517541 contentDescription = null,
518542 modifier = Modifier.size(64.dp),
519- tint = if (ready) ClawTheme.colors.success else ClawTheme.colors.warning,
543+ tint =
544+when (recoveryState) {
545+GatewayRecoveryUiState.Connected -> ClawTheme.colors.success
546+GatewayRecoveryUiState.Pairing -> ClawTheme.colors.text
547+GatewayRecoveryUiState.Finishing -> ClawTheme.colors.text
548+GatewayRecoveryUiState.Failed -> ClawTheme.colors.warning
549+ },
520550 )
521-Text(text = if (ready) "Connected" else "Connection failed", style = ClawTheme.type.display, color = ClawTheme.colors.text)
551+Text(text = recoveryState.title, style = ClawTheme.type.display, color = ClawTheme.colors.text)
522552Text(
523- text = if (ready) "Your Gateway is ready." else "We could not reach your Gateway.\nLet's fix this.",
553+ text = recoveryState.message,
524554 style = ClawTheme.type.body,
525555 color = ClawTheme.colors.textMuted,
526556 textAlign = TextAlign.Center,
@@ -534,18 +564,30 @@ private fun GatewayRecoveryScreen(
534564Text(text = recoveryGatewayDetail(ready = ready, remoteAddress = remoteAddress, statusText = statusText), style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
535565ClawStatusPill(
536566 text =
537-when {
538- ready -> "Healthy"
539- pairingRequired -> "Pairing"
540-else -> "Needs attention"
567+when (recoveryState) {
568+GatewayRecoveryUiState.Connected -> "Healthy"
569+GatewayRecoveryUiState.Pairing -> "Pairing"
570+GatewayRecoveryUiState.Finishing -> "Connecting"
571+GatewayRecoveryUiState.Failed -> "Needs attention"
572+ },
573+ status =
574+when (recoveryState) {
575+GatewayRecoveryUiState.Connected -> ClawStatus.Success
576+GatewayRecoveryUiState.Pairing -> ClawStatus.Neutral
577+GatewayRecoveryUiState.Finishing -> ClawStatus.Neutral
578+GatewayRecoveryUiState.Failed -> ClawStatus.Warning
541579 },
542- status = if (ready) ClawStatus.Success else ClawStatus.Warning,
543580 )
544581 }
545582 }
546583547584Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
548-ClawPrimaryButton(text = if (ready) "Continue" else "Retry connection", icon = if (ready) Icons.Default.CheckCircle else Icons.Default.Refresh, onClick = if (ready) onContinue else onRetry, modifier = Modifier.fillMaxWidth())
585+ClawPrimaryButton(
586+ text = if (ready) "Continue" else "Retry connection",
587+ icon = if (ready) Icons.Default.CheckCircle else Icons.Default.Refresh,
588+ onClick = if (ready) onContinue else onRetry,
589+ modifier = Modifier.fillMaxWidth(),
590+ )
549591OutlinedAction(title = "Edit connection", icon = Icons.Default.Edit, onClick = onEdit)
550592OutlinedAction(title = "Copy diagnostic", icon = Icons.Default.ContentCopy, onClick = { copyGatewayDiagnostic(context, statusText, serverName, remoteAddress, ready) })
551593 }
@@ -824,6 +866,51 @@ private fun PermissionContinueButton(onClick: () -> Unit) {
824866 }
825867}
826868869+internal enum class GatewayRecoveryUiState(
870+val title: String,
871+val message: String,
872+val canAutoRetry: Boolean,
873+) {
874+Connected(
875+ title = "Connected",
876+ message = "Your Gateway is ready.",
877+ canAutoRetry = false,
878+ ),
879+Pairing(
880+ title = "Pairing Gateway",
881+ message = "Approval is in progress.\nOpenClaw will reconnect automatically.",
882+ canAutoRetry = true,
883+ ),
884+Finishing(
885+ title = "Finishing Setup",
886+ message = "Gateway approved this phone.\nOpenClaw is bringing the node online.",
887+ canAutoRetry = true,
888+ ),
889+Failed(
890+ title = "Connection issue",
891+ message = "We could not reach your Gateway.\nLet's fix this.",
892+ canAutoRetry = false,
893+ ),
894+}
895+896+internal fun gatewayRecoveryUiState(
897+ready: Boolean,
898+statusText: String,
899+connectSettling: Boolean,
900+): GatewayRecoveryUiState =
901+when {
902+ ready -> GatewayRecoveryUiState.Connected
903+ connectSettling -> GatewayRecoveryUiState.Finishing
904+ gatewayStatusLooksLikePairing(statusText) -> GatewayRecoveryUiState.Pairing
905+ gatewayStatusLooksLikePartialConnect(statusText) -> GatewayRecoveryUiState.Finishing
906+else -> GatewayRecoveryUiState.Failed
907+ }
908+909+internal fun gatewayStatusLooksLikePartialConnect(statusText: String): Boolean {
910+val lower = gatewayStatusForDisplay(statusText).lowercase()
911+return lower.contains("operator offline") || lower.contains("node offline")
912+}
913+827914private data class GatewayConfig(
828915val host: String,
829916val port: Int,
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。