Compare commits

...

8 Commits

Author SHA1 Message Date
dependabot[bot] d000c79134 build(deps): bump androidGradlePlugin from 8.7.2 to 8.7.3
Bumps `androidGradlePlugin` from 8.7.2 to 8.7.3.

Updates `com.android.application` from 8.7.2 to 8.7.3

Updates `com.android.library` from 8.7.2 to 8.7.3

---
updated-dependencies:
- dependency-name: com.android.application
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.android.library
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-03 13:55:51 +00:00
GitHub Actions c3a2e05eb2 Automated build update 2024-12-01 00:24:55 +00:00
Zane Schepke a992009c71 fix: restart on ping bugs
Fixes bug where restart on ping could kill itself or not start correctly given certain settings combinations.

This change also makes auto tunneling and all or nothing service as this intuitively makes the most sense with the way the global settings are presented.

This change also makes it so users can toggle tunnel on untrusted wifi without location permissions because location permissions are only required when they go to add trusted ssids.
2024-11-30 18:32:50 -05:00
Zane Schepke 57676bf4bb fix: copy tunnel bug 2024-11-30 13:18:47 -05:00
Zane Schepke 921e33cb70 feat: add ethernet tunnel, stop tunnel on offline
closes #460
2024-11-30 12:33:07 -05:00
Zane Schepke 70649383e0 fix: auto tunnel logic and speed
closes #466
2024-11-30 11:13:54 -05:00
GitHub Actions 64a7680b81 Automated build update 2024-11-29 04:35:33 +00:00
Weblate (bot) a81d3a8843 Translations update from Hosted Weblate (#459)
Co-authored-by: solokot <solokot@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: SeanChengN <54seancheng@gmail.com>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Kachelkaiser <kachelkaiser@outlook.com>
Co-authored-by: MouaisTe44 <r.craft.212121@gmail.com>
Co-authored-by: lateweb <weblate@techkoala.net>
2024-11-28 22:48:58 -05:00
48 changed files with 910 additions and 473 deletions
@@ -0,0 +1,246 @@
{
"formatVersion": 1,
"database": {
"version": 12,
"identityHash": "acf79ac5defacda5be6c3f976e777de3",
"entities": [
{
"tableName": "Settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `is_amnezia_enabled` INTEGER NOT NULL DEFAULT false, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT false, `is_wifi_by_shell_enabled` INTEGER NOT NULL DEFAULT false, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT false)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isAutoTunnelEnabled",
"columnName": "is_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnMobileDataEnabled",
"columnName": "is_tunnel_on_mobile_data_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "trustedNetworkSSIDs",
"columnName": "trusted_network_ssids",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isAlwaysOnVpnEnabled",
"columnName": "is_always_on_vpn_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnEthernetEnabled",
"columnName": "is_tunnel_on_ethernet_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isShortcutsEnabled",
"columnName": "is_shortcuts_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isTunnelOnWifiEnabled",
"columnName": "is_tunnel_on_wifi_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isKernelEnabled",
"columnName": "is_kernel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isRestoreOnBootEnabled",
"columnName": "is_restore_on_boot_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isMultiTunnelEnabled",
"columnName": "is_multi_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isAmneziaEnabled",
"columnName": "is_amnezia_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isWildcardsEnabled",
"columnName": "is_wildcards_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isWifiNameByShellEnabled",
"columnName": "is_wifi_by_shell_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isStopOnNoInternetEnabled",
"columnName": "is_stop_on_no_internet_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "TunnelConfig",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false, `is_primary_tunnel` INTEGER NOT NULL DEFAULT false, `am_quick` TEXT NOT NULL DEFAULT '', `is_Active` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `ping_interval` INTEGER DEFAULT null, `ping_cooldown` INTEGER DEFAULT null, `ping_ip` TEXT DEFAULT null, `is_ethernet_tunnel` INTEGER NOT NULL DEFAULT false)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "wgQuick",
"columnName": "wg_quick",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tunnelNetworks",
"columnName": "tunnel_networks",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isMobileDataTunnel",
"columnName": "is_mobile_data_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPrimaryTunnel",
"columnName": "is_primary_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "amQuick",
"columnName": "am_quick",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isActive",
"columnName": "is_Active",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "pingInterval",
"columnName": "ping_interval",
"affinity": "INTEGER",
"notNull": false,
"defaultValue": "null"
},
{
"fieldPath": "pingCooldown",
"columnName": "ping_cooldown",
"affinity": "INTEGER",
"notNull": false,
"defaultValue": "null"
},
{
"fieldPath": "pingIp",
"columnName": "ping_ip",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "null"
},
{
"fieldPath": "isEthernetTunnel",
"columnName": "is_ethernet_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_TunnelConfig_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'acf79ac5defacda5be6c3f976e777de3')"
]
}
}
+1 -1
View File
@@ -157,7 +157,7 @@
android:value="true" />
</service>
<service
android:name=".service.foreground.AutoTunnelService"
android:name=".service.foreground.autotunnel.AutoTunnelService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="systemExempted"
@@ -11,7 +11,7 @@ import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
@Database(
entities = [Settings::class, TunnelConfig::class],
version = 11,
version = 12,
autoMigrations =
[
AutoMigration(from = 1, to = 2),
@@ -41,6 +41,10 @@ import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
to = 11,
spec = RemoveTunnelPauseMigration::class,
),
AutoMigration(
from = 11,
to = 12,
),
],
exportSchema = true,
)
@@ -44,6 +44,9 @@ interface TunnelConfigDao {
@Query("UPDATE TunnelConfig SET is_mobile_data_tunnel = 0 WHERE is_mobile_data_tunnel =1")
suspend fun resetMobileDataTunnel()
@Query("UPDATE TunnelConfig SET is_ethernet_tunnel = 0 WHERE is_ethernet_tunnel =1")
suspend fun resetEthernetTunnel()
@Query("SELECT * FROM TUNNELCONFIG WHERE is_primary_tunnel=1")
suspend fun findByPrimary(): TunnelConfigs
@@ -60,4 +60,9 @@ data class Settings(
defaultValue = "false",
)
val isWifiNameByShellEnabled: Boolean = false,
@ColumnInfo(
name = "is_stop_on_no_internet_enabled",
defaultValue = "false",
)
val isStopOnNoInternetEnabled: Boolean = false,
)
@@ -58,6 +58,11 @@ data class TunnelConfig(
defaultValue = "null",
)
var pingIp: String? = null,
@ColumnInfo(
name = "is_ethernet_tunnel",
defaultValue = "false",
)
var isEthernetTunnel: Boolean = false,
) {
fun toAmConfig(): org.amnezia.awg.config.Config {
@@ -53,6 +53,19 @@ class RoomTunnelConfigRepository(
}
}
override suspend fun updateEthernetTunnel(tunnelConfig: TunnelConfig?) {
withContext(ioDispatcher) {
tunnelConfigDao.resetEthernetTunnel()
tunnelConfig?.let {
save(
it.copy(
isEthernetTunnel = true,
),
)
}
}
}
override suspend fun delete(tunnelConfig: TunnelConfig) {
withContext(ioDispatcher) {
tunnelConfigDao.delete(tunnelConfig)
@@ -15,6 +15,8 @@ interface TunnelConfigRepository {
suspend fun updateMobileDataTunnel(tunnelConfig: TunnelConfig?)
suspend fun updateEthernetTunnel(tunnelConfig: TunnelConfig?)
suspend fun delete(tunnelConfig: TunnelConfig)
suspend fun getById(id: Int): TunnelConfig?
@@ -1,8 +0,0 @@
package com.zaneschepke.wireguardautotunnel.service.foreground
enum class Action {
START,
START_FOREGROUND,
STOP,
STOP_FOREGROUND,
}
@@ -1,107 +0,0 @@
package com.zaneschepke.wireguardautotunnel.service.foreground
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
import com.zaneschepke.wireguardautotunnel.util.extensions.isMatchingToWildcardList
data class AutoTunnelState(
val vpnState: VpnState = VpnState(),
val isWifiConnected: Boolean = false,
val isEthernetConnected: Boolean = false,
val isMobileDataConnected: Boolean = false,
val currentNetworkSSID: String = "",
val settings: Settings = Settings(),
val tunnels: TunnelConfigs = emptyList(),
) {
fun isEthernetConditionMet(): Boolean {
return (
isEthernetConnected &&
settings.isTunnelOnEthernetEnabled
)
}
fun isMobileDataConditionMet(): Boolean {
return (
!isEthernetConnected &&
settings.isTunnelOnMobileDataEnabled &&
!isWifiConnected &&
isMobileDataConnected
)
}
fun isTunnelOffOnMobileDataConditionMet(): Boolean {
return (
!isEthernetConnected &&
!settings.isTunnelOnMobileDataEnabled &&
isMobileDataConnected &&
!isWifiConnected
)
}
fun isUntrustedWifiConditionMet(): Boolean {
return (
!isEthernetConnected &&
isWifiConnected &&
!isCurrentSSIDTrusted() &&
settings.isTunnelOnWifiEnabled
)
}
fun isTrustedWifiConditionMet(): Boolean {
return (
!isEthernetConnected &&
(
isWifiConnected &&
isCurrentSSIDTrusted()
)
)
}
fun isTunnelOffOnWifiConditionMet(): Boolean {
return (
!isEthernetConnected &&
(
isWifiConnected &&
!settings.isTunnelOnWifiEnabled
)
)
}
fun isTunnelOffOnNoConnectivityMet(): Boolean {
return (
!isEthernetConnected &&
!isWifiConnected &&
!isMobileDataConnected
)
}
fun isCurrentSSIDTrusted(): Boolean {
return if (settings.isWildcardsEnabled) {
settings.trustedNetworkSSIDs.isMatchingToWildcardList(currentNetworkSSID)
} else {
settings.trustedNetworkSSIDs.contains(currentNetworkSSID)
}
}
fun isCurrentSSIDActiveTunnelNetwork(): Boolean {
val currentTunnelNetworks = vpnState.tunnelConfig?.tunnelNetworks
return (
if (settings.isWildcardsEnabled) {
currentTunnelNetworks?.isMatchingToWildcardList(currentNetworkSSID)
} else {
currentTunnelNetworks?.contains(currentNetworkSSID)
}
) == true
}
fun getTunnelWithMatchingTunnelNetwork(): TunnelConfig? {
return tunnels.firstOrNull {
if (settings.isWildcardsEnabled) {
it.tunnelNetworks.isMatchingToWildcardList(currentNetworkSSID)
} else {
it.tunnelNetworks.contains(currentNetworkSSID)
}
}
}
}
@@ -3,6 +3,7 @@ package com.zaneschepke.wireguardautotunnel.service.foreground
import android.app.Service
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.AutoTunnelService
import com.zaneschepke.wireguardautotunnel.util.SingletonHolder
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
import jakarta.inject.Inject
@@ -0,0 +1,9 @@
package com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
sealed class AutoTunnelEvent {
data class Start(val tunnelConfig: TunnelConfig? = null) : AutoTunnelEvent()
data object Stop : AutoTunnelEvent()
data object DoNothing : AutoTunnelEvent()
}
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.service.foreground
package com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel
import android.content.Intent
import android.net.NetworkCapabilities
@@ -9,12 +9,12 @@ import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope
import com.wireguard.android.util.RootShell
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.AppShell
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.module.MainImmediateDispatcher
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
@@ -22,7 +22,6 @@ import com.zaneschepke.wireguardautotunnel.service.network.NetworkStatus
import com.zaneschepke.wireguardautotunnel.service.network.WifiService
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.cancelWithMessage
import com.zaneschepke.wireguardautotunnel.util.extensions.getCurrentWifiName
@@ -34,6 +33,7 @@ import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.update
@@ -41,6 +41,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.net.InetAddress
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
import javax.inject.Provider
@@ -85,11 +86,9 @@ class AutoTunnelService : LifecycleService() {
private var wakeLock: PowerManager.WakeLock? = null
private var wifiJob: Job? = null
private var mobileDataJob: Job? = null
private var ethernetJob: Job? = null
private val pingTunnelRestartActive = AtomicBoolean(false)
private var pingJob: Job? = null
private var networkEventJob: Job? = null
override fun onCreate() {
super.onCreate()
@@ -122,6 +121,8 @@ class AutoTunnelService : LifecycleService() {
}
startSettingsJob()
startVpnStateJob()
startNetworkJobs()
startPingStateJob()
}.onFailure {
Timber.e(it)
}
@@ -137,7 +138,6 @@ class AutoTunnelService : LifecycleService() {
}
override fun onDestroy() {
cancelAndResetNetworkJobs()
cancelAndResetPingJob()
serviceManager.autoTunnelService = CompletableDeferred()
super.onDestroy()
@@ -202,6 +202,16 @@ class AutoTunnelService : LifecycleService() {
handleNetworkEventChanges()
}
private fun startPingStateJob() = lifecycleScope.launch {
autoTunnelStateFlow.collect {
if (it.isPingEnabled()) {
pingJob.onNotRunning { pingJob = startPingJob() }
} else {
if (!pingTunnelRestartActive.get()) cancelAndResetPingJob()
}
}
}
private suspend fun watchForMobileDataConnectivityChanges() {
withContext(ioDispatcher) {
Timber.i("Starting mobile data watcher")
@@ -231,8 +241,8 @@ class AutoTunnelService : LifecycleService() {
Timber.i("Starting ping watcher")
runCatching {
do {
val vpnState = tunnelService.get().vpnState.value
if (vpnState.status == TunnelState.UP) {
val vpnState = autoTunnelStateFlow.value.vpnState
if (vpnState.status.isUp() && !autoTunnelStateFlow.value.isNoConnectivity()) {
if (vpnState.tunnelConfig != null) {
val config = TunnelConfig.configFromWgQuick(vpnState.tunnelConfig.wgQuick)
val results = if (vpnState.tunnelConfig.pingIp != null) {
@@ -248,7 +258,9 @@ class AutoTunnelService : LifecycleService() {
if (results.contains(false)) {
Timber.i("Restarting VPN for ping failure")
val cooldown = vpnState.tunnelConfig.pingCooldown
tunnelService.get().bounceTunnel(vpnState.tunnelConfig)
pingTunnelRestartActive.set(true)
tunnelService.get().bounceTunnel()
pingTunnelRestartActive.set(false)
delay(cooldown ?: Constants.PING_COOLDOWN)
continue
}
@@ -266,20 +278,16 @@ class AutoTunnelService : LifecycleService() {
Timber.i("Starting settings watcher")
withContext(ioDispatcher) {
appDataRepository.settings.getSettingsFlow().combine(
// ignore isActive changes to allow manual tunnel overrides
appDataRepository.tunnels.getTunnelConfigsFlow().distinctUntilChanged { old, new ->
old.map { it.isActive } != new.map { it.isActive }
},
appDataRepository.tunnels.getTunnelConfigsFlow(),
) { settings, tunnels ->
Timber.d("Tunnels or settings changed!")
autoTunnelStateFlow.value.copy(
settings = settings,
tunnels = tunnels,
)
}.collect {
Timber.d("got new settings: ${it.settings}")
manageJobsBySettings(it.settings)
autoTunnelStateFlow.emit(it)
Pair(settings, tunnels)
}.collect { pair ->
autoTunnelStateFlow.update {
it.copy(
settings = pair.first,
tunnels = pair.second,
)
}
}
}
}
@@ -287,57 +295,20 @@ class AutoTunnelService : LifecycleService() {
private suspend fun watchForVpnStateChanges() {
Timber.i("Starting vpn state watcher")
withContext(ioDispatcher) {
tunnelService.get().vpnState.distinctUntilChanged { old, new ->
old.tunnelConfig?.id == new.tunnelConfig?.id
}.collect { state ->
tunnelService.get().vpnState.collect { state ->
autoTunnelStateFlow.update {
it.copy(vpnState = state)
}
state.tunnelConfig?.let {
val settings = appDataRepository.settings.getSettings()
if (it.isPingEnabled && !settings.isPingEnabled) {
pingJob.onNotRunning { pingJob = startPingJob() }
}
if (!it.isPingEnabled && !settings.isPingEnabled) {
cancelAndResetPingJob()
}
}
}
}
}
private fun manageJobsBySettings(settings: Settings) {
with(settings) {
if (isPingEnabled) {
pingJob.onNotRunning { pingJob = startPingJob() }
} else {
cancelAndResetPingJob()
}
if (isTunnelOnWifiEnabled || isTunnelOnEthernetEnabled || isTunnelOnMobileDataEnabled) {
startNetworkJobs()
} else {
cancelAndResetNetworkJobs()
}
}
}
private fun startNetworkJobs() {
wifiJob.onNotRunning {
Timber.i("Wifi job starting")
wifiJob = startWifiJob()
}
ethernetJob.onNotRunning {
ethernetJob = startEthernetJob()
Timber.i("Ethernet job starting")
}
mobileDataJob.onNotRunning {
mobileDataJob = startMobileDataJob()
Timber.i("Mobile data job starting")
}
networkEventJob.onNotRunning {
Timber.i("Network event job starting")
networkEventJob = startNetworkEventJob()
}
Timber.i("Starting all network state jobs..")
startWifiJob()
startEthernetJob()
startMobileDataJob()
startNetworkEventJob()
}
private fun cancelAndResetPingJob() {
@@ -345,17 +316,6 @@ class AutoTunnelService : LifecycleService() {
pingJob = null
}
private fun cancelAndResetNetworkJobs() {
networkEventJob?.cancelWithMessage("Network event job canceled")
wifiJob?.cancelWithMessage("Wifi job canceled")
ethernetJob?.cancelWithMessage("Ethernet job canceled")
mobileDataJob?.cancelWithMessage("Mobile data job canceled")
networkEventJob = null
wifiJob = null
ethernetJob = null
mobileDataJob = null
}
private fun emitEthernetConnected(connected: Boolean) {
autoTunnelStateFlow.update {
it.copy(
@@ -455,100 +415,20 @@ class AutoTunnelService : LifecycleService() {
}
}
private suspend fun getMobileDataTunnel(): TunnelConfig? {
return appDataRepository.tunnels.findByMobileDataTunnel().firstOrNull()
}
private suspend fun handleNetworkEventChanges() {
withContext(ioDispatcher) {
Timber.i("Starting network event watcher")
autoTunnelStateFlow.collect { watcherState ->
val autoTunnel = "Auto-tunnel watcher"
// delay for rapid network state changes and then collect latest
delay(Constants.WATCHER_COLLECTION_DELAY)
val activeTunnel = watcherState.vpnState.tunnelConfig
val defaultTunnel = appDataRepository.getPrimaryOrFirstTunnel()
val isTunnelDown = tunnelService.get().getState() == TunnelState.DOWN
when {
watcherState.isEthernetConditionMet() -> {
Timber.i("$autoTunnel - tunnel on on ethernet condition met")
if (isTunnelDown) {
defaultTunnel?.let {
tunnelService.get().startTunnel(it)
}
}
}
watcherState.isMobileDataConditionMet() -> {
Timber.i("$autoTunnel - tunnel on mobile data condition met")
val mobileDataTunnel = getMobileDataTunnel()
val tunnel =
mobileDataTunnel ?: defaultTunnel
if (isTunnelDown || activeTunnel?.isMobileDataTunnel == false) {
tunnel?.let {
tunnelService.get().startTunnel(it)
}
}
}
watcherState.isTunnelOffOnMobileDataConditionMet() -> {
Timber.i("$autoTunnel - tunnel off on mobile data met, turning vpn off")
if (!isTunnelDown) {
activeTunnel?.let {
tunnelService.get().stopTunnel(it)
}
}
}
watcherState.isUntrustedWifiConditionMet() -> {
Timber.i("Untrusted wifi condition met")
if (activeTunnel == null || watcherState.isCurrentSSIDActiveTunnelNetwork() == false ||
isTunnelDown
) {
Timber.i(
"$autoTunnel - tunnel on ssid not associated with current tunnel condition met",
)
watcherState.getTunnelWithMatchingTunnelNetwork()?.let {
Timber.i("Found tunnel associated with this SSID, bringing tunnel up: ${it.name}")
if (isTunnelDown || activeTunnel?.id != it.id) {
tunnelService.get().startTunnel(it)
}
} ?: suspend {
Timber.i("No tunnel associated with this SSID, using defaults")
val default = appDataRepository.getPrimaryOrFirstTunnel()
if (default?.name != tunnelService.get().name || isTunnelDown) {
default?.let {
tunnelService.get().startTunnel(it)
}
}
}.invoke()
}
}
watcherState.isTrustedWifiConditionMet() -> {
Timber.i(
"$autoTunnel - tunnel off on trusted wifi condition met, turning vpn off",
)
if (!isTunnelDown) activeTunnel?.let { tunnelService.get().stopTunnel(it) }
}
watcherState.isTunnelOffOnWifiConditionMet() -> {
Timber.i(
"$autoTunnel - tunnel off on wifi condition met, turning vpn off",
)
if (!isTunnelDown) activeTunnel?.let { tunnelService.get().stopTunnel(it) }
}
// TODO disable for this now
// watcherState.isTunnelOffOnNoConnectivityMet() -> {
// Timber.i(
// "$autoTunnel - tunnel off on no connectivity met, turning vpn off",
// )
// if (!isTunnelDown) activeTunnel?.let { tunnelService.get().stopTunnel(it) }
// }
else -> {
Timber.i("$autoTunnel - no condition met")
}
Timber.i("Starting auto-tunnel network event watcher")
// ignore vpnState emits to allow manual overrides
autoTunnelStateFlow.distinctUntilChanged { old, new ->
old.copy(vpnState = new.vpnState) == new || old.tunnels.map { it.isActive } != new.tunnels.map { it.isActive }
}.collect { watcherState ->
when (val event = watcherState.asAutoTunnelEvent()) {
is AutoTunnelEvent.Start -> tunnelService.get().startTunnel(
event.tunnelConfig
?: appDataRepository.getPrimaryOrFirstTunnel(),
)
is AutoTunnelEvent.Stop -> tunnelService.get().stopTunnel()
AutoTunnelEvent.DoNothing -> Timber.i("Auto-tunneling: no condition met")
}
}
}
@@ -0,0 +1,144 @@
package com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
import com.zaneschepke.wireguardautotunnel.util.extensions.isMatchingToWildcardList
data class AutoTunnelState(
val vpnState: VpnState = VpnState(),
val isWifiConnected: Boolean = false,
val isEthernetConnected: Boolean = false,
val isMobileDataConnected: Boolean = false,
val currentNetworkSSID: String = "",
val settings: Settings = Settings(),
val tunnels: TunnelConfigs = emptyList(),
) {
private fun isMobileDataActive(): Boolean {
return !isEthernetConnected && !isWifiConnected && isMobileDataConnected
}
private fun isMobileTunnelDataChangeNeeded(): Boolean {
val preferredTunnel = preferredMobileDataTunnel()
return preferredTunnel != null &&
vpnState.status.isUp() && preferredTunnel.id != vpnState.tunnelConfig?.id
}
private fun isEthernetTunnelChangeNeeded(): Boolean {
val preferredTunnel = preferredEthernetTunnel()
return preferredTunnel != null && vpnState.status.isUp() && preferredTunnel.id != vpnState.tunnelConfig?.id
}
private fun preferredMobileDataTunnel(): TunnelConfig? {
return tunnels.firstOrNull { it.isMobileDataTunnel } ?: tunnels.firstOrNull { it.isPrimaryTunnel }
}
private fun preferredEthernetTunnel(): TunnelConfig? {
return tunnels.firstOrNull { it.isEthernetTunnel } ?: tunnels.firstOrNull { it.isPrimaryTunnel }
}
private fun preferredWifiTunnel(): TunnelConfig? {
return getTunnelWithMatchingTunnelNetwork() ?: tunnels.firstOrNull { it.isPrimaryTunnel }
}
private fun isWifiActive(): Boolean {
return !isEthernetConnected && isWifiConnected
}
private fun startOnEthernet(): Boolean {
return isEthernetConnected && settings.isTunnelOnEthernetEnabled && vpnState.status.isDown()
}
private fun stopOnEthernet(): Boolean {
return isEthernetConnected && !settings.isTunnelOnEthernetEnabled && vpnState.status.isUp()
}
fun isNoConnectivity(): Boolean {
return !isEthernetConnected && !isWifiConnected && !isMobileDataConnected
}
private fun stopOnMobileData(): Boolean {
return isMobileDataActive() && !settings.isTunnelOnMobileDataEnabled && vpnState.status.isUp()
}
private fun startOnMobileData(): Boolean {
return isMobileDataActive() && settings.isTunnelOnMobileDataEnabled && vpnState.status.isDown()
}
private fun changeOnMobileData(): Boolean {
return isMobileDataActive() && settings.isTunnelOnMobileDataEnabled && isMobileTunnelDataChangeNeeded()
}
private fun changeOnEthernet(): Boolean {
return isEthernetConnected && settings.isTunnelOnEthernetEnabled && isEthernetTunnelChangeNeeded()
}
private fun stopOnWifi(): Boolean {
return isWifiActive() && !settings.isTunnelOnWifiEnabled && vpnState.status.isUp()
}
private fun stopOnTrustedWifi(): Boolean {
return isWifiActive() && settings.isTunnelOnWifiEnabled && vpnState.status.isUp() && isCurrentSSIDTrusted()
}
private fun startOnUntrustedWifi(): Boolean {
return isWifiActive() && settings.isTunnelOnWifiEnabled && vpnState.status.isDown() && !isCurrentSSIDTrusted()
}
private fun changeOnUntrustedWifi(): Boolean {
return isWifiActive() && settings.isTunnelOnWifiEnabled && vpnState.status.isUp() && !isCurrentSSIDTrusted() && !isWifiTunnelPreferred()
}
private fun isWifiTunnelPreferred(): Boolean {
val preferred = preferredWifiTunnel()
val vpnTunnel = vpnState.tunnelConfig
return if (preferred != null && vpnTunnel != null) {
preferred.id == vpnTunnel.id
} else {
true
}
}
fun asAutoTunnelEvent(): AutoTunnelEvent {
return when {
// ethernet scenarios
stopOnEthernet() -> AutoTunnelEvent.Stop
startOnEthernet() || changeOnEthernet() -> AutoTunnelEvent.Start(preferredEthernetTunnel())
// mobile data scenarios
stopOnMobileData() -> AutoTunnelEvent.Stop
startOnMobileData() || changeOnMobileData() -> AutoTunnelEvent.Start(preferredMobileDataTunnel())
// wifi scenarios
stopOnWifi() -> AutoTunnelEvent.Stop
stopOnTrustedWifi() -> AutoTunnelEvent.Stop
startOnUntrustedWifi() || changeOnUntrustedWifi() -> AutoTunnelEvent.Start(preferredWifiTunnel())
// no connectivity
isNoConnectivity() && settings.isStopOnNoInternetEnabled -> AutoTunnelEvent.Stop
else -> AutoTunnelEvent.DoNothing
}
}
private fun isCurrentSSIDTrusted(): Boolean {
return if (settings.isWildcardsEnabled) {
settings.trustedNetworkSSIDs.isMatchingToWildcardList(currentNetworkSSID)
} else {
settings.trustedNetworkSSIDs.contains(currentNetworkSSID)
}
}
private fun getTunnelWithMatchingTunnelNetwork(): TunnelConfig? {
return tunnels.firstOrNull {
if (settings.isWildcardsEnabled) {
it.tunnelNetworks.isMatchingToWildcardList(currentNetworkSSID)
} else {
it.tunnelNetworks.contains(currentNetworkSSID)
}
}
}
fun isPingEnabled(): Boolean {
return settings.isPingEnabled ||
(vpnState.status.isUp() && vpnState.tunnelConfig != null && tunnels.first { it.id == vpnState.tunnelConfig.id }.isPingEnabled)
}
}
@@ -4,9 +4,8 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
import com.zaneschepke.wireguardautotunnel.service.foreground.AutoTunnelService
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.AutoTunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
@@ -47,7 +46,7 @@ class ShortcutsActivity : ComponentActivity() {
tunnelConfig?.let {
when (intent.action) {
Action.START.name -> tunnelService.get().startTunnel(it, true)
Action.STOP.name -> tunnelService.get().stopTunnel(it)
Action.STOP.name -> tunnelService.get().stopTunnel()
else -> Unit
}
}
@@ -64,6 +63,11 @@ class ShortcutsActivity : ComponentActivity() {
finish()
}
enum class Action {
START,
STOP,
}
companion object {
const val LEGACY_TUNNEL_SERVICE_NAME = "WireGuardTunnelService"
const val LEGACY_AUTO_TUNNEL_SERVICE_NAME = "WireGuardConnectivityWatcherService"
@@ -72,7 +72,7 @@ class TunnelControlTile : TileService(), LifecycleOwner {
val lastActive = appDataRepository.getStartTunnelConfig()
lastActive?.let { tunnel ->
if (tunnel.isActive) {
tunnelService.get().stopTunnel(tunnel)
tunnelService.get().stopTunnel()
} else {
tunnelService.get().startTunnel(tunnel, true)
}
@@ -5,11 +5,12 @@ import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import kotlinx.coroutines.flow.StateFlow
interface TunnelService : Tunnel, org.amnezia.awg.backend.Tunnel {
suspend fun startTunnel(tunnelConfig: TunnelConfig, background: Boolean = false): Result<TunnelState>
suspend fun stopTunnel(tunnelConfig: TunnelConfig): Result<TunnelState>
suspend fun startTunnel(tunnelConfig: TunnelConfig?, background: Boolean = false)
suspend fun bounceTunnel(tunnelConfig: TunnelConfig): Result<TunnelState>
suspend fun stopTunnel()
suspend fun bounceTunnel()
val vpnState: StateFlow<VpnState>
@@ -24,6 +24,14 @@ enum class TunnelState {
}
}
fun isDown(): Boolean {
return this == DOWN
}
fun isUp(): Boolean {
return this == UP
}
companion object {
fun from(state: Tunnel.State): TunnelState {
return when (state) {
@@ -21,11 +21,13 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.amnezia.awg.backend.Tunnel
import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
import javax.inject.Provider
@@ -53,7 +55,7 @@ constructor(
private var statsJob: Job? = null
private val runningHandle = AtomicBoolean(false)
private val mutex = Mutex()
private suspend fun backend(): Any {
val settings = appDataRepository.settings.getSettings()
@@ -92,77 +94,58 @@ constructor(
}
}
override suspend fun startTunnel(tunnelConfig: TunnelConfig, background: Boolean): Result<TunnelState> {
return withContext(ioDispatcher) {
if (runningHandle.get() && tunnelConfig == vpnState.value.tunnelConfig) {
Timber.w("Tunnel already running")
return@withContext Result.success(vpnState.value.status)
}
runningHandle.set(true)
onBeforeStart(tunnelConfig)
val settings = appDataRepository.settings.getSettings()
if (background || settings.isKernelEnabled) startBackgroundService()
setState(tunnelConfig, TunnelState.UP).onSuccess {
updateTunnelState(it)
}.onFailure {
Timber.e(it)
onStartFailed()
private fun isTunnelAlreadyRunning(tunnelConfig: TunnelConfig): Boolean {
val isRunning = tunnelConfig == _vpnState.value.tunnelConfig && _vpnState.value.status.isUp()
if (isRunning) Timber.w("Tunnel already running")
return isRunning
}
override suspend fun startTunnel(tunnelConfig: TunnelConfig?, background: Boolean) {
if (tunnelConfig == null) return
withContext(ioDispatcher) {
mutex.withLock {
if (isTunnelAlreadyRunning(tunnelConfig)) return@withContext
onBeforeStart(background)
setState(tunnelConfig, TunnelState.UP).onSuccess {
startStatsJob()
if (it.isUp()) appDataRepository.tunnels.save(tunnelConfig.copy(isActive = true))
updateTunnelState(it, tunnelConfig)
}.onFailure {
Timber.e(it)
}
}
}
}
override suspend fun stopTunnel(tunnelConfig: TunnelConfig): Result<TunnelState> {
return withContext(ioDispatcher) {
onBeforeStop(tunnelConfig)
setState(tunnelConfig, TunnelState.DOWN).onSuccess {
updateTunnelState(it)
}.onFailure {
Timber.e(it)
onStopFailed()
}.also {
stopBackgroundService()
runningHandle.set(false)
override suspend fun stopTunnel() {
withContext(ioDispatcher) {
mutex.withLock {
if (_vpnState.value.status.isDown()) return@withContext
with(_vpnState.value) {
if (tunnelConfig == null) return@withContext
setState(tunnelConfig, TunnelState.DOWN).onSuccess {
updateTunnelState(it, null)
onStop(tunnelConfig)
stopBackgroundService()
}.onFailure {
Timber.e(it)
}
}
}
}
}
// use this when we just want to bounce tunnel and not change tunnelConfig active state
override suspend fun bounceTunnel(tunnelConfig: TunnelConfig): Result<TunnelState> {
toggleTunnel(tunnelConfig)
delay(VPN_RESTART_DELAY)
return toggleTunnel(tunnelConfig)
override suspend fun bounceTunnel() {
if (_vpnState.value.tunnelConfig == null) return
val config = _vpnState.value.tunnelConfig
stopTunnel()
startTunnel(config)
}
private suspend fun toggleTunnel(tunnelConfig: TunnelConfig): Result<TunnelState> {
return withContext(ioDispatcher) {
setState(tunnelConfig, TunnelState.TOGGLE).onSuccess {
updateTunnelState(it)
resetBackendStatistics()
}.onFailure {
Timber.e(it)
}
}
}
private suspend fun onStopFailed() {
_vpnState.value.tunnelConfig?.let {
appDataRepository.tunnels.save(it.copy(isActive = true))
}
}
private suspend fun onStartFailed() {
_vpnState.value.tunnelConfig?.let {
appDataRepository.tunnels.save(it.copy(isActive = false))
}
cancelStatsJob()
resetBackendStatistics()
runningHandle.set(false)
}
private suspend fun shutDownActiveTunnel(config: TunnelConfig) {
private suspend fun shutDownActiveTunnel() {
with(_vpnState.value) {
if (status == TunnelState.UP && tunnelConfig != config) {
tunnelConfig?.let { stopTunnel(it) }
if (status.isUp()) {
stopTunnel()
}
}
}
@@ -177,51 +160,35 @@ constructor(
serviceManager.requestTunnelTileUpdate()
}
private suspend fun onBeforeStart(tunnelConfig: TunnelConfig) {
shutDownActiveTunnel(tunnelConfig)
appDataRepository.tunnels.save(tunnelConfig.copy(isActive = true))
emitVpnStateConfig(tunnelConfig)
private suspend fun onBeforeStart(background: Boolean) {
shutDownActiveTunnel()
resetBackendStatistics()
startStatsJob()
val settings = appDataRepository.settings.getSettings()
if (background || settings.isKernelEnabled) startBackgroundService()
}
private suspend fun onBeforeStop(tunnelConfig: TunnelConfig) {
private suspend fun onStop(tunnelConfig: TunnelConfig) {
appDataRepository.tunnels.save(tunnelConfig.copy(isActive = false))
cancelStatsJob()
resetBackendStatistics()
}
private fun updateTunnelState(state: TunnelState) {
_vpnState.tryEmit(
_vpnState.value.copy(
status = state,
),
)
serviceManager.requestTunnelTileUpdate()
private fun updateTunnelState(state: TunnelState, tunnelConfig: TunnelConfig?) {
_vpnState.update {
it.copy(status = state, tunnelConfig = tunnelConfig)
}
}
private fun emitBackendStatistics(statistics: TunnelStatistics) {
_vpnState.tryEmit(
_vpnState.value.copy(
statistics = statistics,
),
)
}
private fun emitVpnStateConfig(tunnelConfig: TunnelConfig) {
_vpnState.tryEmit(
_vpnState.value.copy(
tunnelConfig = tunnelConfig,
),
)
_vpnState.update {
it.copy(statistics = statistics)
}
}
private fun resetBackendStatistics() {
_vpnState.tryEmit(
_vpnState.value.copy(
statistics = null,
),
)
_vpnState.update {
it.copy(statistics = null)
}
}
override suspend fun getState(): TunnelState {
@@ -265,16 +232,21 @@ constructor(
}
override fun onStateChange(newState: Tunnel.State) {
updateTunnelState(TunnelState.from(newState))
_vpnState.update {
it.copy(status = TunnelState.from(newState))
}
serviceManager.requestTunnelTileUpdate()
}
override fun onStateChange(state: State) {
updateTunnelState(TunnelState.from(state))
_vpnState.update {
it.copy(status = TunnelState.from(state))
}
serviceManager.requestTunnelTileUpdate()
}
companion object {
const val STATS_START_DELAY = 1_000L
const val VPN_STATISTIC_CHECK_INTERVAL = 1_000L
const val VPN_RESTART_DELAY = 1_000L
}
}
@@ -150,7 +150,7 @@ class MainActivity : AppCompatActivity() {
navController,
enterTransition = { fadeIn(tween(Constants.TRANSITION_ANIMATION_TIME)) },
exitTransition = { fadeOut(tween(Constants.TRANSITION_ANIMATION_TIME)) },
startDestination = (if (appUiState.generalState.isPinLockEnabled == true) Route.Lock else Route.Main),
startDestination = (if (appUiState.generalState.isPinLockEnabled) Route.Lock else Route.Main),
) {
composable<Route.Main> {
MainScreen(
@@ -155,7 +155,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) {
val intent = if (uiState.settings.isKernelEnabled) null else VpnService.prepare(context)
if (intent != null) return vpnActivity.launch(intent)
if (!checked) viewModel.onTunnelStop(tunnel).also { return }
if (!checked) viewModel.onTunnelStop().also { return }
viewModel.onTunnelStart(tunnel, uiState.settings.isKernelEnabled)
}
@@ -248,13 +248,10 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
uiState.tunnels,
key = { tunnel -> tunnel.id },
) { tunnel ->
val isActive = uiState.tunnels.any {
it.id == tunnel.id &&
it.isActive
}
val expanded = uiState.generalState.isTunnelStatsExpanded
TunnelRowItem(
isActive,
tunnel.id == uiState.vpnState.tunnelConfig?.id &&
uiState.vpnState.status.isUp(),
expanded,
selectedTunnel?.id == tunnel.id,
tunnel,
@@ -72,9 +72,9 @@ constructor(
tunnelService.get().startTunnel(tunnelConfig, background)
}
fun onTunnelStop(tunnel: TunnelConfig) = viewModelScope.launch {
fun onTunnelStop() = viewModelScope.launch {
Timber.i("Stopping active tunnel")
tunnelService.get().stopTunnel(tunnel)
tunnelService.get().stopTunnel()
}
private fun generateQrCodeDefaultName(config: String): String {
@@ -252,13 +252,7 @@ constructor(
fun onCopyTunnel(tunnel: TunnelConfig) = viewModelScope.launch {
saveTunnel(
tunnel.copy(
id = 0,
isPrimaryTunnel = false,
isMobileDataTunnel = false,
isActive = false,
name = makeTunnelNameUnique(tunnel.name),
),
TunnelConfig(name = makeTunnelNameUnique(tunnel.name), wgQuick = tunnel.wgQuick, amQuick = tunnel.amQuick),
)
}
@@ -9,6 +9,7 @@ import androidx.compose.material.icons.rounded.CopyAll
import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material.icons.rounded.Info
import androidx.compose.material.icons.rounded.Settings
import androidx.compose.material.icons.rounded.SettingsEthernet
import androidx.compose.material.icons.rounded.Smartphone
import androidx.compose.material.icons.rounded.Star
import androidx.compose.material3.Icon
@@ -55,14 +56,12 @@ fun TunnelRowItem(
val itemFocusRequester = remember { FocusRequester() }
ExpandingRowListItem(
leading = {
val icon =
if (tunnel.isPrimaryTunnel) {
Icons.Rounded.Star
} else if (tunnel.isMobileDataTunnel) {
Icons.Rounded.Smartphone
} else {
Icons.Rounded.Circle
}
val icon = when {
tunnel.isPrimaryTunnel -> Icons.Rounded.Star
tunnel.isMobileDataTunnel -> Icons.Rounded.Smartphone
tunnel.isEthernetTunnel -> Icons.Rounded.SettingsEthernet
else -> Icons.Rounded.Circle
}
Icon(
icon,
icon.name,
@@ -15,6 +15,7 @@ import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material.icons.outlined.NetworkPing
import androidx.compose.material.icons.outlined.PhoneAndroid
import androidx.compose.material.icons.outlined.Security
import androidx.compose.material.icons.outlined.SettingsEthernet
import androidx.compose.material.icons.outlined.Star
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@@ -139,6 +140,28 @@ fun OptionsScreen(optionsViewModel: OptionsViewModel = hiltViewModel(), appUiSta
},
onClick = { optionsViewModel.onToggleIsMobileDataTunnel(config) },
),
SelectionItem(
Icons.Outlined.SettingsEthernet,
title = {
Text(
stringResource(R.string.ethernet_tunnel),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
description = {
Text(
stringResource(R.string.set_ethernet_tunnel),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.outline),
)
},
trailing = {
ScaledSwitch(
config.isEthernetTunnel,
onClick = { optionsViewModel.onToggleIsEthernetTunnel(config) },
)
},
onClick = { optionsViewModel.onToggleIsEthernetTunnel(config) },
),
SelectionItem(
Icons.Outlined.NetworkPing,
title = {
@@ -77,4 +77,12 @@ constructor(
),
)
}
fun onToggleIsEthernetTunnel(tunnelConfig: TunnelConfig) = viewModelScope.launch {
if (tunnelConfig.isEthernetTunnel) {
appDataRepository.tunnels.updateEthernetTunnel(null)
} else {
appDataRepository.tunnels.updateEthernetTunnel(tunnelConfig)
}
}
}
@@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.AirplanemodeActive
import androidx.compose.material.icons.outlined.Code
import androidx.compose.material.icons.outlined.Filter1
import androidx.compose.material.icons.outlined.NetworkPing
@@ -71,16 +72,18 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV
isBackgroundLocationGranted = fineLocationState.status.isGranted
}
fun onAutoTunnelWifiChecked() {
if (uiState.settings.isTunnelOnWifiEnabled) viewModel.onToggleTunnelOnWifi().also { return }
when (false) {
isBackgroundLocationGranted -> showLocationDialog = true
fineLocationState.status.isGranted -> showLocationDialog = true
context.isLocationServicesEnabled() ->
showLocationServicesAlertDialog = true
else -> {
viewModel.onToggleTunnelOnWifi()
fun isWifiNameReadable(): Boolean {
return when {
!isBackgroundLocationGranted ||
!fineLocationState.status.isGranted -> {
showLocationDialog = true
false
}
!context.isLocationServicesEnabled() -> {
showLocationServicesAlertDialog = true
false
}
else -> true
}
}
@@ -117,14 +120,14 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV
topBar = {
TopNavBar(stringResource(R.string.auto_tunneling))
},
) {
) { padding ->
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top),
modifier =
Modifier
.fillMaxSize()
.padding(it)
.padding(padding)
.padding(top = 24.dp.scaledHeight())
.padding(horizontal = 24.dp.scaledWidth()),
) {
@@ -147,14 +150,12 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV
enabled = !uiState.settings.isAlwaysOnVpnEnabled,
checked = uiState.settings.isTunnelOnWifiEnabled,
onClick = {
if (uiState.settings.isWifiNameByShellEnabled) viewModel.onToggleTunnelOnWifi().also { return@ScaledSwitch }
onAutoTunnelWifiChecked()
viewModel.onToggleTunnelOnWifi()
},
)
},
onClick = {
if (uiState.settings.isWifiNameByShellEnabled) viewModel.onToggleTunnelOnWifi().also { return@SelectionItem }
onAutoTunnelWifiChecked()
viewModel.onToggleTunnelOnWifi()
},
),
SelectionItem(
@@ -250,7 +251,9 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV
uiState.settings.trustedNetworkSSIDs,
onDelete = viewModel::onDeleteTrustedSSID,
currentText = currentText,
onSave = viewModel::onSaveTrustedSSID,
onSave = { ssid ->
if (uiState.settings.isWifiNameByShellEnabled || isWifiNameReadable()) viewModel.onSaveTrustedSSID(ssid)
},
onValueChange = { currentText = it },
supporting = {
if (uiState.settings.isWildcardsEnabled) {
@@ -323,6 +326,30 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV
viewModel.onToggleRestartOnPing()
},
),
SelectionItem(
Icons.Outlined.AirplanemodeActive,
title = {
Text(
stringResource(R.string.stop_on_no_internet),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
description = {
Text(
stringResource(R.string.stop_on_internet_loss),
style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline),
)
},
trailing = {
ScaledSwitch(
checked = uiState.settings.isStopOnNoInternetEnabled,
onClick = { viewModel.onToggleStopOnNoInternet() },
)
},
onClick = {
viewModel.onToggleStopOnNoInternet()
},
),
),
)
}
@@ -128,4 +128,12 @@ constructor(
)
}
}
fun onToggleStopOnNoInternet() = viewModelScope.launch {
with(settings.value) {
appDataRepository.settings.save(
copy(isStopOnNoInternetEnabled = !isStopOnNoInternetEnabled),
)
}
}
}
+35 -2
View File
@@ -5,7 +5,7 @@
<string name="no_tunnels">Noch keine Tunnel hinzugefügt!</string>
<string name="tunnels">Tunnel</string>
<string name="tunnel_mobile_data">Tunnel für mobile Daten</string>
<string name="privacy_policy">Datenschutzbestimmungen anzeigen</string>
<string name="privacy_policy">Siehe Privacy Policy</string>
<string name="okay">Ok</string>
<string name="tunnel_on_ethernet">Tunnel für Ethernet</string>
<string name="auto_tunneling">Auto-Tunneln</string>
@@ -29,7 +29,7 @@
<string name="addresses">Adressen</string>
<string name="dns_servers">DNS-Server</string>
<string name="mtu">MTU</string>
<string name="peer">Peer</string>
<string name="peer">Gegenstelle</string>
<string name="allowed_ips">Erlaubte IPs</string>
<string name="endpoint">Endpunkt</string>
<string name="name">Name</string>
@@ -138,4 +138,37 @@
<string name="handshake">Handshake</string>
<string name="vpn_denied_dialog_title">Genehmigung verweigert</string>
<string name="logs">Logs</string>
<string name="kernel_not_supported">Kernel nicht unterstützt</string>
<string name="trusted_wifi_names">Vertrauenswürdige WLAN Namen</string>
<string name="requires_app_relaunch">Diese Änderung erfordert einen Neustart der App. Möchten Sie fortfahren?</string>
<string name="selected">Ausgewählt</string>
<string name="use_root_shell_for_wifi">Root-Shell verwenden, um WLAN-Namen zu ermitteln</string>
<string name="light">Hell</string>
<string name="add_wifi_name">WLAN Namen hinzufügen</string>
<string name="dark">Dunkel</string>
<string name="appearance">Erscheinungsbild</string>
<string name="notifications">Benachrichtigungen</string>
<string name="dynamic">Dynamisch</string>
<string name="skip">Überspringen</string>
<string name="learn_more">Mehr erfahren</string>
<string name="wildcards_active">Wildcards verfügbar</string>
<string name="wifi_name_via_shell">WLAN Namen per Shell</string>
<string name="start_auto">Auto-Tunnel starten</string>
<string name="tunnel_running">Laufender Tunnel</string>
<string name="donate">Fürs Projekt spenden</string>
<string name="local_logging">Lokales Logging</string>
<string name="enable_local_logging">Lokales Logging aktivieren</string>
<string name="configuration_change">Konfigurationsänderung</string>
<string name="add_from_clipboard">Aus Zwischenablage einfügen</string>
<string name="kill_switch">Kill Switch</string>
<string name="automatic">Automatisch</string>
<string name="language">Sprache</string>
<string name="display_theme">Anzeigetheme</string>
<string name="on_demand_rules">Regeln für Tunnel bei Bedarf</string>
<string name="launch_app_settings">App Einstellungen aufrufen</string>
<string name="primary_tunnel">Primärer Tunnel</string>
<string name="mobile_tunnel">Mobiler Daten-Tunnel</string>
<string name="use_wildcards">Wildcards für Namen verwenden</string>
<string name="stop_auto">Auto-Tunnel stoppen</string>
<string name="monitoring_state_changes">Überwache Statusänderungen</string>
</resources>
+1
View File
@@ -106,4 +106,5 @@
<string name="prominent_background_location_message">La monitorización SSID Wi-Fi necesita de permiso de ubicación en segundo plano incluso si la app está cerrada. Mira el enlace a la Política de Privacidad en la pantalla de ayuda para más detalles.</string>
<string name="junk_packet_count">Recuento de paquetes basura</string>
<string name="junk_packet_minimum_size">Tamaño mínimo del paquete basura</string>
<string name="add_from_clipboard">Agregar desde el portapapeles</string>
</resources>
+34 -1
View File
@@ -7,7 +7,7 @@
<string name="no_tunnels">Aucun tunnel n\'a été ajouté pour le moment!</string>
<string name="tunnels">Tunnels</string>
<string name="tunnel_mobile_data">Tunnel sur données mobiles</string>
<string name="privacy_policy">Voir la politique de confidentialité</string>
<string name="privacy_policy">Voir la Politique de Confidentialité</string>
<string name="tunnel_on_ethernet">Tunnel sur Ethernet</string>
<string name="prominent_background_location_title">Divulgation de la localisation en arrière-plan</string>
<string name="thank_you">Merci d\'utiliser WG Tunnel!</string>
@@ -138,4 +138,37 @@
<string name="set_custom_ping_internal">Intervalle de ping (sec)</string>
<string name="set_custom_ping_cooldown">Temps d\'attente avant redémarrage du ping (sec)</string>
<string name="sec">sec</string>
<string name="add_from_clipboard">Ajouter depuis le presse-papiers</string>
<string name="primary_tunnel">Tunnel principal</string>
<string name="stop_auto">Arrêter l\'auto-tunnel</string>
<string name="kill_switch">Arrêt d\'urgence</string>
<string name="appearance">Apparence</string>
<string name="notifications">Notifications</string>
<string name="automatic">Automatique</string>
<string name="light">Clair</string>
<string name="dark">Sombre</string>
<string name="dynamic">Dynamique</string>
<string name="language">Langue</string>
<string name="on_demand_rules">Règles de tunnel à la demande</string>
<string name="launch_app_settings">Ouvrir les paramètres de l\'appli</string>
<string name="display_theme">Thème d\'affichage</string>
<string name="selected">Sélectionné</string>
<string name="trusted_wifi_names">Nom wifi de confiance</string>
<string name="add_wifi_name">Ajouter un nom de wifi</string>
<string name="mobile_tunnel">Tunnel de données mobiles</string>
<string name="skip">Passer</string>
<string name="learn_more">En savoir plus</string>
<string name="use_wildcards">Utiliser les wildcards</string>
<string name="wildcards_active">Wildcards activé</string>
<string name="wifi_name_via_shell">Nom du Wifi via le shell</string>
<string name="use_root_shell_for_wifi">Utiliser un shell root pour obtenir le nom du wifi</string>
<string name="tunnel_running">Tunnel en cours d\'exécution</string>
<string name="monitoring_state_changes">Surveiller les changements d’état</string>
<string name="donate">Faire un don au projet</string>
<string name="local_logging">Journalisation locale</string>
<string name="enable_local_logging">Activer la journalisation locale</string>
<string name="configuration_change">Configuration modifiée</string>
<string name="kernel_not_supported">Noyau non supporté</string>
<string name="start_auto">Démarrer l\'auto-tunnel</string>
<string name="requires_app_relaunch">Cette modification nécessite un redémarrage de l\'application. Voulez-vous continuer?</string>
</resources>
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+33
View File
@@ -138,4 +138,37 @@
<string name="sec">сек.</string>
<string name="handshake">рукопожатие</string>
<string name="logs">Журналы</string>
<string name="light">Светлая</string>
<string name="automatic">Автоматически</string>
<string name="dynamic">Динамическая</string>
<string name="language">Язык</string>
<string name="trusted_wifi_names">Доверенные сети Wi-Fi</string>
<string name="primary_tunnel">Основной туннель</string>
<string name="mobile_tunnel">Туннель для мобильных данных</string>
<string name="learn_more">Узнать больше</string>
<string name="kernel_not_supported">Ядро не поддерживается</string>
<string name="requires_app_relaunch">Данное изменение требует перезапуска приложения. Продолжить?</string>
<string name="stop_auto">Остановить автотуннель</string>
<string name="donate">Пожертвовать на проект</string>
<string name="local_logging">Локальное ведение журнала</string>
<string name="wildcards_active">Подстановочные знаки используются</string>
<string name="configuration_change">Изменение конфигурации</string>
<string name="skip">Пропустить</string>
<string name="use_wildcards">Использовать подстановочные знаки в имени</string>
<string name="appearance">Внешний вид</string>
<string name="notifications">Уведомления</string>
<string name="kill_switch">Экстренный разрыв соединения</string>
<string name="dark">Тёмная</string>
<string name="display_theme">Тема</string>
<string name="selected">Выбрано</string>
<string name="add_wifi_name">Добавить сеть Wi-Fi</string>
<string name="launch_app_settings">Настройки запуска приложения</string>
<string name="on_demand_rules">Правила туннеля по запросу</string>
<string name="use_root_shell_for_wifi">Использовать root-доступ для получения имени сети Wi-Fi</string>
<string name="wifi_name_via_shell">Имя Wi-Fi через root</string>
<string name="start_auto">Запустить автотуннель</string>
<string name="tunnel_running">Туннель работает</string>
<string name="monitoring_state_changes">Отслеживание изменений состояния</string>
<string name="enable_local_logging">Включить ведение журнала</string>
<string name="add_from_clipboard">Добавить из буфера обмена</string>
</resources>
+73 -32
View File
@@ -1,14 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="turn_off_tunnel">请关闭连接再操作</string>
<string name="no_tunnels">你还没有添加隧道!</string>
<string name="no_tunnels">你还没有添加隧道</string>
<string name="tunnels">连接列表</string>
<string name="privacy_policy">查看隐私政策</string>
<string name="tunnel_mobile_data">隧道使用手机数据流量</string>
<string name="tunnel_mobile_data">允许隧道使用手机数据流量</string>
<string name="okay"></string>
<string name="tunnel_on_ethernet">在局域网中使用隧道</string>
<string name="error_file_extension">文件类型不是 .conf 或 .zip</string>
<string name="trusted_ssid_value_description">确认修改SSID</string>
<string name="trusted_ssid_value_description">确认修改 SSID</string>
<string name="add_tunnels_text">从文件或 zip 添加</string>
<string name="add_from_qr">从二维码添加</string>
<string name="qr_scan">扫描二维码</string>
@@ -18,15 +18,15 @@
<string name="icon">图标</string>
<string name="public_key">公钥</string>
<string name="addresses">地址</string>
<string name="dns_servers">DNS服务器</string>
<string name="dns_servers">DNS 服务器</string>
<string name="mtu">MTU</string>
<string name="allowed_ips">允许的IP</string>
<string name="allowed_ips">允许的 IP</string>
<string name="name">名称</string>
<string name="peer">节点</string>
<string name="always_on_vpn_support">允许VPN始终在线</string>
<string name="location_services_not_detected">定位服务未开启</string>
<string name="hint_search_packages">查找软件包</string>
<string name="db_name">wg-tunnel数据库</string>
<string name="db_name">wg-tunnel 数据库</string>
<string name="done">完成</string>
<string name="rotate_keys">轮换秘钥</string>
<string name="create_import">手动创建</string>
@@ -39,23 +39,23 @@
<string name="export_configs">导出设置</string>
<string name="docs_description">阅读文档</string>
<string name="email_description">给作者发邮件</string>
<string name="error_root_denied">Root权限未开启</string>
<string name="error_root_denied">root 权限未开启</string>
<string name="error_no_file_explorer">没有安装任何的文件管理器</string>
<string name="error_invalid_code">无效的二维码</string>
<string name="copy_public_key">复制公钥</string>
<string name="email_chooser">发送邮件…</string>
<string name="persistent_keepalive">连接保活</string>
<string name="turn_on_tunnel">此操作需要一个已建立的隧道</string>
<string name="tunnel_on_wifi">在不受信任的wifi上建立隧道</string>
<string name="exclude"></string>
<string name="tunnel_on_wifi">在不受信任的 WiFi 上建立隧道</string>
<string name="exclude"></string>
<string name="comma_separated_list">逗号分隔列表</string>
<string name="base64_key">base64编码</string>
<string name="base64_key">Base64 编码</string>
<string name="use_kernel">使用内核模块</string>
<string name="endpoint"></string>
<string name="thank_you">谢谢使用WG Tunnel!</string>
<string name="prominent_background_location_message">此功能是在app关闭时,后台自动扫描Wi-Fi SSID,需要开启后台位置信息访问权限。更多信息,请在支持页面查看隐私政策。</string>
<string name="vpn_on">VPN已连接</string>
<string name="vpn_off">VPN已关闭</string>
<string name="endpoint"></string>
<string name="thank_you">谢谢使用 WG Tunnel!</string>
<string name="prominent_background_location_message">此功能是在应用关闭时,后台自动扫描 Wi-Fi SSID,需要开启后台位置信息访问权限。更多信息,请在支持页面查看隐私政策。</string>
<string name="vpn_on">VPN 已连接</string>
<string name="vpn_off">VPN 已关闭</string>
<string name="auto_tunneling">自动建立隧道</string>
<string name="add_peer">添加节点</string>
<string name="random">(随机)</string>
@@ -73,13 +73,13 @@
<string name="auto_tunnel_title">自动连接服务</string>
<string name="delete_tunnel">删除隧道</string>
<string name="delete_tunnel_message">确定删除这个隧道吗?</string>
<string name="location_services_missing_message">app不会在你的设备上检测任何已开启的定位服务。根据不同的设备,可能会导致无法获得不可信wifi的名称。你想继续吗?</string>
<string name="location_services_missing_message">应用不会在你的设备上检测任何已开启的定位服务。根据不同的设备,可能会导致无法获得不可信 WiFi 的名称。你想继续吗?</string>
<string name="yes"></string>
<string name="tunneling_apps">正使用隧道的app</string>
<string name="tunneling_apps">正使用隧道的应用</string>
<string name="included">已包含</string>
<string name="excluded">排除</string>
<string name="all">全部</string>
<string name="no_email_detected">未安装邮件app</string>
<string name="no_email_detected">未安装邮件应用</string>
<string name="enable_app_lock">锁定应用</string>
<string name="use_tunnel_on_wifi_name">在指定的wifi上使用此隧道</string>
<string name="version">版本</string>
@@ -88,20 +88,20 @@
<string name="kernel">内核</string>
<string name="junk_packet_minimum_size">无效包最小值</string>
<string name="chat_description">加入社区</string>
<string name="root_accepted">已获取Root权限</string>
<string name="root_accepted">已获取 root 权限</string>
<string name="default_ping_ip">(可选,默认选择节点)</string>
<string name="set_custom_ping_internal">Ping 间隔(秒)</string>
<string name="optional_default">"可选,默认: "</string>
<string name="set_custom_ping_cooldown">Ping 重启间隔(秒)</string>
<string name="show_amnezia_properties">显示Amnezia属性</string>
<string name="show_amnezia_properties">显示 Amnezia 属性</string>
<string name="no_browser_detected">没有安装浏览器</string>
<string name="incorrect_pin">密码不正确</string>
<string name="set_custom_ping_ip">自定义要ping的地址</string>
<string name="set_custom_ping_ip">自定义 Ping 的目标 ip</string>
<string name="watcher_channel_name">守护者通知频道</string>
<string name="vpn_channel_id">VPN频道</string>
<string name="vpn_channel_id">VPN 频道</string>
<string name="junk_packet_count">无效包计数</string>
<string name="app_name">WG Tunnel</string>
<string name="vpn_channel_name">VPN通知频道</string>
<string name="vpn_channel_name">VPN 通知频道</string>
<string name="watcher_channel_id">守护者频道</string>
<string name="open_issue">查看问题</string>
<string name="read_logs">查看日志</string>
@@ -110,24 +110,65 @@
<string name="enter_pin">输入密码</string>
<string name="create_pin">创建密码</string>
<string name="set_primary_tunnel">设置为主隧道</string>
<string name="mobile_data_tunnel">使用手机数据流量</string>
<string name="mobile_data_tunnel">允许使用手机数据流量</string>
<string name="init_packet_junk_size">初始化无效包大小</string>
<string name="junk_packet_maximum_size">无效包最大值</string>
<string name="response_packet_magic_header">响应包的魔法header</string>
<string name="response_packet_magic_header">响应包的 magic header</string>
<string name="response_packet_junk_size">无效的响应包大小</string>
<string name="vpn_denied_dialog_title">拒绝访问</string>
<string name="tunnel_required">此功能需要至少一个隧道</string>
<string name="app_settings">app 设置</string>
<string name="app_settings">应用设置</string>
<string name="background_location_message2">请确保这些权限已开启。</string>
<string name="logs">日志</string>
<string name="restart_on_ping">ping失败之后自动重启隧道(beta)</string>
<string name="restart_on_ping">Ping 失败之后自动重启隧道(beta)</string>
<string name="edit_tunnel">编辑隧道</string>
<string name="init_packet_magic_header">初始化数据包的魔法header</string>
<string name="init_packet_magic_header">初始化数据包的 magic header</string>
<string name="error_file_format">无效的隧道配置文件格式</string>
<string name="always_on_message">VPN连接被拒绝,请检查</string>
<string name="vpn_settings">VPN系统设置</string>
<string name="always_on_message2">始终开启VPN功能是否关闭,然后再尝试连接</string>
<string name="always_on_message">VPN 连接被拒绝,请检查</string>
<string name="vpn_settings">系统 VPN 设置</string>
<string name="always_on_message2">确保始终开启 VPN 功能关闭,然后再尝试连接</string>
<string name="never">从不</string>
<string name="sec"></string>
<string name="handshake">握手</string>
<string name="handshake">握手</string>
<string name="light">亮色</string>
<string name="dark">暗色</string>
<string name="trusted_wifi_names">可信 WiFi SSID</string>
<string name="prominent_background_location_title">后台定位披露</string>
<string name="dynamic">动态颜色</string>
<string name="display_theme">主题</string>
<string name="kill_switch">系统 VPN 设置</string>
<string name="appearance">外观</string>
<string name="notifications">通知</string>
<string name="automatic">跟随系统</string>
<string name="language">语言</string>
<string name="add_wifi_name">添加 WiFi SSID</string>
<string name="primary_tunnel">主隧道</string>
<string name="mobile_tunnel">允许使用移动数据</string>
<string name="add_from_clipboard">从剪贴板添加</string>
<string name="transport_packet_magic_header">传输包的 magic header</string>
<string name="underload_packet_magic_header">欠载数据包 magic header</string>
<string name="restart_at_boot">开机时重新启动</string>
<string name="background_location_message">需要允许所有时间位置权限和/或精确位置才能使用此功能。请参阅</string>
<string name="learn_more">了解更多</string>
<string name="unsure_how">如果你不确定如何进行</string>
<string name="see_the"></string>
<string name="getting_started_guide">入门指南</string>
<string name="selected">已选择</string>
<string name="on_demand_rules">按需隧道规则</string>
<string name="skip">取消</string>
<string name="launch_app_settings">打开应用设置</string>
<string name="use_wildcards">使用 SSID 通配符</string>
<string name="wildcards_active">启用通配符</string>
<string name="wifi_name_via_shell">通过 shell 获取 WiFi 名称</string>
<string name="use_root_shell_for_wifi">使用 root 权限的 shell 来获取 WiFi 名称</string>
<string name="kernel_not_supported">内核不支持</string>
<string name="start_auto">开启自动隧道</string>
<string name="monitoring_state_changes">监控状态变化</string>
<string name="stop_auto">停止自动隧道</string>
<string name="tunnel_running">隧道运行中</string>
<string name="donate">捐赠</string>
<string name="local_logging">开启日志</string>
<string name="enable_local_logging">开启本地日志</string>
<string name="configuration_change">配置更改</string>
<string name="requires_app_relaunch">此更改需要重新启动应用程序。您是否要继续?</string>
</resources>
+5 -1
View File
@@ -133,7 +133,7 @@
<string name="tunnel_required">Feature requires at least one tunnel</string>
<string name="background_location_message">Allow all the time location permission and/or precise location is required for this feature. Please see</string>
<string name="app_settings">app settings</string>
<string name="background_location_message2">to make sure these permissions are enabled.</string>
<string name="background_location_message2">to make sure these permissions are enabled</string>
<string name="root_accepted">Root shell accepted</string>
<string name="set_custom_ping_ip">Set custom ping ip</string>
<string name="default_ping_ip">(optional, defaults to peers)</string>
@@ -178,4 +178,8 @@
<string name="configuration_change">Configuration change</string>
<string name="requires_app_relaunch">This change requires an app relaunch. Would you like to proceed?</string>
<string name="add_from_clipboard">Add from clipboard</string>
<string name="stop_on_no_internet">Stop on no internet</string>
<string name="stop_on_internet_loss">Stop tunnel on internet loss</string>
<string name="ethernet_tunnel">Ethernet tunnel</string>
<string name="set_ethernet_tunnel">Set as ethernet tunnel</string>
</resources>
@@ -1,5 +1,5 @@
Was ist neu:
- Auto-Tunnel Auswahl nach WLAN-Name
- Auto-Tunnel Kontrolle durch Kacheln und Verknüpfungen
- Auto-Tunnel Steuerung durch Kacheln und Verknüpfungen
- Automatischer Neustart des Manuellen Tunnels nach einem Systemneustart
- Verschiedene Fehlerbehebungen und Performanceverbesserungen
@@ -1,5 +1,5 @@
Was ist neu:
- Verbesserung der Benennung von Tunnel-Importen
- Fehler des Anfangszustand beim automatischen Tunneln behoben
- Fehler des Anfangszustand beim Auto-Tunneln behoben
- Verbesserte Fehlerhandhabung
- Fehler beim Import von Amnezia zip behoben
@@ -1,5 +1,5 @@
Was ist neu
- Unterstützung für weitere Sprachen
- bug-fix für auto-tunneling bei Mobilfunk
- bug-fix für auto-tunneling bei mobilen Daten
- für AndroidTV: Bug bei schwebendem Aktionsknopf behoben
- andere Optimisation und Erweiterungen
@@ -0,0 +1,6 @@
Was ist neu?
- UI-Aktualisierung
- Verbesserungen der AndroidTV-Navigation
- Behebung eines Fehlers, der den Akku entleert
- Fehlerbehebung bei Wildcards mit optionaler Einstellung
- Weitere Verbesserungen
@@ -0,0 +1,7 @@
Was ist neu?
- Tunnel aus der Zwischenablage hinzufügen
- Lokalisierungen hinzugefügt
- Behebung eines Fehlers, der den Akku entleert
- Fehler beim Löschen behoben
- Verbesserte Synchronisation der Tunnelkacheln
- Andere Korrekturen und Verbesserungen
@@ -3,12 +3,12 @@ Funktionen
- Hinzufügen von Tunneln über .conf Dateien, Zip, Manuelle Eingabe oder QR Codes
- Automatische Verbindung zum VPN basierend auf der WLAN-SSID, Ethernet und mobilen Daten
- Geteilter Tunnel für Anwendungen mit Suche
- Unterstützung für Wireguard Userspace- und Kernel-modus
- Unterstützung für Wireguard Userspace- und Kernel-Modus
- Amnezia Unterstützung für Benutzeroberflächen-Modus zur DPI/Zensurschutz
- Always-On VPN Unterstützung
- Export von Amnezia- und WireGuard-Tunnel ins Zip Format
- Quicktiles zum aktivieren/deaktivieren der VPN Verbindung
- Feste Shortcuts für den Haupttunnel für automatische Integration
- Feste Shortcuts für den Primären Tunnel für automatische Integration
- Absichtlicher Automationssupport für alle Tunnel
- Automatischer Servicestart nach Geräteneustart
- Akkuerhaltungsfunktionen
@@ -0,0 +1,6 @@
Novedades:
- Actualización de la interfaz de usuario
- Mejoras en la navegación de AndroidTV
- Corrección del error de descarga de la batería
- Se corrigieron los comodines con configuración opcional
- Otras mejoras
@@ -0,0 +1,7 @@
Novedades:
- Agregar túnel desde el portapapeles
- Añadir localizaciones
- Solucionar el error de descarga de la batería
- Corregir error de eliminación
- Mejorar la sincronización de los mosaicos del túnel
- Otras correcciones y mejoras
@@ -0,0 +1,6 @@
Quoi de neuf :
- Mise à jour de l'interface utilisateur
- Améliorations de la navigation sur Android TV
- Correction d'un bug de surconsommation de la batterie
- Correction des wildcards avec réglage optionnel
- Autres améliorations
@@ -0,0 +1,7 @@
Quoi de neuf :
- Ajout des tunnels depuis le presse-papiers
- Ajout des localisations
- Correction d'un bug de surconsommation de la batterie
- Correction d'un bug de suppression
- Amélioration de la synchronisation des tuiles du tunnel.
- Autres améliorations
@@ -0,0 +1,6 @@
Что нового:
- Обновление интерфейса
- Улучшения навигации AndroidTV
- Исправление ошибки разряда батареи
- Исправление подстановочных знаков дополнительной настройкой
- Другие улучшения
@@ -0,0 +1,7 @@
Что нового:
- Добавление туннеля из буфера обмена
- Добавление переводов
- Исправлена ошибка разряда батареи
- Исправлена ошибка удаления
- Улучшена синхронизация плиток туннеля
- Другие исправления и улучшения
+1 -1
View File
@@ -21,7 +21,7 @@ pinLockCompose = "1.0.4"
roomVersion = "2.6.1"
timber = "5.0.1"
tunnel = "1.2.1"
androidGradlePlugin = "8.7.2"
androidGradlePlugin = "8.7.3"
kotlin = "2.0.21"
ksp = "2.0.21-1.0.28"
composeBom = "2024.11.00"
+1 -1
View File
@@ -1 +1 @@
9
11