From 4b188dcf975e01fd30cc80790457373aa8d44b66 Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Tue, 24 Feb 2026 20:55:21 +0530 Subject: [PATCH] fix(android): persist gateway auth state across onboarding --- .../java/ai/openclaw/android/MainViewModel.kt | 4 +++ .../java/ai/openclaw/android/NodeRuntime.kt | 10 ++++++ .../java/ai/openclaw/android/SecurePrefs.kt | 36 ++++++++++++++++--- .../openclaw/android/ui/ConnectTabScreen.kt | 32 +++++++++++++---- .../ai/openclaw/android/ui/OnboardingFlow.kt | 14 ++++---- 5 files changed, 80 insertions(+), 16 deletions(-) diff --git a/apps/android/app/src/main/java/ai/openclaw/android/MainViewModel.kt b/apps/android/app/src/main/java/ai/openclaw/android/MainViewModel.kt index 62f91cf624e..70aa176922c 100644 --- a/apps/android/app/src/main/java/ai/openclaw/android/MainViewModel.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/MainViewModel.kt @@ -139,6 +139,10 @@ class MainViewModel(app: Application) : AndroidViewModel(app) { runtime.setTalkEnabled(enabled) } + fun logGatewayDebugSnapshot(source: String = "manual") { + runtime.logGatewayDebugSnapshot(source) + } + fun refreshGatewayConnection() { runtime.refreshGatewayConnection() } diff --git a/apps/android/app/src/main/java/ai/openclaw/android/NodeRuntime.kt b/apps/android/app/src/main/java/ai/openclaw/android/NodeRuntime.kt index fece6278864..5e6268511aa 100644 --- a/apps/android/app/src/main/java/ai/openclaw/android/NodeRuntime.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/NodeRuntime.kt @@ -4,6 +4,7 @@ import android.Manifest import android.content.Context import android.content.pm.PackageManager import android.os.SystemClock +import android.util.Log import androidx.core.content.ContextCompat import ai.openclaw.android.chat.ChatController import ai.openclaw.android.chat.ChatMessage @@ -534,6 +535,15 @@ 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 token = prefs.loadGatewayToken() diff --git a/apps/android/app/src/main/java/ai/openclaw/android/SecurePrefs.kt b/apps/android/app/src/main/java/ai/openclaw/android/SecurePrefs.kt index e0cacd7a3cc..54f6292d29e 100644 --- a/apps/android/app/src/main/java/ai/openclaw/android/SecurePrefs.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/SecurePrefs.kt @@ -4,6 +4,7 @@ package ai.openclaw.android import android.content.Context import android.content.SharedPreferences +import android.util.Log import androidx.core.content.edit import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.MasterKey @@ -13,6 +14,7 @@ import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonNull import kotlinx.serialization.json.JsonPrimitive +import java.security.MessageDigest import java.util.UUID class SecurePrefs(context: Context) { @@ -98,6 +100,10 @@ class SecurePrefs(context: Context) { private val _talkEnabled = MutableStateFlow(prefs.getBoolean("talk.enabled", false)) val talkEnabled: StateFlow = _talkEnabled + init { + logGatewayToken("init.gateway.manual.token", _gatewayToken.value) + } + fun setLastDiscoveredStableId(value: String) { val trimmed = value.trim() prefs.edit { putString("gateway.lastDiscoveredStableID", trimmed) } @@ -152,8 +158,10 @@ class SecurePrefs(context: Context) { } fun setGatewayToken(value: String) { - prefs.edit { putString("gateway.manual.token", value) } - _gatewayToken.value = value + val trimmed = value.trim() + prefs.edit(commit = true) { putString("gateway.manual.token", trimmed) } + _gatewayToken.value = trimmed + logGatewayToken("setGatewayToken", trimmed) } fun setGatewayPassword(value: String) { @@ -172,10 +180,15 @@ class SecurePrefs(context: Context) { fun loadGatewayToken(): String? { val manual = _gatewayToken.value.trim() - if (manual.isNotEmpty()) return manual + if (manual.isNotEmpty()) { + logGatewayToken("loadGatewayToken.manual", manual) + return manual + } val key = "gateway.token.${_instanceId.value}" val stored = prefs.getString(key, null)?.trim() - return stored?.takeIf { it.isNotEmpty() } + val resolved = stored?.takeIf { it.isNotEmpty() } + logGatewayToken("loadGatewayToken.legacy", resolved.orEmpty()) + return resolved } fun saveGatewayToken(token: String) { @@ -234,6 +247,21 @@ class SecurePrefs(context: Context) { return fresh } + private fun logGatewayToken(event: String, value: String) { + val digest = + if (value.isBlank()) { + "empty" + } else { + try { + val bytes = MessageDigest.getInstance("SHA-256").digest(value.toByteArray(Charsets.UTF_8)) + bytes.take(4).joinToString("") { "%02x".format(it) } + } catch (_: Throwable) { + "hash_err" + } + } + Log.i("OpenClawSecurePrefs", "$event tokenLen=${value.length} tokenSha256Prefix=$digest") + } + private fun loadOrMigrateDisplayName(context: Context): String { val existing = prefs.getString(displayNameKey, null)?.trim().orEmpty() if (existing.isNotEmpty() && existing != "Android Node") return existing diff --git a/apps/android/app/src/main/java/ai/openclaw/android/ui/ConnectTabScreen.kt b/apps/android/app/src/main/java/ai/openclaw/android/ui/ConnectTabScreen.kt index da22795cdc2..24336849d50 100644 --- a/apps/android/app/src/main/java/ai/openclaw/android/ui/ConnectTabScreen.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/ui/ConnectTabScreen.kt @@ -86,16 +86,25 @@ fun ConnectTabScreen(viewModel: MainViewModel) { val manualHost by viewModel.manualHost.collectAsState() val manualPort by viewModel.manualPort.collectAsState() val manualTls by viewModel.manualTls.collectAsState() + val manualEnabled by viewModel.manualEnabled.collectAsState() val gatewayToken by viewModel.gatewayToken.collectAsState() val pendingTrust by viewModel.pendingGatewayTrust.collectAsState() var advancedOpen by rememberSaveable { mutableStateOf(false) } - var inputMode by rememberSaveable { mutableStateOf(ConnectInputMode.SetupCode) } + var inputMode by + remember(manualEnabled, manualHost, gatewayToken) { + mutableStateOf( + if (manualEnabled || manualHost.isNotBlank() || gatewayToken.trim().isNotEmpty()) { + ConnectInputMode.Manual + } else { + ConnectInputMode.SetupCode + }, + ) + } var setupCode by rememberSaveable { mutableStateOf("") } var manualHostInput by rememberSaveable { mutableStateOf(manualHost.ifBlank { "10.0.2.2" }) } var manualPortInput by rememberSaveable { mutableStateOf(manualPort.toString()) } var manualTlsInput by rememberSaveable { mutableStateOf(manualTls) } - var tokenInput by rememberSaveable { mutableStateOf(gatewayToken) } var passwordInput by rememberSaveable { mutableStateOf("") } var validationText by rememberSaveable { mutableStateOf(null) } @@ -192,7 +201,7 @@ fun ConnectTabScreen(viewModel: MainViewModel) { manualHost = manualHostInput, manualPort = manualPortInput, manualTls = manualTlsInput, - token = tokenInput, + token = gatewayToken, password = passwordInput, ) @@ -211,7 +220,9 @@ fun ConnectTabScreen(viewModel: MainViewModel) { viewModel.setManualHost(config.host) viewModel.setManualPort(config.port) viewModel.setManualTls(config.tls) - viewModel.setGatewayToken(config.token) + if (config.token.isNotBlank()) { + viewModel.setGatewayToken(config.token) + } viewModel.setGatewayPassword(config.password) viewModel.connectManual() }, @@ -380,8 +391,8 @@ fun ConnectTabScreen(viewModel: MainViewModel) { Text("Token (optional)", style = mobileCaption1.copy(fontWeight = FontWeight.SemiBold), color = mobileTextSecondary) OutlinedTextField( - value = tokenInput, - onValueChange = { tokenInput = it }, + value = gatewayToken, + onValueChange = { viewModel.setGatewayToken(it) }, placeholder = { Text("token", style = mobileBody, color = mobileTextTertiary) }, modifier = Modifier.fillMaxWidth(), singleLine = true, @@ -411,6 +422,15 @@ 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) } diff --git a/apps/android/app/src/main/java/ai/openclaw/android/ui/OnboardingFlow.kt b/apps/android/app/src/main/java/ai/openclaw/android/ui/OnboardingFlow.kt index d35cab59f54..780781455be 100644 --- a/apps/android/app/src/main/java/ai/openclaw/android/ui/OnboardingFlow.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/ui/OnboardingFlow.kt @@ -201,12 +201,12 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) { val isConnected by viewModel.isConnected.collectAsState() val serverName by viewModel.serverName.collectAsState() val remoteAddress by viewModel.remoteAddress.collectAsState() + val persistedGatewayToken by viewModel.gatewayToken.collectAsState() val pendingTrust by viewModel.pendingGatewayTrust.collectAsState() var step by rememberSaveable { mutableStateOf(OnboardingStep.Welcome) } var setupCode by rememberSaveable { mutableStateOf("") } var gatewayUrl by rememberSaveable { mutableStateOf("") } - var gatewayToken by rememberSaveable { mutableStateOf("") } var gatewayPassword by rememberSaveable { mutableStateOf("") } var gatewayInputMode by rememberSaveable { mutableStateOf(GatewayInputMode.SetupCode) } var manualHost by rememberSaveable { mutableStateOf("10.0.2.2") } @@ -337,7 +337,7 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) { manualHost = manualHost, manualPort = manualPort, manualTls = manualTls, - gatewayToken = gatewayToken, + gatewayToken = persistedGatewayToken, gatewayPassword = gatewayPassword, gatewayError = gatewayError, onInputModeChange = { @@ -357,7 +357,7 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) { gatewayError = null }, onManualTlsChange = { manualTls = it }, - onTokenChange = { gatewayToken = it }, + onTokenChange = viewModel::setGatewayToken, onPasswordChange = { gatewayPassword = it }, ) OnboardingStep.Permissions -> @@ -455,7 +455,7 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) { return@Button } gatewayUrl = parsedSetup.url - gatewayToken = parsedSetup.token.orEmpty() + parsedSetup.token?.let { viewModel.setGatewayToken(it) } gatewayPassword = parsedSetup.password.orEmpty() } else { val manualUrl = composeManualGatewayUrl(manualHost, manualPort, manualTls) @@ -530,14 +530,16 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) { gatewayError = "Invalid gateway URL." return@Button } - val token = gatewayToken.trim() + val token = persistedGatewayToken.trim() val password = gatewayPassword.trim() attemptedConnect = true viewModel.setManualEnabled(true) viewModel.setManualHost(parsed.host) viewModel.setManualPort(parsed.port) viewModel.setManualTls(parsed.tls) - viewModel.setGatewayToken(token) + if (token.isNotEmpty()) { + viewModel.setGatewayToken(token) + } viewModel.setGatewayPassword(password) viewModel.connectManual() },