diff --git a/apps/android/app/src/main/java/ai/openclaw/app/MainViewModel.kt b/apps/android/app/src/main/java/ai/openclaw/app/MainViewModel.kt index f99154c67ad..b4a6f042312 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/MainViewModel.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/MainViewModel.kt @@ -261,6 +261,22 @@ class MainViewModel(app: Application) : AndroidViewModel(app) { ensureRuntime().connect(endpoint) } + fun connect( + endpoint: GatewayEndpoint, + token: String?, + bootstrapToken: String?, + password: String?, + ) { + ensureRuntime().connect( + endpoint, + NodeRuntime.GatewayConnectAuth( + token = token, + bootstrapToken = bootstrapToken, + password = password, + ), + ) + } + fun connectManual() { ensureRuntime().connectManual() } diff --git a/apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt b/apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt index 9a2f0bf0214..d02050512fd 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt @@ -45,6 +45,12 @@ class NodeRuntime( context: Context, val prefs: SecurePrefs = SecurePrefs(context.applicationContext), ) { + data class GatewayConnectAuth( + val token: String?, + val bootstrapToken: String?, + val password: String?, + ) + private val appContext = context.applicationContext private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private val deviceAuthStore = DeviceAuthStore(prefs) @@ -775,12 +781,31 @@ class NodeRuntime( } operatorStatusText = "Connecting…" updateStatus() - val token = prefs.loadGatewayToken() - val bootstrapToken = prefs.loadGatewayBootstrapToken() - val password = prefs.loadGatewayPassword() + connectWithAuth( + endpoint = endpoint, + auth = + GatewayConnectAuth( + token = prefs.loadGatewayToken(), + bootstrapToken = prefs.loadGatewayBootstrapToken(), + password = prefs.loadGatewayPassword(), + ), + reconnect = true, + ) + } + + private fun connectWithAuth( + endpoint: GatewayEndpoint, + auth: GatewayConnectAuth, + reconnect: Boolean = false, + ) { val tls = connectionManager.resolveTlsParams(endpoint) val connectOperator = - shouldConnectOperatorSession(token, bootstrapToken, password, loadStoredRoleDeviceToken("operator")) + shouldConnectOperatorSession( + auth.token, + auth.bootstrapToken, + auth.password, + loadStoredRoleDeviceToken("operator"), + ) if (!connectOperator) { operatorConnected = false operatorStatusText = "Offline" @@ -789,25 +814,27 @@ class NodeRuntime( } else { operatorSession.connect( endpoint, - token, - bootstrapToken, - password, + auth.token, + auth.bootstrapToken, + auth.password, connectionManager.buildOperatorConnectOptions(), tls, ) } nodeSession.connect( endpoint, - token, - bootstrapToken, - password, + auth.token, + auth.bootstrapToken, + auth.password, connectionManager.buildNodeConnectOptions(), tls, ) - if (connectOperator) { + if (reconnect && connectOperator) { operatorSession.reconnect() } - nodeSession.reconnect() + if (reconnect) { + nodeSession.reconnect() + } } fun connect(endpoint: GatewayEndpoint) { @@ -829,36 +856,28 @@ class NodeRuntime( operatorStatusText = "Connecting…" nodeStatusText = "Connecting…" updateStatus() - val token = prefs.loadGatewayToken() - val bootstrapToken = prefs.loadGatewayBootstrapToken() - val password = prefs.loadGatewayPassword() - val connectOperator = - shouldConnectOperatorSession(token, bootstrapToken, password, loadStoredRoleDeviceToken("operator")) - if (!connectOperator) { - operatorConnected = false - operatorStatusText = "Offline" - operatorSession.disconnect() - updateStatus() - } else { - operatorSession.connect( - endpoint, - token, - bootstrapToken, - password, - connectionManager.buildOperatorConnectOptions(), - tls, - ) - } - nodeSession.connect( - endpoint, - token, - bootstrapToken, - password, - connectionManager.buildNodeConnectOptions(), - tls, + connectWithAuth( + endpoint = endpoint, + auth = + GatewayConnectAuth( + token = prefs.loadGatewayToken(), + bootstrapToken = prefs.loadGatewayBootstrapToken(), + password = prefs.loadGatewayPassword(), + ), ) } + fun connect( + endpoint: GatewayEndpoint, + auth: GatewayConnectAuth, + ) { + connectedEndpoint = endpoint + operatorStatusText = "Connecting…" + nodeStatusText = "Connecting…" + updateStatus() + connectWithAuth(endpoint = endpoint, auth = auth) + } + fun acceptGatewayTrustPrompt() { val prompt = _pendingGatewayTrust.value ?: return _pendingGatewayTrust.value = null diff --git a/apps/android/app/src/main/java/ai/openclaw/app/ui/ConnectTabScreen.kt b/apps/android/app/src/main/java/ai/openclaw/app/ui/ConnectTabScreen.kt index 603902b1907..2bb32ae2515 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/ui/ConnectTabScreen.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/ui/ConnectTabScreen.kt @@ -53,6 +53,7 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import ai.openclaw.app.MainViewModel +import ai.openclaw.app.gateway.GatewayEndpoint import ai.openclaw.app.ui.mobileCardSurface private enum class ConnectInputMode { @@ -269,7 +270,12 @@ fun ConnectTabScreen(viewModel: MainViewModel) { viewModel.setGatewayToken("") } viewModel.setGatewayPassword(config.password) - viewModel.connectManual() + viewModel.connect( + GatewayEndpoint.manual(host = config.host, port = config.port), + token = config.token.ifEmpty { null }, + bootstrapToken = config.bootstrapToken.ifEmpty { null }, + password = config.password.ifEmpty { null }, + ) }, modifier = Modifier.fillMaxWidth().height(52.dp), shape = RoundedCornerShape(14.dp), diff --git a/apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt b/apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt index 3868936ae5e..272afd01387 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt @@ -96,6 +96,7 @@ import androidx.lifecycle.compose.LocalLifecycleOwner import ai.openclaw.app.BuildConfig import ai.openclaw.app.LocationMode import ai.openclaw.app.MainViewModel +import ai.openclaw.app.gateway.GatewayEndpoint import ai.openclaw.app.node.DeviceNotificationListenerService import com.google.mlkit.vision.barcode.common.Barcode import com.google.mlkit.vision.codescanner.GmsBarcodeScannerOptions @@ -885,7 +886,17 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) { viewModel.setGatewayToken("") } viewModel.setGatewayPassword(password) - viewModel.connectManual() + viewModel.connect( + GatewayEndpoint.manual(host = parsed.host, port = parsed.port), + token = token.ifEmpty { null }, + bootstrapToken = + if (gatewayInputMode == GatewayInputMode.SetupCode) { + decodeGatewaySetupCode(setupCode)?.bootstrapToken?.trim()?.ifEmpty { null } + } else { + null + }, + password = password.ifEmpty { null }, + ) }, modifier = Modifier.weight(1f).height(52.dp), shape = RoundedCornerShape(14.dp), @@ -1680,21 +1691,22 @@ private fun FinalStep( ) } } + Text("Status", style = onboardingCaption1Style.copy(fontWeight = FontWeight.Bold), color = onboardingTextSecondary) + Surface( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(12.dp), + color = onboardingCommandBg, + border = BorderStroke(1.dp, onboardingCommandBorder), + ) { + Text( + statusLabel, + modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp), + style = onboardingCalloutStyle.copy(fontFamily = FontFamily.Monospace), + color = onboardingCommandText, + ) + } if (showDiagnostics) { Text("Error", style = onboardingCaption1Style.copy(fontWeight = FontWeight.Bold), color = onboardingTextSecondary) - Surface( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(12.dp), - color = onboardingCommandBg, - border = BorderStroke(1.dp, onboardingCommandBorder), - ) { - Text( - statusLabel, - modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp), - style = onboardingCalloutStyle.copy(fontFamily = FontFamily.Monospace), - color = onboardingCommandText, - ) - } Text( "OpenClaw Android ${openClawAndroidVersionLabel()}", style = onboardingCaption1Style,