android: connection steps on Home as in the Final 2026 mockup, read from the daemon log
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.certExpiry
|
import org.strongswan.android.ui.compose.home.certExpiry
|
||||||
import org.strongswan.android.ui.compose.home.snoozeCertExpiry
|
import org.strongswan.android.ui.compose.home.snoozeCertExpiry
|
||||||
|
import org.strongswan.android.ui.compose.home.connectProgress
|
||||||
import org.strongswan.android.R
|
import org.strongswan.android.R
|
||||||
import androidx.compose.foundation.layout.widthIn
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import org.strongswan.android.logic.routing.RuIpListManager
|
import org.strongswan.android.logic.routing.RuIpListManager
|
||||||
@@ -255,6 +256,14 @@ fun HomeScreen(
|
|||||||
val certExpiry by produceState<CertExpiry?>(null, certAlias, certSnoozes) {
|
val certExpiry by produceState<CertExpiry?>(null, certAlias, certSnoozes) {
|
||||||
value = withContext(Dispatchers.IO) { certExpiry(context, certAlias) }
|
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(
|
val model = HomeModel(
|
||||||
state = state,
|
state = state,
|
||||||
traffic = traffic,
|
traffic = traffic,
|
||||||
@@ -270,6 +279,7 @@ fun HomeScreen(
|
|||||||
unread = unreadEvents,
|
unread = unreadEvents,
|
||||||
findingFastest = findingFastest,
|
findingFastest = findingFastest,
|
||||||
certExpiry = certExpiry,
|
certExpiry = certExpiry,
|
||||||
|
connectStep = connectStep,
|
||||||
)
|
)
|
||||||
val actions = HomeActions(
|
val actions = HomeActions(
|
||||||
connect = { selected?.let { viewModel.connect(context, it) } },
|
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 unread: Int,
|
||||||
val findingFastest: Boolean,
|
val findingFastest: Boolean,
|
||||||
val certExpiry: CertExpiry? = null,
|
val certExpiry: CertExpiry? = null,
|
||||||
|
/* finished steps of the running connection (F_HomeConnecting) */
|
||||||
|
val connectStep: Int = 1,
|
||||||
)
|
)
|
||||||
|
|
||||||
/** What Home can do. */
|
/** 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)
|
HeroBlob(heroLookConnecting(), ShxIcons.Power, stringResource(R.string.home_cancel_connect), a.disconnect)
|
||||||
HeroCaption(
|
HeroCaption(
|
||||||
stringResource(if (connecting) R.string.status_connecting else R.string.state_disconnecting),
|
stringResource(if (connecting) R.string.home_connecting else R.string.state_disconnecting),
|
||||||
name,
|
if (connecting) stringResource(R.string.home_connect_step, name, (m.connectStep + 1).coerceAtMost(4)) else name,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else ->
|
else ->
|
||||||
@@ -323,6 +325,20 @@ internal fun HomeContent(m: HomeModel, a: HomeActions)
|
|||||||
connecting || s.vpnState == State.DISCONNECTING ->
|
connecting || s.vpnState == State.DISCONNECTING ->
|
||||||
{
|
{
|
||||||
server()
|
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())
|
ShxButton(stringResource(R.string.home_cancel), a.disconnect, style = ShxButtonStyle.TONAL, height = 48.dp, modifier = Modifier.fillMaxWidth())
|
||||||
}
|
}
|
||||||
else ->
|
else ->
|
||||||
|
|||||||
@@ -1123,4 +1123,10 @@
|
|||||||
<string name="battery_check_again">Проверить снова</string>
|
<string name="battery_check_again">Проверить снова</string>
|
||||||
<string name="consent_hint_title">Сейчас Android спросит разрешение</string>
|
<string name="consent_hint_title">Сейчас Android спросит разрешение</string>
|
||||||
<string name="consent_hint_text">Это стандартное окно для любого VPN. Нажмите «ОК» — без этого подключиться нельзя.</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>
|
</resources>
|
||||||
@@ -1109,4 +1109,10 @@
|
|||||||
<string name="battery_check_again">Check again</string>
|
<string name="battery_check_again">Check again</string>
|
||||||
<string name="consent_hint_title">Android will now ask for permission</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="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>
|
</resources>
|
||||||
Reference in new issue
Block a user