android: connection steps on Home as in the Final 2026 mockup, read from the daemon log
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
a08c193a90
commit
92e2f4f069
5 files changed
+125
-2
No files matched your search
+10
@@ -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<CertExpiry?>(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) } },
|
||||
|
||||
+85
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+18
-2
@@ -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 ->
|
||||
|
||||
@@ -1123,4 +1123,10 @@
|
||||
<string name="battery_check_again">Проверить снова</string>
|
||||
<string name="consent_hint_title">Сейчас Android спросит разрешение</string>
|
||||
<string name="consent_hint_text">Это стандартное окно для любого VPN. Нажмите «ОК» — без этого подключиться нельзя.</string>
|
||||
<string name="connect_step_network">Сеть</string>
|
||||
<string name="connect_step_server">Сервер ответил</string>
|
||||
<string name="connect_step_auth">Проверка входа</string>
|
||||
<string name="connect_step_tunnel">Туннель и маршруты</string>
|
||||
<string name="home_connect_step">%1$s · шаг %2$d из 4</string>
|
||||
<string name="home_connecting">Подключаемся…</string>
|
||||
</resources>
|
||||
@@ -1109,4 +1109,10 @@
|
||||
<string name="battery_check_again">Check again</string>
|
||||
<string name="consent_hint_title">Android will now ask for permission</string>
|
||||
<string name="consent_hint_text">This is the standard window for any VPN. Tap «OK» — without it the VPN cannot connect.</string>
|
||||
<string name="connect_step_network">Network</string>
|
||||
<string name="connect_step_server">Server answered</string>
|
||||
<string name="connect_step_auth">Sign-in check</string>
|
||||
<string name="connect_step_tunnel">Tunnel and routes</string>
|
||||
<string name="home_connect_step">%1$s · step %2$d of 4</string>
|
||||
<string name="home_connecting">Connecting…</string>
|
||||
</resources>
|
||||
Reference in new issue
Block a user