feat: global config overrides (#983)

This commit is contained in:
Zane Schepke
2025-09-30 12:14:09 -04:00
committed by GitHub
parent b7e4f3c3e5
commit 00c2c2ac20
34 changed files with 1119 additions and 264 deletions
@@ -0,0 +1,371 @@
{
"formatVersion": 1,
"database": {
"version": 23,
"identityHash": "c94fe51e6c318edf8bda81ab854c85e5",
"entities": [
{
"tableName": "Settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL DEFAULT 0, `trusted_network_ssids` TEXT NOT NULL DEFAULT '', `is_always_on_vpn_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL DEFAULT 0, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT 0, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT 0, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `is_ping_enabled` INTEGER NOT NULL DEFAULT 0, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT 0, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT 0, `is_lan_on_kill_switch_enabled` INTEGER NOT NULL DEFAULT 0, `debounce_delay_seconds` INTEGER NOT NULL DEFAULT 3, `is_disable_kill_switch_on_trusted_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_unsecure_enabled` INTEGER NOT NULL DEFAULT 0, `wifi_detection_method` INTEGER NOT NULL DEFAULT 0, `is_ping_monitoring_enabled` INTEGER NOT NULL DEFAULT 1, `tunnel_ping_interval_sec` INTEGER NOT NULL DEFAULT 30, `tunnel_ping_attempts` INTEGER NOT NULL DEFAULT 3, `tunnel_ping_timeout_sec` INTEGER, `app_mode` INTEGER NOT NULL DEFAULT 0, `dns_protocol` INTEGER NOT NULL DEFAULT 0, `dns_endpoint` TEXT, `is_tunnel_globals_enabled` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isAutoTunnelEnabled",
"columnName": "is_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isTunnelOnMobileDataEnabled",
"columnName": "is_tunnel_on_mobile_data_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "trustedNetworkSSIDs",
"columnName": "trusted_network_ssids",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isAlwaysOnVpnEnabled",
"columnName": "is_always_on_vpn_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isTunnelOnEthernetEnabled",
"columnName": "is_tunnel_on_ethernet_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isShortcutsEnabled",
"columnName": "is_shortcuts_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isTunnelOnWifiEnabled",
"columnName": "is_tunnel_on_wifi_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isRestoreOnBootEnabled",
"columnName": "is_restore_on_boot_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isMultiTunnelEnabled",
"columnName": "is_multi_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isWildcardsEnabled",
"columnName": "is_wildcards_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isStopOnNoInternetEnabled",
"columnName": "is_stop_on_no_internet_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isLanOnKillSwitchEnabled",
"columnName": "is_lan_on_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "debounceDelaySeconds",
"columnName": "debounce_delay_seconds",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "3"
},
{
"fieldPath": "isDisableKillSwitchOnTrustedEnabled",
"columnName": "is_disable_kill_switch_on_trusted_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isTunnelOnUnsecureEnabled",
"columnName": "is_tunnel_on_unsecure_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "wifiDetectionMethod",
"columnName": "wifi_detection_method",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isPingMonitoringEnabled",
"columnName": "is_ping_monitoring_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "1"
},
{
"fieldPath": "tunnelPingIntervalSeconds",
"columnName": "tunnel_ping_interval_sec",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "30"
},
{
"fieldPath": "tunnelPingAttempts",
"columnName": "tunnel_ping_attempts",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "3"
},
{
"fieldPath": "tunnelPingTimeoutSeconds",
"columnName": "tunnel_ping_timeout_sec",
"affinity": "INTEGER"
},
{
"fieldPath": "appMode",
"columnName": "app_mode",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "dnsProtocol",
"columnName": "dns_protocol",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "dnsEndpoint",
"columnName": "dns_endpoint",
"affinity": "TEXT"
},
{
"fieldPath": "isTunnelGlobalsEnabled",
"columnName": "is_tunnel_globals_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "TunnelConfig",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false, `is_primary_tunnel` INTEGER NOT NULL DEFAULT false, `am_quick` TEXT NOT NULL DEFAULT '', `is_Active` INTEGER NOT NULL DEFAULT false, `restart_on_ping_failure` INTEGER NOT NULL DEFAULT false, `ping_target` TEXT DEFAULT null, `is_ethernet_tunnel` INTEGER NOT NULL DEFAULT false, `is_ipv4_preferred` INTEGER NOT NULL DEFAULT true, `position` INTEGER NOT NULL DEFAULT 0, `auto_tunnel_apps` TEXT NOT NULL DEFAULT '[]')",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "wgQuick",
"columnName": "wg_quick",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tunnelNetworks",
"columnName": "tunnel_networks",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isMobileDataTunnel",
"columnName": "is_mobile_data_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPrimaryTunnel",
"columnName": "is_primary_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "amQuick",
"columnName": "am_quick",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isActive",
"columnName": "is_Active",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "restartOnPingFailure",
"columnName": "restart_on_ping_failure",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "pingTarget",
"columnName": "ping_target",
"affinity": "TEXT",
"defaultValue": "null"
},
{
"fieldPath": "isEthernetTunnel",
"columnName": "is_ethernet_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isIpv4Preferred",
"columnName": "is_ipv4_preferred",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "true"
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "autoTunnelApps",
"columnName": "auto_tunnel_apps",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'[]'"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_TunnelConfig_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
}
]
},
{
"tableName": "proxy_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `socks5_proxy_enabled` INTEGER NOT NULL DEFAULT 0, `socks5_proxy_bind_address` TEXT, `http_proxy_enable` INTEGER NOT NULL DEFAULT 0, `http_proxy_bind_address` TEXT, `proxy_username` TEXT, `proxy_password` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "socks5ProxyEnabled",
"columnName": "socks5_proxy_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "socks5ProxyBindAddress",
"columnName": "socks5_proxy_bind_address",
"affinity": "TEXT"
},
{
"fieldPath": "httpProxyEnabled",
"columnName": "http_proxy_enable",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "httpProxyBindAddress",
"columnName": "http_proxy_bind_address",
"affinity": "TEXT"
},
{
"fieldPath": "proxyUsername",
"columnName": "proxy_username",
"affinity": "TEXT"
},
{
"fieldPath": "proxyPassword",
"columnName": "proxy_password",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c94fe51e6c318edf8bda81ab854c85e5')"
]
}
}
@@ -63,6 +63,7 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.Appear
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.display.DisplayScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.language.LanguageScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.dns.DnsSettingsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.globals.TunnelGlobalsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.logs.LogsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.TunnelMonitoringScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.system.SystemFeaturesScreen
@@ -304,11 +305,7 @@ class MainActivity : AppCompatActivity() {
}
composable<Route.Config> { backStackEntry ->
val args = backStackEntry.toRoute<Route.Config>()
val viewModel =
backStackEntry.sharedViewModel<TunnelsViewModel>(
navController
)
ConfigScreen(args.id, viewModel)
ConfigScreen(args.id)
}
}
@@ -368,6 +365,19 @@ class MainActivity : AppCompatActivity() {
it.sharedViewModel<SettingsViewModel>(navController)
DnsSettingsScreen(viewModel)
}
composable<Route.TunnelGlobals> { backStackEntry ->
val args = backStackEntry.toRoute<Route.TunnelGlobals>()
TunnelGlobalsScreen(args.id)
}
composable<Route.ConfigGlobal> { backStackEntry ->
val args = backStackEntry.toRoute<Route.ConfigGlobal>()
ConfigScreen(args.id)
}
composable<Route.SplitTunnelGlobal> { backStackEntry ->
val args =
backStackEntry.toRoute<Route.SplitTunnelGlobal>()
SplitTunnelScreen(args.id)
}
composable<Route.ProxySettings> { ProxySettingsScreen() }
composable<Route.Appearance> { AppearanceScreen() }
composable<Route.Language> { LanguageScreen() }
@@ -1,6 +1,7 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.di.*
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
@@ -57,6 +58,28 @@ constructor(
val condition: (SideEffectState) -> Boolean,
)
private val settings: StateFlow<GeneralSettings> =
settingsRepository.flow
.filterNotNull()
.stateIn(
scope = applicationScope.plus(ioDispatcher),
started = SharingStarted.Eagerly,
initialValue = GeneralSettings(),
)
private val tunnels: StateFlow<List<TunnelConf>> =
tunnelsRepository.flow.stateIn(
scope = applicationScope.plus(ioDispatcher),
started = SharingStarted.Eagerly,
initialValue = emptyList(),
)
private suspend fun getSettings(): GeneralSettings =
settingsRepository.flow.filterNotNull().first { it != GeneralSettings() }
private suspend fun getTunnels(): List<TunnelConf> =
tunnelsRepository.flow.first { it.isNotEmpty() }
private val tunnelProviderFlow: StateFlow<TunnelProvider> = run {
val currentBackend = AtomicReference(userspaceTunnel)
val currentSettings = AtomicReference(GeneralSettings())
@@ -216,7 +239,17 @@ constructor(
// for VPN Mode, we need to stop active tunnels as we can only have one active at a time
if (activeTunnels.value.isNotEmpty() && tunnelProviderFlow.value == userspaceTunnel)
stopActiveTunnels()
tunnelProviderFlow.value.startTunnel(tunnelConf)
val runConfig =
tunnelConf.run {
if (getSettings().isTunnelGlobalsEnabled) {
val globalTunnel =
getTunnels().firstOrNull { it.tunName == TunnelConfig.GLOBAL_CONFIG_NAME }
?: return@run this
return@run copyWithGlobalValues(globalTunnel)
}
this
}
tunnelProviderFlow.value.startTunnel(runConfig)
}
override suspend fun stopTunnel(tunnelId: Int) {
@@ -12,7 +12,7 @@ import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
@Database(
entities = [Settings::class, TunnelConfig::class, ProxySettings::class],
version = 22,
version = 23,
autoMigrations =
[
AutoMigration(from = 1, to = 2),
@@ -36,6 +36,7 @@ import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
AutoMigration(from = 19, to = 20, spec = ProxyMigration::class),
AutoMigration(from = 20, to = 21, spec = FixProxySettingsMigration::class),
AutoMigration(from = 21, to = 22),
AutoMigration(from = 22, to = 23),
],
exportSchema = true,
)
@@ -71,4 +71,12 @@ interface TunnelConfigDao {
@Query("SELECT * FROM tunnelconfig ORDER BY position")
fun getAllFlow(): Flow<List<TunnelConfig>>
@Query("SELECT * FROM TunnelConfig WHERE name != :globalName")
fun getAllTunnelsExceptGlobal(
globalName: String = TunnelConfig.GLOBAL_CONFIG_NAME
): Flow<List<TunnelConfig>>
@Query("SELECT * FROM TunnelConfig WHERE name = :globalName LIMIT 1")
fun getGlobalTunnel(globalName: String = TunnelConfig.GLOBAL_CONFIG_NAME): Flow<TunnelConfig?>
}
@@ -53,4 +53,6 @@ data class Settings(
@ColumnInfo(name = "dns_protocol", defaultValue = "0")
val dnsProtocol: DnsProtocol = DnsProtocol.fromValue(0),
@ColumnInfo(name = "dns_endpoint") val dnsEndpoint: String? = null,
@ColumnInfo(name = "is_tunnel_globals_enabled", defaultValue = "0")
val isTunnelGlobalsEnabled: Boolean = false,
)
@@ -32,5 +32,6 @@ data class TunnelConfig(
companion object {
const val AM_QUICK_DEFAULT = ""
const val GLOBAL_CONFIG_NAME = "4675ab06-903a-438b-8485-6ea4187a9512"
}
}
@@ -32,6 +32,7 @@ fun Settings.toAppSettings(): GeneralSettings {
appMode = appMode,
dnsProtocol = dnsProtocol,
dnsEndpoint = dnsEndpoint,
isTunnelGlobalsEnabled = isTunnelGlobalsEnabled,
)
}
@@ -61,6 +62,7 @@ fun GeneralSettings.toSettings(): Settings {
appMode = appMode,
dnsProtocol = dnsProtocol,
dnsEndpoint = dnsEndpoint,
isTunnelGlobalsEnabled = isTunnelGlobalsEnabled,
)
}
@@ -7,6 +7,7 @@ import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.util.extensions.Tunnels
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
@@ -21,6 +22,16 @@ class RoomTunnelRepository(
it.map(TunnelConfigMapper::toTunnelConf)
}
override val userTunnelsFlow =
tunnelConfigDao.getAllTunnelsExceptGlobal().flowOn(ioDispatcher).map {
it.map(TunnelConfigMapper::toTunnelConf)
}
override val globalTunnelFlow: Flow<TunnelConf?> =
tunnelConfigDao.getGlobalTunnel().flowOn(ioDispatcher).map {
it?.let(TunnelConfigMapper::toTunnelConf)
}
override suspend fun getAll(): Tunnels {
return withContext(ioDispatcher) {
tunnelConfigDao.getAll().map(TunnelConfigMapper::toTunnelConf)
@@ -31,6 +31,7 @@ data class GeneralSettings(
val appMode: AppMode = AppMode.VPN,
val dnsProtocol: DnsProtocol = DnsProtocol.SYSTEM,
val dnsEndpoint: String? = null,
val isTunnelGlobalsEnabled: Boolean = false,
) {
fun toAutoTunnelStateString(): String {
return """
@@ -1,10 +1,16 @@
package com.zaneschepke.wireguardautotunnel.domain.model
import com.wireguard.config.Config
import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig.Companion.GLOBAL_CONFIG_NAME
import com.zaneschepke.wireguardautotunnel.util.extensions.defaultName
import com.zaneschepke.wireguardautotunnel.util.extensions.isValidIpv4orIpv6Address
import java.io.InputStream
import java.nio.charset.StandardCharsets
import org.amnezia.awg.config.InetEndpoint
import org.amnezia.awg.config.InetNetwork
import org.amnezia.awg.config.Interface
import org.amnezia.awg.config.Peer
import org.amnezia.awg.crypto.KeyPair
data class TunnelConf(
val id: Int = 0,
@@ -60,6 +66,127 @@ data class TunnelConf(
return configFromWgQuick(wgQuick)
}
fun copyWithGlobalValues(globalTunnel: TunnelConf): TunnelConf {
val existingConfig = toAmConfig()
val globalConfig = globalTunnel.toAmConfig()
val newInterfaceBuilder =
Interface.Builder().apply {
setKeyPair(existingConfig.`interface`.keyPair)
setAddresses(existingConfig.`interface`.addresses)
setDnsServers(existingConfig.`interface`.dnsServers)
setDnsSearchDomains(existingConfig.`interface`.dnsSearchDomains)
setExcludedApplications(existingConfig.`interface`.excludedApplications)
setIncludedApplications(existingConfig.`interface`.includedApplications)
existingConfig.`interface`.listenPort.ifPresent { setListenPort(it) }
existingConfig.`interface`.mtu.ifPresent { setMtu(it) }
existingConfig.`interface`.junkPacketCount.ifPresent { setJunkPacketCount(it) }
existingConfig.`interface`.junkPacketMinSize.ifPresent { setJunkPacketMinSize(it) }
existingConfig.`interface`.junkPacketMaxSize.ifPresent { setJunkPacketMaxSize(it) }
existingConfig.`interface`.initPacketJunkSize.ifPresent {
setInitPacketJunkSize(it)
}
existingConfig.`interface`.responsePacketJunkSize.ifPresent {
setResponsePacketJunkSize(it)
}
existingConfig.`interface`.initPacketMagicHeader.ifPresent {
setInitPacketMagicHeader(it)
}
existingConfig.`interface`.responsePacketMagicHeader.ifPresent {
setResponsePacketMagicHeader(it)
}
existingConfig.`interface`.underloadPacketMagicHeader.ifPresent {
setUnderloadPacketMagicHeader(it)
}
existingConfig.`interface`.transportPacketMagicHeader.ifPresent {
setTransportPacketMagicHeader(it)
}
existingConfig.`interface`.i1.ifPresent { setI1(it) }
existingConfig.`interface`.i2.ifPresent { setI2(it) }
existingConfig.`interface`.i3.ifPresent { setI3(it) }
existingConfig.`interface`.i4.ifPresent { setI4(it) }
existingConfig.`interface`.i5.ifPresent { setI5(it) }
existingConfig.`interface`.j1.ifPresent { setJ1(it) }
existingConfig.`interface`.j2.ifPresent { setJ2(it) }
existingConfig.`interface`.j3.ifPresent { setJ3(it) }
existingConfig.`interface`.itime.ifPresent { setItime(it) }
setPreUp(existingConfig.`interface`.preUp)
setPostUp(existingConfig.`interface`.postUp)
setPreDown(existingConfig.`interface`.preDown)
setPostDown(existingConfig.`interface`.postDown)
globalConfig.`interface`.mtu.ifPresent { setMtu(it) }
if (globalConfig.`interface`.dnsServers.isNotEmpty()) {
setDnsServers(globalConfig.`interface`.dnsServers)
}
if (globalConfig.`interface`.dnsSearchDomains.isNotEmpty()) {
setDnsSearchDomains(globalConfig.`interface`.dnsSearchDomains)
}
if (globalConfig.`interface`.excludedApplications.isNotEmpty()) {
setExcludedApplications(globalConfig.`interface`.excludedApplications)
}
if (!globalConfig.`interface`.includedApplications.isEmpty()) {
setIncludedApplications(globalConfig.`interface`.includedApplications)
}
if (globalConfig.`interface`.preUp.isNotEmpty()) {
setPreUp(globalConfig.`interface`.preUp)
}
if (globalConfig.`interface`.postUp.isNotEmpty()) {
setPostUp(globalConfig.`interface`.postUp)
}
if (globalConfig.`interface`.preDown.isNotEmpty()) {
setPreDown(globalConfig.`interface`.preDown)
}
if (globalConfig.`interface`.postDown.isNotEmpty()) {
setPostDown(globalConfig.`interface`.postDown)
}
globalConfig.`interface`.junkPacketCount.ifPresent { setJunkPacketCount(it) }
globalConfig.`interface`.junkPacketMinSize.ifPresent { setJunkPacketMinSize(it) }
globalConfig.`interface`.junkPacketMaxSize.ifPresent { setJunkPacketMaxSize(it) }
globalConfig.`interface`.initPacketJunkSize.ifPresent { setInitPacketJunkSize(it) }
globalConfig.`interface`.responsePacketJunkSize.ifPresent {
setResponsePacketJunkSize(it)
}
globalConfig.`interface`.initPacketMagicHeader.ifPresent {
setInitPacketMagicHeader(it)
}
globalConfig.`interface`.responsePacketMagicHeader.ifPresent {
setResponsePacketMagicHeader(it)
}
globalConfig.`interface`.underloadPacketMagicHeader.ifPresent {
setUnderloadPacketMagicHeader(it)
}
globalConfig.`interface`.transportPacketMagicHeader.ifPresent {
setTransportPacketMagicHeader(it)
}
globalConfig.`interface`.i1.ifPresent { setI1(it) }
globalConfig.`interface`.i2.ifPresent { setI2(it) }
globalConfig.`interface`.i3.ifPresent { setI3(it) }
globalConfig.`interface`.i4.ifPresent { setI4(it) }
globalConfig.`interface`.i5.ifPresent { setI5(it) }
globalConfig.`interface`.j1.ifPresent { setJ1(it) }
globalConfig.`interface`.j2.ifPresent { setJ2(it) }
globalConfig.`interface`.j3.ifPresent { setJ3(it) }
globalConfig.`interface`.itime.ifPresent { setItime(it) }
}
val newInterface = newInterfaceBuilder.build()
val newConfigBuilder =
org.amnezia.awg.config.Config.Builder().apply {
setInterface(newInterface)
addPeers(existingConfig.peers)
}
val newAmConfig = newConfigBuilder.build()
return copy(
wgQuick = newAmConfig.toWgQuickString(true),
amQuick = newAmConfig.toAwgQuickString(true, false),
)
}
companion object {
fun configFromWgQuick(wgQuick: String): Config {
val inputStream: InputStream = wgQuick.byteInputStream()
@@ -96,6 +223,37 @@ data class TunnelConf(
)
}
fun generateDefaultGlobalConfig(): TunnelConf {
val keyPair = KeyPair()
val config =
org.amnezia.awg.config.Config.Builder()
.apply {
setInterface(
Interface.Builder()
.apply {
setKeyPair(keyPair)
parseAddresses("10.0.0.2/32")
}
.build()
)
addPeer(
Peer.Builder()
.apply {
setPublicKey(keyPair.publicKey)
addAllowedIps(listOf(InetNetwork.parse("0.0.0.0/0")))
setEndpoint(InetEndpoint.parse("server.example.com:51820"))
}
.build()
)
}
.build()
return TunnelConf(
tunName = GLOBAL_CONFIG_NAME,
amQuick = config.toAwgQuickString(false, false),
wgQuick = config.toWgQuickString(false),
)
}
private const val IPV6_ALL_NETWORKS = "::/0"
private const val IPV4_ALL_NETWORKS = "0.0.0.0/0"
val ALL_IPS = listOf(IPV4_ALL_NETWORKS, IPV6_ALL_NETWORKS)
@@ -7,6 +7,10 @@ import kotlinx.coroutines.flow.Flow
interface TunnelRepository {
val flow: Flow<List<TunnelConf>>
val userTunnelsFlow: Flow<List<TunnelConf>>
val globalTunnelFlow: Flow<TunnelConf?>
suspend fun getAll(): Tunnels
suspend fun save(tunnelConf: TunnelConf)
@@ -37,6 +37,12 @@ sealed class Route {
@Keep @Serializable data class SplitTunnel(val id: Int) : Route()
@Keep @Serializable data class ConfigGlobal(val id: Int?) : Route()
@Keep @Serializable data class TunnelGlobals(val id: Int) : Route()
@Keep @Serializable data class SplitTunnelGlobal(val id: Int) : Route()
@Keep @Serializable data class TunnelAutoTunnel(val id: Int) : Route()
@Keep @Serializable data object Sort : Route()
@@ -1,6 +1,5 @@
package com.zaneschepke.wireguardautotunnel.ui.navigation.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.TopAppBar
@@ -8,7 +7,6 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.ui.state.NavbarState
import com.zaneschepke.wireguardautotunnel.ui.theme.LockedDownBannerHeight
@@ -18,11 +16,8 @@ fun DynamicTopAppBar(navBarState: NavbarState, modifier: Modifier = Modifier) {
TopAppBar(
modifier = modifier.padding(top = LockedDownBannerHeight),
colors = TopAppBarDefaults.topAppBarColors().copy(Color.Transparent),
title = {
Box(modifier = Modifier.padding(start = 10.dp)) { navBarState.topTitle?.invoke() }
},
actions = {
Box(modifier = Modifier.padding(end = 10.dp)) { navBarState.topTrailing?.invoke() }
},
navigationIcon = { navBarState.topLeading?.invoke() },
title = { navBarState.topTitle?.invoke() },
actions = { navBarState.topTrailing?.invoke() },
)
}
@@ -3,6 +3,7 @@ package com.zaneschepke.wireguardautotunnel.ui.navigation.components
import android.os.Build
import androidx.compose.foundation.layout.Row
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.automirrored.rounded.Sort
import androidx.compose.material.icons.rounded.*
import androidx.compose.material3.Text
@@ -17,6 +18,7 @@ import androidx.navigation.toRoute
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.button.ActionIconButton
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route.Config
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
import com.zaneschepke.wireguardautotunnel.ui.state.NavbarState
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
@@ -47,6 +49,10 @@ fun NavHostController.currentBackStackEntryAsNavbarState(
Route.Config::class.simpleName -> backStackEntry?.toRoute<Route.Config>()
Route.SplitTunnel::class.simpleName ->
backStackEntry?.toRoute<Route.SplitTunnel>()
Route.ConfigGlobal::class.simpleName ->
backStackEntry?.toRoute<Route.ConfigGlobal>()
Route.SplitTunnelGlobal::class.simpleName ->
backStackEntry?.toRoute<Route.SplitTunnelGlobal>()
Route.TunnelAutoTunnel::class.simpleName ->
backStackEntry?.toRoute<Route.TunnelAutoTunnel>()
Route.Sort::class.simpleName -> backStackEntry?.toRoute<Route.Sort>()
@@ -68,6 +74,8 @@ fun NavHostController.currentBackStackEntryAsNavbarState(
backStackEntry?.toRoute<Route.LocationDisclosure>()
Route.Donate::class.simpleName -> backStackEntry?.toRoute<Route.Donate>()
Route.Addresses::class.simpleName -> backStackEntry?.toRoute<Route.Addresses>()
Route.TunnelGlobals::class.simpleName ->
backStackEntry?.toRoute<Route.TunnelGlobals>()
else -> null
}
}
@@ -83,11 +91,21 @@ fun NavHostController.currentBackStackEntryAsNavbarState(
when (route) {
Route.AdvancedAutoTunnel ->
NavbarState(
topLeading = {
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
navController.popBackStack()
}
},
showBottomItems = true,
topTitle = { Text(stringResource(R.string.advanced_settings)) },
)
Route.Appearance ->
NavbarState(
topLeading = {
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
navController.popBackStack()
}
},
showBottomItems = true,
topTitle = { Text(stringResource(R.string.appearance)) },
)
@@ -100,39 +118,43 @@ fun NavHostController.currentBackStackEntryAsNavbarState(
{ Text(stringResource(R.string.auto_tunnel)) }
},
)
is Route.Config -> {
val tunnel = sharedState.tunnels.find { it.id == route.id }
NavbarState(
showBottomItems = true,
topTitle = {
val title = tunnel?.tunName ?: stringResource(R.string.new_tunnel)
Text(title)
},
topTrailing = {
ActionIconButton(Icons.Rounded.Save, R.string.save) {
keyboardController?.hide()
sharedViewModel.postSideEffect(LocalSideEffect.SaveChanges)
}
},
)
}
Route.Display ->
NavbarState(
topLeading = {
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
navController.popBackStack()
}
},
showBottomItems = true,
topTitle = { Text(stringResource(R.string.display_theme)) },
)
Route.Dns ->
NavbarState(
topLeading = {
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
navController.popBackStack()
}
},
showBottomItems = true,
topTitle = { Text(stringResource(R.string.dns_settings)) },
)
Route.Language ->
NavbarState(
topLeading = {
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
navController.popBackStack()
}
},
showBottomItems = true,
topTitle = { Text(stringResource(R.string.language)) },
)
Route.License ->
NavbarState(
topLeading = {
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
navController.popBackStack()
}
},
showBottomItems = true,
topTitle = { Text(stringResource(R.string.licenses)) },
)
@@ -151,6 +173,11 @@ fun NavHostController.currentBackStackEntryAsNavbarState(
)
Route.ProxySettings ->
NavbarState(
topLeading = {
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
navController.popBackStack()
}
},
showBottomItems = true,
topTitle = { Text(stringResource(R.string.proxy_settings)) },
topTrailing = {
@@ -175,6 +202,11 @@ fun NavHostController.currentBackStackEntryAsNavbarState(
)
Route.Sort ->
NavbarState(
topLeading = {
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
navController.popBackStack()
}
},
showBottomItems = true,
topTitle = { Text(stringResource(R.string.sort)) },
topTrailing = {
@@ -188,9 +220,35 @@ fun NavHostController.currentBackStackEntryAsNavbarState(
}
},
)
is Config -> {
val tunnel = sharedState.tunnels.find { it.id == route.id }
NavbarState(
topLeading = {
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
navController.popBackStack()
}
},
showBottomItems = true,
topTitle = {
val title = tunnel?.tunName ?: stringResource(R.string.new_tunnel)
Text(title)
},
topTrailing = {
ActionIconButton(Icons.Rounded.Save, R.string.save) {
keyboardController?.hide()
sharedViewModel.postSideEffect(LocalSideEffect.SaveChanges)
}
},
)
}
is Route.SplitTunnel -> {
val tunnel = sharedState.tunnels.find { it.id == route.id }
NavbarState(
topLeading = {
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
navController.popBackStack()
}
},
topTitle = { Text(tunnel?.tunName ?: "") },
topTrailing = {
ActionIconButton(Icons.Rounded.Save, R.string.save) {
@@ -200,6 +258,39 @@ fun NavHostController.currentBackStackEntryAsNavbarState(
showBottomItems = true,
)
}
is Route.SplitTunnelGlobal -> {
NavbarState(
topLeading = {
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
navController.popBackStack()
}
},
topTitle = { Text(stringResource(R.string.splt_tunneling)) },
topTrailing = {
ActionIconButton(Icons.Rounded.Save, R.string.save) {
sharedViewModel.postSideEffect(LocalSideEffect.SaveChanges)
}
},
showBottomItems = true,
)
}
is Route.ConfigGlobal -> {
NavbarState(
topLeading = {
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
navController.popBackStack()
}
},
showBottomItems = true,
topTitle = { Text(stringResource(R.string.configuration)) },
topTrailing = {
ActionIconButton(Icons.Rounded.Save, R.string.save) {
keyboardController?.hide()
sharedViewModel.postSideEffect(LocalSideEffect.SaveChanges)
}
},
)
}
Route.Support ->
NavbarState(
topTitle = { Text(stringResource(R.string.support)) },
@@ -207,21 +298,44 @@ fun NavHostController.currentBackStackEntryAsNavbarState(
)
Route.SystemFeatures ->
NavbarState(
topLeading = {
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
navController.popBackStack()
}
},
topTitle = { Text(stringResource(R.string.android_integrations)) },
showBottomItems = true,
)
is Route.TunnelAutoTunnel -> {
val tunnel = sharedState.tunnels.find { it.id == route.id }
NavbarState(showBottomItems = true, topTitle = { Text(tunnel?.tunName ?: "") })
NavbarState(
topLeading = {
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
navController.popBackStack()
}
},
showBottomItems = true,
topTitle = { Text(tunnel?.tunName ?: "") },
)
}
Route.TunnelMonitoring ->
NavbarState(
topLeading = {
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
navController.popBackStack()
}
},
topTitle = { Text(stringResource(R.string.tunnel_monitoring)) },
showBottomItems = true,
)
is Route.TunnelOptions -> {
val tunnel = sharedState.tunnels.find { it.id == route.id }
NavbarState(
topLeading = {
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
navController.popBackStack()
}
},
showBottomItems = true,
topTitle = { Text(tunnel?.tunName ?: "") },
topTrailing = {
@@ -230,7 +344,7 @@ fun NavHostController.currentBackStackEntryAsNavbarState(
sharedViewModel.postSideEffect(LocalSideEffect.Modal.QR)
}
ActionIconButton(Icons.Rounded.Edit, R.string.edit_tunnel) {
navigate(Route.Config(route.id))
navigate(Config(route.id))
}
}
},
@@ -285,21 +399,47 @@ fun NavHostController.currentBackStackEntryAsNavbarState(
}
Route.WifiDetectionMethod ->
NavbarState(
topLeading = {
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
navController.popBackStack()
}
},
topTitle = { Text(stringResource(R.string.wifi_detection_method)) },
showBottomItems = true,
)
Route.Donate -> {
NavbarState(
topLeading = {
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
navController.popBackStack()
}
},
topTitle = { Text(stringResource(R.string.donate_title)) },
showBottomItems = true,
)
}
Route.Addresses -> {
NavbarState(
topLeading = {
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
navController.popBackStack()
}
},
topTitle = { Text(stringResource(R.string.addresses)) },
showBottomItems = true,
)
}
is Route.TunnelGlobals -> {
NavbarState(
topLeading = {
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
navController.popBackStack()
}
},
topTitle = { Text(stringResource(R.string.tunnel_global_overrides)) },
showBottomItems = true,
)
}
Route.TunnelsGraph,
Route.SettingsGraph,
Route.AutoTunnelGraph,
@@ -84,7 +84,6 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
Modifier.verticalScroll(rememberScrollState())
.fillMaxSize()
.padding(vertical = 24.dp)
.padding(horizontal = 12.dp)
.then(
if (!isTv) {
Modifier.clickable(
@@ -134,6 +133,16 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
)
}
)
add(
tunnelGlobalsSettingItem(
settingsState.settings.isTunnelGlobalsEnabled,
onClick = viewModel::setTunnelGlobals,
) {
settingsState.globalTunnelConf?.let {
navController.navigate(Route.TunnelGlobals(it.id))
}
}
)
if (showProxySettings)
add(proxYSettingsItem { navController.navigate(Route.ProxySettings) })
}
@@ -0,0 +1,33 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
import android.R.attr.enabled
import android.R.attr.onClick
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItemLabel
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionLabelType
@Composable
fun tunnelGlobalsSettingItem(
enabled: Boolean,
onClick: (Boolean) -> Unit,
onItemClick: () -> Unit,
): SelectionItem {
return SelectionItem(
leading = { Icon(ImageVector.vectorResource(R.drawable.globe), contentDescription = null) },
title = {
SelectionItemLabel(
stringResource(R.string.tunnel_global_overrides),
SelectionLabelType.TITLE,
)
},
trailing = { ScaledSwitch(checked = enabled, onClick = onClick) },
onClick = onItemClick,
)
}
@@ -0,0 +1,40 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.globals
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.globals.components.globalConfigItem
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.tunneloptions.components.splitTunnelingItem
@Composable
fun TunnelGlobalsScreen(globalTunnelId: Int) {
val navController = LocalNavController.current
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.Top),
modifier =
Modifier.verticalScroll(rememberScrollState()).fillMaxSize().padding(vertical = 24.dp),
) {
SurfaceSelectionGroupButton(
listOf(
globalConfigItem { navController.navigate(Route.ConfigGlobal(globalTunnelId)) },
splitTunnelingItem(stringResource(R.string.splt_tunneling)) {
navController.navigate(Route.SplitTunnelGlobal(id = globalTunnelId))
},
)
)
}
}
@@ -0,0 +1,24 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.globals.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.button.ForwardButton
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItemLabel
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionLabelType
@Composable
fun globalConfigItem(onClick: () -> Unit): SelectionItem {
return SelectionItem(
leading = { Icon(Icons.Outlined.Settings, contentDescription = null) },
title = {
SelectionItemLabel(stringResource(R.string.configuration), SelectionLabelType.TITLE)
},
trailing = { ForwardButton { onClick() } },
onClick = onClick,
)
}
@@ -16,7 +16,6 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.autotunnel.components.MobileDataTunnelItem
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.autotunnel.components.PingRestartItem
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.autotunnel.components.WifiTunnelItem
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.autotunnel.components.ethernetTunnelItem
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelsViewModel
@@ -42,13 +41,6 @@ fun TunnelAutoTunnelScreen(tunnelId: Int, viewModel: TunnelsViewModel) {
SurfaceSelectionGroupButton(
items =
buildList {
if (tunnelsState.isPingEnabled) {
add(
PingRestartItem(tunnelConf.restartOnPingFailure) {
viewModel.setRestartOnPing(tunnelId, it)
}
)
}
add(
MobileDataTunnelItem(tunnelConf.isMobileDataTunnel) {
viewModel.setMobileDataTunnel(tunnelId, it)
@@ -1,28 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.autotunnel.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.NetworkPing
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
@Composable
fun PingRestartItem(enabled: Boolean, onClick: (Boolean) -> Unit): SelectionItem {
return SelectionItem(
leading = { Icon(Icons.Outlined.NetworkPing, contentDescription = null) },
title = {
Text(
text = stringResource(R.string.restart_on_ping),
style =
MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = { ScaledSwitch(checked = enabled, onClick = onClick) },
onClick = { onClick(!enabled) },
)
}
@@ -7,11 +7,12 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm
import com.zaneschepke.wireguardautotunnel.ui.common.SecureScreenFromRecording
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.config.components.AddPeerButton
@@ -20,12 +21,11 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.config.components.
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
import com.zaneschepke.wireguardautotunnel.ui.state.ConfigProxy
import com.zaneschepke.wireguardautotunnel.ui.state.PeerProxy
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelsViewModel
import org.orbitmvi.orbit.compose.collectSideEffect
@Composable
fun ConfigScreen(tunnelId: Int? = null, viewModel: TunnelsViewModel = hiltViewModel()) {
val sharedViewModel = LocalSharedVm.current
fun ConfigScreen(tunnelId: Int? = null) {
val viewModel = LocalSharedVm.current
val tunnelsState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
@@ -39,6 +39,7 @@ fun ConfigScreen(tunnelId: Int? = null, viewModel: TunnelsViewModel = hiltViewMo
}
var tunnelName by remember { mutableStateOf(tunnelConf?.tunName ?: "") }
val isGlobalConfig = rememberSaveable { tunnelName == TunnelConfig.GLOBAL_CONFIG_NAME }
val isTunnelNameTaken by
remember(tunnelName, tunnelsState.tunnels) {
@@ -47,7 +48,7 @@ fun ConfigScreen(tunnelId: Int? = null, viewModel: TunnelsViewModel = hiltViewMo
}
}
sharedViewModel.collectSideEffect { sideEffect ->
viewModel.collectSideEffect { sideEffect ->
if (sideEffect is LocalSideEffect.SaveChanges)
viewModel.saveConfigProxy(tunnelId, configProxy, tunnelName)
}
@@ -64,6 +65,7 @@ fun ConfigScreen(tunnelId: Int? = null, viewModel: TunnelsViewModel = hiltViewMo
.padding(horizontal = 12.dp),
) {
InterfaceSection(
isGlobalConfig,
configProxy = configProxy,
tunnelName,
isTunnelNameTaken,
@@ -79,34 +81,38 @@ fun ConfigScreen(tunnelId: Int? = null, viewModel: TunnelsViewModel = hiltViewMo
configProxy = configProxy.copy(`interface` = configProxy.`interface`.setSipMimic())
},
)
PeersSection(
configProxy,
onRemove = {
configProxy =
configProxy.copy(
peers = configProxy.peers.toMutableList().apply { removeAt(it) }
)
},
onToggleLan = { index ->
configProxy =
configProxy.copy(
peers =
configProxy.peers.toMutableList().apply {
val peer = get(index)
val updated =
if (peer.isLanExcluded()) peer.includeLan()
else peer.excludeLan()
set(index, updated)
}
)
},
onUpdatePeer = { peer, index ->
configProxy =
configProxy.copy(
peers = configProxy.peers.toMutableList().apply { set(index, peer) }
)
},
)
AddPeerButton { configProxy = configProxy.copy(peers = configProxy.peers + PeerProxy()) }
if (!isGlobalConfig)
PeersSection(
configProxy,
onRemove = {
configProxy =
configProxy.copy(
peers = configProxy.peers.toMutableList().apply { removeAt(it) }
)
},
onToggleLan = { index ->
configProxy =
configProxy.copy(
peers =
configProxy.peers.toMutableList().apply {
val peer = get(index)
val updated =
if (peer.isLanExcluded()) peer.includeLan()
else peer.excludeLan()
set(index, updated)
}
)
},
onUpdatePeer = { peer, index ->
configProxy =
configProxy.copy(
peers = configProxy.peers.toMutableList().apply { set(index, peer) }
)
},
)
if (!isGlobalConfig)
AddPeerButton {
configProxy = configProxy.copy(peers = configProxy.peers + PeerProxy())
}
}
}
@@ -1,5 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.config.components
import android.R.attr.label
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
@@ -28,6 +29,7 @@ import java.util.*
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun InterfaceFields(
isGlobalConfig: Boolean,
interfaceState: InterfaceProxy,
showScripts: Boolean,
showAmneziaValues: Boolean,
@@ -41,93 +43,98 @@ fun InterfaceFields(
var showPrivateKey by rememberSaveable { mutableStateOf(false) }
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
ConfigurationTextBox(
value = interfaceState.privateKey,
hint =
stringResource(R.string.hint_template, stringResource(R.string.base64_key))
.lowercase(Locale.getDefault()),
onValueChange = { onInterfaceChange(interfaceState.copy(privateKey = it)) },
label = stringResource(R.string.private_key),
modifier = Modifier.fillMaxWidth(),
visualTransformation =
if (showPrivateKey) VisualTransformation.None else PasswordVisualTransformation(),
trailing = {
CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides 4.dp) {
Row(modifier = Modifier.padding(end = 4.dp)) {
IconButton(onClick = { showPrivateKey = !showPrivateKey }) {
Icon(
Icons.Outlined.RemoveRedEye,
stringResource(R.string.show_password),
)
}
IconButton(
enabled = true,
onClick = {
val keypair = KeyPair()
onInterfaceChange(
interfaceState.copy(
privateKey = keypair.privateKey.toBase64(),
publicKey = keypair.publicKey.toBase64(),
)
if (!isGlobalConfig)
ConfigurationTextBox(
value = interfaceState.privateKey,
hint =
stringResource(R.string.hint_template, stringResource(R.string.base64_key))
.lowercase(Locale.getDefault()),
onValueChange = { onInterfaceChange(interfaceState.copy(privateKey = it)) },
label = stringResource(R.string.private_key),
modifier = Modifier.fillMaxWidth(),
visualTransformation =
if (showPrivateKey) VisualTransformation.None
else PasswordVisualTransformation(),
trailing = {
CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides 4.dp) {
Row(modifier = Modifier.padding(end = 4.dp)) {
IconButton(onClick = { showPrivateKey = !showPrivateKey }) {
Icon(
Icons.Outlined.RemoveRedEye,
stringResource(R.string.show_password),
)
},
) {
Icon(
Icons.Rounded.Refresh,
stringResource(R.string.rotate_keys),
tint = MaterialTheme.colorScheme.onSurface,
)
}
IconButton(
enabled = true,
onClick = {
val keypair = KeyPair()
onInterfaceChange(
interfaceState.copy(
privateKey = keypair.privateKey.toBase64(),
publicKey = keypair.publicKey.toBase64(),
)
)
},
) {
Icon(
Icons.Rounded.Refresh,
stringResource(R.string.rotate_keys),
tint = MaterialTheme.colorScheme.onSurface,
)
}
}
}
}
},
enabled = true,
singleLine = true,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
)
ConfigurationTextBox(
value = interfaceState.publicKey,
hint =
stringResource(R.string.hint_template, stringResource(R.string.base64_key))
.lowercase(Locale.getDefault()),
onValueChange = { onInterfaceChange(interfaceState.copy(publicKey = it)) },
label = stringResource(R.string.public_key),
enabled = false,
modifier = Modifier.fillMaxWidth(),
singleLine = true,
trailing = {
IconButton(onClick = { clipboardManager.copy(interfaceState.publicKey) }) {
Icon(
Icons.Rounded.ContentCopy,
stringResource(R.string.copy_public_key),
tint = MaterialTheme.colorScheme.onSurface,
)
}
},
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
)
ConfigurationTextBox(
value = interfaceState.addresses,
onValueChange = { onInterfaceChange(interfaceState.copy(addresses = it)) },
label = stringResource(R.string.addresses),
hint =
stringResource(
R.string.hint_template,
stringResource(R.string.comma_separated).lowercase(locale),
)
.lowercase(Locale.getDefault()),
modifier = Modifier.fillMaxWidth(),
)
ConfigurationTextBox(
value = interfaceState.listenPort,
onValueChange = { onInterfaceChange(interfaceState.copy(listenPort = it)) },
label = stringResource(R.string.listen_port),
hint = stringResource(R.string.random),
modifier = Modifier.fillMaxWidth(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
)
},
enabled = true,
singleLine = true,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
)
if (!isGlobalConfig)
ConfigurationTextBox(
value = interfaceState.publicKey,
hint =
stringResource(R.string.hint_template, stringResource(R.string.base64_key))
.lowercase(Locale.getDefault()),
onValueChange = { onInterfaceChange(interfaceState.copy(publicKey = it)) },
label = stringResource(R.string.public_key),
enabled = false,
modifier = Modifier.fillMaxWidth(),
singleLine = true,
trailing = {
IconButton(onClick = { clipboardManager.copy(interfaceState.publicKey) }) {
Icon(
Icons.Rounded.ContentCopy,
stringResource(R.string.copy_public_key),
tint = MaterialTheme.colorScheme.onSurface,
)
}
},
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
)
if (!isGlobalConfig)
ConfigurationTextBox(
value = interfaceState.addresses,
onValueChange = { onInterfaceChange(interfaceState.copy(addresses = it)) },
label = stringResource(R.string.addresses),
hint =
stringResource(
R.string.hint_template,
stringResource(R.string.comma_separated).lowercase(locale),
)
.lowercase(Locale.getDefault()),
modifier = Modifier.fillMaxWidth(),
)
if (!isGlobalConfig)
ConfigurationTextBox(
value = interfaceState.listenPort,
onValueChange = { onInterfaceChange(interfaceState.copy(listenPort = it)) },
label = stringResource(R.string.listen_port),
hint = stringResource(R.string.random),
modifier = Modifier.fillMaxWidth(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(6.dp),
@@ -20,6 +20,7 @@ import java.util.*
@Composable
fun InterfaceSection(
isGlobalConfig: Boolean,
configProxy: ConfigProxy,
tunnelName: String,
isTunnelNameTaken: Boolean,
@@ -32,6 +33,7 @@ fun InterfaceSection(
var showAmneziaValues by rememberSaveable {
mutableStateOf(configProxy.`interface`.isAmneziaEnabled())
}
var showScripts by rememberSaveable { mutableStateOf(configProxy.hasScripts()) }
var isDropDownExpanded by rememberSaveable { mutableStateOf(false) }
val isAmneziaCompatibilitySet =
@@ -82,17 +84,19 @@ fun InterfaceSection(
},
)
}
ConfigurationTextBox(
value = tunnelName,
onValueChange = onTunnelNameChange,
label = stringResource(R.string.name),
isError = isTunnelNameTaken,
hint =
stringResource(R.string.hint_template, stringResource(R.string.tunnel_name))
.lowercase(Locale.getDefault()),
modifier = Modifier.fillMaxWidth(),
)
if (!isGlobalConfig)
ConfigurationTextBox(
value = tunnelName,
onValueChange = onTunnelNameChange,
label = stringResource(R.string.name),
isError = isTunnelNameTaken,
hint =
stringResource(R.string.hint_template, stringResource(R.string.tunnel_name))
.lowercase(Locale.getDefault()),
modifier = Modifier.fillMaxWidth(),
)
InterfaceFields(
isGlobalConfig,
interfaceState = configProxy.`interface`,
showScripts = showScripts,
showAmneziaValues = showAmneziaValues,
@@ -10,13 +10,16 @@ import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm
import com.zaneschepke.wireguardautotunnel.ui.common.SectionDivider
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.tunneloptions.components.*
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelsViewModel
@@ -61,7 +64,11 @@ fun TunnelOptionsScreen(tunnelId: Int, viewModel: TunnelsViewModel) {
buildList {
add(primaryTunnelItem(tunnelConf) { viewModel.togglePrimaryTunnel(tunnelId) })
add(autoTunnelingItem(tunnelConf, navController))
add(splitTunnelingItem(tunnelConf, navController))
add(
splitTunnelingItem(stringResource(R.string.splt_tunneling)) {
navController.navigate(Route.SplitTunnel(id = tunnelConf.id))
}
)
}
)
SectionDivider()
@@ -1,33 +1,21 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.tunneloptions.components
import android.R.attr.onClick
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.CallSplit
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.navigation.NavController
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.ui.common.button.ForwardButton
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItemLabel
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionLabelType
@Composable
fun splitTunnelingItem(tunnelConf: TunnelConf, navController: NavController): SelectionItem {
fun splitTunnelingItem(label: String, onClick: () -> Unit): SelectionItem {
return SelectionItem(
leading = { Icon(Icons.AutoMirrored.Outlined.CallSplit, contentDescription = null) },
title = {
Text(
text = stringResource(R.string.splt_tunneling),
style =
MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = {
ForwardButton { navController.navigate(Route.SplitTunnel(id = tunnelConf.id)) }
},
onClick = { navController.navigate(Route.SplitTunnel(id = tunnelConf.id)) },
title = { SelectionItemLabel(label, SelectionLabelType.TITLE) },
trailing = { ForwardButton { onClick() } },
onClick = onClick,
)
}
@@ -5,6 +5,7 @@ import androidx.compose.runtime.Composable
data class NavbarState(
val topTitle: (@Composable () -> Unit)? = null,
val topTrailing: (@Composable () -> Unit)? = null,
val topLeading: (@Composable () -> Unit)? = null,
val showBottomItems: Boolean = false,
val removeBottom: Boolean = false,
val isAutoTunnelActive: Boolean = false,
@@ -1,6 +1,7 @@
package com.zaneschepke.wireguardautotunnel.ui.state
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
data class SettingUiState(
val settings: GeneralSettings = GeneralSettings(),
@@ -10,4 +11,5 @@ data class SettingUiState(
val isPinLockEnabled: Boolean = false,
val showDetailedPingStats: Boolean = false,
val stateInitialized: Boolean = false,
val globalTunnelConf: TunnelConf? = null,
)
@@ -4,9 +4,11 @@ import androidx.lifecycle.ViewModel
import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager
import com.zaneschepke.wireguardautotunnel.data.model.DnsProtocol
import com.zaneschepke.wireguardautotunnel.data.model.DnsProvider
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
import com.zaneschepke.wireguardautotunnel.ui.state.SettingUiState
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -23,6 +25,7 @@ constructor(
private val settingsRepository: GeneralSettingRepository,
private val shortcutManager: ShortcutManager,
private val appStateRepository: AppStateRepository,
private val tunnelsRepository: TunnelRepository,
private val globalEffectRepository: GlobalEffectRepository,
) : ContainerHost<SettingUiState, Nothing>, ViewModel() {
@@ -32,7 +35,11 @@ constructor(
buildSettings = { repeatOnSubscribedStopTimeout = 5000L },
) {
intent {
combine(settingsRepository.flow, appStateRepository.flow) { settings, appState ->
combine(
settingsRepository.flow,
appStateRepository.flow,
tunnelsRepository.globalTunnelFlow,
) { settings, appState, tunnel ->
SettingUiState(
settings = settings,
isLocalLoggingEnabled = appState.isLocalLogsEnabled,
@@ -41,6 +48,7 @@ constructor(
isPinLockEnabled = appState.isPinLockEnabled,
showDetailedPingStats = appState.showDetailedPingStats,
stateInitialized = true,
globalTunnelConf = tunnel,
)
}
.collect { reduce { it } }
@@ -72,6 +80,12 @@ constructor(
settingsRepository.save(state.settings.copy(isLanOnKillSwitchEnabled = to))
}
fun setTunnelGlobals(to: Boolean) = intent {
settingsRepository.save(state.settings.copy(isTunnelGlobalsEnabled = to))
if (state.globalTunnelConf == null)
tunnelsRepository.save(TunnelConf.generateDefaultGlobalConfig())
}
fun setTunnelPingIntervalSeconds(to: Int) = intent {
settingsRepository.save(state.settings.copy(tunnelPingIntervalSeconds = to))
}
@@ -20,11 +20,13 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectReposit
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
import com.zaneschepke.wireguardautotunnel.ui.state.ConfigProxy
import com.zaneschepke.wireguardautotunnel.ui.state.SharedAppUiState
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
import com.zaneschepke.wireguardautotunnel.util.FileUtils
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
import com.zaneschepke.wireguardautotunnel.util.StringValue
import com.zaneschepke.wireguardautotunnel.util.extensions.asStringValue
import com.zaneschepke.wireguardautotunnel.util.extensions.saveTunnelsUniquely
import dagger.hilt.android.lifecycle.HiltViewModel
import java.io.File
@@ -36,9 +38,11 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import org.amnezia.awg.config.BadConfigException
import org.orbitmvi.orbit.ContainerHost
import org.orbitmvi.orbit.viewmodel.container
import rikka.shizuku.Shizuku
import timber.log.Timber
import xyz.teamgravity.pin_lock_compose.PinManager
@HiltViewModel
@@ -123,6 +127,46 @@ constructor(
appStateRepository.setPinLockEnabled(enabled)
}
fun saveConfigProxy(tunnelId: Int?, configProxy: ConfigProxy, tunnelName: String) = intent {
if (state.tunnels.any { it.tunName == tunnelName && it.id != tunnelId })
return@intent postSideEffect(
GlobalSideEffect.Toast(StringValue.StringResource(R.string.tunnel_name_taken))
)
runCatching {
val (wg, am) = configProxy.buildConfigs()
val tunnelConf =
if (tunnelId == null) {
TunnelConf.tunnelConfFromQuick(am.toAwgQuickString(true, false), tunnelName)
} else {
val latestTunnel = state.tunnels.find { it.id == tunnelId }
latestTunnel?.copy(
tunName = tunnelName,
amQuick = am.toAwgQuickString(true, false),
wgQuick = wg.toWgQuickString(true),
)
}
if (tunnelConf != null) {
tunnelRepository.save(tunnelConf)
postSideEffect(
GlobalSideEffect.Toast(
StringValue.StringResource(R.string.config_changes_saved)
)
)
postSideEffect(GlobalSideEffect.PopBackStack)
}
}
.onFailure {
Timber.e(it)
val message =
when (it) {
is BadConfigException -> it.asStringValue()
is com.wireguard.config.BadConfigException -> it.asStringValue()
else -> StringValue.StringResource(R.string.unknown_error)
}
postSideEffect(GlobalSideEffect.Snackbar(message))
}
}
fun stopTunnel(tunnelConf: TunnelConf) = intent { tunnelManager.stopTunnel(tunnelConf.id) }
fun setAppMode(appMode: AppMode) = intent {
@@ -11,7 +11,6 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepos
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
import com.zaneschepke.wireguardautotunnel.ui.state.ConfigProxy
import com.zaneschepke.wireguardautotunnel.ui.state.TunnelsUiState
import com.zaneschepke.wireguardautotunnel.util.FileUtils
import com.zaneschepke.wireguardautotunnel.util.StringValue
@@ -27,7 +26,6 @@ import kotlinx.coroutines.flow.combine
import org.amnezia.awg.config.BadConfigException
import org.orbitmvi.orbit.ContainerHost
import org.orbitmvi.orbit.viewmodel.container
import timber.log.Timber
@HiltViewModel
class TunnelsViewModel
@@ -47,7 +45,7 @@ constructor(
buildSettings = { repeatOnSubscribedStopTimeout = 5000L },
) {
combine(
tunnelRepository.flow,
tunnelRepository.userTunnelsFlow,
generalSettingRepository.flow,
appStateRepository.flow,
tunnelManager.activeTunnels,
@@ -69,46 +67,6 @@ constructor(
globalEffectRepository.post(globalSideEffect)
}
fun saveConfigProxy(tunnelId: Int?, configProxy: ConfigProxy, tunnelName: String) = intent {
if (state.tunnels.any { it.tunName == tunnelName && it.id != tunnelId })
return@intent postSideEffect(
GlobalSideEffect.Toast(StringValue.StringResource(R.string.tunnel_name_taken))
)
runCatching {
val (wg, am) = configProxy.buildConfigs()
val tunnelConf =
if (tunnelId == null) {
TunnelConf.tunnelConfFromQuick(am.toAwgQuickString(true, false), tunnelName)
} else {
val latestTunnel = state.tunnels.find { it.id == tunnelId }
latestTunnel?.copy(
tunName = tunnelName,
amQuick = am.toAwgQuickString(true, false),
wgQuick = wg.toWgQuickString(true),
)
}
if (tunnelConf != null) {
tunnelRepository.save(tunnelConf)
postSideEffect(
GlobalSideEffect.Toast(
StringValue.StringResource(R.string.config_changes_saved)
)
)
postSideEffect(GlobalSideEffect.PopBackStack)
}
}
.onFailure {
Timber.e(it)
val message =
when (it) {
is BadConfigException -> it.asStringValue()
is com.wireguard.config.BadConfigException -> it.asStringValue()
else -> StringValue.StringResource(R.string.unknown_error)
}
postSideEffect(GlobalSideEffect.Snackbar(message))
}
}
fun saveSortChanges(tunnels: List<TunnelConf>) = intent {
tunnelRepository.saveAll(tunnels.mapIndexed { index, conf -> conf.copy(position = index) })
postSideEffect(
+9
View File
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M480,880q-83,0 -156,-31.5T197,763q-54,-54 -85.5,-127T80,480q0,-83 31.5,-156T197,197q54,-54 127,-85.5T480,80q83,0 156,31.5T763,197q54,54 85.5,127T880,480q0,83 -31.5,156T763,763q-54,54 -127,85.5T480,880ZM480,800q134,0 227,-93t93,-227q0,-7 -0.5,-14.5T799,453q-5,29 -27,48t-52,19h-80q-33,0 -56.5,-23.5T560,440v-40L400,400v-80q0,-33 23.5,-56.5T480,240h40q0,-23 12.5,-40.5T563,171q-20,-5 -40.5,-8t-42.5,-3q-134,0 -227,93t-93,227h200q66,0 113,47t47,113v40L400,680v110q20,5 39.5,7.5T480,800Z"
android:fillColor="#e3e3e3"/>
</vector>
+3
View File
@@ -391,4 +391,7 @@
the Play Store version of this app. Please browse the project\'s webpages to find where to donate.
</string>
<string name="delete_active_message">Cannot delete active tunnel.</string>
<string name="tunnel_global_overrides">Global tunnel overrides</string>
<string name="back">Back</string>
<string name="configuration">Configuration</string>
</resources>
+1 -2
View File
@@ -1,7 +1,7 @@
[versions]
accompanist = "0.37.3"
activityCompose = "1.11.0"
amneziawgAndroid = "2.1.9"
amneziawgAndroid = "2.1.10"
androidx-junit = "1.3.0"
icmp4a = "1.0.0"
orbitCompose = "10.0.0"
@@ -45,7 +45,6 @@ storage = "1.6.0"
ktfmt = "0.24.0"
licensee = "1.13.0"
[libraries]
# accompanist