From 92e2f4f069fd1ca2048c4d44170d21a555d8f7fc Mon Sep 17 00:00:00 2001 From: Denozordec Date: Mon, 12 Oct 2026 14:39:26 +0700 Subject: [PATCH] android: connection steps on Home as in the Final 2026 mockup, read from the daemon log Co-Authored-By: Claude Opus 5.5 --- .../android/ui/compose/HomeScreen.kt | 10 +++ .../android/ui/compose/home/ConnectSteps.kt | 85 +++++++++++++++++++ .../android/ui/compose/home/HomeContent.kt | 20 ++++- .../app/src/main/res/values-ru/strings.xml | 6 ++ .../app/src/main/res/values/strings.xml | 6 ++ 5 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/home/ConnectSteps.kt diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/HomeScreen.kt b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/HomeScreen.kt index e6734bd83..99af0d24a 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/HomeScreen.kt +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/HomeScreen.kt @@ -124,6 +124,7 @@ import kotlinx.coroutines.withContext import org.strongswan.android.ui.compose.home.CertExpiry import org.strongswan.android.ui.compose.home.certExpiry import org.strongswan.android.ui.compose.home.snoozeCertExpiry +import org.strongswan.android.ui.compose.home.connectProgress import org.strongswan.android.R import androidx.compose.foundation.layout.widthIn import org.strongswan.android.logic.routing.RuIpListManager @@ -255,6 +256,14 @@ fun HomeScreen( val certExpiry by produceState(null, certAlias, certSnoozes) { value = withContext(Dispatchers.IO) { certExpiry(context, certAlias) } } + val connectingNow = state.vpnState == State.CONNECTING + val connectStep by produceState(1, connectingNow) { + while (connectingNow) + { + value = withContext(Dispatchers.IO) { connectProgress(context) } + kotlinx.coroutines.delay(600) + } + } val model = HomeModel( state = state, traffic = traffic, @@ -270,6 +279,7 @@ fun HomeScreen( unread = unreadEvents, findingFastest = findingFastest, certExpiry = certExpiry, + connectStep = connectStep, ) val actions = HomeActions( connect = { selected?.let { viewModel.connect(context, it) } }, diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/home/ConnectSteps.kt b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/home/ConnectSteps.kt new file mode 100644 index 000000000..4e8c6de7e --- /dev/null +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/home/ConnectSteps.kt @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2026 SHX VPN + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + */ + +package org.strongswan.android.ui.compose.home + +import android.content.Context +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import org.strongswan.android.R +import org.strongswan.android.logic.CharonVpnService +import org.strongswan.android.ui.compose.theme.LocalShxColors +import java.io.File +import java.io.RandomAccessFile + +/** + * How far the running connection got (F_HomeConnecting), read from the + * daemon log, which starts fresh for every connection: + * 1 the network is there, 2 the server answered, 3 sign-in passed, + * 4 the tunnel is up. Returns the number of finished steps. + */ +internal fun connectProgress(context: Context): Int +{ + val text = tail(File(context.filesDir, CharonVpnService.LOG_FILE)) ?: return 1 + var done = 1 + if (text.contains("received packet")) + { + done = 2 + } + if (Regex("authentication of .* successful").containsMatchIn(text) || Regex("IKE_SA .* established").containsMatchIn(text)) + { + done = 3 + } + if (Regex("CHILD_SA .* established").containsMatchIn(text)) + { + done = 4 + } + return done +} + +/* the last 64 KiB are plenty for one connection attempt */ +private fun tail(file: File): String? +{ + if (!file.exists()) + { + return null + } + return runCatching { + RandomAccessFile(file, "r").use { f -> + val start = (f.length() - 65536).coerceAtLeast(0) + f.seek(start) + val bytes = ByteArray((f.length() - start).toInt()) + f.readFully(bytes) + String(bytes, Charsets.UTF_8) + } + }.getOrNull() +} + +/** The four steps: ✓ done, … current (purple), empty grey ones ahead. */ +@Composable +internal fun ConnectStepsPanel(done: Int, network: String, serverRtt: String) +{ + val c = LocalShxColors.current + val steps = listOf( + stringResource(R.string.connect_step_network) to network, + stringResource(R.string.connect_step_server) to serverRtt, + stringResource(R.string.connect_step_auth) to "", + stringResource(R.string.connect_step_tunnel) to "", + ) + CheckPanel { + steps.forEachIndexed { i, (title, value) -> + when + { + i < done -> CheckLine("✓", c.successContainer, c.onSuccess, title, value) + i == done -> CheckLine("…", c.accent, androidx.compose.ui.graphics.Color.White, title, "", strong = true) + else -> CheckLine("", c.tile, c.iconMuted, title, "", muted = true) + } + } + } +} diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/home/HomeContent.kt b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/home/HomeContent.kt index ceb59465e..f26d04ed1 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/home/HomeContent.kt +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/home/HomeContent.kt @@ -62,6 +62,8 @@ internal data class HomeModel( val unread: Int, val findingFastest: Boolean, val certExpiry: CertExpiry? = null, + /* finished steps of the running connection (F_HomeConnecting) */ + val connectStep: Int = 1, ) /** What Home can do. */ @@ -187,8 +189,8 @@ internal fun HomeContent(m: HomeModel, a: HomeActions) { HeroBlob(heroLookConnecting(), ShxIcons.Power, stringResource(R.string.home_cancel_connect), a.disconnect) HeroCaption( - stringResource(if (connecting) R.string.status_connecting else R.string.state_disconnecting), - name, + stringResource(if (connecting) R.string.home_connecting else R.string.state_disconnecting), + if (connecting) stringResource(R.string.home_connect_step, name, (m.connectStep + 1).coerceAtMost(4)) else name, ) } else -> @@ -323,6 +325,20 @@ internal fun HomeContent(m: HomeModel, a: HomeActions) connecting || s.vpnState == State.DISCONNECTING -> { server() + if (connecting) + { + val (wifi, cell) = m.networkKinds + ConnectStepsPanel( + m.connectStep, + network = when + { + wifi -> stringResource(R.string.network_wifi) + cell -> stringResource(R.string.network_cellular) + else -> "" + }, + serverRtt = if (m.connectStep >= 2) m.serverRttMs?.let { stringResource(R.string.diag_ms, it.toInt()) }.orEmpty() else "", + ) + } ShxButton(stringResource(R.string.home_cancel), a.disconnect, style = ShxButtonStyle.TONAL, height = 48.dp, modifier = Modifier.fillMaxWidth()) } else -> diff --git a/src/frontends/android/app/src/main/res/values-ru/strings.xml b/src/frontends/android/app/src/main/res/values-ru/strings.xml index c7fb31f43..c16707033 100644 --- a/src/frontends/android/app/src/main/res/values-ru/strings.xml +++ b/src/frontends/android/app/src/main/res/values-ru/strings.xml @@ -1123,4 +1123,10 @@ Проверить снова Сейчас Android спросит разрешение Это стандартное окно для любого VPN. Нажмите «ОК» — без этого подключиться нельзя. + Сеть + Сервер ответил + Проверка входа + Туннель и маршруты + %1$s · шаг %2$d из 4 + Подключаемся… diff --git a/src/frontends/android/app/src/main/res/values/strings.xml b/src/frontends/android/app/src/main/res/values/strings.xml index 607af9f1c..478bc3486 100644 --- a/src/frontends/android/app/src/main/res/values/strings.xml +++ b/src/frontends/android/app/src/main/res/values/strings.xml @@ -1109,4 +1109,10 @@ Check again Android will now ask for permission This is the standard window for any VPN. Tap «OK» — without it the VPN cannot connect. + Network + Server answered + Sign-in check + Tunnel and routes + %1$s · step %2$d of 4 + Connecting…