Compare commits

..

19 Commits

Author SHA1 Message Date
Zane Schepke c71c4e5b29 chore: bump version and notes 2025-03-19 22:35:21 -04:00
Zane Schepke 7f0fea3766 fix: improve wifi monitoring to better handle permission changes 2025-03-19 21:51:54 -04:00
Zane Schepke 53c19762ef fix: attempt to improve tile sync 2025-03-16 23:12:39 -04:00
Zane Schepke c98fa04f73 fix: auto tunnel and tunnel regressions 2025-03-16 20:10:44 -04:00
Zane Schepke aba0f7d4d3 chore: bump deps 2025-03-16 02:05:55 -04:00
Zane Schepke fa517b2124 fix: race conditions (#621) 2025-03-16 02:04:09 -04:00
Zane Schepke d7e2648393 docs: update readme links 2025-03-15 19:22:34 -04:00
Zane Schepke 53ff3bb1e5 chore: bump version with notes 2025-03-15 13:10:53 -04:00
Zane Schepke 97ede3d5b4 fix: set prefer ipv4 as default to mimic old wg behavior 2025-03-15 12:44:56 -04:00
Zane Schepke dcd15f7bd8 fix: adding new peer bug
Closes #612
2025-03-15 11:30:10 -04:00
Zane Schepke 6031d85edd fix: re-enable shortcuts
Closes #616
2025-03-15 00:16:02 -04:00
Weblate (bot) a71f8f86b1 feat: Translations update from Hosted Weblate (#556)
Co-authored-by: Zane Schepke <zanecschepke@gmail.com>
Co-authored-by: solokot <solokot@gmail.com>
Co-authored-by: lateweb <weblate@techkoala.net>
Co-authored-by: Matthaiks <kitynska@gmail.com>
Co-authored-by: Kachelkaiser <kachelkaiser@outlook.com>
Co-authored-by: CyanWolf <hydemr@pm.me>
Co-authored-by: Henrik Sozzi <henrik_sozzi@hotmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Co-authored-by: x86_64-pc-linux-gnu <x86_64-pc-linux-gnu@proton.me>
Co-authored-by: mak7im01 <mak7im02@gmail.com>
Co-authored-by: heykanspor <meingithub@heykan.de>
2025-03-14 22:51:26 -04:00
Zane Schepke 007c9f4c5d fix: static ipv6 endpoint bug 2025-03-14 22:49:08 -04: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
100 changed files with 1877 additions and 914 deletions
+7 -7
View File
@@ -4,7 +4,7 @@ WG Tunnel
<div align="center">
An alternative Android client app for [WireGuard®](https://www.wireguard.com/)
An alternative Android client app for [WireGuard](https://www.wireguard.com/)
and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/)
<br />
<br />
@@ -23,14 +23,14 @@ and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/)
[![Google Play](https://img.shields.io/badge/Google_Play-414141?style=for-the-badge&logo=google-play&logoColor=white)](https://play.google.com/store/apps/details?id=com.zaneschepke.wireguardautotunnel)
[![F-Droid](https://img.shields.io/static/v1?style=for-the-badge&message=F-Droid&color=1976D2&logo=F-Droid&logoColor=FFFFFF&label=)](https://f-droid.org/packages/com.zaneschepke.wireguardautotunnel/)
[![Personal](https://img.shields.io/static/v1?style=for-the-badge&message=Personal&color=1976D2&logo=F-Droid&logoColor=FFFFFF&label=)](https://github.com/zaneschepke/fdroid)
[![Obtainium](https://img.shields.io/badge/Obtainium-414141?style=for-the-badge&logo=Obtainium&logoColor=white)](https://apps.obtainium.imranr.dev/redirect?r=obtainium://app/%7B%22id%22%3A%22com.zaneschepke.wireguardautotunnel%22%2C%22url%22%3A%22https%3A%2F%2Fgithub.com%2Fzaneschepke%2Fwgtunnel%22%2C%22author%22%3A%22zaneschepke%22%2C%22name%22%3A%22WG%20Tunnel%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Atrue%2C%5C%22sortMethodChoice%5C%22%3A%5C%22date%5C%22%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Atrue%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22WG%20Tunnel%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22Zane%20Schepke%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%7D%22%2C%22overrideSource%22%3Anull%7D)
</div>
<div align="center">
[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?style=for-the-badge&logo=discord&logoColor=white)](https://discord.gg/rbRRNh6H7V)
[![Telegram](https://img.shields.io/badge/Telegram-2CA5E0?style=for-the-badge&logo=telegram&logoColor=white)](https://t.me/wgtunnel)
[<img src="https://img.shields.io/badge/Telegram-26A5E4.svg?style=for-the-badge&logo=Telegram&logoColor=white">](https://t.me/wgtunnel)
[<img src="https://img.shields.io/badge/Matrix-000000.svg?style=for-the-badge&logo=Matrix&logoColor=white">](https://matrix.to/#/#wg-tunnel:matrix.zaneschepke.com)
</div>
<details open="open">
@@ -49,7 +49,7 @@ and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/)
<div style="text-align: left;">
## About
Inspired by the official [wireguard-android](https://github.com/WireGuard/wireguard-android) app, WG Tunnel was created to address features and support missing from the official app. This app combines support for both [WireGuard®](https://www.wireguard.com/)
Inspired by the official [wireguard-android](https://github.com/WireGuard/wireguard-android) app, WG Tunnel was created to address features and support missing from the official app. This app combines support for both [WireGuard](https://www.wireguard.com/)
and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/), with its primary feature of auto-tunneling (on-demand tunneling).
</div>
@@ -61,14 +61,14 @@ and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/), with its pr
Thank you to the following:
- All of the users that have helped contribute to the project with ideas, translations, feedback, bug reports, testing, and donations.
- [WireGuard®](https://www.wireguard.com/) - © Jason A. Donenfeld (https://github.com/WireGuard/wireguard-android)
- [WireGuard](https://www.wireguard.com/) - Jason A. Donenfeld (https://github.com/WireGuard/wireguard-android)
- [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) - Amnezia Team (https://github.com/amnezia-vpn/amneziawg-android)
## Screenshots
</div>
<div style="display: flex; flex-wrap: wrap; justify-content: center; gap: 10px;">
<div style="display: flex; flex-wrap: wrap; justify-content: left; gap: 10px;">
<img label="Main" src="fastlane/metadata/android/en-US/images/phoneScreenshots/main_screen.png" width="200" />
<img label="Settings" src="fastlane/metadata/android/en-US/images/phoneScreenshots/settings_screen.png" width="200" />
<img label="Auto" src="fastlane/metadata/android/en-US/images/phoneScreenshots/auto_screen.png" width="200" />
+1 -1
View File
@@ -39,4 +39,4 @@
-dontwarn org.joda.time.Instant
-dontwarn org.slf4j.impl.StaticLoggerBinder
-dontwarn org.slf4j.impl.StaticMDCBinder
-dontwarn org.slf4j.impl.StaticMarkerBinder
-dontwarn org.slf4j.impl.StaticMarkerBinder
+1 -1
View File
@@ -58,4 +58,4 @@
-dontwarn org.joda.time.Instant
-dontwarn org.slf4j.impl.StaticLoggerBinder
-dontwarn org.slf4j.impl.StaticMDCBinder
-dontwarn org.slf4j.impl.StaticMarkerBinder
-dontwarn org.slf4j.impl.StaticMarkerBinder
@@ -1,6 +1,8 @@
package com.zaneschepke.wireguardautotunnel
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Color
import android.os.Build
import android.os.Bundle
@@ -32,12 +34,14 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.toRoute
import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
@@ -68,6 +72,7 @@ import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
import dagger.hilt.android.AndroidEntryPoint
import timber.log.Timber
import javax.inject.Inject
import kotlin.system.exitProcess
@@ -83,6 +88,11 @@ class MainActivity : AppCompatActivity() {
@Inject
lateinit var shortcutManager: ShortcutManager
@Inject
lateinit var networkMonitor: NetworkMonitor
private var lastLocationPermissionState: Boolean? = null
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge(
statusBarStyle = SystemBarStyle.Companion.auto(Color.TRANSPARENT, Color.TRANSPARENT),
@@ -256,4 +266,22 @@ class MainActivity : AppCompatActivity() {
}
}
}
override fun onResume() {
super.onResume()
checkPermissionAndNotify()
}
private fun checkPermissionAndNotify() {
val hasLocation = ContextCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_FINE_LOCATION,
) == PackageManager.PERMISSION_GRANTED
if (lastLocationPermissionState != hasLocation) {
Timber.d("Location permission changed to: $hasLocation")
if (hasLocation) {
networkMonitor.sendLocationPermissionsGrantedBroadcast()
}
lastLocationPermissionState = hasLocation
}
}
}
@@ -96,7 +96,7 @@ class WireGuardAutoTunnel : Application(), Configuration.Provider {
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())
@@ -4,8 +4,6 @@ import android.app.Service
import android.content.Context
import android.content.Intent
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
@@ -20,7 +18,6 @@ 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
@@ -36,8 +33,6 @@ class ServiceManager @Inject constructor(
var autoTunnelService = CompletableDeferred<AutoTunnelService>()
var backgroundService = CompletableDeferred<TunnelForegroundService>()
var autoTunnelTile = CompletableDeferred<AutoTunnelControlTile>()
var tunnelControlTile = CompletableDeferred<TunnelControlTile>()
private fun <T : Service> startService(cls: Class<T>, background: Boolean) {
runCatching {
@@ -61,19 +56,16 @@ class ServiceManager @Inject constructor(
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 }
}
}
updateAutoTunnelTile()
}
fun startBackgroundService(tunnelConf: TunnelConf) {
fun startTunnelForegroundService(tunnelConf: TunnelConf) {
applicationScope.launch(ioDispatcher) {
if (backgroundService.isCompleted) return@launch
runCatching {
@@ -88,7 +80,19 @@ class ServiceManager @Inject constructor(
}
}
fun stopBackgroundService() {
fun updateTunnelForegroundServiceNotification(tunnelConf: TunnelConf) {
applicationScope.launch(ioDispatcher) {
if (!backgroundService.isCompleted) return@launch
runCatching {
val service = backgroundService.await()
service.start(tunnelConf)
}.onFailure {
Timber.e(it)
}
}
}
fun stopTunnelForegroundService() {
applicationScope.launch(ioDispatcher) {
if (!backgroundService.isCompleted) return@launch
runCatching {
@@ -107,34 +111,12 @@ class ServiceManager @Inject constructor(
}
}
suspend fun updateAutoTunnelTile() {
withContext(ioDispatcher) {
runCatching {
val service = withTimeoutOrNull(SERVICE_START_TIMEOUT) { autoTunnelTile.await() }
?: run {
context.requestAutoTunnelTileServiceUpdate()
return@withContext
}
service.updateTileState()
}.onFailure {
Timber.e(it)
}
}
fun updateAutoTunnelTile() {
context.requestAutoTunnelTileServiceUpdate()
}
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 updateTunnelTile() {
context.requestTunnelTileServiceStateUpdate()
}
fun stopAutoTunnel() {
@@ -49,12 +49,12 @@ class TunnelForegroundService : LifecycleService() {
}
fun stop() {
stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf()
}
override fun onDestroy() {
serviceManager.backgroundService = CompletableDeferred()
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
super.onDestroy()
}
@@ -94,9 +94,11 @@ class AutoTunnelService : LifecycleService() {
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
Timber.d("onStartCommand executed with startId: $startId")
serviceManager.autoTunnelService.complete(this)
return super.onStartCommand(intent, flags, startId)
start()
return START_STICKY
}
fun start() {
@@ -178,8 +180,8 @@ class AutoTunnelService : LifecycleService() {
combineSettings(),
appDataRepository.get().settings.flow
.distinctUntilChanged { old, new -> old.isKernelEnabled == new.isKernelEnabled } // Only emit when isKernelEnabled changes
.flatMapLatest { settings ->
networkMonitor.getNetworkStatusFlow(true, settings.isKernelEnabled)
.flatMapLatest {
networkMonitor.networkStatusFlow
.flowOn(ioDispatcher)
.map { buildNetworkState(it) }
}
@@ -4,57 +4,61 @@ import android.content.Intent
import android.os.IBinder
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.lifecycleScope
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class AutoTunnelControlTile : TileService() {
class AutoTunnelControlTile : TileService(), LifecycleOwner {
@Inject
lateinit var appDataRepository: AppDataRepository
@Inject
lateinit var serviceManager: ServiceManager
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
override fun onCreate() {
super.onCreate()
serviceManager.autoTunnelTile.complete(this)
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
}
override fun onDestroy() {
super.onDestroy()
serviceManager.autoTunnelTile = CompletableDeferred()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
}
override fun onStartListening() {
super.onStartListening()
serviceManager.autoTunnelTile.complete(this)
applicationScope.launch {
if (appDataRepository.tunnels.getAll().isEmpty()) return@launch setUnavailable()
updateTileState()
Timber.d("Start listening called for auto tunnel tile")
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
lifecycleScope.launch {
serviceManager.autoTunnelActive.collect {
if (it) setActive() else setInactive()
}
}
}
fun updateTileState() {
serviceManager.autoTunnelActive.value.let {
if (it) setActive() else setInactive()
lifecycleScope.launch {
appDataRepository.tunnels.flow.collect {
if (it.isEmpty()) {
setUnavailable()
} else {
if (qsTile.state == Tile.STATE_ACTIVE) setInactive()
}
}
}
}
override fun onClick() {
super.onClick()
unlockAndRun {
applicationScope.launch {
lifecycleScope.launch {
if (serviceManager.autoTunnelActive.value) {
serviceManager.stopAutoTunnel()
setInactive()
@@ -97,4 +101,7 @@ class AutoTunnelControlTile : TileService() {
qsTile.updateTile()
}
}
override val lifecycle: Lifecycle
get() = lifecycleRegistry
}
@@ -5,58 +5,63 @@ import android.os.Build
import android.os.IBinder
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.lifecycleScope
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class TunnelControlTile : TileService() {
class TunnelControlTile : TileService(), LifecycleOwner {
@Inject
lateinit var appDataRepository: AppDataRepository
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
@Inject
lateinit var serviceManager: ServiceManager
@Inject
lateinit var tunnelManager: TunnelManager
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
private var isCollecting = false
override fun onCreate() {
super.onCreate()
serviceManager.tunnelControlTile.complete(this)
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
}
override fun onDestroy() {
super.onDestroy()
serviceManager.tunnelControlTile = CompletableDeferred()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
}
override fun onStartListening() {
super.onStartListening()
Timber.d("Start listening called")
serviceManager.tunnelControlTile.complete(this)
applicationScope.launch {
updateTileState()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
Timber.d("Start listening called for tunnel tile")
if (isCollecting) return
isCollecting = true
lifecycleScope.launch {
tunnelManager.activeTunnels.collect {
updateTileState()
}
}
}
fun updateTileState() = applicationScope.launch {
private fun updateTileState() = lifecycleScope.launch {
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) }
tunnels.firstOrNull { it.id == keys.first().id }?.let { return@launch updateTile(it.tunName, true) }
} else {
return@launch updateTile(getString(R.string.multiple), true)
}
@@ -69,7 +74,7 @@ class TunnelControlTile : TileService() {
override fun onClick() {
super.onClick()
unlockAndRun {
applicationScope.launch {
lifecycleScope.launch {
if (tunnelManager.activeTunnels.value.isNotEmpty()) return@launch tunnelManager.stopTunnel()
appDataRepository.getStartTunnelConfig()?.let {
tunnelManager.startTunnel(it)
@@ -132,4 +137,6 @@ class TunnelControlTile : TileService() {
Timber.e(it)
}
}
override val lifecycle: Lifecycle
get() = lifecycleRegistry
}
@@ -6,6 +6,7 @@ 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.service.autotunnel.AutoTunnelService
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
@@ -21,6 +22,9 @@ class ShortcutsActivity : ComponentActivity() {
@Inject
lateinit var serviceManager: ServiceManager
@Inject
lateinit var tunnelManager: TunnelManager
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
@@ -39,13 +43,13 @@ class ShortcutsActivity : ComponentActivity() {
.firstOrNull { it.tunName == tunnelName }
} ?: appDataRepository.getStartTunnelConfig()
Timber.d("Shortcut action on name: ${tunnelConfig?.tunName}")
// tunnelConfig?.let {
// when (intent.action) {
// Action.START.name -> tunnelService.get().startTunnel(it)
// Action.STOP.name -> tunnelService.get().stopTunnel()
// else -> Unit
// }
// }
tunnelConfig?.let {
when (intent.action) {
Action.START.name -> tunnelManager.startTunnel(it)
Action.STOP.name -> tunnelManager.stopTunnel()
else -> Unit
}
}
}
AutoTunnelService::class.java.simpleName, LEGACY_AUTO_TUNNEL_SERVICE_NAME -> {
when (intent.action) {
@@ -1,5 +1,6 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.wireguard.android.backend.BackendException
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.networkmonitor.NetworkStatus
@@ -8,12 +9,10 @@ import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
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.TunnelProvider.Companion.CHECK_INTERVAL
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.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
@@ -23,23 +22,23 @@ 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 com.zaneschepke.wireguardautotunnel.util.extensions.toBackendError
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import timber.log.Timber
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicBoolean
open class BaseTunnel(
abstract class BaseTunnel(
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
@ApplicationScope private val applicationScope: CoroutineScope,
private val networkMonitor: NetworkMonitor,
@@ -48,167 +47,71 @@ open class BaseTunnel(
private val notificationManager: NotificationManager,
) : TunnelProvider {
internal val tunnels = MutableStateFlow<List<TunnelConf>>(emptyList())
companion object {
const val CHECK_INTERVAL = 1000L
}
private val _tunnelStates = MutableStateFlow<Map<Int, TunnelState>>(emptyMap())
protected val activeTuns = MutableStateFlow<Map<TunnelConf, TunnelState>>(emptyMap())
override val activeTunnels = activeTuns.asStateFlow()
private val tunnelJobs = ConcurrentHashMap<Int, Job>()
private val tunnelJobs = ConcurrentHashMap<Int, MutableList<Job>>()
private val isNetworkAvailable = AtomicBoolean(false)
protected val mutex = Mutex()
private val isNetworkConnected = MutableStateFlow(true)
init {
applicationScope.launch(ioDispatcher) {
launch { startNetworkJob() }
launch { monitorNetworkStatus() }
launch { monitorTunnelConfigChanges() }
tunnels.collect { tuns ->
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} (ID: ${tun.id})")
tunnelJobs[tun.id] = startTunnelJobs(tun)
}
removedTunIds.forEach { tunId ->
tunnelJobs[tunId]?.cancelWithMessage("Canceling tunnel jobs for tunnel ID: $tunId")
tunnelJobs.remove(tunId)
_tunnelStates.update { it - tunId }
serviceManager.updateTunnelTile()
}
}
}
}
private fun startTunnelJobs(tunnel: TunnelConf) = applicationScope.launch(ioDispatcher) {
launch { startTunnelStatisticsJob(tunnel) }
if (tunnel.isPingEnabled) launch { startPingJob(tunnel) }
}
private fun updateTunnelState(tunnelId: Int, newState: TunnelStatus) {
Timber.d("Updating tunnel state for ID $tunnelId to $newState")
_tunnelStates.update { current ->
val currentState = current[tunnelId]
val updatedState = currentState?.copy(state = newState) ?: TunnelState(state = newState)
val newMap = current + (tunnelId to updatedState)
Timber.d("New tunnel states: $newMap")
newMap
private fun startTunnelJobs(tunnel: TunnelConf): Job {
return applicationScope.launch(ioDispatcher) {
val jobs = mutableListOf<Job>()
jobs += launch { updateTunnelStatistics(tunnel) }
if (tunnel.isPingEnabled) jobs += launch { monitorTunnelPing(tunnel) }
jobs.forEach { it.join() }
}
}
internal fun beforeStartTunnel(tunnelConf: TunnelConf) {
tunnelConf.setStateChangeCallback { state ->
Timber.d("New tunnel state $state")
when (state) {
is Tunnel.State -> updateTunnelState(tunnelConf.id, state.asTunnelState())
is org.amnezia.awg.backend.Tunnel.State -> updateTunnelState(tunnelConf.id, state.asTunnelState())
}
applicationScope.launch(ioDispatcher) {
serviceManager.updateTunnelTile()
}
}
}
override fun startTunnel(tunnelConf: TunnelConf) {
applicationScope.launch(ioDispatcher) {
serviceManager.startBackgroundService(tunnelConf)
appDataRepository.tunnels.save(tunnelConf.copy(isActive = true))
addToActiveTunnels(tunnelConf)
}
}
override fun stopTunnel(tunnelConf: TunnelConf?) {
// Default empty implementation; subclasses override
}
override suspend fun bounceTunnel(tunnelConf: TunnelConf) {
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 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 fun stopAllTunnels() {
tunnels.value.forEach {
stopTunnel(it)
}
}
private fun addToActiveTunnels(conf: TunnelConf) {
tunnels.update {
it.toMutableList().apply {
add(conf)
}
}
}
private fun removeFromActiveTunnels(conf: TunnelConf) {
tunnels.update {
it.toMutableList().apply {
remove(conf)
}
}
}
private suspend fun startNetworkJob() = coroutineScope {
networkMonitor.getNetworkStatusFlow(includeWifiSsid = false, useRootShell = false)
.flowOn(ioDispatcher).collect {
isNetworkAvailable.set(it !is NetworkStatus.Disconnected)
}
}
private suspend fun startPingJob(tunnel: TunnelConf) = coroutineScope {
while (isActive) {
private suspend fun updateTunnelStatistics(tunnel: TunnelConf) {
while (true) {
runCatching {
if (isNetworkAvailable.get() && tunnel.isActive) {
val stats = getStatistics(tunnel)
updateTunnelState(tunnel, stats = stats)
}.onFailure { e ->
Timber.e(e, "Failed to update stats for ${tunnel.tunName}")
}
delay(CHECK_INTERVAL)
}
}
private suspend fun monitorTunnelPing(tunnel: TunnelConf) {
while (true) {
runCatching {
if (isNetworkConnected.value && tunnel.isActive) {
val pingSuccess = tunnel.isTunnelPingable(ioDispatcher)
handlePingResult(tunnel, pingSuccess)
if (!pingSuccess) bounceTunnel(tunnel)
}
delay(tunnel.pingInterval ?: Constants.PING_INTERVAL)
}.onFailure { e ->
Timber.e(e, "Ping failed for ${tunnel.tunName}")
}
delay(tunnel.pingInterval ?: Constants.PING_INTERVAL)
}
}
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)
delay(tunnel.pingCooldown ?: Constants.PING_COOLDOWN)
} else {
Timber.i("Ping result: target was not reachable, but no network available")
}
} else {
Timber.i("Ping result: all ping targets were reached successfully")
protected fun handleBackendThrowable(throwable: Throwable) {
val backendError = when (throwable) {
is BackendException -> throwable.toBackendError()
is org.amnezia.awg.backend.BackendException -> throwable.toBackendError()
else -> BackendError.Unknown
}
}
internal fun handleBackendThrowable(backendError: BackendError) {
val message = when (backendError) {
BackendError.Config -> StringValue.StringResource(R.string.start_failed_config)
BackendError.DNS -> StringValue.StringResource(R.string.dns_error)
BackendError.Unauthorized -> StringValue.StringResource(R.string.unauthorized)
BackendError.Unknown -> StringValue.StringResource(R.string.unknown_error)
}
if (WireGuardAutoTunnel.isForeground()) {
SnackbarController.showMessage(message)
@@ -224,33 +127,133 @@ open class BaseTunnel(
}
}
private suspend fun monitorTunnelConfigChanges() = coroutineScope {
appDataRepository.tunnels.flow.collect { storageTuns ->
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)
protected fun updateTunnelState(tunnelConf: TunnelConf, state: TunnelStatus? = null, stats: TunnelStatistics? = null) {
applicationScope.launch(ioDispatcher) {
mutex.withLock {
activeTuns.update { current ->
val originalConf = current.getKeyById(tunnelConf.id) ?: tunnelConf
val existingState = current.getValueById(tunnelConf.id) ?: TunnelState()
val newState = state ?: existingState.state
if (newState == TunnelStatus.DOWN) {
// Remove tunnel from activeTunnels when it goes DOWN
Timber.d("Removing tunnel ${tunnelConf.id} from activeTunnels as state is DOWN")
current - originalConf
} else if (existingState.state == newState && stats == null) {
Timber.d("Skipping redundant state update for ${tunnelConf.id}: $newState")
current
} else {
val updated = existingState.copy(
state = newState,
statistics = stats ?: existingState.statistics,
)
current + (originalConf to updated)
}
}
}
}
}
private suspend fun startTunnelStatisticsJob(tunnel: TunnelConf) = coroutineScope {
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)
protected suspend fun configureTunnel(tunnelConf: TunnelConf) {
// setup state change callback
tunnelConf.setStateChangeCallback { state ->
Timber.d("State change callback triggered for tunnel ${tunnelConf.id}: ${tunnelConf.tunName} with state $state at ${System.currentTimeMillis()}")
when (state) {
is Tunnel.State -> updateTunnelState(tunnelConf, state.asTunnelState())
is org.amnezia.awg.backend.Tunnel.State -> updateTunnelState(tunnelConf, state.asTunnelState())
}
applicationScope.launch(ioDispatcher) { serviceManager.updateTunnelTile() }
}
activeTuns.update { current ->
current.filter { it.key != tunnelConf } + (tunnelConf to TunnelState())
}
}
protected suspend fun onStartSuccess(tunnelConf: TunnelConf) {
val tunnelCopy = tunnelConf.copyWithCallback(isActive = true)
// start service
if (activeTuns.value.isEmpty()) {
serviceManager.startTunnelForegroundService(tunnelCopy)
} else {
serviceManager.updateTunnelForegroundServiceNotification(tunnelCopy)
}
// save active
appDataRepository.tunnels.save(tunnelCopy)
// start tunnel jobs
tunnelJobs[tunnelCopy.id] = mutableListOf(startTunnelJobs(tunnelConf))
}
override fun startTunnel(tunnelConf: TunnelConf) {
throw NotImplementedError("Must be implemented by subclass")
}
override fun stopTunnel(tunnelConf: TunnelConf?) {
tunnelConf?.let {
applicationScope.launch(ioDispatcher) {
mutex.withLock {
removeActiveTunnel(tunnelConf)
tunnelJobs[tunnelConf.id]?.forEach { it.cancelWithMessage("Cancel tunnel job") }
tunnelJobs.remove(tunnelConf.id)
val lockedConf = it.copyWithCallback(isActive = false)
appDataRepository.tunnels.save(lockedConf)
// TODO improve to handle multiple tunnels
if (activeTuns.value.isEmpty()) {
Timber.d("No tunnels active, stopping background service")
serviceManager.stopTunnelForegroundService()
} else {
Timber.d("Other tunnels still active, updating service notification")
val nextActive = activeTuns.value.keys.firstOrNull()
if (nextActive != null) {
Timber.d("Next active tunnel: ${nextActive.id}")
serviceManager.updateTunnelForegroundServiceNotification(nextActive)
}
}
}
delay(CHECK_INTERVAL)
}.onFailure { exception ->
Timber.e(exception, "Failed to update tunnel statistics for ${tunnel.tunName}")
}
}
}
private fun removeActiveTunnel(tunnelConf: TunnelConf) {
activeTuns.update { current ->
current.toMutableMap().apply { remove(tunnelConf) }
}
}
override suspend fun bounceTunnel(tunnelConf: TunnelConf) {
stopTunnel(tunnelConf)
delay(1000)
startTunnel(tunnelConf)
}
private suspend fun monitorNetworkStatus() {
networkMonitor.networkStatusFlow
.flowOn(ioDispatcher)
.collectLatest { status ->
val isAvailable = status !is NetworkStatus.Disconnected
isNetworkConnected.value = isAvailable
Timber.d("Network status: $isAvailable")
}
}
private suspend fun monitorTunnelConfigChanges() {
appDataRepository.tunnels.flow.collectLatest { storedTunnels ->
mutex.withLock {
storedTunnels.forEach { stored ->
val current = activeTuns.value.keys.find { it.id == stored.id }
if (current != null && !current.isQuickConfigMatching(stored)) {
Timber.d("Config changed for ${stored.id}, bouncing")
bounceTunnel(stored)
}
}
}
}
}
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics? {
throw NotImplementedError("Must be implemented by subclass")
}
override suspend fun runningTunnelNames(): Set<String> = activeTuns.value.keys.map { it.tunName }.toSet()
}
@@ -0,0 +1,25 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
fun Map<TunnelConf, TunnelState>.allDown(): Boolean {
return this.all { it.value.state.isDown() }
}
fun Map<TunnelConf, TunnelState>.hasActive(): Boolean {
return this.any { it.value.state.isUp() }
}
fun Map<TunnelConf, TunnelState>.getValueById(id: Int): TunnelState? {
val key = this.keys.find { it.id == id }
return key?.let { this@getValueById[it] }
}
fun Map<TunnelConf, TunnelState>.getKeyById(id: Int): TunnelConf? {
return this.keys.find { it.id == id }
}
fun Map<TunnelConf, TunnelState>.isUp(tunnelConf: TunnelConf): Boolean {
return this.getValueById(tunnelConf.id)?.state?.isUp() ?: false
}
@@ -1,7 +1,6 @@
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.networkmonitor.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
@@ -10,13 +9,15 @@ 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 com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.withLock
import timber.log.Timber
import javax.inject.Inject
@@ -31,40 +32,60 @@ class KernelTunnel @Inject constructor(
) : BaseTunnel(ioDispatcher, applicationScope, networkMonitor, appDataRepository, serviceManager, notificationManager) {
override fun startTunnel(tunnelConf: TunnelConf) {
Timber.d("Starting tunnel ${tunnelConf.id} kernel")
Timber.i("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())
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())
} else {
Timber.e(it)
// tunnel already active
if (activeTuns.value.any { it.key.id == tunnelConf.id }) return@launch
mutex.withLock {
updateTunnelState(tunnelConf, TunnelStatus.STARTING)
// configure state callback and add to tunnels
configureTunnel(tunnelConf)
updateTunnelState(tunnelConf, backend.setState(tunnelConf, Tunnel.State.UP, tunnelConf.toWgConfig()).asTunnelState())
// run some actions after start success
onStartSuccess(tunnelConf)
}
}.onFailure { exception ->
Timber.e(exception, "Failed to start tunnel ${tunnelConf.id} kernel")
stopTunnel(tunnelConf)
handleBackendThrowable(exception)
}.onSuccess {
Timber.i("Tunnel ${tunnelConf.id} started successfully")
}
}
}
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics {
return WireGuardStatistics(backend.getStatistics(tunnelConf))
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics? {
return try {
WireGuardStatistics(backend.getStatistics(tunnelConf))
} catch (e: Exception) {
Timber.e(e)
null
}
}
override fun stopTunnel(tunnelConf: TunnelConf?) {
applicationScope.launch(ioDispatcher) {
runCatching {
tunnels.value.firstOrNull { it.id == tunnelConf?.id }?.let {
backend.setState(it, Tunnel.State.DOWN, it.toWgConfig())
onTunnelStop(it)
} ?: stopAllTunnels()
}.onFailure {
Timber.e(it)
val originalTunnel = activeTuns.value.keys.find { it.id == tunnelConf?.id }
if (originalTunnel != null) {
Timber.i("Stopping tunnel ${originalTunnel.id} kernel")
mutex.withLock {
updateTunnelState(originalTunnel, backend.setState(originalTunnel, Tunnel.State.DOWN, originalTunnel.toWgConfig()).asTunnelState())
super.stopTunnel(originalTunnel)
}
} else {
Timber.w("Tunnel not found in startedTunnels, stopping all tunnels")
activeTuns.value.keys.forEach { config ->
stopTunnel(config)
}
}
}.onFailure { e ->
Timber.e(e, "Failed to stop tunnel ${tunnelConf?.id}")
}
}
}
@@ -76,7 +76,7 @@ class TunnelManager @Inject constructor(
return tunnelProviderFlow.value.runningTunnelNames()
}
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics {
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics? {
return tunnelProviderFlow.value.getStatistics(tunnelConf)
}
@@ -84,7 +84,7 @@ class TunnelManager @Inject constructor(
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 } }
val tunsToStart = previouslyActiveTuns.filterNot { tun -> activeTunnels.value.any { tun.id == it.key.id } }
if (settings.isKernelEnabled) {
return@launch tunsToStart.forEach {
startTunnel(it)
@@ -7,15 +7,11 @@ import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import kotlinx.coroutines.flow.StateFlow
interface TunnelProvider {
companion object {
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>>
fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics?
val activeTunnels: StateFlow<Map<TunnelConf, TunnelState>>
}
@@ -1,6 +1,5 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.wireguard.android.backend.BackendException
import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
@@ -8,14 +7,17 @@ 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
import com.zaneschepke.wireguardautotunnel.util.extensions.asAmBackendState
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendError
import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.withLock
import org.amnezia.awg.backend.Backend
import org.amnezia.awg.backend.Tunnel
import timber.log.Timber
@@ -32,27 +34,41 @@ class UserspaceTunnel @Inject constructor(
) : BaseTunnel(ioDispatcher, applicationScope, networkMonitor, appDataRepository, serviceManager, notificationManager) {
override fun startTunnel(tunnelConf: TunnelConf) {
Timber.i("Starting tunnel ${tunnelConf.id} userspace")
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())
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())
} else {
Timber.e(it)
// tunnel already active
if (activeTuns.value.any { it.key.id == tunnelConf.id }) return@launch
// stop any active tunnels that aren't this one, userspace only
stopActiveTunnels()
mutex.withLock {
updateTunnelState(tunnelConf, TunnelStatus.STARTING)
// configure state callback and add to tunnels
configureTunnel(tunnelConf)
updateTunnelState(tunnelConf, backend.setState(tunnelConf, Tunnel.State.UP, tunnelConf.toAmConfig()).asTunnelState())
// run some actions after start success
onStartSuccess(tunnelConf)
}
}.onFailure { exception ->
Timber.e(exception, "Failed to start tunnel ${tunnelConf.id} userspace")
stopTunnel(tunnelConf)
handleBackendThrowable(exception)
}.onSuccess {
Timber.i("Tunnel ${tunnelConf.id} started successfully")
}
}
}
private suspend fun stopActiveTunnels() {
activeTunnels.value.forEach { (config, state) ->
if (state.state.isUp()) {
stopTunnel(config)
delay(300)
}
}
}
@@ -60,17 +76,27 @@ class UserspaceTunnel @Inject constructor(
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())
onTunnelStop(it)
} ?: stopAllTunnels()
}.onFailure {
Timber.e(it)
val originalTunnel = activeTuns.value.keys.find { it.id == tunnelConf?.id }
if (originalTunnel != null) {
Timber.i("Stopping tunnel ${originalTunnel.id} userspace")
mutex.withLock {
updateTunnelState(originalTunnel, backend.setState(originalTunnel, Tunnel.State.DOWN, originalTunnel.toAmConfig()).asTunnelState())
super.stopTunnel(originalTunnel)
}
} else {
Timber.w("Tunnel not found in startedTunnels, stopping all tunnels")
activeTuns.value.keys.forEach { config ->
stopTunnel(config)
}
}
}.onFailure { e ->
Timber.e(e, "Failed to stop tunnel ${tunnelConf?.id}")
}
}
}
override suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
Timber.d("Setting backend state: $backendState with allowedIps: $allowedIps")
backend.setBackendState(backendState.asAmBackendState(), allowedIps)
}
@@ -78,7 +104,12 @@ class UserspaceTunnel @Inject constructor(
return backend.runningTunnelNames
}
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics {
return AmneziaStatistics(backend.getStatistics(tunnelConf))
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics? {
return try {
AmneziaStatistics(backend.getStatistics(tunnelConf))
} catch (e: Exception) {
Timber.e(e, "Failed to get stats for ${tunnelConf.tunName}")
null
}
}
}
@@ -13,6 +13,7 @@ import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
import com.zaneschepke.wireguardautotunnel.core.tunnel.UserspaceTunnel
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.AppSettingRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -20,6 +21,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.runBlocking
import org.amnezia.awg.backend.Backend
import org.amnezia.awg.backend.GoBackend
import org.amnezia.awg.backend.RootTunnelActionHandler
@@ -101,8 +103,8 @@ class TunnelModule {
@Provides
@Singleton
fun provideNetworkMonitor(@ApplicationContext context: Context): NetworkMonitor {
return AndroidNetworkMonitor(context)
fun provideNetworkMonitor(@ApplicationContext context: Context, settingsRepository: AppSettingRepository): NetworkMonitor {
return AndroidNetworkMonitor(context) { runBlocking { settingsRepository.get().isWifiNameByShellEnabled } }
}
@Singleton
@@ -1,16 +1,19 @@
package com.zaneschepke.wireguardautotunnel.domain.entity
import com.wireguard.android.backend.Tunnel
import com.wireguard.config.Config
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.isReachable
import com.zaneschepke.wireguardautotunnel.util.extensions.toWgQuickString
import kotlinx.coroutines.withContext
import kotlinx.serialization.Transient
import org.amnezia.awg.backend.Tunnel
import timber.log.Timber
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.io.InputStream
import java.net.InetAddress
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.nio.charset.StandardCharsets
data class TunnelConf(
val id: Int = 0,
@@ -26,13 +29,42 @@ data class TunnelConf(
val pingCooldown: Long? = null,
val pingIp: String? = null,
val isEthernetTunnel: Boolean = false,
val isIpv4Preferred: Boolean = false,
val isIpv4Preferred: Boolean = true,
@Transient
private var stateChangeCallback: ((Any) -> Unit)? = null,
) : Tunnel, com.wireguard.android.backend.Tunnel {
) : Tunnel, org.amnezia.awg.backend.Tunnel {
fun setStateChangeCallback(callback: (Any) -> Unit) {
stateChangeCallback = callback
private val callbackMutex = Mutex()
suspend fun setStateChangeCallback(callback: (Any) -> Unit) {
callbackMutex.withLock {
stateChangeCallback = callback
}
}
fun copyWithCallback(
id: Int = this.id,
tunName: String = this.tunName,
wgQuick: String = this.wgQuick,
tunnelNetworks: List<String> = this.tunnelNetworks,
isMobileDataTunnel: Boolean = this.isMobileDataTunnel,
isPrimaryTunnel: Boolean = this.isPrimaryTunnel,
amQuick: String = this.amQuick,
isActive: Boolean = this.isActive,
isPingEnabled: Boolean = this.isPingEnabled,
pingInterval: Long? = this.pingInterval,
pingCooldown: Long? = this.pingCooldown,
pingIp: String? = this.pingIp,
isEthernetTunnel: Boolean = this.isEthernetTunnel,
isIpv4Preferred: Boolean = this.isIpv4Preferred,
): TunnelConf {
return TunnelConf(
id, tunName, wgQuick, tunnelNetworks, isMobileDataTunnel, isPrimaryTunnel,
amQuick, isActive, isPingEnabled, pingInterval, pingCooldown, pingIp,
isEthernetTunnel, isIpv4Preferred,
).apply {
stateChangeCallback = this@TunnelConf.stateChangeCallback
}
}
fun toAmConfig(): org.amnezia.awg.config.Config {
@@ -43,25 +75,40 @@ data class TunnelConf(
return configFromWgQuick(wgQuick)
}
override fun getName(): String {
return tunName
}
override fun getName(): String = tunName
override fun isIpv4ResolutionPreferred(): Boolean {
return isIpv4Preferred
}
override fun isIpv4ResolutionPreferred(): Boolean = isIpv4Preferred
override fun onStateChange(newState: com.wireguard.android.backend.Tunnel.State) {
stateChangeCallback?.invoke(newState)
override fun onStateChange(newState: org.amnezia.awg.backend.Tunnel.State) {
Timber.d("onStateChange called for tunnel $id: $tunName with state $newState")
runBlocking {
callbackMutex.withLock {
if (stateChangeCallback != null) {
Timber.d("Invoking stateChangeCallback for tunnel $id: $tunName with state $newState")
stateChangeCallback?.invoke(newState)
} else {
Timber.w("No stateChangeCallback set for tunnel $id: $tunName")
}
}
}
}
override fun onStateChange(newState: Tunnel.State) {
stateChangeCallback?.invoke(newState)
Timber.d("onStateChange called for tunnel $id: $tunName with state $newState")
runBlocking {
callbackMutex.withLock {
if (stateChangeCallback != null) {
Timber.d("Invoking stateChangeCallback for tunnel $id: $tunName with state $newState")
stateChangeCallback?.invoke(newState)
} else {
Timber.w("No stateChangeCallback set for tunnel $id: $tunName")
}
}
}
}
fun isQuickConfigMatching(updatedConf: TunnelConf): Boolean {
return updatedConf.wgQuick == wgQuick ||
updatedConf.amQuick == amQuick
return updatedConf.wgQuick == wgQuick || updatedConf.amQuick == amQuick
}
fun isPingConfigMatching(updatedConf: TunnelConf): Boolean {
@@ -78,7 +125,6 @@ data class TunnelConf(
return@withContext InetAddress.getByName(pingIp)
.isReachable(Constants.PING_TIMEOUT.toInt())
}
Timber.i("Pinging all peers")
config.peers.map { peer ->
peer.isReachable(isIpv4Preferred)
}.all { true }
@@ -88,14 +134,14 @@ data class TunnelConf(
companion object {
fun configFromWgQuick(wgQuick: String): Config {
val inputStream: InputStream = wgQuick.byteInputStream()
return inputStream.bufferedReader(Charsets.UTF_8).use {
return inputStream.bufferedReader(StandardCharsets.UTF_8).use {
Config.parse(it)
}
}
fun configFromAmQuick(amQuick: String): org.amnezia.awg.config.Config {
val inputStream: InputStream = amQuick.byteInputStream()
return inputStream.bufferedReader(Charsets.UTF_8).use {
return inputStream.bufferedReader(StandardCharsets.UTF_8).use {
org.amnezia.awg.config.Config.parse(it)
}
}
@@ -4,4 +4,5 @@ sealed class BackendError() {
data object DNS : BackendError()
data object Unauthorized : BackendError()
data object Config : BackendError()
data object Unknown : BackendError()
}
@@ -3,6 +3,8 @@ package com.zaneschepke.wireguardautotunnel.domain.enums
enum class TunnelStatus {
UP,
DOWN,
STARTING,
STOPPING,
;
fun isDown(): Boolean {
@@ -1,5 +1,8 @@
package com.zaneschepke.wireguardautotunnel.domain.state
import com.zaneschepke.wireguardautotunnel.core.tunnel.allDown
import com.zaneschepke.wireguardautotunnel.core.tunnel.hasActive
import com.zaneschepke.wireguardautotunnel.core.tunnel.isUp
import com.zaneschepke.wireguardautotunnel.domain.events.KillSwitchEvent
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
@@ -7,7 +10,7 @@ import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
import com.zaneschepke.wireguardautotunnel.util.extensions.isMatchingToWildcardList
data class AutoTunnelState(
val activeTunnels: Map<Int, TunnelState> = emptyMap(),
val activeTunnels: Map<TunnelConf, TunnelState> = emptyMap(),
val networkState: NetworkState = NetworkState(),
val settings: AppSettings = AppSettings(),
val tunnels: List<TunnelConf> = emptyList(),
@@ -20,12 +23,12 @@ data class AutoTunnelState(
private fun isMobileTunnelDataChangeNeeded(): Boolean {
val preferredTunnel = preferredMobileDataTunnel()
return preferredTunnel != null &&
activeTunnels.isNotEmpty() && !activeTunnels.any { it.key == preferredTunnel.id }
activeTunnels.isNotEmpty() && !activeTunnels.isUp(preferredTunnel)
}
private fun isEthernetTunnelChangeNeeded(): Boolean {
val preferredTunnel = preferredEthernetTunnel()
return preferredTunnel != null && activeTunnels.isNotEmpty() && !activeTunnels.any { it.key == preferredTunnel.id }
return preferredTunnel != null && activeTunnels.isNotEmpty() && !activeTunnels.isUp(preferredTunnel)
}
private fun preferredMobileDataTunnel(): TunnelConf? {
@@ -45,11 +48,11 @@ data class AutoTunnelState(
}
private fun startOnEthernet(): Boolean {
return networkState.isEthernetConnected && settings.isTunnelOnEthernetEnabled && activeTunnels.isEmpty()
return networkState.isEthernetConnected && settings.isTunnelOnEthernetEnabled && activeTunnels.allDown()
}
private fun stopOnEthernet(): Boolean {
return networkState.isEthernetConnected && !settings.isTunnelOnEthernetEnabled && activeTunnels.isNotEmpty()
return networkState.isEthernetConnected && !settings.isTunnelOnEthernetEnabled && activeTunnels.hasActive()
}
// TODO test removed kill switch state check
@@ -67,11 +70,11 @@ data class AutoTunnelState(
}
private fun stopOnMobileData(): Boolean {
return isMobileDataActive() && !settings.isTunnelOnMobileDataEnabled && activeTunnels.isNotEmpty()
return isMobileDataActive() && !settings.isTunnelOnMobileDataEnabled && activeTunnels.hasActive()
}
private fun startOnMobileData(): Boolean {
return isMobileDataActive() && settings.isTunnelOnMobileDataEnabled && activeTunnels.isEmpty()
return isMobileDataActive() && settings.isTunnelOnMobileDataEnabled && activeTunnels.allDown()
}
private fun changeOnMobileData(): Boolean {
@@ -83,24 +86,24 @@ data class AutoTunnelState(
}
private fun stopOnWifi(): Boolean {
return isWifiActive() && !settings.isTunnelOnWifiEnabled && activeTunnels.isNotEmpty()
return isWifiActive() && !settings.isTunnelOnWifiEnabled && activeTunnels.hasActive()
}
private fun stopOnTrustedWifi(): Boolean {
return isWifiActive() && settings.isTunnelOnWifiEnabled && activeTunnels.isNotEmpty() && isCurrentSSIDTrusted()
return isWifiActive() && settings.isTunnelOnWifiEnabled && activeTunnels.hasActive() && isCurrentSSIDTrusted()
}
private fun startOnUntrustedWifi(): Boolean {
return isWifiActive() && settings.isTunnelOnWifiEnabled && activeTunnels.isEmpty() && !isCurrentSSIDTrusted()
return isWifiActive() && settings.isTunnelOnWifiEnabled && activeTunnels.allDown() && !isCurrentSSIDTrusted()
}
private fun changeOnUntrustedWifi(): Boolean {
return isWifiActive() && settings.isTunnelOnWifiEnabled && activeTunnels.isNotEmpty() && !isCurrentSSIDTrusted() && !isWifiTunnelPreferred()
return isWifiActive() && settings.isTunnelOnWifiEnabled && activeTunnels.hasActive() && !isCurrentSSIDTrusted() && !isWifiTunnelPreferred()
}
private fun isWifiTunnelPreferred(): Boolean {
val preferred = preferredWifiTunnel()
return activeTunnels.any { it.key == preferred?.id }
return preferred?.let { activeTunnels.isUp(it) } ?: true
}
fun asAutoTunnelEvent(): AutoTunnelEvent {
@@ -690,7 +690,7 @@ fun ConfigScreen(tunnelConf: TunnelConf?, appViewModel: AppViewModel) {
modifier = Modifier.fillMaxWidth(),
)
val presharedKeyEnabled = (tunnelConf == null) || isAuthenticated ||
with(configPair.second?.peers[index]?.preSharedKey) { this?.isEmpty == true || this?.isPresent == false }
with(configPair.second?.peers?.getOrNull(index)?.preSharedKey) { this?.isEmpty == true || this?.isPresent == false }
OutlinedTextField(
textStyle = MaterialTheme.typography.labelLarge,
modifier =
@@ -38,6 +38,7 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.core.tunnel.getValueById
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
@@ -227,7 +228,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
key = { tunnel -> tunnel.id },
) { tunnel ->
val expanded = uiState.generalState.isTunnelStatsExpanded
val tunnelState = activeTunnels.getOrDefault(tunnel.id, TunnelState())
val tunnelState = activeTunnels.getValueById(tunnel.id) ?: TunnelState()
TunnelRowItem(
tunnelState.state.isUp(),
expanded,
@@ -32,6 +32,7 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.core.tunnel.isUp
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
@@ -195,7 +196,7 @@ fun OptionsScreen(tunnelConf: TunnelConf, appUiState: AppUiState, viewModel: Tun
trailing = {
ScaledSwitch(
checked = tunnelConf.isPingEnabled,
enabled = !appUiState.activeTunnels.containsKey(tunnelConf.id),
enabled = !appUiState.activeTunnels.isUp(tunnelConf),
onClick = { onPingToggle() },
)
},
@@ -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()
},
),
)
@@ -8,7 +8,7 @@ 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 activeTunnels: Map<TunnelConf, TunnelState> = emptyMap(),
val generalState: GeneralState = GeneralState(),
val autoTunnelActive: Boolean = false,
)
@@ -145,17 +145,14 @@ constructor(
with(uiState.value.generalState) {
val toggledOn = !isLocalLogsEnabled
appDataRepository.appState.setLocalLogsEnabled(toggledOn)
if (!toggledOn) onLoggerStop()
_configurationChange.update {
true
if (!toggledOn) {
logReader.stop()
} else {
logReader.start()
}
}
}
private suspend fun onLoggerStop() {
logReader.deleteAndClearLogs()
}
fun onToggleAlwaysOnVPN() = viewModelScope.launch {
with(uiState.value.appSettings) {
appDataRepository.settings.save(
@@ -280,13 +277,13 @@ constructor(
}
}
suspend fun requestRoot(): Result<Unit> {
private suspend fun requestRoot(): Result<Unit> {
return withContext(ioDispatcher) {
runCatching {
rootShell.get().start()
SnackbarController.Companion.showMessage(StringValue.StringResource(R.string.root_accepted))
SnackbarController.showMessage(StringValue.StringResource(R.string.root_accepted))
}.onFailure {
SnackbarController.Companion.showMessage(StringValue.StringResource(R.string.error_root_denied))
SnackbarController.showMessage(StringValue.StringResource(R.string.error_root_denied))
}
}
}
@@ -356,7 +353,7 @@ constructor(
runCatching {
val amConfig = tunnelConfig.toAmConfig()
val wgConfig = tunnelConfig.toWgConfig()
val proxy = InterfaceProxy.Companion.from(amConfig.`interface`)
val proxy = InterfaceProxy.from(amConfig.`interface`)
if (proxy.includedApplications.isEmpty() && proxy.excludedApplications.isEmpty()) return@launch
if (proxy.includedApplications.retainAll(packages.toSet()) || proxy.excludedApplications.retainAll(packages.toSet())) {
updateTunnelConfig(tunnelConfig, amConfig = amConfig, wgConfig = wgConfig, `interface` = proxy)
+15 -2
View File
@@ -162,7 +162,7 @@
<string name="monitoring_state_changes">Überwache Statusänderungen</string>
<string name="stop_on_no_internet">Stoppen wenn keine Internetverbindung besteht</string>
<string name="ethernet_tunnel">Ethernet Tunnel</string>
<string name="set_ethernet_tunnel">Als Ethernet Tunnel setzten</string>
<string name="set_ethernet_tunnel">Als Ethernet Tunnel setzen</string>
<string name="native_kill_switch">Nativer Notschalter</string>
<string name="vpn_kill_switch">VPN Notschalter</string>
<string name="kill_switch_options">Notschalteroptionen</string>
@@ -192,4 +192,17 @@
<string name="hide_amnezia_properties">Amnezia Eigenschaften verbergen</string>
<string name="include_lan">LAN einschließen</string>
<string name="exclude_lan">LAN ausschließen</string>
</resources>
<string name="tunnel_control">Tunnelsteuerung</string>
<string name="kill_switch_off">Notschalter stoppen bei vertrauenswürdigen</string>
<string name="error_tunnel_start">Start des Tunnels fehlgeschlagen</string>
<string name="auto_tunnel">Auto-Tunnel</string>
<string name="export_amnezia">Als Amnezia exportieren</string>
<string name="export_wireguard">Als WireGuard exportieren</string>
<string name="server_ipv4">IPv4 Hostnamensauflösung</string>
<string name="prefer_ipv4">IPv4 Verbindung bevorzugen</string>
<string name="dns_error">Fehler bei Endpunkt DNS Auflösung.</string>
<string name="start_failed_config">Starten des Tunnels wegen Konfigfehler fehlgeschlagen.</string>
<string name="unauthorized">Starten des Tunnels fehlgeschlagen, unberechtigt.</string>
<string name="tunne_start_failed_title">Tunnelfehler</string>
<string name="multiple">Mehrere</string>
</resources>
+14 -1
View File
@@ -192,4 +192,17 @@
<string name="wg_compat_mode">Modo compatibilidad de WG</string>
<string name="quick_actions">Acciones rápidas</string>
<string name="remove_amnezia_compatibility">Eliminar compatibilidad con Amnezia</string>
</resources>
<string name="error_tunnel_start">Fallo al iniciar el túnel</string>
<string name="tunnel_control">Control del túnel</string>
<string name="auto_tunnel">Túnel automático</string>
<string name="kill_switch_off">Detener interruptor de apagado en confianza</string>
<string name="server_ipv4">Resolución de host IPv4</string>
<string name="prefer_ipv4">Preferir conexión IPv4</string>
<string name="dns_error">No se ha podido resolver el DNS del punto final.</string>
<string name="start_failed_config">Fallo al iniciar túnel con error de configuración.</string>
<string name="unauthorized">Fallo al iniciar túnel, no autorizado.</string>
<string name="tunne_start_failed_title">Fallo del túnel</string>
<string name="multiple">Múltiple</string>
<string name="export_amnezia">Exportar cómo Amnezia</string>
<string name="export_wireguard">Exportar cómo WireGuard</string>
</resources>
+72 -3
View File
@@ -13,8 +13,8 @@
<string name="error_no_file_explorer">Nessun esploratore di file installato</string>
<string name="error_invalid_code">QR code non valido</string>
<string name="app_name">Tunnel WG</string>
<string name="vpn_channel_name">Canale di notifica VPN</string>
<string name="turn_off_tunnel">L\'operaz. richiede la disatt. del tunnel</string>
<string name="vpn_channel_name">Canale di notifica VPN</string>
<string name="turn_off_tunnel">L\'operaz. richiede la disatt. del tunnel</string>
<string name="public_key">Chiave pubblica</string>
<string name="addresses">Indirizzi</string>
<string name="dns_servers">Server DNS</string>
@@ -41,7 +41,7 @@
<string name="always_on_vpn_support">Permetti VPN sempre attiva</string>
<string name="location_services_not_detected">Servizi di localizzazione non rilevati</string>
<string name="hint_search_packages">Cerca pacchetti</string>
<string name="auto_tunneling">Tunnel automatico</string>
<string name="auto_tunneling">Tunnel automatico</string>
<string name="vpn_on">VPN on</string>
<string name="vpn_off">VPN off</string>
<string name="create_import">Crea da zero</string>
@@ -56,4 +56,73 @@
<string name="use_kernel">Usa modulo kernel</string>
<string name="error_ssid_exists">L\'SSID esiste già</string>
<string name="error_root_denied">Shell di root negata</string>
<string name="prominent_background_location_title">Permesso localizzazione in background</string>
<string name="location_services_missing_message">Questa app non rileva nessun servizio di localizzazione attiva sul tuo dispositivo. Dipendentemente dal dispositivo, questo potrebbe causare il fallimento a leggere il nome wifi da parte della funzione di wifi non fidate. Vuoi continuare comunque?</string>
<string name="read_logs">Leggi i log</string>
<string name="auto">(auto)</string>
<string name="underload_packet_magic_header">Magic header pacchetto sottocarico</string>
<string name="unsure_how">se non sei sicuro di come procedere</string>
<string name="vpn_settings">Impostazioni sistema VPN</string>
<string name="always_on_message">Permessi connessione VPN negati. Verifica la</string>
<string name="chat_description">Unisciti alla community</string>
<string name="junk_packet_maximum_size">Dimensione massima pacchetti indesiderati</string>
<string name="delete_tunnel">Cancella tunnel</string>
<string name="init_packet_junk_size">Inizializza la dimensione dei pacchetti indesiderati</string>
<string name="always_on_message2">per essere sicuro che la VPN Always-on sia spenta per tutte le altre app e riprova</string>
<string name="background_location_message">Permetti i permessi per la localizzazione durante tutto il tempo e/o la localizzazione precisa è richiesta per questa funzione. Vedi la</string>
<string name="config_changes_saved">Salvate modifiche configurazione.</string>
<string name="delete_tunnel_message">Sei certo di voler cancellare questo tunnel?</string>
<string name="thank_you">Grazie per usare WG Tunnel!</string>
<string name="trusted_ssid_value_description">Invia SSID</string>
<string name="exclude">Escludi</string>
<string name="include">Includi</string>
<string name="yes">Si</string>
<string name="all">tutte</string>
<string name="no_email_detected">Nessuna app email rilevata</string>
<string name="no_browser_detected">Nessun browser rilevato</string>
<string name="incorrect_pin">Il PIN non è corretto</string>
<string name="pin_created">PIN correttamente creato</string>
<string name="enable_app_lock">Abilita blocco app</string>
<string name="restart_on_ping">Riavvia su fallimento ping (beta)</string>
<string name="mobile_data_tunnel">Imposta come tunnel dati mobili</string>
<string name="set_primary_tunnel">Imposta come tunnel principale</string>
<string name="use_tunnel_on_wifi_name">Usa tunnel su nome wifi</string>
<string name="edit_tunnel">Modifica tunnel</string>
<string name="version">Versione</string>
<string name="settings">Impostazioni</string>
<string name="support">Supporto</string>
<string name="kernel">Kernel</string>
<string name="junk_packet_count">Numero pacchetti indesiderati</string>
<string name="junk_packet_minimum_size">Dimensione minima pacchetti indesiderati</string>
<string name="response_packet_junk_size">Dimensione pacchetto indesiderato risposta</string>
<string name="init_packet_magic_header">Magic header pacchetto inizializzazione</string>
<string name="response_packet_magic_header">Magic header pacchetto risposta</string>
<string name="getting_started_guide">guida di avvio rapido</string>
<string name="see_the">Vedi la</string>
<string name="error_file_format">Formato configurazione tunnel non valido</string>
<string name="restart_at_boot">Riavvia al boot</string>
<string name="vpn_denied_dialog_title">Permesso Negato</string>
<string name="tunnel_required">Questa funzione richiede almeno un tunnel</string>
<string name="app_settings">impostazioni app</string>
<string name="background_location_message2">per assicurarti che questi permessi siano abilitati</string>
<string name="root_accepted">Accesso alla shell root accettata</string>
<string name="set_custom_ping_ip">Imposta ip ping personalizzato</string>
<string name="default_ping_ip">(opzionale, default ai peers)</string>
<string name="set_custom_ping_internal">Intervallo ping (sec)</string>
<string name="optional_default">"opzionale, default: "</string>
<string name="set_custom_ping_cooldown">Tempo attesa prima della ripartenza ping (sec)</string>
<string name="show_amnezia_properties">Mostra proprietà Amnezia</string>
<string name="add_tunnels_text">Aggiungi da file o zip</string>
<string name="open_file">Apri file</string>
<string name="add_from_qr">Aggiungi da codice QR</string>
<string name="qr_scan">Scansione QR</string>
<string name="tunnel_name">Nome Tunnel</string>
<string name="tunneling_apps">App nel tunnel</string>
<string name="open_issue">Apri un problema</string>
<string name="enter_pin">Inserisci il tuo PIN</string>
<string name="create_pin">Crea PIN</string>
<string name="transport_packet_magic_header">Magic header pacchetto trasporto</string>
<string name="kill_switch_off">Ferma interruttore di spegnimento su fidate</string>
<string name="prominent_background_location_message">Questa caratteristica richiede il permesso di localizzazione in background per abilitare il monitoraggio dell\'SSID Wi-fi anche quando l\'applicazione è chiusa. Per più dettagli, verifica la Privacy Policy linkata nella schermata di supporto.</string>
<string name="auto_tunnel_title">Servizio auto-tunnel</string>
</resources>
+11 -1
View File
@@ -3,7 +3,7 @@
<string name="tunnels">Tunele</string>
<string name="app_name">WG Tunnel</string>
<string name="unsure_how">jeśli nie masz pewności, jak postępować</string>
<string name="getting_started_guide">przewodnik wprowadzający</string>
<string name="getting_started_guide">przewodnik wprowadzający,</string>
<string name="peer">Peer</string>
<string name="background_location_message2">w celu upewnienia się, że uprawnienia te są włączone</string>
<string name="rotate_keys">Rotuj klucze</string>
@@ -196,4 +196,14 @@
<string name="error_tunnel_start">Nie udało się uruchomić tunelu</string>
<string name="tunnel_control">Kontrola tunelu</string>
<string name="auto_tunnel">Autotunel</string>
<string name="kill_switch_off">Zatrzymaj wyłącznik awaryjny w zaufanej</string>
<string name="server_ipv4">Rozpoznawanie nazw hostów IPv6</string>
<string name="prefer_ipv4">Preferuj połączenie IPv4</string>
<string name="unauthorized">Nie udało się uruchomić tunelu, brak autoryzacji.</string>
<string name="multiple">Wiele</string>
<string name="start_failed_config">Nie udało się uruchomić tunelu z powodu błędu konfiguracji.</string>
<string name="tunne_start_failed_title">Awaria tunelu</string>
<string name="dns_error">Nie udało się rozpoznać punktu końcowego DNS.</string>
<string name="export_amnezia">Eksportuj jako Amnezia</string>
<string name="export_wireguard">Eksportuj jako WireGuard</string>
</resources>
+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>
+16 -3
View File
@@ -3,7 +3,7 @@
<string name="turn_off_tunnel">Действие требует отключения туннеля</string>
<string name="mtu">MTU</string>
<string name="tunnel_name">Имя туннеля</string>
<string name="public_key">Публичный ключ</string>
<string name="public_key">Открытый ключ</string>
<string name="name">Имя</string>
<string name="peer">Пир</string>
<string name="privacy_policy">Посмотреть политику конфиденциальности</string>
@@ -129,7 +129,7 @@
<string name="handshake">рукопожатие</string>
<string name="logs">Журналы</string>
<string name="light">Светлая</string>
<string name="automatic">Автоматически</string>
<string name="automatic">Автоматическая</string>
<string name="dynamic">Динамическая</string>
<string name="language">Язык</string>
<string name="trusted_wifi_names">Доверенные сети Wi-Fi</string>
@@ -192,4 +192,17 @@
<string name="advanced_settings">Дополнительные настройки</string>
<string name="enable_amnezia_compatibility">Включить совместимость с Amnezia</string>
<string name="include_lan">Включить LAN</string>
</resources>
<string name="auto_tunnel">Автотуннель</string>
<string name="tunnel_control">Управление туннелем</string>
<string name="error_tunnel_start">Невозможно запустить туннель</string>
<string name="kill_switch_off">Без экстренного отключения в доверенных</string>
<string name="prefer_ipv4">Предпочитать соединение IPv4</string>
<string name="dns_error">Не получить конечную точку DNS.</string>
<string name="start_failed_config">Невозможно запустить туннель с ошибкой конфигурации.</string>
<string name="unauthorized">Невозможно запустить туннель без авторизации.</string>
<string name="tunne_start_failed_title">Ошибка туннеля</string>
<string name="server_ipv4">Получение имени узла IPv4</string>
<string name="multiple">Несколько</string>
<string name="export_amnezia">Экспортировать как Amnezia</string>
<string name="export_wireguard">Экспортировать как WireGuard</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>
+103 -5
View File
@@ -30,7 +30,7 @@
<string name="getting_started_guide">інструкція щодо початку роботи</string>
<string name="error_file_format">некоректний формат конфігурації тунелю</string>
<string name="vpn_channel_name">Канал сповіщення VPN</string>
<string name="error_file_extension">Файл не є .conf або .zip файлом</string>
<string name="error_file_extension">Файл не є .conf або .zip файлом</string>
<string name="turn_off_tunnel">Дія потребує вимкнення тунелю</string>
<string name="tunnel_mobile_data">Тунелювати мобільні дані</string>
<string name="privacy_policy">Переглянути політику конфіденційності</string>
@@ -51,7 +51,7 @@
<string name="name">Ім\'я</string>
<string name="always_on_vpn_support">Дозволили Always-ON VPN</string>
<string name="location_services_not_detected">Сервіси місце знаходження не знайдено</string>
<string name="auto_tunneling">Авто-тунелювання</string>
<string name="auto_tunneling">Авто-тунелювання</string>
<string name="vpn_on">VPN увімк.</string>
<string name="vpn_off">VPN вимк.</string>
<string name="create_import">Створити з нуля</string>
@@ -59,9 +59,9 @@
<string name="interface_">Інтерфейс</string>
<string name="copy_public_key">Копіювати відкритий ключ</string>
<string name="listen_port">Слухати порт</string>
<string name="preshared_key">Pre-shared key</string>
<string name="preshared_key">Загальний ключ</string>
<string name="seconds">секунд</string>
<string name="persistent_keepalive">Persistent keepalive</string>
<string name="persistent_keepalive">Підтримка роботи тунелю (keepalive)</string>
<string name="error_authorization_failed">Не вдалося авторизуватися</string>
<string name="enabled_app_shortcuts">Дозволити ярлики</string>
<string name="error_authentication_failed">Помилка автентифікації</string>
@@ -74,7 +74,7 @@
<string name="use_kernel">Використовувати модуль режиму ядра</string>
<string name="error_ssid_exists">SSID вже існує</string>
<string name="error_no_file_explorer">Не знайдено файловий менеджер</string>
<string name="auto_tunnel_title">Сервіс авто-тунелювання</string>
<string name="auto_tunnel_title">Сервіс автотунелювання</string>
<string name="delete_tunnel">Видалити тунель</string>
<string name="location_services_missing_message">Додаток не знайшов служб місце знаходження на вашому пристрої. На деяких пристроях це може привести до неможливості визначення назви мережі Wi-Fi і помилок функції недовірених Wi-Fi мереж. Все рівно хочете продовжити?</string>
<string name="delete_tunnel_message">Ви дійсно хочете видалити цей тунель?</string>
@@ -107,4 +107,102 @@
<string name="response_packet_magic_header">Заголовок пакету відповіді</string>
<string name="unsure_how">, якщо не впевнені що робити далі</string>
<string name="see_the">Дивіться</string>
<string name="skip">Пропустити</string>
<string name="trusted_wifi_names">Довірені мережі Wi-Fi</string>
<string name="vpn_denied_dialog_title">Немає дозволу</string>
<string name="app_settings">налаштування програми</string>
<string name="background_location_message2">, щоб переконатися, що ці дозволи надано</string>
<string name="default_ping_ip">(необов\'язково, за замовчуванням для пірів)</string>
<string name="set_custom_ping_internal">Інтервал пінгу (сек.)</string>
<string name="set_custom_ping_cooldown">Час очікування перезапуску пінгу (сек.)</string>
<string name="show_amnezia_properties">Показати налаштування Amnezia</string>
<string name="sec">сек.</string>
<string name="handshake">рукостискання</string>
<string name="notifications">Сповіщення</string>
<string name="light">Світла</string>
<string name="dark">Темна</string>
<string name="language">Мова</string>
<string name="add_wifi_name">Додати мережу Wi-Fi</string>
<string name="on_demand_rules">Правила тунелю на запит</string>
<string name="primary_tunnel">Основний тунель</string>
<string name="launch_app_settings">Налаштування запуску програми</string>
<string name="learn_more">Дізнатись більше</string>
<string name="wildcards_active">Підстановочні знаки використовуються</string>
<string name="wifi_name_via_shell">Ім\'я Wi-Fi через root</string>
<string name="kill_switch">Екстрене відключення</string>
<string name="restart_at_boot">Перезапуск під час завантаження</string>
<string name="vpn_settings">Системні налаштування VPN</string>
<string name="always_on_message">Дозвіл на VPN-з\'єднання було відхилено, перевірте</string>
<string name="root_accepted">Root-доступ дозволено</string>
<string name="set_custom_ping_ip">Призначити свій IP для пінгу</string>
<string name="logs">Журнали</string>
<string name="mobile_tunnel">Тунель для мобільних даних</string>
<string name="display_theme">Тема</string>
<string name="chat_description">Приєднатися до спільноти</string>
<string name="automatic">Автоматично</string>
<string name="never">ніколи</string>
<string name="appearance">Зовнішній вигляд</string>
<string name="dynamic">Динамічна</string>
<string name="use_root_shell_for_wifi">Використовувати root-доступ для отримання імені мережі Wi-Fi</string>
<string name="use_wildcards">Використовувати підстановочні знаки в імені</string>
<string name="optional_default">"необов\'язково, за замовчуванням: "</string>
<string name="always_on_message2">, щоб переконатися, що функція «Постійний VPN» вимкнена для всіх інших програм, і спробуйте ще раз</string>
<string name="tunnel_required">Для цієї функції необхідний хоча б один тунель</string>
<string name="background_location_message">Дозволяти весь час, доки для роботи цієї функції потрібен доступ на місцезнаходження та/або точне місцезнаходження. Дивіться</string>
<string name="advanced_settings">Додаткові налаштування</string>
<string name="quick_actions">Швидкі дії</string>
<string name="tunnel_running">Тунель працює</string>
<string name="monitoring_state_changes">Відстеження змін стану</string>
<string name="donate">Пожертвувати на проект</string>
<string name="requires_app_relaunch">Дана зміна потребує перезапуску програми. Продовжити?</string>
<string name="add_from_clipboard">Додати з буфера обміну</string>
<string name="stop_on_no_internet">Зупинити без інтернету</string>
<string name="native_kill_switch">Штатне екстрене відключення</string>
<string name="vpn_kill_switch">Екстрене відключення VPN</string>
<string name="kill_switch_options">Налаштування екстреного вимкнення</string>
<string name="allow_lan_traffic">Обхід LAN</string>
<string name="bypass_lan_for_kill_switch">Дозволяти трафік LAN при екстреному вимкненні</string>
<string name="auto_tunnel_channel_name">Канал сповіщень автотунелю</string>
<string name="auto_tunnel_channel_description">Канал сповіщень про стан автотунелю</string>
<string name="stop">стоп</string>
<string name="splt_tunneling">Роздільне тунелювання</string>
<string name="show_scripts">Показати сценарії</string>
<string name="pre_up">До активації</string>
<string name="post_up">Після активації</string>
<string name="pre_down">До деактивації</string>
<string name="post_down">Після деактивації</string>
<string name="amnezia_kernel_message">Amnezia недоступна у режимі ядра</string>
<string name="enable_amnezia">Використовувати Amnezia</string>
<string name="wg_compat_mode">Режим сумісності WG</string>
<string name="debounce_delay">Затримка відбою</string>
<string name="hide_amnezia_properties">Приховати налаштування Amnezia</string>
<string name="hide_scripts">Приховати сценарії</string>
<string name="remove_amnezia_compatibility">Вимкнути сумісність з Amnezia</string>
<string name="exclude_lan">Виключити LAN</string>
<string name="include_lan">Увімкнути LAN</string>
<string name="error_tunnel_start">Неможливо запустити тунель</string>
<string name="tunnel_control">Управління тунелем</string>
<string name="auto_tunnel">Автотунель</string>
<string name="local_logging">Локальне ведення журналу</string>
<string name="kernel_not_supported">Ядро не підтримується</string>
<string name="start_auto">Запустити автотунель</string>
<string name="stop_auto">Зупинити автотунель</string>
<string name="enable_local_logging">Увімкнути ведення журналу</string>
<string name="configuration_change">Зміна конфігурації</string>
<string name="stop_on_internet_loss">Зупинити тунель під час втрати інтернету</string>
<string name="ethernet_tunnel">Тунель для Ethernet</string>
<string name="set_ethernet_tunnel">Призначити як тунель для Ethernet</string>
<string name="tunnel_specific_settings">Спеціальні налаштування тунелю</string>
<string name="vpn_channel_description">Канал сповіщень про стан VPN</string>
<string name="enable_amnezia_compatibility">Включити сумісність із Amnezia</string>
<string name="kill_switch_off">Без екстреного вимкнення у довірених</string>
<string name="server_ipv4">Отримання імені вузла IPv4</string>
<string name="prefer_ipv4">Віддавати перевагу з\'єднанню IPv4</string>
<string name="dns_error">Не отримати кінцеву точку DNS.</string>
<string name="start_failed_config">Неможливо запустити тунель із помилкою конфігурації.</string>
<string name="unauthorized">Неможливо запустити тунель без авторизації.</string>
<string name="tunne_start_failed_title">Помилка тунелю</string>
<string name="multiple">Декілька</string>
<string name="export_amnezia">Експортувати як Amnezia</string>
<string name="export_wireguard">Експортувати як WireGuard</string>
</resources>
+47 -2
View File
@@ -65,7 +65,7 @@
<string name="config_changes_saved">设置已保存。</string>
<string name="interface_">接口</string>
<string name="email_subject">WG Tunnel 支持</string>
<string name="auto_tunnel_title">自动连接服务</string>
<string name="auto_tunnel_title">自动隧道服务</string>
<string name="delete_tunnel">删除隧道</string>
<string name="delete_tunnel_message">确定删除这个隧道吗?</string>
<string name="location_services_missing_message">此应用不会在您的设备上检测任何已开启的定位服务。根据不同的设备,可能会导致无法获得不可信 WiFi 的名称。您想继续吗?</string>
@@ -108,7 +108,7 @@
<string name="vpn_denied_dialog_title">拒绝访问</string>
<string name="tunnel_required">此功能需要至少一个隧道</string>
<string name="app_settings">应用设置</string>
<string name="background_location_message2">请确保这些权限已开启</string>
<string name="background_location_message2">请确保这些权限已开启</string>
<string name="logs">日志</string>
<string name="restart_on_ping">Ping 失败之后自动重启隧道(beta)</string>
<string name="edit_tunnel">编辑隧道</string>
@@ -160,4 +160,49 @@
<string name="enable_local_logging">开启本地日志</string>
<string name="configuration_change">配置更改</string>
<string name="requires_app_relaunch">此更改需要重新启动应用程序。您是否要继续?</string>
<string name="stop_on_no_internet">无网络时停用</string>
<string name="stop_on_internet_loss">网络丢失时停止隧道</string>
<string name="bypass_lan_for_kill_switch">绕过局域网流量</string>
<string name="auto_tunnel_channel_name">自动隧道通知频道</string>
<string name="auto_tunnel_channel_description">自动隧道状态通知频道</string>
<string name="stop">停止</string>
<string name="tunnel_specific_settings">隧道特殊设置</string>
<string name="splt_tunneling">隧道分流</string>
<string name="show_scripts">显示脚本</string>
<string name="pre_up">启动前</string>
<string name="post_up">启动后</string>
<string name="pre_down">关闭前</string>
<string name="post_down">关闭后</string>
<string name="amnezia_kernel_message">Amnezia 在内核模式中不可用</string>
<string name="enable_amnezia">开启 Amnezia</string>
<string name="wg_compat_mode">WG 兼容性模式</string>
<string name="quick_actions">快捷操作</string>
<string name="advanced_settings">高级设置</string>
<string name="debounce_delay">防抖延迟</string>
<string name="hide_amnezia_properties">隐藏 Amnezia 属性</string>
<string name="hide_scripts">隐藏脚本</string>
<string name="enable_amnezia_compatibility">开启 Amnezia 兼容性</string>
<string name="remove_amnezia_compatibility">移除 Amnezia 兼容性</string>
<string name="exclude_lan">排除局域网</string>
<string name="include_lan">包含局域网</string>
<string name="error_tunnel_start">开启隧道失败</string>
<string name="tunnel_control">隧道控制</string>
<string name="auto_tunnel">自动隧道</string>
<string name="ethernet_tunnel">以太网隧道</string>
<string name="set_ethernet_tunnel">设置为以太网隧道</string>
<string name="native_kill_switch">系统 VPN 开关</string>
<string name="vpn_kill_switch">VPN 开关</string>
<string name="kill_switch_options">开关选项</string>
<string name="allow_lan_traffic">允许局域网流量</string>
<string name="vpn_channel_description">VPN 状态通知频道</string>
<string name="kill_switch_off">在受信任网络上停止 Kill Switch</string>
<string name="prefer_ipv4">首选 IPv4 连接</string>
<string name="dns_error">解析端点 DNS 失败。</string>
<string name="unauthorized">身份验证未通过,启动流量隧道失败。</string>
<string name="tunne_start_failed_title">隧道失败</string>
<string name="multiple">多个</string>
<string name="server_ipv4">IPv4 主机名解析</string>
<string name="start_failed_config">配置文件错误,启动流量隧道失败。</string>
<string name="export_amnezia">导出为 Amnezia</string>
<string name="export_wireguard">导出为 WireGuard</string>
</resources>
+100 -1
View File
@@ -1,2 +1,101 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="sec"></string>
<string name="error_file_extension">檔案類型不是 .conf 或 .zip</string>
<string name="language">語言</string>
<string name="interface_">界面</string>
<string name="launch_app_settings">打開應用程式設定</string>
<string name="use_kernel">使用核心模組</string>
<string name="version">版本</string>
<string name="app_name">WG Tunnel</string>
<string name="name">名稱</string>
<string name="public_key">公鑰</string>
<string name="privacy_policy">查看隱私政策</string>
<string name="tunnels">隧道列表</string>
<string name="thank_you">感謝您使用 WG Tunnel!</string>
<string name="open_file">開啟檔案</string>
<string name="mtu">MTU</string>
<string name="okay"></string>
<string name="qr_scan">掃描 QR code</string>
<string name="dns_servers">DNS 伺服器</string>
<string name="tunnel_name">隧道名稱</string>
<string name="config_changes_saved">組態變更已儲存。</string>
<string name="exclude">排除</string>
<string name="include">包含</string>
<string name="addresses">地址</string>
<string name="add_tunnels_text">從檔案或 zip 壓縮檔新增</string>
<string name="add_from_qr">從 QR code 新增</string>
<string name="copy_public_key">複製公鑰</string>
<string name="optional_no_recommend">(可選, 不建議)</string>
<string name="optional">(可選)</string>
<string name="random">(隨機)</string>
<string name="cancel">取消</string>
<string name="private_key">私鑰</string>
<string name="listen_port">監聽連接埠</string>
<string name="export_configs">匯出組態</string>
<string name="create_import">手動建立</string>
<string name="seconds"></string>
<string name="docs_description">閱讀文件</string>
<string name="error_ssid_exists">SSID 已經存在</string>
<string name="delete_tunnel">刪除隧道</string>
<string name="delete_tunnel_message">您確定要刪除此隧道?</string>
<string name="yes"></string>
<string name="error_invalid_code">無效的 QR code</string>
<string name="no_browser_detected">未安裝瀏覽器</string>
<string name="no_email_detected">未安裝電子郵件應用程式</string>
<string name="open_issue">建立新的問題</string>
<string name="auto">(自動)</string>
<string name="create_pin">建立 PIN</string>
<string name="enter_pin">輸入 PIN</string>
<string name="pin_created">成功建立 PIN</string>
<string name="incorrect_pin">PIN 不正確</string>
<string name="enable_app_lock">啟用應用程式鎖定</string>
<string name="set_primary_tunnel">設為主要隧道</string>
<string name="edit_tunnel">編輯隧道</string>
<string name="kernel">核心</string>
<string name="vpn_settings">系統 VPN 設定</string>
<string name="support">支持</string>
<string name="getting_started_guide">取得入門指南</string>
<string name="settings">設定</string>
<string name="restart_at_boot">開機時重新啟動</string>
<string name="chat_description">加入社區</string>
<string name="junk_packet_count">無效封包數</string>
<string name="set_custom_ping_internal">Ping 間隔 (秒)</string>
<string name="app_settings">應用程式設定</string>
<string name="logs">日誌</string>
<string name="dark">暗色</string>
<string name="light">亮色</string>
<string name="donate">捐贈給專案</string>
<string name="appearance">外觀</string>
<string name="display_theme">主題</string>
<string name="primary_tunnel">主要隧道</string>
<string name="learn_more">了解更多</string>
<string name="kernel_not_supported">核心不支持</string>
<string name="notifications">通知</string>
<string name="dynamic">動態</string>
<string name="never">從不</string>
<string name="automatic">自動</string>
<string name="add_wifi_name">新增WiFi SSID</string>
<string name="allow_lan_traffic">允許 LAN 流量</string>
<string name="configuration_change">組態變更</string>
<string name="stop_on_no_internet">沒有連上網路時停用</string>
<string name="stop_on_internet_loss">網路連線斷開時停止隧道</string>
<string name="add_from_clipboard">從剪貼簿新增</string>
<string name="stop">停止</string>
<string name="amnezia_kernel_message">Amnezia 無法在核心模式使用</string>
<string name="enable_amnezia">啟用 Amnezia</string>
<string name="wg_compat_mode">WG 相容性模式</string>
<string name="advanced_settings">進階設定</string>
<string name="exclude_lan">排除 LAN</string>
<string name="include_lan">包含 LAN</string>
<string name="error_tunnel_start">啟用隧道失敗</string>
<string name="unknown_error">發生未知錯誤</string>
<string name="error_file_format">無效的隧道組態檔案格式</string>
<string name="endpoint">端點</string>
<string name="location_services_not_detected">未啟用定位服務</string>
<string name="junk_packet_maximum_size">最大無效封包</string>
<string name="no_tunnels">還沒有新增任何隧道!</string>
<string name="allowed_ips">允許的 IP</string>
<string name="junk_packet_minimum_size">最小無效封包</string>
<string name="error_no_file_explorer">未安裝任何檔案管理器</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.2"
const val JVM_TARGET = "17"
const val VERSION_CODE = 36600
const val VERSION_CODE = 37200
const val TARGET_SDK = 35
const val MIN_SDK = 26
const val APP_ID = "com.zaneschepke.wireguardautotunnel"
@@ -0,0 +1,6 @@
Was ist neu?
- Die Ping-Funktion funktioniert jetzt unabhängig vom automatischen Tunnel
- Komfortaktion für Amnezia-Kompatibilität hinzugefügt
- Komfortaktion zum Ausschließen von LAN aus dem Tunnel hinzugefügt
- Option zur Einstellung der Entprellungszeit für Autotunnel hinzugefügt
- Viele Fehlerkorrekturen und Verbesserungen
@@ -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,5 @@
What's new:
- Static IPv6 peer endpoint bug fix
- Dynamic shortcuts bug fix
- Localizations
- Add peer bug fix
@@ -0,0 +1,5 @@
What's new:
- Auto tunnel regression fix
- Tile sync improvements
- Optimize wifi name querying
- Improve network monitoring permission checks
@@ -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
@@ -0,0 +1,6 @@
Novità:
- aggiornamenti UI
- Migliorie navigazione AndroidTV
- Bug consumo batteria risolto
- Correzione impostazioni opzionali caratteri jolly
- Altre migliorie
@@ -0,0 +1,7 @@
Novità:
- Aggiungi tunnel dagli appunti
- Aggiunte localizzazioni
- Corretto bug consumo batteria
- Corretto bug cancellazione
- Migliorata sincronizzazione tile tunnel
- Altre correzioni e migliorie
@@ -0,0 +1,5 @@
Novità:
- Aggiunto interruttore spegnimento VPN con esclusione LAN
- Migliorata velocità e stabilità auto tunnel
- Migliorata sincronizzazione tile
- Vari bug risolti e migliorie
@@ -0,0 +1,3 @@
Novità:
- Corretto bug commutatore modalità kernel
- Corretto bug crash notifica
@@ -0,0 +1,6 @@
Novità:
- Migliorata stabilità auto tunnel
- Corretto salvataggio split tunnel e migliore prestazioni
- Corretto bug modalità kernel
- Corretto bug tile
- Vare altre correzioni e migliorie
@@ -0,0 +1,6 @@
Novità:
- Il ping ora funziona indipendentemente dall'auto tunnel
- Aggiunte azioni per compatibilità Amnezia
- Aggiunte azioni esclusione LAN dal tunnel
- Aggiunta opzione regolazione tempo di rimbalzo per auto tunnel
- Molte correzioni a bug e migliore
@@ -1 +0,0 @@
Uma alternativa de cliente WireGuard VPN com recursos adicionais
@@ -0,0 +1,3 @@
Поліпшення:
- Виправлена помилка з дозволами на Android < 9
- Інші оптимізації
@@ -0,0 +1,5 @@
Поліпшення:
- Додано статистику тунелю на головний екран
- Поліпшено навігацію в екрані налаштувань на Android TV
- Видалено вібрацію при оповіщенні
- Інші виправлення
@@ -0,0 +1,5 @@
Поліпшення:
- Додана підтримка авто-тунелювання лише через мобільну мережу
- Покращено інтерфейс екрану підтримки
- Оновлено посилання на ресурси
- Інші виправлення помилок
@@ -0,0 +1,5 @@
Поліпшення:
- Базова підтримка модуля WireGuard режиму ядра
- Покращено процес розкриття інформації про місцезнаходження
- Виправлено помилку з дозволами авто-тунелю
- Інші виправлення інтерфейсу
@@ -0,0 +1,2 @@
Виправлення:
- Дозвіл переднього плану в Android 14
@@ -0,0 +1,7 @@
Поліпшення:
- Рефакторинг зберігання даних додатком
- Поліпшено навігацію в інтерфейсі для Android TV
- Збільшено ефективність авто-тунелювання
- Поліпшено навігацію
- Можливість призупинити роботу авто-тунелю
- Безліч виправлень помилок
@@ -0,0 +1,7 @@
Поліпшення:
- Рефакторинг зберігання даних додатком
- Поліпшено навігацію в інтерфейсі для Android TV
- Збільшено ефективність авто-тунелювання
- Поліпшено навігацію
- Можливість призупинити роботу авто-тунелю
- Безліч виправлень помилок
@@ -0,0 +1,8 @@
Поліпшення:
- Рефакторинг зберігання даних додатком
- Поліпшено навігацію в інтерфейсі для Android TV
- Збільшено ефективність авто-тунелювання
- Поліпшено навігацію
- Можливість призупинити роботу авто-тунелю
- Виправлено запуск авто-тунелю на передньому плані
- Безліч виправлень помилок
@@ -0,0 +1,7 @@
Поліпшення:
- Додано підтвердження під час видалення тунелю
- Додано дозвіл на роботу у фоні
Виправлення:
- Зависання програми при відключенні тунелю
- Помилка у полі отримувача електронної пошти
- Редагування конфігурації з порожнім полем DNS
@@ -0,0 +1,3 @@
Поліпшення:
- Виправлено збій збереження створених налаштувань
- Збільшено номер версії
@@ -0,0 +1,2 @@
Що нового:
- Тестова версія для налагодження Continuous Integration
@@ -0,0 +1,5 @@
Що нового:
- Автозапуск «Завжди увімкненого» VPN режиму ядра при перезавантаженні системи
- Підтримка піктограм адаптивної теми
- Виправлені значки повідомлень та плитки
- Виправлені значки Android TV
@@ -0,0 +1,5 @@
Що нового:
- Покращено процес першого запуску програми
- Перехід на альтернативну реалізацію бібліотеки WireGuard
- Запит дозволу на доступ до VPN під час першого підключення VPN
- Збільшено номер версії
@@ -0,0 +1,2 @@
Що нового:
- Виправлена помилка в інтерфейсі тунелю
@@ -0,0 +1,4 @@
Що нового:
- Виправлено помилку в інтерфейсі редактора конфігурації
- Додано повідомлення AOVPN під час першого запуску на Graphene OS
- Збільшено номер версії
@@ -0,0 +1,5 @@
Що нового:
- Доданий екран із журналами виконання
- Додано блокування програми
- Додано перезапуск тунелю при збої пінгу
- Різні виправлення помилок
@@ -0,0 +1,5 @@
Що нового:
- Авто-тунель залежно від імені мережі Wi-Fi
- Управління авто-тунелем через плитку та ярлики
- Автозапуск тунелів, активних на момент перезавантаження
- Інші виправлення та покращення продуктивності
@@ -0,0 +1,5 @@
Що нового:
- Поліпшено надійність роботи авто-тунелю
- Поліпшено синхронізацію плитки
- Додані ресурси для інтерфейсу Android TV
- Доданий відбиток APK
@@ -0,0 +1,5 @@
Що нового:
- Виправлена регресія із зупинкою тунелю
- Додано обфускацію лог-файлів
- Приховування плаваючої кнопки під час прокручування екрану
- Додано переклад на турецьку
@@ -0,0 +1,3 @@
Що нового:
- Додавання профілів Amnezia пліч-о-пліч з WireGuard
- Виправлена помилка з ярликами програми
@@ -0,0 +1,6 @@
Що нового:
- Офіційна підтримка AmneziaWG
- Імпорт/експорт конфігурації AmneziaWG
- Одноразове перемикання авто-тунелю при зміні стану мережі
- Підтримка нових мов
- Інші виправлення та покращення
@@ -0,0 +1,5 @@
Що нового:
- Покращено призначення імен під час імпорту тунелів
- Виправлено помилку початкового стану авто-тунелю
- Поліпшено обробку помилок
- Виправлена помилка імпорту Amnezia з .zip
@@ -0,0 +1,5 @@
Що нового:
- Додані нові переклади
- Виправлено роботу авто-тунелю на мобільному інтернеті
- Виправлено проблему з плаваючою кнопкою на Android TV
- Інші оптимізації та покращення
@@ -0,0 +1,4 @@
Що нового:
- Виправлено проблеми з роботою авто-тунелю
- Виправлено проблему з резервним копіюванням Android
- Збільшено номер версії
@@ -0,0 +1,6 @@
Що нового:
- Виправлено проблеми зі збоями
- Поліпшено продуктивність плиток
- Повернуто блокування PIN-кодом
- Додано функцію перезапуску під час завантаження
- Різні виправлення продуктивності та помилок
@@ -0,0 +1,5 @@
Що нового:
- Виправлення інтерфейсу керування тунелем на AndroidTV
- Виправлення помилки блокування у портретній орієнтації
- Виправлена помилка обходу блокування PIN-кодом
- Виправлена помилка з плиткою авто-тунелю
@@ -0,0 +1,6 @@
Що нового:
- Підвищена надійність авто-тунелювання
- Додана підтримка світлих/темних/динамічних тем
- Додана підтримка сценаріїв до/після запуску/зупинки інтерфейсу
- Видалено постійне повідомлення про тунель
- Різні інші виправлення та покращення
@@ -0,0 +1,5 @@
Що нового:
- Виправлені помилки, через які тунелі не запускалися у фоновому режимі
- Додана підтримка перезапуску служб після оновлення
- Поліпшено швидкість анімації інтерфейсу
- Інші оптимізації
@@ -0,0 +1,6 @@
Що нового:
- Додано підтримку підстановочних знаків для імен мереж Wi-Fi
- Редагування налаштувань тунелю/автотунелю безпосередньо під час його роботи
- Виправлено уповільнення передачі даних у мобільних мережах
- Різні виправлення помилок та покращення
- Оптимізація інтерфейсу користувача
@@ -0,0 +1,6 @@
Що нового:
- Пристрої з root-доступом тепер отримують ім'я Wi-Fi без використання розташування
- Покращення прокручування екрана журналів та обміну
- Виправлення помилок імпорту тунелів для AndroidTV 14
- Поліпшення інтерфейсу статистики тунелів
- Інші виправлення помилок та покращення
@@ -0,0 +1,6 @@
Що нового:
- Оновлення інтерфейсу
- Поліпшення навігації AndroidTV
- Виправлення помилки розряду батареї
- Виправлення підстановочних знаків додатковою настройкою
- Інші покращення
@@ -0,0 +1,7 @@
Що нового:
- Додавання тунелю з буфера обміну
- Додавання перекладів
- Виправлено помилку розряду батареї
- Виправлена помилка видалення
- Поліпшено синхронізацію плиток тунелю
- Інші виправлення та покращення
@@ -0,0 +1,5 @@
Що нового:
- Додано екстрене відключення VPN з обходом локальної мережі
- Покращена швидкість та надійність автоматичного тунелювання
- Покращена синхронізація плиток
- Різні виправлення помилок та покращення
@@ -0,0 +1,3 @@
Що нового:
- Виправлено помилку перемикання режиму ядра
- Виправлено збій, що виникав при повідомленні
@@ -0,0 +1,6 @@
Що нового:
- Підвищення надійності автоматичного тунелювання
- Виправлена помилка збереження розділеного тунелю та покращена продуктивність
- Виправлено помилку в режимі ядра
- Виправлена помилка з плиткою
- Різні інші виправлення та покращення
@@ -0,0 +1,6 @@
Що нового:
- Функція пінгу тепер працює незалежно від автоматичного тунелювання
- Функціонал сумісності з Amnezia став зручнішим
- Додано функціонал виключення локальної мережі з тунелю
- Додано параметр затримки відбою для автоматичного тунелювання
- Безліч виправлень помилок та покращень
@@ -4,4 +4,5 @@
- 提高自动隧道效率
- 优化整体导航体验
- 增加自动隧道暂停功能
- 修复自动隧道前台启动的问题
- 修复多项错误
@@ -1,5 +1,6 @@
更新:
- 已root的设备获取wifi名称无需定位权限
- 已root的设备获取wifi名称无需定位权限
- 优化日志界面的滑动与分享体验
- 修复AndroidTV 14 的隧道导入bug
- 增强隧道统计数字用户界面
- 其它的bug修复与功能优化
+5 -5
View File
@@ -1,7 +1,7 @@
[versions]
accompanist = "0.37.2"
activityCompose = "1.10.1"
amneziawgAndroid = "1.3.0"
amneziawgAndroid = "1.3.1"
androidx-junit = "1.2.1"
appcompat = "1.7.0"
biometricKtx = "1.2.0-alpha05"
@@ -15,15 +15,15 @@ junit = "4.13.2"
kotlinx-serialization-json = "1.8.0"
lifecycle-runtime-compose = "2.8.7"
material3 = "1.3.1"
navigationCompose = "2.8.8"
navigationCompose = "2.8.9"
pinLockCompose = "1.0.4"
roomVersion = "2.6.1"
timber = "5.0.1"
tunnel = "1.2.6"
androidGradlePlugin = "8.10.0-alpha08"
tunnel = "1.2.7"
androidGradlePlugin = "8.8.0-alpha05"
kotlin = "2.1.10"
ksp = "2.1.10-1.0.31"
composeBom = "2025.02.00"
composeBom = "2025.03.00"
compose = "1.7.8"
workRuntimeKtxVersion = "2.10.0"
zxingAndroidEmbedded = "4.3.0"
@@ -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)
}
}
+8 -6
View File
@@ -1,9 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"
>
<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"/>
<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"
tools:targetApi="29" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
</manifest>
@@ -1,74 +1,139 @@
package com.zaneschepke.networkmonitor
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.location.LocationManager
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.net.wifi.WifiManager
import android.os.Build
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 kotlinx.coroutines.runBlocking
import timber.log.Timber
class AndroidNetworkMonitor(
context: Context,
private val useRootShellCallback: suspend () -> Boolean,
) : NetworkMonitor {
companion object {
const val LOCATION_GRANTED = "LOCATION_PERMISSIONS_GRANTED"
const val LOCATION_SERVICES_FILTER = "android.location.PROVIDERS_CHANGED"
}
private val appContext = context.applicationContext
private val packageName = appContext.packageName
private val connectivityManager = appContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
private val wifiManager = appContext.getSystemService(Context.WIFI_SERVICE) as WifiManager?
private val locationManager = appContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
private val rootShell = RootShell(context)
private var includeWifiSsid = false
private var useRootShell = false
@get:Synchronized @set:Synchronized
var currentSsid: String? = null
@get:Synchronized @set:Synchronized
var wifiConnected = 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) {
return if (runBlocking { useRootShellCallback() }) {
rootShell.getCurrentWifiName()
} else {
wifiManager.connectionInfo?.ssid?.trim('"')?.takeIf {
it != "<unknown>" && it.isNotEmpty()
if (wifiManager == null) return null
try {
wifiManager.connectionInfo?.ssid?.trim('"')?.takeIf { it.isNotEmpty() }
} catch (e: Exception) {
Timber.e(e)
null
}
}
}
fun handleUnknownWifi() {
val newSsid = getWifiSsid()
// Only update if new SSID is valid; preserve existing valid SSID otherwise
if (newSsid != null && newSsid != WifiManager.UNKNOWN_SSID) {
currentSsid = newSsid
trySend(WifiState(connected = wifiConnected, ssid = currentSsid))
} else if (currentSsid == null || currentSsid == WifiManager.UNKNOWN_SSID) {
currentSsid = newSsid
trySend(WifiState(connected = wifiConnected, ssid = currentSsid))
}
Timber.d("handleUnknownWifi: currentSsid=$currentSsid")
}
val locationPermissionReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
Timber.d("locationPermissionReceiver received intent with action: ${intent.action}")
if (intent.action == "$packageName.$LOCATION_GRANTED") {
Timber.d("Received update: Precise and all-the-time location permissions are enabled")
handleUnknownWifi()
}
}
}
val locationServicesReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == LOCATION_SERVICES_FILTER) {
val isGpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
val isNetworkEnabled = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
val isLocationServicesEnabled = isGpsEnabled || isNetworkEnabled
Timber.d("Location Services state changed. Enabled: $isLocationServicesEnabled, GPS: $isGpsEnabled, Network: $isNetworkEnabled")
if (isLocationServicesEnabled) handleUnknownWifi()
}
}
}
// Use RECEIVER_NOT_EXPORTED for Android 14+ compatibility
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
Context.RECEIVER_EXPORTED
} else {
0
}
appContext.registerReceiver(
locationPermissionReceiver,
IntentFilter("$packageName.$LOCATION_GRANTED"),
flags,
)
appContext.registerReceiver(
locationServicesReceiver,
IntentFilter(LOCATION_SERVICES_FILTER),
flags,
)
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))
}
currentSsid = getWifiSsid()
wifiConnected = true
trySend(WifiState(connected = true, ssid = currentSsid))
}
override fun onLost(network: Network) {
Timber.d("Wi-Fi onLost: network=$network")
currentSsid = null
wifiConnected = false
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))
}
Timber.d("Wi-Fi onCapabilitiesChanged: network=$network, networkCapabilities=$networkCapabilities")
}
}
@@ -80,7 +145,11 @@ class AndroidNetworkMonitor(
connectivityManager.registerNetworkCallback(request, callback)
trySend(WifiState())
awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
awaitClose {
connectivityManager.unregisterNetworkCallback(callback)
appContext.unregisterReceiver(locationPermissionReceiver)
appContext.unregisterReceiver(locationServicesReceiver)
}
}
private val cellularFlow: Flow<TransportState> = callbackFlow {
@@ -104,7 +173,9 @@ class AndroidNetworkMonitor(
connectivityManager.registerNetworkCallback(request, callback)
trySend(TransportState())
awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
awaitClose {
connectivityManager.unregisterNetworkCallback(callback)
}
}
private val ethernetFlow: Flow<TransportState> = callbackFlow {
@@ -131,21 +202,24 @@ class AndroidNetworkMonitor(
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()
override val networkStatusFlow = 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()
override fun sendLocationPermissionsGrantedBroadcast() {
val action = "$packageName.$LOCATION_GRANTED"
val intent = Intent(action)
Timber.d("Sending broadcast: $action")
appContext.sendBroadcast(intent)
}
}
@@ -3,5 +3,6 @@ package com.zaneschepke.networkmonitor
import kotlinx.coroutines.flow.Flow
interface NetworkMonitor {
fun getNetworkStatusFlow(includeWifiSsid: Boolean, useRootShell: Boolean): Flow<NetworkStatus>
val networkStatusFlow: Flow<NetworkStatus>
fun sendLocationPermissionsGrantedBroadcast()
}
+1 -1
View File
@@ -1 +1 @@
4
4