Compare commits

...

18 Commits

Author SHA1 Message Date
Zane Schepke cf8d9c6e4b remove provider 2025-07-05 20:46:46 -04:00
Zane Schepke 1892b9b4a2 make flow hot for flatmap 2025-07-05 15:33:48 -04:00
Zane Schepke 0f246bf066 fix: improve shizuku permission handling 2025-07-05 13:44:51 -04:00
Zane Schepke 0b87d4f23e add init shizuku support 2025-07-05 07:18:59 -04:00
Zane Schepke c8b65fb7fa ci: fix token 2025-06-19 00:58:34 -04:00
Zane Schepke feec7f0ffc chore: bump version 2025-06-17 15:43:46 -04:00
Zane Schepke b63c6a9b73 fix: simplify update check dialog ui 2025-06-17 15:34:24 -04:00
Zane Schepke 46975607c4 fix: version check name change 2025-06-17 14:04:00 -04:00
Zane Schepke 0c7bcb5453 fix: nightly version check 2025-06-14 16:59:40 -04:00
Zane Schepke 599bf9c9e0 fix: wifi name surrounding quotes, prevent multiple auto-tunnel jobs
#768
#797
2025-06-14 15:39:22 -04: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
Zane Schepke aa33aebd2f chore: add full description fastlane 2025-05-16 05:51:13 -04:00
Zane Schepke 53b1d03ca8 ci: permission fix 2025-05-16 05:28:39 -04:00
Zane Schepke 53f72850e2 refactor: remove redundant pt 2025-05-16 05:10:33 -04:00
98 changed files with 1355 additions and 504 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
- name: Check for new commits
id: check
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
NEW_COMMITS=$(git rev-list --count --after="$(date -Iseconds -d '23 hours ago')" ${{ github.sha }})
echo "new_commits=$NEW_COMMITS" >> $GITHUB_OUTPUT
+5 -1
View File
@@ -123,7 +123,7 @@ android {
licensee {
Constants.allowedLicenses.forEach { allow(it) }
allowUrl(Constants.XZING_LICENSE_URL)
Constants.allowedLicenseUrls.forEach { allowUrl(it) }
}
applicationVariants.all {
@@ -219,6 +219,10 @@ dependencies {
implementation(libs.ktor.client.content.negotiation)
implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.slf4j.android)
// shizuku
implementation(libs.shizuku.api)
implementation(libs.shizuku.provider)
}
tasks.register<Copy>("copyLicenseeJsonToAssets") {
@@ -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
@@ -86,6 +87,8 @@ class MainActivity : AppCompatActivity() {
private var lastLocationPermissionState: Boolean? = null
val REQUEST_CODE = 123
@SuppressLint("BatteryLife")
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge(
@@ -284,6 +287,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
@@ -24,22 +24,10 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.distinctByKeys
import dagger.hilt.android.AndroidEntryPoint
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Job
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import timber.log.Timber
@AndroidEntryPoint
@@ -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
@@ -29,20 +29,8 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.Tunnels
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import javax.inject.Provider
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import timber.log.Timber
@AndroidEntryPoint
@@ -72,6 +60,8 @@ class AutoTunnelService : LifecycleService() {
private val binder = LocalBinder(this)
private var isServiceRunning = false
override fun onCreate() {
super.onCreate()
launchWatcherNotification()
@@ -90,6 +80,8 @@ class AutoTunnelService : LifecycleService() {
}
fun start() {
if (isServiceRunning) return
isServiceRunning = true
kotlin
.runCatching {
launchWatcherNotification()
@@ -102,6 +94,7 @@ class AutoTunnelService : LifecycleService() {
}
fun stop() {
isServiceRunning = false
wakeLock?.let { if (it.isHeld) it.release() }
stopSelf()
}
@@ -261,18 +254,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, newVersion: String): AppUpdate {
with(gitHubRelease) {
val apkAsset = assets.firstOrNull { it.name.endsWith(".apk") }
return AppUpdate(
version = newVersion,
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,10 +2,12 @@ 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.Constants
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
import io.ktor.client.HttpClient
import io.ktor.client.request.get
@@ -30,24 +32,30 @@ class GitHubUpdateRepository(
override suspend fun checkForUpdate(currentVersion: String): Result<AppUpdate?> =
withContext(ioDispatcher) {
Timber.i("Checking for update")
val isNightly = BuildConfig.VERSION_NAME.contains("nightly")
val release =
if (BuildConfig.VERSION_NAME.contains("nightly")) {
gitHubApi.getNightlyRelease(githubOwner, githubRepo)
if (isNightly) {
gitHubApi.getNightlyRelease(githubOwner, githubRepo).onFailure(Timber::e)
} else {
gitHubApi.getLatestRelease(githubOwner, githubRepo)
gitHubApi.getLatestRelease(githubOwner, githubRepo).onFailure(Timber::e)
}
release.map { release ->
val apkAsset =
release.assets.find { asset ->
asset.name.startsWith("wgtunnel-full-v") && asset.name.endsWith(".apk")
asset.name.startsWith("wgtunnel-${Constants.STANDALONE_FLAVOR}-v") &&
asset.name.endsWith(".apk")
}
val newVersion =
apkAsset?.name?.removePrefix("wgtunnel-full-v")?.removeSuffix(".apk")
?: return@map null
apkAsset
?.name
?.removePrefix("wgtunnel-${Constants.STANDALONE_FLAVOR}-v")
?.removeSuffix(".apk") ?: return@map null
Timber.i("Latest version: $newVersion, current version: $currentVersion")
if (isNightly && newVersion != currentVersion)
return@map GitHubReleaseMapper.toAppUpdate(release, newVersion)
if (NumberUtils.compareVersions(newVersion, currentVersion) > 0) {
release.toAppUpdate()
GitHubReleaseMapper.toAppUpdate(release, newVersion)
} 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)
}
}
}
@@ -21,7 +21,9 @@ import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.map
import org.amnezia.awg.backend.Backend
import org.amnezia.awg.backend.GoBackend
import org.amnezia.awg.backend.RootTunnelActionHandler
@@ -112,10 +114,23 @@ class TunnelModule {
fun provideNetworkMonitor(
@ApplicationContext context: Context,
settingsRepository: AppSettingRepository,
@ApplicationScope applicationScope: CoroutineScope,
@AppShell appShell: RootShell,
): NetworkMonitor {
return AndroidNetworkMonitor(context) {
runBlocking { settingsRepository.get().isWifiNameByShellEnabled }
}
return AndroidNetworkMonitor(
context,
object : AndroidNetworkMonitor.ConfigurationListener {
override val detectionMethod: Flow<AndroidNetworkMonitor.WifiDetectionMethod>
get() =
settingsRepository.flow
.distinctUntilChangedBy { it.wifiDetectionMethod }
.map { it.wifiDetectionMethod }
override val rootShell: RootShell
get() = appShell
},
applicationScope,
)
}
@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,10 @@ fun WifiTunnelingItems(
currentText = currentText,
onSave = { ssid ->
if (
uiState.appSettings.isWifiNameByShellEnabled ||
uiState.appSettings.wifiDetectionMethod ==
AndroidNetworkMonitor.WifiDetectionMethod.ROOT ||
uiState.appSettings.wifiDetectionMethod ==
AndroidNetworkMonitor.WifiDetectionMethod.SHIZUKU ||
isWifiNameReadable()
) {
viewModel.handleEvent(AppEvent.SaveTrustedSSID(ssid))
@@ -0,0 +1,39 @@
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)
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
@@ -12,6 +12,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextLinkStyles
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withLink
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -72,8 +78,41 @@ fun SupportScreen(viewModel: SupportViewModel = hiltViewModel(), appViewModel: A
verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.Top),
modifier = Modifier.fillMaxWidth(),
) {
Text(uiState.appUpdate?.version ?: "")
Text(uiState.appUpdate?.releaseNotes ?: "")
val annotatedString = buildAnnotatedString {
append("${uiState.appUpdate?.version ?: ""}\n")
// Add clickable text for second line
withLink(
link =
LinkAnnotation.Clickable(
tag = stringResource(id = R.string.release_notes),
linkInteractionListener = {
val version =
if (BuildConfig.VERSION_NAME.contains("nightly")) {
"nightly"
} else {
uiState.appUpdate
?.version
?.removePrefix("v")
?.trim() ?: ""
}
val url = "${Constants.BASE_RELEASE_URL}$version".trim()
context.openWebUrl(url)
},
styles =
TextLinkStyles(
style =
SpanStyle(
color = MaterialTheme.colorScheme.primary,
textDecoration = TextDecoration.Underline,
)
),
)
) {
append(stringResource(R.string.release_notes))
}
}
Text(text = annotatedString)
if (uiState.isLoading) {
LinearProgressIndicator(
progress = { uiState.downloadProgress },
@@ -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(
@@ -38,4 +38,6 @@ object Constants {
const val GOOGLE_PLAY_FLAVOR = "google"
const val STANDALONE_FLAVOR = "standalone"
const val RELEASE = "release"
const val BASE_RELEASE_URL = "https://github.com/wgtunnel/wgtunnel/releases/tag/"
}
@@ -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)
}
}
@@ -1,5 +1,6 @@
package com.zaneschepke.wireguardautotunnel.viewmodel
import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.net.Uri
import android.os.Build
import androidx.lifecycle.ViewModel
@@ -8,6 +9,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 +20,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
@@ -45,6 +47,7 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.amnezia.awg.config.BadConfigException
import org.amnezia.awg.config.Config
import rikka.shizuku.Shizuku
import timber.log.Timber
import xyz.teamgravity.pin_lock_compose.PinManager
@@ -177,7 +180,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 +212,46 @@ 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 -> if (!requestRoot()) return
AndroidNetworkMonitor.WifiDetectionMethod.SHIZUKU -> {
Shizuku.addRequestPermissionResultListener(
Shizuku.OnRequestPermissionResultListener { requestCode: Int, grantResult: Int
->
if (grantResult != PERMISSION_GRANTED)
return@OnRequestPermissionResultListener
viewModelScope.launch {
saveSettings(appSettings.copy(wifiDetectionMethod = detectionMethod))
}
}
)
try {
if (Shizuku.checkSelfPermission() != PERMISSION_GRANTED)
return Shizuku.requestPermission(123)
} catch (e: Exception) {
Timber.e(e)
return handleShowMessage(
StringValue.StringResource(R.string.shizuku_not_detected)
)
}
}
else -> Unit
}
saveSettings(appSettings.copy(wifiDetectionMethod = detectionMethod))
}
private fun handleToggleSelectAllTunnels(tunnels: List<TunnelConf>) =
_appViewState.update { it ->
val remove = tunnels.size == it.selectedTunnels.size
@@ -264,6 +302,7 @@ constructor(
}
}
// TODO
private fun handleTunnelErrors() =
viewModelScope.launch { tunnelManager.errorEvents.collect { errorEvent -> } }
@@ -630,14 +669,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()
+21 -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,29 @@
<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>
<string name="release_notes">Release notes</string>
<string name="shizuku_not_detected">Shizuku not detected</string>
</resources>
+3 -3
View File
@@ -1,7 +1,7 @@
object Constants {
const val VERSION_NAME = "3.9.2"
const val VERSION_NAME = "3.9.3"
const val JVM_TARGET = "17"
const val VERSION_CODE = 39200
const val VERSION_CODE = 39300
const val TARGET_SDK = 35
const val MIN_SDK = 26
const val APP_ID = "com.zaneschepke.wireguardautotunnel"
@@ -13,5 +13,5 @@ object Constants {
const val PRERELEASE = "prerelease"
val allowedLicenses = listOf("MIT", "Apache-2.0", "BSD-3-Clause")
const val XZING_LICENSE_URL: String = "https://github.com/journeyapps/zxing-android-embedded/blob/master/COPYING"
val allowedLicenseUrls = listOf("https://github.com/journeyapps/zxing-android-embedded/blob/master/COPYING", "https://github.com/RikkaApps/Shizuku-API/blob/master/LICENSE")
}
@@ -0,0 +1,4 @@
What's new:
- Fixes deprecate location API bug by adding Wi-Fi info method selection
- Simplified update check dialog UI
- Improve auto-tunnel reliability with delayed recheck
@@ -0,0 +1,14 @@
Features
- Add tunnels via .conf file, zip, manual entry, or QR code
- Auto connect to VPN based on Wi-Fi SSID, ethernet, or mobile data
- Split tunneling by application with search
- WireGuard support for kernel and userspace modes
- Amnezia support for userspace mode for DPI/censorship protection
- Always-On VPN support
- Export Amnezia and WireGuard tunnels to zip
- Quick tile support for VPN toggling
- Static shortcuts support for primary tunnel for automation integration
- Intent automation support for all tunnels
- Automatic service restart after reboot
- Battery preservation measures
@@ -1 +0,0 @@
Um cliente de VPN alternativo para WireGuard com recursos adicionais
-1
View File
@@ -1 +0,0 @@
WG Tunnel
+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
+33 -27
View File
@@ -3,7 +3,8 @@ accompanist = "0.37.3"
activityCompose = "1.10.1"
amneziawgAndroid = "1.4.0"
androidx-junit = "1.2.1"
appcompat = "1.7.0"
shizuku = "13.1.5"
appcompat = "1.7.1"
biometricKtx = "1.2.0-alpha05"
coreKtx = "1.16.0"
datastorePreferences = "1.2.0-alpha02"
@@ -14,7 +15,7 @@ hiltCompiler = "1.2.0"
junit = "4.13.2"
kotlinx-serialization-json = "1.8.1"
ktorClientCore = "3.1.3"
lifecycle-runtime-compose = "2.9.0"
lifecycle-runtime-compose = "2.9.1"
material3 = "1.3.2"
navigationCompose = "2.9.0"
pinLockCompose = "1.0.4"
@@ -24,11 +25,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 +49,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 +65,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 +73,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 +99,21 @@ 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" }
shizuku-api = { module = "dev.rikka.shizuku:api", version.ref = "shizuku" }
shizuku-provider = { module = "dev.rikka.shizuku:provider", version.ref = "shizuku" }
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" }
+4
View File
@@ -43,5 +43,9 @@ dependencies {
implementation(libs.tunnel)
// shizuku
implementation(libs.shizuku.api)
implementation(libs.shizuku.provider)
implementation(libs.timber)
}
@@ -7,5 +7,15 @@
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"
tools:targetApi="29" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<application>
<provider
android:name="rikka.shizuku.ShizukuProvider"
android:authorities="${applicationId}.shizuku"
android:enabled="true"
android:exported="true"
android:multiprocess="false"
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
</application>
</manifest>
@@ -12,46 +12,61 @@ 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 com.zaneschepke.networkmonitor.shizuku.ShizukuShell
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import timber.log.Timber
class AndroidNetworkMonitor(
context: Context,
private val useRootShellCallback: suspend () -> Boolean,
private val appContext: Context,
private val configurationListener: ConfigurationListener,
private val applicationScope: CoroutineScope,
) : NetworkMonitor {
interface ConfigurationListener {
val detectionMethod: Flow<WifiDetectionMethod>
val rootShell: RootShell
}
companion object {
const val LOCATION_GRANTED = "LOCATION_PERMISSIONS_GRANTED"
const val LOCATION_SERVICES_FILTER = "android.location.PROVIDERS_CHANGED"
const val ANDROID_UNKNOWN_SSID = "<unknown ssid>"
}
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 =
appContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
private val wifiManager = appContext.getSystemService(Context.WIFI_SERVICE) as WifiManager?
private val locationManager =
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,
@@ -61,34 +76,42 @@ class AndroidNetworkMonitor(
data class TransportState(val connected: Boolean = false)
private val wifiFlow: Flow<WifiState> = callbackFlow {
@OptIn(ExperimentalCoroutinesApi::class)
private val wifiFlow: Flow<WifiState> =
configurationListener.detectionMethod.flatMapLatest { detectionMethod
-> // cancels previous flow
createWifiNetworkCallbackFlow(detectionMethod) // Create a new flow for each new method
}
private fun createWifiNetworkCallbackFlow(
detectionMethod: WifiDetectionMethod
): Flow<WifiState> = callbackFlow {
@Suppress("DEPRECATION")
suspend fun getWifiSsid(): String? {
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 ANDROID_UNKNOWN_SSID
try {
wifiManager.connectionInfo?.ssid?.trim('"')?.takeIf { it.isNotEmpty() }
?: ANDROID_UNKNOWN_SSID
} catch (e: Exception) {
Timber.e(e)
ANDROID_UNKNOWN_SSID
}
}
}
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 != 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))
}
}
}
@@ -102,7 +125,7 @@ class AndroidNetworkMonitor(
Timber.d(
"Received update: Precise and all-the-time location permissions are enabled"
)
launch { handleUnknownWifi() }
applicationScope.launch { handleUnknownWifi() }
}
}
}
@@ -119,7 +142,8 @@ class AndroidNetworkMonitor(
Timber.d(
"Location Services state changed. Enabled: $isLocationServicesEnabled, GPS: $isGpsEnabled, Network: $isNetworkEnabled"
)
if (isLocationServicesEnabled) launch { handleUnknownWifi() }
if (isLocationServicesEnabled)
applicationScope.launch { handleUnknownWifi() }
}
}
}
@@ -144,39 +168,92 @@ 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 =
try {
when (detectionMethod) {
WifiDetectionMethod.DEFAULT ->
networkCapabilities?.getWifiSsid() ?: getWifiSsid()
WifiDetectionMethod.LEGACY -> getWifiSsid()
WifiDetectionMethod.ROOT ->
configurationListener.rootShell.getCurrentWifiName()
WifiDetectionMethod.SHIZUKU ->
ShizukuShell(applicationScope)
.singleResponseCommand(WIFI_SSID_SHELL_COMMAND)
}
.trim()
.replace(Regex("[\n\r]"), "")
} catch (e: Exception) {
Timber.e(e)
ANDROID_UNKNOWN_SSID
}
.also { Timber.d("Current SSID via ${detectionMethod.name}: $it") }
securityType = wifiManager?.getCurrentSecurityType()
wifiConnected = true
trySend(
WifiState(connected = true, ssid = currentSsid, securityType = securityType)
)
}
}
val callback =
when {
detectionMethod == WifiDetectionMethod.LEGACY ||
Build.VERSION.SDK_INT < Build.VERSION_CODES.S ->
object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
applicationScope.launch { handleOnWifiAvailable(network, null) }
}
override fun onLost(network: Network) {
applicationScope.launch { handleOnWifiLost(network) }
}
}
else ->
object : ConnectivityManager.NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) {
override fun onAvailable(network: Network) {
if (detectionMethod != WifiDetectionMethod.DEFAULT)
applicationScope.launch { handleOnWifiAvailable(network, null) }
}
override fun onCapabilitiesChanged(
network: Network,
networkCapabilities: NetworkCapabilities,
) {
if (detectionMethod == WifiDetectionMethod.DEFAULT)
applicationScope.launch {
handleOnWifiAvailable(network, networkCapabilities)
}
}
override fun onLost(network: Network) {
applicationScope.launch { handleOnWifiLost(network) }
}
}
}
}
val request =
@@ -189,7 +266,14 @@ class AndroidNetworkMonitor(
trySend(WifiState())
awaitClose {
connectivityManager.unregisterNetworkCallback(callback)
try {
connectivityManager.unregisterNetworkCallback(callback)
} catch (e: IllegalArgumentException) {
Timber.e(
e,
"Flow failed to unregister NetworkCallback, was already unregistered or not registered correctly.",
)
}
appContext.unregisterReceiver(locationPermissionReceiver)
appContext.unregisterReceiver(locationServicesReceiver)
}
@@ -264,6 +348,7 @@ class AndroidNetworkMonitor(
.also { Timber.d("NetworkStatus: $it") }
}
.distinctUntilChanged()
.shareIn(applicationScope, SharingStarted.WhileSubscribed(5000), replay = 1)
override fun sendLocationPermissionsGrantedBroadcast() {
val action = "$packageName.$LOCATION_GRANTED"
@@ -1,16 +1,19 @@
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
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor.Companion.ANDROID_UNKNOWN_SSID
fun RootShell.getCurrentWifiName(): String? {
const val WIFI_SSID_SHELL_COMMAND =
"dumpsys wifi | grep 'Supplicant state: COMPLETED' | grep -o 'SSID: \"[^\"]*\"' | cut -d '\"' -f2"
fun RootShell.getCurrentWifiName(): String {
val response = mutableListOf<String>()
this.run(
response,
"dumpsys wifi | grep 'Supplicant state: COMPLETED' | grep -o 'SSID: [^,]*' | cut -d ' ' -f2- | tr -d '\"'",
)
return response.firstOrNull()
run(response, WIFI_SSID_SHELL_COMMAND)
return response.firstOrNull() ?: ANDROID_UNKNOWN_SSID
}
@Suppress("DEPRECATION")
@@ -21,3 +24,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.removeSurrounding("\"").trim()
}
}
return ANDROID_UNKNOWN_SSID
}
@@ -0,0 +1,123 @@
package com.zaneschepke.networkmonitor.shizuku
import android.os.ParcelFileDescriptor
import java.io.BufferedReader
import java.io.FileInputStream
import java.io.InputStreamReader
import kotlin.coroutines.resumeWithException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import moe.shizuku.server.IRemoteProcess
import moe.shizuku.server.IShizukuService
import rikka.shizuku.Shizuku
import timber.log.Timber
class ShizukuShell(private val applicationScope: CoroutineScope) {
interface CommandResultListener {
/*
* Runs after the command executes, at least partially. Does not run with 'done' if the command throws an error.
* output: The output of the command
* done: If the command has finished executing
*/
fun onCommandResult(output: String, done: Boolean) {}
/*
* Runs if the command throws an error.
* error: The error message
*/
fun onCommandError(error: String) {}
}
fun command(command: String, listener: CommandResultListener, lineBundle: Int = 50) {
applicationScope.launch {
var process: IRemoteProcess? = null
var inputStreamPfd: ParcelFileDescriptor? = null
var errorStreamPfd: ParcelFileDescriptor? = null
try {
process =
IShizukuService.Stub.asInterface(Shizuku.getBinder())
.newProcess(arrayOf("sh", "-c", command), null, null)
inputStreamPfd = process.inputStream
errorStreamPfd = process.errorStream
FileInputStream(inputStreamPfd.fileDescriptor).use { inputStream ->
FileInputStream(errorStreamPfd.fileDescriptor).use { errorStream ->
BufferedReader(InputStreamReader(inputStream)).use { reader ->
BufferedReader(InputStreamReader(errorStream)).use { err ->
val output = StringBuilder()
val errorData = StringBuilder()
var line: String?
var lineCount = 0
// Read output first
while (reader.readLine().also { line = it } != null) {
lineCount++
output.append(line).append("\n")
if (lineCount == lineBundle) {
lineCount = 0
listener.onCommandResult(
output.toString().trim().replace(Regex("[\n\r]"), ""),
false,
)
output.clear()
}
}
// Send any remaining buffered output
if (output.isNotBlank()) {
listener.onCommandResult(
output.toString().trim().replace(Regex("[\n\r]"), ""),
false,
)
}
// Read error stream
while (err.readLine().also { line = it } != null) {
errorData.append(line).append("\n")
}
if (errorData.isNotBlank()) {
listener.onCommandError(
errorData.toString().trim().replace(Regex("[\n\r]"), "")
)
} else {
listener.onCommandResult(
output.toString().trim().replace(Regex("[\n\r]"), ""),
true,
)
}
// Wait for the process to complete
process.waitFor()
}
}
}
}
} catch (e: Exception) {
Timber.e(e, "ShizukuShell command failed for command: $command")
listener.onCommandError(e.message ?: "Unknown Shizuku command error")
} finally {
inputStreamPfd?.close()
errorStreamPfd?.close()
process?.destroy()
}
}
}
suspend fun singleResponseCommand(command: String) =
suspendCancellableCoroutine { continuation ->
command(
command,
object : CommandResultListener {
override fun onCommandResult(output: String, done: Boolean) {
if (done) continuation.resumeWith(Result.success(output))
}
override fun onCommandError(error: String) {
continuation.resumeWithException(Exception(error))
}
},
)
}
}