mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-08 06:54:24 +00:00
fix(android): persist gateway auth state across onboarding
This commit is contained in:
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user