mirror of
https://github.com/wgtunnel/android.git
synced 2026-07-03 14:07:49 +02:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5e7379f94b | |||
| c71c4e5b29 | |||
| 7f0fea3766 | |||
| 53c19762ef | |||
| c98fa04f73 | |||
| aba0f7d4d3 | |||
| fa517b2124 | |||
| d7e2648393 | |||
| 53ff3bb1e5 | |||
| 97ede3d5b4 | |||
| dcd15f7bd8 | |||
| 6031d85edd | |||
| a71f8f86b1 | |||
| 007c9f4c5d | |||
| e32a99db77 | |||
| 6670a62e2f | |||
| 34b20bd7f7 |
@@ -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/)
|
||||
[](https://play.google.com/store/apps/details?id=com.zaneschepke.wireguardautotunnel)
|
||||
[](https://f-droid.org/packages/com.zaneschepke.wireguardautotunnel/)
|
||||
[](https://github.com/zaneschepke/fdroid)
|
||||
[](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">
|
||||
|
||||
[](https://discord.gg/rbRRNh6H7V)
|
||||
[](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" />
|
||||
|
||||
@@ -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
|
||||
Vendored
+1
-1
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+19
-37
@@ -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() {
|
||||
|
||||
+1
-1
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
+5
-3
@@ -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) }
|
||||
}
|
||||
|
||||
+26
-19
@@ -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
|
||||
}
|
||||
|
||||
+24
-17
@@ -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
|
||||
}
|
||||
|
||||
+11
-7
@@ -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>>
|
||||
}
|
||||
|
||||
+59
-28
@@ -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 {
|
||||
|
||||
+15
-12
@@ -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 {
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+2
-1
@@ -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() },
|
||||
)
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -147,6 +147,8 @@ constructor(
|
||||
appDataRepository.appState.setLocalLogsEnabled(toggledOn)
|
||||
if (!toggledOn) {
|
||||
logReader.stop()
|
||||
} else {
|
||||
logReader.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -275,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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -351,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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
object Constants {
|
||||
const val VERSION_NAME = "3.7.0"
|
||||
const val VERSION_NAME = "3.7.2"
|
||||
const val JVM_TARGET = "17"
|
||||
const val VERSION_CODE = 37000
|
||||
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,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修复与功能优化
|
||||
|
||||
@@ -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"
|
||||
@@ -9,21 +9,21 @@ coreKtx = "1.15.0"
|
||||
datastorePreferences = "1.1.3"
|
||||
desugar_jdk_libs = "2.1.5"
|
||||
espressoCore = "3.6.1"
|
||||
hiltAndroid = "2.55"
|
||||
hiltAndroid = "2.56"
|
||||
hiltCompiler = "1.2.0"
|
||||
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"
|
||||
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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
+112
-38
@@ -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
@@ -1 +1 @@
|
||||
4
|
||||
4
|
||||
|
||||
Reference in New Issue
Block a user