Compare commits

..

1 Commits

Author SHA1 Message Date
Zane Schepke 594ed85a71 start sticky 2025-02-22 07:52:16 -05:00
62 changed files with 1202 additions and 1518 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,40 +3,3 @@
-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,40 +22,3 @@
-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
+18 -5
View File
@@ -3,8 +3,13 @@
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.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<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"
@@ -163,17 +168,25 @@
</service> </service>
<receiver <receiver
android:name=".core.broadcast.RestartReceiver" android:name=".core.broadcast.BootReceiver"
android:enabled="true" android:enabled="true"
android:exported="true"> android:exported="false">
<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" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver
android:name=".core.broadcast.AppUpdateReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
</receiver>
<receiver <receiver
android:name=".core.broadcast.KernelReceiver" android:name=".core.broadcast.KernelReceiver"
android:exported="false" android:exported="false"
@@ -40,6 +40,7 @@ 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
@@ -66,6 +67,7 @@ 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
@@ -119,13 +121,22 @@ 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) {
@@ -210,17 +221,16 @@ class MainActivity : AppCompatActivity() {
composable<Route.Logs> { composable<Route.Logs> {
LogsScreen() LogsScreen()
} }
composable<Route.Config> { backStack -> composable<Route.Config> {
val args = backStack.toRoute<Route.Config>() val args = it.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> { backStack -> composable<Route.TunnelOptions> {
val args = backStack.toRoute<Route.TunnelOptions>() val args = it.toRoute<Route.TunnelOptions>()
appUiState.tunnels.firstOrNull { it.id == args.id }?.let { config -> val config = appUiState.tunnels.first { it.id == args.id }
OptionsScreen(config, appUiState) OptionsScreen(config)
}
} }
composable<Route.Lock> { composable<Route.Lock> {
PinLockScreen(viewModel) PinLockScreen(viewModel)
@@ -231,17 +241,15 @@ class MainActivity : AppCompatActivity() {
composable<Route.KillSwitch> { composable<Route.KillSwitch> {
KillSwitchScreen(appUiState, viewModel) KillSwitchScreen(appUiState, viewModel)
} }
composable<Route.SplitTunnel> { backStack -> composable<Route.SplitTunnel> {
val args = backStack.toRoute<Route.SplitTunnel>() val args = it.toRoute<Route.SplitTunnel>()
appUiState.tunnels.firstOrNull { it.id == args.id }?.let { val config = appUiState.tunnels.first { it.id == args.id }
SplitTunnelScreen(it, viewModel) SplitTunnelScreen(config, viewModel)
}
} }
composable<Route.TunnelAutoTunnel> { backStack -> composable<Route.TunnelAutoTunnel> {
val args = backStack.toRoute<Route.TunnelOptions>() val args = it.toRoute<Route.TunnelOptions>()
appUiState.tunnels.firstOrNull { it.id == args.id }?.let { val config = appUiState.tunnels.first { it.id == args.id }
TunnelAutoTunnelScreen(it, appUiState.appSettings) TunnelAutoTunnelScreen(config, appUiState.appSettings)
}
} }
} }
BackHandler { BackHandler {
@@ -11,7 +11,6 @@ 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
@@ -92,11 +91,9 @@ class WireGuardAutoTunnel : Application(), Configuration.Provider {
} }
} }
ServiceWorker.start(this)
applicationScope.launch { applicationScope.launch {
withContext(mainDispatcher) { withContext(mainDispatcher) {
if (appDataRepository.appState.isLocalLogsEnabled() && !isRunningOnTv()) logReader.start() if (appDataRepository.appState.isLocalLogsEnabled() && !isRunningOnTv()) logReader.initialize()
} }
if (!appDataRepository.settings.get().isKernelEnabled) { if (!appDataRepository.settings.get().isKernelEnabled) {
tunnelManager.setBackendState(BackendState.SERVICE_ACTIVE, emptyList()) tunnelManager.setBackendState(BackendState.SERVICE_ACTIVE, emptyList())
@@ -0,0 +1,45 @@
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()
}
}
}
}
}
@@ -0,0 +1,44 @@
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 { name -> tunnelManager.runningTunnelNames().forEach {
val tunnel = tunnelRepository.findByTunnelName(name) val tunnel = tunnelRepository.findByTunnelName(it)
tunnel?.let { tunnel?.let {
tunnelRepository.save(it.copy(isActive = true)) tunnelRepository.save(it.copy(isActive = true))
} }
@@ -1,64 +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 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")
}
}
}
}
@@ -0,0 +1,161 @@
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('"')
}
}
}
@@ -0,0 +1,16 @@
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
}
@@ -0,0 +1,13 @@
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,35 +3,29 @@ 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.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi
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
class ServiceManager @Inject constructor( @OptIn(ExperimentalCoroutinesApi::class)
private val context: Context, class ServiceManager
@IoDispatcher private val ioDispatcher: CoroutineDispatcher, @Inject constructor(private val context: Context, private val ioDispatcher: CoroutineDispatcher, private val appDataRepository: AppDataRepository) {
@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>()
@@ -50,111 +44,76 @@ class ServiceManager @Inject constructor(
}.onFailure { Timber.e(it) } }.onFailure { Timber.e(it) }
} }
fun startAutoTunnel(background: Boolean) { suspend 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) { runCatching {
_autoTunnelActive.update { true } startService(AutoTunnelService::class.java, background)
return@launch autoTunnelService.await()
} autoTunnelService.getCompleted().start()
runCatching { _autoTunnelActive.update { true }
autoTunnelService = CompletableDeferred() updateAutoTunnelTile()
startService(AutoTunnelService::class.java, background) }.onFailure {
val service = withTimeoutOrNull(SERVICE_START_TIMEOUT) { autoTunnelService.await() } Timber.e(it)
?: throw IllegalStateException("AutoTunnelService start timed out")
service.start()
_autoTunnelActive.update { true }
updateAutoTunnelTile()
}.onFailure {
Timber.e(it)
_autoTunnelActive.update { false }
}
} }
} }
fun startBackgroundService(tunnelConf: TunnelConf) { suspend fun startBackgroundService(tunnelConf: TunnelConf) {
applicationScope.launch(ioDispatcher) { if (backgroundService.isCompleted) return
if (backgroundService.isCompleted) return@launch runCatching {
runCatching { startService(TunnelForegroundService::class.java, true)
backgroundService = CompletableDeferred() backgroundService.await()
startService(TunnelForegroundService::class.java, true) backgroundService.getCompleted().start(tunnelConf)
val service = withTimeoutOrNull(SERVICE_START_TIMEOUT) { backgroundService.await() } }.onFailure {
?: throw IllegalStateException("Background service start timed out") Timber.e(it)
service.start(tunnelConf)
}.onFailure {
Timber.e(it)
}
} }
} }
fun stopBackgroundService() { fun stopBackgroundService() {
applicationScope.launch(ioDispatcher) { if (!backgroundService.isCompleted) return
if (!backgroundService.isCompleted) return@launch runCatching {
runCatching { backgroundService.getCompleted().stop()
val service = backgroundService.await() }.onFailure {
service.stop() Timber.e(it)
backgroundService = CompletableDeferred()
}.onFailure {
Timber.e(it)
}
} }
} }
fun toggleAutoTunnel(background: Boolean) { suspend fun toggleAutoTunnel(background: Boolean) {
applicationScope.launch(ioDispatcher) {
if (_autoTunnelActive.value) stopAutoTunnel() else startAutoTunnel(background)
}
}
suspend fun updateAutoTunnelTile() {
withContext(ioDispatcher) { withContext(ioDispatcher) {
runCatching { if (_autoTunnelActive.value) return@withContext stopAutoTunnel()
val service = withTimeoutOrNull(SERVICE_START_TIMEOUT) { autoTunnelTile.await() } startAutoTunnel(background)
?: run {
context.requestAutoTunnelTileServiceUpdate()
return@withContext
}
service.updateTileState()
}.onFailure {
Timber.e(it)
}
} }
} }
suspend fun updateTunnelTile() { fun updateAutoTunnelTile() {
if (autoTunnelTile.isCompleted) {
autoTunnelTile.getCompleted().updateTileState()
} else {
context.requestAutoTunnelTileServiceUpdate()
}
}
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@launch if (!autoTunnelService.isCompleted) return@withContext
runCatching { runCatching {
val service = autoTunnelService.await() autoTunnelService.getCompleted().stop()
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,8 +35,9 @@ 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 super.onStartCommand(intent, flags, startId) return START_NOT_STICKY
} }
fun start(tunnelConf: TunnelConf) { fun start(tunnelConf: TunnelConf) {
@@ -1,42 +1,44 @@
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.zaneschepke.networkmonitor.NetworkMonitor import com.wireguard.android.util.RootShell
import com.zaneschepke.networkmonitor.NetworkStatus
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification import com.zaneschepke.wireguardautotunnel.di.AppShell
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.di.MainImmediateDispatcher import com.zaneschepke.wireguardautotunnel.di.MainImmediateDispatcher
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
import com.zaneschepke.wireguardautotunnel.domain.events.KillSwitchEvent
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.state.AutoTunnelState import com.zaneschepke.wireguardautotunnel.domain.state.AutoTunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.NetworkState import com.zaneschepke.wireguardautotunnel.domain.state.NetworkState
import com.zaneschepke.wireguardautotunnel.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.WireGuardNotification
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
import com.zaneschepke.wireguardautotunnel.domain.events.KillSwitchEvent
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
@@ -47,6 +49,10 @@ 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
@@ -94,9 +100,10 @@ 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 super.onStartCommand(intent, flags, startId) return START_NOT_STICKY
} }
fun start() { fun start() {
@@ -155,54 +162,49 @@ class AutoTunnelService : LifecycleService() {
} }
} }
private fun buildNetworkState(networkStatus: NetworkStatus): NetworkState { private suspend fun buildNetworkState(connectivityState: ConnectivityState): NetworkState {
return with(autoTunnelStateFlow.value.networkState) { return with(autoTunnelStateFlow.value.networkState) {
val wifiName = when (networkStatus) { val wifiName = when {
is NetworkStatus.Connected -> { connectivityState.wifiAvailable &&
networkStatus.wifiSsid (wifiName == null || wifiName == Constants.UNREADABLE_SSID || networkMonitor.didWifiChangeSinceLastCapabilitiesQuery) -> {
networkMonitor.getWifiCapabilities()?.let { getWifiName(it) } ?: wifiName
} }
else -> null !connectivityState.wifiAvailable -> null
else -> wifiName
} }
copy( copy(
isWifiConnected = networkStatus.wifiConnected, isWifiConnected = connectivityState.wifiAvailable,
isMobileDataConnected = networkStatus.cellularConnected, isMobileDataConnected = connectivityState.cellularAvailable,
isEthernetConnected = networkStatus.ethernetConnected, isEthernetConnected = isEthernetConnected,
wifiName = wifiName, wifiName = wifiName,
) )
} }
} }
@OptIn(ExperimentalCoroutinesApi::class)
private fun startAutoTunnelStateJob() = lifecycleScope.launch(ioDispatcher) { private fun startAutoTunnelStateJob() = lifecycleScope.launch(ioDispatcher) {
combine( combine(
combineSettings(), combineSettings(),
appDataRepository.get().settings.flow networkMonitor.status.map {
.distinctUntilChanged { old, new -> old.isKernelEnabled == new.isKernelEnabled } // Only emit when isKernelEnabled changes buildNetworkState(it)
.flatMapLatest { settings -> }.distinctUntilChanged(),
networkMonitor.getNetworkStatusFlow(true, settings.isKernelEnabled)
.flowOn(ioDispatcher)
.map { buildNetworkState(it) }
}
.distinctUntilChanged(),
) { double, networkState -> ) { double, networkState ->
AutoTunnelState( AutoTunnelState(tunnelManager.activeTunnels().value, networkState, double.first, double.second)
tunnelManager.activeTunnels.value,
networkState,
double.first,
double.second,
)
}.collect { state -> }.collect { state ->
autoTunnelStateFlow.update { autoTunnelStateFlow.update {
it.copy( it.copy(activeTunnels = state.activeTunnels, networkState = state.networkState, settings = state.settings, tunnels = state.tunnels)
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,
@@ -239,7 +241,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 ${watcherState.networkState}") Timber.d("New auto tunnel state emitted")
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,14 +52,9 @@ class TunnelControlTile : TileService() {
} }
fun updateTileState() = applicationScope.launch { fun updateTileState() = applicationScope.launch {
val tunnels = appDataRepository.tunnels.getAll() if (appDataRepository.tunnels.getAll().isEmpty()) return@launch setUnavailable()
if (tunnels.isEmpty()) return@launch setUnavailable() with(tunnelManager.activeTunnels().value) {
with(tunnelManager.activeTunnels.value) { if (isNotEmpty()) return@launch updateTile(if (size == 1) first().tunName else getString(R.string.multiple), true)
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)
@@ -70,7 +65,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,10 +1,8 @@
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
@@ -16,12 +14,10 @@ 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
@@ -31,12 +27,11 @@ 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.flowOn import kotlinx.coroutines.flow.distinctUntilChanged
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(
@@ -50,98 +45,79 @@ open class BaseTunnel(
internal val tunnels = MutableStateFlow<List<TunnelConf>>(emptyList()) internal val tunnels = MutableStateFlow<List<TunnelConf>>(emptyList())
private val _tunnelStates = MutableStateFlow<Map<Int, TunnelState>>(emptyMap()) private val tunnelJobs = mutableMapOf<TunnelConf, Job>()
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 { startNetworkJob() } launch {
launch { monitorTunnelConfigChanges() } startNetworkJob()
}
tunnels.collect { tuns -> tunnels.collect { tuns ->
val previousTunIds = tunnelJobs.keys.toSet() val previousTuns = tunnelJobs.keys.toSet()
val currentTunIds = tuns.map { it.id }.toSet() val newTuns = tuns - previousTuns
val newTuns = tuns.filter { it.id !in previousTunIds } val removedItems = previousTuns - tuns.toSet()
val removedTunIds = previousTunIds - currentTunIds
newTuns.forEach { tun -> newTuns.forEach { tun ->
Timber.d("Starting tunnel jobs for tun ${tun.name} (ID: ${tun.id})") Timber.d("Starting tunnel jobs for tun ${tun.name}")
tunnelJobs[tun.id] = startTunnelJobs(tun) tunnelJobs[tun] = startTunnelJobs(tun)
} }
removedTunIds.forEach { tunId -> removedItems.forEach { tun ->
tunnelJobs[tunId]?.cancelWithMessage("Canceling tunnel jobs for tunnel ID: $tunId") tunnelJobs[tun]?.cancelWithMessage("Canceling tunnel jobs for tunnel: ${tun.name}")
tunnelJobs.remove(tunId) tunnelJobs.remove(tun)
_tunnelStates.update { it - tunId }
serviceManager.updateTunnelTile()
} }
}
}
}
private fun startTunnelJobs(tunnel: TunnelConf) = applicationScope.launch(ioDispatcher) {
launch { startTunnelStatisticsJob(tunnel) }
if (tunnel.isPingEnabled) 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
}
}
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() serviceManager.updateTunnelTile()
} }
} }
} }
override fun startTunnel(tunnelConf: TunnelConf) { private fun startTunnelJobs(tunnel: TunnelConf) = applicationScope.launch(ioDispatcher) {
applicationScope.launch(ioDispatcher) { launch {
serviceManager.startBackgroundService(tunnelConf) startTunnelStatisticsJob(tunnel)
appDataRepository.tunnels.save(tunnelConf.copy(isActive = true)) }
addToActiveTunnels(tunnelConf) launch {
startPingJob(tunnel)
}
launch {
startTunnelConfigChangeJob(tunnel)
} }
} }
override fun stopTunnel(tunnelConf: TunnelConf?) {
// Default empty implementation; subclasses override
}
override suspend fun bounceTunnel(tunnelConf: TunnelConf) { override suspend fun bounceTunnel(tunnelConf: TunnelConf) {
stopTunnel(tunnelConf) if (tunnels.value.any { it.id == tunnelConf.id }) {
delay(1000) toggleTunnel(tunnelConf, TunnelStatus.DOWN)
startTunnel(tunnelConf) toggleTunnel(tunnelConf, TunnelStatus.UP)
}
} }
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 fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics { override suspend fun activeTunnels(): StateFlow<List<TunnelConf>> {
throw NotImplementedError("Get statistics not implemented in base class") return tunnels.asStateFlow()
} }
override val activeTunnels: StateFlow<Map<Int, TunnelState>> override suspend fun startTunnel(tunnelConf: TunnelConf) {
get() = _tunnelStates.asStateFlow() 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")
}
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))
@@ -149,13 +125,13 @@ open class BaseTunnel(
if (tunnels.value.isEmpty()) serviceManager.stopBackgroundService() if (tunnels.value.isEmpty()) serviceManager.stopBackgroundService()
} }
internal fun stopAllTunnels() { internal suspend fun stopAllTunnels() {
tunnels.value.forEach { tunnels.value.forEach {
stopTunnel(it) stopTunnel(it)
} }
} }
private fun addToActiveTunnels(conf: TunnelConf) { internal fun addToActiveTunnels(conf: TunnelConf) {
tunnels.update { tunnels.update {
it.toMutableList().apply { it.toMutableList().apply {
add(conf) add(conf)
@@ -172,26 +148,23 @@ open class BaseTunnel(
} }
private suspend fun startNetworkJob() = coroutineScope { private suspend fun startNetworkJob() = coroutineScope {
networkMonitor.getNetworkStatusFlow(includeWifiSsid = false, useRootShell = false) networkMonitor.status.distinctUntilChanged().collect {
.flowOn(ioDispatcher).collect { isNetworkAvailable.set(!it.allOffline)
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, pingSuccess: Boolean) { private suspend fun handlePingResult(tunnel: TunnelConf, pingResult: List<Boolean>) {
if (!pingSuccess) { if (pingResult.contains(false)) {
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)
@@ -224,33 +197,23 @@ open class BaseTunnel(
} }
} }
private suspend fun monitorTunnelConfigChanges() = coroutineScope { private suspend fun startTunnelConfigChangeJob(tunnel: TunnelConf) = coroutineScope {
appDataRepository.tunnels.flow.collect { storageTuns -> appDataRepository.tunnels.flow.collect { storageTuns ->
storageTuns.forEach { storageTun -> storageTuns.firstOrNull { it.id == tunnel.id }?.let { storageTun ->
val currentTun = tunnels.value.firstOrNull { it.id == storageTun.id } if (tunnel.isQuickConfigChanged(storageTun) || tunnel.isPingConfigMatching(storageTun)) {
if (currentTun != null) { bounceTunnel(tunnel)
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 (this.isActive) { while (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)
}.onFailure { exception ->
Timber.e(exception, "Failed to update tunnel statistics for ${tunnel.tunName}")
} }
delay(CHECK_INTERVAL)
} }
} }
} }
@@ -3,20 +3,21 @@ 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.networkmonitor.NetworkMonitor 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.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.launch import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@@ -30,18 +31,13 @@ class KernelTunnel @Inject constructor(
networkMonitor: NetworkMonitor, networkMonitor: NetworkMonitor,
) : BaseTunnel(ioDispatcher, applicationScope, networkMonitor, appDataRepository, serviceManager, notificationManager) { ) : BaseTunnel(ioDispatcher, applicationScope, networkMonitor, appDataRepository, serviceManager, notificationManager) {
override fun startTunnel(tunnelConf: TunnelConf) { override suspend fun startTunnel(tunnelConf: TunnelConf) {
Timber.d("Starting tunnel ${tunnelConf.id} kernel") withContext(ioDispatcher) {
applicationScope.launch(ioDispatcher) { super.startTunnel(tunnelConf)
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())
Timber.d("Calling super.startTunnel") addToActiveTunnels(tunnelConf)
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())
@@ -52,14 +48,15 @@ class KernelTunnel @Inject constructor(
} }
} }
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics { override suspend fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics {
return WireGuardStatistics(backend.getStatistics(tunnelConf)) return WireGuardStatistics(backend.getStatistics(tunnelConf))
} }
override fun stopTunnel(tunnelConf: TunnelConf?) { override suspend fun stopTunnel(tunnelConf: TunnelConf?) {
applicationScope.launch(ioDispatcher) { withContext(ioDispatcher) {
val tunnel = tunnels.value.firstOrNull { it.id == tunnelConf?.id }
runCatching { runCatching {
tunnels.value.firstOrNull { it.id == tunnelConf?.id }?.let { tunnel?.let {
backend.setState(it, Tunnel.State.DOWN, it.toWgConfig()) backend.setState(it, Tunnel.State.DOWN, it.toWgConfig())
onTunnelStop(it) onTunnelStop(it)
} ?: stopAllTunnels() } ?: stopAllTunnels()
@@ -69,6 +66,13 @@ 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.domain.state.TunnelStatistics import com.zaneschepke.wireguardautotunnel.util.extensions.withData
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.flatMapLatest import kotlinx.coroutines.flow.first
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,69 +28,71 @@ class TunnelManager @Inject constructor(
@IoDispatcher private val ioDispatcher: CoroutineDispatcher, @IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : TunnelProvider { ) : TunnelProvider {
@OptIn(ExperimentalCoroutinesApi::class) val appSettings: StateFlow<AppSettings?> = appDataRepository.settings.flow.stateIn(
private val tunnelProviderFlow = appDataRepository.settings.flow scope = applicationScope.plus(ioDispatcher),
.filterNotNull() started = SharingStarted.Eagerly,
.flatMapLatest { settings -> initialValue = null,
MutableStateFlow(if (settings.isKernelEnabled) kernelTunnel else userspaceTunnel) )
}
.stateIn(
scope = applicationScope.plus(ioDispatcher),
started = SharingStarted.Eagerly,
initialValue = userspaceTunnel,
)
@OptIn(ExperimentalCoroutinesApi::class) override suspend fun activeTunnels(): StateFlow<List<TunnelConf>> {
override val activeTunnels = appDataRepository.settings.flow return withContext(ioDispatcher) {
.filterNotNull() appSettings.filterNotNull().first().let {
.flatMapLatest { settings -> if (it.isKernelEnabled) return@withContext kernelTunnel.activeTunnels()
if (settings.isKernelEnabled) { userspaceTunnel.activeTunnels()
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 fun stopTunnel(tunnelConf: TunnelConf?) { override suspend fun startTunnel(tunnelConf: TunnelConf) {
tunnelProviderFlow.value.stopTunnel(tunnelConf) appSettings.withData {
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) {
tunnelProviderFlow.value.bounceTunnel(tunnelConf) appSettings.withData {
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>) {
tunnelProviderFlow.value.setBackendState(backendState, allowedIps) appSettings.withData {
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> {
return tunnelProviderFlow.value.runningTunnelNames() appSettings.filterNotNull().first().let {
if (it.isKernelEnabled) return kernelTunnel.runningTunnelNames()
return userspaceTunnel.runningTunnelNames()
}
} }
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics { suspend fun restorePreviousState() {
return tunnelProviderFlow.value.getStatistics(tunnelConf) withContext(ioDispatcher) {
} with(appDataRepository.settings.get()) {
if (isRestoreOnBootEnabled) {
fun restorePreviousState() = applicationScope.launch(ioDispatcher) { val previouslyActiveTuns = appDataRepository.tunnels.getActive()
val settings = appDataRepository.settings.get() // handle kernel mode
if (settings.isRestoreOnBootEnabled) { val tunsToStart = previouslyActiveTuns.filterNot { tun -> activeTunnels().value.any { tun.id == it.id } }
val previouslyActiveTuns = appDataRepository.tunnels.getActive() if (isKernelEnabled) {
val tunsToStart = previouslyActiveTuns.filterNot { tun -> activeTunnels.value.any { tun.id == it.key } } return@withContext tunsToStart.forEach {
if (settings.isKernelEnabled) { startTunnel(it)
return@launch tunsToStart.forEach { }
startTunnel(it) }
// handle userspace
if (activeTunnels().value.isEmpty()) tunsToStart.firstOrNull()?.let { startTunnel(it) }
} }
} else {
tunsToStart.firstOrNull()?.let { startTunnel(it) }
} }
} }
} }
@@ -2,20 +2,23 @@ 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 {
companion object {
const val CHECK_INTERVAL = 1000L
}
fun startTunnel(tunnelConf: TunnelConf) suspend fun activeTunnels(): StateFlow<List<TunnelConf>>
fun stopTunnel(tunnelConf: TunnelConf? = null)
suspend fun startTunnel(tunnelConf: TunnelConf)
suspend fun stopTunnel(tunnelConf: TunnelConf? = null)
suspend fun bounceTunnel(tunnelConf: TunnelConf) suspend fun bounceTunnel(tunnelConf: TunnelConf)
suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>)
suspend fun runningTunnelNames(): Set<String> suspend fun runningTunnelNames(): Set<String>
fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics
val activeTunnels: StateFlow<Map<Int, TunnelState>> companion object {
const val CHECK_INTERVAL = 1_000L
}
} }
@@ -1,13 +1,14 @@
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.networkmonitor.NetworkMonitor 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.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
@@ -15,7 +16,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.launch import kotlinx.coroutines.withContext
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
@@ -29,24 +30,15 @@ class UserspaceTunnel @Inject constructor(
notificationManager: NotificationManager, notificationManager: NotificationManager,
private val backend: Backend, private val backend: Backend,
networkMonitor: NetworkMonitor, networkMonitor: NetworkMonitor,
) : BaseTunnel(ioDispatcher, applicationScope, networkMonitor, appDataRepository, serviceManager, notificationManager) { ) : TunnelProvider, BaseTunnel(ioDispatcher, applicationScope, networkMonitor, appDataRepository, serviceManager, notificationManager) {
override fun startTunnel(tunnelConf: TunnelConf) { override suspend fun startTunnel(tunnelConf: TunnelConf) {
applicationScope.launch(ioDispatcher) { withContext(ioDispatcher) {
Timber.d("Starting tunnel ${tunnelConf.id} userspace") super.startTunnel(tunnelConf)
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())
Timber.d("Calling super.startTunnel") addToActiveTunnels(tunnelConf)
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())
@@ -57,8 +49,19 @@ class UserspaceTunnel @Inject constructor(
} }
} }
override fun stopTunnel(tunnelConf: TunnelConf?) { override suspend fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics {
applicationScope.launch(ioDispatcher) { return AmneziaStatistics(backend.getStatistics(tunnelConf))
}
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())
@@ -77,8 +80,4 @@ 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()
} }
@@ -0,0 +1,16 @@
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,8 +4,7 @@ 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.networkmonitor.AndroidNetworkMonitor 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.core.tunnel.KernelTunnel import com.zaneschepke.wireguardautotunnel.core.tunnel.KernelTunnel
@@ -99,20 +98,13 @@ 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, applicationScope, appDataRepository) return ServiceManager(context, ioDispatcher, appDataRepository)
} }
} }
@@ -1,11 +1,14 @@
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
@@ -27,13 +30,9 @@ 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 {
fun setStateChangeCallback(callback: (Any) -> Unit) { val state = MutableStateFlow(TunnelState())
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 })
@@ -51,17 +50,21 @@ data class TunnelConf(
return isIpv4Preferred return isIpv4Preferred
} }
override fun onStateChange(newState: com.wireguard.android.backend.Tunnel.State) {
stateChangeCallback?.invoke(newState)
}
override fun onStateChange(newState: Tunnel.State) { override fun onStateChange(newState: Tunnel.State) {
stateChangeCallback?.invoke(newState) state.update {
it.copy(state = newState.asTunnelState())
}
} }
fun isQuickConfigMatching(updatedConf: TunnelConf): Boolean { override fun onStateChange(newState: com.wireguard.android.backend.Tunnel.State) {
return updatedConf.wgQuick == wgQuick || state.update {
updatedConf.amQuick == amQuick it.copy(state = newState.asTunnelState())
}
}
fun isQuickConfigChanged(updatedConf: TunnelConf): Boolean {
return updatedConf.wgQuick != wgQuick ||
updatedConf.amQuick != amQuick
} }
fun isPingConfigMatching(updatedConf: TunnelConf): Boolean { fun isPingConfigMatching(updatedConf: TunnelConf): Boolean {
@@ -71,17 +74,18 @@ data class TunnelConf(
updatedConf.pingInterval == pingInterval updatedConf.pingInterval == pingInterval
} }
suspend fun isTunnelPingable(context: CoroutineContext): Boolean { suspend fun pingTunnel(context: CoroutineContext): List<Boolean> {
return withContext(context) { return withContext(context) {
val config = toWgConfig() val config = toWgConfig()
if (pingIp != null) { if (pingIp != null) {
return@withContext InetAddress.getByName(pingIp) Timber.i("Pinging custom ip")
.isReachable(Constants.PING_TIMEOUT.toInt()) listOf(InetAddress.getByName(pingIp).isReachable(Constants.PING_TIMEOUT.toInt()))
} else {
Timber.i("Pinging all peers")
config.peers.map { peer ->
peer.isReachable(isIpv4Preferred)
}
} }
Timber.i("Pinging all peers")
config.peers.map { peer ->
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: Map<Int, TunnelState> = emptyMap(), val activeTunnels: List<TunnelConf> = emptyList(),
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.key == preferredTunnel.id } activeTunnels.isNotEmpty() && !activeTunnels.any { it.id == preferredTunnel.id }
} }
private fun isEthernetTunnelChangeNeeded(): Boolean { private fun isEthernetTunnelChangeNeeded(): Boolean {
val preferredTunnel = preferredEthernetTunnel() val preferredTunnel = preferredEthernetTunnel()
return preferredTunnel != null && activeTunnels.isNotEmpty() && !activeTunnels.any { it.key == preferredTunnel.id } return preferredTunnel != null && activeTunnels.isNotEmpty() && !activeTunnels.any { it.id == 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())
} }
private fun isNoConnectivity(): Boolean { 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.key == preferred?.id } return activeTunnels.any { it.id == 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.tunnelManager.activeTunnels.collectAsStateWithLifecycle(emptyMap()) val activeTunnels by viewModel.activeTunnels.collectAsStateWithLifecycle(emptyList())
val collator = Collator.getInstance(Locale.getDefault()) val collator = Collator.getInstance(Locale.getDefault())
@@ -89,7 +89,6 @@ 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) {
@@ -133,8 +132,15 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
} }
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConf) { fun onTunnelToggle(checked: Boolean, tunnel: TunnelConf) {
if (!checked) return viewModel.onTunnelStop(tunnel).let { } if (!checked) {
if (uiState.appSettings.isKernelEnabled) viewModel.onTunnelStart(tunnel) else startTunnel(tunnel) viewModel.onTunnelStop(tunnel)
return
}
if (uiState.appSettings.isKernelEnabled) {
viewModel.onTunnelStart(tunnel)
} else {
startTunnel.invoke(tunnel)
}
} }
Scaffold( Scaffold(
@@ -227,13 +233,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.getOrDefault(tunnel.id, TunnelState()) val tunnelState = activeTunnels.firstOrNull { it.id == tunnel.id }?.state?.collectAsStateWithLifecycle()
TunnelRowItem( TunnelRowItem(
tunnelState.state.isUp(), tunnel.isActive,
expanded, expanded,
selectedTunnel?.id == tunnel.id, selectedTunnel?.id == tunnel.id,
tunnel, tunnel,
tunnelState = tunnelState, tunnelState = tunnelState?.value ?: TunnelState(),
{ selectedTunnel = tunnel }, { selectedTunnel = tunnel },
{ viewModel.onExpandedChanged(!expanded) }, { viewModel.onExpandedChanged(!expanded) },
onDelete = { showDeleteTunnelAlertDialog = true }, onDelete = { showDeleteTunnelAlertDialog = true },
@@ -41,7 +41,6 @@ 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
@@ -52,7 +51,7 @@ import kotlin.text.isNullOrBlank
import kotlin.text.toLong import kotlin.text.toLong
@Composable @Composable
fun OptionsScreen(tunnelConf: TunnelConf, appUiState: AppUiState, viewModel: TunnelOptionsViewModel = hiltViewModel()) { fun OptionsScreen(tunnelConf: TunnelConf, viewModel: TunnelOptionsViewModel = hiltViewModel()) {
val navController = LocalNavController.current val navController = LocalNavController.current
var currentText by remember { mutableStateOf("") } var currentText by remember { mutableStateOf("") }
@@ -195,7 +194,6 @@ fun OptionsScreen(tunnelConf: TunnelConf, appUiState: AppUiState, viewModel: Tun
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.appSettings.isAutoTunnelEnabled || // uiState.settings.isAutoTunnelEnabled ||
uiState.appSettings.isAlwaysOnVpnEnabled || // uiState.settings.isAlwaysOnVpnEnabled ||
uiState.activeTunnels.isNotEmpty() // (uiState.vpnState.status == TunnelState.UP)
), // ),
) )
}, },
onClick = { onClick = {
@@ -18,6 +18,8 @@ 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
@@ -34,6 +36,7 @@ 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
@@ -48,6 +51,20 @@ 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),
@@ -98,12 +115,12 @@ fun SupportScreen(appUiState: AppUiState, appViewModel: AppViewModel) {
ScaledSwitch( ScaledSwitch(
appUiState.generalState.isLocalLogsEnabled, appUiState.generalState.isLocalLogsEnabled,
onClick = { onClick = {
appViewModel.onToggleLocalLogging() showDialog = true
}, },
) )
}, },
onClick = { onClick = {
appViewModel.onToggleLocalLogging() showDialog = true
}, },
), ),
) )
@@ -3,12 +3,10 @@ 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,6 +2,7 @@ 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
@@ -45,9 +46,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().resolved.isPresent this.endpoint.get().getResolved(preferIpv4).isPresent
) { ) {
this.endpoint.get().resolved.get().host this.endpoint.get().getResolved(preferIpv4).get().host
} else { } else {
Constants.DEFAULT_PING_IP Constants.DEFAULT_PING_IP
} }
@@ -86,6 +87,12 @@ 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,6 +8,12 @@ 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,9 +31,11 @@ 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.first import kotlinx.coroutines.flow.onCompletion
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
@@ -73,13 +75,11 @@ 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, activeTunnels, autoTunnel -> ) { settings, tunnels, generalState, autoTunnel ->
AppUiState( AppUiState(
settings, settings,
tunnels, tunnels,
activeTunnels,
generalState, generalState,
autoTunnel, autoTunnel,
) )
@@ -103,13 +103,14 @@ constructor(
private suspend fun appReadyCheck() { private suspend fun appReadyCheck() {
val tunnelCount = appDataRepository.tunnels.count() val tunnelCount = appDataRepository.tunnels.count()
uiState.first { it.tunnels.count() == tunnelCount } uiState.takeWhile { it.tunnels.size != tunnelCount }.onCompletion {
_isAppReady.emit(true) _isAppReady.emit(true)
}.collect()
} }
private suspend fun initTunnels() { private suspend fun initTunnels() {
tunnels.withData { tunnels -> tunnels.withData {
tunnels.filter { it.isActive }.forEach { it.filter { it.isActive }.forEach {
tunnelManager.startTunnel(it) tunnelManager.startTunnel(it)
} }
} }
@@ -145,12 +146,17 @@ 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) { if (!toggledOn) onLoggerStop()
logReader.stop() _configurationChange.update {
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,6 +24,8 @@ 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
@@ -42,6 +44,15 @@ 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 {
+1 -176
View File
@@ -1,177 +1,2 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources></resources>
<string name="tunnel_on_ethernet">Túnel na ethernet</string>
<string name="public_key">Chave pública</string>
<string name="addresses">Endereços</string>
<string name="dns_servers">Servidores DNS</string>
<string name="endpoint">Endpoint</string>
<string name="name">Nome</string>
<string name="create_import">Criar do zero</string>
<string name="rotate_keys">Revezar chaves</string>
<string name="private_key">Chave privada</string>
<string name="base64_key">Chave base64</string>
<string name="optional_no_recommend">(opcional, não recomendado)</string>
<string name="preshared_key">Chave pré-partilhada</string>
<string name="seconds">segundos</string>
<string name="export_configs">Exportar configurações</string>
<string name="error_no_file_explorer">Nenhum explorador de ficheiros instalado</string>
<string name="error_invalid_code">Código QR inválido</string>
<string name="auto_tunnel_title">Serviço de Auto-túnel</string>
<string name="all">todos</string>
<string name="enter_pin">Digite o seu pin</string>
<string name="use_tunnel_on_wifi_name">Usar túnel em wifi com nome</string>
<string name="version">Versão</string>
<string name="junk_packet_count">Quantidade de pacotes-lixo</string>
<string name="junk_packet_minimum_size">Tamanho mínimo de pacote-lixo</string>
<string name="junk_packet_maximum_size">Tamanho máximo de pacote-lixo</string>
<string name="init_packet_junk_size">Tamanho de pacote-lixo inicial</string>
<string name="response_packet_junk_size">Tamanho de resposta de pacote-lixo</string>
<string name="app_name">WG Tunnel</string>
<string name="no_tunnels">Nenhum túnel foi adicionado!</string>
<string name="error_file_extension">O ficheiro não é .conf ou .zip</string>
<string name="prominent_background_location_message">Este recurso precisa de permissões de localização em segundo plano para ativar o monitoramento do SSID da rede Wi-Fi mesmo quando a aplicação está fechado. Para mais pormenores, por favor veja a Política de Privacidade no ecrã de Suporte.</string>
<string name="turn_off_tunnel">Esta ação só é possível com o túnel inativo</string>
<string name="enabled_app_shortcuts">Ativar atalhos de aplicações</string>
<string name="tunnels">Túneis</string>
<string name="privacy_policy">Ver a Política de Privacidade</string>
<string name="okay">OK</string>
<string name="tunnel_mobile_data">Túnel em dados móveis</string>
<string name="prominent_background_location_title">Revelar a localização em segundo plano</string>
<string name="thank_you">Obrigado por usar o WG Tunnel!</string>
<string name="trusted_ssid_value_description">Envie o SSID</string>
<string name="open_file">Abrir Ficheiro</string>
<string name="add_from_qr">Adicionar a partir de código QR</string>
<string name="add_tunnels_text">Adicionar a partir de ficheiro ou zip</string>
<string name="qr_scan">Escanear o código QR</string>
<string name="tunnel_name">Nome do Túnel</string>
<string name="config_changes_saved">Mudanças nas configurações gravadas.</string>
<string name="exclude">Excluir</string>
<string name="include">Incluir</string>
<string name="mtu">MTU</string>
<string name="always_on_vpn_support">Permitir VPN sempre ligada</string>
<string name="allowed_ips">IPs Permitidos</string>
<string name="peer">Par</string>
<string name="location_services_not_detected">Serviço de localização não foi detetado</string>
<string name="hint_search_packages">Procurar pacotes</string>
<string name="auto_tunneling">Auto-túnel</string>
<string name="vpn_on">VPN ligada</string>
<string name="vpn_off">VPN desligada</string>
<string name="listen_port">Porta de escuta</string>
<string name="turn_on_tunnel">Esta ação precisa um túnel ativo</string>
<string name="add_peer">Adicionar par</string>
<string name="interface_">Interface</string>
<string name="copy_public_key">Copiar chave pública</string>
<string name="comma_separated_list">Lista separada por vírgulas</string>
<string name="optional">(opcional)</string>
<string name="random">(aleatório)</string>
<string name="persistent_keepalive">Manter a conexão persistente (keepalive)</string>
<string name="cancel">Cancelar</string>
<string name="error_authentication_failed">Autenticação falhou</string>
<string name="error_authorization_failed">Autorização falhou</string>
<string name="restart_on_ping">Reiniciar em falha de ping (beta)</string>
<string name="email_description">Me envie um email</string>
<string name="error_ssid_exists">SSID já existe</string>
<string name="delete_tunnel_message">Tem certeza que quer apagar este túnel?</string>
<string name="yes">Sim</string>
<string name="unknown_error">Ocorreu um erro desconhecido</string>
<string name="email_subject">Apoio para o WG Tunnel</string>
<string name="tunnel_on_wifi">Túnel em Wi-Fi não confiável</string>
<string name="delete_tunnel">Apagar túnel</string>
<string name="email_chooser">Enviar um email…</string>
<string name="use_kernel">Usar o módulo do kernel</string>
<string name="docs_description">Ler a documentação</string>
<string name="error_root_denied">Shell Root negado</string>
<string name="location_services_missing_message">A aplicação não detetou o serviço de localização ativado no seu dispositivo. Dependendo do dispositivo, isto pode causar que a função de Wi-Fi não confiável falhe em ler o nome do Wi-Fi. Quer continuar mesmo assim?</string>
<string name="open_issue">Abrir um problema</string>
<string name="tunneling_apps">Aplicações com túnel</string>
<string name="no_email_detected">Nenhuma aplicação de email detetado</string>
<string name="no_browser_detected">Nenhum navegador detetado</string>
<string name="incorrect_pin">O Pin está errado</string>
<string name="auto">(automático)</string>
<string name="read_logs">Ler os registos</string>
<string name="pin_created">Pin criado com sucesso</string>
<string name="create_pin">Criar um pin</string>
<string name="enable_app_lock">Ligar bloqueio de aplicação</string>
<string name="edit_tunnel">Editar túnel</string>
<string name="mobile_data_tunnel">Selecionar como túnel em dados móveis</string>
<string name="set_primary_tunnel">Selecionar como túnel principal</string>
<string name="support">Suporte</string>
<string name="kernel">Kernel</string>
<string name="settings">Configurações</string>
<string name="unsure_how">se não tiver certeza em como continuar</string>
<string name="see_the">Veja o</string>
<string name="getting_started_guide">guia de início rápido</string>
<string name="error_file_format">Formato de configuração inválido</string>
<string name="vpn_channel_name">Canal de notificações VPN</string>
<string name="set_custom_ping_ip">Definir ip ping personalizado</string>
<string name="vpn_denied_dialog_title">Permissão negada</string>
<string name="vpn_settings">Configurações do sistema VPN</string>
<string name="always_on_message">A permissão de conexão VPN foi negada. Por favor, verifique</string>
<string name="chat_description">Junte-se à comunidade</string>
<string name="tunnel_required">Característica requer pelo menos um túnel</string>
<string name="app_settings">configurações da app</string>
<string name="background_location_message2">para garantir que essas permissões estejam ativadas.</string>
<string name="root_accepted">Shell root aceito</string>
<string name="optional_default">"opcional, padrão: "</string>
<string name="show_amnezia_properties">Mostrar propriedades de Amnezia</string>
<string name="never">nunca</string>
<string name="default_ping_ip">(opcional, padrão para pares)</string>
<string name="set_custom_ping_internal">Intervalo de Ping (seg)</string>
<string name="handshake">handshake</string>
<string name="background_location_message">Permitir que toda a permissão de localização do tempo e/ou localização precisa é necessária para este recurso. Por favor, veja</string>
<string name="sec">seg</string>
<string name="notifications">Notificações</string>
<string name="exclude_lan">Excluir LAN</string>
<string name="hide_scripts">Ocultar scripts</string>
<string name="trusted_wifi_names">Nomes de Wi-Fi confiáveis</string>
<string name="hide_amnezia_properties">Ocultar propriedades Amnezia</string>
<string name="remove_amnezia_compatibility">Remover compatibilidade Amnezia</string>
<string name="include_lan">Incluir LAN</string>
<string name="language">Idioma</string>
<string name="add_wifi_name">Adicionar nome Wi-Fi</string>
<string name="display_theme">Tema</string>
<string name="on_demand_rules">Regras de tunelamento sob demanda</string>
<string name="dark">Escuro</string>
<string name="dynamic">Dinâmico</string>
<string name="skip">Pular</string>
<string name="mobile_tunnel">Túnel com dados móveis</string>
<string name="requires_app_relaunch">Para aplicar as mudanças é necessário reiniciar o aplicativo. Deseja prosseguir ?</string>
<string name="add_from_clipboard">Adicionar da área de transferência</string>
<string name="restart_at_boot">Ativar na inicialização</string>
<string name="appearance">Aparência</string>
<string name="automatic">Automático</string>
<string name="light">Claro</string>
<string name="wildcards_active">Wildcards ativos</string>
<string name="learn_more">Saber mais</string>
<string name="use_wildcards">Usar nomes coringas</string>
<string name="wifi_name_via_shell">Nome do Wi-Fi por shell</string>
<string name="use_root_shell_for_wifi">Obter o nome do Wi-Fi através do shell root</string>
<string name="start_auto">Iniciar túnel automático</string>
<string name="stop_auto">Pausar túnel automático</string>
<string name="monitoring_state_changes">Monitorar status de alterações</string>
<string name="tunnel_running">Túnel em execução</string>
<string name="donate">Contribua com esse projeto</string>
<string name="local_logging">Registro local</string>
<string name="enable_local_logging">Ativar registro local</string>
<string name="configuration_change">Configuração alterada</string>
<string name="stop_on_no_internet">Interromper quando não há internet</string>
<string name="stop_on_internet_loss">Interrompa o túnel quando a internet não estiver disponível</string>
<string name="ethernet_tunnel">Túnel ethernet</string>
<string name="set_ethernet_tunnel">Definir como túnel ethernet</string>
<string name="native_kill_switch">Interruptor de desligamento padrão</string>
<string name="vpn_kill_switch">Interruptor de desligamento VPN</string>
<string name="kill_switch_options">Opções do interruptor de desligamento</string>
<string name="allow_lan_traffic">Permitir tráfego LAN</string>
<string name="bypass_lan_for_kill_switch">Ignorar LAN no interruptor de desligamento</string>
<string name="splt_tunneling">Tunelamento dividido</string>
<string name="stop">pausar</string>
<string name="tunnel_specific_settings">Configurações específicas no túnel</string>
<string name="show_scripts">Mostrar scripts</string>
<string name="amnezia_kernel_message">Amnezia indisponível no kernel</string>
<string name="enable_amnezia">Ativar Amnezia</string>
<string name="wg_compat_mode">Modo de compatibilidade WG</string>
<string name="quick_actions">Ações rápidas</string>
<string name="kernel_not_supported">Kernel não suportado</string>
<string name="advanced_settings">Configurações avançadas</string>
<string name="enable_amnezia_compatibility">Ativar compatibilidade Amnezia</string>
</resources>
+177
View File
@@ -0,0 +1,177 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="tunnel_on_ethernet">Túnel na ethernet</string>
<string name="public_key">Chave pública</string>
<string name="addresses">Endereços</string>
<string name="dns_servers">Servidores DNS</string>
<string name="endpoint">Endpoint</string>
<string name="name">Nome</string>
<string name="create_import">Criar do zero</string>
<string name="rotate_keys">Revezar chaves</string>
<string name="private_key">Chave privada</string>
<string name="base64_key">Chave base64</string>
<string name="optional_no_recommend">(opcional, não recomendado)</string>
<string name="preshared_key">Chave pré-partilhada</string>
<string name="seconds">segundos</string>
<string name="export_configs">Exportar configurações</string>
<string name="error_no_file_explorer">Nenhum explorador de ficheiros instalado</string>
<string name="error_invalid_code">Código QR inválido</string>
<string name="auto_tunnel_title">Serviço de Auto-túnel</string>
<string name="all">todos</string>
<string name="enter_pin">Digite o seu pin</string>
<string name="use_tunnel_on_wifi_name">Usar túnel em wifi com nome</string>
<string name="version">Versão</string>
<string name="junk_packet_count">Quantidade de pacotes-lixo</string>
<string name="junk_packet_minimum_size">Tamanho mínimo de pacote-lixo</string>
<string name="junk_packet_maximum_size">Tamanho máximo de pacote-lixo</string>
<string name="init_packet_junk_size">Tamanho de pacote-lixo inicial</string>
<string name="response_packet_junk_size">Tamanho de resposta de pacote-lixo</string>
<string name="app_name">WG Tunnel</string>
<string name="no_tunnels">Nenhum túnel foi adicionado!</string>
<string name="error_file_extension">O ficheiro não é .conf ou .zip</string>
<string name="prominent_background_location_message">Este recurso precisa de permissões de localização em segundo plano para ativar o monitoramento do SSID da rede Wi-Fi mesmo quando a aplicação está fechado. Para mais pormenores, por favor veja a Política de Privacidade no ecrã de Suporte.</string>
<string name="turn_off_tunnel">Esta ação só é possível com o túnel inativo</string>
<string name="enabled_app_shortcuts">Ativar atalhos de aplicações</string>
<string name="tunnels">Túneis</string>
<string name="privacy_policy">Ver a Política de Privacidade</string>
<string name="okay">OK</string>
<string name="tunnel_mobile_data">Túnel em dados móveis</string>
<string name="prominent_background_location_title">Revelar a localização em segundo plano</string>
<string name="thank_you">Obrigado por usar o WG Tunnel!</string>
<string name="trusted_ssid_value_description">Envie o SSID</string>
<string name="open_file">Abrir Ficheiro</string>
<string name="add_from_qr">Adicionar a partir de código QR</string>
<string name="add_tunnels_text">Adicionar a partir de ficheiro ou zip</string>
<string name="qr_scan">Escanear o código QR</string>
<string name="tunnel_name">Nome do Túnel</string>
<string name="config_changes_saved">Mudanças nas configurações gravadas.</string>
<string name="exclude">Excluir</string>
<string name="include">Incluir</string>
<string name="mtu">MTU</string>
<string name="always_on_vpn_support">Permitir VPN sempre ligada</string>
<string name="allowed_ips">IPs Permitidos</string>
<string name="peer">Par</string>
<string name="location_services_not_detected">Serviço de localização não foi detetado</string>
<string name="hint_search_packages">Procurar pacotes</string>
<string name="auto_tunneling">Auto-túnel</string>
<string name="vpn_on">VPN ligada</string>
<string name="vpn_off">VPN desligada</string>
<string name="listen_port">Porta de escuta</string>
<string name="turn_on_tunnel">Esta ação precisa um túnel ativo</string>
<string name="add_peer">Adicionar par</string>
<string name="interface_">Interface</string>
<string name="copy_public_key">Copiar chave pública</string>
<string name="comma_separated_list">Lista separada por vírgulas</string>
<string name="optional">(opcional)</string>
<string name="random">(aleatório)</string>
<string name="persistent_keepalive">Manter a conexão persistente (keepalive)</string>
<string name="cancel">Cancelar</string>
<string name="error_authentication_failed">Autenticação falhou</string>
<string name="error_authorization_failed">Autorização falhou</string>
<string name="restart_on_ping">Reiniciar em falha de ping (beta)</string>
<string name="email_description">Me envie um email</string>
<string name="error_ssid_exists">SSID já existe</string>
<string name="delete_tunnel_message">Tem certeza que quer apagar este túnel?</string>
<string name="yes">Sim</string>
<string name="unknown_error">Ocorreu um erro desconhecido</string>
<string name="email_subject">Apoio para o WG Tunnel</string>
<string name="tunnel_on_wifi">Túnel em Wi-Fi não confiável</string>
<string name="delete_tunnel">Apagar túnel</string>
<string name="email_chooser">Enviar um email…</string>
<string name="use_kernel">Usar o módulo do kernel</string>
<string name="docs_description">Ler a documentação</string>
<string name="error_root_denied">Shell Root negado</string>
<string name="location_services_missing_message">A aplicação não detetou o serviço de localização ativado no seu dispositivo. Dependendo do dispositivo, isto pode causar que a função de Wi-Fi não confiável falhe em ler o nome do Wi-Fi. Quer continuar mesmo assim?</string>
<string name="open_issue">Abrir um problema</string>
<string name="tunneling_apps">Aplicações com túnel</string>
<string name="no_email_detected">Nenhuma aplicação de email detetado</string>
<string name="no_browser_detected">Nenhum navegador detetado</string>
<string name="incorrect_pin">O Pin está errado</string>
<string name="auto">(automático)</string>
<string name="read_logs">Ler os registos</string>
<string name="pin_created">Pin criado com sucesso</string>
<string name="create_pin">Criar um pin</string>
<string name="enable_app_lock">Ligar bloqueio de aplicação</string>
<string name="edit_tunnel">Editar túnel</string>
<string name="mobile_data_tunnel">Selecionar como túnel em dados móveis</string>
<string name="set_primary_tunnel">Selecionar como túnel principal</string>
<string name="support">Suporte</string>
<string name="kernel">Kernel</string>
<string name="settings">Configurações</string>
<string name="unsure_how">se não tiver certeza em como continuar</string>
<string name="see_the">Veja o</string>
<string name="getting_started_guide">guia de início rápido</string>
<string name="error_file_format">Formato de configuração inválido</string>
<string name="vpn_channel_name">Canal de notificações VPN</string>
<string name="set_custom_ping_ip">Definir ip ping personalizado</string>
<string name="vpn_denied_dialog_title">Permissão negada</string>
<string name="vpn_settings">Configurações do sistema VPN</string>
<string name="always_on_message">A permissão de conexão VPN foi negada. Por favor, verifique</string>
<string name="chat_description">Junte-se à comunidade</string>
<string name="tunnel_required">Característica requer pelo menos um túnel</string>
<string name="app_settings">configurações da app</string>
<string name="background_location_message2">para garantir que essas permissões estejam ativadas.</string>
<string name="root_accepted">Shell root aceito</string>
<string name="optional_default">"opcional, padrão: "</string>
<string name="show_amnezia_properties">Mostrar propriedades de Amnezia</string>
<string name="never">nunca</string>
<string name="default_ping_ip">(opcional, padrão para pares)</string>
<string name="set_custom_ping_internal">Intervalo de Ping (seg)</string>
<string name="handshake">handshake</string>
<string name="background_location_message">Permitir que toda a permissão de localização do tempo e/ou localização precisa é necessária para este recurso. Por favor, veja</string>
<string name="sec">seg</string>
<string name="notifications">Notificações</string>
<string name="exclude_lan">Excluir LAN</string>
<string name="hide_scripts">Ocultar scripts</string>
<string name="trusted_wifi_names">Nomes de Wi-Fi confiáveis</string>
<string name="hide_amnezia_properties">Ocultar propriedades Amnezia</string>
<string name="remove_amnezia_compatibility">Remover compatibilidade Amnezia</string>
<string name="include_lan">Incluir LAN</string>
<string name="language">Idioma</string>
<string name="add_wifi_name">Adicionar nome Wi-Fi</string>
<string name="display_theme">Tema</string>
<string name="on_demand_rules">Regras de tunelamento sob demanda</string>
<string name="dark">Escuro</string>
<string name="dynamic">Dinâmico</string>
<string name="skip">Pular</string>
<string name="mobile_tunnel">Túnel com dados móveis</string>
<string name="requires_app_relaunch">Para aplicar as mudanças é necessário reiniciar o aplicativo. Deseja prosseguir ?</string>
<string name="add_from_clipboard">Adicionar da área de transferência</string>
<string name="restart_at_boot">Ativar na inicialização</string>
<string name="appearance">Aparência</string>
<string name="automatic">Automático</string>
<string name="light">Claro</string>
<string name="wildcards_active">Wildcards ativos</string>
<string name="learn_more">Saber mais</string>
<string name="use_wildcards">Usar nomes coringas</string>
<string name="wifi_name_via_shell">Nome do Wi-Fi por shell</string>
<string name="use_root_shell_for_wifi">Obter o nome do Wi-Fi através do shell root</string>
<string name="start_auto">Iniciar túnel automático</string>
<string name="stop_auto">Pausar túnel automático</string>
<string name="monitoring_state_changes">Monitorar status de alterações</string>
<string name="tunnel_running">Túnel em execução</string>
<string name="donate">Contribua com esse projeto</string>
<string name="local_logging">Registro local</string>
<string name="enable_local_logging">Ativar registro local</string>
<string name="configuration_change">Configuração alterada</string>
<string name="stop_on_no_internet">Interromper quando não há internet</string>
<string name="stop_on_internet_loss">Interrompa o túnel quando a internet não estiver disponível</string>
<string name="ethernet_tunnel">Túnel ethernet</string>
<string name="set_ethernet_tunnel">Definir como túnel ethernet</string>
<string name="native_kill_switch">Interruptor de desligamento padrão</string>
<string name="vpn_kill_switch">Interruptor de desligamento VPN</string>
<string name="kill_switch_options">Opções do interruptor de desligamento</string>
<string name="allow_lan_traffic">Permitir tráfego LAN</string>
<string name="bypass_lan_for_kill_switch">Ignorar LAN no interruptor de desligamento</string>
<string name="splt_tunneling">Tunelamento dividido</string>
<string name="stop">pausar</string>
<string name="tunnel_specific_settings">Configurações específicas no túnel</string>
<string name="show_scripts">Mostrar scripts</string>
<string name="amnezia_kernel_message">Amnezia indisponível no kernel</string>
<string name="enable_amnezia">Ativar Amnezia</string>
<string name="wg_compat_mode">Modo de compatibilidade WG</string>
<string name="quick_actions">Ações rápidas</string>
<string name="kernel_not_supported">Kernel não suportado</string>
<string name="advanced_settings">Configurações avançadas</string>
<string name="enable_amnezia_compatibility">Ativar compatibilidade Amnezia</string>
</resources>
+43 -151
View File
@@ -1,50 +1,43 @@
<?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="error_file_extension">Dosya .conf veya .zip değil</string>
<string name="github_url" translatable="false">https://github.com/zaneschepke/wgtunnel/issues</string> <string name="turn_off_tunnel">İşlem için tünelin kapalı olması gerekiyor</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="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 üzerinde tünel</string> <string name="tunnel_on_ethernet">Ethernet\'te tünel</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_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_title">Arka Plan Konum Açıklaması</string> <string name="prominent_background_location_title">Arka Plan Konum Açıklaması</string>
<string name="thank_you">WG Tunneli kullandığınız için teşekkürler!</string> <string name="thank_you">WG Tunnel\'ı kullandığınız için teşekkürler!</string>
<string name="trusted_ssid_value_description">SSID Gönder</string> <string name="trusted_ssid_value_description">SSID\'yi gönder</string>
<string name="add_tunnels_text">Dosyadan veya zipten ekle</string> <string name="add_tunnels_text">Dosyadan veya zip\'ten 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 Tara</string> <string name="qr_scan">QR Tarama</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"></string> <string name="peer"> (peer)</string>
<string name="allowed_ips">İzin verilen IPler</string> <string name="allowed_ips">İzin verilen IP\'ler</string>
<string name="endpoint">Nokta</string> <string name="endpoint">nokta (endpoint)</string>
<string name="name">Ad</string> <string name="name">Ad</string>
<string name="always_on_vpn_support">Her Zaman Açık VPNe İzin Ver</string> <string name="always_on_vpn_support">Her Zaman Açık VPN\'e İzin Ver</string>
<string name="location_services_not_detected">Konum Servisleri Algılanmadı</string> <string name="location_services_not_detected">Konum Hizmetleri Algılanmadı</string>
<string name="hint_search_packages">Paketleri ara</string> <string name="hint_search_packages">Paketlerde 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">Bu işlem aktif bir tünel gerektirir</string> <string name="turn_on_tunnel">İşlem için aktif tünel gerekiyor</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>
@@ -56,42 +49,41 @@
<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">Ön paylaşımlı anahtar</string> <string name="preshared_key">Önceden paylaşılmış 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</string> <string name="error_authentication_failed">Kimlik doğrulama başarısız oldu</string>
<string name="error_authorization_failed">Yetkilendirme başarısız</string> <string name="error_authorization_failed">Yetkilendirme başarısız oldu</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 wifida tünel</string> <string name="tunnel_on_wifi">Güvenilmeyen wifi\'da 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">Çekirdek modülünü kullan</string> <string name="use_kernel">Kernel 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 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="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="auto_tunnel_title">Otomatik tünel servisi</string> <string name="auto_tunnel_title">Otomatik Tünel Hizmeti</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ünelleme uygulamaları</string> <string name="tunneling_apps">Tünellenen 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">Bir sorun aç</string> <string name="open_issue">Sorun bildir</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">Pininizi girin</string> <string name="enter_pin">PIN\'inizi 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>
@@ -102,118 +94,18 @@
<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">Çöp paket sayısı</string> <string name="junk_packet_count">Gereksiz paket sayısı</string>
<string name="junk_packet_minimum_size">Çöp paket minimum boyutu</string> <string name="junk_packet_minimum_size">Gereksiz paket minimum boyutu</string>
<string name="junk_packet_maximum_size">Çöp paket maksimum boyutu</string> <string name="junk_packet_maximum_size">Gereksiz paket maksimum boyutu</string>
<string name="init_packet_junk_size">Başlangıç paketi çöp boyutu</string> <string name="init_packet_junk_size">Başlatma paketi gereksiz boyutu</string>
<string name="response_packet_junk_size">Yanıt paketi çöp boyutu</string> <string name="response_packet_junk_size">Yanıt paketi gereksiz boyutu</string>
<string name="init_packet_magic_header">Başlangıç paketi sihirli başlığı</string> <string name="init_packet_magic_header">Başlatma 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_guide">başlangıç kılavuzu</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="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">Başlangıçta yeniden başlat</string> <string name="restart_at_boot">Önyüklemede 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.7.0" const val VERSION_NAME = "3.6.6"
const val JVM_TARGET = "17" const val JVM_TARGET = "17"
const val VERSION_CODE = 37000 const val VERSION_CODE = 36600
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"
@@ -1,6 +0,0 @@
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
@@ -1,14 +0,0 @@
Features
- Add tunnels via .conf file, zip, manual entry, or QR code
- Auto connect to VPN based on Wi-Fi SSID, ethernet, or mobile data
- Split tunneling by application with search
- WireGuard support for kernel and userspace modes
- Amnezia support for userspace mode for DPI/censorship protection
- Always-On VPN support
- Export Amnezia and WireGuard tunnels to zip
- Quick tile support for VPN toggling
- Static shortcuts support for primary tunnel for automation integration
- Intent automation support for all tunnels
- Automatic service restart after reboot
- Battery preservation measures
@@ -0,0 +1 @@
Uma alternativa de cliente WireGuard VPN com recursos adicionais
+10 -10
View File
@@ -1,13 +1,13 @@
[versions] [versions]
accompanist = "0.37.2" accompanist = "0.37.0"
activityCompose = "1.10.1" activityCompose = "1.10.0"
amneziawgAndroid = "1.3.0" amneziawgAndroid = "1.2.6"
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.3" datastorePreferences = "1.1.2"
desugar_jdk_libs = "2.1.5" desugar_jdk_libs = "2.1.4"
espressoCore = "3.6.1" espressoCore = "3.6.1"
hiltAndroid = "2.55" hiltAndroid = "2.55"
hiltCompiler = "1.2.0" hiltCompiler = "1.2.0"
@@ -15,15 +15,15 @@ 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.8" navigationCompose = "2.8.7"
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.6" tunnel = "1.2.2"
androidGradlePlugin = "8.8.0-alpha05" androidGradlePlugin = "8.8.0-alpha05"
kotlin = "2.1.10" kotlin = "2.1.10"
ksp = "2.1.10-1.0.31" ksp = "2.1.10-1.0.30"
composeBom = "2025.03.00" composeBom = "2025.02.00"
compose = "1.7.8" compose = "1.7.8"
workRuntimeKtxVersion = "2.10.0" workRuntimeKtxVersion = "2.10.0"
zxingAndroidEmbedded = "4.3.0" zxingAndroidEmbedded = "4.3.0"
@@ -32,7 +32,7 @@ gradlePlugins-grgit = "5.3.0"
#plugins #plugins
material = "1.12.0" material = "1.12.0"
gradlePlugins-ktlint="12.2.0" gradlePlugins-ktlint="12.1.2"
[libraries] [libraries]
@@ -1,90 +0,0 @@
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,8 +4,7 @@ import com.zaneschepke.logcatter.model.LogMessage
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface LogReader { interface LogReader {
fun start() fun initialize(onLogMessage: ((message: LogMessage) -> Unit)? = null)
fun stop()
fun zipLogFiles(path: String) fun zipLogFiles(path: String)
suspend fun deleteAndClearLogs() suspend fun deleteAndClearLogs()
val bufferedLogs: Flow<LogMessage> val bufferedLogs: Flow<LogMessage>
@@ -1,92 +0,0 @@
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,32 +1,250 @@
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 lateinit var logcatManager: LogcatManager private val findKeyRegex = """[A-Za-z0-9+/]{42}[AEIMQUYcgkosw480]=""".toRegex()
private var isInitialized = false private val findIpv6AddressRegex = """^([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}${'$'}""".toRegex()
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(this) { synchronized(LogcatHelperInit) {
if (isInitialized) return logcatManager LogcatHelperInit.maxFileSize = maxFileSize
val logDir = "$storageDir${File.separator}logs" LogcatHelperInit.maxFolderSize = maxFolderSize
File(logDir).mkdirs() LogcatHelperInit.pID = android.os.Process.myPid()
logcatManager = LogcatManager( LogcatHelperInit.publicAppDirectory = storageDir
pid = android.os.Process.myPid(), LogcatHelperInit.logcatPath = LogcatHelperInit.publicAppDirectory + File.separator + "logs"
logDir = logDir, val logDirectory = File(LogcatHelperInit.logcatPath)
maxFileSize = maxFileSize, if (!logDirectory.exists()) {
maxFolderSize = maxFolderSize, logDirectory.mkdir()
) }
ProcessLifecycleOwner.get().lifecycle.addObserver(logcatManager) return Logcat
isInitialized = true }
return logcatManager }
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>(
replay = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
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() }
}
}
} }
} }
} }
@@ -1,63 +0,0 @@
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
@@ -1 +0,0 @@
/build
-51
View File
@@ -1,51 +0,0 @@
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
@@ -1,21 +0,0 @@
# 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
@@ -1,24 +0,0 @@
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)
}
}
@@ -1,9 +0,0 @@
<?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>
@@ -1,151 +0,0 @@
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()
}
}
@@ -1,9 +0,0 @@
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()
}
@@ -1,7 +0,0 @@
package com.zaneschepke.networkmonitor
import kotlinx.coroutines.flow.Flow
interface NetworkMonitor {
fun getNetworkStatusFlow(includeWifiSsid: Boolean, useRootShell: Boolean): Flow<NetworkStatus>
}
@@ -1,20 +0,0 @@
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
}
@@ -1,17 +0,0 @@
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,4 +34,3 @@ rootProject.name = "WG Tunnel"
include(":app") include(":app")
include(":logcatter") include(":logcatter")
include(":networkmonitor")
+1 -1
View File
@@ -1 +1 @@
4 1