mirror of
https://github.com/wgtunnel/android.git
synced 2026-07-03 14:07:49 +02:00
feat!: tun monitoring, move ping restarts to auto-tunnel w/recovery (#885)
This is a big one.. oops. Main changes: - Make ping monitor more robust and global, with ping target overrides of the default cloudflare fallback target per tunnel (for full tunnels, otherwise we ping the internal tun ip) - Include ping restart recovery to prevent tun being down if dns failures happen after a bounce - Ping monitoring itself remains per tunnel and works without auto tunnel active, but moves the restart feature back to be managed by and integrated with auto tunnel to prevent inconsistencies and conflicts - Ping statistics can be optionally included to be displayed with tun statistics - Adds the beginnings of monitoring logs for handshake and data packet failures for userspace tuns (to be incorporated with restarts/tun status later) - Improve tun error notifications, adds ping restart notifications - Major refactor of auto tunnel logic to make it more modular and extensible for new auto tunnel conditions - A bunch of other stuff..
This commit is contained in:
@@ -107,7 +107,12 @@ android {
|
|||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin { compilerOptions { jvmTarget = JvmTarget.JVM_17 } }
|
kotlin {
|
||||||
|
compilerOptions {
|
||||||
|
jvmTarget = JvmTarget.JVM_17
|
||||||
|
freeCompilerArgs = listOf("-XXLanguage:+PropertyParamAnnotationDefaultTargetMode")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose = true
|
compose = true
|
||||||
@@ -213,6 +218,7 @@ dependencies {
|
|||||||
implementation(libs.ktor.client.content.negotiation)
|
implementation(libs.ktor.client.content.negotiation)
|
||||||
implementation(libs.ktor.serialization.kotlinx.json)
|
implementation(libs.ktor.serialization.kotlinx.json)
|
||||||
implementation(libs.slf4j.android)
|
implementation(libs.slf4j.android)
|
||||||
|
implementation(libs.icmp4a)
|
||||||
|
|
||||||
// shizuku
|
// shizuku
|
||||||
implementation(libs.shizuku.api)
|
implementation(libs.shizuku.api)
|
||||||
|
|||||||
@@ -0,0 +1,316 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 19,
|
||||||
|
"identityHash": "82bdb96b7a9f8695a34ad1ec21d9aea8",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "Settings",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `is_amnezia_enabled` INTEGER NOT NULL DEFAULT false, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT false, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT false, `is_vpn_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_lan_on_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `debounce_delay_seconds` INTEGER NOT NULL DEFAULT 3, `is_disable_kill_switch_on_trusted_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_unsecure_enabled` INTEGER NOT NULL DEFAULT false, `wifi_detection_method` INTEGER NOT NULL DEFAULT 0, `is_ping_monitoring_enabled` INTEGER NOT NULL DEFAULT true, `tunnel_ping_interval_sec` INTEGER NOT NULL DEFAULT 30, `tunnel_ping_attempts` INTEGER NOT NULL DEFAULT 3, `tunnel_ping_timeout_sec` INTEGER)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isAutoTunnelEnabled",
|
||||||
|
"columnName": "is_tunnel_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isTunnelOnMobileDataEnabled",
|
||||||
|
"columnName": "is_tunnel_on_mobile_data_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "trustedNetworkSSIDs",
|
||||||
|
"columnName": "trusted_network_ssids",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isAlwaysOnVpnEnabled",
|
||||||
|
"columnName": "is_always_on_vpn_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isTunnelOnEthernetEnabled",
|
||||||
|
"columnName": "is_tunnel_on_ethernet_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isShortcutsEnabled",
|
||||||
|
"columnName": "is_shortcuts_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isTunnelOnWifiEnabled",
|
||||||
|
"columnName": "is_tunnel_on_wifi_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isKernelEnabled",
|
||||||
|
"columnName": "is_kernel_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isRestoreOnBootEnabled",
|
||||||
|
"columnName": "is_restore_on_boot_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isMultiTunnelEnabled",
|
||||||
|
"columnName": "is_multi_tunnel_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isPingEnabled",
|
||||||
|
"columnName": "is_ping_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isAmneziaEnabled",
|
||||||
|
"columnName": "is_amnezia_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isWildcardsEnabled",
|
||||||
|
"columnName": "is_wildcards_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isStopOnNoInternetEnabled",
|
||||||
|
"columnName": "is_stop_on_no_internet_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isVpnKillSwitchEnabled",
|
||||||
|
"columnName": "is_vpn_kill_switch_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isKernelKillSwitchEnabled",
|
||||||
|
"columnName": "is_kernel_kill_switch_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isLanOnKillSwitchEnabled",
|
||||||
|
"columnName": "is_lan_on_kill_switch_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isTunnelOnUnsecureEnabled",
|
||||||
|
"columnName": "is_tunnel_on_unsecure_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "wifiDetectionMethod",
|
||||||
|
"columnName": "wifi_detection_method",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isPingMonitoringEnabled",
|
||||||
|
"columnName": "is_ping_monitoring_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "true"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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`)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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, '82bdb96b7a9f8695a34ad1ec21d9aea8')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -62,6 +62,7 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.displa
|
|||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.language.LanguageScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.language.LanguageScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.killswitch.KillSwitchScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.killswitch.KillSwitchScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.logs.LogsScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.logs.LogsScreen
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.TunnelMonitoringScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.license.LicenseScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.support.license.LicenseScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
|
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
|
||||||
@@ -289,7 +290,12 @@ class MainActivity : AppCompatActivity() {
|
|||||||
appUiState.tunnels
|
appUiState.tunnels
|
||||||
.firstOrNull { it.id == args.id }
|
.firstOrNull { it.id == args.id }
|
||||||
?.let { config ->
|
?.let { config ->
|
||||||
TunnelOptionsScreen(config, viewModel, appViewState)
|
TunnelOptionsScreen(
|
||||||
|
config,
|
||||||
|
viewModel,
|
||||||
|
appViewState,
|
||||||
|
appUiState.appSettings,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
composable<Route.Lock> { PinLockScreen(viewModel) }
|
composable<Route.Lock> { PinLockScreen(viewModel) }
|
||||||
@@ -310,6 +316,9 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
composable<Route.Sort> { SortScreen(appUiState, viewModel) }
|
composable<Route.Sort> { SortScreen(appUiState, viewModel) }
|
||||||
|
composable<Route.TunnelMonitoring> {
|
||||||
|
TunnelMonitoringScreen(appUiState, viewModel)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import androidx.hilt.work.HiltWorkerFactory
|
|||||||
import androidx.work.Configuration
|
import androidx.work.Configuration
|
||||||
import com.wireguard.android.backend.GoBackend
|
import com.wireguard.android.backend.GoBackend
|
||||||
import com.zaneschepke.logcatter.LogReader
|
import com.zaneschepke.logcatter.LogReader
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationMonitor
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.worker.ServiceWorker
|
import com.zaneschepke.wireguardautotunnel.core.worker.ServiceWorker
|
||||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||||
@@ -18,14 +19,10 @@ import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
|
|||||||
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
|
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.cancel
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@HiltAndroidApp
|
@HiltAndroidApp
|
||||||
@@ -46,6 +43,8 @@ class WireGuardAutoTunnel : Application(), Configuration.Provider {
|
|||||||
|
|
||||||
@Inject @MainDispatcher lateinit var mainDispatcher: CoroutineDispatcher
|
@Inject @MainDispatcher lateinit var mainDispatcher: CoroutineDispatcher
|
||||||
|
|
||||||
|
@Inject lateinit var notificationMonitor: NotificationMonitor
|
||||||
|
|
||||||
@Inject lateinit var tunnelManager: TunnelManager
|
@Inject lateinit var tunnelManager: TunnelManager
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
@@ -80,6 +79,7 @@ class WireGuardAutoTunnel : Application(), Configuration.Provider {
|
|||||||
ServiceWorker.start(this)
|
ServiceWorker.start(this)
|
||||||
|
|
||||||
applicationScope.launch {
|
applicationScope.launch {
|
||||||
|
launch { notificationMonitor.handleApplicationNotifications() }
|
||||||
appDataRepository.appState.getLocale()?.let {
|
appDataRepository.appState.getLocale()?.let {
|
||||||
withContext(mainDispatcher) { LocaleUtil.changeLocale(it) }
|
withContext(mainDispatcher) { LocaleUtil.changeLocale(it) }
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -49,7 +49,8 @@ interface NotificationManager {
|
|||||||
const val AUTO_TUNNEL_NOTIFICATION_ID = 122
|
const val AUTO_TUNNEL_NOTIFICATION_ID = 122
|
||||||
// for tunnel foreground notification
|
// for tunnel foreground notification
|
||||||
const val VPN_NOTIFICATION_ID = 100
|
const val VPN_NOTIFICATION_ID = 100
|
||||||
const val TUNNEL_STATUS_NOTIFICATION_ID = 101
|
const val TUNNEL_ERROR_NOTIFICATION_ID = 101
|
||||||
|
const val TUNNEL_MESSAGES_NOTIFICATION_ID = 102
|
||||||
const val EXTRA_ID = "id"
|
const val EXTRA_ID = "id"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+63
@@ -0,0 +1,63 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.core.notification
|
||||||
|
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.events.BackendError
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||||
|
import jakarta.inject.Inject
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class NotificationMonitor
|
||||||
|
@Inject
|
||||||
|
constructor(
|
||||||
|
private val tunnelManager: TunnelManager,
|
||||||
|
private val notificationManager: NotificationManager,
|
||||||
|
) {
|
||||||
|
suspend fun handleApplicationNotifications() = coroutineScope {
|
||||||
|
launch { handleTunnelErrors() }
|
||||||
|
launch { handleTunnelMessages() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun handleTunnelErrors() =
|
||||||
|
tunnelManager.errorEvents.collectLatest { (tunnelConf, error) ->
|
||||||
|
if (!WireGuardAutoTunnel.uiActive.value) {
|
||||||
|
val notification =
|
||||||
|
notificationManager.createNotification(
|
||||||
|
WireGuardNotification.NotificationChannels.VPN,
|
||||||
|
title = StringValue.DynamicString(tunnelConf.name),
|
||||||
|
description =
|
||||||
|
when (error) {
|
||||||
|
is BackendError.BounceFailed -> error.toStringValue()
|
||||||
|
else ->
|
||||||
|
StringValue.StringResource(
|
||||||
|
R.string.tunnel_error_template,
|
||||||
|
error.toStringRes(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
notificationManager.show(
|
||||||
|
NotificationManager.TUNNEL_ERROR_NOTIFICATION_ID,
|
||||||
|
notification,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun handleTunnelMessages() =
|
||||||
|
tunnelManager.messageEvents.collectLatest { (tunnelConf, message) ->
|
||||||
|
if (!WireGuardAutoTunnel.uiActive.value) {
|
||||||
|
val notification =
|
||||||
|
notificationManager.createNotification(
|
||||||
|
WireGuardNotification.NotificationChannels.VPN,
|
||||||
|
title = StringValue.DynamicString(tunnelConf.name),
|
||||||
|
description = message.toStringValue(),
|
||||||
|
)
|
||||||
|
notificationManager.show(
|
||||||
|
NotificationManager.TUNNEL_MESSAGES_NOTIFICATION_ID,
|
||||||
|
notification,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-6
@@ -7,9 +7,6 @@ import android.content.ServiceConnection
|
|||||||
import android.net.VpnService
|
import android.net.VpnService
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelService
|
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelService
|
||||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
|
||||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
|
||||||
import com.zaneschepke.wireguardautotunnel.di.MainDispatcher
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
|
import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
|
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
|
||||||
@@ -29,9 +26,9 @@ class ServiceManager
|
|||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
private val ioDispatcher: CoroutineDispatcher,
|
||||||
@ApplicationScope private val applicationScope: CoroutineScope,
|
private val applicationScope: CoroutineScope,
|
||||||
@MainDispatcher private val mainDispatcher: CoroutineDispatcher,
|
private val mainDispatcher: CoroutineDispatcher,
|
||||||
private val appDataRepository: AppDataRepository,
|
private val appDataRepository: AppDataRepository,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
|||||||
+26
-186
@@ -7,26 +7,24 @@ import android.os.IBinder
|
|||||||
import androidx.core.app.ServiceCompat
|
import androidx.core.app.ServiceCompat
|
||||||
import androidx.lifecycle.LifecycleService
|
import androidx.lifecycle.LifecycleService
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
|
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelMonitor
|
||||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.distinctByKeys
|
import com.zaneschepke.wireguardautotunnel.util.extensions.distinctByKeys
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import io.ktor.util.collections.*
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
@@ -36,23 +34,18 @@ class TunnelForegroundService : LifecycleService() {
|
|||||||
|
|
||||||
@Inject lateinit var serviceManager: ServiceManager
|
@Inject lateinit var serviceManager: ServiceManager
|
||||||
|
|
||||||
@Inject lateinit var networkMonitor: NetworkMonitor
|
@Inject lateinit var tunnelManager: TunnelManager
|
||||||
|
|
||||||
|
@Inject lateinit var tunnelMonitor: TunnelMonitor
|
||||||
|
|
||||||
@Inject @IoDispatcher lateinit var ioDispatcher: CoroutineDispatcher
|
@Inject @IoDispatcher lateinit var ioDispatcher: CoroutineDispatcher
|
||||||
|
|
||||||
@Inject lateinit var tunnelRepo: TunnelRepository
|
@Inject lateinit var appDataRepository: AppDataRepository
|
||||||
|
|
||||||
@Inject lateinit var tunnelManager: TunnelManager
|
|
||||||
|
|
||||||
private val isNetworkConnected = MutableStateFlow(true)
|
|
||||||
|
|
||||||
private val tunnelJobs = ConcurrentHashMap<TunnelConf, Job>()
|
|
||||||
private val pingJobs = ConcurrentHashMap<TunnelConf, Job>()
|
|
||||||
|
|
||||||
private val jobsMutex = Mutex()
|
|
||||||
|
|
||||||
class LocalBinder(val service: TunnelForegroundService) : Binder()
|
class LocalBinder(val service: TunnelForegroundService) : Binder()
|
||||||
|
|
||||||
|
private val tunnelJobs = ConcurrentMap<TunnelConf, Job>()
|
||||||
|
|
||||||
private val binder = LocalBinder(this)
|
private val binder = LocalBinder(this)
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
@@ -85,94 +78,23 @@ class TunnelForegroundService : LifecycleService() {
|
|||||||
fun start() =
|
fun start() =
|
||||||
lifecycleScope.launch(ioDispatcher) {
|
lifecycleScope.launch(ioDispatcher) {
|
||||||
tunnelManager.activeTunnels.distinctByKeys().collect { activeTunnels ->
|
tunnelManager.activeTunnels.distinctByKeys().collect { activeTunnels ->
|
||||||
// No active tunnels and no jobs: nothing to do
|
val activeTunConfigs = activeTunnels.keys
|
||||||
if (activeTunnels.isEmpty() && tunnelJobs.isEmpty()) return@collect
|
val obsoleteJobs = tunnelJobs.keys - activeTunConfigs
|
||||||
|
obsoleteJobs.forEach { tunnelConf -> tunnelJobs[tunnelConf]?.cancel() }
|
||||||
// Synchronize jobs with active tunnels
|
activeTunConfigs.forEach { tun ->
|
||||||
synchronizeJobs(activeTunnels)
|
if (tunnelJobs.containsKey(tun)) return@forEach
|
||||||
updateServiceNotification()
|
tunnelJobs[tun] = launch { tunnelMonitor.startMonitoring(tun, true) }
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun synchronizeJobs(activeTunnels: Map<TunnelConf, TunnelState>) {
|
|
||||||
jobsMutex.withLock {
|
|
||||||
// Stop jobs for tunnels that are no longer active
|
|
||||||
stopInactiveJobs(activeTunnels)
|
|
||||||
// Start jobs for new tunnels
|
|
||||||
startNewJobs(activeTunnels)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun stopInactiveJobs(activeTunnels: Map<TunnelConf, TunnelState>) {
|
|
||||||
// If no active tunnels, clear all jobs
|
|
||||||
if (activeTunnels.isEmpty()) {
|
|
||||||
clearAllJobs()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Stop jobs for tunnels not in activeTunnels
|
|
||||||
val tunnelsToStop = tunnelJobs.keys - activeTunnels.keys
|
|
||||||
tunnelsToStop.forEach { tun -> stopTunnelJobs(tun) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun clearAllJobs() {
|
|
||||||
tunnelJobs.forEach { (tun, job) ->
|
|
||||||
Timber.d("Stopping tunnel job for ${tun.tunName}")
|
|
||||||
job.cancel()
|
|
||||||
}
|
|
||||||
tunnelJobs.clear()
|
|
||||||
|
|
||||||
pingJobs.forEach { (tun, job) ->
|
|
||||||
if (isPingBounce(tun)) {
|
|
||||||
Timber.d("Preserving ping job for ${tun.tunName} due to PING bounce")
|
|
||||||
return@forEach
|
|
||||||
}
|
|
||||||
Timber.d("Stopping ping job for ${tun.tunName}")
|
|
||||||
job.cancel()
|
|
||||||
}
|
|
||||||
pingJobs.entries.removeIf { (tun, _) -> !isPingBounce(tun) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun stopTunnelJobs(tun: TunnelConf) {
|
|
||||||
tunnelJobs.remove(tun)?.cancel()
|
|
||||||
Timber.d("Stopped tunnel job for ${tun.tunName}")
|
|
||||||
if (isPingBounce(tun))
|
|
||||||
return Timber.d("Preserving ${tun.tunName} ping job due to ping bounce")
|
|
||||||
pingJobs.remove(tun)?.cancel()
|
|
||||||
Timber.d("Stopped ping job for ${tun.tunName}")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startNewJobs(activeTunnels: Map<TunnelConf, TunnelState>) {
|
|
||||||
val tunnelsToStart = activeTunnels.keys - tunnelJobs.keys
|
|
||||||
tunnelsToStart.forEach { tun ->
|
|
||||||
tunnelJobs[tun] = startTunnelJobs(tun)
|
|
||||||
Timber.d("Started tunnel job for ${tun.tunName}")
|
|
||||||
|
|
||||||
if (pingJobs[tun]?.isActive == true) {
|
|
||||||
Timber.d("Reusing active ping job for ${tun.tunName}")
|
|
||||||
} else {
|
|
||||||
pingJobs[tun]?.cancel() // Cancel any stale job
|
|
||||||
if (tun.isPingEnabled) {
|
|
||||||
if (tun.isStaticallyConfigured()) {
|
|
||||||
Timber.d("Skipping ping for statically configured tunnel")
|
|
||||||
} else {
|
|
||||||
pingJobs[tun] = startPingJob(tun)
|
|
||||||
Timber.d("Started ping job for ${tun.tunName}")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
updateServiceNotification(activeTunnels)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun isPingBounce(tun: TunnelConf): Boolean =
|
|
||||||
tunnelManager.bouncingTunnelIds[tun.id] == TunnelStatus.StopReason.PING
|
|
||||||
|
|
||||||
// TODO Would be cool to have this include kill switch
|
// TODO Would be cool to have this include kill switch
|
||||||
// TODO also we need to include errors
|
private fun updateServiceNotification(activeTunnels: Map<TunnelConf, TunnelState>) {
|
||||||
private fun updateServiceNotification() {
|
|
||||||
val notification =
|
val notification =
|
||||||
when (tunnelJobs.size) {
|
when (activeTunnels.size) {
|
||||||
0 -> onCreateNotification()
|
0 -> onCreateNotification()
|
||||||
1 -> createTunnelNotification(tunnelJobs.keys.first())
|
1 -> createTunnelNotification(activeTunnels.keys.first())
|
||||||
else -> createTunnelsNotification()
|
else -> createTunnelsNotification()
|
||||||
}
|
}
|
||||||
ServiceCompat.startForeground(
|
ServiceCompat.startForeground(
|
||||||
@@ -183,90 +105,18 @@ class TunnelForegroundService : LifecycleService() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// use same scope so we can cancel all of these
|
|
||||||
private fun startTunnelJobs(tunnelConf: TunnelConf) =
|
|
||||||
lifecycleScope.launch(ioDispatcher) {
|
|
||||||
// monitor if we have internet connectivity
|
|
||||||
launch { startNetworkMonitorJob() }
|
|
||||||
// job to trigger stats emit on interval
|
|
||||||
launch { startTunnelStatsJob(tunnelConf) }
|
|
||||||
// monitor changes to the tunnel config
|
|
||||||
launch { startTunnelConfChangesJob(tunnelConf) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun startTunnelConfChangesJob(tunnelConf: TunnelConf) {
|
|
||||||
tunnelRepo.flow
|
|
||||||
.flowOn(ioDispatcher)
|
|
||||||
.map { storedTunnels -> storedTunnels.firstOrNull { it.id == tunnelConf.id } }
|
|
||||||
.filterNotNull()
|
|
||||||
// only emit when one of these 3 values change
|
|
||||||
.distinctUntilChanged { old, new -> old == new }
|
|
||||||
.collect { storedTunnel ->
|
|
||||||
if (tunnelConf != storedTunnel) {
|
|
||||||
Timber.d("Config changed for ${storedTunnel.tunName}, bouncing")
|
|
||||||
// let this complete, even after cancel
|
|
||||||
withContext(NonCancellable) {
|
|
||||||
tunnelManager.bounceTunnel(
|
|
||||||
storedTunnel,
|
|
||||||
TunnelStatus.StopReason.CONFIG_CHANGED,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun startNetworkMonitorJob() {
|
|
||||||
networkMonitor.connectivityStateFlow.flowOn(ioDispatcher).collectLatest { status ->
|
|
||||||
isNetworkConnected.value = status.hasConnectivity()
|
|
||||||
Timber.d("Network available: $status")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun startTunnelStatsJob(tunnel: TunnelConf) = coroutineScope {
|
|
||||||
while (isActive) {
|
|
||||||
tunnelManager.updateTunnelStatistics(tunnel)
|
|
||||||
delay(STATS_DELAY)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startPingJob(tunnel: TunnelConf) =
|
|
||||||
lifecycleScope.launch(ioDispatcher) {
|
|
||||||
// delay for initial duration
|
|
||||||
delay(tunnel.pingInterval ?: Constants.PING_INTERVAL)
|
|
||||||
while (isActive) {
|
|
||||||
val shouldBounce = shouldBounceTunnel(tunnel)
|
|
||||||
val delayMs =
|
|
||||||
if (shouldBounce) {
|
|
||||||
// let this complete, even after cancel
|
|
||||||
withContext(NonCancellable) {
|
|
||||||
tunnelManager.bounceTunnel(tunnel, TunnelStatus.StopReason.PING)
|
|
||||||
}
|
|
||||||
tunnel.pingCooldown ?: Constants.PING_COOLDOWN
|
|
||||||
} else {
|
|
||||||
tunnel.pingInterval ?: Constants.PING_INTERVAL
|
|
||||||
}
|
|
||||||
delay(delayMs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun shouldBounceTunnel(tunnel: TunnelConf): Boolean {
|
|
||||||
if (!isNetworkConnected.value) {
|
|
||||||
Timber.d("Network disconnected, skipping ping for ${tunnel.tunName}")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return runCatching { !tunnel.isTunnelPingable(ioDispatcher) }
|
|
||||||
.onFailure { e -> Timber.e(e, "Ping check failed for ${tunnel.tunName}") }
|
|
||||||
.getOrDefault(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun stop() {
|
fun stop() {
|
||||||
|
Timber.d("Stop called")
|
||||||
|
tunnelJobs.forEach { it.value.cancel() }
|
||||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||||
stopSelf()
|
stopSelf()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
tunnelJobs.forEach { it.value.cancel() }
|
||||||
serviceManager.handleTunnelServiceDestroy()
|
serviceManager.handleTunnelServiceDestroy()
|
||||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||||
|
Timber.d("onDestroy")
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,14 +151,4 @@ class TunnelForegroundService : LifecycleService() {
|
|||||||
title = getString(R.string.tunnel_starting),
|
title = getString(R.string.tunnel_starting),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO add notification handling and optional log reading for restart on handshake failures
|
|
||||||
companion object {
|
|
||||||
const val STATS_DELAY = 1_000L
|
|
||||||
// ipv6 disabled or block on network
|
|
||||||
// Failed to send handshake initiation: write udp [::]"
|
|
||||||
// Failed to send data packets: write udp [::]
|
|
||||||
// Failed to send data packets: write udp 0.0.0.0:51820
|
|
||||||
// Handshake did not complete after 5 seconds, retrying
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+259
-122
@@ -7,17 +7,19 @@ import androidx.core.app.ServiceCompat
|
|||||||
import androidx.lifecycle.LifecycleService
|
import androidx.lifecycle.LifecycleService
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
|
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
|
||||||
|
import com.zaneschepke.networkmonitor.ConnectivityState
|
||||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
|
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelMonitor
|
||||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus.StopReason.Ping
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
|
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.events.KillSwitchEvent
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.AppSettings
|
import com.zaneschepke.wireguardautotunnel.domain.model.AppSettings
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||||
@@ -25,12 +27,15 @@ import com.zaneschepke.wireguardautotunnel.domain.state.AutoTunnelState
|
|||||||
import com.zaneschepke.wireguardautotunnel.domain.state.NetworkState
|
import com.zaneschepke.wireguardautotunnel.domain.state.NetworkState
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.Tunnels
|
import com.zaneschepke.wireguardautotunnel.util.extensions.Tunnels
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.zipWithPrevious
|
import com.zaneschepke.wireguardautotunnel.util.extensions.toMillis
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Provider
|
import javax.inject.Provider
|
||||||
|
import kotlin.math.pow
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
@@ -48,11 +53,19 @@ class AutoTunnelService : LifecycleService() {
|
|||||||
|
|
||||||
@Inject lateinit var tunnelManager: TunnelManager
|
@Inject lateinit var tunnelManager: TunnelManager
|
||||||
|
|
||||||
|
@Inject lateinit var tunnelMonitor: TunnelMonitor
|
||||||
|
|
||||||
private val defaultState = AutoTunnelState()
|
private val defaultState = AutoTunnelState()
|
||||||
|
|
||||||
|
private val autoTunMutex = Mutex()
|
||||||
|
|
||||||
private val autoTunnelStateFlow = MutableStateFlow(defaultState)
|
private val autoTunnelStateFlow = MutableStateFlow(defaultState)
|
||||||
|
|
||||||
private var killSwitchJob: Job? = null
|
private val bounceCounts = MutableStateFlow<Map<Int, Int>>(emptyMap())
|
||||||
|
|
||||||
|
private var eventHandlerJob: Job? = null
|
||||||
|
|
||||||
|
private val lastBounceTimes = mutableMapOf<Int, Long>()
|
||||||
|
|
||||||
class LocalBinder(val service: AutoTunnelService) : Binder()
|
class LocalBinder(val service: AutoTunnelService) : Binder()
|
||||||
|
|
||||||
@@ -77,10 +90,8 @@ class AutoTunnelService : LifecycleService() {
|
|||||||
|
|
||||||
fun start() {
|
fun start() {
|
||||||
launchWatcherNotification()
|
launchWatcherNotification()
|
||||||
startAutoTunnelJob()
|
|
||||||
startAutoTunnelStateJob()
|
startAutoTunnelStateJob()
|
||||||
killSwitchJob = startKillSwitchJob()
|
startLocationPermissionsNotificationJob()
|
||||||
startNotificationJob()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stop() {
|
fun stop() {
|
||||||
@@ -90,6 +101,7 @@ class AutoTunnelService : LifecycleService() {
|
|||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
serviceManager.handleAutoTunnelServiceDestroy()
|
serviceManager.handleAutoTunnelServiceDestroy()
|
||||||
restoreVpnKillSwitch()
|
restoreVpnKillSwitch()
|
||||||
|
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,7 +111,7 @@ class AutoTunnelService : LifecycleService() {
|
|||||||
settings.isVpnKillSwitchEnabled &&
|
settings.isVpnKillSwitchEnabled &&
|
||||||
tunnelManager.getBackendState() != BackendState.KILL_SWITCH_ACTIVE
|
tunnelManager.getBackendState() != BackendState.KILL_SWITCH_ACTIVE
|
||||||
) {
|
) {
|
||||||
killSwitchJob?.cancel()
|
eventHandlerJob?.cancel()
|
||||||
val allowedIps =
|
val allowedIps =
|
||||||
if (settings.isLanOnKillSwitchEnabled) TunnelConf.LAN_BYPASS_ALLOWED_IPS
|
if (settings.isLanOnKillSwitchEnabled) TunnelConf.LAN_BYPASS_ALLOWED_IPS
|
||||||
else emptyList()
|
else emptyList()
|
||||||
@@ -131,47 +143,141 @@ class AutoTunnelService : LifecycleService() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
private fun startAutoTunnelStateJob() =
|
private fun startAutoTunnelStateJob() =
|
||||||
lifecycleScope.launch(ioDispatcher) {
|
lifecycleScope.launch(ioDispatcher) {
|
||||||
combine(
|
val networkFlow =
|
||||||
combineSettings(),
|
debouncedConnectivityStateFlow
|
||||||
appDataRepository
|
.flowOn(ioDispatcher)
|
||||||
.get()
|
.map(NetworkState::from)
|
||||||
.settings
|
.map { StateChange.NetworkChange(it) }
|
||||||
.flow
|
.distinctUntilChanged()
|
||||||
.distinctUntilChanged { old, new ->
|
|
||||||
old.isKernelEnabled == new.isKernelEnabled
|
val settingsFlow =
|
||||||
} // Only emit when isKernelEnabled changes
|
combineSettings().map { StateChange.SettingsChange(it.first, it.second) }
|
||||||
.flatMapLatest {
|
|
||||||
networkMonitor.connectivityStateFlow
|
val tunnelsFlow =
|
||||||
.flowOn(ioDispatcher)
|
tunnelManager.activeTunnels.map { StateChange.ActiveTunnelsChange(it) }
|
||||||
.map(NetworkState::from)
|
|
||||||
}
|
val monitoringFlow =
|
||||||
.distinctUntilChanged(),
|
tunnelManager.activeTunnels
|
||||||
) { double, networkState ->
|
.map { map -> map.mapValues { (_, state) -> state.pingStates } }
|
||||||
AutoTunnelState(
|
.distinctUntilChanged()
|
||||||
tunnelManager.activeTunnels.value,
|
.map { StateChange.MonitoringChange(it) }
|
||||||
networkState,
|
|
||||||
double.first,
|
var reevaluationJob: Job? = null
|
||||||
double.second,
|
|
||||||
)
|
// get everything in sync before we use merge
|
||||||
}
|
combine(networkFlow, settingsFlow, tunnelsFlow, monitoringFlow) {
|
||||||
.collect { state ->
|
network,
|
||||||
|
settings,
|
||||||
|
tunnels,
|
||||||
|
monitoring ->
|
||||||
autoTunnelStateFlow.update {
|
autoTunnelStateFlow.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
activeTunnels = state.activeTunnels,
|
activeTunnels = tunnels.activeTunnels,
|
||||||
networkState = state.networkState,
|
networkState = network.networkState,
|
||||||
settings = state.settings,
|
settings = settings.settings,
|
||||||
tunnels = state.tunnels,
|
tunnels = settings.tunnels,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.first()
|
||||||
|
|
||||||
|
// use merge to limit the noise of a combine and also increase the scalability of auto
|
||||||
|
// tunnel handling new states
|
||||||
|
merge(networkFlow, settingsFlow, tunnelsFlow, monitoringFlow).collect { change ->
|
||||||
|
if (change !is StateChange.ActiveTunnelsChange) {
|
||||||
|
Timber.d("New state changed to ${change.javaClass.simpleName}")
|
||||||
|
}
|
||||||
|
|
||||||
|
when (change) {
|
||||||
|
is StateChange.NetworkChange -> {
|
||||||
|
reevaluationJob?.cancel()
|
||||||
|
val previousState = autoTunnelStateFlow.value
|
||||||
|
autoTunnelStateFlow.update { it.copy(networkState = change.networkState) }
|
||||||
|
// Android late mobile data state change, we can ignore handling this
|
||||||
|
if (
|
||||||
|
isAndroidLateCellularActiveChange(
|
||||||
|
previousState.networkState,
|
||||||
|
change.networkState,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Timber.d("Android late cellular active state change")
|
||||||
|
return@collect
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is StateChange.SettingsChange -> {
|
||||||
|
reevaluationJob?.cancel()
|
||||||
|
autoTunnelStateFlow.update {
|
||||||
|
it.copy(settings = change.settings, tunnels = change.tunnels)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is StateChange.ActiveTunnelsChange -> {
|
||||||
|
autoTunnelStateFlow.update { it.copy(activeTunnels = change.activeTunnels) }
|
||||||
|
return@collect
|
||||||
|
}
|
||||||
|
is StateChange.MonitoringChange -> {
|
||||||
|
change.pingStates.forEach { (config, pingState) ->
|
||||||
|
Timber.d("Ping state $pingState")
|
||||||
|
if (pingState?.all { it.value.isReachable } == true) {
|
||||||
|
Timber.d("Clearing bounce count on success")
|
||||||
|
bounceCounts.update { current ->
|
||||||
|
current.toMutableMap().apply { remove(config.id) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return@collect handleAutoTunnelEvent(
|
||||||
|
autoTunnelStateFlow.value.determineAutoTunnelEvent(
|
||||||
|
StateChange.MonitoringChange(change.pingStates)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAutoTunnelEvent(autoTunnelStateFlow.value.determineAutoTunnelEvent(change))
|
||||||
|
|
||||||
|
reevaluationJob = launch {
|
||||||
|
delay(REEVALUATE_CHECK_DELAY)
|
||||||
|
val currentState = autoTunnelStateFlow.value
|
||||||
|
if (currentState != defaultState) {
|
||||||
|
Timber.d("Re-evaluating auto-tunnel state..")
|
||||||
|
handleAutoTunnelEvent(currentState.determineAutoTunnelEvent(change))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isAndroidLateCellularActiveChange(
|
||||||
|
previous: NetworkState,
|
||||||
|
new: NetworkState,
|
||||||
|
): Boolean {
|
||||||
|
return (previous.isWifiConnected != new.isWifiConnected &&
|
||||||
|
previous.wifiName == new.wifiName &&
|
||||||
|
previous.isMobileDataConnected != new.isMobileDataConnected)
|
||||||
|
}
|
||||||
|
|
||||||
|
// all relevant settings to auto tunnel
|
||||||
|
private fun areAutoTunnelSettingsTheSame(old: AppSettings, new: AppSettings): Boolean {
|
||||||
|
return (old.isTunnelOnWifiEnabled == new.isTunnelOnWifiEnabled &&
|
||||||
|
old.isTunnelOnMobileDataEnabled == new.isTunnelOnMobileDataEnabled &&
|
||||||
|
old.isTunnelOnEthernetEnabled == new.isTunnelOnEthernetEnabled &&
|
||||||
|
old.trustedNetworkSSIDs == new.trustedNetworkSSIDs &&
|
||||||
|
old.isPingEnabled == new.isPingEnabled &&
|
||||||
|
old.debounceDelaySeconds == new.debounceDelaySeconds &&
|
||||||
|
old.wifiDetectionMethod == new.wifiDetectionMethod &&
|
||||||
|
old.isVpnKillSwitchEnabled == new.isVpnKillSwitchEnabled &&
|
||||||
|
old.isLanOnKillSwitchEnabled == new.isLanOnKillSwitchEnabled &&
|
||||||
|
old.isDisableKillSwitchOnTrustedEnabled == new.isDisableKillSwitchOnTrustedEnabled &&
|
||||||
|
old.isStopOnNoInternetEnabled == new.isStopOnNoInternetEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
private fun combineSettings(): Flow<Pair<AppSettings, Tunnels>> {
|
private fun combineSettings(): Flow<Pair<AppSettings, Tunnels>> {
|
||||||
return combine(
|
return combine(
|
||||||
appDataRepository.get().settings.flow,
|
appDataRepository
|
||||||
|
.get()
|
||||||
|
.settings
|
||||||
|
.flow
|
||||||
|
.distinctUntilChanged(::areAutoTunnelSettingsTheSame),
|
||||||
appDataRepository.get().tunnels.flow.map { tunnels ->
|
appDataRepository.get().tunnels.flow.map { tunnels ->
|
||||||
// isActive is ignored for equality checks so user can manually toggle off
|
// isActive is ignored for equality checks so user can manually toggle off
|
||||||
// tunnel with auto-tunnel
|
// tunnel with auto-tunnel
|
||||||
@@ -183,25 +289,53 @@ class AutoTunnelService : LifecycleService() {
|
|||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
// watch for changes to location permission and notify user it will impact auto-tunneling
|
private fun areAutoTunnelPermissionsRequiredTheSame(
|
||||||
// TODO can add deeplinks later back to the app for fixing
|
old: AutoTunnelState,
|
||||||
// TODO or a recheck button for location permission so we dont have to poll it
|
new: AutoTunnelState,
|
||||||
private fun startNotificationJob(): Job =
|
): Boolean {
|
||||||
|
return (old.settings.wifiDetectionMethod == new.settings.wifiDetectionMethod &&
|
||||||
|
old.networkState.locationPermissionGranted ==
|
||||||
|
new.networkState.locationPermissionGranted &&
|
||||||
|
old.networkState.locationServicesEnabled == new.networkState.locationServicesEnabled &&
|
||||||
|
old.tunnels == new.tunnels &&
|
||||||
|
old.settings.trustedNetworkSSIDs == new.settings.trustedNetworkSSIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// watch for changes to location permission and notify user it will impact auto-tunneling
|
||||||
|
// TODO or a recheck button for location permission so we dont have to poll it
|
||||||
|
private fun startLocationPermissionsNotificationJob(): Job =
|
||||||
lifecycleScope.launch(ioDispatcher) {
|
lifecycleScope.launch(ioDispatcher) {
|
||||||
var locationServicesShown = false
|
var locationServicesShown = false
|
||||||
var locationPermissionsShown = false
|
var locationPermissionsShown = false
|
||||||
autoTunnelStateFlow.zipWithPrevious().collect { (previous, current) ->
|
|
||||||
when (current.settings.wifiDetectionMethod) {
|
data class NetworkPermissionState(
|
||||||
AndroidNetworkMonitor.WifiDetectionMethod.DEFAULT,
|
val detectionMethod: AndroidNetworkMonitor.WifiDetectionMethod,
|
||||||
AndroidNetworkMonitor.WifiDetectionMethod.LEGACY -> {
|
val locationServicesEnabled: Boolean,
|
||||||
with(current.networkState) {
|
val locationPermissionsEnabled: Boolean,
|
||||||
|
val ssidReadRequired: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
|
autoTunnelStateFlow
|
||||||
|
.distinctUntilChanged(::areAutoTunnelPermissionsRequiredTheSame)
|
||||||
|
.map {
|
||||||
|
NetworkPermissionState(
|
||||||
|
it.settings.wifiDetectionMethod,
|
||||||
|
it.networkState.locationServicesEnabled == true,
|
||||||
|
it.networkState.locationPermissionGranted == true,
|
||||||
|
(it.tunnels.any { tunnel -> tunnel.tunnelNetworks.isNotEmpty() } ||
|
||||||
|
it.settings.trustedNetworkSSIDs.isNotEmpty()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.collect { state ->
|
||||||
|
when (state.detectionMethod) {
|
||||||
|
AndroidNetworkMonitor.WifiDetectionMethod.DEFAULT,
|
||||||
|
AndroidNetworkMonitor.WifiDetectionMethod.LEGACY -> {
|
||||||
if (
|
if (
|
||||||
locationPermissionGranted == false &&
|
!state.locationPermissionsEnabled &&
|
||||||
(previous?.networkState?.locationPermissionGranted == true ||
|
!locationPermissionsShown &&
|
||||||
!locationServicesShown)
|
state.ssidReadRequired
|
||||||
) {
|
) {
|
||||||
locationServicesShown = true
|
locationPermissionsShown = true
|
||||||
Timber.i("Detected location permission lost")
|
|
||||||
val notification =
|
val notification =
|
||||||
notificationManager.createNotification(
|
notificationManager.createNotification(
|
||||||
WireGuardNotification.NotificationChannels.AUTO_TUNNEL,
|
WireGuardNotification.NotificationChannels.AUTO_TUNNEL,
|
||||||
@@ -215,12 +349,11 @@ class AutoTunnelService : LifecycleService() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
locationServicesEnabled == false &&
|
!state.locationServicesEnabled &&
|
||||||
(previous?.networkState?.locationServicesEnabled == true ||
|
!locationServicesShown &&
|
||||||
!locationPermissionsShown)
|
state.ssidReadRequired
|
||||||
) {
|
) {
|
||||||
locationPermissionsShown = true
|
locationServicesShown = true
|
||||||
Timber.i("Detected location services lost")
|
|
||||||
val notification =
|
val notification =
|
||||||
notificationManager.createNotification(
|
notificationManager.createNotification(
|
||||||
WireGuardNotification.NotificationChannels.AUTO_TUNNEL,
|
WireGuardNotification.NotificationChannels.AUTO_TUNNEL,
|
||||||
@@ -233,93 +366,97 @@ class AutoTunnelService : LifecycleService() {
|
|||||||
notification,
|
notification,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (
|
if (state.locationServicesEnabled || !state.ssidReadRequired) {
|
||||||
locationServicesEnabled == true &&
|
|
||||||
previous?.networkState?.locationServicesEnabled == false
|
|
||||||
) {
|
|
||||||
notificationManager.remove(
|
notificationManager.remove(
|
||||||
NotificationManager.AUTO_TUNNEL_LOCATION_SERVICES_ID
|
NotificationManager.AUTO_TUNNEL_LOCATION_SERVICES_ID
|
||||||
)
|
)
|
||||||
|
locationServicesShown = false
|
||||||
}
|
}
|
||||||
if (
|
if (state.locationPermissionsEnabled || !state.ssidReadRequired) {
|
||||||
locationPermissionGranted == true &&
|
|
||||||
previous?.networkState?.locationPermissionGranted == false
|
|
||||||
) {
|
|
||||||
notificationManager.remove(
|
notificationManager.remove(
|
||||||
NotificationManager.AUTO_TUNNEL_LOCATION_PERMISSION_ID
|
NotificationManager.AUTO_TUNNEL_LOCATION_PERMISSION_ID
|
||||||
)
|
)
|
||||||
|
locationPermissionsShown = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else -> Unit
|
||||||
}
|
}
|
||||||
else -> Unit
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun handleAutoTunnelEvent(autoTunnelEvent: AutoTunnelEvent) {
|
||||||
|
autoTunMutex.withLock {
|
||||||
|
when (
|
||||||
|
val event =
|
||||||
|
autoTunnelEvent.also {
|
||||||
|
Timber.i("Auto tunnel event: ${it.javaClass.simpleName}")
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
is AutoTunnelEvent.Start ->
|
||||||
|
(event.tunnelConf ?: appDataRepository.get().getPrimaryOrFirstTunnel())?.let {
|
||||||
|
tunnelManager.startTunnel(it)
|
||||||
|
}
|
||||||
|
is AutoTunnelEvent.Stop -> tunnelManager.stopTunnel()
|
||||||
|
AutoTunnelEvent.DoNothing -> Timber.i("Auto-tunneling: nothing to do")
|
||||||
|
is AutoTunnelEvent.Bounce ->
|
||||||
|
handleBounceWithBackoff(event.configsPeerKeyResolvedMap)
|
||||||
|
is AutoTunnelEvent.StartKillSwitch -> {
|
||||||
|
Timber.d("Starting kill switch")
|
||||||
|
tunnelManager.setBackendState(BackendState.KILL_SWITCH_ACTIVE, event.allowedIps)
|
||||||
|
}
|
||||||
|
AutoTunnelEvent.StopKillSwitch -> {
|
||||||
|
Timber.d("Stopping kill switch")
|
||||||
|
tunnelManager.setBackendState(BackendState.SERVICE_ACTIVE, emptySet())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun startKillSwitchJob() =
|
private suspend fun handleBounceWithBackoff(
|
||||||
lifecycleScope.launch(ioDispatcher) {
|
configsPeerKeyResolvedMap: List<Pair<TunnelConf, Map<String, String?>>>
|
||||||
autoTunnelStateFlow.collect {
|
) { // Simplified param: no failureCount
|
||||||
if (it == defaultState) return@collect
|
val settings = appDataRepository.get().settings.get()
|
||||||
when (val event = it.asKillSwitchEvent()) {
|
val pingIntervalMillis = settings.tunnelPingIntervalSeconds.toMillis()
|
||||||
KillSwitchEvent.DoNothing -> Unit
|
configsPeerKeyResolvedMap.forEach { (config, peerMap) ->
|
||||||
is KillSwitchEvent.Start -> {
|
val bounceCount = bounceCounts.value.getOrDefault(config.id, 0)
|
||||||
Timber.d("Starting kill switch")
|
val exponent = bounceCount.toDouble()
|
||||||
tunnelManager.setBackendState(
|
val backoffDelay =
|
||||||
BackendState.KILL_SWITCH_ACTIVE,
|
(pingIntervalMillis * 2.0.pow(exponent)).toLong().coerceAtMost(MAX_BACKOFF_MS)
|
||||||
event.allowedIps,
|
val currentTime = System.currentTimeMillis()
|
||||||
)
|
val lastTime = lastBounceTimes.getOrDefault(config.id, 0L)
|
||||||
}
|
if (currentTime - lastTime >= backoffDelay) {
|
||||||
KillSwitchEvent.Stop -> {
|
Timber.d(
|
||||||
Timber.d("Stopping kill switch")
|
"Bouncing tunnel ${config.name} after detecting failure, with bounce count $bounceCount and calculated backoff delay $backoffDelay ms"
|
||||||
tunnelManager.setBackendState(BackendState.SERVICE_ACTIVE, emptySet())
|
)
|
||||||
}
|
tunnelManager.bounceTunnel(config, Ping(peerMap))
|
||||||
|
lastBounceTimes[config.id] = currentTime
|
||||||
|
bounceCounts.update { current ->
|
||||||
|
current.toMutableMap().apply { this[config.id] = (this[config.id] ?: 0) + 1 }
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
Timber.d(
|
||||||
|
"Backoff in progress for tunnel ${config.name}, skipping bounce (required delay: $backoffDelay ms)"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(FlowPreview::class)
|
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
|
||||||
private fun startAutoTunnelJob() =
|
private val debouncedConnectivityStateFlow: Flow<ConnectivityState> by lazy {
|
||||||
lifecycleScope.launch(ioDispatcher) {
|
appDataRepository
|
||||||
Timber.i("Starting auto-tunnel network event watcher")
|
.get()
|
||||||
val settings = appDataRepository.get().settings.get()
|
.settings
|
||||||
|
.flow
|
||||||
var reevaluationJob: Job? = null
|
.map { it.debounceDelaySeconds.toMillis() }
|
||||||
|
.distinctUntilChanged()
|
||||||
autoTunnelStateFlow.debounce(settings.debounceDelayMillis()).collect { watcherState ->
|
.flatMapLatest { debounceMillis ->
|
||||||
if (watcherState == defaultState) return@collect
|
networkMonitor.connectivityStateFlow.debounce(debounceMillis)
|
||||||
reevaluationJob?.cancel()
|
|
||||||
handleAutoTunnelEvent(watcherState)
|
|
||||||
|
|
||||||
// schedule one-time re-evaluation
|
|
||||||
reevaluationJob = launch {
|
|
||||||
delay(REEVALUATE_CHECK_DELAY)
|
|
||||||
if (watcherState != defaultState) {
|
|
||||||
Timber.d("Re-evaluating auto-tunnel state..")
|
|
||||||
handleAutoTunnelEvent(watcherState)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun handleAutoTunnelEvent(watcherState: AutoTunnelState) {
|
|
||||||
Timber.i("Auto-tunnel settings: ${watcherState.settings.toAutoTunnelStateString()}")
|
|
||||||
Timber.i("Auto-tunnel network state: ${watcherState.networkState}")
|
|
||||||
when (
|
|
||||||
val event =
|
|
||||||
watcherState.asAutoTunnelEvent().also {
|
|
||||||
Timber.i("Auto-tunnel event: ${it.javaClass.simpleName}")
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
is AutoTunnelEvent.Start ->
|
|
||||||
(event.tunnelConf ?: appDataRepository.get().getPrimaryOrFirstTunnel())?.let {
|
|
||||||
tunnelManager.startTunnel(it)
|
|
||||||
}
|
|
||||||
is AutoTunnelEvent.Stop -> tunnelManager.stopTunnel()
|
|
||||||
AutoTunnelEvent.DoNothing -> Timber.i("Auto-tunneling: nothing to do")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val REEVALUATE_CHECK_DELAY = 5_000L
|
// try to keep this window short as it will interrupt manual overrides
|
||||||
|
const val REEVALUATE_CHECK_DELAY = 2_000L
|
||||||
|
const val MAX_BACKOFF_MS = 300_000L // 5 minutes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.core.service.autotunnel
|
||||||
|
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.model.AppSettings
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.state.NetworkState
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.state.PingState
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.Tunnels
|
||||||
|
import org.amnezia.awg.crypto.Key
|
||||||
|
|
||||||
|
sealed class StateChange {
|
||||||
|
data class NetworkChange(val networkState: NetworkState) : StateChange()
|
||||||
|
|
||||||
|
data class SettingsChange(val settings: AppSettings, val tunnels: Tunnels) : StateChange()
|
||||||
|
|
||||||
|
data class ActiveTunnelsChange(val activeTunnels: Map<TunnelConf, TunnelState>) : StateChange()
|
||||||
|
|
||||||
|
data class MonitoringChange(val pingStates: Map<TunnelConf, Map<Key, PingState>?>) :
|
||||||
|
StateChange()
|
||||||
|
}
|
||||||
@@ -2,37 +2,42 @@ package com.zaneschepke.wireguardautotunnel.core.tunnel
|
|||||||
|
|
||||||
import com.wireguard.android.backend.Tunnel
|
import com.wireguard.android.backend.Tunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendError
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.events.BackendError
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.state.PingState
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.state.ConfigProxy
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState
|
import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import kotlin.concurrent.thread
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import org.amnezia.awg.crypto.Key
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
abstract class BaseTunnel(
|
abstract class BaseTunnel(
|
||||||
@ApplicationScope private val applicationScope: CoroutineScope,
|
private val applicationScope: CoroutineScope,
|
||||||
private val appDataRepository: AppDataRepository,
|
private val appDataRepository: AppDataRepository,
|
||||||
private val serviceManager: ServiceManager,
|
private val serviceManager: ServiceManager,
|
||||||
) : TunnelProvider {
|
) : TunnelProvider {
|
||||||
|
|
||||||
private val _errorEvents =
|
private val _errorEvents = MutableSharedFlow<Pair<TunnelConf, BackendError>>()
|
||||||
MutableSharedFlow<Pair<TunnelConf, BackendError>>(replay = 0, extraBufferCapacity = 1)
|
|
||||||
override val errorEvents = _errorEvents.asSharedFlow()
|
override val errorEvents = _errorEvents.asSharedFlow()
|
||||||
|
|
||||||
|
private val _messageEvents = MutableSharedFlow<Pair<TunnelConf, BackendMessage>>()
|
||||||
|
override val messageEvents = _messageEvents.asSharedFlow()
|
||||||
|
|
||||||
private val activeTuns = MutableStateFlow<Map<TunnelConf, TunnelState>>(emptyMap())
|
private val activeTuns = MutableStateFlow<Map<TunnelConf, TunnelState>>(emptyMap())
|
||||||
private val tunThreads = ConcurrentHashMap<Int, Thread>()
|
private val tunJobs = ConcurrentHashMap<Int, Job>()
|
||||||
override val activeTunnels = activeTuns.asStateFlow()
|
override val activeTunnels = activeTuns.asStateFlow()
|
||||||
|
|
||||||
private val tunMutex = Mutex()
|
private val tunMutex = Mutex()
|
||||||
@@ -49,32 +54,43 @@ abstract class BaseTunnel(
|
|||||||
return serviceManager.hasVpnPermission()
|
return serviceManager.hasVpnPermission()
|
||||||
}
|
}
|
||||||
|
|
||||||
protected suspend fun updateTunnelStatus(
|
override suspend fun updateTunnelStatus(
|
||||||
tunnelConf: TunnelConf,
|
tunnelConf: TunnelConf,
|
||||||
status: TunnelStatus? = null,
|
status: TunnelStatus?,
|
||||||
stats: TunnelStatistics? = null,
|
stats: TunnelStatistics?,
|
||||||
|
pingStates: Map<Key, PingState>?,
|
||||||
|
handshakeSuccessLogs: Boolean?,
|
||||||
) {
|
) {
|
||||||
tunStatusMutex.withLock {
|
tunStatusMutex.withLock {
|
||||||
activeTuns.update { currentTuns ->
|
activeTuns.update { currentTuns ->
|
||||||
val originalConf = currentTuns.getKeyById(tunnelConf.id) ?: tunnelConf
|
val originalConf = currentTuns.getKeyById(tunnelConf.id) ?: tunnelConf
|
||||||
val existingState = currentTuns.getValueById(tunnelConf.id) ?: TunnelState()
|
val existingState = currentTuns.getValueById(tunnelConf.id) ?: TunnelState()
|
||||||
val newState = status ?: existingState.status
|
val newStatus = status ?: existingState.status
|
||||||
if (newState == TunnelStatus.Down) {
|
if (newStatus == TunnelStatus.Down) {
|
||||||
Timber.d("Removing tunnel ${tunnelConf.id} from activeTunnels as state is DOWN")
|
Timber.d("Removing tunnel ${tunnelConf.id} from activeTunnels as state is DOWN")
|
||||||
cleanUpTunThread(tunnelConf)
|
cleanUpTunJob(tunnelConf)
|
||||||
currentTuns - originalConf
|
currentTuns - originalConf
|
||||||
} else if (existingState.status == newState && stats == null) {
|
} else if (
|
||||||
Timber.d("Skipping redundant state update for ${tunnelConf.id}: $newState")
|
existingState.status == newStatus &&
|
||||||
|
stats == null &&
|
||||||
|
pingStates == null &&
|
||||||
|
handshakeSuccessLogs == null
|
||||||
|
) {
|
||||||
|
Timber.d("Skipping redundant state update for ${tunnelConf.id}: $newStatus")
|
||||||
currentTuns
|
currentTuns
|
||||||
} else {
|
} else {
|
||||||
val updated =
|
val updated =
|
||||||
existingState.copy(
|
existingState.copy(
|
||||||
status = newState,
|
status = newStatus,
|
||||||
statistics = stats ?: existingState.statistics,
|
statistics = stats ?: existingState.statistics,
|
||||||
|
pingStates = pingStates ?: existingState.pingStates,
|
||||||
|
handshakeSuccessLogs =
|
||||||
|
handshakeSuccessLogs ?: existingState.handshakeSuccessLogs,
|
||||||
)
|
)
|
||||||
currentTuns + (originalConf to updated)
|
currentTuns + (originalConf to updated)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
handleServiceStateOnChange()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,44 +120,100 @@ abstract class BaseTunnel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun updateTunnelStatistics(tunnel: TunnelConf) {
|
|
||||||
val stats = getStatistics(tunnel)
|
|
||||||
updateTunnelStatus(tunnel, null, stats)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun startTunnel(tunnelConf: TunnelConf) {
|
override suspend fun startTunnel(tunnelConf: TunnelConf) {
|
||||||
if (activeTuns.exists(tunnelConf.id) || tunThreads.containsKey(tunnelConf.id)) return
|
if (activeTuns.exists(tunnelConf.id) || tunJobs.containsKey(tunnelConf.id))
|
||||||
|
return Timber.w("Tunnel is already running ${tunnelConf.name}")
|
||||||
|
// For userspace, we need to make sure all previous tunnels are down
|
||||||
if (this@BaseTunnel is UserspaceTunnel) stopActiveTunnels()
|
if (this@BaseTunnel is UserspaceTunnel) stopActiveTunnels()
|
||||||
tunMutex.withLock {
|
tunMutex.withLock {
|
||||||
tunThreads[tunnelConf.id] = thread {
|
val job =
|
||||||
try {
|
applicationScope.launch {
|
||||||
runBlocking {
|
try {
|
||||||
Timber.d("Starting tunnel ${tunnelConf.id}...")
|
Timber.d("Starting tunnel ${tunnelConf.id}...")
|
||||||
startTunnelInner(tunnelConf)
|
startTunnelInner(tunnelConf)
|
||||||
Timber.d("Started complete for tunnel ${tunnelConf.name}...")
|
Timber.d("Started complete for tunnel ${tunnelConf.name}...")
|
||||||
|
// catch cancellation that could occur before and during startTunnelInner
|
||||||
|
// and trigger at that suspend point
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
Timber.w(
|
||||||
|
"Tunnel start has been cancelled as ${tunnelConf.name} failed to start"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} catch (e: InterruptedException) {
|
|
||||||
Timber.w(
|
|
||||||
"Tunnel start has been interrupted as ${tunnelConf.name} failed to start"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
tunJobs[tunnelConf.id] = job
|
||||||
|
job.invokeOnCompletion {
|
||||||
|
tunJobs.remove(tunnelConf.id)
|
||||||
|
Timber.d("Start job completed for tunnel ${tunnelConf.id}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun startTunnelInner(tunnelConf: TunnelConf) {
|
private suspend fun startTunnelInner(tunnelConf: TunnelConf) {
|
||||||
configureTunnelCallbacks(tunnelConf)
|
configureTunnelCallbacks(tunnelConf)
|
||||||
Timber.d("Starting backend for tunnel ${tunnelConf.id}...")
|
Timber.d("Starting backend for tunnel ${tunnelConf.id}...")
|
||||||
try {
|
|
||||||
startBackend(tunnelConf)
|
var currentConf = tunnelConf
|
||||||
updateTunnelStatus(tunnelConf, TunnelStatus.Up)
|
var restoreAttempted = false
|
||||||
Timber.d("Started for tun ${tunnelConf.id}...")
|
var originalError: BackendError? = null
|
||||||
saveTunnelActiveState(tunnelConf, true)
|
|
||||||
serviceManager.startTunnelForegroundService()
|
while (true) {
|
||||||
} catch (e: BackendError) {
|
try {
|
||||||
Timber.e(e, "Failed to start backend for ${tunnelConf.name}")
|
startBackend(currentConf)
|
||||||
_errorEvents.emit(tunnelConf to e)
|
updateTunnelStatus(currentConf, TunnelStatus.Up)
|
||||||
updateTunnelStatus(tunnelConf, TunnelStatus.Down)
|
Timber.d("Started for tun ${currentConf.id}...")
|
||||||
|
saveTunnelActiveState(currentConf, true)
|
||||||
|
serviceManager.startTunnelForegroundService()
|
||||||
|
if (restoreAttempted)
|
||||||
|
_messageEvents.emit(tunnelConf to BackendMessage.BounceRecovery)
|
||||||
|
if (bouncingTunnelIds[currentConf.id] is TunnelStatus.StopReason.Ping) {
|
||||||
|
_messageEvents.emit(tunnelConf to BackendMessage.BounceSuccess)
|
||||||
|
}
|
||||||
|
return // Success, return
|
||||||
|
} catch (e: BackendError) {
|
||||||
|
originalError = originalError ?: e
|
||||||
|
val bounceReason = bouncingTunnelIds[currentConf.id]
|
||||||
|
if (!restoreAttempted && bounceReason is TunnelStatus.StopReason.Ping) {
|
||||||
|
Timber.i(
|
||||||
|
"Attempting to recover bounce failure with previously resolved endpoints for ${currentConf.name}"
|
||||||
|
)
|
||||||
|
try {
|
||||||
|
val previouslyResolved = bounceReason.previouslyResolvedEndpoints
|
||||||
|
val configProxy = ConfigProxy.from(currentConf.toAmConfig())
|
||||||
|
val updatedConfigProxy =
|
||||||
|
configProxy.copy(
|
||||||
|
peers =
|
||||||
|
configProxy.peers.map {
|
||||||
|
it.copy(
|
||||||
|
endpoint =
|
||||||
|
previouslyResolved[it.publicKey] ?: it.endpoint
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
val (wg, amnezia) = updatedConfigProxy.buildConfigs()
|
||||||
|
currentConf =
|
||||||
|
currentConf.copyWithCallback(
|
||||||
|
amQuick = amnezia.toAwgQuickString(true),
|
||||||
|
wgQuick = wg.toWgQuickString(true),
|
||||||
|
)
|
||||||
|
bouncingTunnelIds.remove(currentConf.id)
|
||||||
|
restoreAttempted = true
|
||||||
|
continue // Retry
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(
|
||||||
|
e,
|
||||||
|
"Failed to update config with resolved endpoints for ${currentConf.name}",
|
||||||
|
)
|
||||||
|
// Fall through to failure (will emit BounceFailed since
|
||||||
|
// retryAttempted=true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Timber.e(e, "Failed to start backend for ${currentConf.name}")
|
||||||
|
val emitError =
|
||||||
|
if (restoreAttempted) BackendError.BounceFailed(originalError) else e
|
||||||
|
_errorEvents.emit(currentConf to emitError)
|
||||||
|
updateTunnelStatus(currentConf, TunnelStatus.Down)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,30 +246,23 @@ abstract class BaseTunnel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleServiceStateOnChange() {
|
private fun handleServiceStateOnChange() {
|
||||||
if (activeTuns.value.isEmpty() && bouncingTunnelIds.isEmpty())
|
if (activeTuns.value.isEmpty()) serviceManager.stopTunnelForegroundService()
|
||||||
serviceManager.stopTunnelForegroundService()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun handleStuckStartingTunnelShutdown(tunnel: TunnelConf) {
|
private suspend fun handleStuckStartingTunnelShutdown(tunnel: TunnelConf) {
|
||||||
Timber.d("Stuck in starting state so shutting down tunnel thread for tunnel ${tunnel.name}")
|
Timber.d("Stuck in starting state so cancelling job for tunnel ${tunnel.name}")
|
||||||
try {
|
try {
|
||||||
tunThreads[tunnel.id]?.let {
|
tunJobs[tunnel.id]?.cancel() ?: Timber.d("No job found for ${tunnel.name}")
|
||||||
if (it.state != Thread.State.TERMINATED) {
|
|
||||||
it.interrupt()
|
|
||||||
} else {
|
|
||||||
Timber.d("Thread already terminated")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "Failed to stop tunnel thread for ${tunnel.name}")
|
Timber.e(e, "Failed to cancel job for ${tunnel.name}")
|
||||||
} finally {
|
} finally {
|
||||||
updateTunnelStatus(tunnel, TunnelStatus.Down)
|
updateTunnelStatus(tunnel, TunnelStatus.Down)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cleanUpTunThread(tunnel: TunnelConf) {
|
private fun cleanUpTunJob(tunnel: TunnelConf) {
|
||||||
Timber.d("Removing thread for ${tunnel.name}")
|
Timber.d("Removing job for ${tunnel.name}")
|
||||||
tunThreads -= tunnel.id
|
tunJobs -= tunnel.id
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun removeActiveTunnel(tunnelConf: TunnelConf) {
|
private fun removeActiveTunnel(tunnelConf: TunnelConf) {
|
||||||
@@ -210,16 +275,10 @@ abstract class BaseTunnel(
|
|||||||
"Bounce tunnel ${tunnelConf.name} for reason: $reason, current bouncing: ${bouncingTunnelIds.size}"
|
"Bounce tunnel ${tunnelConf.name} for reason: $reason, current bouncing: ${bouncingTunnelIds.size}"
|
||||||
)
|
)
|
||||||
bouncingTunnelIds[tunnelConf.id] = reason
|
bouncingTunnelIds[tunnelConf.id] = reason
|
||||||
try {
|
runCatching {
|
||||||
stopTunnel(tunnelConf, reason)
|
stopTunnel(tunnelConf, reason)
|
||||||
delay(BOUNCE_DELAY)
|
delay(BOUNCE_DELAY)
|
||||||
startTunnel(tunnelConf)
|
startTunnel(tunnelConf)
|
||||||
} finally {
|
|
||||||
bouncingTunnelIds.remove(tunnelConf.id)
|
|
||||||
handleServiceStateOnChange()
|
|
||||||
Timber.d(
|
|
||||||
"Cleared bounce state for ${tunnelConf.name}, remaining: ${bouncingTunnelIds.size}"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import com.wireguard.android.backend.BackendException
|
|||||||
import com.wireguard.android.backend.Tunnel
|
import com.wireguard.android.backend.Tunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendError
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.events.BackendError
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
||||||
@@ -42,7 +42,11 @@ constructor(
|
|||||||
updateTunnelStatus(tunnel, TunnelStatus.Starting)
|
updateTunnelStatus(tunnel, TunnelStatus.Starting)
|
||||||
backend.setState(tunnel, Tunnel.State.UP, tunnel.toWgConfig())
|
backend.setState(tunnel, Tunnel.State.UP, tunnel.toWgConfig())
|
||||||
} catch (e: BackendException) {
|
} catch (e: BackendException) {
|
||||||
|
Timber.e(e, "Failed to start up backend for tunnel ${tunnel.name}")
|
||||||
throw e.toBackendError()
|
throw e.toBackendError()
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
Timber.e(e, "Failed to start up backend for tunnel ${tunnel.name}")
|
||||||
|
throw BackendError.Config
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+56
-82
@@ -1,35 +1,32 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
|
|
||||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
|
||||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
|
||||||
import com.zaneschepke.wireguardautotunnel.di.Kernel
|
|
||||||
import com.zaneschepke.wireguardautotunnel.di.Userspace
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendError
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.events.BackendError
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.state.PingState
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
||||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
|
import kotlinx.coroutines.plus
|
||||||
|
import org.amnezia.awg.crypto.Key
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
class TunnelManager
|
class TunnelManager
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
@Kernel private val kernelTunnel: TunnelProvider,
|
private val kernelTunnel: TunnelProvider,
|
||||||
@Userspace private val userspaceTunnel: TunnelProvider,
|
private val userspaceTunnel: TunnelProvider,
|
||||||
private val appDataRepository: AppDataRepository,
|
private val appDataRepository: AppDataRepository,
|
||||||
@ApplicationScope private val applicationScope: CoroutineScope,
|
applicationScope: CoroutineScope,
|
||||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
ioDispatcher: CoroutineDispatcher,
|
||||||
private val notificationManager: NotificationManager,
|
|
||||||
) : TunnelProvider {
|
) : TunnelProvider {
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
@@ -45,63 +42,29 @@ constructor(
|
|||||||
initialValue = userspaceTunnel,
|
initialValue = userspaceTunnel,
|
||||||
)
|
)
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
override val activeTunnels: StateFlow<Map<TunnelConf, TunnelState>> =
|
||||||
override val activeTunnels =
|
tunnelProviderFlow.value.activeTunnels
|
||||||
appDataRepository.settings.flow
|
|
||||||
.filterNotNull()
|
|
||||||
.flatMapLatest { settings ->
|
|
||||||
if (settings.isKernelEnabled) {
|
|
||||||
kernelTunnel.activeTunnels
|
|
||||||
} else {
|
|
||||||
userspaceTunnel.activeTunnels
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.stateIn(
|
|
||||||
scope = applicationScope.plus(ioDispatcher),
|
|
||||||
started = SharingStarted.Eagerly,
|
|
||||||
initialValue = emptyMap(),
|
|
||||||
)
|
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
override val errorEvents: SharedFlow<Pair<TunnelConf, BackendError>> =
|
override val errorEvents: SharedFlow<Pair<TunnelConf, BackendError>> =
|
||||||
combine(
|
tunnelProviderFlow
|
||||||
tunnelProviderFlow.flatMapLatest { it.errorEvents },
|
.flatMapLatest { it.errorEvents }
|
||||||
WireGuardAutoTunnel.uiActive,
|
|
||||||
) { errorEvent, isEnabled ->
|
|
||||||
if (isEnabled) errorEvent else null
|
|
||||||
}
|
|
||||||
.filterNotNull()
|
|
||||||
.shareIn(
|
.shareIn(
|
||||||
scope = applicationScope.plus(ioDispatcher),
|
scope = applicationScope.plus(ioDispatcher),
|
||||||
started = SharingStarted.WhileSubscribed(5_000),
|
started = SharingStarted.Eagerly,
|
||||||
replay = 0,
|
replay = 0,
|
||||||
)
|
)
|
||||||
|
|
||||||
// observe tunnel errors and launch notifications if ui is inactive
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
init {
|
override val messageEvents: SharedFlow<Pair<TunnelConf, BackendMessage>> =
|
||||||
applicationScope.launch(ioDispatcher) {
|
tunnelProviderFlow
|
||||||
tunnelProviderFlow
|
.flatMapLatest { it.messageEvents }
|
||||||
.flatMapLatest { it.errorEvents }
|
.filterNotNull()
|
||||||
.collect { (tunnelConf, error) ->
|
.shareIn(
|
||||||
if (!WireGuardAutoTunnel.uiActive.value) {
|
scope = applicationScope.plus(ioDispatcher),
|
||||||
val notification =
|
started = SharingStarted.Eagerly,
|
||||||
notificationManager.createNotification(
|
replay = 0,
|
||||||
WireGuardNotification.NotificationChannels.VPN,
|
)
|
||||||
title = StringValue.DynamicString(tunnelConf.name),
|
|
||||||
description =
|
|
||||||
StringValue.StringResource(
|
|
||||||
R.string.tunnel_error_template,
|
|
||||||
error.toStringRes(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
notificationManager.show(
|
|
||||||
NotificationManager.TUNNEL_STATUS_NOTIFICATION_ID,
|
|
||||||
notification,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override val bouncingTunnelIds: ConcurrentHashMap<Int, TunnelStatus.StopReason> =
|
override val bouncingTunnelIds: ConcurrentHashMap<Int, TunnelStatus.StopReason> =
|
||||||
tunnelProviderFlow.value.bouncingTunnelIds
|
tunnelProviderFlow.value.bouncingTunnelIds
|
||||||
@@ -110,8 +73,8 @@ constructor(
|
|||||||
return userspaceTunnel.hasVpnPermission()
|
return userspaceTunnel.hasVpnPermission()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun updateTunnelStatistics(tunnel: TunnelConf) {
|
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics? {
|
||||||
tunnelProviderFlow.value.updateTunnelStatistics(tunnel)
|
return tunnelProviderFlow.value.getStatistics(tunnelConf)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun startTunnel(tunnelConf: TunnelConf) {
|
override suspend fun startTunnel(tunnelConf: TunnelConf) {
|
||||||
@@ -138,24 +101,35 @@ constructor(
|
|||||||
return tunnelProviderFlow.value.runningTunnelNames()
|
return tunnelProviderFlow.value.runningTunnelNames()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics? {
|
override suspend fun updateTunnelStatus(
|
||||||
return tunnelProviderFlow.value.getStatistics(tunnelConf)
|
tunnelConf: TunnelConf,
|
||||||
|
status: TunnelStatus?,
|
||||||
|
stats: TunnelStatistics?,
|
||||||
|
pingStates: Map<Key, PingState>?,
|
||||||
|
handshakeSuccessLogs: Boolean?,
|
||||||
|
) {
|
||||||
|
tunnelProviderFlow.value.updateTunnelStatus(
|
||||||
|
tunnelConf,
|
||||||
|
status,
|
||||||
|
stats,
|
||||||
|
pingStates,
|
||||||
|
handshakeSuccessLogs,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun restorePreviousState(): Job =
|
suspend fun restorePreviousState() {
|
||||||
applicationScope.launch(ioDispatcher) {
|
val settings = appDataRepository.settings.get()
|
||||||
val settings = appDataRepository.settings.get()
|
if (settings.isRestoreOnBootEnabled) {
|
||||||
if (settings.isRestoreOnBootEnabled) {
|
val previouslyActiveTuns = appDataRepository.tunnels.getActive()
|
||||||
val previouslyActiveTuns = appDataRepository.tunnels.getActive()
|
val tunsToStart =
|
||||||
val tunsToStart =
|
previouslyActiveTuns.filterNot { tun ->
|
||||||
previouslyActiveTuns.filterNot { tun ->
|
activeTunnels.value.any { tun.id == it.key.id }
|
||||||
activeTunnels.value.any { tun.id == it.key.id }
|
|
||||||
}
|
|
||||||
if (settings.isKernelEnabled) {
|
|
||||||
return@launch tunsToStart.forEach { startTunnel(it) }
|
|
||||||
} else {
|
|
||||||
tunsToStart.firstOrNull()?.let { startTunnel(it) }
|
|
||||||
}
|
}
|
||||||
|
if (settings.isKernelEnabled) {
|
||||||
|
return tunsToStart.forEach { startTunnel(it) }
|
||||||
|
} else {
|
||||||
|
tunsToStart.firstOrNull()?.let { startTunnel(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,266 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||||
|
|
||||||
|
import com.zaneschepke.logcatter.LogReader
|
||||||
|
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.state.FailureReason
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.state.PingState
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.toMillis
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils
|
||||||
|
import dagger.hilt.android.scopes.ServiceScoped
|
||||||
|
import io.ktor.util.collections.*
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
import org.amnezia.awg.crypto.Key
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
@ServiceScoped
|
||||||
|
class TunnelMonitor
|
||||||
|
@Inject
|
||||||
|
constructor(
|
||||||
|
private val appDataRepository: AppDataRepository,
|
||||||
|
private val tunnelManager: TunnelManager,
|
||||||
|
private val networkMonitor: NetworkMonitor,
|
||||||
|
private val networkUtils: NetworkUtils,
|
||||||
|
private val logReader: LogReader,
|
||||||
|
) {
|
||||||
|
|
||||||
|
@OptIn(FlowPreview::class)
|
||||||
|
suspend fun startMonitoring(tunnelConf: TunnelConf, withLogs: Boolean): Job = coroutineScope {
|
||||||
|
launch {
|
||||||
|
launch { startTunnelConfChangesJob(tunnelConf) }
|
||||||
|
launch { startPingMonitor(tunnelConf) }
|
||||||
|
launch { startWgStatsPoll(tunnelConf) }
|
||||||
|
if (withLogs) launch { startLogsMonitor(tunnelConf) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun startTunnelConfChangesJob(tunnelConf: TunnelConf) {
|
||||||
|
appDataRepository.tunnels.flow
|
||||||
|
.map { storedTunnels -> storedTunnels.firstOrNull { it.id == tunnelConf.id } }
|
||||||
|
.filterNotNull()
|
||||||
|
.distinctUntilChanged { old, new -> old == new }
|
||||||
|
.collect { storedTunnel ->
|
||||||
|
if (tunnelConf != storedTunnel) {
|
||||||
|
Timber.d("Config changed for ${storedTunnel.tunName}, bouncing")
|
||||||
|
withContext(NonCancellable) {
|
||||||
|
tunnelManager.bounceTunnel(
|
||||||
|
storedTunnel,
|
||||||
|
TunnelStatus.StopReason.ConfigChanged,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun startLogsMonitor(tunnelConf: TunnelConf) {
|
||||||
|
logReader.liveLogs.collect { log ->
|
||||||
|
val healthLogs =
|
||||||
|
when {
|
||||||
|
log.message.contains(HANDSHAKE_RESPONSE_TEXT, true) ||
|
||||||
|
log.message.contains(KEEPALIVE_RESPONSE_TEXT, true) -> true
|
||||||
|
log.message.contains(HANDSHAKE_INIT_FAILED_TEXT, true) ||
|
||||||
|
log.message.contains(HANDSHAKE_NOT_COMPLETED_TEXT) ||
|
||||||
|
log.message.contains(DATA_PACKET_FAILED_TEXT) -> false
|
||||||
|
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
healthLogs?.let { healthy ->
|
||||||
|
tunnelManager.updateTunnelStatus(tunnelConf, null, null, null, healthy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun startPingMonitor(tunnelConf: TunnelConf) = coroutineScope {
|
||||||
|
val pingStatsFlow = MutableStateFlow<Map<Key, PingState>>(emptyMap())
|
||||||
|
|
||||||
|
val tunStateFlow =
|
||||||
|
tunnelManager.activeTunnels.mapNotNull { it.getValueById(tunnelConf.id) }.stateIn(this)
|
||||||
|
|
||||||
|
val connectivityStateFlow = networkMonitor.connectivityStateFlow.stateIn(this)
|
||||||
|
|
||||||
|
val isNetworkConnected = connectivityStateFlow.map { it.hasConnectivity() }.stateIn(this)
|
||||||
|
|
||||||
|
data class NetworkChangeKey(
|
||||||
|
val ethernetConnected: Boolean,
|
||||||
|
val wifiConnected: Boolean,
|
||||||
|
val cellularConnected: Boolean,
|
||||||
|
val wifiSsid: String?,
|
||||||
|
)
|
||||||
|
|
||||||
|
connectivityStateFlow
|
||||||
|
.map {
|
||||||
|
NetworkChangeKey(
|
||||||
|
ethernetConnected = it.ethernetConnected,
|
||||||
|
wifiConnected = it.wifiState.connected,
|
||||||
|
cellularConnected = it.cellularConnected,
|
||||||
|
wifiSsid = if (it.wifiState.connected) it.wifiState.ssid else null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.stateIn(this)
|
||||||
|
|
||||||
|
appDataRepository.settings.flow
|
||||||
|
.distinctUntilChanged { old, new ->
|
||||||
|
old.isPingEnabled == new.isPingEnabled &&
|
||||||
|
old.tunnelPingIntervalSeconds == new.tunnelPingIntervalSeconds &&
|
||||||
|
old.tunnelPingAttempts == new.tunnelPingAttempts &&
|
||||||
|
old.tunnelPingTimeoutSeconds == new.tunnelPingTimeoutSeconds
|
||||||
|
}
|
||||||
|
.collectLatest { settings ->
|
||||||
|
if (!settings.isPingEnabled) return@collectLatest
|
||||||
|
|
||||||
|
Timber.d("Starting pinger for ${tunnelConf.tunName} with settings")
|
||||||
|
|
||||||
|
val config = tunnelConf.toAmConfig()
|
||||||
|
|
||||||
|
val pingablePeers = config.peers.filter { it.allowedIps.isNotEmpty() }
|
||||||
|
if (pingablePeers.isEmpty()) return@collectLatest
|
||||||
|
|
||||||
|
suspend fun performPing() {
|
||||||
|
val updates = ConcurrentMap<Key, PingState>()
|
||||||
|
|
||||||
|
pingablePeers.forEach { peer ->
|
||||||
|
val previousState = pingStatsFlow.value[peer.publicKey] ?: PingState()
|
||||||
|
|
||||||
|
val allowedIpStr = peer.allowedIps.firstOrNull()?.toString()
|
||||||
|
if (allowedIpStr == null) {
|
||||||
|
updates[peer.publicKey] =
|
||||||
|
previousState.copy(
|
||||||
|
isReachable = false,
|
||||||
|
failureReason = FailureReason.NoResolvedEndpoint,
|
||||||
|
lastPingAttemptMillis = System.currentTimeMillis(),
|
||||||
|
)
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
|
||||||
|
val host =
|
||||||
|
{
|
||||||
|
val parts = allowedIpStr.split("/")
|
||||||
|
val internalIp = if (parts.size == 2) parts[0] else allowedIpStr
|
||||||
|
|
||||||
|
val prefix =
|
||||||
|
if (parts.size == 2) parts[1].toIntOrNull() ?: 32 else 32
|
||||||
|
if (prefix <= 1) {
|
||||||
|
tunnelConf.pingTarget ?: CLOUDFLARE_IPV4_IP
|
||||||
|
} else {
|
||||||
|
internalIp.removeSurrounding("[", "]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.invoke()
|
||||||
|
|
||||||
|
val attemptTime = System.currentTimeMillis()
|
||||||
|
runCatching {
|
||||||
|
val pingStats =
|
||||||
|
settings.tunnelPingTimeoutSeconds?.let {
|
||||||
|
networkUtils.pingWithStats(
|
||||||
|
host,
|
||||||
|
settings.tunnelPingAttempts,
|
||||||
|
it.toMillis(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
?: networkUtils.pingWithStats(
|
||||||
|
host,
|
||||||
|
settings.tunnelPingAttempts,
|
||||||
|
)
|
||||||
|
|
||||||
|
updates[peer.publicKey] =
|
||||||
|
previousState.copy(
|
||||||
|
transmitted = pingStats.transmitted,
|
||||||
|
received = pingStats.received,
|
||||||
|
packetLoss = pingStats.packetLoss,
|
||||||
|
rttMin = pingStats.rttMin,
|
||||||
|
rttMax = pingStats.rttMax,
|
||||||
|
rttAvg = pingStats.rttAvg,
|
||||||
|
rttStddev = pingStats.rttStddev,
|
||||||
|
isReachable = pingStats.isReachable,
|
||||||
|
failureReason =
|
||||||
|
if (pingStats.isReachable) null
|
||||||
|
else FailureReason.PingFailed,
|
||||||
|
lastSuccessfulPingMillis =
|
||||||
|
pingStats.lastSuccessfulPingMillis
|
||||||
|
?: previousState.lastSuccessfulPingMillis,
|
||||||
|
pingTarget = host,
|
||||||
|
lastPingAttemptMillis = attemptTime,
|
||||||
|
)
|
||||||
|
Timber.d(
|
||||||
|
"Ping completed for peer ${peer.publicKey.toBase64().substring(0, 5)}.. to host $host with stats: $pingStats"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.onFailure {
|
||||||
|
Timber.e(
|
||||||
|
it,
|
||||||
|
"Ping failed for peer ${peer.publicKey} in ${tunnelConf.tunName} to host $host",
|
||||||
|
)
|
||||||
|
updates[peer.publicKey] =
|
||||||
|
previousState.copy(
|
||||||
|
isReachable = false,
|
||||||
|
failureReason = FailureReason.PingFailed,
|
||||||
|
pingTarget = host,
|
||||||
|
lastPingAttemptMillis = attemptTime,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updates.isNotEmpty()) {
|
||||||
|
pingStatsFlow.update { updates }
|
||||||
|
tunnelManager.updateTunnelStatus(tunnelConf, null, null, updates)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the tunnel to be fully active
|
||||||
|
tunStateFlow.filter { state -> state.status == TunnelStatus.Up }.first()
|
||||||
|
|
||||||
|
// small delay to make sure tunnel is fully up before we actively monitor
|
||||||
|
delay(3_000L)
|
||||||
|
|
||||||
|
while (isActive) {
|
||||||
|
if (isNetworkConnected.value) {
|
||||||
|
performPing()
|
||||||
|
} else {
|
||||||
|
pingStatsFlow.update { current ->
|
||||||
|
current.mapValues { entry ->
|
||||||
|
entry.value.copy(
|
||||||
|
isReachable = false,
|
||||||
|
failureReason = FailureReason.NoConnectivity,
|
||||||
|
lastPingAttemptMillis = System.currentTimeMillis(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tunnelManager.updateTunnelStatus(
|
||||||
|
tunnelConf,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
pingStatsFlow.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
delay(settings.tunnelPingIntervalSeconds.toMillis())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun startWgStatsPoll(tunnelConf: TunnelConf) = coroutineScope {
|
||||||
|
while (isActive) {
|
||||||
|
val stats = tunnelManager.getStatistics(tunnelConf)
|
||||||
|
tunnelManager.updateTunnelStatus(tunnelConf, null, stats, null)
|
||||||
|
delay(STATS_DELAY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val CLOUDFLARE_IPV6_IP = "2606:4700:4700::1111"
|
||||||
|
const val CLOUDFLARE_IPV4_IP = "1.1.1.1"
|
||||||
|
|
||||||
|
const val STATS_DELAY = 1_000L
|
||||||
|
|
||||||
|
const val KEEPALIVE_RESPONSE_TEXT = "Receiving keepalive packet"
|
||||||
|
const val HANDSHAKE_RESPONSE_TEXT = "Received handshake response"
|
||||||
|
const val HANDSHAKE_INIT_FAILED_TEXT = "Failed to send handshake initiation: write udp"
|
||||||
|
const val DATA_PACKET_FAILED_TEXT = "Failed to send data packets"
|
||||||
|
const val HANDSHAKE_NOT_COMPLETED_TEXT =
|
||||||
|
"Handshake did not complete after 5 seconds, retrying"
|
||||||
|
}
|
||||||
|
}
|
||||||
+17
-6
@@ -1,14 +1,17 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendError
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.events.BackendError
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.state.PingState
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import org.amnezia.awg.crypto.Key
|
||||||
|
|
||||||
interface TunnelProvider {
|
interface TunnelProvider {
|
||||||
/** Starts the specified tunnel configuration. */
|
/** Starts the specified tunnel configuration. */
|
||||||
@@ -23,19 +26,19 @@ interface TunnelProvider {
|
|||||||
*/
|
*/
|
||||||
suspend fun stopTunnel(
|
suspend fun stopTunnel(
|
||||||
tunnelConf: TunnelConf? = null,
|
tunnelConf: TunnelConf? = null,
|
||||||
reason: TunnelStatus.StopReason = TunnelStatus.StopReason.USER,
|
reason: TunnelStatus.StopReason = TunnelStatus.StopReason.User,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bounces (stops and restarts) the specified tunnel.
|
* Bounces (stops and restarts) the specified tunnel.
|
||||||
*
|
*
|
||||||
* @param tunnelConf The tunnel to bounce.
|
* @param tunnelConf The tunnel to bounce.
|
||||||
* @param reason The reason for bouncing, defaults to USER for manual actions. Callers should
|
* @param reason The reason for bouncing, defaults to User for manual actions. Callers should
|
||||||
* override with specific reasons (e.g., PING, CONFIG_CHANGED) when applicable.
|
* override with specific reasons (e.g., Ping, ConfigChanged) when applicable.
|
||||||
*/
|
*/
|
||||||
suspend fun bounceTunnel(
|
suspend fun bounceTunnel(
|
||||||
tunnelConf: TunnelConf,
|
tunnelConf: TunnelConf,
|
||||||
reason: TunnelStatus.StopReason = TunnelStatus.StopReason.USER,
|
reason: TunnelStatus.StopReason = TunnelStatus.StopReason.User,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun setBackendState(backendState: BackendState, allowedIps: Collection<String>)
|
fun setBackendState(backendState: BackendState, allowedIps: Collection<String>)
|
||||||
@@ -50,9 +53,17 @@ interface TunnelProvider {
|
|||||||
|
|
||||||
val errorEvents: SharedFlow<Pair<TunnelConf, BackendError>>
|
val errorEvents: SharedFlow<Pair<TunnelConf, BackendError>>
|
||||||
|
|
||||||
|
val messageEvents: SharedFlow<Pair<TunnelConf, BackendMessage>>
|
||||||
|
|
||||||
val bouncingTunnelIds: ConcurrentHashMap<Int, TunnelStatus.StopReason>
|
val bouncingTunnelIds: ConcurrentHashMap<Int, TunnelStatus.StopReason>
|
||||||
|
|
||||||
fun hasVpnPermission(): Boolean
|
fun hasVpnPermission(): Boolean
|
||||||
|
|
||||||
suspend fun updateTunnelStatistics(tunnel: TunnelConf)
|
suspend fun updateTunnelStatus(
|
||||||
|
tunnelConf: TunnelConf,
|
||||||
|
status: TunnelStatus? = null,
|
||||||
|
stats: TunnelStatistics? = null,
|
||||||
|
pingStates: Map<Key, PingState>? = null,
|
||||||
|
handshakeSuccessLogs: Boolean? = null,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-2
@@ -1,9 +1,9 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.events.BackendError
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.AmneziaStatistics
|
import com.zaneschepke.wireguardautotunnel.domain.state.AmneziaStatistics
|
||||||
@@ -23,7 +23,7 @@ import timber.log.Timber
|
|||||||
class UserspaceTunnel
|
class UserspaceTunnel
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
@ApplicationScope private val applicationScope: CoroutineScope,
|
applicationScope: CoroutineScope,
|
||||||
val serviceManager: ServiceManager,
|
val serviceManager: ServiceManager,
|
||||||
val appDataRepository: AppDataRepository,
|
val appDataRepository: AppDataRepository,
|
||||||
private val backend: Backend,
|
private val backend: Backend,
|
||||||
@@ -40,6 +40,9 @@ constructor(
|
|||||||
} catch (e: BackendException) {
|
} catch (e: BackendException) {
|
||||||
Timber.e(e, "Failed to start up backend for tunnel ${tunnel.name}")
|
Timber.e(e, "Failed to start up backend for tunnel ${tunnel.name}")
|
||||||
throw e.toBackendError()
|
throw e.toBackendError()
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
Timber.e(e, "Failed to start up backend for tunnel ${tunnel.name}")
|
||||||
|
throw BackendError.Config
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,7 @@ package com.zaneschepke.wireguardautotunnel.core.worker
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.hilt.work.HiltWorker
|
import androidx.hilt.work.HiltWorker
|
||||||
import androidx.work.CoroutineWorker
|
import androidx.work.*
|
||||||
import androidx.work.ExistingPeriodicWorkPolicy
|
|
||||||
import androidx.work.PeriodicWorkRequestBuilder
|
|
||||||
import androidx.work.WorkManager
|
|
||||||
import androidx.work.WorkerParameters
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.data
|
package com.zaneschepke.wireguardautotunnel.data
|
||||||
|
|
||||||
import androidx.room.AutoMigration
|
import androidx.room.*
|
||||||
import androidx.room.Database
|
|
||||||
import androidx.room.DeleteColumn
|
|
||||||
import androidx.room.RoomDatabase
|
|
||||||
import androidx.room.TypeConverters
|
|
||||||
import androidx.room.migration.AutoMigrationSpec
|
import androidx.room.migration.AutoMigrationSpec
|
||||||
import com.zaneschepke.wireguardautotunnel.data.dao.SettingsDao
|
import com.zaneschepke.wireguardautotunnel.data.dao.SettingsDao
|
||||||
import com.zaneschepke.wireguardautotunnel.data.dao.TunnelConfigDao
|
import com.zaneschepke.wireguardautotunnel.data.dao.TunnelConfigDao
|
||||||
@@ -13,7 +9,7 @@ import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
|
|||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
entities = [Settings::class, TunnelConfig::class],
|
entities = [Settings::class, TunnelConfig::class],
|
||||||
version = 18,
|
version = 19,
|
||||||
autoMigrations =
|
autoMigrations =
|
||||||
[
|
[
|
||||||
AutoMigration(from = 1, to = 2),
|
AutoMigration(from = 1, to = 2),
|
||||||
@@ -33,6 +29,7 @@ import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
|
|||||||
AutoMigration(from = 15, to = 16),
|
AutoMigration(from = 15, to = 16),
|
||||||
AutoMigration(from = 16, to = 17, spec = WifiDetectionMigration::class),
|
AutoMigration(from = 16, to = 17, spec = WifiDetectionMigration::class),
|
||||||
AutoMigration(from = 17, to = 18),
|
AutoMigration(from = 17, to = 18),
|
||||||
|
AutoMigration(from = 18, to = 19, spec = PingMigration::class),
|
||||||
],
|
],
|
||||||
exportSchema = true,
|
exportSchema = true,
|
||||||
)
|
)
|
||||||
@@ -52,3 +49,22 @@ class RemoveTunnelPauseMigration : AutoMigrationSpec
|
|||||||
|
|
||||||
@DeleteColumn(tableName = "Settings", columnName = "is_wifi_by_shell_enabled")
|
@DeleteColumn(tableName = "Settings", columnName = "is_wifi_by_shell_enabled")
|
||||||
class WifiDetectionMigration : AutoMigrationSpec
|
class WifiDetectionMigration : AutoMigrationSpec
|
||||||
|
|
||||||
|
@DeleteColumn.Entries(
|
||||||
|
DeleteColumn(tableName = "TunnelConfig", columnName = "ping_interval"),
|
||||||
|
DeleteColumn(tableName = "TunnelConfig", columnName = "ping_cooldown"),
|
||||||
|
DeleteColumn(tableName = "Settings", columnName = "split_tunnel_apps"),
|
||||||
|
)
|
||||||
|
@RenameColumn.Entries(
|
||||||
|
RenameColumn(
|
||||||
|
tableName = "TunnelConfig",
|
||||||
|
fromColumnName = "is_ping_enabled",
|
||||||
|
toColumnName = "restart_on_ping_failure",
|
||||||
|
),
|
||||||
|
RenameColumn(
|
||||||
|
tableName = "TunnelConfig",
|
||||||
|
fromColumnName = "ping_ip",
|
||||||
|
toColumnName = "ping_target",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
class PingMigration : AutoMigrationSpec
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ class DataStoreManager(
|
|||||||
val theme = stringPreferencesKey("THEME")
|
val theme = stringPreferencesKey("THEME")
|
||||||
val isRemoteControlEnabled = booleanPreferencesKey("IS_REMOTE_CONTROL_ENABLED")
|
val isRemoteControlEnabled = booleanPreferencesKey("IS_REMOTE_CONTROL_ENABLED")
|
||||||
val remoteKey = stringPreferencesKey("REMOTE_KEY")
|
val remoteKey = stringPreferencesKey("REMOTE_KEY")
|
||||||
|
val showDetailedPingStats = booleanPreferencesKey("SHOW_DETAILED_PING_STATS")
|
||||||
}
|
}
|
||||||
|
|
||||||
// preferences
|
// preferences
|
||||||
|
|||||||
@@ -6,19 +6,19 @@ import kotlinx.serialization.json.Json
|
|||||||
|
|
||||||
class DatabaseConverters {
|
class DatabaseConverters {
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun listToString(value: MutableList<String>): String {
|
fun listToString(value: List<String>): String {
|
||||||
return Json.encodeToString(value)
|
return Json.encodeToString(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun stringToList(value: String): MutableList<String> {
|
fun stringToList(value: String): List<String> {
|
||||||
if (value.isBlank() || value.isEmpty()) return mutableListOf()
|
if (value.isBlank() || value.isEmpty()) return mutableListOf()
|
||||||
return try {
|
return try {
|
||||||
Json.decodeFromString<MutableList<String>>(value)
|
Json.decodeFromString<List<String>>(value)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
val list = value.split(",").toMutableList()
|
val list = value.split(",").toMutableList()
|
||||||
val json = listToString(list)
|
val json = listToString(list)
|
||||||
Json.decodeFromString<MutableList<String>>(json)
|
Json.decodeFromString<List<String>>(json)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.data.dao
|
package com.zaneschepke.wireguardautotunnel.data.dao
|
||||||
|
|
||||||
import androidx.room.Dao
|
import androidx.room.*
|
||||||
import androidx.room.Delete
|
|
||||||
import androidx.room.Insert
|
|
||||||
import androidx.room.OnConflictStrategy
|
|
||||||
import androidx.room.Query
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.entity.Settings
|
import com.zaneschepke.wireguardautotunnel.data.entity.Settings
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.data.dao
|
package com.zaneschepke.wireguardautotunnel.data.dao
|
||||||
|
|
||||||
import androidx.room.Dao
|
import androidx.room.*
|
||||||
import androidx.room.Delete
|
|
||||||
import androidx.room.Insert
|
|
||||||
import androidx.room.OnConflictStrategy
|
|
||||||
import androidx.room.Query
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
|
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ data class GeneralState(
|
|||||||
val expandedTunnelIds: List<Int> = emptyList(),
|
val expandedTunnelIds: List<Int> = emptyList(),
|
||||||
val isLocalLogsEnabled: Boolean = IS_LOGS_ENABLED_DEFAULT,
|
val isLocalLogsEnabled: Boolean = IS_LOGS_ENABLED_DEFAULT,
|
||||||
val isRemoteControlEnabled: Boolean = IS_REMOTE_CONTROL_ENABLED,
|
val isRemoteControlEnabled: Boolean = IS_REMOTE_CONTROL_ENABLED,
|
||||||
|
val showDetailedPingStats: Boolean = SHOW_DETAILED_PING_STATS_DEFAULT,
|
||||||
val remoteKey: String? = null,
|
val remoteKey: String? = null,
|
||||||
val locale: String? = null,
|
val locale: String? = null,
|
||||||
val theme: Theme = Theme.AUTOMATIC,
|
val theme: Theme = Theme.AUTOMATIC,
|
||||||
@@ -20,5 +21,6 @@ data class GeneralState(
|
|||||||
const val PIN_LOCK_ENABLED_DEFAULT = false
|
const val PIN_LOCK_ENABLED_DEFAULT = false
|
||||||
const val IS_LOGS_ENABLED_DEFAULT = false
|
const val IS_LOGS_ENABLED_DEFAULT = false
|
||||||
const val IS_REMOTE_CONTROL_ENABLED = false
|
const val IS_REMOTE_CONTROL_ENABLED = false
|
||||||
|
const val SHOW_DETAILED_PING_STATS_DEFAULT = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ data class Settings(
|
|||||||
@ColumnInfo(name = "is_tunnel_enabled") val isAutoTunnelEnabled: Boolean = false,
|
@ColumnInfo(name = "is_tunnel_enabled") val isAutoTunnelEnabled: Boolean = false,
|
||||||
@ColumnInfo(name = "is_tunnel_on_mobile_data_enabled")
|
@ColumnInfo(name = "is_tunnel_on_mobile_data_enabled")
|
||||||
val isTunnelOnMobileDataEnabled: Boolean = false,
|
val isTunnelOnMobileDataEnabled: Boolean = false,
|
||||||
@ColumnInfo(name = "trusted_network_ssids")
|
@ColumnInfo(name = "trusted_network_ssids") val trustedNetworkSSIDs: List<String> = emptyList(),
|
||||||
val trustedNetworkSSIDs: MutableList<String> = mutableListOf(),
|
|
||||||
@ColumnInfo(name = "is_always_on_vpn_enabled") val isAlwaysOnVpnEnabled: Boolean = false,
|
@ColumnInfo(name = "is_always_on_vpn_enabled") val isAlwaysOnVpnEnabled: Boolean = false,
|
||||||
@ColumnInfo(name = "is_tunnel_on_ethernet_enabled")
|
@ColumnInfo(name = "is_tunnel_on_ethernet_enabled")
|
||||||
val isTunnelOnEthernetEnabled: Boolean = false,
|
val isTunnelOnEthernetEnabled: Boolean = false,
|
||||||
@@ -45,12 +44,15 @@ data class Settings(
|
|||||||
val isDisableKillSwitchOnTrustedEnabled: Boolean = false,
|
val isDisableKillSwitchOnTrustedEnabled: Boolean = false,
|
||||||
@ColumnInfo(name = "is_tunnel_on_unsecure_enabled", defaultValue = "false")
|
@ColumnInfo(name = "is_tunnel_on_unsecure_enabled", defaultValue = "false")
|
||||||
val isTunnelOnUnsecureEnabled: Boolean = false,
|
val isTunnelOnUnsecureEnabled: Boolean = false,
|
||||||
@ColumnInfo(name = "split_tunnel_apps", defaultValue = "")
|
|
||||||
val splitTunnelApps: MutableList<String> = mutableListOf(),
|
|
||||||
@ColumnInfo(name = "wifi_detection_method", defaultValue = "0")
|
@ColumnInfo(name = "wifi_detection_method", defaultValue = "0")
|
||||||
val wifiDetectionMethod: WifiDetectionMethod = WifiDetectionMethod.fromValue(0),
|
val wifiDetectionMethod: WifiDetectionMethod = WifiDetectionMethod.fromValue(0),
|
||||||
|
@ColumnInfo(name = "is_ping_monitoring_enabled", defaultValue = "true")
|
||||||
|
val isPingMonitoringEnabled: Boolean = true,
|
||||||
|
@ColumnInfo(name = "tunnel_ping_interval_sec", defaultValue = "30")
|
||||||
|
val tunnelPingIntervalSeconds: Int = 30,
|
||||||
|
@ColumnInfo(name = "tunnel_ping_attempts", defaultValue = "3") val tunnelPingAttempts: Int = 3,
|
||||||
|
@ColumnInfo(name = "tunnel_ping_timeout_sec") val tunnelPingTimeoutSeconds: Int? = null,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
enum class WifiDetectionMethod(val value: Int) {
|
enum class WifiDetectionMethod(val value: Int) {
|
||||||
DEFAULT(0),
|
DEFAULT(0),
|
||||||
LEGACY(1),
|
LEGACY(1),
|
||||||
|
|||||||
@@ -11,23 +11,23 @@ data class TunnelConfig(
|
|||||||
@ColumnInfo(name = "name") val name: String,
|
@ColumnInfo(name = "name") val name: String,
|
||||||
@ColumnInfo(name = "wg_quick") val wgQuick: String,
|
@ColumnInfo(name = "wg_quick") val wgQuick: String,
|
||||||
@ColumnInfo(name = "tunnel_networks", defaultValue = "")
|
@ColumnInfo(name = "tunnel_networks", defaultValue = "")
|
||||||
val tunnelNetworks: MutableList<String> = mutableListOf(),
|
val tunnelNetworks: List<String> = listOf(),
|
||||||
@ColumnInfo(name = "is_mobile_data_tunnel", defaultValue = "false")
|
@ColumnInfo(name = "is_mobile_data_tunnel", defaultValue = "false")
|
||||||
val isMobileDataTunnel: Boolean = false,
|
val isMobileDataTunnel: Boolean = false,
|
||||||
@ColumnInfo(name = "is_primary_tunnel", defaultValue = "false")
|
@ColumnInfo(name = "is_primary_tunnel", defaultValue = "false")
|
||||||
val isPrimaryTunnel: Boolean = false,
|
val isPrimaryTunnel: Boolean = false,
|
||||||
@ColumnInfo(name = "am_quick", defaultValue = "") val amQuick: String = AM_QUICK_DEFAULT,
|
@ColumnInfo(name = "am_quick", defaultValue = "") val amQuick: String = AM_QUICK_DEFAULT,
|
||||||
@ColumnInfo(name = "is_Active", defaultValue = "false") val isActive: Boolean = false,
|
@ColumnInfo(name = "is_Active", defaultValue = "false") val isActive: Boolean = false,
|
||||||
@ColumnInfo(name = "is_ping_enabled", defaultValue = "false")
|
@ColumnInfo(name = "restart_on_ping_failure", defaultValue = "false")
|
||||||
val isPingEnabled: Boolean = false,
|
val restartOnPingFailure: Boolean = false,
|
||||||
@ColumnInfo(name = "ping_interval", defaultValue = "null") val pingInterval: Long? = null,
|
@ColumnInfo(name = "ping_target", defaultValue = "null") var pingTarget: String? = null,
|
||||||
@ColumnInfo(name = "ping_cooldown", defaultValue = "null") val pingCooldown: Long? = null,
|
|
||||||
@ColumnInfo(name = "ping_ip", defaultValue = "null") var pingIp: String? = null,
|
|
||||||
@ColumnInfo(name = "is_ethernet_tunnel", defaultValue = "false")
|
@ColumnInfo(name = "is_ethernet_tunnel", defaultValue = "false")
|
||||||
val isEthernetTunnel: Boolean = false,
|
val isEthernetTunnel: Boolean = false,
|
||||||
@ColumnInfo(name = "is_ipv4_preferred", defaultValue = "true")
|
@ColumnInfo(name = "is_ipv4_preferred", defaultValue = "true")
|
||||||
val isIpv4Preferred: Boolean = true,
|
val isIpv4Preferred: Boolean = true,
|
||||||
@ColumnInfo(name = "position", defaultValue = "0") val position: Int = 0,
|
@ColumnInfo(name = "position", defaultValue = "0") val position: Int = 0,
|
||||||
|
@ColumnInfo(name = "auto_tunnel_apps", defaultValue = "[]")
|
||||||
|
val autoTunnelApps: List<String> = listOf(),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
+2
@@ -13,6 +13,7 @@ object GeneralStateMapper {
|
|||||||
expandedTunnelIds,
|
expandedTunnelIds,
|
||||||
isLocalLogsEnabled,
|
isLocalLogsEnabled,
|
||||||
isRemoteControlEnabled,
|
isRemoteControlEnabled,
|
||||||
|
showDetailedPingStats,
|
||||||
remoteKey,
|
remoteKey,
|
||||||
locale,
|
locale,
|
||||||
theme,
|
theme,
|
||||||
@@ -28,6 +29,7 @@ object GeneralStateMapper {
|
|||||||
expandedTunnelIds,
|
expandedTunnelIds,
|
||||||
isLocalLogsEnabled,
|
isLocalLogsEnabled,
|
||||||
isRemoteControlEnabled,
|
isRemoteControlEnabled,
|
||||||
|
showDetailedPingStats,
|
||||||
remoteKey,
|
remoteKey,
|
||||||
locale,
|
locale,
|
||||||
theme,
|
theme,
|
||||||
|
|||||||
-1
@@ -2,7 +2,6 @@ package com.zaneschepke.wireguardautotunnel.data.mapper
|
|||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.entity.GitHubRelease
|
import com.zaneschepke.wireguardautotunnel.data.entity.GitHubRelease
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.AppUpdate
|
import com.zaneschepke.wireguardautotunnel.domain.model.AppUpdate
|
||||||
import kotlin.collections.firstOrNull
|
|
||||||
|
|
||||||
object GitHubReleaseMapper {
|
object GitHubReleaseMapper {
|
||||||
fun toAppUpdate(gitHubRelease: GitHubRelease, newVersion: String): AppUpdate {
|
fun toAppUpdate(gitHubRelease: GitHubRelease, newVersion: String): AppUpdate {
|
||||||
|
|||||||
@@ -28,11 +28,13 @@ object SettingsMapper {
|
|||||||
debounceDelaySeconds = settings.debounceDelaySeconds,
|
debounceDelaySeconds = settings.debounceDelaySeconds,
|
||||||
isDisableKillSwitchOnTrustedEnabled = settings.isDisableKillSwitchOnTrustedEnabled,
|
isDisableKillSwitchOnTrustedEnabled = settings.isDisableKillSwitchOnTrustedEnabled,
|
||||||
isTunnelOnUnsecureEnabled = settings.isTunnelOnUnsecureEnabled,
|
isTunnelOnUnsecureEnabled = settings.isTunnelOnUnsecureEnabled,
|
||||||
splitTunnelApps = settings.splitTunnelApps,
|
|
||||||
wifiDetectionMethod =
|
wifiDetectionMethod =
|
||||||
AndroidNetworkMonitor.WifiDetectionMethod.fromValue(
|
AndroidNetworkMonitor.WifiDetectionMethod.fromValue(
|
||||||
settings.wifiDetectionMethod.value
|
settings.wifiDetectionMethod.value
|
||||||
),
|
),
|
||||||
|
tunnelPingIntervalSeconds = settings.tunnelPingIntervalSeconds,
|
||||||
|
tunnelPingAttempts = settings.tunnelPingAttempts,
|
||||||
|
tunnelPingTimeoutSeconds = settings.tunnelPingTimeoutSeconds,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +43,7 @@ object SettingsMapper {
|
|||||||
id = appSettings.id,
|
id = appSettings.id,
|
||||||
isAutoTunnelEnabled = appSettings.isAutoTunnelEnabled,
|
isAutoTunnelEnabled = appSettings.isAutoTunnelEnabled,
|
||||||
isTunnelOnMobileDataEnabled = appSettings.isTunnelOnMobileDataEnabled,
|
isTunnelOnMobileDataEnabled = appSettings.isTunnelOnMobileDataEnabled,
|
||||||
trustedNetworkSSIDs = appSettings.trustedNetworkSSIDs.toMutableList(),
|
trustedNetworkSSIDs = appSettings.trustedNetworkSSIDs,
|
||||||
isAlwaysOnVpnEnabled = appSettings.isAlwaysOnVpnEnabled,
|
isAlwaysOnVpnEnabled = appSettings.isAlwaysOnVpnEnabled,
|
||||||
isTunnelOnEthernetEnabled = appSettings.isTunnelOnEthernetEnabled,
|
isTunnelOnEthernetEnabled = appSettings.isTunnelOnEthernetEnabled,
|
||||||
isShortcutsEnabled = appSettings.isShortcutsEnabled,
|
isShortcutsEnabled = appSettings.isShortcutsEnabled,
|
||||||
@@ -59,9 +61,11 @@ object SettingsMapper {
|
|||||||
debounceDelaySeconds = appSettings.debounceDelaySeconds,
|
debounceDelaySeconds = appSettings.debounceDelaySeconds,
|
||||||
isDisableKillSwitchOnTrustedEnabled = appSettings.isDisableKillSwitchOnTrustedEnabled,
|
isDisableKillSwitchOnTrustedEnabled = appSettings.isDisableKillSwitchOnTrustedEnabled,
|
||||||
isTunnelOnUnsecureEnabled = appSettings.isTunnelOnUnsecureEnabled,
|
isTunnelOnUnsecureEnabled = appSettings.isTunnelOnUnsecureEnabled,
|
||||||
splitTunnelApps = appSettings.splitTunnelApps.toMutableList(),
|
|
||||||
wifiDetectionMethod =
|
wifiDetectionMethod =
|
||||||
Settings.WifiDetectionMethod.fromValue(appSettings.wifiDetectionMethod.value),
|
Settings.WifiDetectionMethod.fromValue(appSettings.wifiDetectionMethod.value),
|
||||||
|
tunnelPingIntervalSeconds = appSettings.tunnelPingIntervalSeconds,
|
||||||
|
tunnelPingAttempts = appSettings.tunnelPingAttempts,
|
||||||
|
tunnelPingTimeoutSeconds = appSettings.tunnelPingTimeoutSeconds,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-9
@@ -15,10 +15,8 @@ object TunnelConfigMapper {
|
|||||||
isPrimaryTunnel,
|
isPrimaryTunnel,
|
||||||
amQuick,
|
amQuick,
|
||||||
isActive,
|
isActive,
|
||||||
isPingEnabled,
|
pingTarget,
|
||||||
pingInterval,
|
restartOnPingFailure,
|
||||||
pingCooldown,
|
|
||||||
pingIp,
|
|
||||||
isEthernetTunnel,
|
isEthernetTunnel,
|
||||||
isIpv4Preferred,
|
isIpv4Preferred,
|
||||||
position,
|
position,
|
||||||
@@ -32,15 +30,13 @@ object TunnelConfigMapper {
|
|||||||
id,
|
id,
|
||||||
tunName,
|
tunName,
|
||||||
wgQuick,
|
wgQuick,
|
||||||
tunnelNetworks.toMutableList(),
|
tunnelNetworks,
|
||||||
isMobileDataTunnel,
|
isMobileDataTunnel,
|
||||||
isPrimaryTunnel,
|
isPrimaryTunnel,
|
||||||
amQuick,
|
amQuick,
|
||||||
isActive,
|
isActive,
|
||||||
isPingEnabled,
|
restartOnPingFailure,
|
||||||
pingInterval,
|
pingTarget,
|
||||||
pingCooldown,
|
|
||||||
pingIp,
|
|
||||||
isEthernetTunnel,
|
isEthernetTunnel,
|
||||||
isIpv4Preferred,
|
isIpv4Preferred,
|
||||||
position,
|
position,
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.data.network
|
package com.zaneschepke.wireguardautotunnel.data.network
|
||||||
|
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.*
|
||||||
import io.ktor.client.engine.okhttp.OkHttp
|
import io.ktor.client.engine.okhttp.*
|
||||||
import io.ktor.client.plugins.HttpTimeout
|
import io.ktor.client.plugins.*
|
||||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
import io.ktor.client.plugins.contentnegotiation.*
|
||||||
import io.ktor.serialization.kotlinx.json.json
|
import io.ktor.serialization.kotlinx.json.*
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
object KtorClient {
|
object KtorClient {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.data.network
|
package com.zaneschepke.wireguardautotunnel.data.network
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.entity.GitHubRelease
|
import com.zaneschepke.wireguardautotunnel.data.entity.GitHubRelease
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.*
|
||||||
import io.ktor.client.call.body
|
import io.ktor.client.call.*
|
||||||
import io.ktor.client.plugins.ClientRequestException
|
import io.ktor.client.plugins.*
|
||||||
import io.ktor.client.request.get
|
import io.ktor.client.request.*
|
||||||
import io.ktor.http.HttpStatusCode
|
import io.ktor.http.*
|
||||||
|
|
||||||
class KtorGitHubApi(private val client: HttpClient) : GitHubApi {
|
class KtorGitHubApi(private val client: HttpClient) : GitHubApi {
|
||||||
override suspend fun getLatestRelease(owner: String, repo: String): Result<GitHubRelease> {
|
override suspend fun getLatestRelease(owner: String, repo: String): Result<GitHubRelease> {
|
||||||
|
|||||||
+12
@@ -119,6 +119,15 @@ class DataStoreAppStateRepository(private val dataStoreManager: DataStoreManager
|
|||||||
return dataStoreManager.getFromStore(DataStoreManager.remoteKey)
|
return dataStoreManager.getFromStore(DataStoreManager.remoteKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun setShowDetailedPingStats(showDetailedPing: Boolean) {
|
||||||
|
dataStoreManager.saveToDataStore(DataStoreManager.showDetailedPingStats, showDetailedPing)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getShowDetailedPing(): Boolean {
|
||||||
|
return dataStoreManager.getFromStore(DataStoreManager.showDetailedPingStats)
|
||||||
|
?: GeneralState.SHOW_DETAILED_PING_STATS_DEFAULT
|
||||||
|
}
|
||||||
|
|
||||||
override val flow: Flow<AppState> =
|
override val flow: Flow<AppState> =
|
||||||
dataStoreManager.preferencesFlow
|
dataStoreManager.preferencesFlow
|
||||||
.map { prefs ->
|
.map { prefs ->
|
||||||
@@ -144,6 +153,9 @@ class DataStoreAppStateRepository(private val dataStoreManager: DataStoreManager
|
|||||||
isRemoteControlEnabled =
|
isRemoteControlEnabled =
|
||||||
pref[DataStoreManager.isRemoteControlEnabled]
|
pref[DataStoreManager.isRemoteControlEnabled]
|
||||||
?: GeneralState.IS_REMOTE_CONTROL_ENABLED,
|
?: GeneralState.IS_REMOTE_CONTROL_ENABLED,
|
||||||
|
showDetailedPingStats =
|
||||||
|
pref[DataStoreManager.showDetailedPingStats]
|
||||||
|
?: GeneralState.SHOW_DETAILED_PING_STATS_DEFAULT,
|
||||||
remoteKey = pref[DataStoreManager.remoteKey],
|
remoteKey = pref[DataStoreManager.remoteKey],
|
||||||
locale = pref[DataStoreManager.locale],
|
locale = pref[DataStoreManager.locale],
|
||||||
theme = getTheme(),
|
theme = getTheme(),
|
||||||
|
|||||||
+5
-7
@@ -9,13 +9,11 @@ import com.zaneschepke.wireguardautotunnel.domain.model.AppUpdate
|
|||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.UpdateRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.UpdateRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.*
|
||||||
import io.ktor.client.request.get
|
import io.ktor.client.request.*
|
||||||
import io.ktor.client.statement.HttpResponse
|
import io.ktor.client.statement.*
|
||||||
import io.ktor.client.statement.bodyAsChannel
|
import io.ktor.http.*
|
||||||
import io.ktor.http.contentLength
|
import io.ktor.utils.io.*
|
||||||
import io.ktor.utils.io.ByteReadChannel
|
|
||||||
import io.ktor.utils.io.readAvailable
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|||||||
@@ -4,9 +4,12 @@ import android.content.Context
|
|||||||
import com.zaneschepke.logcatter.LogReader
|
import com.zaneschepke.logcatter.LogReader
|
||||||
import com.zaneschepke.logcatter.LogcatReader
|
import com.zaneschepke.logcatter.LogcatReader
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationMonitor
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
|
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
|
||||||
import com.zaneschepke.wireguardautotunnel.core.shortcut.DynamicShortcutManager
|
import com.zaneschepke.wireguardautotunnel.core.shortcut.DynamicShortcutManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager
|
import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
@@ -47,4 +50,19 @@ class AppModule {
|
|||||||
): ShortcutManager {
|
): ShortcutManager {
|
||||||
return DynamicShortcutManager(context, ioDispatcher)
|
return DynamicShortcutManager(context, ioDispatcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
fun provideNetworkUtils(@IoDispatcher ioDispatcher: CoroutineDispatcher): NetworkUtils {
|
||||||
|
return NetworkUtils(ioDispatcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
fun provideNotificationMonitor(
|
||||||
|
tunnelManager: TunnelManager,
|
||||||
|
notificationManager: NotificationManager,
|
||||||
|
): NotificationMonitor {
|
||||||
|
return NotificationMonitor(tunnelManager, notificationManager)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,22 +11,14 @@ import com.zaneschepke.wireguardautotunnel.data.dao.TunnelConfigDao
|
|||||||
import com.zaneschepke.wireguardautotunnel.data.network.GitHubApi
|
import com.zaneschepke.wireguardautotunnel.data.network.GitHubApi
|
||||||
import com.zaneschepke.wireguardautotunnel.data.network.KtorClient
|
import com.zaneschepke.wireguardautotunnel.data.network.KtorClient
|
||||||
import com.zaneschepke.wireguardautotunnel.data.network.KtorGitHubApi
|
import com.zaneschepke.wireguardautotunnel.data.network.KtorGitHubApi
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRoomRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.*
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.DataStoreAppStateRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.*
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.GitHubUpdateRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.RoomSettingsRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.RoomTunnelRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppSettingRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.UpdateRepository
|
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.*
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
|
||||||
|
|||||||
@@ -4,16 +4,15 @@ import android.content.Context
|
|||||||
import com.wireguard.android.backend.WgQuickBackend
|
import com.wireguard.android.backend.WgQuickBackend
|
||||||
import com.wireguard.android.util.RootShell
|
import com.wireguard.android.util.RootShell
|
||||||
import com.wireguard.android.util.ToolsInstaller
|
import com.wireguard.android.util.ToolsInstaller
|
||||||
|
import com.zaneschepke.logcatter.LogReader
|
||||||
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
|
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
|
||||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.KernelTunnel
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.*
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.UserspaceTunnel
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppSettingRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppSettingRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
@@ -108,7 +107,6 @@ class TunnelModule {
|
|||||||
appDataRepository,
|
appDataRepository,
|
||||||
applicationScope,
|
applicationScope,
|
||||||
ioDispatcher,
|
ioDispatcher,
|
||||||
notificationManager,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,4 +151,23 @@ class TunnelModule {
|
|||||||
appDataRepository,
|
appDataRepository,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
fun provideTunnelMonitor(
|
||||||
|
@ApplicationContext context: Context,
|
||||||
|
tunnelManager: TunnelManager,
|
||||||
|
networkMonitor: NetworkMonitor,
|
||||||
|
networkUtils: NetworkUtils,
|
||||||
|
logReader: LogReader,
|
||||||
|
appDataRepository: AppDataRepository,
|
||||||
|
): TunnelMonitor {
|
||||||
|
return TunnelMonitor(
|
||||||
|
appDataRepository,
|
||||||
|
tunnelManager,
|
||||||
|
networkMonitor,
|
||||||
|
networkUtils,
|
||||||
|
logReader,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,10 +10,12 @@ sealed class TunnelStatus {
|
|||||||
|
|
||||||
data object Starting : TunnelStatus()
|
data object Starting : TunnelStatus()
|
||||||
|
|
||||||
enum class StopReason {
|
sealed class StopReason {
|
||||||
USER,
|
data object User : StopReason()
|
||||||
PING,
|
|
||||||
CONFIG_CHANGED,
|
data class Ping(val previouslyResolvedEndpoints: Map<String, String?>) : StopReason()
|
||||||
|
|
||||||
|
data object ConfigChanged : StopReason()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isDown(): Boolean {
|
fun isDown(): Boolean {
|
||||||
|
|||||||
@@ -5,7 +5,14 @@ import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
|||||||
sealed class AutoTunnelEvent {
|
sealed class AutoTunnelEvent {
|
||||||
data class Start(val tunnelConf: TunnelConf? = null) : AutoTunnelEvent()
|
data class Start(val tunnelConf: TunnelConf? = null) : AutoTunnelEvent()
|
||||||
|
|
||||||
|
data class Bounce(val configsPeerKeyResolvedMap: List<Pair<TunnelConf, Map<String, String?>>>) :
|
||||||
|
AutoTunnelEvent()
|
||||||
|
|
||||||
data object Stop : AutoTunnelEvent()
|
data object Stop : AutoTunnelEvent()
|
||||||
|
|
||||||
data object DoNothing : AutoTunnelEvent()
|
data object DoNothing : AutoTunnelEvent()
|
||||||
|
|
||||||
|
data class StartKillSwitch(val allowedIps: List<String>) : AutoTunnelEvent()
|
||||||
|
|
||||||
|
data object StopKillSwitch : AutoTunnelEvent()
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-1
@@ -1,6 +1,7 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.domain.enums
|
package com.zaneschepke.wireguardautotunnel.domain.events
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||||
|
|
||||||
sealed class BackendError : Exception() {
|
sealed class BackendError : Exception() {
|
||||||
data object DNS : BackendError()
|
data object DNS : BackendError()
|
||||||
@@ -19,6 +20,8 @@ sealed class BackendError : Exception() {
|
|||||||
|
|
||||||
data object TunnelNameTooLong : BackendError()
|
data object TunnelNameTooLong : BackendError()
|
||||||
|
|
||||||
|
data class BounceFailed(val error: BackendError) : BackendError()
|
||||||
|
|
||||||
fun toStringRes() =
|
fun toStringRes() =
|
||||||
when (this) {
|
when (this) {
|
||||||
Config -> R.string.config_error
|
Config -> R.string.config_error
|
||||||
@@ -29,5 +32,17 @@ sealed class BackendError : Exception() {
|
|||||||
ServiceNotRunning -> R.string.service_running_error
|
ServiceNotRunning -> R.string.service_running_error
|
||||||
Unknown -> R.string.unknown_error
|
Unknown -> R.string.unknown_error
|
||||||
TunnelNameTooLong -> R.string.error_tunnel_name
|
TunnelNameTooLong -> R.string.error_tunnel_name
|
||||||
|
is BounceFailed -> R.string.bounce_failed_template
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun toStringValue(): StringValue {
|
||||||
|
return when (val backendError = this) {
|
||||||
|
is BounceFailed ->
|
||||||
|
StringValue.StringResource(
|
||||||
|
backendError.toStringRes(),
|
||||||
|
backendError.error.toStringRes(),
|
||||||
|
)
|
||||||
|
else -> StringValue.StringResource(backendError.toStringRes())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.domain.events
|
||||||
|
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||||
|
|
||||||
|
sealed class BackendMessage {
|
||||||
|
|
||||||
|
data object BounceSuccess : BackendMessage()
|
||||||
|
|
||||||
|
data object BounceRecovery : BackendMessage()
|
||||||
|
|
||||||
|
fun toStringRes() =
|
||||||
|
when (this) {
|
||||||
|
BounceRecovery -> R.string.pinger_bounce_recovery
|
||||||
|
BounceSuccess -> R.string.pinger_bounce_successful
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toStringValue() = StringValue.StringResource(this.toStringRes())
|
||||||
|
}
|
||||||
@@ -24,14 +24,12 @@ data class AppSettings(
|
|||||||
val debounceDelaySeconds: Int = 3,
|
val debounceDelaySeconds: Int = 3,
|
||||||
val isDisableKillSwitchOnTrustedEnabled: Boolean = false,
|
val isDisableKillSwitchOnTrustedEnabled: Boolean = false,
|
||||||
val isTunnelOnUnsecureEnabled: Boolean = false,
|
val isTunnelOnUnsecureEnabled: Boolean = false,
|
||||||
val splitTunnelApps: List<String> = emptyList(),
|
|
||||||
val wifiDetectionMethod: AndroidNetworkMonitor.WifiDetectionMethod =
|
val wifiDetectionMethod: AndroidNetworkMonitor.WifiDetectionMethod =
|
||||||
AndroidNetworkMonitor.WifiDetectionMethod.DEFAULT,
|
AndroidNetworkMonitor.WifiDetectionMethod.DEFAULT,
|
||||||
|
val tunnelPingIntervalSeconds: Int = 30,
|
||||||
|
val tunnelPingAttempts: Int = 3,
|
||||||
|
val tunnelPingTimeoutSeconds: Int? = null,
|
||||||
) {
|
) {
|
||||||
fun debounceDelayMillis(): Long {
|
|
||||||
return debounceDelaySeconds * 1000L
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toAutoTunnelStateString(): String {
|
fun toAutoTunnelStateString(): String {
|
||||||
return """
|
return """
|
||||||
TunnelOnWifi: $isTunnelOnWifiEnabled
|
TunnelOnWifi: $isTunnelOnWifiEnabled
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ data class AppState(
|
|||||||
val expandedTunnelIds: List<Int>,
|
val expandedTunnelIds: List<Int>,
|
||||||
val isLocalLogsEnabled: Boolean,
|
val isLocalLogsEnabled: Boolean,
|
||||||
val isRemoteControlEnabled: Boolean,
|
val isRemoteControlEnabled: Boolean,
|
||||||
|
val showDetailedPingStats: Boolean,
|
||||||
val remoteKey: String?,
|
val remoteKey: String?,
|
||||||
val locale: String?,
|
val locale: String?,
|
||||||
val theme: Theme,
|
val theme: Theme,
|
||||||
|
|||||||
@@ -2,14 +2,9 @@ package com.zaneschepke.wireguardautotunnel.domain.model
|
|||||||
|
|
||||||
import com.wireguard.android.backend.Tunnel
|
import com.wireguard.android.backend.Tunnel
|
||||||
import com.wireguard.config.Config
|
import com.wireguard.config.Config
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.*
|
import com.zaneschepke.wireguardautotunnel.util.extensions.*
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.net.InetAddress
|
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
import kotlin.coroutines.CoroutineContext
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
data class TunnelConf(
|
data class TunnelConf(
|
||||||
val id: Int = 0,
|
val id: Int = 0,
|
||||||
@@ -20,10 +15,8 @@ data class TunnelConf(
|
|||||||
val isPrimaryTunnel: Boolean = false,
|
val isPrimaryTunnel: Boolean = false,
|
||||||
val amQuick: String,
|
val amQuick: String,
|
||||||
val isActive: Boolean = false,
|
val isActive: Boolean = false,
|
||||||
val isPingEnabled: Boolean = false,
|
val pingTarget: String? = null,
|
||||||
val pingInterval: Long? = null,
|
val restartOnPingFailure: Boolean = false,
|
||||||
val pingCooldown: Long? = null,
|
|
||||||
val pingIp: String? = null,
|
|
||||||
val isEthernetTunnel: Boolean = false,
|
val isEthernetTunnel: Boolean = false,
|
||||||
val isIpv4Preferred: Boolean = true,
|
val isIpv4Preferred: Boolean = true,
|
||||||
val position: Int = 0,
|
val position: Int = 0,
|
||||||
@@ -46,10 +39,8 @@ data class TunnelConf(
|
|||||||
isPrimaryTunnel == other.isPrimaryTunnel &&
|
isPrimaryTunnel == other.isPrimaryTunnel &&
|
||||||
isMobileDataTunnel == other.isMobileDataTunnel &&
|
isMobileDataTunnel == other.isMobileDataTunnel &&
|
||||||
isEthernetTunnel == other.isEthernetTunnel &&
|
isEthernetTunnel == other.isEthernetTunnel &&
|
||||||
isPingEnabled == other.isPingEnabled &&
|
pingTarget == other.pingTarget &&
|
||||||
pingIp == other.pingIp &&
|
restartOnPingFailure == other.restartOnPingFailure &&
|
||||||
pingCooldown == other.pingCooldown &&
|
|
||||||
pingInterval == other.pingInterval &&
|
|
||||||
tunnelNetworks == other.tunnelNetworks &&
|
tunnelNetworks == other.tunnelNetworks &&
|
||||||
isIpv4Preferred == other.isIpv4Preferred
|
isIpv4Preferred == other.isIpv4Preferred
|
||||||
}
|
}
|
||||||
@@ -75,10 +66,8 @@ data class TunnelConf(
|
|||||||
isPrimaryTunnel: Boolean = this.isPrimaryTunnel,
|
isPrimaryTunnel: Boolean = this.isPrimaryTunnel,
|
||||||
amQuick: String = this.amQuick,
|
amQuick: String = this.amQuick,
|
||||||
isActive: Boolean = this.isActive,
|
isActive: Boolean = this.isActive,
|
||||||
isPingEnabled: Boolean = this.isPingEnabled,
|
restartOnPingFailure: Boolean = this.restartOnPingFailure,
|
||||||
pingInterval: Long? = this.pingInterval,
|
pingIp: String? = this.pingTarget,
|
||||||
pingCooldown: Long? = this.pingCooldown,
|
|
||||||
pingIp: String? = this.pingIp,
|
|
||||||
isEthernetTunnel: Boolean = this.isEthernetTunnel,
|
isEthernetTunnel: Boolean = this.isEthernetTunnel,
|
||||||
isIpv4Preferred: Boolean = this.isIpv4Preferred,
|
isIpv4Preferred: Boolean = this.isIpv4Preferred,
|
||||||
): TunnelConf {
|
): TunnelConf {
|
||||||
@@ -91,10 +80,8 @@ data class TunnelConf(
|
|||||||
isPrimaryTunnel,
|
isPrimaryTunnel,
|
||||||
amQuick,
|
amQuick,
|
||||||
isActive,
|
isActive,
|
||||||
isPingEnabled,
|
|
||||||
pingInterval,
|
|
||||||
pingCooldown,
|
|
||||||
pingIp,
|
pingIp,
|
||||||
|
restartOnPingFailure,
|
||||||
isEthernetTunnel,
|
isEthernetTunnel,
|
||||||
isIpv4Preferred,
|
isIpv4Preferred,
|
||||||
position,
|
position,
|
||||||
@@ -138,21 +125,6 @@ data class TunnelConf(
|
|||||||
return tunnelName
|
return tunnelName
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun isTunnelPingable(context: CoroutineContext): Boolean {
|
|
||||||
return withContext(context) {
|
|
||||||
val config = toWgConfig()
|
|
||||||
if (pingIp != null) {
|
|
||||||
return@withContext InetAddress.getByName(pingIp)
|
|
||||||
.isReachable(Constants.PING_TIMEOUT.toInt())
|
|
||||||
.also { Timber.i("Ping reachable $pingIp: $it") }
|
|
||||||
}
|
|
||||||
config.peers
|
|
||||||
.map { peer -> peer.isReachable() }
|
|
||||||
.all { true }
|
|
||||||
.also { Timber.i("Ping of all peers reachable: $it") }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun configFromWgQuick(wgQuick: String): Config {
|
fun configFromWgQuick(wgQuick: String): Config {
|
||||||
val inputStream: InputStream = wgQuick.byteInputStream()
|
val inputStream: InputStream = wgQuick.byteInputStream()
|
||||||
|
|||||||
+4
@@ -41,5 +41,9 @@ interface AppStateRepository {
|
|||||||
|
|
||||||
suspend fun getRemoteKey(): String?
|
suspend fun getRemoteKey(): String?
|
||||||
|
|
||||||
|
suspend fun setShowDetailedPingStats(showDetailedPing: Boolean)
|
||||||
|
|
||||||
|
suspend fun getShowDetailedPing(): Boolean
|
||||||
|
|
||||||
val flow: Flow<AppState>
|
val flow: Flow<AppState>
|
||||||
}
|
}
|
||||||
|
|||||||
+83
-119
@@ -1,10 +1,8 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.domain.state
|
package com.zaneschepke.wireguardautotunnel.domain.state
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.allDown
|
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.StateChange
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.hasActive
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.isUp
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
|
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.events.KillSwitchEvent
|
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent.*
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.AppSettings
|
import com.zaneschepke.wireguardautotunnel.domain.model.AppSettings
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isMatchingToWildcardList
|
import com.zaneschepke.wireguardautotunnel.util.extensions.isMatchingToWildcardList
|
||||||
@@ -16,6 +14,68 @@ data class AutoTunnelState(
|
|||||||
val tunnels: List<TunnelConf> = emptyList(),
|
val tunnels: List<TunnelConf> = emptyList(),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
fun determineAutoTunnelEvent(stateChange: StateChange): AutoTunnelEvent {
|
||||||
|
when (val change = stateChange) {
|
||||||
|
is StateChange.NetworkChange,
|
||||||
|
is StateChange.SettingsChange -> {
|
||||||
|
// Compute desired tunnel based on network conditions
|
||||||
|
var desiredTunnel: TunnelConf? = null
|
||||||
|
if (networkState.isEthernetConnected && settings.isTunnelOnEthernetEnabled) {
|
||||||
|
desiredTunnel = preferredEthernetTunnel()
|
||||||
|
} else if (isMobileDataActive() && settings.isTunnelOnMobileDataEnabled) {
|
||||||
|
desiredTunnel = preferredMobileDataTunnel()
|
||||||
|
} else if (
|
||||||
|
isWifiActive() && settings.isTunnelOnWifiEnabled && !isCurrentSSIDTrusted()
|
||||||
|
) {
|
||||||
|
desiredTunnel = preferredWifiTunnel()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override for no connectivity if enabled
|
||||||
|
if (isNoConnectivity() && settings.isStopOnNoInternetEnabled) {
|
||||||
|
desiredTunnel = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine current active tunnel (assuming only one can be active)
|
||||||
|
val currentTunnel = activeTunnels.entries.firstOrNull()?.key
|
||||||
|
|
||||||
|
// Handle tunnel start/stop/change
|
||||||
|
if (desiredTunnel != null) {
|
||||||
|
if (currentTunnel != desiredTunnel) {
|
||||||
|
// Start or switch to the desired tunnel (overrides any kill switch)
|
||||||
|
return Start(desiredTunnel)
|
||||||
|
}
|
||||||
|
// If already active and matching, fall through to kill switch check (though
|
||||||
|
// unlikely needed)
|
||||||
|
} else {
|
||||||
|
if (currentTunnel != null) {
|
||||||
|
// Stop the active tunnel (then next emission can handle kill switch if
|
||||||
|
// needed)
|
||||||
|
return AutoTunnelEvent.Stop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Handle kill switch only if no user tunnel is or will be active
|
||||||
|
if (stopKillSwitchOnTrusted()) {
|
||||||
|
return AutoTunnelEvent.StopKillSwitch
|
||||||
|
}
|
||||||
|
if (startKillSwitch()) {
|
||||||
|
val allowedIps =
|
||||||
|
if (settings.isLanOnKillSwitchEnabled) TunnelConf.LAN_BYPASS_ALLOWED_IPS
|
||||||
|
else emptyList()
|
||||||
|
return StartKillSwitch(allowedIps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is StateChange.MonitoringChange -> {
|
||||||
|
val bounceTunnels = bounceOnPingFailed()
|
||||||
|
if (bounceTunnels.isNotEmpty()) {
|
||||||
|
return Bounce(bounceTunnels)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is StateChange.ActiveTunnelsChange -> Unit
|
||||||
|
}
|
||||||
|
return DoNothing
|
||||||
|
}
|
||||||
|
|
||||||
// also need to check for Wi-Fi state as there is some overlap when they are both connected
|
// also need to check for Wi-Fi state as there is some overlap when they are both connected
|
||||||
private fun isMobileDataActive(): Boolean {
|
private fun isMobileDataActive(): Boolean {
|
||||||
return !networkState.isEthernetConnected &&
|
return !networkState.isEthernetConnected &&
|
||||||
@@ -23,32 +83,22 @@ data class AutoTunnelState(
|
|||||||
networkState.isMobileDataConnected
|
networkState.isMobileDataConnected
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isMobileTunnelDataChangeNeeded(): Boolean {
|
|
||||||
val preferredTunnel = preferredMobileDataTunnel()
|
|
||||||
return preferredTunnel != null &&
|
|
||||||
activeTunnels.isNotEmpty() &&
|
|
||||||
!activeTunnels.isUp(preferredTunnel)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isEthernetTunnelChangeNeeded(): Boolean {
|
|
||||||
val preferredTunnel = preferredEthernetTunnel()
|
|
||||||
return preferredTunnel != null &&
|
|
||||||
activeTunnels.isNotEmpty() &&
|
|
||||||
!activeTunnels.isUp(preferredTunnel)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun preferredMobileDataTunnel(): TunnelConf? {
|
private fun preferredMobileDataTunnel(): TunnelConf? {
|
||||||
return tunnels.firstOrNull { it.isMobileDataTunnel }
|
return tunnels.firstOrNull { it.isMobileDataTunnel }
|
||||||
?: tunnels.firstOrNull { it.isPrimaryTunnel }
|
?: tunnels.firstOrNull { it.isPrimaryTunnel }
|
||||||
|
?: tunnels.firstOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun preferredEthernetTunnel(): TunnelConf? {
|
private fun preferredEthernetTunnel(): TunnelConf? {
|
||||||
return tunnels.firstOrNull { it.isEthernetTunnel }
|
return tunnels.firstOrNull { it.isEthernetTunnel }
|
||||||
?: tunnels.firstOrNull { it.isPrimaryTunnel }
|
?: tunnels.firstOrNull { it.isPrimaryTunnel }
|
||||||
|
?: tunnels.firstOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun preferredWifiTunnel(): TunnelConf? {
|
private fun preferredWifiTunnel(): TunnelConf? {
|
||||||
return getTunnelWithMatchingTunnelNetwork() ?: tunnels.firstOrNull { it.isPrimaryTunnel }
|
return getTunnelWithMatchingTunnelNetwork()
|
||||||
|
?: tunnels.firstOrNull { it.isPrimaryTunnel }
|
||||||
|
?: tunnels.firstOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ignore cellular state as there is overlap where it may still be active, but not prioritized
|
// ignore cellular state as there is overlap where it may still be active, but not prioritized
|
||||||
@@ -56,19 +106,6 @@ data class AutoTunnelState(
|
|||||||
return !networkState.isEthernetConnected && networkState.isWifiConnected
|
return !networkState.isEthernetConnected && networkState.isWifiConnected
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startOnEthernet(): Boolean {
|
|
||||||
return networkState.isEthernetConnected &&
|
|
||||||
settings.isTunnelOnEthernetEnabled &&
|
|
||||||
activeTunnels.allDown()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun stopOnEthernet(): Boolean {
|
|
||||||
return networkState.isEthernetConnected &&
|
|
||||||
!settings.isTunnelOnEthernetEnabled &&
|
|
||||||
activeTunnels.hasActive()
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO test removed kill switch state check
|
|
||||||
private fun stopKillSwitchOnTrusted(): Boolean {
|
private fun stopKillSwitchOnTrusted(): Boolean {
|
||||||
return networkState.isWifiConnected &&
|
return networkState.isWifiConnected &&
|
||||||
settings.isVpnKillSwitchEnabled &&
|
settings.isVpnKillSwitchEnabled &&
|
||||||
@@ -76,7 +113,6 @@ data class AutoTunnelState(
|
|||||||
isCurrentSSIDTrusted()
|
isCurrentSSIDTrusted()
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO test, removed kill switch state check
|
|
||||||
private fun startKillSwitch(): Boolean {
|
private fun startKillSwitch(): Boolean {
|
||||||
return settings.isVpnKillSwitchEnabled &&
|
return settings.isVpnKillSwitchEnabled &&
|
||||||
(!settings.isDisableKillSwitchOnTrustedEnabled || !isCurrentSSIDTrusted())
|
(!settings.isDisableKillSwitchOnTrustedEnabled || !isCurrentSSIDTrusted())
|
||||||
@@ -88,93 +124,21 @@ data class AutoTunnelState(
|
|||||||
!networkState.isMobileDataConnected
|
!networkState.isMobileDataConnected
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun stopOnMobileData(): Boolean {
|
private fun bounceOnPingFailed(): List<Pair<TunnelConf, Map<String, String?>>> {
|
||||||
return isMobileDataActive() &&
|
return activeTunnels.entries
|
||||||
!settings.isTunnelOnMobileDataEnabled &&
|
.filter { (tunnel, state) ->
|
||||||
activeTunnels.hasActive()
|
tunnel.restartOnPingFailure &&
|
||||||
}
|
(state.pingStates?.any { (key, pingState) ->
|
||||||
|
pingState.failureReason == FailureReason.PingFailed
|
||||||
private fun startOnMobileData(): Boolean {
|
} ?: false)
|
||||||
return isMobileDataActive() &&
|
}
|
||||||
settings.isTunnelOnMobileDataEnabled &&
|
.map { (tunnel, state) ->
|
||||||
activeTunnels.allDown()
|
val peerMap =
|
||||||
}
|
(state.statistics?.getPeers()?.associate { peerKey ->
|
||||||
|
peerKey.toBase64() to state.statistics.peerStats(peerKey)?.resolvedEndpoint
|
||||||
private fun changeOnMobileData(): Boolean {
|
} ?: emptyMap())
|
||||||
return isMobileDataActive() &&
|
Pair(tunnel, peerMap)
|
||||||
settings.isTunnelOnMobileDataEnabled &&
|
|
||||||
isMobileTunnelDataChangeNeeded()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun changeOnEthernet(): Boolean {
|
|
||||||
return networkState.isEthernetConnected &&
|
|
||||||
settings.isTunnelOnEthernetEnabled &&
|
|
||||||
isEthernetTunnelChangeNeeded()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun stopOnWifi(): Boolean {
|
|
||||||
return isWifiActive() && !settings.isTunnelOnWifiEnabled && activeTunnels.hasActive()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun stopOnTrustedWifi(): Boolean {
|
|
||||||
return isWifiActive() &&
|
|
||||||
settings.isTunnelOnWifiEnabled &&
|
|
||||||
activeTunnels.hasActive() &&
|
|
||||||
isCurrentSSIDTrusted()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startOnUntrustedWifi(): Boolean {
|
|
||||||
return isWifiActive() &&
|
|
||||||
settings.isTunnelOnWifiEnabled &&
|
|
||||||
activeTunnels.allDown() &&
|
|
||||||
!isCurrentSSIDTrusted()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun changeOnUntrustedWifi(): Boolean {
|
|
||||||
return isWifiActive() &&
|
|
||||||
settings.isTunnelOnWifiEnabled &&
|
|
||||||
activeTunnels.hasActive() &&
|
|
||||||
!isCurrentSSIDTrusted() &&
|
|
||||||
!isWifiTunnelPreferred()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isWifiTunnelPreferred(): Boolean {
|
|
||||||
val preferred = preferredWifiTunnel()
|
|
||||||
return preferred?.let { activeTunnels.isUp(it) } ?: true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun asAutoTunnelEvent(): AutoTunnelEvent {
|
|
||||||
return when {
|
|
||||||
// ethernet scenarios
|
|
||||||
stopOnEthernet() -> AutoTunnelEvent.Stop
|
|
||||||
startOnEthernet() || changeOnEthernet() ->
|
|
||||||
AutoTunnelEvent.Start(preferredEthernetTunnel())
|
|
||||||
// mobile data scenarios
|
|
||||||
stopOnMobileData() -> AutoTunnelEvent.Stop
|
|
||||||
startOnMobileData() || changeOnMobileData() ->
|
|
||||||
AutoTunnelEvent.Start(preferredMobileDataTunnel())
|
|
||||||
// wifi scenarios
|
|
||||||
stopOnWifi() -> AutoTunnelEvent.Stop
|
|
||||||
stopOnTrustedWifi() -> AutoTunnelEvent.Stop
|
|
||||||
startOnUntrustedWifi() || changeOnUntrustedWifi() ->
|
|
||||||
AutoTunnelEvent.Start(preferredWifiTunnel())
|
|
||||||
// no connectivity
|
|
||||||
isNoConnectivity() && settings.isStopOnNoInternetEnabled -> AutoTunnelEvent.Stop
|
|
||||||
else -> AutoTunnelEvent.DoNothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun asKillSwitchEvent(): KillSwitchEvent {
|
|
||||||
return when {
|
|
||||||
stopKillSwitchOnTrusted() -> KillSwitchEvent.Stop
|
|
||||||
startKillSwitch() -> {
|
|
||||||
val allowedIps =
|
|
||||||
if (settings.isLanOnKillSwitchEnabled) TunnelConf.LAN_BYPASS_ALLOWED_IPS
|
|
||||||
else emptyList()
|
|
||||||
KillSwitchEvent.Start(allowedIps)
|
|
||||||
}
|
}
|
||||||
else -> KillSwitchEvent.DoNothing
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isCurrentSSIDTrusted(): Boolean {
|
private fun isCurrentSSIDTrusted(): Boolean {
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.domain.state
|
||||||
|
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelMonitor.Companion.CLOUDFLARE_IPV4_IP
|
||||||
|
|
||||||
|
enum class FailureReason {
|
||||||
|
NoConnectivity,
|
||||||
|
PingFailed,
|
||||||
|
NoResolvedEndpoint,
|
||||||
|
Timeout,
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
data class PingState(
|
||||||
|
val transmitted: Int = 0,
|
||||||
|
val received: Int = 0,
|
||||||
|
val packetLoss: Double = 0.0,
|
||||||
|
val rttMin: Double = 0.0,
|
||||||
|
val rttMax: Double = 0.0,
|
||||||
|
val rttAvg: Double = 0.0,
|
||||||
|
val rttStddev: Double = 0.0,
|
||||||
|
val isReachable: Boolean = false,
|
||||||
|
val lastSuccessfulPingMillis: Long? = null,
|
||||||
|
val lastPingAttemptMillis: Long? = null,
|
||||||
|
val failureReason: FailureReason? = null,
|
||||||
|
val pingTarget: String = CLOUDFLARE_IPV4_IP,
|
||||||
|
)
|
||||||
@@ -2,9 +2,12 @@ package com.zaneschepke.wireguardautotunnel.domain.state
|
|||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
||||||
|
import org.amnezia.awg.crypto.Key
|
||||||
|
|
||||||
data class TunnelState(
|
data class TunnelState(
|
||||||
val status: TunnelStatus = TunnelStatus.Down,
|
val status: TunnelStatus = TunnelStatus.Down,
|
||||||
val backendState: BackendState = BackendState.INACTIVE,
|
val backendState: BackendState = BackendState.INACTIVE,
|
||||||
val statistics: TunnelStatistics? = null,
|
val statistics: TunnelStatistics? = null,
|
||||||
|
val pingStates: Map<Key, PingState>? = null,
|
||||||
|
val handshakeSuccessLogs: Boolean? = null,
|
||||||
)
|
)
|
||||||
|
|||||||
+14
-3
@@ -3,13 +3,24 @@ package com.zaneschepke.wireguardautotunnel.domain.state
|
|||||||
import org.amnezia.awg.crypto.Key
|
import org.amnezia.awg.crypto.Key
|
||||||
|
|
||||||
abstract class TunnelStatistics {
|
abstract class TunnelStatistics {
|
||||||
@JvmRecord
|
open class PeerStats(
|
||||||
data class PeerStats(
|
|
||||||
val rxBytes: Long,
|
val rxBytes: Long,
|
||||||
val txBytes: Long,
|
val txBytes: Long,
|
||||||
val latestHandshakeEpochMillis: Long,
|
val latestHandshakeEpochMillis: Long,
|
||||||
val resolvedEndpoint: String,
|
val resolvedEndpoint: String,
|
||||||
)
|
) {
|
||||||
|
// mimic data class copy
|
||||||
|
open fun copy(
|
||||||
|
rxBytes: Long = this.rxBytes,
|
||||||
|
txBytes: Long = this.txBytes,
|
||||||
|
latestHandshakeEpochMillis: Long = this.latestHandshakeEpochMillis,
|
||||||
|
resolvedEndpoint: String = this.resolvedEndpoint,
|
||||||
|
): PeerStats = PeerStats(rxBytes, txBytes, latestHandshakeEpochMillis, resolvedEndpoint)
|
||||||
|
|
||||||
|
// Manual toString: Format like data class
|
||||||
|
override fun toString(): String =
|
||||||
|
"PeerStats(rxBytes=$rxBytes, txBytes=$txBytes, latestHandshakeEpochMillis=$latestHandshakeEpochMillis, resolvedEndpoint=$resolvedEndpoint)"
|
||||||
|
}
|
||||||
|
|
||||||
abstract fun peerStats(peer: Key): PeerStats?
|
abstract fun peerStats(peer: Key): PeerStats?
|
||||||
|
|
||||||
|
|||||||
@@ -47,4 +47,6 @@ sealed class Route {
|
|||||||
@Serializable data object Logs : Route()
|
@Serializable data object Logs : Route()
|
||||||
|
|
||||||
@Serializable data object Sort : Route()
|
@Serializable data object Sort : Route()
|
||||||
|
|
||||||
|
@Serializable data object TunnelMonitoring : Route()
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-5
@@ -1,10 +1,6 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.common.animation
|
package com.zaneschepke.wireguardautotunnel.ui.common.animation
|
||||||
|
|
||||||
import androidx.compose.animation.core.LinearEasing
|
import androidx.compose.animation.core.*
|
||||||
import androidx.compose.animation.core.animateFloat
|
|
||||||
import androidx.compose.animation.core.infiniteRepeatable
|
|
||||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
|
||||||
import androidx.compose.animation.core.tween
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
|||||||
+1
-5
@@ -3,11 +3,7 @@ package com.zaneschepke.wireguardautotunnel.ui.common.button
|
|||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
|||||||
+2
-11
@@ -2,18 +2,9 @@ package com.zaneschepke.wireguardautotunnel.ui.common.button
|
|||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material3.CardDefaults
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.ripple
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
|
|||||||
+3
-5
@@ -12,11 +12,7 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
@@ -32,6 +28,7 @@ fun SubmitConfigurationTextBox(
|
|||||||
hint: String,
|
hint: String,
|
||||||
isErrorValue: (value: String?) -> Boolean,
|
isErrorValue: (value: String?) -> Boolean,
|
||||||
onSubmit: (value: String) -> Unit,
|
onSubmit: (value: String) -> Unit,
|
||||||
|
supportingText: @Composable (() -> Unit)? = null,
|
||||||
keyboardOptions: KeyboardOptions =
|
keyboardOptions: KeyboardOptions =
|
||||||
KeyboardOptions(capitalization = KeyboardCapitalization.None, imeAction = ImeAction.Done),
|
KeyboardOptions(capitalization = KeyboardCapitalization.None, imeAction = ImeAction.Done),
|
||||||
) {
|
) {
|
||||||
@@ -49,6 +46,7 @@ fun SubmitConfigurationTextBox(
|
|||||||
value = stateValue,
|
value = stateValue,
|
||||||
onValueChange = { stateValue = it },
|
onValueChange = { stateValue = it },
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
|
supportingText = supportingText,
|
||||||
label = {
|
label = {
|
||||||
Text(
|
Text(
|
||||||
label,
|
label,
|
||||||
|
|||||||
+1
-5
@@ -1,10 +1,6 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.common.dialog
|
package com.zaneschepke.wireguardautotunnel.ui.common.dialog
|
||||||
|
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Surface
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|||||||
+19
-11
@@ -1,4 +1,4 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.common
|
package com.zaneschepke.wireguardautotunnel.ui.common.dropdown
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -6,11 +6,7 @@ import androidx.compose.foundation.layout.height
|
|||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.ArrowDropDown
|
import androidx.compose.material.icons.filled.ArrowDropDown
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -20,9 +16,9 @@ import com.zaneschepke.wireguardautotunnel.R
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun <T> DropdownSelector(
|
fun <T> DropdownSelector(
|
||||||
currentValue: T,
|
currentValue: T?,
|
||||||
options: List<T>,
|
options: List<T?>,
|
||||||
onValueSelected: (T) -> Unit,
|
onValueSelected: (T?) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
label: @Composable (() -> Unit)? = null,
|
label: @Composable (() -> Unit)? = null,
|
||||||
isExpanded: Boolean = false,
|
isExpanded: Boolean = false,
|
||||||
@@ -33,7 +29,10 @@ fun <T> DropdownSelector(
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
if (label != null) label()
|
if (label != null) label()
|
||||||
Text(text = currentValue.toString(), style = MaterialTheme.typography.bodyMedium)
|
Text(
|
||||||
|
text = currentValue?.toString() ?: stringResource(R.string._default),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
)
|
||||||
Icon(Icons.Default.ArrowDropDown, contentDescription = stringResource(R.string.dropdown))
|
Icon(Icons.Default.ArrowDropDown, contentDescription = stringResource(R.string.dropdown))
|
||||||
}
|
}
|
||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
@@ -44,11 +43,20 @@ fun <T> DropdownSelector(
|
|||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
) {
|
) {
|
||||||
options.forEach { option ->
|
options.forEach { option ->
|
||||||
|
if (option == null) {
|
||||||
|
return@forEach DropdownMenuItem(
|
||||||
|
text = { Text(text = stringResource(R.string._default)) },
|
||||||
|
onClick = {
|
||||||
|
onValueSelected(null)
|
||||||
|
onDismiss()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text(text = option.toString()) },
|
text = { Text(text = option.toString()) },
|
||||||
onClick = {
|
onClick = {
|
||||||
onValueSelected(option)
|
onValueSelected(option)
|
||||||
onDismiss() // Close dropdown after selection
|
onDismiss()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.common.dropdown
|
||||||
|
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LabelledNumberDropdown(
|
||||||
|
title: @Composable () -> Unit,
|
||||||
|
description: (@Composable () -> Unit)? = null,
|
||||||
|
leading: @Composable () -> Unit,
|
||||||
|
onSelected: (Int?) -> Unit,
|
||||||
|
options: List<Int?>,
|
||||||
|
currentValue: Int?,
|
||||||
|
) {
|
||||||
|
var isDropDownExpanded by remember { mutableStateOf(false) }
|
||||||
|
SurfaceSelectionGroupButton(
|
||||||
|
listOf(
|
||||||
|
SelectionItem(
|
||||||
|
leading = leading,
|
||||||
|
title = title,
|
||||||
|
description = description,
|
||||||
|
onClick = { isDropDownExpanded = true },
|
||||||
|
trailing = {
|
||||||
|
DropdownSelector(
|
||||||
|
currentValue = currentValue,
|
||||||
|
options = options,
|
||||||
|
onValueSelected = { num -> onSelected(num) },
|
||||||
|
isExpanded = isDropDownExpanded,
|
||||||
|
onDismiss = { isDropDownExpanded = false },
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
-7
@@ -1,12 +1,6 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.common.snackbar
|
package com.zaneschepke.wireguardautotunnel.ui.common.snackbar
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.layout.IntrinsicSize
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.rounded.Info
|
import androidx.compose.material.icons.rounded.Info
|
||||||
|
|||||||
+6
@@ -163,6 +163,12 @@ fun currentNavBackStackEntryAsNavBarState(
|
|||||||
route = Route.Display,
|
route = Route.Display,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
backStackEntry.isCurrentRoute(Route.TunnelMonitoring::class) ->
|
||||||
|
NavBarState(
|
||||||
|
topTitle = { Text(stringResource(R.string.tunnel_monitoring)) },
|
||||||
|
route = Route.TunnelMonitoring,
|
||||||
|
)
|
||||||
|
|
||||||
backStackEntry.isCurrentRoute(Route.WifiDetectionMethod::class) ->
|
backStackEntry.isCurrentRoute(Route.WifiDetectionMethod::class) ->
|
||||||
NavBarState(
|
NavBarState(
|
||||||
topTitle = { Text(stringResource(R.string.wifi_detection_method)) },
|
topTitle = { Text(stringResource(R.string.wifi_detection_method)) },
|
||||||
|
|||||||
+1
-5
@@ -9,11 +9,7 @@ import androidx.compose.foundation.verticalScroll
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.CheckCircle
|
import androidx.compose.material.icons.outlined.CheckCircle
|
||||||
import androidx.compose.material.icons.outlined.Info
|
import androidx.compose.material.icons.outlined.Info
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
|||||||
+25
-4
@@ -6,13 +6,21 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.PauseCircle
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.advanced.components.DebounceDelaySelector
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.dropdown.LabelledNumberDropdown
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
|
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
||||||
|
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AutoTunnelAdvancedScreen(appUiState: AppUiState, viewModel: AppViewModel) {
|
fun AutoTunnelAdvancedScreen(appUiState: AppUiState, viewModel: AppViewModel) {
|
||||||
@@ -25,9 +33,22 @@ fun AutoTunnelAdvancedScreen(appUiState: AppUiState, viewModel: AppViewModel) {
|
|||||||
.padding(vertical = 24.dp)
|
.padding(vertical = 24.dp)
|
||||||
.padding(horizontal = 12.dp),
|
.padding(horizontal = 12.dp),
|
||||||
) {
|
) {
|
||||||
DebounceDelaySelector(
|
LabelledNumberDropdown(
|
||||||
currentDelay = appUiState.appSettings.debounceDelaySeconds,
|
title = {
|
||||||
onEvent = viewModel::handleEvent,
|
Text(
|
||||||
|
stringResource(R.string.debounce_delay),
|
||||||
|
style =
|
||||||
|
MaterialTheme.typography.bodyMedium.copy(
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
leading = { Icon(Icons.Outlined.PauseCircle, null) },
|
||||||
|
onSelected = { selected ->
|
||||||
|
viewModel.handleEvent(AppEvent.SetDebounceDelay(selected!!))
|
||||||
|
},
|
||||||
|
options = (0..10).toList(),
|
||||||
|
currentValue = appUiState.appSettings.debounceDelaySeconds,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-50
@@ -1,50 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.advanced.components
|
|
||||||
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.outlined.PauseCircle
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.DropdownSelector
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
|
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun DebounceDelaySelector(currentDelay: Int, onEvent: (AppEvent) -> Unit) {
|
|
||||||
var isDropDownExpanded by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
SurfaceSelectionGroupButton(
|
|
||||||
listOf(
|
|
||||||
SelectionItem(
|
|
||||||
leading = { Icon(Icons.Outlined.PauseCircle, contentDescription = null) },
|
|
||||||
title = {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.debounce_delay),
|
|
||||||
style =
|
|
||||||
MaterialTheme.typography.bodyMedium.copy(
|
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
|
||||||
),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onClick = { isDropDownExpanded = true },
|
|
||||||
trailing = {
|
|
||||||
DropdownSelector(
|
|
||||||
currentValue = currentDelay,
|
|
||||||
options = (0..10).toList(),
|
|
||||||
onValueSelected = { num -> onEvent(AppEvent.SetDebounceDelay(num)) },
|
|
||||||
isExpanded = isDropDownExpanded,
|
|
||||||
onDismiss = { isDropDownExpanded = false },
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
+1
-6
@@ -1,12 +1,7 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components
|
package com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components
|
||||||
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
|||||||
+5
-6
@@ -6,12 +6,7 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -19,6 +14,7 @@ import com.zaneschepke.wireguardautotunnel.domain.model.AppSettings
|
|||||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
|
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.autotunnel.components.MobileDataTunnelItem
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.autotunnel.components.MobileDataTunnelItem
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.autotunnel.components.PingRestartItem
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.autotunnel.components.WifiTunnelItem
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.autotunnel.components.WifiTunnelItem
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.autotunnel.components.ethernetTunnelItem
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.autotunnel.components.ethernetTunnelItem
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
||||||
@@ -45,6 +41,9 @@ fun TunnelAutoTunnelScreen(
|
|||||||
SurfaceSelectionGroupButton(
|
SurfaceSelectionGroupButton(
|
||||||
items =
|
items =
|
||||||
buildList {
|
buildList {
|
||||||
|
if (appSettings.isPingEnabled) {
|
||||||
|
add(PingRestartItem(tunnelConf, viewModel))
|
||||||
|
}
|
||||||
add(MobileDataTunnelItem(tunnelConf, viewModel))
|
add(MobileDataTunnelItem(tunnelConf, viewModel))
|
||||||
add(ethernetTunnelItem(tunnelConf, viewModel))
|
add(ethernetTunnelItem(tunnelConf, viewModel))
|
||||||
add(
|
add(
|
||||||
|
|||||||
+4
-4
@@ -1,4 +1,4 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.main.tunneloptions.components
|
package com.zaneschepke.wireguardautotunnel.ui.screens.main.autotunnel.components
|
||||||
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.NetworkPing
|
import androidx.compose.material.icons.outlined.NetworkPing
|
||||||
@@ -27,10 +27,10 @@ fun PingRestartItem(tunnelConf: TunnelConf, viewModel: AppViewModel): SelectionI
|
|||||||
},
|
},
|
||||||
trailing = {
|
trailing = {
|
||||||
ScaledSwitch(
|
ScaledSwitch(
|
||||||
checked = tunnelConf.isPingEnabled,
|
checked = tunnelConf.restartOnPingFailure,
|
||||||
onClick = { viewModel.handleEvent(AppEvent.TogglePingTunnelEnabled(tunnelConf)) },
|
onClick = { viewModel.handleEvent(AppEvent.ToggleRestartOnPingFailure(tunnelConf)) },
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onClick = { viewModel.handleEvent(AppEvent.TogglePingTunnelEnabled(tunnelConf)) },
|
onClick = { viewModel.handleEvent(AppEvent.ToggleRestartOnPingFailure(tunnelConf)) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+1
-5
@@ -1,10 +1,6 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.main.autotunnel.components
|
package com.zaneschepke.wireguardautotunnel.ui.screens.main.autotunnel.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.Security
|
import androidx.compose.material.icons.outlined.Security
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
|||||||
+2
-12
@@ -5,18 +5,8 @@ import androidx.compose.foundation.layout.Row
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.ContentPasteGo
|
import androidx.compose.material.icons.filled.*
|
||||||
import androidx.compose.material.icons.filled.Create
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material.icons.filled.FileOpen
|
|
||||||
import androidx.compose.material.icons.filled.Link
|
|
||||||
import androidx.compose.material.icons.filled.QrCode
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.HorizontalDivider
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
|||||||
+2
@@ -69,6 +69,7 @@ fun TunnelList(
|
|||||||
isSelected = selected,
|
isSelected = selected,
|
||||||
tunnel = tunnel,
|
tunnel = tunnel,
|
||||||
tunnelState = tunnelState,
|
tunnelState = tunnelState,
|
||||||
|
appSettings = appUiState.appSettings,
|
||||||
onTvClick = {
|
onTvClick = {
|
||||||
navController.navigate(Route.TunnelOptions(tunnel.id))
|
navController.navigate(Route.TunnelOptions(tunnel.id))
|
||||||
viewModel.handleEvent(AppEvent.ClearSelectedTunnels)
|
viewModel.handleEvent(AppEvent.ClearSelectedTunnels)
|
||||||
@@ -78,6 +79,7 @@ fun TunnelList(
|
|||||||
},
|
},
|
||||||
onSwitchClick = { checked -> onToggleTunnel(tunnel, checked) },
|
onSwitchClick = { checked -> onToggleTunnel(tunnel, checked) },
|
||||||
isTv = isTv,
|
isTv = isTv,
|
||||||
|
showDetailedStats = appUiState.appState.showDetailedPingStats,
|
||||||
modifier =
|
modifier =
|
||||||
if (!isTv)
|
if (!isTv)
|
||||||
Modifier.combinedClickable(
|
Modifier.combinedClickable(
|
||||||
|
|||||||
+10
-6
@@ -4,11 +4,7 @@ import androidx.compose.foundation.layout.Arrangement
|
|||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.rounded.Circle
|
import androidx.compose.material.icons.rounded.*
|
||||||
import androidx.compose.material.icons.rounded.Settings
|
|
||||||
import androidx.compose.material.icons.rounded.SettingsEthernet
|
|
||||||
import androidx.compose.material.icons.rounded.Smartphone
|
|
||||||
import androidx.compose.material.icons.rounded.Star
|
|
||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
@@ -24,6 +20,7 @@ import androidx.compose.ui.semantics.semantics
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.model.AppSettings
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.ExpandingRowListItem
|
import com.zaneschepke.wireguardautotunnel.ui.common.ExpandingRowListItem
|
||||||
@@ -36,10 +33,12 @@ fun TunnelRowItem(
|
|||||||
isSelected: Boolean,
|
isSelected: Boolean,
|
||||||
tunnel: TunnelConf,
|
tunnel: TunnelConf,
|
||||||
tunnelState: TunnelState,
|
tunnelState: TunnelState,
|
||||||
|
appSettings: AppSettings,
|
||||||
onTvClick: () -> Unit,
|
onTvClick: () -> Unit,
|
||||||
onToggleSelectedTunnel: (TunnelConf) -> Unit,
|
onToggleSelectedTunnel: (TunnelConf) -> Unit,
|
||||||
onSwitchClick: (Boolean) -> Unit,
|
onSwitchClick: (Boolean) -> Unit,
|
||||||
isTv: Boolean,
|
isTv: Boolean,
|
||||||
|
showDetailedStats: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@@ -113,7 +112,12 @@ fun TunnelRowItem(
|
|||||||
text = tunnel.tunName,
|
text = tunnel.tunName,
|
||||||
expanded = {
|
expanded = {
|
||||||
if (tunnelState.status != TunnelStatus.Down) {
|
if (tunnelState.status != TunnelStatus.Down) {
|
||||||
TunnelStatisticsRow(tunnelState.statistics, tunnel)
|
TunnelStatisticsRow(
|
||||||
|
tunnelState,
|
||||||
|
tunnel,
|
||||||
|
appSettings.isPingEnabled,
|
||||||
|
showDetailedStats,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
trailing = {
|
trailing = {
|
||||||
|
|||||||
+179
-76
@@ -4,106 +4,209 @@ import androidx.compose.animation.AnimatedVisibility
|
|||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.toThreeDecimalPlaceString
|
import com.zaneschepke.wireguardautotunnel.util.extensions.toThreeDecimalPlaceString
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TunnelStatisticsRow(statistics: TunnelStatistics?, tunnelConf: TunnelConf) {
|
fun TunnelStatisticsRow(
|
||||||
val config = TunnelConf.configFromAmQuick(tunnelConf.wgQuick)
|
tunnelState: TunnelState,
|
||||||
|
tunnelConf: TunnelConf,
|
||||||
|
pingEnabled: Boolean,
|
||||||
|
showDetailedStats: Boolean,
|
||||||
|
) {
|
||||||
|
val config = remember(tunnelConf) { TunnelConf.configFromAmQuick(tunnelConf.wgQuick) }
|
||||||
|
val peerText = stringResource(R.string.peer)
|
||||||
|
val handshakeText = stringResource(R.string.handshake)
|
||||||
|
val endpointText = stringResource(R.string.endpoint)
|
||||||
|
val neverText = stringResource(R.string.never)
|
||||||
|
val textStyle = MaterialTheme.typography.bodySmall
|
||||||
|
val textColor = MaterialTheme.colorScheme.outline
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxWidth().padding(start = 45.dp, bottom = 10.dp, end = 10.dp),
|
modifier = Modifier.fillMaxWidth().padding(start = 45.dp, bottom = 10.dp, end = 10.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(10.dp, Alignment.CenterVertically),
|
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
) {
|
) {
|
||||||
config.peers.forEach { peer ->
|
config.peers.forEachIndexed { index, peer ->
|
||||||
val peerId = remember { peer.publicKey.toBase64().subSequence(0, 3).toString() + "***" }
|
key(peer.publicKey.toBase64()) { // Key by peer ID to skip recomposition if unchanged
|
||||||
val endpoint =
|
val peerStats =
|
||||||
remember(statistics) { statistics?.peerStats(peer.publicKey)?.resolvedEndpoint }
|
remember(tunnelState.statistics, peer, tunnelConf) {
|
||||||
val peerRxMB by
|
tunnelState.statistics?.peerStats(peer.publicKey)
|
||||||
remember(statistics) {
|
|
||||||
derivedStateOf {
|
|
||||||
statistics
|
|
||||||
?.peerStats(peer.publicKey)
|
|
||||||
?.rxBytes
|
|
||||||
?.let { NumberUtils.bytesToMB(it) }
|
|
||||||
?.toThreeDecimalPlaceString()
|
|
||||||
}
|
}
|
||||||
}
|
val peerId =
|
||||||
val peerTxMB by
|
remember(peer) {
|
||||||
remember(statistics) {
|
peer.publicKey.toBase64().subSequence(0, 3).toString() + "***"
|
||||||
derivedStateOf {
|
|
||||||
statistics
|
|
||||||
?.peerStats(peer.publicKey)
|
|
||||||
?.txBytes
|
|
||||||
?.let { NumberUtils.bytesToMB(it) }
|
|
||||||
?.toThreeDecimalPlaceString()
|
|
||||||
}
|
}
|
||||||
}
|
val endpoint by
|
||||||
val handshake by
|
remember(peerStats) { derivedStateOf { peerStats?.resolvedEndpoint } }
|
||||||
remember(statistics) {
|
val peerRxMB by
|
||||||
derivedStateOf {
|
remember(peerStats) {
|
||||||
statistics?.peerStats(peer.publicKey)?.latestHandshakeEpochMillis?.let {
|
derivedStateOf {
|
||||||
if (it == 0L) {
|
peerStats
|
||||||
null
|
?.rxBytes
|
||||||
} else {
|
?.let { NumberUtils.bytesToMB(it) }
|
||||||
"${NumberUtils.getSecondsBetweenTimestampAndNow(it)}"
|
?.toThreeDecimalPlaceString() ?: "0.00"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val peerTxMB by
|
||||||
|
remember(peerStats) {
|
||||||
|
derivedStateOf {
|
||||||
|
peerStats
|
||||||
|
?.txBytes
|
||||||
|
?.let { NumberUtils.bytesToMB(it) }
|
||||||
|
?.toThreeDecimalPlaceString() ?: "0.00"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val handshake by
|
||||||
|
remember(peerStats) {
|
||||||
|
derivedStateOf {
|
||||||
|
peerStats?.latestHandshakeEpochMillis?.let {
|
||||||
|
if (it == 0L) null
|
||||||
|
else NumberUtils.getSecondsBetweenTimestampAndNow(it).toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
val pingState by
|
||||||
Row(
|
remember(tunnelState.pingStates) {
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
derivedStateOf {
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.Start),
|
tunnelState.pingStates?.getOrDefault(peer.publicKey, null)
|
||||||
) {
|
}
|
||||||
Text(
|
}
|
||||||
stringResource(R.string.peer).lowercase() + ": $peerId",
|
val lastPingedSeconds by
|
||||||
style = MaterialTheme.typography.bodySmall,
|
remember(peerStats) {
|
||||||
color = MaterialTheme.colorScheme.outline,
|
derivedStateOf {
|
||||||
)
|
pingState?.lastSuccessfulPingMillis?.let {
|
||||||
Text(
|
NumberUtils.getSecondsBetweenTimestampAndNow(it)
|
||||||
stringResource(R.string.handshake) +
|
}
|
||||||
": ${if(handshake == null) stringResource(R.string.never) else handshake + " " + stringResource(R.string.sec)}",
|
}
|
||||||
style = MaterialTheme.typography.bodySmall,
|
}
|
||||||
color = MaterialTheme.colorScheme.outline,
|
|
||||||
)
|
// Group peer stats in a column with internal spacing
|
||||||
}
|
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.Start),
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
"rx: ${peerRxMB ?: 0.00} MB",
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.outline,
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
"tx: ${peerTxMB ?: 0.00} MB",
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.outline,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (endpoint != null) {
|
|
||||||
AnimatedVisibility(visible = true) {
|
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.Start),
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
) {
|
||||||
|
Text("$peerText: $peerId", style = textStyle, color = textColor)
|
||||||
|
Text(
|
||||||
|
"$handshakeText: ${handshake?.let { stringResource(R.string.sec_ago_template, it)} ?: neverText}",
|
||||||
|
style = textStyle,
|
||||||
|
color = textColor,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.endpoint).lowercase() + ": $endpoint",
|
stringResource(R.string.rx_template, peerRxMB),
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = textStyle,
|
||||||
color = MaterialTheme.colorScheme.outline,
|
color = textColor,
|
||||||
)
|
)
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.tx_template, peerTxMB),
|
||||||
|
style = textStyle,
|
||||||
|
color = textColor,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
AnimatedVisibility(visible = endpoint != null) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
) {
|
||||||
|
Text("$endpointText: $endpoint", style = textStyle, color = textColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AnimatedVisibility(visible = pingState != null && pingEnabled) {
|
||||||
|
pingState?.let {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(
|
||||||
|
R.string.reachable_template,
|
||||||
|
stringResource(
|
||||||
|
if (it.isReachable) R.string._true
|
||||||
|
else R.string._false
|
||||||
|
),
|
||||||
|
),
|
||||||
|
style = textStyle,
|
||||||
|
color = textColor,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
stringResource(
|
||||||
|
R.string.ping_target_template,
|
||||||
|
it.pingTarget,
|
||||||
|
),
|
||||||
|
style = textStyle,
|
||||||
|
color = textColor,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (showDetailedStats) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.latency_template, it.rttAvg),
|
||||||
|
style = textStyle,
|
||||||
|
color = textColor,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.jitter_template, it.rttStddev),
|
||||||
|
style = textStyle,
|
||||||
|
color = textColor,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(
|
||||||
|
R.string.packets_sent_template,
|
||||||
|
it.transmitted,
|
||||||
|
),
|
||||||
|
style = textStyle,
|
||||||
|
color = textColor,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
stringResource(
|
||||||
|
R.string.packet_loss_template,
|
||||||
|
it.packetLoss,
|
||||||
|
),
|
||||||
|
style = textStyle,
|
||||||
|
color = textColor,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(
|
||||||
|
R.string.ping_success_template,
|
||||||
|
lastPingedSeconds?.let { sec ->
|
||||||
|
stringResource(R.string.sec_ago_template, sec)
|
||||||
|
} ?: neverText,
|
||||||
|
),
|
||||||
|
style = textStyle,
|
||||||
|
color = textColor,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-5
@@ -7,11 +7,7 @@ import androidx.compose.material3.AlertDialog
|
|||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|||||||
+1
-6
@@ -5,12 +5,7 @@ import androidx.compose.foundation.layout.Column
|
|||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.rounded.MoreVert
|
import androidx.compose.material.icons.rounded.MoreVert
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.shadow
|
import androidx.compose.ui.draw.shadow
|
||||||
|
|||||||
+1
-5
@@ -9,11 +9,7 @@ import androidx.compose.foundation.text.KeyboardOptions
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.rounded.ContentCopy
|
import androidx.compose.material.icons.rounded.ContentCopy
|
||||||
import androidx.compose.material.icons.rounded.Refresh
|
import androidx.compose.material.icons.rounded.Refresh
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.OutlinedTextField
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
|
|||||||
+2
-10
@@ -1,19 +1,11 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.main.config.components
|
package com.zaneschepke.wireguardautotunnel.ui.screens.main.config.components
|
||||||
|
|
||||||
import androidx.compose.foundation.focusGroup
|
import androidx.compose.foundation.focusGroup
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
|||||||
+3
-18
@@ -2,28 +2,13 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.main.config.components
|
|||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.focusGroup
|
import androidx.compose.foundation.focusGroup
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.rounded.Delete
|
import androidx.compose.material.icons.rounded.Delete
|
||||||
import androidx.compose.material.icons.rounded.MoreVert
|
import androidx.compose.material.icons.rounded.MoreVert
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Surface
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.shadow
|
import androidx.compose.ui.draw.shadow
|
||||||
|
|||||||
+1
-5
@@ -18,11 +18,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
|||||||
import java.text.Collator
|
import java.text.Collator
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.stateIn
|
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
|
|||||||
+1
-5
@@ -2,11 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.main.splittunnel.componen
|
|||||||
|
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
|||||||
+1
-6
@@ -1,12 +1,7 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.main.splittunnel.components
|
package com.zaneschepke.wireguardautotunnel.ui.screens.main.splittunnel.components
|
||||||
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
|||||||
+1
-6
@@ -8,12 +8,7 @@ import androidx.compose.foundation.layout.size
|
|||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.Check
|
import androidx.compose.material.icons.outlined.Check
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.MultiChoiceSegmentedButtonRow
|
|
||||||
import androidx.compose.material3.SegmentedButton
|
|
||||||
import androidx.compose.material3.SegmentedButtonDefaults
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
|||||||
+10
-11
@@ -10,6 +10,7 @@ import androidx.compose.runtime.*
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.model.AppSettings
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.SectionDivider
|
import com.zaneschepke.wireguardautotunnel.ui.common.SectionDivider
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
|
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
|
||||||
@@ -25,6 +26,7 @@ fun TunnelOptionsScreen(
|
|||||||
tunnelConf: TunnelConf,
|
tunnelConf: TunnelConf,
|
||||||
viewModel: AppViewModel,
|
viewModel: AppViewModel,
|
||||||
appViewState: AppViewState,
|
appViewState: AppViewState,
|
||||||
|
appSettings: AppSettings,
|
||||||
) {
|
) {
|
||||||
val isTv = LocalIsAndroidTV.current
|
val isTv = LocalIsAndroidTV.current
|
||||||
|
|
||||||
@@ -36,7 +38,10 @@ fun TunnelOptionsScreen(
|
|||||||
// Show authorization prompt if needed
|
// Show authorization prompt if needed
|
||||||
if (showAuthPrompt) {
|
if (showAuthPrompt) {
|
||||||
AuthorizationPromptWrapper(
|
AuthorizationPromptWrapper(
|
||||||
onDismiss = { showAuthPrompt = false },
|
onDismiss = {
|
||||||
|
showAuthPrompt = false
|
||||||
|
viewModel.handleEvent(AppEvent.SetShowModal(AppViewState.ModalType.NONE))
|
||||||
|
},
|
||||||
onSuccess = {
|
onSuccess = {
|
||||||
showAuthPrompt = false
|
showAuthPrompt = false
|
||||||
isAuthorized = true
|
isAuthorized = true
|
||||||
@@ -72,15 +77,9 @@ fun TunnelOptionsScreen(
|
|||||||
SplitTunnelingItem(tunnelConf),
|
SplitTunnelingItem(tunnelConf),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
SectionDivider()
|
if (appSettings.isPingEnabled) {
|
||||||
SurfaceSelectionGroupButton(
|
SectionDivider()
|
||||||
items =
|
SurfaceSelectionGroupButton(items = listOf(pingConfigItem(tunnelConf, viewModel)))
|
||||||
buildList {
|
}
|
||||||
add(PingRestartItem(tunnelConf, viewModel))
|
|
||||||
if (tunnelConf.isPingEnabled) {
|
|
||||||
add(PingConfigItem(tunnelConf, viewModel))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-72
@@ -1,72 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.main.tunneloptions.components
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.config.SubmitConfigurationTextBox
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isValidIpv4orIpv6Address
|
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun PingConfigItem(tunnelConf: TunnelConf, viewModel: AppViewModel): SelectionItem {
|
|
||||||
return SelectionItem(
|
|
||||||
title = {},
|
|
||||||
description = {
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
|
||||||
SubmitConfigurationTextBox(
|
|
||||||
value = tunnelConf.pingIp,
|
|
||||||
label = stringResource(R.string.set_custom_ping_ip),
|
|
||||||
hint = stringResource(R.string.default_ping_ip),
|
|
||||||
isErrorValue = { it?.isNotBlank() == true && !it.isValidIpv4orIpv6Address() },
|
|
||||||
onSubmit = { ip ->
|
|
||||||
viewModel.handleEvent(AppEvent.SetTunnelPingIp(tunnelConf, ip))
|
|
||||||
},
|
|
||||||
)
|
|
||||||
SubmitConfigurationTextBox(
|
|
||||||
value = tunnelConf.pingInterval?.let { (it / 1000).toString() },
|
|
||||||
label = stringResource(R.string.set_custom_ping_internal),
|
|
||||||
hint =
|
|
||||||
"(${stringResource(R.string.optional_default)} ${Constants.PING_INTERVAL / 1000})",
|
|
||||||
keyboardOptions =
|
|
||||||
KeyboardOptions(
|
|
||||||
keyboardType = KeyboardType.Number,
|
|
||||||
imeAction = ImeAction.Done,
|
|
||||||
),
|
|
||||||
isErrorValue = {
|
|
||||||
it?.toLongOrNull()?.let { value -> value >= Long.MAX_VALUE / 1000 } ?: false
|
|
||||||
},
|
|
||||||
onSubmit = { interval ->
|
|
||||||
viewModel.handleEvent(AppEvent.SetTunnelPingInterval(tunnelConf, interval))
|
|
||||||
},
|
|
||||||
)
|
|
||||||
SubmitConfigurationTextBox(
|
|
||||||
value = tunnelConf.pingCooldown?.let { (it / 1000).toString() },
|
|
||||||
label = stringResource(R.string.set_custom_ping_cooldown),
|
|
||||||
hint =
|
|
||||||
"(${stringResource(R.string.optional_default)} ${Constants.PING_COOLDOWN / 1000})",
|
|
||||||
keyboardOptions =
|
|
||||||
KeyboardOptions(
|
|
||||||
keyboardType = KeyboardType.Number,
|
|
||||||
imeAction = ImeAction.Done,
|
|
||||||
),
|
|
||||||
isErrorValue = {
|
|
||||||
it?.toLongOrNull()?.let { value -> value >= Long.MAX_VALUE / 1000 } ?: false
|
|
||||||
},
|
|
||||||
onSubmit = { cooldown ->
|
|
||||||
viewModel.handleEvent(AppEvent.SetTunnelPingCooldown(tunnelConf, cooldown))
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
+4
-29
@@ -2,30 +2,13 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.main.tunneloptions.compon
|
|||||||
|
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.Check
|
import androidx.compose.material.icons.outlined.Check
|
||||||
import androidx.compose.material.icons.outlined.VpnKey
|
import androidx.compose.material.icons.outlined.VpnKey
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.MultiChoiceSegmentedButtonRow
|
|
||||||
import androidx.compose.material3.SegmentedButton
|
|
||||||
import androidx.compose.material3.SegmentedButtonDefaults
|
|
||||||
import androidx.compose.material3.Surface
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.DisposableEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
@@ -38,15 +21,7 @@ import com.zaneschepke.wireguardautotunnel.R
|
|||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.ConfigType
|
import com.zaneschepke.wireguardautotunnel.domain.enums.ConfigType
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.setScreenBrightness
|
import com.zaneschepke.wireguardautotunnel.util.extensions.setScreenBrightness
|
||||||
import io.github.alexzhirkevich.qrose.options.QrBallShape
|
import io.github.alexzhirkevich.qrose.options.*
|
||||||
import io.github.alexzhirkevich.qrose.options.QrBrush
|
|
||||||
import io.github.alexzhirkevich.qrose.options.QrErrorCorrectionLevel
|
|
||||||
import io.github.alexzhirkevich.qrose.options.QrFrameShape
|
|
||||||
import io.github.alexzhirkevich.qrose.options.QrOptions
|
|
||||||
import io.github.alexzhirkevich.qrose.options.QrPixelShape
|
|
||||||
import io.github.alexzhirkevich.qrose.options.circle
|
|
||||||
import io.github.alexzhirkevich.qrose.options.roundCorners
|
|
||||||
import io.github.alexzhirkevich.qrose.options.solid
|
|
||||||
import io.github.alexzhirkevich.qrose.rememberQrCodePainter
|
import io.github.alexzhirkevich.qrose.rememberQrCodePainter
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.main.tunneloptions.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.config.SubmitConfigurationTextBox
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.isValidIpv4orIpv6Address
|
||||||
|
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
||||||
|
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun pingConfigItem(tunnelConf: TunnelConf, viewModel: AppViewModel): SelectionItem {
|
||||||
|
return SelectionItem(
|
||||||
|
title = {},
|
||||||
|
description = {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
|
SubmitConfigurationTextBox(
|
||||||
|
value = tunnelConf.pingTarget,
|
||||||
|
label = stringResource(R.string.set_custom_ping_target),
|
||||||
|
hint = stringResource(R.string.ip_or_hostname),
|
||||||
|
isErrorValue = {
|
||||||
|
it?.isNotBlank() == true &&
|
||||||
|
!it.isValidIpv4orIpv6Address() &&
|
||||||
|
!android.util.Patterns.DOMAIN_NAME.matcher(it).matches()
|
||||||
|
},
|
||||||
|
supportingText = { Text(stringResource(R.string.ping_target_description)) },
|
||||||
|
onSubmit = { ip ->
|
||||||
|
viewModel.handleEvent(AppEvent.SetTunnelPingTarget(tunnelConf, ip))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
+4
-11
@@ -20,15 +20,7 @@ import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelec
|
|||||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalIsAndroidTV
|
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalIsAndroidTV
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalNavController
|
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalNavController
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components.AdvancedSettingsItem
|
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components.AdvancedSettingsItem
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.AppShortcutsItem
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.*
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.KernelModeItem
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.LocalLoggingItem
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.PinLockItem
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.ReadLogsItem
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.RestartAtBootItem
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.alwaysOnVpnItem
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.appearanceItem
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.killSwitchItem
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
|
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
||||||
|
|
||||||
@@ -63,7 +55,8 @@ fun SettingsScreen(uiState: AppUiState, viewModel: AppViewModel) {
|
|||||||
SurfaceSelectionGroupButton(
|
SurfaceSelectionGroupButton(
|
||||||
items =
|
items =
|
||||||
buildList {
|
buildList {
|
||||||
add(AppShortcutsItem(uiState, viewModel))
|
add(tunnelMonitoringItem())
|
||||||
|
add(appShortcutsItem(uiState, viewModel))
|
||||||
if (!isTv) add(alwaysOnVpnItem(uiState, viewModel))
|
if (!isTv) add(alwaysOnVpnItem(uiState, viewModel))
|
||||||
add(killSwitchItem())
|
add(killSwitchItem())
|
||||||
add(RestartAtBootItem(uiState, viewModel))
|
add(RestartAtBootItem(uiState, viewModel))
|
||||||
@@ -81,7 +74,7 @@ fun SettingsScreen(uiState: AppUiState, viewModel: AppViewModel) {
|
|||||||
)
|
)
|
||||||
SectionDivider()
|
SectionDivider()
|
||||||
if (!isTv) {
|
if (!isTv) {
|
||||||
SurfaceSelectionGroupButton(items = listOf(KernelModeItem(uiState, viewModel)))
|
SurfaceSelectionGroupButton(items = listOf(kernelModeItem(uiState, viewModel)))
|
||||||
SectionDivider()
|
SectionDivider()
|
||||||
}
|
}
|
||||||
SurfaceSelectionGroupButton(
|
SurfaceSelectionGroupButton(
|
||||||
|
|||||||
+1
-1
@@ -15,7 +15,7 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
|||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppShortcutsItem(uiState: AppUiState, viewModel: AppViewModel): SelectionItem {
|
fun appShortcutsItem(uiState: AppUiState, viewModel: AppViewModel): SelectionItem {
|
||||||
return SelectionItem(
|
return SelectionItem(
|
||||||
leading = { Icon(Icons.Filled.AppShortcut, contentDescription = null) },
|
leading = { Icon(Icons.Filled.AppShortcut, contentDescription = null) },
|
||||||
trailing = {
|
trailing = {
|
||||||
+1
-1
@@ -15,7 +15,7 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
|||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun KernelModeItem(uiState: AppUiState, viewModel: AppViewModel): SelectionItem {
|
fun kernelModeItem(uiState: AppUiState, viewModel: AppViewModel): SelectionItem {
|
||||||
return SelectionItem(
|
return SelectionItem(
|
||||||
leading = { Icon(Icons.Outlined.Code, contentDescription = null) },
|
leading = { Icon(Icons.Outlined.Code, contentDescription = null) },
|
||||||
title = {
|
title = {
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
|
||||||
|
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.MonitorHeart
|
||||||
|
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.Route
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.button.ForwardButton
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalNavController
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun tunnelMonitoringItem(): SelectionItem {
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
return SelectionItem(
|
||||||
|
leading = { Icon(Icons.Outlined.MonitorHeart, null) },
|
||||||
|
trailing = { ForwardButton { navController.navigate(Route.TunnelMonitoring) } },
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.tunnel_monitoring),
|
||||||
|
style =
|
||||||
|
MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = { navController.navigate(Route.TunnelMonitoring) },
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
-8
@@ -6,14 +6,7 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.runtime.snapshotFlow
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
|||||||
+1
-6
@@ -7,12 +7,7 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material.icons.filled.FolderZip
|
import androidx.compose.material.icons.filled.FolderZip
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material3.HorizontalDivider
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
|||||||
+117
@@ -0,0 +1,117 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring
|
||||||
|
|
||||||
|
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.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Replay
|
||||||
|
import androidx.compose.material.icons.outlined.Timer
|
||||||
|
import androidx.compose.material.icons.outlined.TimerOff
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
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.common.button.surface.SurfaceSelectionGroupButton
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.dropdown.LabelledNumberDropdown
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.components.detailedPingStatsItem
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.components.enablePingMonitoringItem
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
|
||||||
|
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
||||||
|
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TunnelMonitoringScreen(uiState: AppUiState, viewModel: AppViewModel) {
|
||||||
|
val pingInterval: Int by
|
||||||
|
remember(uiState.appSettings) {
|
||||||
|
mutableIntStateOf(uiState.appSettings.tunnelPingIntervalSeconds)
|
||||||
|
}
|
||||||
|
val pingAttempts: Int by
|
||||||
|
remember(uiState.appSettings) { mutableIntStateOf(uiState.appSettings.tunnelPingAttempts) }
|
||||||
|
val pingTimeout: Int? by
|
||||||
|
remember(uiState.appSettings) {
|
||||||
|
mutableStateOf(uiState.appSettings.tunnelPingTimeoutSeconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.Start,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.Top),
|
||||||
|
modifier =
|
||||||
|
Modifier.fillMaxSize()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(vertical = 24.dp)
|
||||||
|
.padding(horizontal = 12.dp),
|
||||||
|
) {
|
||||||
|
SurfaceSelectionGroupButton(listOf(enablePingMonitoringItem(uiState, viewModel)))
|
||||||
|
if (uiState.appSettings.isPingEnabled) {
|
||||||
|
LabelledNumberDropdown(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.tunnel_ping_interval),
|
||||||
|
style =
|
||||||
|
MaterialTheme.typography.bodyMedium.copy(
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
leading = { Icon(Icons.Outlined.Timer, contentDescription = null) },
|
||||||
|
currentValue = pingInterval,
|
||||||
|
onSelected = { selected ->
|
||||||
|
viewModel.handleEvent(AppEvent.SetPingInterval(selected!!))
|
||||||
|
},
|
||||||
|
options = (10..60).step(10).toList(),
|
||||||
|
)
|
||||||
|
LabelledNumberDropdown(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.attempts_per_interval),
|
||||||
|
style =
|
||||||
|
MaterialTheme.typography.bodyMedium.copy(
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
leading = { Icon(Icons.Outlined.Replay, contentDescription = null) },
|
||||||
|
currentValue = pingAttempts,
|
||||||
|
onSelected = { selected ->
|
||||||
|
viewModel.handleEvent(AppEvent.SetPingAttempts(selected!!))
|
||||||
|
},
|
||||||
|
options = (1..5).toList(),
|
||||||
|
)
|
||||||
|
LabelledNumberDropdown(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.ping_timeout),
|
||||||
|
style =
|
||||||
|
MaterialTheme.typography.bodyMedium.copy(
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
leading = { Icon(Icons.Outlined.TimerOff, contentDescription = null) },
|
||||||
|
currentValue = pingTimeout,
|
||||||
|
description = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.timeout_all_attempts),
|
||||||
|
style =
|
||||||
|
MaterialTheme.typography.bodySmall.copy(
|
||||||
|
color = MaterialTheme.colorScheme.outline
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onSelected = { selected ->
|
||||||
|
viewModel.handleEvent(AppEvent.SetPingTimeout(selected))
|
||||||
|
},
|
||||||
|
options = (10..20).toList() + null,
|
||||||
|
)
|
||||||
|
SurfaceSelectionGroupButton(listOf(detailedPingStatsItem(uiState, viewModel)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.components
|
||||||
|
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.QueryStats
|
||||||
|
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
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
|
||||||
|
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
||||||
|
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun detailedPingStatsItem(uiState: AppUiState, viewModel: AppViewModel): SelectionItem {
|
||||||
|
return SelectionItem(
|
||||||
|
leading = { Icon(Icons.Outlined.QueryStats, contentDescription = null) },
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.display_detailed_ping_stats),
|
||||||
|
style =
|
||||||
|
MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailing = {
|
||||||
|
ScaledSwitch(
|
||||||
|
checked = uiState.appState.showDetailedPingStats,
|
||||||
|
onClick = { viewModel.handleEvent(AppEvent.ToggleShowDetailedPingStats) },
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = { viewModel.handleEvent(AppEvent.ToggleShowDetailedPingStats) },
|
||||||
|
)
|
||||||
|
}
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.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
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
|
||||||
|
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
||||||
|
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun enablePingMonitoringItem(uiState: AppUiState, viewModel: AppViewModel): SelectionItem {
|
||||||
|
return SelectionItem(
|
||||||
|
leading = { Icon(Icons.Outlined.NetworkPing, contentDescription = null) },
|
||||||
|
trailing = {
|
||||||
|
ScaledSwitch(
|
||||||
|
checked = uiState.appSettings.isPingEnabled,
|
||||||
|
onClick = { viewModel.handleEvent(AppEvent.TogglePingMonitoring) },
|
||||||
|
)
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.monitoring_ping),
|
||||||
|
style =
|
||||||
|
MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = { viewModel.handleEvent(AppEvent.TogglePingMonitoring) },
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
-5
@@ -12,12 +12,8 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.graphics.StrokeCap
|
import androidx.compose.ui.graphics.StrokeCap
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.LinkAnnotation
|
import androidx.compose.ui.text.*
|
||||||
import androidx.compose.ui.text.SpanStyle
|
|
||||||
import androidx.compose.ui.text.TextLinkStyles
|
|
||||||
import androidx.compose.ui.text.buildAnnotatedString
|
|
||||||
import androidx.compose.ui.text.style.TextDecoration
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
import androidx.compose.ui.text.withLink
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
|||||||
-3
@@ -1,9 +1,6 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.support.components
|
package com.zaneschepke.wireguardautotunnel.ui.screens.support.components
|
||||||
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Balance
|
|
||||||
import androidx.compose.material.icons.filled.Book
|
|
||||||
import androidx.compose.material.icons.filled.Policy
|
|
||||||
import androidx.compose.material.icons.outlined.Balance
|
import androidx.compose.material.icons.outlined.Balance
|
||||||
import androidx.compose.material.icons.outlined.Book
|
import androidx.compose.material.icons.outlined.Book
|
||||||
import androidx.compose.material.icons.outlined.Policy
|
import androidx.compose.material.icons.outlined.Policy
|
||||||
|
|||||||
-2
@@ -2,9 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.support.components
|
|||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.CloudDownload
|
|
||||||
import androidx.compose.material.icons.outlined.CloudDownload
|
import androidx.compose.material.icons.outlined.CloudDownload
|
||||||
import androidx.compose.material.icons.rounded.CloudDownload
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
|||||||
+1
-6
@@ -5,12 +5,7 @@ import android.content.Context
|
|||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
|||||||
@@ -27,9 +27,10 @@ object Constants {
|
|||||||
const val SUBSCRIPTION_TIMEOUT = 5_000L
|
const val SUBSCRIPTION_TIMEOUT = 5_000L
|
||||||
|
|
||||||
const val DEFAULT_PING_IP = "1.1.1.1"
|
const val DEFAULT_PING_IP = "1.1.1.1"
|
||||||
const val PING_TIMEOUT = 5_000L
|
const val PING_TIMEOUT: Int = 5_000
|
||||||
const val PING_INTERVAL = 60_000L
|
|
||||||
const val PING_COOLDOWN = PING_INTERVAL * 60 // one hour
|
const val PING_ATTEMPTS: Int = 3
|
||||||
|
const val PING_INTERVAL = 30
|
||||||
|
|
||||||
val amProperties = listOf("Jc", "Jmin", "Jmax", "S1", "S2", "H1", "H2", "H3", "H4")
|
val amProperties = listOf("Jc", "Jmin", "Jmax", "S1", "S2", "H1", "H2", "H3", "H4")
|
||||||
const val QR_CODE_NAME_PROPERTY = "# Name ="
|
const val QR_CODE_NAME_PROPERTY = "# Name ="
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
|
|||||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.text.DecimalFormat
|
import java.text.DecimalFormat
|
||||||
|
import kotlin.math.pow
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
fun BigDecimal.toThreeDecimalPlaceString(): String {
|
fun BigDecimal.toThreeDecimalPlaceString(): String {
|
||||||
val df = DecimalFormat("#.###")
|
val df = DecimalFormat("#.###")
|
||||||
@@ -28,3 +30,12 @@ fun <T> MutableList<T>.addAllUnique(elements: Collection<T>, comparator: (T, T)
|
|||||||
fun <T, R : Comparable<R>> List<T>.isSortedBy(selector: (T) -> R): Boolean {
|
fun <T, R : Comparable<R>> List<T>.isSortedBy(selector: (T) -> R): Boolean {
|
||||||
return zipWithNext().all { (a, b) -> selector(a) <= selector(b) }
|
return zipWithNext().all { (a, b) -> selector(a) <= selector(b) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Int.toMillis(): Long {
|
||||||
|
return this * 1_000L
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Double.round(decimals: Int): Double {
|
||||||
|
val factor = 10.0.pow(decimals)
|
||||||
|
return (this * factor).roundToInt() / factor
|
||||||
|
}
|
||||||
|
|||||||
+1
-15
@@ -2,17 +2,15 @@ package com.zaneschepke.wireguardautotunnel.util.extensions
|
|||||||
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import com.wireguard.android.backend.BackendException
|
import com.wireguard.android.backend.BackendException
|
||||||
import com.wireguard.config.Peer
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendError
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.HandshakeStatus
|
import com.zaneschepke.wireguardautotunnel.domain.enums.HandshakeStatus
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.events.BackendError
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree
|
import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.Straw
|
import com.zaneschepke.wireguardautotunnel.ui.theme.Straw
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||||
import java.net.InetAddress
|
|
||||||
import org.amnezia.awg.backend.Backend
|
import org.amnezia.awg.backend.Backend
|
||||||
import org.amnezia.awg.backend.Tunnel
|
import org.amnezia.awg.backend.Tunnel
|
||||||
import org.amnezia.awg.config.Config
|
import org.amnezia.awg.config.Config
|
||||||
@@ -40,18 +38,6 @@ fun TunnelStatistics.PeerStats.handshakeStatus(): HandshakeStatus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Peer.isReachable(): Boolean {
|
|
||||||
val host =
|
|
||||||
if (this.endpoint.isPresent) {
|
|
||||||
this.endpoint.get().host
|
|
||||||
} else {
|
|
||||||
Constants.DEFAULT_PING_IP
|
|
||||||
}
|
|
||||||
Timber.d("Checking reachability of peer: $host")
|
|
||||||
val reachable = InetAddress.getByName(host).isReachable(Constants.PING_TIMEOUT.toInt())
|
|
||||||
return reachable
|
|
||||||
}
|
|
||||||
|
|
||||||
fun TunnelStatistics?.asColor(): Color {
|
fun TunnelStatistics?.asColor(): Color {
|
||||||
return this?.mapPeerStats()
|
return this?.mapPeerStats()
|
||||||
?.map { it.value?.handshakeStatus() }
|
?.map { it.value?.handshakeStatus() }
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.util.network
|
||||||
|
|
||||||
|
import com.marsounjan.icmp4a.Icmp
|
||||||
|
import com.marsounjan.icmp4a.Icmp4a
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.round
|
||||||
|
import java.io.IOException
|
||||||
|
import java.time.Instant
|
||||||
|
import kotlin.math.sqrt
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
class NetworkUtils(private val ioDispatcher: CoroutineDispatcher) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a ping with stats, wrapped in a coroutine for async execution. Dynamically handles
|
||||||
|
* IPv4/ICMP or IPv6/ICMPv6 based on the host.
|
||||||
|
*
|
||||||
|
* @param host The host to ping (domain, IPv4, or IPv6 address).
|
||||||
|
* @param count Number of ping attempts.
|
||||||
|
* @param timeoutMillis Overall timeout in milliseconds for the entire operation.
|
||||||
|
* @return PingStats if successful, with isReachable set based on whether any packets were
|
||||||
|
* received, and lastSuccessfulPingMillis set to the approximate epoch millis of the last
|
||||||
|
* successful ping response.
|
||||||
|
* @throws IOException on failure (e.g., unknown host or other errors).
|
||||||
|
* @throws TimeoutCancellationException on timeout.
|
||||||
|
*/
|
||||||
|
suspend fun pingWithStats(
|
||||||
|
host: String,
|
||||||
|
count: Int,
|
||||||
|
timeoutMillis: Long = (count * 2000L),
|
||||||
|
): PingStats {
|
||||||
|
return withTimeout(timeoutMillis) {
|
||||||
|
withContext(ioDispatcher) {
|
||||||
|
val icmp = Icmp4a()
|
||||||
|
val stats = PingStats()
|
||||||
|
val rttList = mutableListOf<Double>()
|
||||||
|
var received = 0
|
||||||
|
var lastSuccessTime: Long? = null
|
||||||
|
|
||||||
|
icmp
|
||||||
|
.pingInterval(host, count = count, intervalMillis = 500)
|
||||||
|
.onEach { status ->
|
||||||
|
when (val result = status.result) {
|
||||||
|
is Icmp.PingResult.Success -> {
|
||||||
|
received++
|
||||||
|
rttList.add(result.ms.toDouble())
|
||||||
|
lastSuccessTime = Instant.now().toEpochMilli()
|
||||||
|
}
|
||||||
|
is Icmp.PingResult.Failed -> {
|
||||||
|
Timber.w("Ping failed with result: ${result.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.catch {
|
||||||
|
when (it) {
|
||||||
|
is CancellationException -> Timber.d("Ping completed")
|
||||||
|
else -> throw it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.collect()
|
||||||
|
|
||||||
|
if (rttList.isNotEmpty()) {
|
||||||
|
stats.transmitted = count
|
||||||
|
stats.received = received
|
||||||
|
stats.packetLoss = ((count - received).toDouble().round(2) / count) * 100
|
||||||
|
stats.rttMin = rttList.minOrNull()?.round(2) ?: 0.0
|
||||||
|
stats.rttAvg = rttList.average().round(2)
|
||||||
|
stats.rttMax = rttList.maxOrNull()?.round(2) ?: 0.0
|
||||||
|
val mean = stats.rttAvg
|
||||||
|
stats.rttStddev =
|
||||||
|
sqrt(rttList.map { (it - mean) * (it - mean) }.average()).round(2)
|
||||||
|
stats.isReachable = received > 0
|
||||||
|
stats.lastSuccessfulPingMillis = lastSuccessTime
|
||||||
|
} else {
|
||||||
|
stats.isReachable = false
|
||||||
|
}
|
||||||
|
stats
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.util.network
|
||||||
|
|
||||||
|
data class PingStats(
|
||||||
|
var transmitted: Int = 0,
|
||||||
|
var received: Int = 0,
|
||||||
|
var packetLoss: Double = 0.0, // percentage
|
||||||
|
var rttMin: Double = 0.0,
|
||||||
|
var rttAvg: Double = 0.0,
|
||||||
|
var rttMax: Double = 0.0,
|
||||||
|
var rttStddev: Double = 0.0,
|
||||||
|
var isReachable: Boolean = false,
|
||||||
|
var lastSuccessfulPingMillis: Long? = null,
|
||||||
|
) {
|
||||||
|
fun handleOffline(): PingStats {
|
||||||
|
return copy(
|
||||||
|
transmitted = 0,
|
||||||
|
received = 0,
|
||||||
|
packetLoss = 0.0,
|
||||||
|
rttMin = 0.0,
|
||||||
|
rttAvg = 0.0,
|
||||||
|
rttMax = 0.0,
|
||||||
|
rttStddev = 0.0,
|
||||||
|
isReachable = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user