Compare commits

...

6 Commits

Author SHA1 Message Date
dependabot[bot] c5c411b962 chore(deps): bump ktorClientCore from 3.1.3 to 3.2.0
Bumps `ktorClientCore` from 3.1.3 to 3.2.0.

Updates `io.ktor:ktor-client-cio` from 3.1.3 to 3.2.0
- [Release notes](https://github.com/ktorio/ktor/releases)
- [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ktorio/ktor/compare/3.1.3...3.2.0)

Updates `io.ktor:ktor-client-content-negotiation` from 3.1.3 to 3.2.0
- [Release notes](https://github.com/ktorio/ktor/releases)
- [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ktorio/ktor/compare/3.1.3...3.2.0)

Updates `io.ktor:ktor-client-core` from 3.1.3 to 3.2.0
- [Release notes](https://github.com/ktorio/ktor/releases)
- [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ktorio/ktor/compare/3.1.3...3.2.0)

Updates `io.ktor:ktor-client-okhttp` from 3.1.3 to 3.2.0
- [Release notes](https://github.com/ktorio/ktor/releases)
- [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ktorio/ktor/compare/3.1.3...3.2.0)

Updates `io.ktor:ktor-serialization-kotlinx-json` from 3.1.3 to 3.2.0
- [Release notes](https://github.com/ktorio/ktor/releases)
- [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ktorio/ktor/compare/3.1.3...3.2.0)

---
updated-dependencies:
- dependency-name: io.ktor:ktor-client-cio
  dependency-version: 3.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.ktor:ktor-client-content-negotiation
  dependency-version: 3.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.ktor:ktor-client-core
  dependency-version: 3.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.ktor:ktor-client-okhttp
  dependency-version: 3.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.ktor:ktor-serialization-kotlinx-json
  dependency-version: 3.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-13 13:44:28 +00:00
Zane Schepke 03345bdf86 fix!: deprecated wifi info api, add detection option selection
This will break for users who have selected get tunnel name via root shell. They will need to select the new option on update. Also, this will cause more location pings. Users who want less location pings will have to enable the legacy api option manually.

#768
#797
2025-06-12 21:49:48 -04:00
Zane Schepke b07e604003 chore: bump deps 2025-06-11 07:16:24 -04:00
Zane Schepke c8b3af4857 refactor: format 2025-05-28 04:07:50 -04:00
Zane Schepke 0a3447c63d fix: improve auto tunnel reliability with delayed check
refactor: add more auto tunnel logging

#797
2025-05-28 04:06:26 -04:00
Zane Schepke 7f3297db79 fix: rapid toggling crash bug, typo 2025-05-28 02:25:25 -04:00
86 changed files with 1038 additions and 442 deletions
@@ -155,9 +155,7 @@
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
},
{
"tableName": "TunnelConfig",
@@ -227,21 +225,18 @@
"fieldPath": "pingInterval",
"columnName": "ping_interval",
"affinity": "INTEGER",
"notNull": false,
"defaultValue": "null"
},
{
"fieldPath": "pingCooldown",
"columnName": "ping_cooldown",
"affinity": "INTEGER",
"notNull": false,
"defaultValue": "null"
},
{
"fieldPath": "pingIp",
"columnName": "ping_ip",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "null"
},
{
@@ -275,11 +270,9 @@
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
}
],
"foreignKeys": []
]
}
],
"views": [],
"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, 'ae51793c4d09ea3194ecd26f0606f35c')"
@@ -0,0 +1,295 @@
{
"formatVersion": 1,
"database": {
"version": 17,
"identityHash": "380d82359c99933cc9ce783347c4ec31",
"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, `split_tunnel_apps` TEXT NOT NULL DEFAULT '', `wifi_detection_method` INTEGER NOT NULL DEFAULT 0)",
"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": "splitTunnelApps",
"columnName": "split_tunnel_apps",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "wifiDetectionMethod",
"columnName": "wifi_detection_method",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "TunnelConfig",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false, `is_primary_tunnel` INTEGER NOT NULL DEFAULT false, `am_quick` TEXT NOT NULL DEFAULT '', `is_Active` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `ping_interval` INTEGER DEFAULT null, `ping_cooldown` INTEGER DEFAULT null, `ping_ip` TEXT DEFAULT null, `is_ethernet_tunnel` INTEGER NOT NULL DEFAULT false, `is_ipv4_preferred` INTEGER NOT NULL DEFAULT true)",
"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": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "pingInterval",
"columnName": "ping_interval",
"affinity": "INTEGER",
"defaultValue": "null"
},
{
"fieldPath": "pingCooldown",
"columnName": "ping_cooldown",
"affinity": "INTEGER",
"defaultValue": "null"
},
{
"fieldPath": "pingIp",
"columnName": "ping_ip",
"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"
}
],
"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, '380d82359c99933cc9ce783347c4ec31')"
]
}
}
@@ -49,6 +49,7 @@ import com.zaneschepke.wireguardautotunnel.ui.navigation.components.DynamicTopAp
import com.zaneschepke.wireguardautotunnel.ui.navigation.components.currentNavBackStackEntryAsNavBarState
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.AutoTunnelScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.advanced.AutoTunnelAdvancedScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.detection.WifiDetectionMethodScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.disclosure.LocationDisclosureScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.main.autotunnel.TunnelAutoTunnelScreen
@@ -284,6 +285,9 @@ class MainActivity : AppCompatActivity() {
composable<Route.AutoTunnelAdvanced> {
AutoTunnelAdvancedScreen(appUiState, viewModel)
}
composable<Route.WifiDetectionMethod> {
WifiDetectionMethodScreen(appUiState, viewModel)
}
composable<Route.Logs> { LogsScreen(appViewState, viewModel) }
composable<Route.Config> { backStack ->
val args = backStack.toRoute<Route.Config>()
@@ -14,9 +14,9 @@ import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
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.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.util.Constants
@@ -15,12 +15,12 @@ import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotificati
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
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.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.state.AutoTunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.NetworkState
@@ -33,6 +33,7 @@ import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
@@ -261,18 +262,44 @@ class AutoTunnelService : LifecycleService() {
lifecycleScope.launch(ioDispatcher) {
Timber.i("Starting auto-tunnel network event watcher")
val settings = appDataRepository.get().settings.get()
Timber.d("Starting with debounce delay of: ${settings.debounceDelaySeconds} seconds")
var reevaluationJob: Job? = null
autoTunnelStateFlow.debounce(settings.debounceDelayMillis()).collect { watcherState ->
if (watcherState == defaultState) return@collect
Timber.d("New auto tunnel state emitted ${watcherState.networkState}")
when (val event = watcherState.asAutoTunnelEvent()) {
is AutoTunnelEvent.Start ->
(event.tunnelConf ?: appDataRepository.get().getPrimaryOrFirstTunnel())
?.let { tunnelManager.startTunnel(it) }
// TODO improve this to target specific tunnels to better support multi-tunnel
is AutoTunnelEvent.Stop -> tunnelManager.stopTunnel()
AutoTunnelEvent.DoNothing -> Timber.i("Auto-tunneling: no condition met")
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 {
const val REEVALUATE_CHECK_DELAY = 5_000L
}
}
@@ -13,7 +13,7 @@ import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import dagger.hilt.android.AndroidEntryPoint
@@ -3,9 +3,9 @@ package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendError
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.TunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
@@ -114,16 +114,16 @@ abstract class BaseTunnel(
if (this@BaseTunnel is UserspaceTunnel) stopActiveTunnels()
tunMutex.withLock {
tunThreads[tunnelConf.id] = thread {
runBlocking {
try {
try {
runBlocking {
Timber.d("Starting tunnel ${tunnelConf.id}...")
startTunnelInner(tunnelConf)
Timber.d("Started complete for tunnel ${tunnelConf.name}...")
} catch (e: InterruptedException) {
Timber.w(
"Tunnel start has been interrupted as ${tunnelConf.name} failed to start"
)
}
} catch (e: InterruptedException) {
Timber.w(
"Tunnel start has been interrupted as ${tunnelConf.name} failed to start"
)
}
}
}
@@ -1,7 +1,7 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import kotlinx.coroutines.flow.MutableStateFlow
@@ -5,9 +5,9 @@ import com.wireguard.android.backend.BackendException
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
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.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.domain.state.WireGuardStatistics
@@ -4,10 +4,10 @@ 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.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendError
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
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.TunnelStatistics
import java.util.concurrent.ConcurrentHashMap
@@ -1,9 +1,9 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendError
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import java.util.concurrent.ConcurrentHashMap
@@ -2,9 +2,9 @@ package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
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.AmneziaStatistics
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
@@ -8,12 +8,12 @@ import androidx.room.TypeConverters
import androidx.room.migration.AutoMigrationSpec
import com.zaneschepke.wireguardautotunnel.data.dao.SettingsDao
import com.zaneschepke.wireguardautotunnel.data.dao.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.data.model.Settings
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.entity.Settings
import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
@Database(
entities = [Settings::class, TunnelConfig::class],
version = 16,
version = 17,
autoMigrations =
[
AutoMigration(from = 1, to = 2),
@@ -31,10 +31,11 @@ import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
AutoMigration(from = 13, to = 14),
AutoMigration(from = 14, to = 15),
AutoMigration(from = 15, to = 16),
AutoMigration(from = 16, to = 17, spec = WifiDetectionMigration::class),
],
exportSchema = true,
)
@TypeConverters(DatabaseListConverters::class)
@TypeConverters(DatabaseConverters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun settingDao(): SettingsDao
@@ -47,3 +48,6 @@ class RemoveLegacySettingColumnsMigration : AutoMigrationSpec
@DeleteColumn(tableName = "Settings", columnName = "is_auto_tunnel_paused")
class RemoveTunnelPauseMigration : AutoMigrationSpec
@DeleteColumn(tableName = "Settings", columnName = "is_wifi_by_shell_enabled")
class WifiDetectionMigration : AutoMigrationSpec
@@ -1,9 +1,10 @@
package com.zaneschepke.wireguardautotunnel.data
import androidx.room.TypeConverter
import com.zaneschepke.wireguardautotunnel.data.entity.Settings
import kotlinx.serialization.json.Json
class DatabaseListConverters {
class DatabaseConverters {
@TypeConverter
fun listToString(value: MutableList<String>): String {
return Json.encodeToString(value)
@@ -20,4 +21,10 @@ class DatabaseListConverters {
Json.decodeFromString<MutableList<String>>(json)
}
}
@TypeConverter fun fromStatus(status: Settings.WifiDetectionMethod): Int = status.value
@TypeConverter
fun toStatus(value: Int): Settings.WifiDetectionMethod =
Settings.WifiDetectionMethod.fromValue(value)
}
@@ -5,7 +5,7 @@ import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.zaneschepke.wireguardautotunnel.data.model.Settings
import com.zaneschepke.wireguardautotunnel.data.entity.Settings
import kotlinx.coroutines.flow.Flow
@Dao
@@ -5,7 +5,7 @@ import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
import kotlinx.coroutines.flow.Flow
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.data.model
package com.zaneschepke.wireguardautotunnel.data.entity
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@@ -0,0 +1,24 @@
package com.zaneschepke.wireguardautotunnel.data.entity
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
data class GeneralState(
val isLocationDisclosureShown: Boolean = LOCATION_DISCLOSURE_SHOWN_DEFAULT,
val isBatteryOptimizationDisableShown: Boolean = BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT,
val isPinLockEnabled: Boolean = PIN_LOCK_ENABLED_DEFAULT,
val expandedTunnelIds: List<Int> = emptyList(),
val isLocalLogsEnabled: Boolean = IS_LOGS_ENABLED_DEFAULT,
val isRemoteControlEnabled: Boolean = IS_REMOTE_CONTROL_ENABLED,
val remoteKey: String? = null,
val locale: String? = null,
val theme: Theme = Theme.AUTOMATIC,
) {
companion object {
const val LOCATION_DISCLOSURE_SHOWN_DEFAULT = false
const val BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT = false
const val PIN_LOCK_ENABLED_DEFAULT = false
const val IS_LOGS_ENABLED_DEFAULT = false
const val IS_REMOTE_CONTROL_ENABLED = false
}
}
@@ -0,0 +1,12 @@
package com.zaneschepke.wireguardautotunnel.data.entity
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class GitHubRelease(
@SerialName("tag_name") val tagName: String,
val name: String?,
val body: String?,
val assets: List<Asset>,
)
@@ -1,9 +1,8 @@
package com.zaneschepke.wireguardautotunnel.data.model
package com.zaneschepke.wireguardautotunnel.data.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
@Entity
data class Settings(
@@ -32,8 +31,6 @@ data class Settings(
val isAmneziaEnabled: Boolean = false,
@ColumnInfo(name = "is_wildcards_enabled", defaultValue = "false")
val isWildcardsEnabled: Boolean = false,
@ColumnInfo(name = "is_wifi_by_shell_enabled", defaultValue = "false")
val isWifiNameByShellEnabled: Boolean = false,
@ColumnInfo(name = "is_stop_on_no_internet_enabled", defaultValue = "false")
val isStopOnNoInternetEnabled: Boolean = false,
@ColumnInfo(name = "is_vpn_kill_switch_enabled", defaultValue = "false")
@@ -46,61 +43,23 @@ data class Settings(
val debounceDelaySeconds: Int = 3,
@ColumnInfo(name = "is_disable_kill_switch_on_trusted_enabled", defaultValue = "false")
val isDisableKillSwitchOnTrustedEnabled: Boolean = false,
@ColumnInfo(name = "is_tunnel_on_unsecure_enabled", defaultValue = "false")
val isTunnelOnUnsecureEnabled: Boolean = false,
@ColumnInfo(name = "split_tunnel_apps", defaultValue = "")
val splitTunnelApps: MutableList<String> = mutableListOf(),
@ColumnInfo(name = "wifi_detection_method", defaultValue = "0")
val wifiDetectionMethod: WifiDetectionMethod = WifiDetectionMethod.fromValue(0),
) {
fun toAppSettings(): AppSettings {
return AppSettings(
id,
isAutoTunnelEnabled,
isTunnelOnMobileDataEnabled,
trustedNetworkSSIDs,
isAlwaysOnVpnEnabled,
isTunnelOnEthernetEnabled,
isShortcutsEnabled,
isTunnelOnWifiEnabled,
isKernelEnabled,
isRestoreOnBootEnabled,
isMultiTunnelEnabled,
isPingEnabled,
isAmneziaEnabled,
isWildcardsEnabled,
isWifiNameByShellEnabled,
isStopOnNoInternetEnabled,
isVpnKillSwitchEnabled,
isKernelKillSwitchEnabled,
isLanOnKillSwitchEnabled,
debounceDelaySeconds,
isDisableKillSwitchOnTrustedEnabled,
)
}
enum class WifiDetectionMethod(val value: Int) {
DEFAULT(0),
LEGACY(1),
ROOT(2),
SHIZUKU(3);
companion object {
fun from(appSettings: AppSettings): Settings {
return with(appSettings) {
Settings(
id,
isAutoTunnelEnabled,
isTunnelOnMobileDataEnabled,
trustedNetworkSSIDs.toMutableList(),
isAlwaysOnVpnEnabled,
isTunnelOnEthernetEnabled,
isShortcutsEnabled,
isTunnelOnWifiEnabled,
isKernelEnabled,
isRestoreOnBootEnabled,
isMultiTunnelEnabled,
isPingEnabled,
isAmneziaEnabled,
isWildcardsEnabled,
isWifiNameByShellEnabled,
isStopOnNoInternetEnabled,
isVpnKillSwitchEnabled,
isKernelKillSwitchEnabled,
isLanOnKillSwitchEnabled,
debounceDelaySeconds,
isDisableKillSwitchOnTrustedEnabled,
)
}
companion object {
fun fromValue(value: Int): WifiDetectionMethod =
entries.find { it.value == value } ?: DEFAULT
}
}
}
@@ -1,10 +1,9 @@
package com.zaneschepke.wireguardautotunnel.data.model
package com.zaneschepke.wireguardautotunnel.data.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
@Entity(indices = [Index(value = ["name"], unique = true)])
data class TunnelConfig(
@@ -30,48 +29,7 @@ data class TunnelConfig(
var isIpv4Preferred: Boolean = true,
) {
fun toTunnel(): TunnelConf {
return TunnelConf(
id,
name,
wgQuick,
tunnelNetworks,
isMobileDataTunnel,
isPrimaryTunnel,
amQuick,
isActive,
isPingEnabled,
pingInterval,
pingCooldown,
pingIp,
isEthernetTunnel,
isIpv4Preferred,
)
}
companion object {
const val AM_QUICK_DEFAULT = ""
fun from(tunnelConf: TunnelConf): TunnelConfig {
return with(tunnelConf) {
return TunnelConfig(
id,
tunName,
wgQuick,
tunnelNetworks.toMutableList(),
isMobileDataTunnel,
isPrimaryTunnel,
amQuick,
isActive,
isPingEnabled,
pingInterval,
pingCooldown,
pingIp,
isEthernetTunnel,
isIpv4Preferred,
)
}
}
}
}
@@ -0,0 +1,37 @@
package com.zaneschepke.wireguardautotunnel.data.mapper
import com.zaneschepke.wireguardautotunnel.data.entity.GeneralState
import com.zaneschepke.wireguardautotunnel.domain.model.AppState
object GeneralStateMapper {
fun toAppState(generalState: GeneralState): AppState =
with(generalState) {
AppState(
isLocationDisclosureShown,
isBatteryOptimizationDisableShown,
isPinLockEnabled,
expandedTunnelIds,
isLocalLogsEnabled,
isRemoteControlEnabled,
remoteKey,
locale,
theme,
)
}
fun toGeneralState(appState: AppState): GeneralState {
return with(appState) {
GeneralState(
isLocationDisclosureShown,
isBatteryOptimizationDisableShown,
isPinLockEnabled,
expandedTunnelIds,
isLocalLogsEnabled,
isRemoteControlEnabled,
remoteKey,
locale,
theme,
)
}
}
}
@@ -0,0 +1,20 @@
package com.zaneschepke.wireguardautotunnel.data.mapper
import com.zaneschepke.wireguardautotunnel.data.entity.GitHubRelease
import com.zaneschepke.wireguardautotunnel.domain.model.AppUpdate
import kotlin.collections.firstOrNull
object GitHubReleaseMapper {
fun toAppUpdate(gitHubRelease: GitHubRelease): AppUpdate {
with(gitHubRelease) {
val apkAsset = assets.firstOrNull { it.name.endsWith(".apk") }
return AppUpdate(
version = tagName.removePrefix("v"),
title = name ?: "Update $tagName",
releaseNotes = body ?: "No release notes provided",
apkUrl = apkAsset?.browserDownloadUrl,
apkFileName = apkAsset?.name,
)
}
}
}
@@ -0,0 +1,67 @@
package com.zaneschepke.wireguardautotunnel.data.mapper
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
import com.zaneschepke.wireguardautotunnel.data.entity.Settings
import com.zaneschepke.wireguardautotunnel.domain.model.AppSettings
object SettingsMapper {
fun toAppSettings(settings: Settings): AppSettings {
return AppSettings(
id = settings.id,
isAutoTunnelEnabled = settings.isAutoTunnelEnabled,
isTunnelOnMobileDataEnabled = settings.isTunnelOnMobileDataEnabled,
trustedNetworkSSIDs = settings.trustedNetworkSSIDs,
isAlwaysOnVpnEnabled = settings.isAlwaysOnVpnEnabled,
isTunnelOnEthernetEnabled = settings.isTunnelOnEthernetEnabled,
isShortcutsEnabled = settings.isShortcutsEnabled,
isTunnelOnWifiEnabled = settings.isTunnelOnWifiEnabled,
isKernelEnabled = settings.isKernelEnabled,
isRestoreOnBootEnabled = settings.isRestoreOnBootEnabled,
isMultiTunnelEnabled = settings.isMultiTunnelEnabled,
isPingEnabled = settings.isPingEnabled,
isAmneziaEnabled = settings.isAmneziaEnabled,
isWildcardsEnabled = settings.isWildcardsEnabled,
isStopOnNoInternetEnabled = settings.isStopOnNoInternetEnabled,
isVpnKillSwitchEnabled = settings.isVpnKillSwitchEnabled,
isKernelKillSwitchEnabled = settings.isKernelKillSwitchEnabled,
isLanOnKillSwitchEnabled = settings.isLanOnKillSwitchEnabled,
debounceDelaySeconds = settings.debounceDelaySeconds,
isDisableKillSwitchOnTrustedEnabled = settings.isDisableKillSwitchOnTrustedEnabled,
isTunnelOnUnsecureEnabled = settings.isTunnelOnUnsecureEnabled,
splitTunnelApps = settings.splitTunnelApps,
wifiDetectionMethod =
AndroidNetworkMonitor.WifiDetectionMethod.fromValue(
settings.wifiDetectionMethod.value
),
)
}
fun toSettings(appSettings: AppSettings): Settings {
return Settings(
id = appSettings.id,
isAutoTunnelEnabled = appSettings.isAutoTunnelEnabled,
isTunnelOnMobileDataEnabled = appSettings.isTunnelOnMobileDataEnabled,
trustedNetworkSSIDs = appSettings.trustedNetworkSSIDs.toMutableList(),
isAlwaysOnVpnEnabled = appSettings.isAlwaysOnVpnEnabled,
isTunnelOnEthernetEnabled = appSettings.isTunnelOnEthernetEnabled,
isShortcutsEnabled = appSettings.isShortcutsEnabled,
isTunnelOnWifiEnabled = appSettings.isTunnelOnWifiEnabled,
isKernelEnabled = appSettings.isKernelEnabled,
isRestoreOnBootEnabled = appSettings.isRestoreOnBootEnabled,
isMultiTunnelEnabled = appSettings.isMultiTunnelEnabled,
isPingEnabled = appSettings.isPingEnabled,
isAmneziaEnabled = appSettings.isAmneziaEnabled,
isWildcardsEnabled = appSettings.isWildcardsEnabled,
isStopOnNoInternetEnabled = appSettings.isStopOnNoInternetEnabled,
isVpnKillSwitchEnabled = appSettings.isVpnKillSwitchEnabled,
isKernelKillSwitchEnabled = appSettings.isKernelKillSwitchEnabled,
isLanOnKillSwitchEnabled = appSettings.isLanOnKillSwitchEnabled,
debounceDelaySeconds = appSettings.debounceDelaySeconds,
isDisableKillSwitchOnTrustedEnabled = appSettings.isDisableKillSwitchOnTrustedEnabled,
isTunnelOnUnsecureEnabled = appSettings.isTunnelOnUnsecureEnabled,
splitTunnelApps = appSettings.splitTunnelApps.toMutableList(),
wifiDetectionMethod =
Settings.WifiDetectionMethod.fromValue(appSettings.wifiDetectionMethod.value),
)
}
}
@@ -0,0 +1,48 @@
package com.zaneschepke.wireguardautotunnel.data.mapper
import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
object TunnelConfigMapper {
fun toTunnelConf(tunnelConfig: TunnelConfig): TunnelConf {
return with(tunnelConfig) {
TunnelConf(
id,
name,
wgQuick,
tunnelNetworks,
isMobileDataTunnel,
isPrimaryTunnel,
amQuick,
isActive,
isPingEnabled,
pingInterval,
pingCooldown,
pingIp,
isEthernetTunnel,
isIpv4Preferred,
)
}
}
fun toTunnelConfig(tunnelConf: TunnelConf): TunnelConfig {
return with(tunnelConf) {
TunnelConfig(
id,
tunName,
wgQuick,
tunnelNetworks.toMutableList(),
isMobileDataTunnel,
isPrimaryTunnel,
amQuick,
isActive,
isPingEnabled,
pingInterval,
pingCooldown,
pingIp,
isEthernetTunnel,
isIpv4Preferred,
)
}
}
}
@@ -1,54 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.model
import com.zaneschepke.wireguardautotunnel.domain.entity.AppState
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
data class GeneralState(
val isLocationDisclosureShown: Boolean = LOCATION_DISCLOSURE_SHOWN_DEFAULT,
val isBatteryOptimizationDisableShown: Boolean = BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT,
val isPinLockEnabled: Boolean = PIN_LOCK_ENABLED_DEFAULT,
val expandedTunnelIds: List<Int> = emptyList(),
val isLocalLogsEnabled: Boolean = IS_LOGS_ENABLED_DEFAULT,
val isRemoteControlEnabled: Boolean = IS_REMOTE_CONTROL_ENABLED,
val remoteKey: String? = null,
val locale: String? = null,
val theme: Theme = Theme.AUTOMATIC,
) {
fun toAppState(): AppState =
AppState(
isLocationDisclosureShown,
isBatteryOptimizationDisableShown,
isPinLockEnabled,
expandedTunnelIds,
isLocalLogsEnabled,
isRemoteControlEnabled,
remoteKey,
locale,
theme,
)
companion object {
fun from(appState: AppState): GeneralState {
return with(appState) {
GeneralState(
isLocationDisclosureShown,
isBatteryOptimizationDisableShown,
isPinLockEnabled,
expandedTunnelIds,
isLocalLogsEnabled,
isRemoteControlEnabled,
remoteKey,
locale,
theme,
)
}
}
const val LOCATION_DISCLOSURE_SHOWN_DEFAULT = false
const val BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT = false
const val PIN_LOCK_ENABLED_DEFAULT = false
const val IS_LOGS_ENABLED_DEFAULT = false
const val IS_REMOTE_CONTROL_ENABLED = false
}
}
@@ -1,24 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.model
import com.zaneschepke.wireguardautotunnel.domain.entity.AppUpdate
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class GitHubRelease(
@SerialName("tag_name") val tagName: String,
val name: String?,
val body: String?,
val assets: List<Asset>,
) {
fun toAppUpdate(): AppUpdate {
val apkAsset = assets.firstOrNull { it.name.endsWith(".apk") }
return AppUpdate(
version = tagName.removePrefix("v"),
title = name ?: "Update $tagName",
releaseNotes = body ?: "No release notes provided",
apkUrl = apkAsset?.browserDownloadUrl,
apkFileName = apkAsset?.name,
)
}
}
@@ -1,6 +1,6 @@
package com.zaneschepke.wireguardautotunnel.data.network
import com.zaneschepke.wireguardautotunnel.data.model.GitHubRelease
import com.zaneschepke.wireguardautotunnel.data.entity.GitHubRelease
interface GitHubApi {
suspend fun getLatestRelease(owner: String, repo: String): Result<GitHubRelease>
@@ -1,6 +1,6 @@
package com.zaneschepke.wireguardautotunnel.data.network
import com.zaneschepke.wireguardautotunnel.data.model.GitHubRelease
import com.zaneschepke.wireguardautotunnel.data.entity.GitHubRelease
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.plugins.ClientRequestException
@@ -1,6 +1,6 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.AppSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
@@ -1,8 +1,9 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.DataStoreManager
import com.zaneschepke.wireguardautotunnel.data.model.GeneralState
import com.zaneschepke.wireguardautotunnel.domain.entity.AppState
import com.zaneschepke.wireguardautotunnel.data.entity.GeneralState
import com.zaneschepke.wireguardautotunnel.data.mapper.GeneralStateMapper
import com.zaneschepke.wireguardautotunnel.domain.model.AppState
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
import kotlinx.coroutines.flow.Flow
@@ -153,5 +154,5 @@ class DataStoreAppStateRepository(private val dataStoreManager: DataStoreManager
}
} ?: GeneralState()
}
.map { it.toAppState() }
.map(GeneralStateMapper::toAppState)
}
@@ -2,9 +2,10 @@ package com.zaneschepke.wireguardautotunnel.data.repository
import android.content.Context
import com.zaneschepke.wireguardautotunnel.BuildConfig
import com.zaneschepke.wireguardautotunnel.data.mapper.GitHubReleaseMapper
import com.zaneschepke.wireguardautotunnel.data.network.GitHubApi
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.entity.AppUpdate
import com.zaneschepke.wireguardautotunnel.domain.model.AppUpdate
import com.zaneschepke.wireguardautotunnel.domain.repository.UpdateRepository
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
import io.ktor.client.HttpClient
@@ -47,7 +48,7 @@ class GitHubUpdateRepository(
Timber.i("Latest version: $newVersion, current version: $currentVersion")
if (NumberUtils.compareVersions(newVersion, currentVersion) > 0) {
release.toAppUpdate()
GitHubReleaseMapper.toAppUpdate(release)
} else {
null
}
@@ -1,9 +1,10 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.dao.SettingsDao
import com.zaneschepke.wireguardautotunnel.data.model.Settings
import com.zaneschepke.wireguardautotunnel.data.entity.Settings
import com.zaneschepke.wireguardautotunnel.data.mapper.SettingsMapper
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
import com.zaneschepke.wireguardautotunnel.domain.model.AppSettings
import com.zaneschepke.wireguardautotunnel.domain.repository.AppSettingRepository
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.flowOn
@@ -16,15 +17,15 @@ class RoomSettingsRepository(
) : AppSettingRepository {
override suspend fun save(appSettings: AppSettings) {
withContext(ioDispatcher) { settingsDoa.save(Settings.from(appSettings)) }
withContext(ioDispatcher) { settingsDoa.save(SettingsMapper.toSettings(appSettings)) }
}
override val flow =
settingsDoa.getSettingsFlow().flowOn(ioDispatcher).map { it.toAppSettings() }
settingsDoa.getSettingsFlow().flowOn(ioDispatcher).map(SettingsMapper::toAppSettings)
override suspend fun get(): AppSettings {
return withContext(ioDispatcher) {
(settingsDoa.getAll().firstOrNull() ?: Settings()).toAppSettings()
SettingsMapper.toAppSettings(settingsDoa.getAll().firstOrNull() ?: Settings())
}
}
}
@@ -1,9 +1,9 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.dao.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.mapper.TunnelConfigMapper
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.util.extensions.Tunnels
import kotlinx.coroutines.CoroutineDispatcher
@@ -17,19 +17,25 @@ class RoomTunnelRepository(
) : TunnelRepository {
override val flow =
tunnelConfigDao.getAllFlow().flowOn(ioDispatcher).map { it.map { it.toTunnel() } }
tunnelConfigDao.getAllFlow().flowOn(ioDispatcher).map {
it.map(TunnelConfigMapper::toTunnelConf)
}
override suspend fun getAll(): Tunnels {
return withContext(ioDispatcher) { tunnelConfigDao.getAll().map { it.toTunnel() } }
return withContext(ioDispatcher) {
tunnelConfigDao.getAll().map(TunnelConfigMapper::toTunnelConf)
}
}
override suspend fun save(tunnelConf: TunnelConf) {
withContext(ioDispatcher) { tunnelConfigDao.save(TunnelConfig.from(tunnelConf)) }
withContext(ioDispatcher) {
tunnelConfigDao.save(TunnelConfigMapper.toTunnelConfig(tunnelConf))
}
}
override suspend fun saveAll(tunnelConfList: List<TunnelConf>) {
withContext(ioDispatcher) {
tunnelConfigDao.saveAll(tunnelConfList.map(TunnelConfig::from))
tunnelConfigDao.saveAll(tunnelConfList.map(TunnelConfigMapper::toTunnelConfig))
}
}
@@ -55,15 +61,21 @@ class RoomTunnelRepository(
}
override suspend fun delete(tunnelConf: TunnelConf) {
withContext(ioDispatcher) { tunnelConfigDao.delete(TunnelConfig.from(tunnelConf)) }
withContext(ioDispatcher) {
tunnelConfigDao.delete(TunnelConfigMapper.toTunnelConfig(tunnelConf))
}
}
override suspend fun getById(id: Int): TunnelConf? {
return withContext(ioDispatcher) { tunnelConfigDao.getById(id.toLong())?.toTunnel() }
return withContext(ioDispatcher) {
tunnelConfigDao.getById(id.toLong())?.let(TunnelConfigMapper::toTunnelConf)
}
}
override suspend fun getActive(): Tunnels {
return withContext(ioDispatcher) { tunnelConfigDao.getActive().map { it.toTunnel() } }
return withContext(ioDispatcher) {
tunnelConfigDao.getActive().map(TunnelConfigMapper::toTunnelConf)
}
}
override suspend fun count(): Int {
@@ -71,22 +83,26 @@ class RoomTunnelRepository(
}
override suspend fun findByTunnelName(name: String): TunnelConf? {
return withContext(ioDispatcher) { tunnelConfigDao.getByName(name)?.toTunnel() }
return withContext(ioDispatcher) {
tunnelConfigDao.getByName(name)?.let(TunnelConfigMapper::toTunnelConf)
}
}
override suspend fun findByTunnelNetworksName(name: String): Tunnels {
return withContext(ioDispatcher) {
tunnelConfigDao.findByTunnelNetworkName(name).map { it.toTunnel() }
tunnelConfigDao.findByTunnelNetworkName(name).map(TunnelConfigMapper::toTunnelConf)
}
}
override suspend fun findByMobileDataTunnel(): Tunnels {
return withContext(ioDispatcher) {
tunnelConfigDao.findByMobileDataTunnel().map { it.toTunnel() }
tunnelConfigDao.findByMobileDataTunnel().map(TunnelConfigMapper::toTunnelConf)
}
}
override suspend fun findPrimary(): Tunnels {
return withContext(ioDispatcher) { tunnelConfigDao.findByPrimary().map { it.toTunnel() } }
return withContext(ioDispatcher) {
tunnelConfigDao.findByPrimary().map(TunnelConfigMapper::toTunnelConf)
}
}
}
@@ -113,9 +113,11 @@ class TunnelModule {
@ApplicationContext context: Context,
settingsRepository: AppSettingRepository,
): NetworkMonitor {
return AndroidNetworkMonitor(context) {
runBlocking { settingsRepository.get().isWifiNameByShellEnabled }
}
val method = runBlocking { settingsRepository.get().wifiDetectionMethod }
return AndroidNetworkMonitor(
context,
AndroidNetworkMonitor.WifiDetectionMethod.fromValue(method.value),
)
}
@Singleton
@@ -1,6 +1,6 @@
package com.zaneschepke.wireguardautotunnel.domain.events
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
sealed class AutoTunnelEvent {
data class Start(val tunnelConf: TunnelConf? = null) : AutoTunnelEvent()
@@ -1,4 +1,6 @@
package com.zaneschepke.wireguardautotunnel.domain.entity
package com.zaneschepke.wireguardautotunnel.domain.model
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
data class AppSettings(
val id: Int = 0,
@@ -15,15 +17,30 @@ data class AppSettings(
val isPingEnabled: Boolean = false,
val isAmneziaEnabled: Boolean = false,
val isWildcardsEnabled: Boolean = false,
val isWifiNameByShellEnabled: Boolean = false,
val isStopOnNoInternetEnabled: Boolean = false,
val isVpnKillSwitchEnabled: Boolean = false,
val isKernelKillSwitchEnabled: Boolean = false,
val isLanOnKillSwitchEnabled: Boolean = false,
val debounceDelaySeconds: Int = 3,
val isDisableKillSwitchOnTrustedEnabled: Boolean = false,
val isTunnelOnUnsecureEnabled: Boolean = false,
val splitTunnelApps: List<String> = emptyList(),
val wifiDetectionMethod: AndroidNetworkMonitor.WifiDetectionMethod =
AndroidNetworkMonitor.WifiDetectionMethod.DEFAULT,
) {
fun debounceDelayMillis(): Long {
return debounceDelaySeconds * 1000L
}
fun toAutoTunnelStateString(): String {
return """
TunnelOnWifi: $isTunnelOnWifiEnabled
TunnelOnMobileData: $isTunnelOnMobileDataEnabled
TunnelOnEthernet: $isTunnelOnEthernetEnabled
Wildcards: $isWildcardsEnabled
StopOnNoInternet: $isStopOnNoInternetEnabled
Trusted Networks: $trustedNetworkSSIDs
"""
.trimIndent()
}
}
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.domain.entity
package com.zaneschepke.wireguardautotunnel.domain.model
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.domain.entity
package com.zaneschepke.wireguardautotunnel.domain.model
data class AppUpdate(
val version: String,
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.domain.entity
package com.zaneschepke.wireguardautotunnel.domain.model
import com.wireguard.android.backend.Tunnel
import com.wireguard.config.Config
@@ -1,6 +1,6 @@
package com.zaneschepke.wireguardautotunnel.domain.repository
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
interface AppDataRepository {
suspend fun getPrimaryOrFirstTunnel(): TunnelConf?
@@ -1,6 +1,6 @@
package com.zaneschepke.wireguardautotunnel.domain.repository
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
import com.zaneschepke.wireguardautotunnel.domain.model.AppSettings
import kotlinx.coroutines.flow.Flow
interface AppSettingRepository {
@@ -1,6 +1,6 @@
package com.zaneschepke.wireguardautotunnel.domain.repository
import com.zaneschepke.wireguardautotunnel.domain.entity.AppState
import com.zaneschepke.wireguardautotunnel.domain.model.AppState
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
import kotlinx.coroutines.flow.Flow
@@ -1,6 +1,6 @@
package com.zaneschepke.wireguardautotunnel.domain.repository
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.util.extensions.Tunnels
import kotlinx.coroutines.flow.Flow
@@ -1,6 +1,6 @@
package com.zaneschepke.wireguardautotunnel.domain.repository
import com.zaneschepke.wireguardautotunnel.domain.entity.AppUpdate
import com.zaneschepke.wireguardautotunnel.domain.model.AppUpdate
import java.io.File
interface UpdateRepository {
@@ -3,10 +3,10 @@ package com.zaneschepke.wireguardautotunnel.domain.state
import com.zaneschepke.wireguardautotunnel.core.tunnel.allDown
import com.zaneschepke.wireguardautotunnel.core.tunnel.hasActive
import com.zaneschepke.wireguardautotunnel.core.tunnel.isUp
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
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.TunnelConf
import com.zaneschepke.wireguardautotunnel.util.extensions.isMatchingToWildcardList
data class AutoTunnelState(
@@ -16,6 +16,7 @@ data class AutoTunnelState(
val tunnels: List<TunnelConf> = emptyList(),
) {
// also need to check for Wi-Fi state as there is some overlap when they are both connected
private fun isMobileDataActive(): Boolean {
return !networkState.isEthernetConnected &&
!networkState.isWifiConnected &&
@@ -50,6 +51,7 @@ data class AutoTunnelState(
return getTunnelWithMatchingTunnelNetwork() ?: tunnels.firstOrNull { it.isPrimaryTunnel }
}
// ignore cellular state as there is overlap where it may still be active, but not prioritized
private fun isWifiActive(): Boolean {
return !networkState.isEthernetConnected && networkState.isWifiConnected
}
@@ -13,6 +13,8 @@ sealed class Route {
@Serializable data object AutoTunnelAdvanced : Route()
@Serializable data object WifiDetectionMethod : Route()
@Serializable data object LocationDisclosure : Route()
@Serializable data object Appearance : Route()
@@ -73,7 +73,7 @@ fun IconSurfaceButton(
else MaterialTheme.colorScheme.onSurface,
)
}
Column {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(title, style = MaterialTheme.typography.titleMedium)
description?.let {
Text(
@@ -188,6 +188,14 @@ fun currentNavBackStackEntryAsNavBarState(
route = Route.Display,
)
backStackEntry.isCurrentRoute(Route.WifiDetectionMethod::class) ->
NavBarState(
showTop = true,
showBottom = true,
topTitle = { Text(stringResource(R.string.wifi_detection_method)) },
route = Route.WifiDetectionMethod,
)
backStackEntry.isCurrentRoute(Route.KillSwitch::class) ->
NavBarState(
showTop = true,
@@ -1,18 +1,9 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
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.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Code
import androidx.compose.material.icons.outlined.Filter1
import androidx.compose.material.icons.outlined.Security
import androidx.compose.material.icons.outlined.VpnKeyOff
import androidx.compose.material.icons.outlined.Wifi
import androidx.compose.material.icons.outlined.*
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@@ -26,14 +17,19 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
import com.zaneschepke.networkmonitor.NetworkStatus
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.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberClipboardHelper
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.LearnMoreLinkLabel
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
import com.zaneschepke.wireguardautotunnel.util.extensions.asString
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
@@ -47,6 +43,7 @@ fun WifiTunnelingItems(
isWifiNameReadable: () -> Boolean,
): List<SelectionItem> {
val context = LocalContext.current
val navController = LocalNavController.current
val clipboardHelper = rememberClipboardHelper()
val baseItems =
@@ -107,40 +104,40 @@ fun WifiTunnelingItems(
}
},
onClick = { viewModel.handleEvent(AppEvent.ToggleAutoTunnelOnWifi) },
),
SelectionItem(
leadingIcon = Icons.Outlined.Code,
title = {
Text(
stringResource(R.string.wifi_name_via_shell),
style =
MaterialTheme.typography.bodyMedium.copy(
MaterialTheme.colorScheme.onSurface
),
)
},
description = {
Text(
stringResource(R.string.use_root_shell_for_wifi),
style =
MaterialTheme.typography.bodySmall.copy(
MaterialTheme.colorScheme.outline
),
)
},
trailing = {
ScaledSwitch(
checked = uiState.appSettings.isWifiNameByShellEnabled,
onClick = { viewModel.handleEvent(AppEvent.ToggleRootShellWifi) },
)
},
onClick = { viewModel.handleEvent(AppEvent.ToggleRootShellWifi) },
),
)
)
return if (uiState.appSettings.isTunnelOnWifiEnabled) {
baseItems +
listOf(
SelectionItem(
leadingIcon = Icons.Outlined.WifiFind,
title = {
Text(
stringResource(R.string.wifi_detection_method),
style =
MaterialTheme.typography.bodyMedium.copy(
MaterialTheme.colorScheme.onSurface
),
)
},
description = {
Text(
stringResource(
R.string.current_template,
uiState.appSettings.wifiDetectionMethod.asString(context),
),
style =
MaterialTheme.typography.bodySmall.copy(
MaterialTheme.colorScheme.outline
),
)
},
trailing = {
ForwardButton { navController.navigate(Route.WifiDetectionMethod) }
},
onClick = { navController.navigate(Route.WifiDetectionMethod) },
),
SelectionItem(
leadingIcon = Icons.Outlined.Filter1,
title = {
@@ -205,7 +202,8 @@ fun WifiTunnelingItems(
currentText = currentText,
onSave = { ssid ->
if (
uiState.appSettings.isWifiNameByShellEnabled ||
uiState.appSettings.wifiDetectionMethod ==
AndroidNetworkMonitor.WifiDetectionMethod.ROOT ||
isWifiNameReadable()
) {
viewModel.handleEvent(AppEvent.SaveTrustedSSID(ssid))
@@ -0,0 +1,41 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.detection
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.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
import com.zaneschepke.wireguardautotunnel.ui.common.button.IconSurfaceButton
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
import com.zaneschepke.wireguardautotunnel.util.extensions.asDescriptionString
import com.zaneschepke.wireguardautotunnel.util.extensions.asString
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
@Composable
fun WifiDetectionMethodScreen(uiState: AppUiState, viewModel: AppViewModel) {
val context = LocalContext.current
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top),
modifier = Modifier.fillMaxSize().padding(top = 24.dp).padding(horizontal = 24.dp),
) {
enumValues<AndroidNetworkMonitor.WifiDetectionMethod>().forEach {
val title = it.asString(context)
val description = it.asDescriptionString(context)
// TODO skip shizuku for now
if (it == AndroidNetworkMonitor.WifiDetectionMethod.SHIZUKU) return@forEach
IconSurfaceButton(
title = title,
onClick = { viewModel.handleEvent(AppEvent.SetDetectionMethod(it)) },
selected = uiState.appSettings.wifiDetectionMethod == it,
description = description,
)
}
}
}
@@ -67,7 +67,9 @@ fun MainScreen(appUiState: AppUiState, appViewState: AppViewState, viewModel: Ap
)
return@rememberLauncherForActivityResult
}
scanLauncher.launch(ScanOptions().setDesiredBarcodeFormats(ScanOptions.QR_CODE).setBeepEnabled(false))
scanLauncher.launch(
ScanOptions().setDesiredBarcodeFormats(ScanOptions.QR_CODE).setBeepEnabled(false)
)
}
if (appViewState.showModal == AppViewState.ModalType.DELETE) {
@@ -15,8 +15,8 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.model.AppSettings
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.screens.main.autotunnel.components.EthernetTunnelItem
import com.zaneschepke.wireguardautotunnel.ui.screens.main.autotunnel.components.MobileDataTunnelItem
@@ -7,7 +7,7 @@ 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.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
@@ -7,7 +7,7 @@ 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.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
@@ -16,8 +16,8 @@ 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.domain.entity.AppSettings
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.model.AppSettings
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components.TrustedNetworkTextBox
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components.WildcardsLabel
@@ -16,7 +16,7 @@ import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.core.tunnel.getValueById
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalIsAndroidTV
@@ -22,7 +22,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.ui.common.ExpandingRowListItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
@@ -16,7 +16,7 @@ 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.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
import com.zaneschepke.wireguardautotunnel.util.extensions.toThreeDecimalPlaceString
@@ -20,7 +20,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.MainActivity
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.components.AddPeerButton
import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.components.InterfaceSection
@@ -4,7 +4,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.state.ConfigUiState
import com.zaneschepke.wireguardautotunnel.ui.state.ConfigProxy
@@ -4,7 +4,7 @@ import android.content.Context
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.screens.main.splittunnel.state.SplitOption
@@ -1,6 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main.splittunnel.state
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
data class SplitTunnelUiState(
val loading: Boolean = true,
@@ -10,7 +10,7 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.ui.common.SectionDivider
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalIsAndroidTV
@@ -7,7 +7,7 @@ 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.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.button.ForwardButton
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
@@ -9,7 +9,7 @@ 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.entity.TunnelConf
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
@@ -7,7 +7,7 @@ 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.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
@@ -7,7 +7,7 @@ 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.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
@@ -35,8 +35,8 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import com.zaneschepke.wireguardautotunnel.MainActivity
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.ConfigType
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.util.extensions.setScreenBrightness
import io.github.alexzhirkevich.qrose.options.QrBallShape
import io.github.alexzhirkevich.qrose.options.QrBrush
@@ -7,7 +7,7 @@ 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.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
@@ -7,7 +7,7 @@ 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.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.button.ForwardButton
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
@@ -1,6 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.support
import com.zaneschepke.wireguardautotunnel.domain.entity.AppUpdate
import com.zaneschepke.wireguardautotunnel.domain.model.AppUpdate
import com.zaneschepke.wireguardautotunnel.util.StringValue
data class SupportUiState(
@@ -4,7 +4,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.zaneschepke.wireguardautotunnel.BuildConfig
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.domain.entity.AppUpdate
import com.zaneschepke.wireguardautotunnel.domain.model.AppUpdate
import com.zaneschepke.wireguardautotunnel.domain.repository.UpdateRepository
import com.zaneschepke.wireguardautotunnel.util.FileUtils
import com.zaneschepke.wireguardautotunnel.util.StringValue
@@ -1,17 +1,18 @@
package com.zaneschepke.wireguardautotunnel.ui.state
import com.zaneschepke.networkmonitor.NetworkStatus
import com.zaneschepke.wireguardautotunnel.data.model.GeneralState
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
import com.zaneschepke.wireguardautotunnel.domain.entity.AppState
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.data.entity.GeneralState
import com.zaneschepke.wireguardautotunnel.data.mapper.GeneralStateMapper
import com.zaneschepke.wireguardautotunnel.domain.model.AppSettings
import com.zaneschepke.wireguardautotunnel.domain.model.AppState
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
data class AppUiState(
val appSettings: AppSettings = AppSettings(),
val tunnels: List<TunnelConf> = emptyList(),
val activeTunnels: Map<TunnelConf, TunnelState> = emptyMap(),
val appState: AppState = GeneralState().toAppState(),
val appState: AppState = GeneralStateMapper.toAppState(GeneralState()),
val isAutoTunnelActive: Boolean = false,
val appConfigurationChange: Boolean = false,
val isAppLoaded: Boolean = false,
@@ -1,6 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.state
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.util.StringValue
data class AppViewState(
@@ -1,7 +1,7 @@
package com.zaneschepke.wireguardautotunnel.ui.state
import com.wireguard.config.Peer
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.util.extensions.joinAndTrim
data class PeerProxy(
@@ -10,8 +10,8 @@ import android.provider.OpenableColumns
import androidx.annotation.RequiresApi
import androidx.core.content.FileProvider
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.util.extensions.getInputStreamFromUri
import com.zaneschepke.wireguardautotunnel.util.extensions.installApk
import com.zaneschepke.wireguardautotunnel.util.extensions.launchShareFile
@@ -1,8 +1,8 @@
package com.zaneschepke.wireguardautotunnel.util.extensions
import android.content.pm.PackageInfo
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import java.math.BigDecimal
import java.text.DecimalFormat
@@ -1,6 +1,9 @@
package com.zaneschepke.wireguardautotunnel.util.extensions
import android.content.Context
import androidx.navigation.NavController
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.navigation.isCurrentRoute
@@ -11,3 +14,25 @@ fun NavController.goFromRoot(route: Route) {
launchSingleTop = true
}
}
fun AndroidNetworkMonitor.WifiDetectionMethod.asString(context: Context): String {
return when (this) {
AndroidNetworkMonitor.WifiDetectionMethod.DEFAULT -> context.getString(R.string._default)
AndroidNetworkMonitor.WifiDetectionMethod.LEGACY -> context.getString(R.string.legacy)
AndroidNetworkMonitor.WifiDetectionMethod.ROOT -> context.getString(R.string.root)
AndroidNetworkMonitor.WifiDetectionMethod.SHIZUKU -> context.getString(R.string.shizuku)
}
}
fun AndroidNetworkMonitor.WifiDetectionMethod.asDescriptionString(context: Context): String? {
return when (this) {
AndroidNetworkMonitor.WifiDetectionMethod.LEGACY ->
context.getString(R.string.legacy_api_description)
AndroidNetworkMonitor.WifiDetectionMethod.ROOT ->
context.getString(R.string.use_root_shell_for_wifi)
AndroidNetworkMonitor.WifiDetectionMethod.SHIZUKU ->
context.getString(R.string.use_shell_via_shizuku)
AndroidNetworkMonitor.WifiDetectionMethod.DEFAULT ->
context.getString(R.string.use_android_recommended)
}
}
@@ -8,6 +8,7 @@ import com.wireguard.android.backend.WgQuickBackend
import com.wireguard.android.util.RootShell
import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.logcatter.model.LogMessage
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.networkmonitor.NetworkStatus
import com.zaneschepke.wireguardautotunnel.R
@@ -18,11 +19,11 @@ import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.AppShell
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.di.MainDispatcher
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
import com.zaneschepke.wireguardautotunnel.domain.entity.AppState
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.enums.ConfigType
import com.zaneschepke.wireguardautotunnel.domain.model.AppSettings
import com.zaneschepke.wireguardautotunnel.domain.model.AppState
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
@@ -177,7 +178,6 @@ constructor(
handleDeleteTrustedSSID(event.ssid, state.appSettings)
AppEvent.ToggleAutoTunnelWildcards ->
handleToggleAutoTunnelWildcards(state.appSettings)
AppEvent.ToggleRootShellWifi -> handleToggleRootShellWifi(state.appSettings)
is AppEvent.SaveTrustedSSID ->
handleSaveTrustedSSID(event.ssid, state.appSettings)
AppEvent.ToggleAutoTunnelOnEthernet ->
@@ -210,10 +210,31 @@ constructor(
AppEvent.ClearSelectedTunnels -> clearSelectedTunnels()
is AppEvent.SetShowModal ->
_appViewState.update { it.copy(showModal = event.modalType) }
is AppEvent.SetDetectionMethod ->
handleSetDetectionMethod(event.detectionMethod, state.appSettings)
}
}
}
private suspend fun handleSetDetectionMethod(
detectionMethod: AndroidNetworkMonitor.WifiDetectionMethod,
appSettings: AppSettings,
) {
if (detectionMethod == appSettings.wifiDetectionMethod) return
when (detectionMethod) {
AndroidNetworkMonitor.WifiDetectionMethod.ROOT -> {
val allowed = requestRoot()
if (!allowed) return
}
// TODO check if shizuku available
AndroidNetworkMonitor.WifiDetectionMethod.SHIZUKU -> Unit
else -> Unit
}
saveSettings(appSettings.copy(wifiDetectionMethod = detectionMethod))
handleShowMessage(StringValue.StringResource(R.string.app_restart_required))
}
private fun handleToggleSelectAllTunnels(tunnels: List<TunnelConf>) =
_appViewState.update { it ->
val remove = tunnels.size == it.selectedTunnels.size
@@ -264,6 +285,7 @@ constructor(
}
}
// TODO
private fun handleTunnelErrors() =
viewModelScope.launch { tunnelManager.errorEvents.collect { errorEvent -> } }
@@ -630,14 +652,6 @@ constructor(
)
)
private suspend fun handleToggleRootShellWifi(appSettings: AppSettings) {
if (requestRoot()) {
saveSettings(
appSettings.copy(isWifiNameByShellEnabled = !appSettings.isWifiNameByShellEnabled)
)
}
}
private suspend fun handleToggleTunnelOnEthernet(appSettings: AppSettings) =
saveSettings(
appSettings.copy(isTunnelOnEthernetEnabled = !appSettings.isTunnelOnEthernetEnabled)
@@ -1,8 +1,9 @@
package com.zaneschepke.wireguardautotunnel.viewmodel.event
import android.net.Uri
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
import com.zaneschepke.wireguardautotunnel.domain.enums.ConfigType
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.ui.state.AppViewState
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
import com.zaneschepke.wireguardautotunnel.util.StringValue
@@ -78,6 +79,9 @@ sealed class AppEvent {
data class SetTheme(val theme: Theme) : AppEvent()
data class SetDetectionMethod(val detectionMethod: AndroidNetworkMonitor.WifiDetectionMethod) :
AppEvent()
data object ToggleAutoTunnelOnWifi : AppEvent()
data object ToggleAutoTunnelOnCellular : AppEvent()
@@ -90,8 +94,6 @@ sealed class AppEvent {
data object ToggleAutoTunnelWildcards : AppEvent()
data object ToggleRootShellWifi : AppEvent()
data class DeleteTrustedSSID(val ssid: String) : AppEvent()
data class SaveTrustedSSID(val ssid: String) : AppEvent()
+19 -2
View File
@@ -152,7 +152,7 @@
<string name="learn_more">Learn more</string>
<string name="wildcards_active">Wildcards active</string>
<string name="wifi_name_via_shell">Wifi name via shell</string>
<string name="use_root_shell_for_wifi">Use root shell to get wifi name</string>
<string name="use_root_shell_for_wifi">Use a root shell to get Wi-Fi information, preventing the need for location permissions</string>
<string name="kernel_not_supported">Kernel not supported</string>
<string name="start_auto">Start auto-tunnel</string>
<string name="stop_auto">Stop auto-tunnel</string>
@@ -222,6 +222,7 @@
<string name="remote_key_template">Key: %1$s</string>
<string name="version_template">Version: %1$s</string>
<string name="security_template">Security: %1$s</string>
<string name="current_template">Current: %1$s</string>
<string name="flavor_template">Flavor: %1$s</string>
<string name="config_error">config error</string>
<string name="dns_resolve_error">dns resolution error</string>
@@ -255,11 +256,27 @@
<string name="install_updated_permission">This app needs permission to install updates.</string>
<string name="allow">Allow</string>
<string name="licenses">Licenses</string>
<string name="update_check_unsupported">Update check not supported this build type.</string>
<string name="update_check_unsupported">Update check is not supported for this build type.</string>
<string name="darker">Darker</string>
<string name="amoled">AMOLED</string>
<string name="show_qr">Show QR</string>
<string name="amnezia">Amnezia</string>
<string name="wireguard">WireGuard</string>
<string name="done">Done</string>
<string name="use_deprecated_wifi_api">Use deprecated Wi-Fi API</string>
<string name="app_restart_required">App restart required</string>
<string name="wifi_detection_method">Wi-Fi detection method</string>
<string name="_default">Default</string>
<string name="legacy">Legacy</string>
<string name="root">Root</string>
<string name="shizuku">Shizuku</string>
<string name="legacy_api_description">Use an Android 12 deprecated method for getting Wi-Fi information that limits
location queries, but may not work reliably on newer devices
</string>
<string name="use_shell_via_shizuku">Use shell via Shizuku to get Wi-Fi information, preventing the need for
location permission on non-rooted devices
</string>
<string name="use_android_recommended">Use Android\'s recommended method for getting Wi-Fi information, based on
Android version
</string>
</resources>
+1 -1
View File
@@ -6,7 +6,7 @@
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8
org.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=2g -XX:+HeapDumpOnOutOfMemoryError
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+31 -28
View File
@@ -3,7 +3,7 @@ accompanist = "0.37.3"
activityCompose = "1.10.1"
amneziawgAndroid = "1.4.0"
androidx-junit = "1.2.1"
appcompat = "1.7.0"
appcompat = "1.7.1"
biometricKtx = "1.2.0-alpha05"
coreKtx = "1.16.0"
datastorePreferences = "1.2.0-alpha02"
@@ -13,8 +13,8 @@ hiltAndroid = "2.56.2"
hiltCompiler = "1.2.0"
junit = "4.13.2"
kotlinx-serialization-json = "1.8.1"
ktorClientCore = "3.1.3"
lifecycle-runtime-compose = "2.9.0"
ktorClientCore = "3.2.0"
lifecycle-runtime-compose = "2.9.1"
material3 = "1.3.2"
navigationCompose = "2.9.0"
pinLockCompose = "1.0.4"
@@ -24,11 +24,11 @@ semver4j = "3.1.0"
slf4jAndroid = "1.7.36"
timber = "5.0.1"
tunnel = "1.3.0"
androidGradlePlugin = "8.9.3"
androidGradlePlugin = "8.10.1"
kotlin = "2.1.21"
ksp = "2.1.21-2.0.1"
composeBom = "2025.05.00"
compose = "1.8.1"
ksp = "2.1.21-2.0.2"
composeBom = "2025.06.00"
compose = "1.8.2"
icons = "1.7.8"
workRuntimeKtxVersion = "2.10.1"
zxingAndroidEmbedded = "4.3.0"
@@ -48,19 +48,15 @@ licensee = "1.13.0"
accompanist-drawablepainter = { module = "com.google.accompanist:accompanist-drawablepainter", version.ref = "accompanist" }
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" }
#room
amneziawg-android = { module = "com.zaneschepke:amneziawg-android", version.ref = "amneziawgAndroid" }
androidx-biometric-ktx = { module = "androidx.biometric:biometric-ktx", version.ref = "biometricKtx" }
# db
androidx-core = { module = "androidx.core:core", version.ref = "coreKtx" }
androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" }
androidx-hilt-work = { module = "androidx.hilt:hilt-work", version.ref = "hiltCompiler" }
androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle-runtime-compose" }
androidx-lifecycle-service = { module = "androidx.lifecycle:lifecycle-service", version.ref = "lifecycle-runtime-compose" }
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomVersion" }
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomVersion" }
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomVersion" }
androidx-storage = { group = "androidx.test.services", name = "storage", version.ref = "storage" }
#compose
# ui
androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" }
androidx-compose-ui-test = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "compose" }
androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" }
@@ -68,15 +64,7 @@ androidx-compose-manifest = { module = "androidx.compose.ui:ui-test-manifest", v
androidx-compose-ui-graphics = { module = "androidx.compose.ui:ui-graphics", version.ref = "compose" }
androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" }
androidx-compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose" }
#hilt
androidx-room-testing = { module = "androidx.room:room-testing", version.ref = "roomVersion" }
androidx-work-runtime = { module = "androidx.work:work-runtime-ktx", version.ref = "workRuntimeKtxVersion" }
desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" }
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroid" }
hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hiltAndroid" }
androidx-hilt-compiler = { module = "androidx.hilt:hilt-compiler", version.ref = "hiltCompiler" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" }
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" }
@@ -84,10 +72,22 @@ androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", vers
androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltCompiler" }
androidx-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-junit" }
androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coreSplashscreen" }
androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle-runtime-compose" }
androidx-material3 = { module = "androidx.compose.material3:material3", version.ref = "material3" }
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
# lifecyle
androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle-runtime-compose" }
androidx-lifecycle-service = { module = "androidx.lifecycle:lifecycle-service", version.ref = "lifecycle-runtime-compose" }
androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle-runtime-compose" }
# di
androidx-room-testing = { module = "androidx.room:room-testing", version.ref = "roomVersion" }
androidx-work-runtime = { module = "androidx.work:work-runtime-ktx", version.ref = "workRuntimeKtxVersion" }
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroid" }
hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hiltAndroid" }
androidx-hilt-compiler = { module = "androidx.hilt:hilt-compiler", version.ref = "hiltCompiler" }
androidx-hilt-work = { module = "androidx.hilt:hilt-work", version.ref = "hiltCompiler" }
junit = { module = "junit:junit", version.ref = "junit" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" }
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktorClientCore" }
@@ -98,16 +98,19 @@ ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx
lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle-runtime-compose" }
material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "icons" }
# util
pin-lock-compose = { module = "com.zaneschepke:pin_lock_compose", version.ref = "pinLockCompose" }
qrose = { module = "io.github.alexzhirkevich:qrose", version.ref = "qrose" }
semver4j = { module = "com.vdurmont:semver4j", version.ref = "semver4j" }
slf4j-android = { module = "org.slf4j:slf4j-android", version.ref = "slf4jAndroid" }
timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
tunnel = { module = "com.zaneschepke:wireguard-android", version.ref = "tunnel" }
zxing-android-embedded = { module = "com.journeyapps:zxing-android-embedded", version.ref = "zxingAndroidEmbedded" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
androidx-storage = { group = "androidx.test.services", name = "storage", version.ref = "storage" }
desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" }
androidx-biometric-ktx = { module = "androidx.biometric:biometric-ktx", version.ref = "biometricKtx" }
# tunnel
tunnel = { module = "com.zaneschepke:wireguard-android", version.ref = "tunnel" }
amneziawg-android = { module = "com.zaneschepke:amneziawg-android", version.ref = "amneziawgAndroid" }
[plugins]
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
@@ -12,7 +12,6 @@ import android.net.NetworkRequest
import android.net.wifi.WifiManager
import android.os.Build
import com.wireguard.android.util.RootShell
import java.util.Collections
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose
@@ -21,19 +20,31 @@ import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import timber.log.Timber
class AndroidNetworkMonitor(
context: Context,
private val useRootShellCallback: suspend () -> Boolean,
) : NetworkMonitor {
class AndroidNetworkMonitor(context: Context, val wifiDetectionMethod: WifiDetectionMethod) :
NetworkMonitor {
companion object {
const val LOCATION_GRANTED = "LOCATION_PERMISSIONS_GRANTED"
const val LOCATION_SERVICES_FILTER = "android.location.PROVIDERS_CHANGED"
}
enum class WifiDetectionMethod(val value: Int) {
DEFAULT(0),
LEGACY(1),
ROOT(2),
SHIZUKU(3);
companion object {
fun fromValue(value: Int): WifiDetectionMethod =
WifiDetectionMethod.entries.find { it.value == value } ?: DEFAULT
}
}
private val appContext = context.applicationContext
private val packageName = appContext.packageName
private val connectivityManager =
@@ -43,15 +54,17 @@ class AndroidNetworkMonitor(
appContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
private val rootShell = RootShell(context)
private val wifiMutex = Mutex()
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
@get:Synchronized @set:Synchronized var currentSsid: String? = null
@get:Synchronized @set:Synchronized var securityType: WifiSecurityType? = null
private var currentSsid: String? = null
private var securityType: WifiSecurityType? = null
@get:Synchronized @set:Synchronized var wifiConnected = false
private var wifiConnected = false
// Track active Wi-Fi networks and last active network ID
private val activeNetworks = Collections.synchronizedSet(mutableSetOf<String>())
private val activeWifiNetworks = mutableSetOf<String>()
data class WifiState(
val connected: Boolean = false,
@@ -65,30 +78,28 @@ class AndroidNetworkMonitor(
@Suppress("DEPRECATION")
suspend fun getWifiSsid(): String? {
return withContext(ioDispatcher) {
if (useRootShellCallback()) {
rootShell.getCurrentWifiName()
} else {
if (wifiManager == null) return@withContext null
try {
wifiManager.connectionInfo?.ssid?.trim('"')?.takeIf { it.isNotEmpty() }
} catch (e: Exception) {
Timber.e(e)
null
}
if (wifiManager == null) return@withContext null
try {
wifiManager.connectionInfo?.ssid?.trim('"')?.takeIf { it.isNotEmpty() }
} catch (e: Exception) {
Timber.e(e)
null
}
}
}
suspend fun handleUnknownWifi() {
val newSsid = getWifiSsid()
val securityType = wifiManager?.getCurrentSecurityType()
// Only update if new SSID is valid; preserve existing valid SSID otherwise
if (newSsid != null && newSsid != WifiManager.UNKNOWN_SSID) {
currentSsid = newSsid
trySend(WifiState(wifiConnected, currentSsid, securityType))
} else if (currentSsid == null || currentSsid == WifiManager.UNKNOWN_SSID) {
currentSsid = newSsid
trySend(WifiState(wifiConnected, currentSsid, securityType))
wifiMutex.withLock {
val newSsid = getWifiSsid()
val securityType = wifiManager?.getCurrentSecurityType()
// Only update if new SSID is valid; preserve existing valid SSID otherwise
if (newSsid != null && newSsid != WifiManager.UNKNOWN_SSID) {
currentSsid = newSsid
trySend(WifiState(wifiConnected, currentSsid, securityType))
} else if (currentSsid == null || currentSsid == WifiManager.UNKNOWN_SSID) {
currentSsid = newSsid
trySend(WifiState(wifiConnected, currentSsid, securityType))
}
}
}
@@ -144,39 +155,81 @@ class AndroidNetworkMonitor(
flags,
)
val callback =
object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
Timber.d("Wi-Fi onAvailable: network=$network")
activeNetworks.add(network.toString())
launch {
currentSsid = getWifiSsid()
securityType = wifiManager?.getCurrentSecurityType()
wifiConnected = true
trySend(
WifiState(
connected = true,
ssid = currentSsid,
securityType = securityType,
)
)
}
suspend fun handleOnWifiLost(network: Network) {
wifiMutex.withLock {
Timber.d("Wi-Fi onLost: network=$network")
activeWifiNetworks.remove(network.toString())
if (activeWifiNetworks.isEmpty()) {
Timber.d(
"All Wi-Fi networks disconnected, clearing currentSsid and wifiConnected"
)
currentSsid = null
wifiConnected = false
trySend(WifiState(connected = false, ssid = null, securityType = null))
} else {
Timber.d("Wi-Fi onLost, but still connected to other networks, ignoring")
}
}
}
override fun onLost(network: Network) {
Timber.d("Wi-Fi onLost: network=$network")
activeNetworks.remove(network.toString())
if (activeNetworks.isEmpty()) {
Timber.d(
"All Wi-Fi networks disconnected, clearing currentSsid and wifiConnected"
)
currentSsid = null
wifiConnected = false
trySend(WifiState(connected = false, ssid = null, securityType = null))
} else {
Timber.d("Wi-Fi onLost, but still connected to other networks, ignoring")
suspend fun handleOnWifiAvailable(
network: Network,
networkCapabilities: NetworkCapabilities?,
) {
wifiMutex.withLock {
Timber.d("Wi-Fi onAvailable: network=$network")
activeWifiNetworks.add(network.toString())
currentSsid =
when (wifiDetectionMethod) {
WifiDetectionMethod.DEFAULT ->
networkCapabilities?.getWifiSsid() ?: getWifiSsid()
WifiDetectionMethod.LEGACY -> getWifiSsid()
WifiDetectionMethod.ROOT -> rootShell.getCurrentWifiName()
// TODO implement Shizuku
else -> networkCapabilities?.getWifiSsid() ?: getWifiSsid()
}
securityType = wifiManager?.getCurrentSecurityType()
wifiConnected = true
trySend(
WifiState(connected = true, ssid = currentSsid, securityType = securityType)
)
}
}
val callback =
when {
wifiDetectionMethod == WifiDetectionMethod.LEGACY ||
Build.VERSION.SDK_INT < Build.VERSION_CODES.S ->
object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
Timber.i("Wi-Fi change detected using old API")
launch { handleOnWifiAvailable(network, null) }
}
override fun onLost(network: Network) {
launch { handleOnWifiLost(network) }
}
}
else ->
object : ConnectivityManager.NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) {
private var netCapabilities: NetworkCapabilities? = null
override fun onAvailable(network: Network) {
Timber.i("Wi-Fi change detected using new API")
launch { handleOnWifiAvailable(network, netCapabilities) }
}
override fun onCapabilitiesChanged(
network: Network,
networkCapabilities: NetworkCapabilities,
) {
launch { wifiMutex.withLock { netCapabilities = networkCapabilities } }
}
override fun onLost(network: Network) {
launch { handleOnWifiLost(network) }
}
}
}
}
val request =
@@ -1,5 +1,7 @@
package com.zaneschepke.networkmonitor
import android.net.NetworkCapabilities
import android.net.wifi.WifiInfo
import android.net.wifi.WifiManager
import android.os.Build
import com.wireguard.android.util.RootShell
@@ -21,3 +23,14 @@ fun WifiManager.getCurrentSecurityType(): WifiSecurityType? {
null
}
}
fun NetworkCapabilities.getWifiSsid(): String? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val info: WifiInfo
if (transportInfo is WifiInfo) {
info = transportInfo as WifiInfo
return info.ssid
}
}
return null
}