mirror of
https://github.com/wgtunnel/android.git
synced 2026-07-03 14:07:49 +02:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d000c79134 | |||
| c3a2e05eb2 | |||
| a992009c71 | |||
| 57676bf4bb | |||
| 921e33cb70 | |||
| 70649383e0 | |||
| 64a7680b81 | |||
| a81d3a8843 |
@@ -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')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
+13
@@ -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)
|
||||
|
||||
+2
@@ -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,
|
||||
}
|
||||
-107
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
@@ -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
|
||||
|
||||
+9
@@ -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()
|
||||
}
|
||||
+50
-170
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
+144
@@ -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)
|
||||
}
|
||||
}
|
||||
+7
-3
@@ -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"
|
||||
|
||||
+1
-1
@@ -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)
|
||||
}
|
||||
|
||||
+4
-3
@@ -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) {
|
||||
|
||||
+68
-96
@@ -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,
|
||||
|
||||
+3
-9
@@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+7
-8
@@ -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,
|
||||
|
||||
+23
@@ -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 = {
|
||||
|
||||
+8
@@ -77,4 +77,12 @@ constructor(
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun onToggleIsEthernetTunnel(tunnelConfig: TunnelConfig) = viewModelScope.launch {
|
||||
if (tunnelConfig.isEthernetTunnel) {
|
||||
appDataRepository.tunnels.updateEthernetTunnel(null)
|
||||
} else {
|
||||
appDataRepository.tunnels.updateEthernetTunnel(tunnelConfig)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+43
-16
@@ -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()
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
+8
@@ -128,4 +128,12 @@ constructor(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onToggleStopOnNoInternet() = viewModelScope.launch {
|
||||
with(settings.value) {
|
||||
appDataRepository.settings.save(
|
||||
copy(isStopOnNoInternetEnabled = !isStopOnNoInternetEnabled),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 @@
|
||||
Что нового:
|
||||
- Добавление туннеля из буфера обмена
|
||||
- Добавление переводов
|
||||
- Исправлена ошибка разряда батареи
|
||||
- Исправлена ошибка удаления
|
||||
- Улучшена синхронизация плиток туннеля
|
||||
- Другие исправления и улучшения
|
||||
@@ -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
@@ -1 +1 @@
|
||||
9
|
||||
11
|
||||
Reference in New Issue
Block a user