Compare commits

...

27 Commits

Author SHA1 Message Date
dependabot[bot] 53da891e67 chore(deps): bump androidx.navigation:navigation-compose
Bumps androidx.navigation:navigation-compose from 2.8.8 to 2.8.9.

---
updated-dependencies:
- dependency-name: androidx.navigation:navigation-compose
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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