Compare commits

..

23 Commits

Author SHA1 Message Date
Zane Schepke 01e15099ca chore: bump version with notes 2025-03-08 22:27:17 -05:00
Zane Schepke 8f2fd93e77 fix: improve local logging 2025-03-08 22:11:23 -05:00
𝗛𝗼𝗹𝗶 94197c9943 feat: Update Turkish Translation (#605) 2025-03-08 21:32:41 -05:00
Zane Schepke 39fc9014d8 fix: bump deps for dns fix 2025-03-08 21:31:31 -05:00
Zane Schepke 8860b45230 fix: service manager bug 2025-03-02 17:51:48 -05:00
Zane Schepke e220b26d88 fix: temporarily disallow start/stop of pinger while tunnel running 2025-03-02 17:25:29 -05:00
Zane Schepke 2302b473b4 refactor: tunnel toggling 2025-03-02 16:47:55 -05:00
Zane Schepke b01473073f fix: add restart support htc quickboot 2025-03-02 12:09:57 -05:00
Zane Schepke 3ae9a24ca4 refactor: restore on restart/update 2025-03-02 12:01:28 -05:00
Zane Schepke dc3f7fa736 refactor: improve service manager 2025-03-02 11:38:36 -05:00
Zane Schepke 93d6f8aa45 fix: module flavors 2025-03-01 21:07:47 -05:00
Zane Schepke 3ea4aea5cf fix: improve network status monitoring 2025-03-01 20:17:19 -05:00
Zane Schepke 68b41c8925 fix: startup/nav bugs 2025-03-01 12:15:37 -05:00
Zane Schepke 06de1f24c2 fix: tunnel race condition 2025-03-01 11:21:07 -05:00
Zane Schepke a39feeeea6 fix: proguard rules 2025-02-23 23:21:58 -05:00
Zane Schepke c1619ff012 temp disable proguard 2025-02-23 23:15:40 -05:00
Zane Schepke 2534b86005 fix: add back rules 2025-02-23 22:50:20 -05:00
Zane Schepke 15c550737c bump deps, remove redudant proguard rules 2025-02-23 22:39:15 -05:00
Zane Schepke e1e7e27bb5 fix: proguard (#590) 2025-02-23 16:41:51 -05:00
Zane Schepke 8021c133a5 fix: proguard 2025-02-23 15:58:08 -05:00
Zane Schepke 6009445a15 bump tunnel deps 2025-02-23 15:16:12 -05:00
Zane Schepke f80af9dd5e fix: tunnel state bug 2025-02-22 23:21:25 -05:00
Zane Schepke 3f912ed532 fix: tunnel change restart logic 2025-02-22 13:07:15 -05:00
58 changed files with 1324 additions and 1019 deletions
+1 -1
View File
@@ -144,8 +144,8 @@ android {
} }
dependencies { dependencies {
implementation(project(":logcatter")) implementation(project(":logcatter"))
implementation(project(":networkmonitor"))
implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.runtime.ktx)
+37
View File
@@ -3,3 +3,40 @@
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite { -keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
<fields>; <fields>;
} }
# Keep all classes in the org.xbill.DNS package and subpackages
-keep class org.xbill.DNS.** { *; }
-dontwarn org.xbill.DNS.**
# Preserve JNA classes if used (e.g., for IPHlpAPI on Windows)
-keep class com.sun.jna.** { *; }
-dontwarn com.sun.jna.**
# Keep DNS resolver configuration classes that might be loaded dynamically
-keep class org.xbill.DNS.config.** { *; }
-dontwarn org.xbill.DNS.config.**
-keep class org.xbill.DNS.** { *; }
# Prevent optimization issues with native or reflection-based calls
-dontoptimize
-dontshrink
# Uncomment the above if errors persist, but use sparingly as theyre broad
# Suppress warnings about missing classes if not all features are used
-dontwarn java.lang.management.**
-dontwarn sun.nio.ch.**
-dontwarn com.google.api.client.http.GenericUrl
-dontwarn com.google.api.client.http.HttpHeaders
-dontwarn com.google.api.client.http.HttpRequest
-dontwarn com.google.api.client.http.HttpRequestFactory
-dontwarn com.google.api.client.http.HttpResponse
-dontwarn com.google.api.client.http.HttpTransport
-dontwarn com.google.api.client.http.javanet.NetHttpTransport$Builder
-dontwarn com.google.api.client.http.javanet.NetHttpTransport
-dontwarn javax.lang.model.element.Modifier
-dontwarn org.joda.time.Instant
-dontwarn org.slf4j.impl.StaticLoggerBinder
-dontwarn org.slf4j.impl.StaticMDCBinder
-dontwarn org.slf4j.impl.StaticMarkerBinder
+37
View File
@@ -22,3 +22,40 @@
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite { -keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
<fields>; <fields>;
} }
# Keep all classes in the org.xbill.DNS package and subpackages
-keep class org.xbill.DNS.** { *; }
-dontwarn org.xbill.DNS.**
# Preserve JNA classes if used (e.g., for IPHlpAPI on Windows)
-keep class com.sun.jna.** { *; }
-dontwarn com.sun.jna.**
# Keep DNS resolver configuration classes that might be loaded dynamically
-keep class org.xbill.DNS.config.** { *; }
-dontwarn org.xbill.DNS.config.**
-keep class org.xbill.DNS.** { *; }
# Prevent optimization issues with native or reflection-based calls
-dontoptimize
-dontshrink
# Uncomment the above if errors persist, but use sparingly as theyre broad
# Suppress warnings about missing classes if not all features are used
-dontwarn java.lang.management.**
-dontwarn sun.nio.ch.**
-dontwarn com.google.api.client.http.GenericUrl
-dontwarn com.google.api.client.http.HttpHeaders
-dontwarn com.google.api.client.http.HttpRequest
-dontwarn com.google.api.client.http.HttpRequestFactory
-dontwarn com.google.api.client.http.HttpResponse
-dontwarn com.google.api.client.http.HttpTransport
-dontwarn com.google.api.client.http.javanet.NetHttpTransport$Builder
-dontwarn com.google.api.client.http.javanet.NetHttpTransport
-dontwarn javax.lang.model.element.Modifier
-dontwarn org.joda.time.Instant
-dontwarn org.slf4j.impl.StaticLoggerBinder
-dontwarn org.slf4j.impl.StaticMDCBinder
-dontwarn org.slf4j.impl.StaticMarkerBinder
+3 -16
View File
@@ -3,13 +3,8 @@
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<!--foreground service exempt android 14--> <!--foreground service exempt android 14-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"
@@ -168,22 +163,14 @@
</service> </service>
<receiver <receiver
android:name=".core.broadcast.BootReceiver" android:name=".core.broadcast.RestartReceiver"
android:enabled="true" android:enabled="true"
android:exported="false"> android:exported="true">
<intent-filter> <intent-filter>
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<action android:name="android.intent.action.BOOT_COMPLETED" /> <action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.ACTION_BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" /> <action android:name="android.intent.action.QUICKBOOT_POWERON" />
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" /> <action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
</intent-filter>
</receiver>
<receiver
android:name=".core.broadcast.AppUpdateReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" /> <action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter> </intent-filter>
</receiver> </receiver>
@@ -40,7 +40,6 @@ import androidx.navigation.compose.rememberNavController
import androidx.navigation.toRoute import androidx.navigation.toRoute
import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.core.worker.ServiceWorker
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.ui.Route import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
@@ -67,7 +66,6 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.support.LogsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject import javax.inject.Inject
@@ -121,22 +119,13 @@ class MainActivity : AppCompatActivity() {
viewModel.getEmitSplitTunnelApps(this@MainActivity) viewModel.getEmitSplitTunnelApps(this@MainActivity)
} }
LaunchedEffect(appUiState.autoTunnelActive) {
requestAutoTunnelTileServiceUpdate()
}
with(appUiState.appSettings) { with(appUiState.appSettings) {
LaunchedEffect(isAutoTunnelEnabled) {
this@MainActivity.requestAutoTunnelTileServiceUpdate()
}
LaunchedEffect(isShortcutsEnabled) { LaunchedEffect(isShortcutsEnabled) {
if (!isShortcutsEnabled) return@LaunchedEffect shortcutManager.removeShortcuts() if (!isShortcutsEnabled) return@LaunchedEffect shortcutManager.removeShortcuts()
shortcutManager.addShortcuts() shortcutManager.addShortcuts()
} }
} }
ServiceWorker.start(this)
CompositionLocalProvider(LocalNavController provides navController) { CompositionLocalProvider(LocalNavController provides navController) {
SnackbarControllerProvider { host -> SnackbarControllerProvider { host ->
WireguardAutoTunnelTheme(theme = appUiState.generalState.theme) { WireguardAutoTunnelTheme(theme = appUiState.generalState.theme) {
@@ -221,16 +210,17 @@ class MainActivity : AppCompatActivity() {
composable<Route.Logs> { composable<Route.Logs> {
LogsScreen() LogsScreen()
} }
composable<Route.Config> { composable<Route.Config> { backStack ->
val args = it.toRoute<Route.Config>() val args = backStack.toRoute<Route.Config>()
val config = val config =
appUiState.tunnels.firstOrNull { it.id == args.id } appUiState.tunnels.firstOrNull { it.id == args.id }
ConfigScreen(config, viewModel) ConfigScreen(config, viewModel)
} }
composable<Route.TunnelOptions> { composable<Route.TunnelOptions> { backStack ->
val args = it.toRoute<Route.TunnelOptions>() val args = backStack.toRoute<Route.TunnelOptions>()
val config = appUiState.tunnels.first { it.id == args.id } appUiState.tunnels.firstOrNull { it.id == args.id }?.let { config ->
OptionsScreen(config) OptionsScreen(config, appUiState)
}
} }
composable<Route.Lock> { composable<Route.Lock> {
PinLockScreen(viewModel) PinLockScreen(viewModel)
@@ -241,15 +231,17 @@ class MainActivity : AppCompatActivity() {
composable<Route.KillSwitch> { composable<Route.KillSwitch> {
KillSwitchScreen(appUiState, viewModel) KillSwitchScreen(appUiState, viewModel)
} }
composable<Route.SplitTunnel> { composable<Route.SplitTunnel> { backStack ->
val args = it.toRoute<Route.SplitTunnel>() val args = backStack.toRoute<Route.SplitTunnel>()
val config = appUiState.tunnels.first { it.id == args.id } appUiState.tunnels.firstOrNull { it.id == args.id }?.let {
SplitTunnelScreen(config, viewModel) SplitTunnelScreen(it, viewModel)
}
}
composable<Route.TunnelAutoTunnel> { backStack ->
val args = backStack.toRoute<Route.TunnelOptions>()
appUiState.tunnels.firstOrNull { it.id == args.id }?.let {
TunnelAutoTunnelScreen(it, appUiState.appSettings)
} }
composable<Route.TunnelAutoTunnel> {
val args = it.toRoute<Route.TunnelOptions>()
val config = appUiState.tunnels.first { it.id == args.id }
TunnelAutoTunnelScreen(config, appUiState.appSettings)
} }
} }
BackHandler { BackHandler {
@@ -11,6 +11,7 @@ import androidx.work.Configuration
import com.wireguard.android.backend.GoBackend import com.wireguard.android.backend.GoBackend
import com.zaneschepke.logcatter.LogReader import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.core.worker.ServiceWorker
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.di.MainDispatcher import com.zaneschepke.wireguardautotunnel.di.MainDispatcher
@@ -91,9 +92,11 @@ class WireGuardAutoTunnel : Application(), Configuration.Provider {
} }
} }
ServiceWorker.start(this)
applicationScope.launch { applicationScope.launch {
withContext(mainDispatcher) { withContext(mainDispatcher) {
if (appDataRepository.appState.isLocalLogsEnabled() && !isRunningOnTv()) logReader.initialize() if (appDataRepository.appState.isLocalLogsEnabled() && !isRunningOnTv()) logReader.start()
} }
if (!appDataRepository.settings.get().isKernelEnabled) { if (!appDataRepository.settings.get().isKernelEnabled) {
tunnelManager.setBackendState(BackendState.SERVICE_ACTIVE, emptyList()) tunnelManager.setBackendState(BackendState.SERVICE_ACTIVE, emptyList())
@@ -1,45 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.broadcast
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint
class AppUpdateReceiver : BroadcastReceiver() {
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
@Inject
lateinit var appDataRepository: AppDataRepository
@Inject
lateinit var serviceManager: ServiceManager
@Inject
lateinit var tunnelManager: TunnelManager
override fun onReceive(context: Context, intent: Intent) {
if (intent.action != Intent.ACTION_MY_PACKAGE_REPLACED) return
serviceManager.updateTunnelTile()
serviceManager.updateAutoTunnelTile()
applicationScope.launch {
with(appDataRepository.settings.get()) {
if (isRestoreOnBootEnabled) {
// If auto tunnel is enabled, just start it and let auto tunnel start appropriate tun
if (isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) return@launch serviceManager.startAutoTunnel(true)
tunnelManager.restorePreviousState()
}
}
}
}
}
@@ -1,44 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.broadcast
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint
class BootReceiver : BroadcastReceiver() {
@Inject
lateinit var appDataRepository: AppDataRepository
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
@Inject
lateinit var serviceManager: ServiceManager
@Inject
lateinit var tunnelManager: TunnelManager
override fun onReceive(context: Context, intent: Intent) {
if (Intent.ACTION_BOOT_COMPLETED != intent.action) return
serviceManager.updateTunnelTile()
serviceManager.updateAutoTunnelTile()
applicationScope.launch {
with(appDataRepository.settings.get()) {
if (isRestoreOnBootEnabled) {
// If auto tunnel is enabled, just start it and let auto tunnel start appropriate tun
if (isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) return@launch serviceManager.startAutoTunnel(true)
tunnelManager.restorePreviousState()
}
}
}
}
}
@@ -32,8 +32,8 @@ class KernelReceiver : BroadcastReceiver() {
val action = intent.action ?: return val action = intent.action ?: return
applicationScope.launch { applicationScope.launch {
if (action == REFRESH_TUNNELS_ACTION) { if (action == REFRESH_TUNNELS_ACTION) {
tunnelManager.runningTunnelNames().forEach { tunnelManager.runningTunnelNames().forEach { name ->
val tunnel = tunnelRepository.findByTunnelName(it) val tunnel = tunnelRepository.findByTunnelName(name)
tunnel?.let { tunnel?.let {
tunnelRepository.save(it.copy(isActive = true)) tunnelRepository.save(it.copy(isActive = true))
} }
@@ -0,0 +1,64 @@
package com.zaneschepke.wireguardautotunnel.core.broadcast
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class RestartReceiver : BroadcastReceiver() {
@Inject
lateinit var appDataRepository: AppDataRepository
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
@Inject
lateinit var serviceManager: ServiceManager
@Inject
lateinit var tunnelManager: TunnelManager
@Inject
@IoDispatcher
lateinit var ioDispatcher: CoroutineDispatcher
override fun onReceive(context: Context, intent: Intent) {
val action = intent.action ?: return
if (action != Intent.ACTION_BOOT_COMPLETED &&
action != Intent.ACTION_MY_PACKAGE_REPLACED &&
action != "com.htc.intent.action.QUICKBOOT_POWERON"
) {
return
}
Timber.d("RestartReceiver triggered with action: ${intent.action}")
applicationScope.launch(ioDispatcher) {
serviceManager.updateTunnelTile()
serviceManager.updateAutoTunnelTile()
val settings = appDataRepository.settings.get()
if (settings.isRestoreOnBootEnabled) {
if (settings.isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) {
Timber.d("Starting auto-tunnel on boot/update")
serviceManager.startAutoTunnel(true)
} else {
Timber.d("Restoring previous tunnel state")
tunnelManager.restorePreviousState()
}
} else {
Timber.d("Restore on boot disabled, skipping")
}
}
}
}
@@ -1,161 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.network
import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.net.wifi.SupplicantState
import android.net.wifi.WifiManager
import android.os.Build
import com.zaneschepke.wireguardautotunnel.domain.state.ConnectivityState
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.flowOn
import javax.inject.Inject
class InternetConnectivityMonitor
@Inject
constructor(
@ApplicationContext private val context: Context,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : NetworkMonitor {
private val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
@get:Synchronized @set:Synchronized
private var wifiCapabilities: NetworkCapabilities? = null
@get:Synchronized @set:Synchronized
private var wifiNetworkChanged: Boolean = false
override val didWifiChangeSinceLastCapabilitiesQuery: Boolean
get() = wifiNetworkChanged
override val status = callbackFlow {
var wifiState: Boolean = false
var ethernetState: Boolean = false
var cellularState: Boolean = false
fun emitState() {
trySend(ConnectivityState(wifiState, ethernetState, cellularState))
}
val currentNetwork = connectivityManager.activeNetwork
if (currentNetwork == null) {
emitState()
}
fun updateCapabilityState(up: Boolean, network: Network) {
with(connectivityManager.getNetworkCapabilities(network)) {
when {
this == null -> return
hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> wifiState = up
hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ->
cellularState = up
hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) ->
ethernetState = up
}
}
}
fun onWifiChange(network: Network, callback: () -> Unit) {
if (connectivityManager.getNetworkCapabilities(network)?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true) {
callback()
}
}
fun onAvailable(network: Network) {
onWifiChange(network) {
wifiNetworkChanged = true
}
}
fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
onWifiChange(network) {
wifiCapabilities = networkCapabilities
}
updateCapabilityState(true, network)
emitState()
}
val networkStatusCallback =
when (Build.VERSION.SDK_INT) {
in Build.VERSION_CODES.S..Int.MAX_VALUE -> {
object :
ConnectivityManager.NetworkCallback(
FLAG_INCLUDE_LOCATION_INFO,
) {
override fun onAvailable(network: Network) {
onAvailable(network)
}
override fun onLost(network: Network) {
updateCapabilityState(false, network)
emitState()
}
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
onCapabilitiesChanged(network, networkCapabilities)
}
}
}
else -> {
object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
onAvailable(network)
}
override fun onLost(network: Network) {
updateCapabilityState(false, network)
emitState()
}
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
onCapabilitiesChanged(network, networkCapabilities)
}
}
}
}
val request =
NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
.build()
connectivityManager.registerNetworkCallback(request, networkStatusCallback)
awaitClose { connectivityManager.unregisterNetworkCallback(networkStatusCallback) }
}.flowOn(ioDispatcher)
override fun getWifiCapabilities(): NetworkCapabilities? {
wifiNetworkChanged = false
return wifiCapabilities
}
companion object {
fun getNetworkName(networkCapabilities: NetworkCapabilities, context: Context): String? {
var ssid = networkCapabilities.getWifiName()
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S) {
val wifiManager =
context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
@Suppress("DEPRECATION")
val info = wifiManager.connectionInfo
if (info.supplicantState === SupplicantState.COMPLETED) {
ssid = info.ssid
}
}
return ssid?.trim('"')
}
}
}
@@ -1,16 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.network
import android.net.NetworkCapabilities
import android.net.wifi.WifiInfo
import android.os.Build
fun NetworkCapabilities.getWifiName(): String? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val info: WifiInfo
if (transportInfo is WifiInfo) {
info = transportInfo as WifiInfo
return info.ssid
}
}
return null
}
@@ -1,13 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.network
import android.net.NetworkCapabilities
import com.zaneschepke.wireguardautotunnel.domain.state.ConnectivityState
import kotlinx.coroutines.flow.Flow
interface NetworkMonitor {
val status: Flow<ConnectivityState>
// util to help limit location queries
val didWifiChangeSinceLastCapabilitiesQuery: Boolean
fun getWifiCapabilities(): NetworkCapabilities?
}
@@ -3,29 +3,35 @@ package com.zaneschepke.wireguardautotunnel.core.service
import android.app.Service import android.app.Service
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelService import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelService
import com.zaneschepke.wireguardautotunnel.core.service.tile.AutoTunnelControlTile import com.zaneschepke.wireguardautotunnel.core.service.tile.AutoTunnelControlTile
import com.zaneschepke.wireguardautotunnel.core.service.tile.TunnelControlTile import com.zaneschepke.wireguardautotunnel.core.service.tile.TunnelControlTile
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
import jakarta.inject.Inject import jakarta.inject.Inject
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import timber.log.Timber import timber.log.Timber
@OptIn(ExperimentalCoroutinesApi::class) class ServiceManager @Inject constructor(
class ServiceManager private val context: Context,
@Inject constructor(private val context: Context, private val ioDispatcher: CoroutineDispatcher, private val appDataRepository: AppDataRepository) { @IoDispatcher private val ioDispatcher: CoroutineDispatcher,
@ApplicationScope private val applicationScope: CoroutineScope,
private val appDataRepository: AppDataRepository,
) {
private val _autoTunnelActive = MutableStateFlow(false) private val _autoTunnelActive = MutableStateFlow(false)
val autoTunnelActive = _autoTunnelActive.asStateFlow() val autoTunnelActive = _autoTunnelActive.asStateFlow()
var autoTunnelService = CompletableDeferred<AutoTunnelService>() var autoTunnelService = CompletableDeferred<AutoTunnelService>()
@@ -44,76 +50,111 @@ class ServiceManager
}.onFailure { Timber.e(it) } }.onFailure { Timber.e(it) }
} }
suspend fun startAutoTunnel(background: Boolean) { fun startAutoTunnel(background: Boolean) {
applicationScope.launch(ioDispatcher) {
val settings = appDataRepository.settings.get() val settings = appDataRepository.settings.get()
appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = true)) appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = true))
if (autoTunnelService.isCompleted) return _autoTunnelActive.update { true } if (autoTunnelService.isCompleted) {
_autoTunnelActive.update { true }
return@launch
}
runCatching { runCatching {
autoTunnelService = CompletableDeferred()
startService(AutoTunnelService::class.java, background) startService(AutoTunnelService::class.java, background)
autoTunnelService.await() val service = withTimeoutOrNull(SERVICE_START_TIMEOUT) { autoTunnelService.await() }
autoTunnelService.getCompleted().start() ?: throw IllegalStateException("AutoTunnelService start timed out")
service.start()
_autoTunnelActive.update { true } _autoTunnelActive.update { true }
updateAutoTunnelTile() updateAutoTunnelTile()
}.onFailure { }.onFailure {
Timber.e(it) Timber.e(it)
_autoTunnelActive.update { false }
}
} }
} }
suspend fun startBackgroundService(tunnelConf: TunnelConf) { fun startBackgroundService(tunnelConf: TunnelConf) {
if (backgroundService.isCompleted) return applicationScope.launch(ioDispatcher) {
if (backgroundService.isCompleted) return@launch
runCatching { runCatching {
backgroundService = CompletableDeferred()
startService(TunnelForegroundService::class.java, true) startService(TunnelForegroundService::class.java, true)
backgroundService.await() val service = withTimeoutOrNull(SERVICE_START_TIMEOUT) { backgroundService.await() }
backgroundService.getCompleted().start(tunnelConf) ?: throw IllegalStateException("Background service start timed out")
service.start(tunnelConf)
}.onFailure { }.onFailure {
Timber.e(it) Timber.e(it)
} }
} }
}
fun stopBackgroundService() { fun stopBackgroundService() {
if (!backgroundService.isCompleted) return applicationScope.launch(ioDispatcher) {
if (!backgroundService.isCompleted) return@launch
runCatching { runCatching {
backgroundService.getCompleted().stop() val service = backgroundService.await()
service.stop()
backgroundService = CompletableDeferred()
}.onFailure { }.onFailure {
Timber.e(it) Timber.e(it)
} }
} }
}
suspend fun toggleAutoTunnel(background: Boolean) { fun toggleAutoTunnel(background: Boolean) {
applicationScope.launch(ioDispatcher) {
if (_autoTunnelActive.value) stopAutoTunnel() else startAutoTunnel(background)
}
}
suspend fun updateAutoTunnelTile() {
withContext(ioDispatcher) { withContext(ioDispatcher) {
if (_autoTunnelActive.value) return@withContext stopAutoTunnel() runCatching {
startAutoTunnel(background) val service = withTimeoutOrNull(SERVICE_START_TIMEOUT) { autoTunnelTile.await() }
} ?: run {
}
fun updateAutoTunnelTile() {
if (autoTunnelTile.isCompleted) {
autoTunnelTile.getCompleted().updateTileState()
} else {
context.requestAutoTunnelTileServiceUpdate() context.requestAutoTunnelTileServiceUpdate()
return@withContext
}
service.updateTileState()
}.onFailure {
Timber.e(it)
}
} }
} }
fun updateTunnelTile() { suspend fun updateTunnelTile() {
if (tunnelControlTile.isCompleted) {
tunnelControlTile.getCompleted().updateTileState()
} else {
context.requestTunnelTileServiceStateUpdate()
}
}
suspend fun stopAutoTunnel() {
withContext(ioDispatcher) { withContext(ioDispatcher) {
runCatching {
val service = withTimeoutOrNull(SERVICE_START_TIMEOUT) { tunnelControlTile.await() }
?: run {
context.requestTunnelTileServiceStateUpdate()
return@withContext
}
service.updateTileState()
}.onFailure {
Timber.e(it)
}
}
}
fun stopAutoTunnel() {
applicationScope.launch(ioDispatcher) {
val settings = appDataRepository.settings.get() val settings = appDataRepository.settings.get()
appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = false)) appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = false))
if (!autoTunnelService.isCompleted) return@withContext if (!autoTunnelService.isCompleted) return@launch
runCatching { runCatching {
autoTunnelService.getCompleted().stop() val service = autoTunnelService.await()
service.stop()
_autoTunnelActive.update { false } _autoTunnelActive.update { false }
autoTunnelService = CompletableDeferred()
updateAutoTunnelTile() updateAutoTunnelTile()
}.onFailure { }.onFailure {
Timber.e(it) Timber.e(it)
} }
} }
} }
companion object {
const val SERVICE_START_TIMEOUT = 5_000L
}
} }
@@ -35,9 +35,8 @@ class TunnelForegroundService : LifecycleService() {
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
serviceManager.backgroundService.complete(this) serviceManager.backgroundService.complete(this)
return START_NOT_STICKY return super.onStartCommand(intent, flags, startId)
} }
fun start(tunnelConf: TunnelConf) { fun start(tunnelConf: TunnelConf) {
@@ -1,44 +1,42 @@
package com.zaneschepke.wireguardautotunnel.core.service.autotunnel package com.zaneschepke.wireguardautotunnel.core.service.autotunnel
import android.content.Intent import android.content.Intent
import android.net.NetworkCapabilities
import android.os.IBinder import android.os.IBinder
import android.os.PowerManager import android.os.PowerManager
import androidx.core.app.ServiceCompat import androidx.core.app.ServiceCompat
import androidx.lifecycle.LifecycleService import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.wireguard.android.util.RootShell import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.networkmonitor.NetworkStatus
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.di.AppShell
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.di.MainImmediateDispatcher
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.domain.state.AutoTunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.NetworkState
import com.zaneschepke.wireguardautotunnel.core.network.InternetConnectivityMonitor
import com.zaneschepke.wireguardautotunnel.core.network.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.domain.state.ConnectivityState
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.di.MainImmediateDispatcher
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
import com.zaneschepke.wireguardautotunnel.domain.events.KillSwitchEvent import com.zaneschepke.wireguardautotunnel.domain.events.KillSwitchEvent
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.state.AutoTunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.NetworkState
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.Tunnels import com.zaneschepke.wireguardautotunnel.util.extensions.Tunnels
import com.zaneschepke.wireguardautotunnel.util.extensions.getCurrentWifiName
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -49,10 +47,6 @@ import javax.inject.Provider
@AndroidEntryPoint @AndroidEntryPoint
class AutoTunnelService : LifecycleService() { class AutoTunnelService : LifecycleService() {
@Inject
@AppShell
lateinit var rootShell: Provider<RootShell>
@Inject @Inject
lateinit var networkMonitor: NetworkMonitor lateinit var networkMonitor: NetworkMonitor
@@ -100,10 +94,9 @@ class AutoTunnelService : LifecycleService() {
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
Timber.d("onStartCommand executed with startId: $startId") Timber.d("onStartCommand executed with startId: $startId")
serviceManager.autoTunnelService.complete(this) serviceManager.autoTunnelService.complete(this)
return START_NOT_STICKY return super.onStartCommand(intent, flags, startId)
} }
fun start() { fun start() {
@@ -162,49 +155,54 @@ class AutoTunnelService : LifecycleService() {
} }
} }
private suspend fun buildNetworkState(connectivityState: ConnectivityState): NetworkState { private fun buildNetworkState(networkStatus: NetworkStatus): NetworkState {
return with(autoTunnelStateFlow.value.networkState) { return with(autoTunnelStateFlow.value.networkState) {
val wifiName = when { val wifiName = when (networkStatus) {
connectivityState.wifiAvailable && is NetworkStatus.Connected -> {
(wifiName == null || wifiName == Constants.UNREADABLE_SSID || networkMonitor.didWifiChangeSinceLastCapabilitiesQuery) -> { networkStatus.wifiSsid
networkMonitor.getWifiCapabilities()?.let { getWifiName(it) } ?: wifiName
} }
!connectivityState.wifiAvailable -> null else -> null
else -> wifiName
} }
copy( copy(
isWifiConnected = connectivityState.wifiAvailable, isWifiConnected = networkStatus.wifiConnected,
isMobileDataConnected = connectivityState.cellularAvailable, isMobileDataConnected = networkStatus.cellularConnected,
isEthernetConnected = isEthernetConnected, isEthernetConnected = networkStatus.ethernetConnected,
wifiName = wifiName, wifiName = wifiName,
) )
} }
} }
@OptIn(ExperimentalCoroutinesApi::class)
private fun startAutoTunnelStateJob() = lifecycleScope.launch(ioDispatcher) { private fun startAutoTunnelStateJob() = lifecycleScope.launch(ioDispatcher) {
combine( combine(
combineSettings(), combineSettings(),
networkMonitor.status.map { appDataRepository.get().settings.flow
buildNetworkState(it) .distinctUntilChanged { old, new -> old.isKernelEnabled == new.isKernelEnabled } // Only emit when isKernelEnabled changes
}.distinctUntilChanged(), .flatMapLatest { settings ->
networkMonitor.getNetworkStatusFlow(true, settings.isKernelEnabled)
.flowOn(ioDispatcher)
.map { buildNetworkState(it) }
}
.distinctUntilChanged(),
) { double, networkState -> ) { double, networkState ->
AutoTunnelState(tunnelManager.activeTunnels().value, networkState, double.first, double.second) AutoTunnelState(
tunnelManager.activeTunnels.value,
networkState,
double.first,
double.second,
)
}.collect { state -> }.collect { state ->
autoTunnelStateFlow.update { autoTunnelStateFlow.update {
it.copy(activeTunnels = state.activeTunnels, networkState = state.networkState, settings = state.settings, tunnels = state.tunnels) it.copy(
activeTunnels = state.activeTunnels,
networkState = state.networkState,
settings = state.settings,
tunnels = state.tunnels,
)
} }
} }
} }
private suspend fun getWifiName(wifiCapabilities: NetworkCapabilities): String? {
val setting = appDataRepository.get().settings.get()
return if (setting.isWifiNameByShellEnabled) {
rootShell.get().getCurrentWifiName()
} else {
InternetConnectivityMonitor.getNetworkName(wifiCapabilities, this@AutoTunnelService)
}
}
private fun combineSettings(): Flow<Pair<AppSettings, Tunnels>> { private fun combineSettings(): Flow<Pair<AppSettings, Tunnels>> {
return combine( return combine(
appDataRepository.get().settings.flow, appDataRepository.get().settings.flow,
@@ -241,7 +239,7 @@ class AutoTunnelService : LifecycleService() {
Timber.d("Starting with debounce delay of: ${settings.debounceDelaySeconds} seconds") Timber.d("Starting with debounce delay of: ${settings.debounceDelaySeconds} seconds")
autoTunnelStateFlow.debounce(settings.debounceDelayMillis()).collect { watcherState -> autoTunnelStateFlow.debounce(settings.debounceDelayMillis()).collect { watcherState ->
if (watcherState == defaultState) return@collect if (watcherState == defaultState) return@collect
Timber.d("New auto tunnel state emitted") Timber.d("New auto tunnel state emitted ${watcherState.networkState}")
when (val event = watcherState.asAutoTunnelEvent()) { when (val event = watcherState.asAutoTunnelEvent()) {
is AutoTunnelEvent.Start -> (event.tunnelConf ?: appDataRepository.get().getPrimaryOrFirstTunnel())?.let { is AutoTunnelEvent.Start -> (event.tunnelConf ?: appDataRepository.get().getPrimaryOrFirstTunnel())?.let {
tunnelManager.startTunnel(it) tunnelManager.startTunnel(it)
@@ -52,9 +52,14 @@ class TunnelControlTile : TileService() {
} }
fun updateTileState() = applicationScope.launch { fun updateTileState() = applicationScope.launch {
if (appDataRepository.tunnels.getAll().isEmpty()) return@launch setUnavailable() val tunnels = appDataRepository.tunnels.getAll()
with(tunnelManager.activeTunnels().value) { if (tunnels.isEmpty()) return@launch setUnavailable()
if (isNotEmpty()) return@launch updateTile(if (size == 1) first().tunName else getString(R.string.multiple), true) with(tunnelManager.activeTunnels.value) {
if (isNotEmpty()) if (size == 1) {
tunnels.firstOrNull { it.id == keys.first() }?.let { return@launch updateTile(it.tunName, true) }
} else {
return@launch updateTile(getString(R.string.multiple), true)
}
} }
appDataRepository.getStartTunnelConfig()?.let { appDataRepository.getStartTunnelConfig()?.let {
updateTile(it.tunName, false) updateTile(it.tunName, false)
@@ -65,7 +70,7 @@ class TunnelControlTile : TileService() {
super.onClick() super.onClick()
unlockAndRun { unlockAndRun {
applicationScope.launch { applicationScope.launch {
if (tunnelManager.activeTunnels().value.isNotEmpty()) return@launch tunnelManager.stopTunnel() if (tunnelManager.activeTunnels.value.isNotEmpty()) return@launch tunnelManager.stopTunnel()
appDataRepository.getStartTunnelConfig()?.let { appDataRepository.getStartTunnelConfig()?.let {
tunnelManager.startTunnel(it) tunnelManager.startTunnel(it)
} }
@@ -1,8 +1,10 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.networkmonitor.NetworkStatus
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.core.network.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
@@ -14,10 +16,12 @@ import com.zaneschepke.wireguardautotunnel.domain.enums.BackendError
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.StringValue import com.zaneschepke.wireguardautotunnel.util.StringValue
import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState
import com.zaneschepke.wireguardautotunnel.util.extensions.cancelWithMessage import com.zaneschepke.wireguardautotunnel.util.extensions.cancelWithMessage
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -27,11 +31,12 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
open class BaseTunnel( open class BaseTunnel(
@@ -45,93 +50,112 @@ open class BaseTunnel(
internal val tunnels = MutableStateFlow<List<TunnelConf>>(emptyList()) internal val tunnels = MutableStateFlow<List<TunnelConf>>(emptyList())
private val tunnelJobs = mutableMapOf<TunnelConf, Job>() private val _tunnelStates = MutableStateFlow<Map<Int, TunnelState>>(emptyMap())
private val tunnelJobs = ConcurrentHashMap<Int, Job>()
private val isNetworkAvailable = AtomicBoolean(false) private val isNetworkAvailable = AtomicBoolean(false)
init { init {
applicationScope.launch(ioDispatcher) { applicationScope.launch(ioDispatcher) {
launch { launch { startNetworkJob() }
startNetworkJob() launch { monitorTunnelConfigChanges() }
}
tunnels.collect { tuns -> tunnels.collect { tuns ->
val previousTuns = tunnelJobs.keys.toSet() val previousTunIds = tunnelJobs.keys.toSet()
val newTuns = tuns - previousTuns val currentTunIds = tuns.map { it.id }.toSet()
val removedItems = previousTuns - tuns.toSet() val newTuns = tuns.filter { it.id !in previousTunIds }
val removedTunIds = previousTunIds - currentTunIds
newTuns.forEach { tun -> newTuns.forEach { tun ->
Timber.d("Starting tunnel jobs for tun ${tun.name}") Timber.d("Starting tunnel jobs for tun ${tun.name} (ID: ${tun.id})")
tunnelJobs[tun] = startTunnelJobs(tun) tunnelJobs[tun.id] = startTunnelJobs(tun)
} }
removedItems.forEach { tun -> removedTunIds.forEach { tunId ->
tunnelJobs[tun]?.cancelWithMessage("Canceling tunnel jobs for tunnel: ${tun.name}") tunnelJobs[tunId]?.cancelWithMessage("Canceling tunnel jobs for tunnel ID: $tunId")
tunnelJobs.remove(tun) tunnelJobs.remove(tunId)
} _tunnelStates.update { it - tunId }
serviceManager.updateTunnelTile() serviceManager.updateTunnelTile()
} }
} }
} }
}
private fun startTunnelJobs(tunnel: TunnelConf) = applicationScope.launch(ioDispatcher) { private fun startTunnelJobs(tunnel: TunnelConf) = applicationScope.launch(ioDispatcher) {
launch { launch { startTunnelStatisticsJob(tunnel) }
startTunnelStatisticsJob(tunnel) if (tunnel.isPingEnabled) launch { startPingJob(tunnel) }
} }
launch {
startPingJob(tunnel) private fun updateTunnelState(tunnelId: Int, newState: TunnelStatus) {
Timber.d("Updating tunnel state for ID $tunnelId to $newState")
_tunnelStates.update { current ->
val currentState = current[tunnelId]
val updatedState = currentState?.copy(state = newState) ?: TunnelState(state = newState)
val newMap = current + (tunnelId to updatedState)
Timber.d("New tunnel states: $newMap")
newMap
} }
launch {
startTunnelConfigChangeJob(tunnel)
} }
internal fun beforeStartTunnel(tunnelConf: TunnelConf) {
tunnelConf.setStateChangeCallback { state ->
Timber.d("New tunnel state $state")
when (state) {
is Tunnel.State -> updateTunnelState(tunnelConf.id, state.asTunnelState())
is org.amnezia.awg.backend.Tunnel.State -> updateTunnelState(tunnelConf.id, state.asTunnelState())
}
applicationScope.launch(ioDispatcher) {
serviceManager.updateTunnelTile()
}
}
}
override fun startTunnel(tunnelConf: TunnelConf) {
applicationScope.launch(ioDispatcher) {
serviceManager.startBackgroundService(tunnelConf)
appDataRepository.tunnels.save(tunnelConf.copy(isActive = true))
addToActiveTunnels(tunnelConf)
}
}
override fun stopTunnel(tunnelConf: TunnelConf?) {
// Default empty implementation; subclasses override
} }
override suspend fun bounceTunnel(tunnelConf: TunnelConf) { override suspend fun bounceTunnel(tunnelConf: TunnelConf) {
if (tunnels.value.any { it.id == tunnelConf.id }) { stopTunnel(tunnelConf)
toggleTunnel(tunnelConf, TunnelStatus.DOWN) delay(1000)
toggleTunnel(tunnelConf, TunnelStatus.UP) startTunnel(tunnelConf)
}
} }
override suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) { override suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
// Default empty implementation
} }
override suspend fun runningTunnelNames(): Set<String> { override suspend fun runningTunnelNames(): Set<String> {
return emptySet() return emptySet()
} }
override suspend fun activeTunnels(): StateFlow<List<TunnelConf>> { override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics {
return tunnels.asStateFlow()
}
override suspend fun startTunnel(tunnelConf: TunnelConf) {
if (tunnels.value.any { it.id == tunnelConf.id }) return Timber.w("Tunnel already running")
serviceManager.startBackgroundService(tunnelConf)
appDataRepository.tunnels.save(tunnelConf.copy(isActive = true))
}
override suspend fun stopTunnel(tunnelConf: TunnelConf?) {
}
open suspend fun toggleTunnel(tunnelConf: TunnelConf, state: TunnelStatus) {
}
open suspend fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics {
throw NotImplementedError("Get statistics not implemented in base class") throw NotImplementedError("Get statistics not implemented in base class")
} }
override val activeTunnels: StateFlow<Map<Int, TunnelState>>
get() = _tunnelStates.asStateFlow()
internal suspend fun onTunnelStop(tunnelConf: TunnelConf) { internal suspend fun onTunnelStop(tunnelConf: TunnelConf) {
appDataRepository.tunnels.save(tunnelConf.copy(isActive = false)) appDataRepository.tunnels.save(tunnelConf.copy(isActive = false))
removeFromActiveTunnels(tunnelConf) removeFromActiveTunnels(tunnelConf)
if (tunnels.value.isEmpty()) serviceManager.stopBackgroundService() if (tunnels.value.isEmpty()) serviceManager.stopBackgroundService()
} }
internal suspend fun stopAllTunnels() { internal fun stopAllTunnels() {
tunnels.value.forEach { tunnels.value.forEach {
stopTunnel(it) stopTunnel(it)
} }
} }
internal fun addToActiveTunnels(conf: TunnelConf) { private fun addToActiveTunnels(conf: TunnelConf) {
tunnels.update { tunnels.update {
it.toMutableList().apply { it.toMutableList().apply {
add(conf) add(conf)
@@ -148,23 +172,26 @@ open class BaseTunnel(
} }
private suspend fun startNetworkJob() = coroutineScope { private suspend fun startNetworkJob() = coroutineScope {
networkMonitor.status.distinctUntilChanged().collect { networkMonitor.getNetworkStatusFlow(includeWifiSsid = false, useRootShell = false)
isNetworkAvailable.set(!it.allOffline) .flowOn(ioDispatcher).collect {
isNetworkAvailable.set(it !is NetworkStatus.Disconnected)
} }
} }
private suspend fun startPingJob(tunnel: TunnelConf) = coroutineScope { private suspend fun startPingJob(tunnel: TunnelConf) = coroutineScope {
while (isActive) { while (isActive) {
runCatching {
if (isNetworkAvailable.get() && tunnel.isActive) { if (isNetworkAvailable.get() && tunnel.isActive) {
val pingResult = tunnel.pingTunnel(ioDispatcher) val pingSuccess = tunnel.isTunnelPingable(ioDispatcher)
handlePingResult(tunnel, pingResult) handlePingResult(tunnel, pingSuccess)
}
delay(tunnel.pingInterval ?: Constants.PING_INTERVAL)
} }
delay(CHECK_INTERVAL)
} }
} }
private suspend fun handlePingResult(tunnel: TunnelConf, pingResult: List<Boolean>) { private suspend fun handlePingResult(tunnel: TunnelConf, pingSuccess: Boolean) {
if (pingResult.contains(false)) { if (!pingSuccess) {
if (isNetworkAvailable.get()) { if (isNetworkAvailable.get()) {
Timber.i("Ping result: target was not reachable, bouncing the tunnel") Timber.i("Ping result: target was not reachable, bouncing the tunnel")
bounceTunnel(tunnel) bounceTunnel(tunnel)
@@ -197,23 +224,33 @@ open class BaseTunnel(
} }
} }
private suspend fun startTunnelConfigChangeJob(tunnel: TunnelConf) = coroutineScope { private suspend fun monitorTunnelConfigChanges() = coroutineScope {
appDataRepository.tunnels.flow.collect { storageTuns -> appDataRepository.tunnels.flow.collect { storageTuns ->
storageTuns.firstOrNull { it.id == tunnel.id }?.let { storageTun -> storageTuns.forEach { storageTun ->
if (tunnel.isQuickConfigChanged(storageTun) || tunnel.isPingConfigMatching(storageTun)) { val currentTun = tunnels.value.firstOrNull { it.id == storageTun.id }
bounceTunnel(tunnel) if (currentTun != null) {
if (!currentTun.isQuickConfigMatching(storageTun)) {
Timber.d("Tunnel config changed for ID $storageTun, bouncing tunnel")
bounceTunnel(storageTun)
}
} }
} }
} }
} }
private suspend fun startTunnelStatisticsJob(tunnel: TunnelConf) = coroutineScope { private suspend fun startTunnelStatisticsJob(tunnel: TunnelConf) = coroutineScope {
while (isActive) { while (this.isActive) {
runCatching {
val stats = getStatistics(tunnel) val stats = getStatistics(tunnel)
tunnel.state.update { _tunnelStates.update { currentStates ->
it.copy(statistics = stats) val updatedState = currentStates[tunnel.id]?.copy(statistics = stats)
?: TunnelState(statistics = stats)
currentStates + (tunnel.id to updatedState)
} }
delay(CHECK_INTERVAL) delay(CHECK_INTERVAL)
}.onFailure { exception ->
Timber.e(exception, "Failed to update tunnel statistics for ${tunnel.tunName}")
}
} }
} }
} }
@@ -3,21 +3,20 @@ package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.wireguard.android.backend.Backend import com.wireguard.android.backend.Backend
import com.wireguard.android.backend.BackendException import com.wireguard.android.backend.BackendException
import com.wireguard.android.backend.Tunnel import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.core.network.NetworkMonitor import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.domain.state.WireGuardStatistics import com.zaneschepke.wireguardautotunnel.domain.state.WireGuardStatistics
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendError import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendError
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.withContext import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@@ -31,13 +30,18 @@ class KernelTunnel @Inject constructor(
networkMonitor: NetworkMonitor, networkMonitor: NetworkMonitor,
) : BaseTunnel(ioDispatcher, applicationScope, networkMonitor, appDataRepository, serviceManager, notificationManager) { ) : BaseTunnel(ioDispatcher, applicationScope, networkMonitor, appDataRepository, serviceManager, notificationManager) {
override suspend fun startTunnel(tunnelConf: TunnelConf) { override fun startTunnel(tunnelConf: TunnelConf) {
withContext(ioDispatcher) { Timber.d("Starting tunnel ${tunnelConf.id} kernel")
super.startTunnel(tunnelConf) applicationScope.launch(ioDispatcher) {
if (tunnels.value.any { it.id == tunnelConf.id }) return@launch Timber.w("Tunnel already running")
runCatching { runCatching {
Timber.d("Setting backend state UP")
super.beforeStartTunnel(tunnelConf)
backend.setState(tunnelConf, Tunnel.State.UP, tunnelConf.toWgConfig()) backend.setState(tunnelConf, Tunnel.State.UP, tunnelConf.toWgConfig())
addToActiveTunnels(tunnelConf) Timber.d("Calling super.startTunnel")
super.startTunnel(tunnelConf)
}.onFailure { }.onFailure {
Timber.e(it, "Failed to start tunnel ${tunnelConf.id} kernel")
onTunnelStop(tunnelConf) onTunnelStop(tunnelConf)
if (it is BackendException) { if (it is BackendException) {
handleBackendThrowable(it.toBackendError()) handleBackendThrowable(it.toBackendError())
@@ -48,15 +52,14 @@ class KernelTunnel @Inject constructor(
} }
} }
override suspend fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics { override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics {
return WireGuardStatistics(backend.getStatistics(tunnelConf)) return WireGuardStatistics(backend.getStatistics(tunnelConf))
} }
override suspend fun stopTunnel(tunnelConf: TunnelConf?) { override fun stopTunnel(tunnelConf: TunnelConf?) {
withContext(ioDispatcher) { applicationScope.launch(ioDispatcher) {
val tunnel = tunnels.value.firstOrNull { it.id == tunnelConf?.id }
runCatching { runCatching {
tunnel?.let { tunnels.value.firstOrNull { it.id == tunnelConf?.id }?.let {
backend.setState(it, Tunnel.State.DOWN, it.toWgConfig()) backend.setState(it, Tunnel.State.DOWN, it.toWgConfig())
onTunnelStop(it) onTunnelStop(it)
} ?: stopAllTunnels() } ?: stopAllTunnels()
@@ -66,13 +69,6 @@ class KernelTunnel @Inject constructor(
} }
} }
override suspend fun toggleTunnel(tunnelConf: TunnelConf, status: TunnelStatus) {
when (status) {
TunnelStatus.UP -> backend.setState(tunnelConf, Tunnel.State.UP, tunnelConf.toWgConfig())
TunnelStatus.DOWN -> backend.setState(tunnelConf, Tunnel.State.DOWN, tunnelConf.toWgConfig())
}
}
override suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) { override suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
Timber.w("Not yet implemented for kernel") Timber.w("Not yet implemented for kernel")
} }
@@ -4,20 +4,20 @@ import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.di.Kernel import com.zaneschepke.wireguardautotunnel.di.Kernel
import com.zaneschepke.wireguardautotunnel.di.Userspace import com.zaneschepke.wireguardautotunnel.di.Userspace
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.util.extensions.withData import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import kotlinx.coroutines.withContext
import javax.inject.Inject import javax.inject.Inject
class TunnelManager @Inject constructor( class TunnelManager @Inject constructor(
@@ -28,71 +28,69 @@ class TunnelManager @Inject constructor(
@IoDispatcher private val ioDispatcher: CoroutineDispatcher, @IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : TunnelProvider { ) : TunnelProvider {
val appSettings: StateFlow<AppSettings?> = appDataRepository.settings.flow.stateIn( @OptIn(ExperimentalCoroutinesApi::class)
private val tunnelProviderFlow = appDataRepository.settings.flow
.filterNotNull()
.flatMapLatest { settings ->
MutableStateFlow(if (settings.isKernelEnabled) kernelTunnel else userspaceTunnel)
}
.stateIn(
scope = applicationScope.plus(ioDispatcher), scope = applicationScope.plus(ioDispatcher),
started = SharingStarted.Eagerly, started = SharingStarted.Eagerly,
initialValue = null, initialValue = userspaceTunnel,
) )
override suspend fun activeTunnels(): StateFlow<List<TunnelConf>> { @OptIn(ExperimentalCoroutinesApi::class)
return withContext(ioDispatcher) { override val activeTunnels = appDataRepository.settings.flow
appSettings.filterNotNull().first().let { .filterNotNull()
if (it.isKernelEnabled) return@withContext kernelTunnel.activeTunnels() .flatMapLatest { settings ->
userspaceTunnel.activeTunnels() if (settings.isKernelEnabled) {
kernelTunnel.activeTunnels
} else {
userspaceTunnel.activeTunnels
} }
} }
.stateIn(
scope = applicationScope.plus(ioDispatcher),
started = SharingStarted.Eagerly,
initialValue = emptyMap(),
)
override fun startTunnel(tunnelConf: TunnelConf) {
tunnelProviderFlow.value.startTunnel(tunnelConf)
} }
override suspend fun startTunnel(tunnelConf: TunnelConf) { override fun stopTunnel(tunnelConf: TunnelConf?) {
appSettings.withData { tunnelProviderFlow.value.stopTunnel(tunnelConf)
if (it.isKernelEnabled) return@withData kernelTunnel.startTunnel(tunnelConf)
userspaceTunnel.startTunnel(tunnelConf)
}
}
override suspend fun stopTunnel(tunnelConf: TunnelConf?) {
appSettings.withData {
if (it.isKernelEnabled) return@withData kernelTunnel.stopTunnel(tunnelConf)
userspaceTunnel.stopTunnel(tunnelConf)
}
} }
override suspend fun bounceTunnel(tunnelConf: TunnelConf) { override suspend fun bounceTunnel(tunnelConf: TunnelConf) {
appSettings.withData { tunnelProviderFlow.value.bounceTunnel(tunnelConf)
if (it.isKernelEnabled) return@withData kernelTunnel.stopTunnel(tunnelConf)
userspaceTunnel.stopTunnel(tunnelConf)
}
} }
override suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) { override suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
appSettings.withData { tunnelProviderFlow.value.setBackendState(backendState, allowedIps)
if (it.isKernelEnabled) return@withData kernelTunnel.setBackendState(backendState, allowedIps)
userspaceTunnel.setBackendState(backendState, allowedIps)
}
} }
override suspend fun runningTunnelNames(): Set<String> { override suspend fun runningTunnelNames(): Set<String> {
appSettings.filterNotNull().first().let { return tunnelProviderFlow.value.runningTunnelNames()
if (it.isKernelEnabled) return kernelTunnel.runningTunnelNames()
return userspaceTunnel.runningTunnelNames()
}
} }
suspend fun restorePreviousState() { override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics {
withContext(ioDispatcher) { return tunnelProviderFlow.value.getStatistics(tunnelConf)
with(appDataRepository.settings.get()) { }
if (isRestoreOnBootEnabled) {
fun restorePreviousState() = applicationScope.launch(ioDispatcher) {
val settings = appDataRepository.settings.get()
if (settings.isRestoreOnBootEnabled) {
val previouslyActiveTuns = appDataRepository.tunnels.getActive() val previouslyActiveTuns = appDataRepository.tunnels.getActive()
// handle kernel mode val tunsToStart = previouslyActiveTuns.filterNot { tun -> activeTunnels.value.any { tun.id == it.key } }
val tunsToStart = previouslyActiveTuns.filterNot { tun -> activeTunnels().value.any { tun.id == it.id } } if (settings.isKernelEnabled) {
if (isKernelEnabled) { return@launch tunsToStart.forEach {
return@withContext tunsToStart.forEach {
startTunnel(it) startTunnel(it)
} }
} } else {
// handle userspace tunsToStart.firstOrNull()?.let { startTunnel(it) }
if (activeTunnels().value.isEmpty()) tunsToStart.firstOrNull()?.let { startTunnel(it) }
}
} }
} }
} }
@@ -2,23 +2,20 @@ package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
interface TunnelProvider { interface TunnelProvider {
suspend fun activeTunnels(): StateFlow<List<TunnelConf>>
suspend fun startTunnel(tunnelConf: TunnelConf)
suspend fun stopTunnel(tunnelConf: TunnelConf? = null)
suspend fun bounceTunnel(tunnelConf: TunnelConf)
suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>)
suspend fun runningTunnelNames(): Set<String>
companion object { companion object {
const val CHECK_INTERVAL = 1_000L const val CHECK_INTERVAL = 1000L
} }
fun startTunnel(tunnelConf: TunnelConf)
fun stopTunnel(tunnelConf: TunnelConf? = null)
suspend fun bounceTunnel(tunnelConf: TunnelConf)
suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>)
suspend fun runningTunnelNames(): Set<String>
fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics
val activeTunnels: StateFlow<Map<Int, TunnelState>>
} }
@@ -1,14 +1,13 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.wireguard.android.backend.BackendException import com.wireguard.android.backend.BackendException
import com.zaneschepke.wireguardautotunnel.core.network.NetworkMonitor import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.state.AmneziaStatistics import com.zaneschepke.wireguardautotunnel.domain.state.AmneziaStatistics
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
@@ -16,7 +15,7 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.asAmBackendState
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendError import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendError
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.withContext import kotlinx.coroutines.launch
import org.amnezia.awg.backend.Backend import org.amnezia.awg.backend.Backend
import org.amnezia.awg.backend.Tunnel import org.amnezia.awg.backend.Tunnel
import timber.log.Timber import timber.log.Timber
@@ -30,15 +29,24 @@ class UserspaceTunnel @Inject constructor(
notificationManager: NotificationManager, notificationManager: NotificationManager,
private val backend: Backend, private val backend: Backend,
networkMonitor: NetworkMonitor, networkMonitor: NetworkMonitor,
) : TunnelProvider, BaseTunnel(ioDispatcher, applicationScope, networkMonitor, appDataRepository, serviceManager, notificationManager) { ) : BaseTunnel(ioDispatcher, applicationScope, networkMonitor, appDataRepository, serviceManager, notificationManager) {
override suspend fun startTunnel(tunnelConf: TunnelConf) { override fun startTunnel(tunnelConf: TunnelConf) {
withContext(ioDispatcher) { applicationScope.launch(ioDispatcher) {
super.startTunnel(tunnelConf) Timber.d("Starting tunnel ${tunnelConf.id} userspace")
if (tunnels.value.any { it.id == tunnelConf.id }) return@launch Timber.w("Tunnel already running")
if (tunnels.value.isNotEmpty()) {
Timber.d("Stopping all tunnels")
stopAllTunnels()
}
runCatching { runCatching {
Timber.d("Setting backend state UP")
super.beforeStartTunnel(tunnelConf)
backend.setState(tunnelConf, Tunnel.State.UP, tunnelConf.toAmConfig()) backend.setState(tunnelConf, Tunnel.State.UP, tunnelConf.toAmConfig())
addToActiveTunnels(tunnelConf) Timber.d("Calling super.startTunnel")
super.startTunnel(tunnelConf)
}.onFailure { }.onFailure {
Timber.e(it, "Failed to start tunnel ${tunnelConf.id} userspace")
onTunnelStop(tunnelConf) onTunnelStop(tunnelConf)
if (it is BackendException) { if (it is BackendException) {
handleBackendThrowable(it.toBackendError()) handleBackendThrowable(it.toBackendError())
@@ -49,19 +57,8 @@ class UserspaceTunnel @Inject constructor(
} }
} }
override suspend fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics { override fun stopTunnel(tunnelConf: TunnelConf?) {
return AmneziaStatistics(backend.getStatistics(tunnelConf)) applicationScope.launch(ioDispatcher) {
}
override suspend fun toggleTunnel(tunnelConf: TunnelConf, status: TunnelStatus) {
when (status) {
TunnelStatus.UP -> backend.setState(tunnelConf, Tunnel.State.UP, tunnelConf.toAmConfig())
TunnelStatus.DOWN -> backend.setState(tunnelConf, Tunnel.State.DOWN, tunnelConf.toAmConfig())
}
}
override suspend fun stopTunnel(tunnelConf: TunnelConf?) {
withContext(ioDispatcher) {
runCatching { runCatching {
tunnels.value.firstOrNull { it.id == tunnelConf?.id }?.let { tunnels.value.firstOrNull { it.id == tunnelConf?.id }?.let {
backend.setState(it, Tunnel.State.DOWN, it.toAmConfig()) backend.setState(it, Tunnel.State.DOWN, it.toAmConfig())
@@ -80,4 +77,8 @@ class UserspaceTunnel @Inject constructor(
override suspend fun runningTunnelNames(): Set<String> { override suspend fun runningTunnelNames(): Set<String> {
return backend.runningTunnelNames return backend.runningTunnelNames
} }
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics {
return AmneziaStatistics(backend.getStatistics(tunnelConf))
}
} }
@@ -53,7 +53,7 @@ class ServiceWorker @AssistedInject constructor(
Timber.i("Service worker started") Timber.i("Service worker started")
with(appDataRepository.settings.get()) { with(appDataRepository.settings.get()) {
if (isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) return@with serviceManager.startAutoTunnel(true) if (isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) return@with serviceManager.startAutoTunnel(true)
if (tunnelManager.activeTunnels().value.isEmpty()) tunnelManager.restorePreviousState() if (tunnelManager.activeTunnels.value.isEmpty()) tunnelManager.restorePreviousState()
} }
Result.success() Result.success()
} }
@@ -1,16 +0,0 @@
package com.zaneschepke.wireguardautotunnel.di
import com.zaneschepke.wireguardautotunnel.core.network.InternetConnectivityMonitor
import com.zaneschepke.wireguardautotunnel.core.network.NetworkMonitor
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
abstract class ServiceModule {
@Binds
abstract fun provideInternetConnectivityService(wifiService: InternetConnectivityMonitor): NetworkMonitor
}
@@ -4,7 +4,8 @@ import android.content.Context
import com.wireguard.android.backend.WgQuickBackend import com.wireguard.android.backend.WgQuickBackend
import com.wireguard.android.util.RootShell import com.wireguard.android.util.RootShell
import com.wireguard.android.util.ToolsInstaller import com.wireguard.android.util.ToolsInstaller
import com.zaneschepke.wireguardautotunnel.core.network.NetworkMonitor import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.KernelTunnel import com.zaneschepke.wireguardautotunnel.core.tunnel.KernelTunnel
@@ -98,13 +99,20 @@ class TunnelModule {
return TunnelManager(kernelTunnel, userspaceTunnel, appDataRepository, applicationScope, ioDispatcher) return TunnelManager(kernelTunnel, userspaceTunnel, appDataRepository, applicationScope, ioDispatcher)
} }
@Provides
@Singleton
fun provideNetworkMonitor(@ApplicationContext context: Context): NetworkMonitor {
return AndroidNetworkMonitor(context)
}
@Singleton @Singleton
@Provides @Provides
fun provideServiceManager( fun provideServiceManager(
@ApplicationContext context: Context, @ApplicationContext context: Context,
@IoDispatcher ioDispatcher: CoroutineDispatcher, @IoDispatcher ioDispatcher: CoroutineDispatcher,
@ApplicationScope applicationScope: CoroutineScope,
appDataRepository: AppDataRepository, appDataRepository: AppDataRepository,
): ServiceManager { ): ServiceManager {
return ServiceManager(context, ioDispatcher, appDataRepository) return ServiceManager(context, ioDispatcher, applicationScope, appDataRepository)
} }
} }
@@ -1,14 +1,11 @@
package com.zaneschepke.wireguardautotunnel.domain.entity package com.zaneschepke.wireguardautotunnel.domain.entity
import com.wireguard.config.Config import com.wireguard.config.Config
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState
import com.zaneschepke.wireguardautotunnel.util.extensions.isReachable import com.zaneschepke.wireguardautotunnel.util.extensions.isReachable
import com.zaneschepke.wireguardautotunnel.util.extensions.toWgQuickString import com.zaneschepke.wireguardautotunnel.util.extensions.toWgQuickString
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.Transient
import org.amnezia.awg.backend.Tunnel import org.amnezia.awg.backend.Tunnel
import timber.log.Timber import timber.log.Timber
import java.io.InputStream import java.io.InputStream
@@ -30,9 +27,13 @@ data class TunnelConf(
val pingIp: String? = null, val pingIp: String? = null,
val isEthernetTunnel: Boolean = false, val isEthernetTunnel: Boolean = false,
val isIpv4Preferred: Boolean = false, val isIpv4Preferred: Boolean = false,
@Transient
private var stateChangeCallback: ((Any) -> Unit)? = null,
) : Tunnel, com.wireguard.android.backend.Tunnel { ) : Tunnel, com.wireguard.android.backend.Tunnel {
val state = MutableStateFlow(TunnelState()) fun setStateChangeCallback(callback: (Any) -> Unit) {
stateChangeCallback = callback
}
fun toAmConfig(): org.amnezia.awg.config.Config { fun toAmConfig(): org.amnezia.awg.config.Config {
return configFromAmQuick(amQuick.ifBlank { wgQuick }) return configFromAmQuick(amQuick.ifBlank { wgQuick })
@@ -50,21 +51,17 @@ data class TunnelConf(
return isIpv4Preferred return isIpv4Preferred
} }
override fun onStateChange(newState: Tunnel.State) {
state.update {
it.copy(state = newState.asTunnelState())
}
}
override fun onStateChange(newState: com.wireguard.android.backend.Tunnel.State) { override fun onStateChange(newState: com.wireguard.android.backend.Tunnel.State) {
state.update { stateChangeCallback?.invoke(newState)
it.copy(state = newState.asTunnelState())
}
} }
fun isQuickConfigChanged(updatedConf: TunnelConf): Boolean { override fun onStateChange(newState: Tunnel.State) {
return updatedConf.wgQuick != wgQuick || stateChangeCallback?.invoke(newState)
updatedConf.amQuick != amQuick }
fun isQuickConfigMatching(updatedConf: TunnelConf): Boolean {
return updatedConf.wgQuick == wgQuick ||
updatedConf.amQuick == amQuick
} }
fun isPingConfigMatching(updatedConf: TunnelConf): Boolean { fun isPingConfigMatching(updatedConf: TunnelConf): Boolean {
@@ -74,18 +71,17 @@ data class TunnelConf(
updatedConf.pingInterval == pingInterval updatedConf.pingInterval == pingInterval
} }
suspend fun pingTunnel(context: CoroutineContext): List<Boolean> { suspend fun isTunnelPingable(context: CoroutineContext): Boolean {
return withContext(context) { return withContext(context) {
val config = toWgConfig() val config = toWgConfig()
if (pingIp != null) { if (pingIp != null) {
Timber.i("Pinging custom ip") return@withContext InetAddress.getByName(pingIp)
listOf(InetAddress.getByName(pingIp).isReachable(Constants.PING_TIMEOUT.toInt())) .isReachable(Constants.PING_TIMEOUT.toInt())
} else { }
Timber.i("Pinging all peers") Timber.i("Pinging all peers")
config.peers.map { peer -> config.peers.map { peer ->
peer.isReachable(isIpv4Preferred) peer.isReachable(isIpv4Preferred)
} }.all { true }
}
} }
} }
@@ -7,7 +7,7 @@ import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
import com.zaneschepke.wireguardautotunnel.util.extensions.isMatchingToWildcardList import com.zaneschepke.wireguardautotunnel.util.extensions.isMatchingToWildcardList
data class AutoTunnelState( data class AutoTunnelState(
val activeTunnels: List<TunnelConf> = emptyList(), val activeTunnels: Map<Int, TunnelState> = emptyMap(),
val networkState: NetworkState = NetworkState(), val networkState: NetworkState = NetworkState(),
val settings: AppSettings = AppSettings(), val settings: AppSettings = AppSettings(),
val tunnels: List<TunnelConf> = emptyList(), val tunnels: List<TunnelConf> = emptyList(),
@@ -20,12 +20,12 @@ data class AutoTunnelState(
private fun isMobileTunnelDataChangeNeeded(): Boolean { private fun isMobileTunnelDataChangeNeeded(): Boolean {
val preferredTunnel = preferredMobileDataTunnel() val preferredTunnel = preferredMobileDataTunnel()
return preferredTunnel != null && return preferredTunnel != null &&
activeTunnels.isNotEmpty() && !activeTunnels.any { it.id == preferredTunnel.id } activeTunnels.isNotEmpty() && !activeTunnels.any { it.key == preferredTunnel.id }
} }
private fun isEthernetTunnelChangeNeeded(): Boolean { private fun isEthernetTunnelChangeNeeded(): Boolean {
val preferredTunnel = preferredEthernetTunnel() val preferredTunnel = preferredEthernetTunnel()
return preferredTunnel != null && activeTunnels.isNotEmpty() && !activeTunnels.any { it.id == preferredTunnel.id } return preferredTunnel != null && activeTunnels.isNotEmpty() && !activeTunnels.any { it.key == preferredTunnel.id }
} }
private fun preferredMobileDataTunnel(): TunnelConf? { private fun preferredMobileDataTunnel(): TunnelConf? {
@@ -62,7 +62,7 @@ data class AutoTunnelState(
return settings.isVpnKillSwitchEnabled && (!settings.isDisableKillSwitchOnTrustedEnabled || !isCurrentSSIDTrusted()) return settings.isVpnKillSwitchEnabled && (!settings.isDisableKillSwitchOnTrustedEnabled || !isCurrentSSIDTrusted())
} }
fun isNoConnectivity(): Boolean { private fun isNoConnectivity(): Boolean {
return !networkState.isEthernetConnected && !networkState.isWifiConnected && !networkState.isMobileDataConnected return !networkState.isEthernetConnected && !networkState.isWifiConnected && !networkState.isMobileDataConnected
} }
@@ -100,7 +100,7 @@ data class AutoTunnelState(
private fun isWifiTunnelPreferred(): Boolean { private fun isWifiTunnelPreferred(): Boolean {
val preferred = preferredWifiTunnel() val preferred = preferredWifiTunnel()
return activeTunnels.any { it.id == preferred?.id } return activeTunnels.any { it.key == preferred?.id }
} }
fun asAutoTunnelEvent(): AutoTunnelEvent { fun asAutoTunnelEvent(): AutoTunnelEvent {
@@ -77,7 +77,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
var selectedTunnel by remember { mutableStateOf<TunnelConf?>(null) } var selectedTunnel by remember { mutableStateOf<TunnelConf?>(null) }
val isRunningOnTv = remember { context.isRunningOnTv() } val isRunningOnTv = remember { context.isRunningOnTv() }
val activeTunnels by viewModel.activeTunnels.collectAsStateWithLifecycle(emptyList()) val activeTunnels by viewModel.tunnelManager.activeTunnels.collectAsStateWithLifecycle(emptyMap())
val collator = Collator.getInstance(Locale.getDefault()) val collator = Collator.getInstance(Locale.getDefault())
@@ -89,6 +89,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
val startTunnel = withVpnPermission<TunnelConf> { val startTunnel = withVpnPermission<TunnelConf> {
viewModel.onTunnelStart(it) viewModel.onTunnelStart(it)
} }
val autoTunnelToggleBattery = withIgnoreBatteryOpt(uiState.generalState.isBatteryOptimizationDisableShown) { val autoTunnelToggleBattery = withIgnoreBatteryOpt(uiState.generalState.isBatteryOptimizationDisableShown) {
if (!uiState.generalState.isBatteryOptimizationDisableShown) viewModel.setBatteryOptimizeDisableShown() if (!uiState.generalState.isBatteryOptimizationDisableShown) viewModel.setBatteryOptimizeDisableShown()
if (uiState.appSettings.isKernelEnabled) { if (uiState.appSettings.isKernelEnabled) {
@@ -132,15 +133,8 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
} }
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConf) { fun onTunnelToggle(checked: Boolean, tunnel: TunnelConf) {
if (!checked) { if (!checked) return viewModel.onTunnelStop(tunnel).let { }
viewModel.onTunnelStop(tunnel) if (uiState.appSettings.isKernelEnabled) viewModel.onTunnelStart(tunnel) else startTunnel(tunnel)
return
}
if (uiState.appSettings.isKernelEnabled) {
viewModel.onTunnelStart(tunnel)
} else {
startTunnel.invoke(tunnel)
}
} }
Scaffold( Scaffold(
@@ -233,13 +227,13 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
key = { tunnel -> tunnel.id }, key = { tunnel -> tunnel.id },
) { tunnel -> ) { tunnel ->
val expanded = uiState.generalState.isTunnelStatsExpanded val expanded = uiState.generalState.isTunnelStatsExpanded
val tunnelState = activeTunnels.firstOrNull { it.id == tunnel.id }?.state?.collectAsStateWithLifecycle() val tunnelState = activeTunnels.getOrDefault(tunnel.id, TunnelState())
TunnelRowItem( TunnelRowItem(
tunnel.isActive, tunnelState.state.isUp(),
expanded, expanded,
selectedTunnel?.id == tunnel.id, selectedTunnel?.id == tunnel.id,
tunnel, tunnel,
tunnelState = tunnelState?.value ?: TunnelState(), tunnelState = tunnelState,
{ selectedTunnel = tunnel }, { selectedTunnel = tunnel },
{ viewModel.onExpandedChanged(!expanded) }, { viewModel.onExpandedChanged(!expanded) },
onDelete = { showDeleteTunnelAlertDialog = true }, onDelete = { showDeleteTunnelAlertDialog = true },
@@ -41,6 +41,7 @@ import com.zaneschepke.wireguardautotunnel.ui.common.config.SubmitConfigurationT
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.ForwardButton import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.ForwardButton
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.isValidIpv4orIpv6Address import com.zaneschepke.wireguardautotunnel.util.extensions.isValidIpv4orIpv6Address
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
@@ -51,7 +52,7 @@ import kotlin.text.isNullOrBlank
import kotlin.text.toLong import kotlin.text.toLong
@Composable @Composable
fun OptionsScreen(tunnelConf: TunnelConf, viewModel: TunnelOptionsViewModel = hiltViewModel()) { fun OptionsScreen(tunnelConf: TunnelConf, appUiState: AppUiState, viewModel: TunnelOptionsViewModel = hiltViewModel()) {
val navController = LocalNavController.current val navController = LocalNavController.current
var currentText by remember { mutableStateOf("") } var currentText by remember { mutableStateOf("") }
@@ -194,6 +195,7 @@ fun OptionsScreen(tunnelConf: TunnelConf, viewModel: TunnelOptionsViewModel = hi
trailing = { trailing = {
ScaledSwitch( ScaledSwitch(
checked = tunnelConf.isPingEnabled, checked = tunnelConf.isPingEnabled,
enabled = !appUiState.activeTunnels.containsKey(tunnelConf.id),
onClick = { onPingToggle() }, onClick = { onPingToggle() },
) )
}, },
@@ -352,11 +352,11 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
ScaledSwitch( ScaledSwitch(
uiState.appSettings.isKernelEnabled, uiState.appSettings.isKernelEnabled,
onClick = { appViewModel.onToggleKernelMode() }, onClick = { appViewModel.onToggleKernelMode() },
// enabled = !( enabled = !(
// uiState.settings.isAutoTunnelEnabled || uiState.appSettings.isAutoTunnelEnabled ||
// uiState.settings.isAlwaysOnVpnEnabled || uiState.appSettings.isAlwaysOnVpnEnabled ||
// (uiState.vpnState.status == TunnelState.UP) uiState.activeTunnels.isNotEmpty()
// ), ),
) )
}, },
onClick = { onClick = {
@@ -18,8 +18,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -36,7 +34,6 @@ import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.common.label.VersionLabel import com.zaneschepke.wireguardautotunnel.ui.common.label.VersionLabel
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
@@ -51,20 +48,6 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
fun SupportScreen(appUiState: AppUiState, appViewModel: AppViewModel) { fun SupportScreen(appUiState: AppUiState, appViewModel: AppViewModel) {
val context = LocalContext.current val context = LocalContext.current
val navController = LocalNavController.current val navController = LocalNavController.current
var showDialog by remember { mutableStateOf(false) }
if (showDialog) {
InfoDialog(onAttest = {
showDialog = false
appViewModel.onToggleLocalLogging()
}, onDismiss = {
showDialog = false
}, title = {
Text(stringResource(R.string.configuration_change))
}, body = { Text(stringResource(R.string.requires_app_relaunch)) }, confirmText = { Text(stringResource(R.string.yes)) })
}
Column( Column(
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top), verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top),
@@ -115,12 +98,12 @@ fun SupportScreen(appUiState: AppUiState, appViewModel: AppViewModel) {
ScaledSwitch( ScaledSwitch(
appUiState.generalState.isLocalLogsEnabled, appUiState.generalState.isLocalLogsEnabled,
onClick = { onClick = {
showDialog = true appViewModel.onToggleLocalLogging()
}, },
) )
}, },
onClick = { onClick = {
showDialog = true appViewModel.onToggleLocalLogging()
}, },
), ),
) )
@@ -3,10 +3,12 @@ package com.zaneschepke.wireguardautotunnel.ui.state
import com.zaneschepke.wireguardautotunnel.data.model.GeneralState import com.zaneschepke.wireguardautotunnel.data.model.GeneralState
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
data class AppUiState( data class AppUiState(
val appSettings: AppSettings = AppSettings(), val appSettings: AppSettings = AppSettings(),
val tunnels: List<TunnelConf> = emptyList(), val tunnels: List<TunnelConf> = emptyList(),
val activeTunnels: Map<Int, TunnelState> = emptyMap(),
val generalState: GeneralState = GeneralState(), val generalState: GeneralState = GeneralState(),
val autoTunnelActive: Boolean = false, val autoTunnelActive: Boolean = false,
) )
@@ -2,7 +2,6 @@ package com.zaneschepke.wireguardautotunnel.util.extensions
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import com.wireguard.android.backend.BackendException import com.wireguard.android.backend.BackendException
import com.wireguard.android.util.RootShell
import com.wireguard.config.Peer import com.wireguard.config.Peer
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendError import com.zaneschepke.wireguardautotunnel.domain.enums.BackendError
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
@@ -46,9 +45,9 @@ fun TunnelStatistics.PeerStats.handshakeStatus(): HandshakeStatus {
fun Peer.isReachable(preferIpv4: Boolean): Boolean { fun Peer.isReachable(preferIpv4: Boolean): Boolean {
val host = val host =
if (this.endpoint.isPresent && if (this.endpoint.isPresent &&
this.endpoint.get().getResolved(preferIpv4).isPresent this.endpoint.get().resolved.isPresent
) { ) {
this.endpoint.get().getResolved(preferIpv4).get().host this.endpoint.get().resolved.get().host
} else { } else {
Constants.DEFAULT_PING_IP Constants.DEFAULT_PING_IP
} }
@@ -87,12 +86,6 @@ fun Config.toWgQuickString(): String {
return lines.joinToString(System.lineSeparator()) return lines.joinToString(System.lineSeparator())
} }
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()
}
fun Backend.BackendState.asBackendState(): BackendState { fun Backend.BackendState.asBackendState(): BackendState {
return BackendState.valueOf(this.name) return BackendState.valueOf(this.name)
} }
@@ -8,12 +8,6 @@ import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.ui.Route import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.isCurrentRoute import com.zaneschepke.wireguardautotunnel.ui.common.navigation.isCurrentRoute
fun NavController.navigateAndForget(route: Route) {
navigate(route) {
popUpTo(0)
}
}
fun NavController.goFromRoot(route: Route) { fun NavController.goFromRoot(route: Route) {
if (currentBackStackEntry?.isCurrentRoute(route::class) == true) return if (currentBackStackEntry?.isCurrentRoute(route::class) == true) return
this.navigate(route) { this.navigate(route) {
@@ -31,11 +31,9 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
@@ -75,11 +73,13 @@ constructor(
appDataRepository.settings.flow, appDataRepository.settings.flow,
appDataRepository.tunnels.flow, appDataRepository.tunnels.flow,
appDataRepository.appState.flow, appDataRepository.appState.flow,
tunnelManager.activeTunnels,
serviceManager.autoTunnelActive, serviceManager.autoTunnelActive,
) { settings, tunnels, generalState, autoTunnel -> ) { settings, tunnels, generalState, activeTunnels, autoTunnel ->
AppUiState( AppUiState(
settings, settings,
tunnels, tunnels,
activeTunnels,
generalState, generalState,
autoTunnel, autoTunnel,
) )
@@ -103,14 +103,13 @@ constructor(
private suspend fun appReadyCheck() { private suspend fun appReadyCheck() {
val tunnelCount = appDataRepository.tunnels.count() val tunnelCount = appDataRepository.tunnels.count()
uiState.takeWhile { it.tunnels.size != tunnelCount }.onCompletion { uiState.first { it.tunnels.count() == tunnelCount }
_isAppReady.emit(true) _isAppReady.emit(true)
}.collect()
} }
private suspend fun initTunnels() { private suspend fun initTunnels() {
tunnels.withData { tunnels.withData { tunnels ->
it.filter { it.isActive }.forEach { tunnels.filter { it.isActive }.forEach {
tunnelManager.startTunnel(it) tunnelManager.startTunnel(it)
} }
} }
@@ -146,17 +145,12 @@ constructor(
with(uiState.value.generalState) { with(uiState.value.generalState) {
val toggledOn = !isLocalLogsEnabled val toggledOn = !isLocalLogsEnabled
appDataRepository.appState.setLocalLogsEnabled(toggledOn) appDataRepository.appState.setLocalLogsEnabled(toggledOn)
if (!toggledOn) onLoggerStop() if (!toggledOn) {
_configurationChange.update { logReader.stop()
true
} }
} }
} }
private suspend fun onLoggerStop() {
logReader.deleteAndClearLogs()
}
fun onToggleAlwaysOnVPN() = viewModelScope.launch { fun onToggleAlwaysOnVPN() = viewModelScope.launch {
with(uiState.value.appSettings) { with(uiState.value.appSettings) {
appDataRepository.settings.save( appDataRepository.settings.save(
@@ -24,8 +24,6 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.toWgQuickString
import com.zaneschepke.wireguardautotunnel.util.extensions.withData import com.zaneschepke.wireguardautotunnel.util.extensions.withData
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.amnezia.awg.config.Config import org.amnezia.awg.config.Config
@@ -44,15 +42,6 @@ constructor(
appDataRepository: AppDataRepository, appDataRepository: AppDataRepository,
) : BaseViewModel(appDataRepository) { ) : BaseViewModel(appDataRepository) {
private val _activeTunnels = MutableStateFlow<List<TunnelConf>>(emptyList())
val activeTunnels = _activeTunnels.asStateFlow()
init {
viewModelScope.launch {
tunnelManager.activeTunnels().collect(_activeTunnels::emit)
}
}
fun onDelete(tunnel: TunnelConf) = viewModelScope.launch { fun onDelete(tunnel: TunnelConf) = viewModelScope.launch {
appSettings.withData { settings -> appSettings.withData { settings ->
tunnels.withData { tunnels.withData {
+146 -38
View File
@@ -1,43 +1,50 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name">WG Tunnel</string> <string name="app_name">WG Tunnel</string>
<string name="vpn_channel_id" translatable="false">VPN Channel</string>
<string name="vpn_channel_name">VPN Bildirim Kanalı</string> <string name="vpn_channel_name">VPN Bildirim Kanalı</string>
<string name="github_url" translatable="false">https://github.com/zaneschepke/wgtunnel/issues</string>
<string name="docs_url" translatable="false">https://zaneschepke.com/wgtunnel-docs/overview.html</string>
<string name="privacy_policy_url" translatable="false">https://zaneschepke.com/wgtunnel-docs/privacypolicy.html</string>
<string name="docs_wildcards" translatable="false">https://zaneschepke.com/wgtunnel-docs/features.html#wildcard-wi-fi-name-support</string>
<string name="donate_url" translatable="false">https://zaneschepke.com/donate/</string>
<string name="error_file_extension">Dosya .conf veya .zip değil</string> <string name="error_file_extension">Dosya .conf veya .zip değil</string>
<string name="turn_off_tunnel">İşlem için tünelin kapalı olması gerekiyor</string> <string name="turn_off_tunnel">Bu işlem tünelin kapalı olmasını gerektirir</string>
<string name="no_tunnels">Henüz tünel eklenmedi!</string> <string name="no_tunnels">Henüz tünel eklenmedi!</string>
<string name="tunnels">Tüneller</string> <string name="tunnels">Tüneller</string>
<string name="tunnel_mobile_data">Mobil veride tünel</string> <string name="tunnel_mobile_data">Mobil veride tünel</string>
<string name="privacy_policy">Gizlilik Politikasını Görüntüle</string> <string name="privacy_policy">Gizlilik politikasını görüntüle</string>
<string name="okay">Tamam</string> <string name="okay">Tamam</string>
<string name="tunnel_on_ethernet">Ethernet\'te tünel</string> <string name="tunnel_on_ethernet">Ethernet üzerinde tünel</string>
<string name="prominent_background_location_message">Bu özellik, uygulama kapalıyken bile Wi-Fi SSID izlemesini etkinleştirmek için arka plan konum iznine ihtiyaç duyar. Daha fazla ayrıntı için lütfen Destek ekranında bağlantısı verilen Gizlilik Politikasına bakın.</string> <string name="prominent_background_location_message">Bu özellik, uygulamanın kapalı olduğu durumlarda bile Wi-Fi SSID izlemesini etkinleştirmek için arka planda konum izni gerektirir. Daha fazla ayrıntı için lütfen Destek ekranında bağlantısı verilen Gizlilik Politikasına bakın.</string>
<string name="prominent_background_location_title">Arka Plan Konum Açıklaması</string> <string name="prominent_background_location_title">Arka Plan Konum Açıklaması</string>
<string name="thank_you">WG Tunnel\'ı kullandığınız için teşekkürler!</string> <string name="thank_you">WG Tunneli kullandığınız için teşekkürler!</string>
<string name="trusted_ssid_value_description">SSID\'yi gönder</string> <string name="trusted_ssid_value_description">SSID Gönder</string>
<string name="add_tunnels_text">Dosyadan veya zip\'ten ekle</string> <string name="add_tunnels_text">Dosyadan veya zipten ekle</string>
<string name="open_file">Dosya Aç</string> <string name="open_file">Dosya Aç</string>
<string name="add_from_qr">QR kodundan ekle</string> <string name="add_from_qr">QR kodundan ekle</string>
<string name="qr_scan">QR Tarama</string> <string name="qr_scan">QR Tara</string>
<string name="tunnel_name">Tünel Adı</string> <string name="tunnel_name">Tünel Adı</string>
<string name="exclude">Hariç tut</string> <string name="exclude">Hariç Tut</string>
<string name="include">Dahil et</string> <string name="include">Dahil Et</string>
<string name="config_changes_saved">Yapılandırma değişiklikleri kaydedildi.</string> <string name="config_changes_saved">Yapılandırma değişiklikleri kaydedildi.</string>
<string name="public_key">Genel anahtar</string> <string name="public_key">Genel anahtar</string>
<string name="addresses">Adresler</string> <string name="addresses">Adresler</string>
<string name="dns_servers">DNS sunucuları</string> <string name="dns_servers">DNS sunucuları</string>
<string name="mtu">MTU</string> <string name="mtu">MTU</string>
<string name="peer"> (peer)</string> <string name="peer"></string>
<string name="allowed_ips">İzin verilen IP\'ler</string> <string name="allowed_ips">İzin verilen IPler</string>
<string name="endpoint">nokta (endpoint)</string> <string name="endpoint">Nokta</string>
<string name="name">Ad</string> <string name="name">Ad</string>
<string name="always_on_vpn_support">Her Zaman Açık VPN\'e İzin Ver</string> <string name="always_on_vpn_support">Her Zaman Açık VPNe İzin Ver</string>
<string name="location_services_not_detected">Konum Hizmetleri Algılanmadı</string> <string name="location_services_not_detected">Konum Servisleri Algılanmadı</string>
<string name="hint_search_packages">Paketlerde ara</string> <string name="hint_search_packages">Paketleri ara</string>
<string name="db_name" translatable="false">wg-tunnel-db</string>
<string name="auto_tunneling">Otomatik tünelleme</string> <string name="auto_tunneling">Otomatik tünelleme</string>
<string name="vpn_on">VPN açık</string> <string name="vpn_on">VPN açık</string>
<string name="vpn_off">VPN kapalı</string> <string name="vpn_off">VPN kapalı</string>
<string name="create_import">Sıfırdan oluştur</string> <string name="create_import">Sıfırdan oluştur</string>
<string name="turn_on_tunnel">İşlem için aktif tünel gerekiyor</string> <string name="turn_on_tunnel">Bu işlem aktif bir tünel gerektirir</string>
<string name="add_peer">Eş ekle</string> <string name="add_peer">Eş ekle</string>
<string name="interface_">Arayüz</string> <string name="interface_">Arayüz</string>
<string name="rotate_keys">Anahtarları döndür</string> <string name="rotate_keys">Anahtarları döndür</string>
@@ -49,41 +56,42 @@
<string name="random">(rastgele)</string> <string name="random">(rastgele)</string>
<string name="optional">(isteğe bağlı)</string> <string name="optional">(isteğe bağlı)</string>
<string name="optional_no_recommend">(isteğe bağlı, önerilmez)</string> <string name="optional_no_recommend">(isteğe bağlı, önerilmez)</string>
<string name="preshared_key">Önceden paylaşılmış anahtar</string> <string name="preshared_key">Ön paylaşımlı anahtar</string>
<string name="seconds">saniye</string> <string name="seconds">saniye</string>
<string name="persistent_keepalive">Kalıcı canlı tutma</string> <string name="persistent_keepalive">Kalıcı canlı tutma</string>
<string name="cancel">İptal</string> <string name="cancel">İptal</string>
<string name="error_authentication_failed">Kimlik doğrulama başarısız oldu</string> <string name="error_authentication_failed">Kimlik doğrulama başarısız</string>
<string name="error_authorization_failed">Yetkilendirme başarısız oldu</string> <string name="error_authorization_failed">Yetkilendirme başarısız</string>
<string name="enabled_app_shortcuts">Uygulama kısayollarını etkinleştir</string> <string name="enabled_app_shortcuts">Uygulama kısayollarını etkinleştir</string>
<string name="export_configs">Yapılandırmaları dışa aktar</string> <string name="export_configs">Yapılandırmaları dışa aktar</string>
<string name="unknown_error">Bilinmeyen bir hata oluştu</string> <string name="unknown_error">Bilinmeyen bir hata oluştu</string>
<string name="tunnel_on_wifi">Güvenilmeyen wifi\'da tünel</string> <string name="tunnel_on_wifi">Güvenilmeyen wifida tünel</string>
<string name="my_email" translatable="false">support@zaneschepke.com</string>
<string name="email_subject">WG Tunnel Desteği</string> <string name="email_subject">WG Tunnel Desteği</string>
<string name="email_chooser">E-posta gönder…</string> <string name="email_chooser">E-posta gönder…</string>
<string name="docs_description">Belgeleri oku</string> <string name="docs_description">Belgeleri oku</string>
<string name="email_description">Bana e-posta gönder</string> <string name="email_description">Bana e-posta gönder</string>
<string name="use_kernel">Kernel modülünü kullan</string> <string name="use_kernel">Çekirdek modülünü kullan</string>
<string name="error_ssid_exists">SSID zaten mevcut</string> <string name="error_ssid_exists">SSID zaten mevcut</string>
<string name="error_root_denied">Root kabuğu reddedildi</string> <string name="error_root_denied">Root kabuğu reddedildi</string>
<string name="error_no_file_explorer">Dosya gezgini yüklü değil</string> <string name="error_no_file_explorer">Dosya gezgini yüklü değil</string>
<string name="error_invalid_code">Geçersiz QR kodu</string> <string name="error_invalid_code">Geçersiz QR kodu</string>
<string name="location_services_missing_message">Uygulama, cihazınızda etkinleştirilmiş herhangi bir konum hizmeti algılamıyor. Cihaza bağlı olarak, bu durum güvenilmeyen wifi özelliğinin wifi adını okumasını engelleyebilir. Yine de devam etmek istiyor musunuz?</string> <string name="location_services_missing_message">Uygulama, cihazınızda etkinleştirilmiş herhangi bir konum servisi algılamıyor. Cihaza bağlı olarak, bu durum güvenilmeyen wifi özelliğinin wifi adını okuyamamasını sağlayabilir. Yine de devam etmek ister misiniz?</string>
<string name="auto_tunnel_title">Otomatik Tünel Hizmeti</string> <string name="auto_tunnel_title">Otomatik tünel servisi</string>
<string name="delete_tunnel">Tüneli sil</string> <string name="delete_tunnel">Tüneli sil</string>
<string name="delete_tunnel_message">Bu tüneli silmek istediğinizden emin misiniz?</string> <string name="delete_tunnel_message">Bu tüneli silmek istediğinizden emin misiniz?</string>
<string name="yes">Evet</string> <string name="yes">Evet</string>
<string name="tunneling_apps">Tünellenen uygulamalar</string> <string name="tunneling_apps">Tünelleme uygulamaları</string>
<string name="all">tümü</string> <string name="all">tümü</string>
<string name="no_email_detected">E-posta uygulaması algılanmadı</string> <string name="no_email_detected">E-posta uygulaması algılanmadı</string>
<string name="no_browser_detected">Tarayıcı algılanmadı</string> <string name="no_browser_detected">Tarayıcı algılanmadı</string>
<string name="open_issue">Sorun bildir</string> <string name="open_issue">Bir sorun aç</string>
<string name="read_logs">Günlükleri oku</string> <string name="read_logs">Günlükleri oku</string>
<string name="auto">(otomatik)</string> <string name="auto">(otomatik)</string>
<string name="incorrect_pin">PIN yanlış</string> <string name="incorrect_pin">Pin yanlış</string>
<string name="pin_created">PIN başarıyla oluşturuldu</string> <string name="pin_created">Pin başarıyla oluşturuldu</string>
<string name="enter_pin">PIN\'inizi girin</string> <string name="enter_pin">Pininizi girin</string>
<string name="create_pin">PIN oluştur</string> <string name="create_pin">Pin oluştur</string>
<string name="enable_app_lock">Uygulama kilidini etkinleştir</string> <string name="enable_app_lock">Uygulama kilidini etkinleştir</string>
<string name="restart_on_ping">Ping başarısız olduğunda yeniden başlat (beta)</string> <string name="restart_on_ping">Ping başarısız olduğunda yeniden başlat (beta)</string>
<string name="mobile_data_tunnel">Mobil veri tüneli olarak ayarla</string> <string name="mobile_data_tunnel">Mobil veri tüneli olarak ayarla</string>
@@ -94,18 +102,118 @@
<string name="settings">Ayarlar</string> <string name="settings">Ayarlar</string>
<string name="support">Destek</string> <string name="support">Destek</string>
<string name="kernel">Çekirdek</string> <string name="kernel">Çekirdek</string>
<string name="junk_packet_count">Gereksiz paket sayısı</string> <string name="junk_packet_count">Çöp paket sayısı</string>
<string name="junk_packet_minimum_size">Gereksiz paket minimum boyutu</string> <string name="junk_packet_minimum_size">Çöp paket minimum boyutu</string>
<string name="junk_packet_maximum_size">Gereksiz paket maksimum boyutu</string> <string name="junk_packet_maximum_size">Çöp paket maksimum boyutu</string>
<string name="init_packet_junk_size">Başlatma paketi gereksiz boyutu</string> <string name="init_packet_junk_size">Başlangıç paketi çöp boyutu</string>
<string name="response_packet_junk_size">Yanıt paketi gereksiz boyutu</string> <string name="response_packet_junk_size">Yanıt paketi çöp boyutu</string>
<string name="init_packet_magic_header">Başlatma paketi sihirli başlığı</string> <string name="init_packet_magic_header">Başlangıç paketi sihirli başlığı</string>
<string name="response_packet_magic_header">Yanıt paketi sihirli başlığı</string> <string name="response_packet_magic_header">Yanıt paketi sihirli başlığı</string>
<string name="transport_packet_magic_header">Taşıma paketi sihirli başlığı</string> <string name="transport_packet_magic_header">Taşıma paketi sihirli başlığı</string>
<string name="underload_packet_magic_header">Düşük yük paketi sihirli başlığı</string> <string name="underload_packet_magic_header">Düşük yük paketi sihirli başlığı</string>
<string name="telegram_url" translatable="false">https://t.me/wgtunnel</string>
<string name="unsure_how">nasıl devam edeceğinizden emin değilseniz</string> <string name="unsure_how">nasıl devam edeceğinizden emin değilseniz</string>
<string name="see_the">Bakın:</string> <string name="see_the">Bakınız</string>
<string name="getting_started_url" translatable="false">https://zaneschepke.com/wgtunnel-docs/getting-started.html</string>
<string name="getting_started_guide">başlangıç kılavuzu</string> <string name="getting_started_guide">başlangıç kılavuzu</string>
<string name="error_file_format">Geçersiz tünel yapılandırma formatı</string> <string name="error_file_format">Geçersiz tünel yapılandırma formatı</string>
<string name="restart_at_boot">Önyüklemede yeniden başlat</string> <string name="restart_at_boot">Başlangıçta yeniden başlat</string>
<string name="vpn_denied_dialog_title">İzin Reddedildi</string>
<string name="vpn_settings">VPN sistem ayarları</string>
<string name="always_on_message">VPN bağlantı izni reddedildi. Lütfen</string>
<string name="always_on_message2">diğer tüm uygulamalar için Her Zaman Açık VPNin kapalı olduğundan emin olun ve tekrar deneyin</string>
<string name="chat_description">Topluluğa katıl</string>
<string name="tunnel_required">Bu özellik en az bir tünel gerektirir</string>
<string name="background_location_message">Bu özellik için her zaman konum izni ve/veya hassas konum gereklidir. Lütfen</string>
<string name="app_settings">uygulama ayarları</string>
<string name="background_location_message2">bu izinlerin etkin olduğundan emin olun</string>
<string name="root_accepted">Root kabuğu kabul edildi</string>
<string name="set_custom_ping_ip">Özel ping IPsi ayarla</string>
<string name="default_ping_ip">(isteğe bağlı, varsayılan eşler)</string>
<string name="set_custom_ping_internal">Ping aralığı (saniye)</string>
<string name="optional_default">"isteğe bağlı, varsayılan: "</string>
<string name="set_custom_ping_cooldown">Ping yeniden başlatma bekleme süresi (saniye)</string>
<string name="show_amnezia_properties">Amnezia özelliklerini göster</string>
<string name="never">asla</string>
<string name="sec">sn</string>
<string name="handshake">el sıkışma</string>
<string name="logs">Günlükler</string>
<string name="kill_switch">Kill Switch</string>
<string name="appearance">Görünüm</string>
<string name="notifications">Bildirimler</string>
<string name="automatic">Otomatik</string>
<string name="light">Açık</string>
<string name="dark">Koyu</string>
<string name="dynamic">Dinamik</string>
<string name="language">Dil</string>
<string name="display_theme">Ekran teması</string>
<string name="trusted_wifi_names">Güvenilir wifi adları</string>
<string name="add_wifi_name">Wifi adı ekle</string>
<string name="on_demand_rules">İsteğe bağlı tünel kuralları</string>
<string name="primary_tunnel">Birincil tünel</string>
<string name="mobile_tunnel">Mobil veri tüneli</string>
<string name="skip">Atla</string>
<string name="launch_app_settings">Uygulama ayarlarını başlat</string>
<string name="use_wildcards">İsim jokerlerini kullan</string>
<string name="learn_more">Daha fazla bilgi</string>
<string name="wildcards_active">Jokerler etkin</string>
<string name="wifi_name_via_shell">Kabuk üzerinden wifi adı</string>
<string name="use_root_shell_for_wifi">Wifi adını almak için root kabuğunu kullan</string>
<string name="kernel_not_supported">Çekirdek desteklenmiyor</string>
<string name="start_auto">Otomatik tüneli başlat</string>
<string name="stop_auto">Otomatik tüneli durdur</string>
<string name="tunnel_running">Tünel çalışıyor</string>
<string name="monitoring_state_changes">Durum değişikliklerini izleme</string>
<string name="donate">Projeye bağış yap</string>
<string name="local_logging">Yerel günlüğe kaydetme</string>
<string name="enable_local_logging">Yerel günlüğe kaydetmeyi etkinleştir</string>
<string name="configuration_change">Yapılandırma değişikliği</string>
<string name="requires_app_relaunch">Bu değişiklik uygulamanın yeniden başlatılmasını gerektirir. Devam etmek ister misiniz?</string>
<string name="add_from_clipboard">Panodan ekle</string>
<string name="stop_on_no_internet">İnternet olmadığında durdur</string>
<string name="stop_on_internet_loss">İnternet kaybında tüneli durdur</string>
<string name="ethernet_tunnel">Ethernet tüneli</string>
<string name="set_ethernet_tunnel">Ethernet tüneli olarak ayarla</string>
<string name="native_kill_switch">Yerel kill switch</string>
<string name="vpn_kill_switch">VPN kill switch</string>
<string name="kill_switch_options">Kill switch seçenekleri</string>
<string name="allow_lan_traffic">LAN trafiğine izin ver</string>
<string name="bypass_lan_for_kill_switch">Kill switch için LAN’ı atla</string>
<string name="vpn_channel_description">VPN durum bildirimleri için bir kanal</string>
<string name="auto_tunnel_channel_id" translatable="false">Auto-tunnel Channel</string>
<string name="auto_tunnel_channel_name">Otomatik Tünel Bildirim Kanalı</string>
<string name="auto_tunnel_channel_description">Otomatik tünel durum bildirimleri için bir kanal</string>
<string name="stop">durdur</string>
<string name="splt_tunneling">Bölünmüş tünelleme</string>
<string name="tunnel_specific_settings">Tünele özgü ayarlar</string>
<string name="show_scripts">Komut dosyalarını göster</string>
<string name="pre_up">Ön çalıştırma</string>
<string name="post_up">Sonra çalıştırma</string>
<string name="pre_down">Ön kapatma</string>
<string name="post_down">Sonra kapatma</string>
<string name="amnezia_kernel_message">Amnezia çekirdek modunda kullanılamaz</string>
<string name="enable_amnezia">Amneziayı etkinleştir</string>
<string name="wg_compat_mode">WG uyumluluk modu</string>
<string name="quick_actions">Hızlı eylemler</string>
<string name="advanced_settings">Gelişmiş ayarlar</string>
<string name="debounce_delay">Gecikme süresi</string>
<string name="hide_amnezia_properties">Amnezia özelliklerini gizle</string>
<string name="hide_scripts">Komut dosyalarını gizle</string>
<string name="enable_amnezia_compatibility">Amnezia uyumluluğunu etkinleştir</string>
<string name="remove_amnezia_compatibility">Amnezia uyumluluğunu kaldır</string>
<string name="exclude_lan">LAN’ı hariç tut</string>
<string name="include_lan">LAN’ı dahil et</string>
<string name="error_tunnel_start">Tünel başlatma başarısız</string>
<string name="tunnel_control">Tünel kontrolü</string>
<string name="auto_tunnel">Otomatik tünel</string>
<string name="kill_switch_off">Güvenilirde kill switchi durdur</string>
<string name="server_ipv4">IPv4 ana makine çözünürlüğü</string>
<string name="prefer_ipv4">IPv4 bağlantısını tercih et</string>
<string name="dns_error">Uç nokta DNSsi çözülemedi.</string>
<string name="start_failed_config">Yapılandırma hatası nedeniyle tünel başlatılamadı.</string>
<string name="unauthorized">Yetkisiz, tünel başlatılamadı.</string>
<string name="tunne_start_failed_title">Tünel hatası</string>
<string name="multiple">Çoklu</string>
<string name="export_amnezia">Amnezia olarak dışa aktar</string>
<string name="export_wireguard">WireGuard olarak dışa aktar</string>
</resources> </resources>
+2 -2
View File
@@ -1,7 +1,7 @@
object Constants { object Constants {
const val VERSION_NAME = "3.6.6" const val VERSION_NAME = "3.7.0"
const val JVM_TARGET = "17" const val JVM_TARGET = "17"
const val VERSION_CODE = 36600 const val VERSION_CODE = 37000
const val TARGET_SDK = 35 const val TARGET_SDK = 35
const val MIN_SDK = 26 const val MIN_SDK = 26
const val APP_ID = "com.zaneschepke.wireguardautotunnel" const val APP_ID = "com.zaneschepke.wireguardautotunnel"
@@ -0,0 +1,6 @@
What's new:
- Multiple tunnel support for kernel mode
- Override for WG default DNS Ipv4 preference
- Stop kill switch on trusted support
- Limit location querying by auto tunnel
- Various bug fixes and improvements
+9 -9
View File
@@ -1,13 +1,13 @@
[versions] [versions]
accompanist = "0.37.0" accompanist = "0.37.2"
activityCompose = "1.10.0" activityCompose = "1.10.1"
amneziawgAndroid = "1.2.6" amneziawgAndroid = "1.3.0"
androidx-junit = "1.2.1" androidx-junit = "1.2.1"
appcompat = "1.7.0" appcompat = "1.7.0"
biometricKtx = "1.2.0-alpha05" biometricKtx = "1.2.0-alpha05"
coreKtx = "1.15.0" coreKtx = "1.15.0"
datastorePreferences = "1.1.2" datastorePreferences = "1.1.3"
desugar_jdk_libs = "2.1.4" desugar_jdk_libs = "2.1.5"
espressoCore = "3.6.1" espressoCore = "3.6.1"
hiltAndroid = "2.55" hiltAndroid = "2.55"
hiltCompiler = "1.2.0" hiltCompiler = "1.2.0"
@@ -15,14 +15,14 @@ junit = "4.13.2"
kotlinx-serialization-json = "1.8.0" kotlinx-serialization-json = "1.8.0"
lifecycle-runtime-compose = "2.8.7" lifecycle-runtime-compose = "2.8.7"
material3 = "1.3.1" material3 = "1.3.1"
navigationCompose = "2.8.7" navigationCompose = "2.8.8"
pinLockCompose = "1.0.4" pinLockCompose = "1.0.4"
roomVersion = "2.6.1" roomVersion = "2.6.1"
timber = "5.0.1" timber = "5.0.1"
tunnel = "1.2.2" tunnel = "1.2.6"
androidGradlePlugin = "8.8.0-alpha05" androidGradlePlugin = "8.8.0-alpha05"
kotlin = "2.1.10" kotlin = "2.1.10"
ksp = "2.1.10-1.0.30" ksp = "2.1.10-1.0.31"
composeBom = "2025.02.00" composeBom = "2025.02.00"
compose = "1.7.8" compose = "1.7.8"
workRuntimeKtxVersion = "2.10.0" workRuntimeKtxVersion = "2.10.0"
@@ -32,7 +32,7 @@ gradlePlugins-grgit = "5.3.0"
#plugins #plugins
material = "1.12.0" material = "1.12.0"
gradlePlugins-ktlint="12.1.2" gradlePlugins-ktlint="12.2.0"
[libraries] [libraries]
@@ -0,0 +1,90 @@
package com.zaneschepke.logcatter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.BufferedOutputStream
import java.io.File
import java.io.FileOutputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
class LogFileManager(
private val logDir: String,
private val maxFileSize: Long,
private val maxFolderSize: Long,
) {
private var currentFile: File? = null
private var outputStream: FileOutputStream? = null
val ioDispatcher = Dispatchers.IO
init {
rotateIfNeeded()
}
suspend fun writeLog(line: String) = withContext(ioDispatcher) {
rotateIfNeeded()
outputStream?.write((line + System.lineSeparator()).toByteArray())
outputStream?.flush()
}
suspend fun zipLogs(zipFilePath: String) = withContext(ioDispatcher) {
outputStream?.close()
val sourceDir = File(logDir)
if (!sourceDir.exists() || !sourceDir.isDirectory) return@withContext
val outputZipFile = File(zipFilePath)
ZipOutputStream(BufferedOutputStream(FileOutputStream(outputZipFile))).use { zos ->
sourceDir.walkTopDown().forEach { file ->
val zipFileName = file.absolutePath.removePrefix(sourceDir.absolutePath).removePrefix("/")
val entry = ZipEntry("$zipFileName${if (file.isDirectory) "/" else ""}")
zos.putNextEntry(entry)
if (file.isFile) {
file.inputStream().use { it.copyTo(zos) }
}
}
}
rotateIfNeeded()
}
suspend fun deleteAllLogs() = withContext(ioDispatcher) {
outputStream?.close()
File(logDir).listFiles()?.forEach { it.delete() }
rotateIfNeeded()
}
fun close() {
outputStream?.close()
outputStream = null
currentFile = null
}
private fun rotateIfNeeded() {
val folderSize = getFolderSize(File(logDir))
if (folderSize >= maxFolderSize) {
deleteOldestFile()
}
val fileSize = currentFile?.length() ?: 0L
if (currentFile == null || fileSize >= maxFileSize) {
outputStream?.close()
currentFile = File(logDir, "logcat_${System.currentTimeMillis()}.txt")
outputStream = FileOutputStream(currentFile!!)
}
}
private fun getFolderSize(dir: File): Long {
var size = 0L
if (dir.isDirectory && dir.listFiles() != null) {
dir.listFiles()!!.forEach { file ->
size += if (file.isDirectory) getFolderSize(file) else file.length()
}
}
return size
}
private fun deleteOldestFile() {
File(logDir).listFiles()
?.toList()
?.minByOrNull { it.lastModified() }
?.delete()
}
}
@@ -4,7 +4,8 @@ import com.zaneschepke.logcatter.model.LogMessage
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface LogReader { interface LogReader {
fun initialize(onLogMessage: ((message: LogMessage) -> Unit)? = null) fun start()
fun stop()
fun zipLogFiles(path: String) fun zipLogFiles(path: String)
suspend fun deleteAndClearLogs() suspend fun deleteAndClearLogs()
val bufferedLogs: Flow<LogMessage> val bufferedLogs: Flow<LogMessage>
@@ -0,0 +1,92 @@
package com.zaneschepke.logcatter
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import com.zaneschepke.logcatter.model.LogMessage
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
class LogcatManager(
pid: Int,
logDir: String,
maxFileSize: Long,
maxFolderSize: Long,
) : LogReader, DefaultLifecycleObserver {
private val logScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val fileManager = LogFileManager(logDir, maxFileSize, maxFolderSize)
private val logcatReader = LogcatStreamReader(pid, fileManager)
private var logJob: Job? = null
private var isStarted = false
private val _bufferedLogs = MutableSharedFlow<LogMessage>(
replay = 10_000,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
private val _liveLogs = MutableSharedFlow<LogMessage>(
replay = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
override val bufferedLogs: Flow<LogMessage> = _bufferedLogs.asSharedFlow()
override val liveLogs: Flow<LogMessage> = _liveLogs.asSharedFlow()
override fun onCreate(owner: LifecycleOwner) {
// for auto start
// start()
}
override fun onDestroy(owner: LifecycleOwner) {
stop()
logScope.cancel()
}
override fun start() {
if (isStarted) return
stop()
logJob = logScope.launch {
logcatReader.readLogs().collect { logMessage ->
_bufferedLogs.emit(logMessage)
_liveLogs.emit(logMessage)
}
}
isStarted = true
}
override fun stop() {
if (!isStarted) return
logJob?.cancel()
logcatReader.stop()
fileManager.close()
isStarted = false
}
override fun zipLogFiles(path: String) {
logScope.launch {
val wasStarted = isStarted
stop()
fileManager.zipLogs(path)
if (wasStarted) {
logcatReader.clearLogs()
start()
}
}
}
@OptIn(ExperimentalCoroutinesApi::class)
override suspend fun deleteAndClearLogs() {
val wasStarted = isStarted
stop()
_bufferedLogs.resetReplayCache()
fileManager.deleteAllLogs()
if (wasStarted) start()
}
}
@@ -1,250 +1,32 @@
package com.zaneschepke.logcatter package com.zaneschepke.logcatter
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.zaneschepke.logcatter.model.LogMessage
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.BufferedOutputStream
import java.io.BufferedReader
import java.io.File import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStreamReader
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
object LogcatReader { object LogcatReader {
private const val MAX_FILE_SIZE = 2097152L // 2MB private const val MAX_FILE_SIZE = 2097152L // 2MB
private const val MAX_FOLDER_SIZE = 10485760L // 10MB private const val MAX_FOLDER_SIZE = 10485760L // 10MB
private val findKeyRegex = """[A-Za-z0-9+/]{42}[AEIMQUYcgkosw480]=""".toRegex() private lateinit var logcatManager: LogcatManager
private val findIpv6AddressRegex = """^([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}${'$'}""".toRegex() private var isInitialized = false
private val findIpv4AddressRegex = """((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}""".toRegex()
private val findTunnelNameRegex = """(?<=tunnel ).*?(?= UP| DOWN)""".toRegex()
private val ioDispatcher = Dispatchers.IO
private object LogcatHelperInit {
var maxFileSize: Long = MAX_FILE_SIZE
var maxFolderSize: Long = MAX_FOLDER_SIZE
var pID: Int = 0
var publicAppDirectory = ""
var logcatPath = ""
}
fun init(maxFileSize: Long = MAX_FILE_SIZE, maxFolderSize: Long = MAX_FOLDER_SIZE, storageDir: String): LogReader { fun init(maxFileSize: Long = MAX_FILE_SIZE, maxFolderSize: Long = MAX_FOLDER_SIZE, storageDir: String): LogReader {
if (maxFileSize > maxFolderSize) { if (maxFileSize > maxFolderSize) {
throw IllegalStateException("maxFileSize must be less than maxFolderSize") throw IllegalStateException("maxFileSize must be less than maxFolderSize")
} }
synchronized(LogcatHelperInit) { synchronized(this) {
LogcatHelperInit.maxFileSize = maxFileSize if (isInitialized) return logcatManager
LogcatHelperInit.maxFolderSize = maxFolderSize val logDir = "$storageDir${File.separator}logs"
LogcatHelperInit.pID = android.os.Process.myPid() File(logDir).mkdirs()
LogcatHelperInit.publicAppDirectory = storageDir logcatManager = LogcatManager(
LogcatHelperInit.logcatPath = LogcatHelperInit.publicAppDirectory + File.separator + "logs" pid = android.os.Process.myPid(),
val logDirectory = File(LogcatHelperInit.logcatPath) logDir = logDir,
if (!logDirectory.exists()) { maxFileSize = maxFileSize,
logDirectory.mkdir() maxFolderSize = maxFolderSize,
}
return Logcat
}
}
internal object Logcat : LogReader {
private lateinit var logcatReader: LogcatReader
override fun initialize(onLogMessage: ((message: LogMessage) -> Unit)?) {
logcatReader = LogcatReader(LogcatHelperInit.pID.toString(), LogcatHelperInit.logcatPath, onLogMessage)
ProcessLifecycleOwner.get().lifecycle.addObserver(logcatReader)
}
private fun obfuscator(log: String): String {
return findKeyRegex.replace(log, "<crypto-key>").let { first ->
findIpv6AddressRegex.replace(first, "<ipv6-address>").let { second ->
findTunnelNameRegex.replace(second, "<tunnel>")
}
}.let { last -> findIpv4AddressRegex.replace(last, "<ipv4-address>") }
}
override fun zipLogFiles(path: String) {
logcatReader.cancel()
zipAll(path)
logcatReader.onCreate(ProcessLifecycleOwner.get())
}
private fun zipAll(zipFilePath: String) {
val sourceFile = File(LogcatHelperInit.logcatPath)
val outputZipFile = File(zipFilePath)
ZipOutputStream(BufferedOutputStream(FileOutputStream(outputZipFile))).use { zos ->
sourceFile.walkTopDown().forEach { file ->
val zipFileName = file.absolutePath.removePrefix(sourceFile.absolutePath).removePrefix("/")
val entry = ZipEntry("$zipFileName${(if (file.isDirectory) "/" else "")}")
zos.putNextEntry(entry)
if (file.isFile) {
file.inputStream().use {
it.copyTo(zos)
}
}
}
}
}
@OptIn(ExperimentalCoroutinesApi::class)
override suspend fun deleteAndClearLogs() {
withContext(ioDispatcher) {
logcatReader.cancel()
_bufferedLogs.resetReplayCache()
logcatReader.deleteAllFiles()
logcatReader.onCreate(ProcessLifecycleOwner.get())
}
}
private val _bufferedLogs = MutableSharedFlow<LogMessage>(
replay = 10_000,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
) )
private val _liveLogs = MutableSharedFlow<LogMessage>( ProcessLifecycleOwner.get().lifecycle.addObserver(logcatManager)
replay = 1, isInitialized = true
onBufferOverflow = BufferOverflow.DROP_OLDEST, return logcatManager
)
override val bufferedLogs: Flow<LogMessage> = _bufferedLogs.asSharedFlow()
override val liveLogs: Flow<LogMessage> = _liveLogs.asSharedFlow()
private class LogcatReader(
pID: String,
private val logcatPath: String,
private val callback: ((input: LogMessage) -> Unit)?,
) : DefaultLifecycleObserver {
private var logcatProc: Process? = null
private var reader: BufferedReader? = null
private val command = "logcat -v epoch | grep \"($pID)\""
private val clearLogCommand = "logcat -c"
private var logJob: Job? = null
private var outputStream: FileOutputStream? = null
override fun onCreate(owner: LifecycleOwner) {
super.onCreate(owner)
logJob = owner.lifecycleScope.launch(ioDispatcher) {
try {
if (outputStream == null) outputStream = createNewLogFileStream()
clear()
logcatProc = Runtime.getRuntime().exec(command)
reader = BufferedReader(InputStreamReader(logcatProc!!.inputStream), 1024)
var line: String? = null
while (true) {
line = reader?.readLine()
if (line.isNullOrEmpty()) continue
outputStream?.let {
if (it.channel.size() >= LogcatHelperInit.maxFileSize) {
it.close()
outputStream = createNewLogFileStream()
}
if (getFolderSize(logcatPath) >= LogcatHelperInit.maxFolderSize) {
deleteOldestFile()
}
line.let { text ->
val sanitized = obfuscator(text)
it.write((sanitized + System.lineSeparator()).toByteArray())
try {
val logMessage = LogMessage.from(text)
_bufferedLogs.tryEmit(logMessage)
_liveLogs.tryEmit(logMessage)
callback?.let {
it(logMessage)
}
} catch (e: Exception) {
Timber.e(e)
}
}
}
}
} catch (e: IOException) {
Timber.e(e)
} finally {
reset()
}
}
logJob?.invokeOnCompletion {
reset()
}
}
override fun onDestroy(owner: LifecycleOwner) {
super.onDestroy(owner)
logJob?.cancel()
}
fun cancel() {
logJob?.cancel()
}
private fun reset() {
logcatProc?.destroy()
logcatProc = null
reader?.close()
outputStream?.close()
reader = null
outputStream = null
}
fun clear() {
Runtime.getRuntime().exec(clearLogCommand)
}
private fun getFolderSize(path: String): Long {
File(path).run {
var size = 0L
if (this.isDirectory && this.listFiles() != null) {
for (file in this.listFiles()!!) {
size += getFolderSize(file.absolutePath)
}
} else {
size = this.length()
}
return size
}
}
private fun createLogFile(dir: String): File {
return File(dir, "logcat_" + System.currentTimeMillis() + ".txt")
}
fun deleteOldestFile() {
val directory = File(logcatPath)
if (directory.isDirectory) {
directory.listFiles()?.toMutableList()?.run {
this.sortBy { it.lastModified() }
this.first().delete()
}
}
}
private fun createNewLogFileStream(): FileOutputStream {
return FileOutputStream(createLogFile(logcatPath))
}
fun deleteAllFiles() {
val directory = File(logcatPath)
directory.listFiles()?.toMutableList()?.run {
this.forEach { it.delete() }
}
}
} }
} }
} }
@@ -0,0 +1,63 @@
package com.zaneschepke.logcatter
import com.zaneschepke.logcatter.model.LogMessage
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStreamReader
class LogcatStreamReader(
private val pid: Int,
private val fileManager: LogFileManager,
) {
private val bufferSize = 1024
private var process: Process? = null
private var reader: BufferedReader? = null
private val command = "logcat -v epoch | grep \"($pid)\""
private val clearCommand = "logcat -c"
private val ioDispatcher = Dispatchers.IO
fun readLogs(): Flow<LogMessage> = flow {
try {
clearLogs()
process = Runtime.getRuntime().exec(command)
reader = BufferedReader(InputStreamReader(process!!.inputStream), bufferSize)
reader!!.lineSequence().forEach { line ->
if (line.isNotEmpty()) {
fileManager.writeLog(line)
emit(LogMessage.from(line))
}
}
} catch (e: IOException) {
// do nothing
} finally {
stop()
}
}.flowOn(ioDispatcher)
fun start() {
if (process == null) {
try {
process = Runtime.getRuntime().exec(command)
reader = BufferedReader(InputStreamReader(process!!.inputStream), bufferSize)
} catch (e: IOException) {
// do nothing
}
}
}
fun stop() {
process?.destroy()
reader?.close()
process = null
reader = null
}
fun clearLogs() {
Runtime.getRuntime().exec(clearCommand)
}
}
+1
View File
@@ -0,0 +1 @@
/build
+51
View File
@@ -0,0 +1,51 @@
plugins {
alias(libs.plugins.androidLibrary)
alias(libs.plugins.kotlin.android)
}
android {
namespace = "com.zaneschepke.networkmonitor"
compileSdk = 34
defaultConfig {
minSdk = 26
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
create(Constants.PRERELEASE) {
initWith(getByName(Constants.RELEASE))
}
create(Constants.NIGHTLY) {
initWith(getByName(Constants.RELEASE))
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = Constants.JVM_TARGET
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
implementation(libs.tunnel)
implementation(libs.timber)
}
View File
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,24 @@
package com.zaneschepke.networkmonitor
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.zaneschepke.networkmonitor.test", appContext.packageName)
}
}
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
</manifest>
@@ -0,0 +1,151 @@
package com.zaneschepke.networkmonitor
import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.net.wifi.WifiManager
import com.wireguard.android.util.RootShell
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 timber.log.Timber
class AndroidNetworkMonitor(
context: Context,
) : NetworkMonitor {
private val appContext = context.applicationContext
private val connectivityManager = appContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
private val wifiManager = appContext.getSystemService(Context.WIFI_SERVICE) as WifiManager?
private val rootShell = RootShell(context)
private var includeWifiSsid = false
private var useRootShell = false
data class WifiState(val connected: Boolean = false, val ssid: String? = null)
data class TransportState(val connected: Boolean = false)
private val wifiFlow: Flow<WifiState> = callbackFlow {
var currentSsid: String? = null
@Suppress("DEPRECATION")
fun getWifiSsid(): String? {
if (!includeWifiSsid || wifiManager == null) return null
return if (useRootShell) {
rootShell.getCurrentWifiName()
} else {
wifiManager.connectionInfo?.ssid?.trim('"')?.takeIf {
it != "<unknown>" && it.isNotEmpty()
}
}
}
val callback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
Timber.d("Wi-Fi onAvailable: network=$network")
val capabilities = connectivityManager.getNetworkCapabilities(network)
val connected = capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true
if (connected) {
val ssid = getWifiSsid()
currentSsid = ssid
trySend(WifiState(connected = true, ssid = ssid))
}
}
override fun onLost(network: Network) {
Timber.d("Wi-Fi onLost: network=$network")
currentSsid = null
trySend(WifiState(connected = false, ssid = null))
}
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
val connected = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
val ssid = if (connected) getWifiSsid() else null
if (ssid != currentSsid) {
currentSsid = ssid
trySend(WifiState(connected = connected, ssid = ssid))
}
}
}
val request = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.build()
connectivityManager.registerNetworkCallback(request, callback)
trySend(WifiState())
awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
}
private val cellularFlow: Flow<TransportState> = callbackFlow {
val callback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
Timber.d("Cellular onAvailable: network=$network")
trySend(TransportState(connected = true))
}
override fun onLost(network: Network) {
Timber.d("Cellular onLost: network=$network")
trySend(TransportState(connected = false))
}
}
val request = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
.build()
connectivityManager.registerNetworkCallback(request, callback)
trySend(TransportState())
awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
}
private val ethernetFlow: Flow<TransportState> = callbackFlow {
val callback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
Timber.d("Ethernet onAvailable: network=$network")
trySend(TransportState(connected = true))
}
override fun onLost(network: Network) {
Timber.d("Ethernet onLost: network=$network")
trySend(TransportState(connected = false))
}
}
val request = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
.build()
connectivityManager.registerNetworkCallback(request, callback)
trySend(TransportState())
awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
}
override fun getNetworkStatusFlow(includeWifiSsid: Boolean, useRootShell: Boolean): Flow<NetworkStatus> {
this.includeWifiSsid = includeWifiSsid
this.useRootShell = useRootShell
return combine(wifiFlow, cellularFlow, ethernetFlow) { wifi, cellular, ethernet ->
val hasAnyConnection = wifi.connected || cellular.connected || ethernet.connected
if (hasAnyConnection) {
NetworkStatus.Connected(
wifiSsid = wifi.ssid,
wifiConnected = wifi.connected,
cellularConnected = cellular.connected,
ethernetConnected = ethernet.connected,
)
} else {
NetworkStatus.Disconnected
}.also { Timber.d("NetworkStatus: $it") }
}.distinctUntilChanged()
}
}
@@ -0,0 +1,9 @@
package com.zaneschepke.networkmonitor
import com.wireguard.android.util.RootShell
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()
}
@@ -0,0 +1,7 @@
package com.zaneschepke.networkmonitor
import kotlinx.coroutines.flow.Flow
interface NetworkMonitor {
fun getNetworkStatusFlow(includeWifiSsid: Boolean, useRootShell: Boolean): Flow<NetworkStatus>
}
@@ -0,0 +1,20 @@
package com.zaneschepke.networkmonitor
sealed class NetworkStatus {
data object Disconnected : NetworkStatus() {
override val wifiConnected = false
override val ethernetConnected = false
override val cellularConnected = false
}
data class Connected(
val wifiSsid: String? = null,
override val wifiConnected: Boolean = false,
override val ethernetConnected: Boolean = false,
override val cellularConnected: Boolean = false,
) : NetworkStatus()
abstract val wifiConnected: Boolean
abstract val ethernetConnected: Boolean
abstract val cellularConnected: Boolean
}
@@ -0,0 +1,17 @@
package com.zaneschepke.networkmonitor
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}
+1
View File
@@ -34,3 +34,4 @@ rootProject.name = "WG Tunnel"
include(":app") include(":app")
include(":logcatter") include(":logcatter")
include(":networkmonitor")
+1 -1
View File
@@ -1 +1 @@
1 4