From 00c2c2ac203b4e01fa5eee4107d166b645ec0241 Mon Sep 17 00:00:00 2001 From: Zane Schepke Date: Tue, 30 Sep 2025 12:14:09 -0400 Subject: [PATCH] feat: global config overrides (#983) --- .../23.json | 371 ++++++++++++++++++ .../wireguardautotunnel/MainActivity.kt | 20 +- .../core/tunnel/TunnelManager.kt | 35 +- .../wireguardautotunnel/data/AppDatabase.kt | 3 +- .../data/dao/TunnelConfigDao.kt | 8 + .../data/entity/Settings.kt | 2 + .../data/entity/TunnelConfig.kt | 1 + .../data/mapper/SettingsMapper.kt | 2 + .../data/repository/RoomTunnelRepository.kt | 11 + .../domain/model/GeneralSettings.kt | 1 + .../domain/model/TunnelConf.kt | 158 ++++++++ .../domain/repository/TunnelRepository.kt | 4 + .../ui/navigation/Route.kt | 6 + .../navigation/components/DynamicTopAppBar.kt | 11 +- .../currentBackStackEntryAsNavbarState.kt | 176 ++++++++- .../ui/screens/settings/SettingsScreen.kt | 11 +- .../components/tunnelGlobalsSettingItem.kt | 33 ++ .../settings/globals/TunnelGlobalsScreen.kt | 40 ++ .../globals/components/globalConfigItem.kt | 24 ++ .../autotunnel/TunnelAutoTunnelScreen.kt | 8 - .../autotunnel/components/PingRestartItem.kt | 28 -- .../ui/screens/tunnels/config/ConfigScreen.kt | 74 ++-- .../config/components/InterfaceFields.kt | 175 +++++---- .../config/components/InterfaceSection.kt | 24 +- .../tunneloptions/TunnelOptionsScreen.kt | 9 +- .../components/splitTunnelingItem.kt | 26 +- .../ui/state/NavbarState.kt | 1 + .../ui/state/SettingUiState.kt | 2 + .../viewmodel/SettingsViewModel.kt | 16 +- .../viewmodel/SharedAppViewModel.kt | 44 +++ .../viewmodel/TunnelsViewModel.kt | 44 +-- app/src/main/res/drawable/globe.xml | 9 + app/src/main/res/values/strings.xml | 3 + gradle/libs.versions.toml | 3 +- 34 files changed, 1119 insertions(+), 264 deletions(-) create mode 100644 app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/23.json create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/tunnelGlobalsSettingItem.kt create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/globals/TunnelGlobalsScreen.kt create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/globals/components/globalConfigItem.kt delete mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/autotunnel/components/PingRestartItem.kt create mode 100644 app/src/main/res/drawable/globe.xml diff --git a/app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/23.json b/app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/23.json new file mode 100644 index 00000000..1f76745c --- /dev/null +++ b/app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/23.json @@ -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')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/MainActivity.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/MainActivity.kt index 4724464b..212faef2 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/MainActivity.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/MainActivity.kt @@ -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 { backStackEntry -> val args = backStackEntry.toRoute() - val viewModel = - backStackEntry.sharedViewModel( - navController - ) - ConfigScreen(args.id, viewModel) + ConfigScreen(args.id) } } @@ -368,6 +365,19 @@ class MainActivity : AppCompatActivity() { it.sharedViewModel(navController) DnsSettingsScreen(viewModel) } + composable { backStackEntry -> + val args = backStackEntry.toRoute() + TunnelGlobalsScreen(args.id) + } + composable { backStackEntry -> + val args = backStackEntry.toRoute() + ConfigScreen(args.id) + } + composable { backStackEntry -> + val args = + backStackEntry.toRoute() + SplitTunnelScreen(args.id) + } composable { ProxySettingsScreen() } composable { AppearanceScreen() } composable { LanguageScreen() } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/TunnelManager.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/TunnelManager.kt index 353ca5e1..1a1d83c6 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/TunnelManager.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/TunnelManager.kt @@ -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 = + settingsRepository.flow + .filterNotNull() + .stateIn( + scope = applicationScope.plus(ioDispatcher), + started = SharingStarted.Eagerly, + initialValue = GeneralSettings(), + ) + + private val tunnels: StateFlow> = + 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 = + tunnelsRepository.flow.first { it.isNotEmpty() } + private val tunnelProviderFlow: StateFlow = 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) { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/AppDatabase.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/AppDatabase.kt index e2ee7a2e..93c9096d 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/AppDatabase.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/AppDatabase.kt @@ -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, ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/dao/TunnelConfigDao.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/dao/TunnelConfigDao.kt index 8feb4b5e..b0e26172 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/dao/TunnelConfigDao.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/dao/TunnelConfigDao.kt @@ -71,4 +71,12 @@ interface TunnelConfigDao { @Query("SELECT * FROM tunnelconfig ORDER BY position") fun getAllFlow(): Flow> + + @Query("SELECT * FROM TunnelConfig WHERE name != :globalName") + fun getAllTunnelsExceptGlobal( + globalName: String = TunnelConfig.GLOBAL_CONFIG_NAME + ): Flow> + + @Query("SELECT * FROM TunnelConfig WHERE name = :globalName LIMIT 1") + fun getGlobalTunnel(globalName: String = TunnelConfig.GLOBAL_CONFIG_NAME): Flow } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/entity/Settings.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/entity/Settings.kt index 26892247..2f4189b7 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/entity/Settings.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/entity/Settings.kt @@ -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, ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/entity/TunnelConfig.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/entity/TunnelConfig.kt index aba183e5..33afa36c 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/entity/TunnelConfig.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/entity/TunnelConfig.kt @@ -32,5 +32,6 @@ data class TunnelConfig( companion object { const val AM_QUICK_DEFAULT = "" + const val GLOBAL_CONFIG_NAME = "4675ab06-903a-438b-8485-6ea4187a9512" } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/mapper/SettingsMapper.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/mapper/SettingsMapper.kt index 1713b1dc..37194fff 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/mapper/SettingsMapper.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/mapper/SettingsMapper.kt @@ -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, ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/RoomTunnelRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/RoomTunnelRepository.kt index 6d4c878e..19b1af7d 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/RoomTunnelRepository.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/RoomTunnelRepository.kt @@ -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 = + tunnelConfigDao.getGlobalTunnel().flowOn(ioDispatcher).map { + it?.let(TunnelConfigMapper::toTunnelConf) + } + override suspend fun getAll(): Tunnels { return withContext(ioDispatcher) { tunnelConfigDao.getAll().map(TunnelConfigMapper::toTunnelConf) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/model/GeneralSettings.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/model/GeneralSettings.kt index 4787d374..b60aeabc 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/model/GeneralSettings.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/model/GeneralSettings.kt @@ -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 """ diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/model/TunnelConf.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/model/TunnelConf.kt index 4574d274..089feb0c 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/model/TunnelConf.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/model/TunnelConf.kt @@ -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) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/repository/TunnelRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/repository/TunnelRepository.kt index 2687adf5..04efe113 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/repository/TunnelRepository.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/repository/TunnelRepository.kt @@ -7,6 +7,10 @@ import kotlinx.coroutines.flow.Flow interface TunnelRepository { val flow: Flow> + val userTunnelsFlow: Flow> + + val globalTunnelFlow: Flow + suspend fun getAll(): Tunnels suspend fun save(tunnelConf: TunnelConf) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/navigation/Route.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/navigation/Route.kt index 843da4ea..d0aad847 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/navigation/Route.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/navigation/Route.kt @@ -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() diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/navigation/components/DynamicTopAppBar.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/navigation/components/DynamicTopAppBar.kt index 6c762eb8..327ec392 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/navigation/components/DynamicTopAppBar.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/navigation/components/DynamicTopAppBar.kt @@ -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() }, ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/navigation/components/currentBackStackEntryAsNavbarState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/navigation/components/currentBackStackEntryAsNavbarState.kt index 8a8255d4..f9fa9f4f 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/navigation/components/currentBackStackEntryAsNavbarState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/navigation/components/currentBackStackEntryAsNavbarState.kt @@ -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.SplitTunnel::class.simpleName -> backStackEntry?.toRoute() + Route.ConfigGlobal::class.simpleName -> + backStackEntry?.toRoute() + Route.SplitTunnelGlobal::class.simpleName -> + backStackEntry?.toRoute() Route.TunnelAutoTunnel::class.simpleName -> backStackEntry?.toRoute() Route.Sort::class.simpleName -> backStackEntry?.toRoute() @@ -68,6 +74,8 @@ fun NavHostController.currentBackStackEntryAsNavbarState( backStackEntry?.toRoute() Route.Donate::class.simpleName -> backStackEntry?.toRoute() Route.Addresses::class.simpleName -> backStackEntry?.toRoute() + Route.TunnelGlobals::class.simpleName -> + backStackEntry?.toRoute() 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, diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt index cf4a9935..c9da0ebb 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt @@ -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) }) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/tunnelGlobalsSettingItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/tunnelGlobalsSettingItem.kt new file mode 100644 index 00000000..c89601f0 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/tunnelGlobalsSettingItem.kt @@ -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, + ) +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/globals/TunnelGlobalsScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/globals/TunnelGlobalsScreen.kt new file mode 100644 index 00000000..0342ab47 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/globals/TunnelGlobalsScreen.kt @@ -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)) + }, + ) + ) + } +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/globals/components/globalConfigItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/globals/components/globalConfigItem.kt new file mode 100644 index 00000000..02d42cf1 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/globals/components/globalConfigItem.kt @@ -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, + ) +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/autotunnel/TunnelAutoTunnelScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/autotunnel/TunnelAutoTunnelScreen.kt index 510bfedd..fca13516 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/autotunnel/TunnelAutoTunnelScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/autotunnel/TunnelAutoTunnelScreen.kt @@ -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) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/autotunnel/components/PingRestartItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/autotunnel/components/PingRestartItem.kt deleted file mode 100644 index c51b96cf..00000000 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/autotunnel/components/PingRestartItem.kt +++ /dev/null @@ -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) }, - ) -} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/config/ConfigScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/config/ConfigScreen.kt index a41b020c..4ac7d634 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/config/ConfigScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/config/ConfigScreen.kt @@ -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()) + } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/config/components/InterfaceFields.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/config/components/InterfaceFields.kt index fc398384..77a9b174 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/config/components/InterfaceFields.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/config/components/InterfaceFields.kt @@ -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), diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/config/components/InterfaceSection.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/config/components/InterfaceSection.kt index bb41469e..2d782df9 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/config/components/InterfaceSection.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/config/components/InterfaceSection.kt @@ -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, diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/tunneloptions/TunnelOptionsScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/tunneloptions/TunnelOptionsScreen.kt index c27ec989..75d5ec84 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/tunneloptions/TunnelOptionsScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/tunneloptions/TunnelOptionsScreen.kt @@ -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() diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/tunneloptions/components/splitTunnelingItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/tunneloptions/components/splitTunnelingItem.kt index d0bac00a..c6b55aa2 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/tunneloptions/components/splitTunnelingItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/tunneloptions/components/splitTunnelingItem.kt @@ -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, ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/NavbarState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/NavbarState.kt index d4316ff2..8db8ff3b 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/NavbarState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/NavbarState.kt @@ -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, diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/SettingUiState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/SettingUiState.kt index 05e7faa9..37a4af8b 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/SettingUiState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/SettingUiState.kt @@ -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, ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/SettingsViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/SettingsViewModel.kt index 73dee9d6..ac129e6b 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/SettingsViewModel.kt @@ -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, 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)) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/SharedAppViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/SharedAppViewModel.kt index 8052549b..5a5a0852 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/SharedAppViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/SharedAppViewModel.kt @@ -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 { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/TunnelsViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/TunnelsViewModel.kt index ce06e1fd..15ffd919 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/TunnelsViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/TunnelsViewModel.kt @@ -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) = intent { tunnelRepository.saveAll(tunnels.mapIndexed { index, conf -> conf.copy(position = index) }) postSideEffect( diff --git a/app/src/main/res/drawable/globe.xml b/app/src/main/res/drawable/globe.xml new file mode 100644 index 00000000..0fdb9059 --- /dev/null +++ b/app/src/main/res/drawable/globe.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bc069e31..97e933ab 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -391,4 +391,7 @@ the Play Store version of this app. Please browse the project\'s webpages to find where to donate. Cannot delete active tunnel. + Global tunnel overrides + Back + Configuration diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ba887b02..238867a0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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