android: routing - advanced block (RU details, GeoIP, force prefixes), kill switch vs excluded apps note

This commit is contained in:
Denozordec committed 2026-10-10 00:07:12 +07:00
1 parent c5585df135
commit 8bd65d535e
4 files changed
+108 -50

No files matched your search

@@ -61,6 +61,12 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearWavyProgressIndicator
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.clickable
import androidx.compose.material.icons.rounded.ExpandLess
import androidx.compose.material.icons.rounded.ExpandMore
import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.semantics
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
@@ -119,6 +125,7 @@ import org.strongswan.android.ui.compose.components.ShxSection
import org.strongswan.android.ui.compose.components.ShxSectionDivider
import org.strongswan.android.ui.compose.theme.LocalShxStatusColors
import org.strongswan.android.ui.compose.theme.ScreenPadding
import org.strongswan.android.ui.compose.theme.ShxSpacing
@Composable
fun RoutingScreen()
@@ -246,6 +253,19 @@ private fun RoutingContent(snackbarHostState: SnackbarHostState)
modifier = Modifier.padding(horizontal = ScreenPadding),
)
/* kill switch + excluded apps: excluded apps are added to the VPN's disallowed list
* (CharonVpnService.addDisallowedApplication), so the system lockdown does not
* cover them - on a tunnel drop they keep using the normal network */
if (config.enabled && excludedApps > 0 && remember { context.isVpnLockdownEnabled() })
{
Text(
text = stringResource(R.string.routing_killswitch_note, excludedApps),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = ScreenPadding + ShxSpacing.xs, vertical = ShxSpacing.sm),
)
}
/* ---- dead direct destinations the engine keeps refusing ---- */
val deadTargets by produceState(initialValue = emptyList<DeadTargetUi>())
{
@@ -322,55 +342,6 @@ private fun RoutingContent(snackbarHostState: SnackbarHostState)
onCheckedChange = { ruList.setEnabled(it) },
onUpdate = { refreshGeoip() },
)
if (ruState.enabled)
{
ShxSectionDivider()
RuModeSelector(
mode = ruState.mode,
onModeChange = { ruList.setMode(it) },
)
if (ruState.status == RuIpListStatus.UPDATING)
{
LinearWavyProgressIndicator(
Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 4.dp),
)
}
GeoipChipsRow(state = ruState)
RuDetailsDisclosure(mode = ruState.mode)
}
}
/* ---- force-tunnel prefixes: always through the tunnel ---- */
ShxSection(title = stringResource(R.string.routing_force_section))
{
if (config.forceTunnelPrefixes.isEmpty())
{
Text(
text = stringResource(R.string.routing_force_summary),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp),
)
}
else
{
config.forceTunnelPrefixes.forEachIndexed { index, entry ->
ForcePrefixRow(
entry = entry,
onToggle = { manager.setForcePrefixEnabled(entry.id, it) },
onEdit = { forceDialogId = entry.id },
onDelete = { manager.deleteForcePrefix(entry.id) },
)
if (index < config.forceTunnelPrefixes.lastIndex)
{
ShxSectionDivider()
}
}
}
ShxSectionDivider()
AddForcePrefixRow(onClick = { forceDialogId = "" })
}
/* ---- rule sets ---- */
@@ -410,6 +381,89 @@ private fun RoutingContent(snackbarHostState: SnackbarHostState)
AddRuleRow(onClick = { addNewSet() })
}
}
/* ---- advanced: userspace mode, GeoIP list, force-tunnel prefixes ---- */
var advancedOpen by rememberSaveable { mutableStateOf(false) }
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clickable { advancedOpen = !advancedOpen }
.padding(horizontal = ScreenPadding + 16.dp, vertical = 16.dp),
)
{
Text(
text = stringResource(R.string.routing_advanced).uppercase(),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.weight(1f).semantics { heading() },
)
Icon(
if (advancedOpen) Icons.Rounded.ExpandLess else Icons.Rounded.ExpandMore,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
AnimatedVisibility(visible = advancedOpen)
{
Column {
if (ruState.enabled)
{
ShxSection(title = stringResource(R.string.routing_section_ru))
{
if (ruState.enabled)
{
RuModeSelector(
mode = ruState.mode,
onModeChange = { ruList.setMode(it) },
)
if (ruState.status == RuIpListStatus.UPDATING)
{
LinearWavyProgressIndicator(
Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 4.dp),
)
}
GeoipChipsRow(state = ruState)
RuDetailsDisclosure(mode = ruState.mode)
}
}
}
/* ---- force-tunnel prefixes: always through the tunnel ---- */
ShxSection(title = stringResource(R.string.routing_force_section))
{
if (config.forceTunnelPrefixes.isEmpty())
{
Text(
text = stringResource(R.string.routing_force_summary),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp),
)
}
else
{
config.forceTunnelPrefixes.forEachIndexed { index, entry ->
ForcePrefixRow(
entry = entry,
onToggle = { manager.setForcePrefixEnabled(entry.id, it) },
onEdit = { forceDialogId = entry.id },
onDelete = { manager.deleteForcePrefix(entry.id) },
)
if (index < config.forceTunnelPrefixes.lastIndex)
{
ShxSectionDivider()
}
}
}
ShxSectionDivider()
AddForcePrefixRow(onClick = { forceDialogId = "" })
}
}
}
}
/* ---- rule set editor, hosted outside the scrolling column ---- */
@@ -612,7 +612,7 @@ private fun updateStatusText(state: UpdateState): String
* Checks whether the system-level lockdown ("Block connections without VPN") is
* active for our package. Returns false if not determinable.
*/
private fun Context.isVpnLockdownEnabled(): Boolean
internal fun Context.isVpnLockdownEnabled(): Boolean
{
return try
{
@@ -491,4 +491,6 @@
<string name="home_manage_profiles">Управление профилями</string>
<string name="availability_expand">Показать детали</string>
<string name="availability_collapse">Скрыть детали</string>
<string name="routing_advanced">Дополнительно</string>
<string name="routing_killswitch_note">Исключённые приложения (%1$d) всегда идут мимо VPN, в том числе при обрыве туннеля: kill switch их не блокирует.</string>
</resources>
@@ -496,4 +496,6 @@
<string name="home_manage_profiles">Manage profiles</string>
<string name="availability_expand">Show details</string>
<string name="availability_collapse">Hide details</string>
<string name="routing_advanced">Advanced</string>
<string name="routing_killswitch_note">%1$d excluded app(s) always use the normal network, even when the tunnel drops: the kill switch does not block them.</string>
</resources>