mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-07 22:44:16 +00:00
refactor(android-ui): unify gateway config resolution paths
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
package ai.openclaw.android.ui
|
||||
|
||||
import android.util.Base64
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
@@ -47,37 +46,13 @@ import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.net.toUri
|
||||
import ai.openclaw.android.MainViewModel
|
||||
import java.util.Locale
|
||||
import org.json.JSONObject
|
||||
|
||||
private enum class ConnectInputMode {
|
||||
SetupCode,
|
||||
Manual,
|
||||
}
|
||||
|
||||
private data class ParsedConnectGateway(
|
||||
val host: String,
|
||||
val port: Int,
|
||||
val tls: Boolean,
|
||||
val displayUrl: String,
|
||||
)
|
||||
|
||||
private data class ConnectSetupCodePayload(
|
||||
val url: String,
|
||||
val token: String?,
|
||||
val password: String?,
|
||||
)
|
||||
|
||||
private data class ConnectConfig(
|
||||
val host: String,
|
||||
val port: Int,
|
||||
val tls: Boolean,
|
||||
val token: String,
|
||||
val password: String,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun ConnectTabScreen(viewModel: MainViewModel) {
|
||||
val statusText by viewModel.statusText.collectAsState()
|
||||
@@ -132,9 +107,9 @@ fun ConnectTabScreen(viewModel: MainViewModel) {
|
||||
)
|
||||
}
|
||||
|
||||
val setupResolvedEndpoint = remember(setupCode) { decodeConnectSetupCode(setupCode)?.url?.let { parseConnectGateway(it)?.displayUrl } }
|
||||
val setupResolvedEndpoint = remember(setupCode) { decodeGatewaySetupCode(setupCode)?.url?.let { parseGatewayEndpoint(it)?.displayUrl } }
|
||||
val manualResolvedEndpoint = remember(manualHostInput, manualPortInput, manualTlsInput) {
|
||||
composeConnectManualGatewayUrl(manualHostInput, manualPortInput, manualTlsInput)?.let { parseConnectGateway(it)?.displayUrl }
|
||||
composeGatewayManualUrl(manualHostInput, manualPortInput, manualTlsInput)?.let { parseGatewayEndpoint(it)?.displayUrl }
|
||||
}
|
||||
|
||||
val activeEndpoint =
|
||||
@@ -195,14 +170,14 @@ fun ConnectTabScreen(viewModel: MainViewModel) {
|
||||
}
|
||||
|
||||
val config =
|
||||
resolveConnectConfig(
|
||||
mode = inputMode,
|
||||
resolveGatewayConnectConfig(
|
||||
useSetupCode = inputMode == ConnectInputMode.SetupCode,
|
||||
setupCode = setupCode,
|
||||
manualHost = manualHostInput,
|
||||
manualPort = manualPortInput,
|
||||
manualTls = manualTlsInput,
|
||||
token = gatewayToken,
|
||||
password = passwordInput,
|
||||
fallbackToken = gatewayToken,
|
||||
fallbackPassword = passwordInput,
|
||||
)
|
||||
|
||||
if (config == null) {
|
||||
@@ -520,91 +495,3 @@ private fun outlinedColors() =
|
||||
unfocusedTextColor = mobileText,
|
||||
cursorColor = mobileAccent,
|
||||
)
|
||||
|
||||
private fun resolveConnectConfig(
|
||||
mode: ConnectInputMode,
|
||||
setupCode: String,
|
||||
manualHost: String,
|
||||
manualPort: String,
|
||||
manualTls: Boolean,
|
||||
token: String,
|
||||
password: String,
|
||||
): ConnectConfig? {
|
||||
return if (mode == ConnectInputMode.SetupCode) {
|
||||
val setup = decodeConnectSetupCode(setupCode) ?: return null
|
||||
val parsed = parseConnectGateway(setup.url) ?: return null
|
||||
ConnectConfig(
|
||||
host = parsed.host,
|
||||
port = parsed.port,
|
||||
tls = parsed.tls,
|
||||
token = setup.token ?: token.trim(),
|
||||
password = setup.password ?: password.trim(),
|
||||
)
|
||||
} else {
|
||||
val manualUrl = composeConnectManualGatewayUrl(manualHost, manualPort, manualTls) ?: return null
|
||||
val parsed = parseConnectGateway(manualUrl) ?: return null
|
||||
ConnectConfig(
|
||||
host = parsed.host,
|
||||
port = parsed.port,
|
||||
tls = parsed.tls,
|
||||
token = token.trim(),
|
||||
password = password.trim(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseConnectGateway(rawInput: String): ParsedConnectGateway? {
|
||||
val raw = rawInput.trim()
|
||||
if (raw.isEmpty()) return null
|
||||
|
||||
val normalized = if (raw.contains("://")) raw else "https://$raw"
|
||||
val uri = normalized.toUri()
|
||||
val host = uri.host?.trim().orEmpty()
|
||||
if (host.isEmpty()) return null
|
||||
|
||||
val scheme = uri.scheme?.trim()?.lowercase(Locale.US).orEmpty()
|
||||
val tls =
|
||||
when (scheme) {
|
||||
"ws", "http" -> false
|
||||
"wss", "https" -> true
|
||||
else -> true
|
||||
}
|
||||
val port = uri.port.takeIf { it in 1..65535 } ?: 18789
|
||||
val displayUrl = "${if (tls) "https" else "http"}://$host:$port"
|
||||
|
||||
return ParsedConnectGateway(host = host, port = port, tls = tls, displayUrl = displayUrl)
|
||||
}
|
||||
|
||||
private fun decodeConnectSetupCode(rawInput: String): ConnectSetupCodePayload? {
|
||||
val trimmed = rawInput.trim()
|
||||
if (trimmed.isEmpty()) return null
|
||||
|
||||
val padded =
|
||||
trimmed
|
||||
.replace('-', '+')
|
||||
.replace('_', '/')
|
||||
.let { normalized ->
|
||||
val remainder = normalized.length % 4
|
||||
if (remainder == 0) normalized else normalized + "=".repeat(4 - remainder)
|
||||
}
|
||||
|
||||
return try {
|
||||
val decoded = String(Base64.decode(padded, Base64.DEFAULT), Charsets.UTF_8)
|
||||
val obj = JSONObject(decoded)
|
||||
val url = obj.optString("url").trim()
|
||||
if (url.isEmpty()) return null
|
||||
val token = obj.optString("token").trim().ifEmpty { null }
|
||||
val password = obj.optString("password").trim().ifEmpty { null }
|
||||
ConnectSetupCodePayload(url = url, token = token, password = password)
|
||||
} catch (_: Throwable) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun composeConnectManualGatewayUrl(hostInput: String, portInput: String, tls: Boolean): String? {
|
||||
val host = hostInput.trim()
|
||||
val port = portInput.trim().toIntOrNull() ?: return null
|
||||
if (host.isEmpty() || port !in 1..65535) return null
|
||||
val scheme = if (tls) "https" else "http"
|
||||
return "$scheme://$host:$port"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
package ai.openclaw.android.ui
|
||||
|
||||
import android.util.Base64
|
||||
import androidx.core.net.toUri
|
||||
import java.util.Locale
|
||||
import org.json.JSONObject
|
||||
|
||||
internal data class GatewayEndpointConfig(
|
||||
val host: String,
|
||||
val port: Int,
|
||||
val tls: Boolean,
|
||||
val displayUrl: String,
|
||||
)
|
||||
|
||||
internal data class GatewaySetupCode(
|
||||
val url: String,
|
||||
val token: String?,
|
||||
val password: String?,
|
||||
)
|
||||
|
||||
internal data class GatewayConnectConfig(
|
||||
val host: String,
|
||||
val port: Int,
|
||||
val tls: Boolean,
|
||||
val token: String,
|
||||
val password: String,
|
||||
)
|
||||
|
||||
internal fun resolveGatewayConnectConfig(
|
||||
useSetupCode: Boolean,
|
||||
setupCode: String,
|
||||
manualHost: String,
|
||||
manualPort: String,
|
||||
manualTls: Boolean,
|
||||
fallbackToken: String,
|
||||
fallbackPassword: String,
|
||||
): GatewayConnectConfig? {
|
||||
if (useSetupCode) {
|
||||
val setup = decodeGatewaySetupCode(setupCode) ?: return null
|
||||
val parsed = parseGatewayEndpoint(setup.url) ?: return null
|
||||
return GatewayConnectConfig(
|
||||
host = parsed.host,
|
||||
port = parsed.port,
|
||||
tls = parsed.tls,
|
||||
token = setup.token ?: fallbackToken.trim(),
|
||||
password = setup.password ?: fallbackPassword.trim(),
|
||||
)
|
||||
}
|
||||
|
||||
val manualUrl = composeGatewayManualUrl(manualHost, manualPort, manualTls) ?: return null
|
||||
val parsed = parseGatewayEndpoint(manualUrl) ?: return null
|
||||
return GatewayConnectConfig(
|
||||
host = parsed.host,
|
||||
port = parsed.port,
|
||||
tls = parsed.tls,
|
||||
token = fallbackToken.trim(),
|
||||
password = fallbackPassword.trim(),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun parseGatewayEndpoint(rawInput: String): GatewayEndpointConfig? {
|
||||
val raw = rawInput.trim()
|
||||
if (raw.isEmpty()) return null
|
||||
|
||||
val normalized = if (raw.contains("://")) raw else "https://$raw"
|
||||
val uri = normalized.toUri()
|
||||
val host = uri.host?.trim().orEmpty()
|
||||
if (host.isEmpty()) return null
|
||||
|
||||
val scheme = uri.scheme?.trim()?.lowercase(Locale.US).orEmpty()
|
||||
val tls =
|
||||
when (scheme) {
|
||||
"ws", "http" -> false
|
||||
"wss", "https" -> true
|
||||
else -> true
|
||||
}
|
||||
val port = uri.port.takeIf { it in 1..65535 } ?: 18789
|
||||
val displayUrl = "${if (tls) "https" else "http"}://$host:$port"
|
||||
|
||||
return GatewayEndpointConfig(host = host, port = port, tls = tls, displayUrl = displayUrl)
|
||||
}
|
||||
|
||||
internal fun decodeGatewaySetupCode(rawInput: String): GatewaySetupCode? {
|
||||
val trimmed = rawInput.trim()
|
||||
if (trimmed.isEmpty()) return null
|
||||
|
||||
val padded =
|
||||
trimmed
|
||||
.replace('-', '+')
|
||||
.replace('_', '/')
|
||||
.let { normalized ->
|
||||
val remainder = normalized.length % 4
|
||||
if (remainder == 0) normalized else normalized + "=".repeat(4 - remainder)
|
||||
}
|
||||
|
||||
return try {
|
||||
val decoded = String(Base64.decode(padded, Base64.DEFAULT), Charsets.UTF_8)
|
||||
val obj = JSONObject(decoded)
|
||||
val url = obj.optString("url").trim()
|
||||
if (url.isEmpty()) return null
|
||||
val token = obj.optString("token").trim().ifEmpty { null }
|
||||
val password = obj.optString("password").trim().ifEmpty { null }
|
||||
GatewaySetupCode(url = url, token = token, password = password)
|
||||
} catch (_: Throwable) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
internal fun composeGatewayManualUrl(hostInput: String, portInput: String, tls: Boolean): String? {
|
||||
val host = hostInput.trim()
|
||||
val port = portInput.trim().toIntOrNull() ?: return null
|
||||
if (host.isEmpty() || port !in 1..65535) return null
|
||||
val scheme = if (tls) "https" else "http"
|
||||
return "$scheme://$host:$port"
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.util.Base64
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.background
|
||||
@@ -72,12 +71,9 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import ai.openclaw.android.LocationMode
|
||||
import ai.openclaw.android.MainViewModel
|
||||
import ai.openclaw.android.R
|
||||
import java.util.Locale
|
||||
import org.json.JSONObject
|
||||
|
||||
private enum class OnboardingStep(val index: Int, val label: String) {
|
||||
Welcome(1, "Welcome"),
|
||||
@@ -91,19 +87,6 @@ private enum class GatewayInputMode {
|
||||
Manual,
|
||||
}
|
||||
|
||||
private data class ParsedGateway(
|
||||
val host: String,
|
||||
val port: Int,
|
||||
val tls: Boolean,
|
||||
val displayUrl: String,
|
||||
)
|
||||
|
||||
private data class SetupCodePayload(
|
||||
val url: String,
|
||||
val token: String?,
|
||||
val password: String?,
|
||||
)
|
||||
|
||||
private val onboardingBackgroundGradient =
|
||||
listOf(
|
||||
Color(0xFFFFFFFF),
|
||||
@@ -377,7 +360,7 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) {
|
||||
)
|
||||
OnboardingStep.FinalCheck ->
|
||||
FinalStep(
|
||||
parsedGateway = parseGateway(gatewayUrl),
|
||||
parsedGateway = parseGatewayEndpoint(gatewayUrl),
|
||||
statusText = statusText,
|
||||
isConnected = isConnected,
|
||||
serverName = serverName,
|
||||
@@ -444,12 +427,12 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) {
|
||||
Button(
|
||||
onClick = {
|
||||
if (gatewayInputMode == GatewayInputMode.SetupCode) {
|
||||
val parsedSetup = decodeSetupCode(setupCode)
|
||||
val parsedSetup = decodeGatewaySetupCode(setupCode)
|
||||
if (parsedSetup == null) {
|
||||
gatewayError = "Invalid setup code."
|
||||
return@Button
|
||||
}
|
||||
val parsedGateway = parseGateway(parsedSetup.url)
|
||||
val parsedGateway = parseGatewayEndpoint(parsedSetup.url)
|
||||
if (parsedGateway == null) {
|
||||
gatewayError = "Setup code has invalid gateway URL."
|
||||
return@Button
|
||||
@@ -458,8 +441,8 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) {
|
||||
parsedSetup.token?.let { viewModel.setGatewayToken(it) }
|
||||
gatewayPassword = parsedSetup.password.orEmpty()
|
||||
} else {
|
||||
val manualUrl = composeManualGatewayUrl(manualHost, manualPort, manualTls)
|
||||
val parsedGateway = manualUrl?.let(::parseGateway)
|
||||
val manualUrl = composeGatewayManualUrl(manualHost, manualPort, manualTls)
|
||||
val parsedGateway = manualUrl?.let(::parseGatewayEndpoint)
|
||||
if (parsedGateway == null) {
|
||||
gatewayError = "Manual endpoint is invalid."
|
||||
return@Button
|
||||
@@ -524,7 +507,7 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) {
|
||||
} else {
|
||||
Button(
|
||||
onClick = {
|
||||
val parsed = parseGateway(gatewayUrl)
|
||||
val parsed = parseGatewayEndpoint(gatewayUrl)
|
||||
if (parsed == null) {
|
||||
step = OnboardingStep.Gateway
|
||||
gatewayError = "Invalid gateway URL."
|
||||
@@ -639,8 +622,8 @@ private fun GatewayStep(
|
||||
onTokenChange: (String) -> Unit,
|
||||
onPasswordChange: (String) -> Unit,
|
||||
) {
|
||||
val resolvedEndpoint = remember(setupCode) { decodeSetupCode(setupCode)?.url?.let { parseGateway(it)?.displayUrl } }
|
||||
val manualResolvedEndpoint = remember(manualHost, manualPort, manualTls) { composeManualGatewayUrl(manualHost, manualPort, manualTls)?.let { parseGateway(it)?.displayUrl } }
|
||||
val resolvedEndpoint = remember(setupCode) { decodeGatewaySetupCode(setupCode)?.url?.let { parseGatewayEndpoint(it)?.displayUrl } }
|
||||
val manualResolvedEndpoint = remember(manualHost, manualPort, manualTls) { composeGatewayManualUrl(manualHost, manualPort, manualTls)?.let { parseGatewayEndpoint(it)?.displayUrl } }
|
||||
|
||||
StepShell(title = "Gateway Connection") {
|
||||
GuideBlock(title = "Get setup code + gateway URL") {
|
||||
@@ -1046,7 +1029,7 @@ private fun PermissionToggleRow(
|
||||
|
||||
@Composable
|
||||
private fun FinalStep(
|
||||
parsedGateway: ParsedGateway?,
|
||||
parsedGateway: GatewayEndpointConfig?,
|
||||
statusText: String,
|
||||
isConnected: Boolean,
|
||||
serverName: String?,
|
||||
@@ -1132,62 +1115,6 @@ private fun Bullet(text: String) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseGateway(rawInput: String): ParsedGateway? {
|
||||
val raw = rawInput.trim()
|
||||
if (raw.isEmpty()) return null
|
||||
|
||||
val normalized = if (raw.contains("://")) raw else "https://$raw"
|
||||
val uri = normalized.toUri()
|
||||
val host = uri.host?.trim().orEmpty()
|
||||
if (host.isEmpty()) return null
|
||||
|
||||
val scheme = uri.scheme?.trim()?.lowercase(Locale.US).orEmpty()
|
||||
val tls =
|
||||
when (scheme) {
|
||||
"ws", "http" -> false
|
||||
"wss", "https" -> true
|
||||
else -> true
|
||||
}
|
||||
val port = uri.port.takeIf { it in 1..65535 } ?: 18789
|
||||
val displayUrl = "${if (tls) "https" else "http"}://$host:$port"
|
||||
|
||||
return ParsedGateway(host = host, port = port, tls = tls, displayUrl = displayUrl)
|
||||
}
|
||||
|
||||
private fun decodeSetupCode(rawInput: String): SetupCodePayload? {
|
||||
val trimmed = rawInput.trim()
|
||||
if (trimmed.isEmpty()) return null
|
||||
|
||||
val padded =
|
||||
trimmed
|
||||
.replace('-', '+')
|
||||
.replace('_', '/')
|
||||
.let { normalized ->
|
||||
val remainder = normalized.length % 4
|
||||
if (remainder == 0) normalized else normalized + "=".repeat(4 - remainder)
|
||||
}
|
||||
|
||||
return try {
|
||||
val decoded = String(Base64.decode(padded, Base64.DEFAULT), Charsets.UTF_8)
|
||||
val obj = JSONObject(decoded)
|
||||
val url = obj.optString("url").trim()
|
||||
if (url.isEmpty()) return null
|
||||
val token = obj.optString("token").trim().ifEmpty { null }
|
||||
val password = obj.optString("password").trim().ifEmpty { null }
|
||||
SetupCodePayload(url = url, token = token, password = password)
|
||||
} catch (_: Throwable) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun isPermissionGranted(context: Context, permission: String): Boolean {
|
||||
return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
private fun composeManualGatewayUrl(hostInput: String, portInput: String, tls: Boolean): String? {
|
||||
val host = hostInput.trim()
|
||||
val port = portInput.trim().toIntOrNull() ?: return null
|
||||
if (host.isEmpty() || port !in 1..65535) return null
|
||||
val scheme = if (tls) "https" else "http"
|
||||
return "$scheme://$host:$port"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user