fix(android): persist gateway auth state across onboarding

This commit is contained in:
Ayaan Zaidi
2026-02-24 20:55:21 +05:30
committed by Ayaan Zaidi
parent f853622eca
commit 4b188dcf97
5 changed files with 80 additions and 16 deletions

View File

@@ -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()
}

View File

@@ -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()

View File

@@ -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<Boolean> = _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

View File

@@ -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<String?>(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)
}

View File

@@ -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()
},