From 8892a1cd45f7793e8a1945b657a210f13fdfbdb7 Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Tue, 24 Feb 2026 22:11:11 +0530 Subject: [PATCH] refactor(android-ui): unify gateway config resolution paths --- .../openclaw/android/ui/ConnectTabScreen.kt | 125 +----------------- .../android/ui/GatewayConfigResolver.kt | 115 ++++++++++++++++ .../ai/openclaw/android/ui/OnboardingFlow.kt | 91 ++----------- 3 files changed, 130 insertions(+), 201 deletions(-) create mode 100644 apps/android/app/src/main/java/ai/openclaw/android/ui/GatewayConfigResolver.kt 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 24336849d50..9f7cf2211a1 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 @@ -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" -} diff --git a/apps/android/app/src/main/java/ai/openclaw/android/ui/GatewayConfigResolver.kt b/apps/android/app/src/main/java/ai/openclaw/android/ui/GatewayConfigResolver.kt new file mode 100644 index 00000000000..5036c6290d3 --- /dev/null +++ b/apps/android/app/src/main/java/ai/openclaw/android/ui/GatewayConfigResolver.kt @@ -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" +} 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 780781455be..8c732d9c360 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 @@ -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" -}