Compare commits

..

20 Commits

Author SHA1 Message Date
Zane Schepke 6762d4733e chore: bump version 2024-12-09 22:46:27 -05:00
Zane Schepke add18d5cef chore: bump version w/notes 2024-12-09 22:44:37 -05:00
Zane Schepke c5b42b55c3 feat: improved notifications with actions (#481) 2024-12-09 22:38:55 -05:00
Zane Schepke 670d9d680c fix: make logging lifecycle aware 2024-12-09 21:41:59 -05:00
GitHub Actions bbfc0e2fab Automated build update 2024-12-09 03:44:20 +00:00
Zane Schepke 708b4c7646 fix: auto tunnel wifi whitelist bug
#472
2024-12-08 15:06:13 -05:00
Zane Schepke efba604c31 fix: screen padding/scroll bug
closes #479
2024-12-08 14:43:15 -05:00
GitHub Actions 1441488053 Automated build update 2024-12-08 03:46:39 +00:00
Zane Schepke bb3b05d224 fix: locale change bug and sort tunnels alphabetically 2024-12-07 22:34:45 -05:00
Zane Schepke cda747deee feat: add vpn kill switch (#476) 2024-12-07 18:10:03 -05: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
GitHub Actions bad2f55121 Automated build update 2024-11-24 03:41:29 +00:00
Zane Schepke fecc55fe9c chore: add id title 2024-11-23 17:39:47 -05:00
Zane Schepke 4d2e9629f9 refactor: pt language code 2024-11-23 17:26:53 -05:00
95 changed files with 2365 additions and 1329 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')"
]
}
}
@@ -0,0 +1,267 @@
{
"formatVersion": 1,
"database": {
"version": 13,
"identityHash": "ff209157b98a641c424f5086818ec585",
"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, `is_vpn_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_lan_on_kill_switch_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"
},
{
"fieldPath": "isVpnKillSwitchEnabled",
"columnName": "is_vpn_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isKernelKillSwitchEnabled",
"columnName": "is_kernel_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isLanOnKillSwitchEnabled",
"columnName": "is_lan_on_kill_switch_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, 'ff209157b98a641c424f5086818ec585')"
]
}
}
+6 -1
View File
@@ -157,7 +157,7 @@
android:value="true" /> android:value="true" />
</service> </service>
<service <service
android:name=".service.foreground.AutoTunnelService" android:name=".service.foreground.autotunnel.AutoTunnelService"
android:enabled="true" android:enabled="true"
android:exported="false" android:exported="false"
android:foregroundServiceType="systemExempted" android:foregroundServiceType="systemExempted"
@@ -204,5 +204,10 @@
<action android:name="com.wireguard.android.action.REFRESH_TUNNEL_STATES" /> <action android:name="com.wireguard.android.action.REFRESH_TUNNEL_STATES" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver
android:name=".receiver.NotificationActionReceiver"
android:exported="false"
android:permission="${applicationId}.permission.CONTROL_TUNNELS">
</receiver>
</application> </application>
</manifest> </manifest>
@@ -3,12 +3,14 @@ package com.zaneschepke.wireguardautotunnel
import android.app.Application import android.app.Application
import android.os.StrictMode import android.os.StrictMode
import android.os.StrictMode.ThreadPolicy import android.os.StrictMode.ThreadPolicy
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.LocaleListCompat
import com.zaneschepke.logcatter.LogReader import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.module.MainDispatcher
import com.zaneschepke.wireguardautotunnel.service.tunnel.BackendState
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
@@ -16,6 +18,7 @@ import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@@ -32,10 +35,20 @@ class WireGuardAutoTunnel : Application() {
@Inject @Inject
lateinit var appStateRepository: AppStateRepository lateinit var appStateRepository: AppStateRepository
@Inject
lateinit var settingsRepository: SettingsRepository
@Inject
lateinit var tunnelService: TunnelService
@Inject @Inject
@IoDispatcher @IoDispatcher
lateinit var ioDispatcher: CoroutineDispatcher lateinit var ioDispatcher: CoroutineDispatcher
@Inject
@MainDispatcher
lateinit var mainDispatcher: CoroutineDispatcher
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
instance = this instance = this
@@ -52,21 +65,26 @@ class WireGuardAutoTunnel : Application() {
} else { } else {
Timber.plant(ReleaseTree()) Timber.plant(ReleaseTree())
} }
applicationScope.launch { applicationScope.launch {
withContext(mainDispatcher) {
if (appStateRepository.isLocalLogsEnabled() && !isRunningOnTv()) logReader.initialize()
}
if (!settingsRepository.getSettings().isKernelEnabled) {
tunnelService.setBackendState(BackendState.SERVICE_ACTIVE, emptyList())
}
appStateRepository.getLocale()?.let { appStateRepository.getLocale()?.let {
val locale = LocaleUtil.getLocaleFromPrefCode(it) LocaleUtil.changeLocale(it)
val appLocale: LocaleListCompat = LocaleListCompat.forLanguageTags(locale)
AppCompatDelegate.setApplicationLocales(appLocale)
} }
} }
if (!isRunningOnTv()) { }
applicationScope.launch(ioDispatcher) {
if (appStateRepository.isLocalLogsEnabled()) { override fun onTerminate() {
Timber.d("Starting logger") applicationScope.launch {
logReader.start() tunnelService.setBackendState(BackendState.INACTIVE, emptyList())
}
}
} }
super.onTerminate()
} }
companion object { companion object {
@@ -11,7 +11,7 @@ import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
@Database( @Database(
entities = [Settings::class, TunnelConfig::class], entities = [Settings::class, TunnelConfig::class],
version = 11, version = 13,
autoMigrations = autoMigrations =
[ [
AutoMigration(from = 1, to = 2), AutoMigration(from = 1, to = 2),
@@ -41,6 +41,14 @@ import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
to = 11, to = 11,
spec = RemoveTunnelPauseMigration::class, spec = RemoveTunnelPauseMigration::class,
), ),
AutoMigration(
from = 11,
to = 12,
),
AutoMigration(
from = 12,
to = 13,
),
], ],
exportSchema = true, exportSchema = true,
) )
@@ -16,7 +16,7 @@ object Queries {
VALUES VALUES
('false', ('false',
'false', 'false',
'sampleSSID1,sampleSSID2', '',
'false', 'false',
'false', 'false',
'false', 'false',
@@ -44,6 +44,9 @@ interface TunnelConfigDao {
@Query("UPDATE TunnelConfig SET is_mobile_data_tunnel = 0 WHERE is_mobile_data_tunnel =1") @Query("UPDATE TunnelConfig SET is_mobile_data_tunnel = 0 WHERE is_mobile_data_tunnel =1")
suspend fun resetMobileDataTunnel() 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") @Query("SELECT * FROM TUNNELCONFIG WHERE is_primary_tunnel=1")
suspend fun findByPrimary(): TunnelConfigs suspend fun findByPrimary(): TunnelConfigs
@@ -60,4 +60,24 @@ data class Settings(
defaultValue = "false", defaultValue = "false",
) )
val isWifiNameByShellEnabled: Boolean = false, val isWifiNameByShellEnabled: Boolean = false,
@ColumnInfo(
name = "is_stop_on_no_internet_enabled",
defaultValue = "false",
)
val isStopOnNoInternetEnabled: Boolean = false,
@ColumnInfo(
name = "is_vpn_kill_switch_enabled",
defaultValue = "false",
)
val isVpnKillSwitchEnabled: Boolean = false,
@ColumnInfo(
name = "is_kernel_kill_switch_enabled",
defaultValue = "false",
)
val isKernelKillSwitchEnabled: Boolean = false,
@ColumnInfo(
name = "is_lan_on_kill_switch_enabled",
defaultValue = "false",
)
val isLanOnKillSwitchEnabled: Boolean = false,
) )
@@ -58,6 +58,11 @@ data class TunnelConfig(
defaultValue = "null", defaultValue = "null",
) )
var pingIp: String? = null, var pingIp: String? = null,
@ColumnInfo(
name = "is_ethernet_tunnel",
defaultValue = "false",
)
var isEthernetTunnel: Boolean = false,
) { ) {
fun toAmConfig(): org.amnezia.awg.config.Config { fun toAmConfig(): org.amnezia.awg.config.Config {
@@ -87,5 +92,14 @@ data class TunnelConfig(
} }
const val AM_QUICK_DEFAULT = "" const val AM_QUICK_DEFAULT = ""
val IPV4_PUBLIC_NETWORKS = setOf(
"0.0.0.0/5", "8.0.0.0/7", "11.0.0.0/8", "12.0.0.0/6", "16.0.0.0/4", "32.0.0.0/3",
"64.0.0.0/2", "128.0.0.0/3", "160.0.0.0/5", "168.0.0.0/6", "172.0.0.0/12",
"172.32.0.0/11", "172.64.0.0/10", "172.128.0.0/9", "173.0.0.0/8", "174.0.0.0/7",
"176.0.0.0/4", "192.0.0.0/9", "192.128.0.0/11", "192.160.0.0/13", "192.169.0.0/16",
"192.170.0.0/15", "192.172.0.0/14", "192.176.0.0/12", "192.192.0.0/10",
"193.0.0.0/8", "194.0.0.0/7", "196.0.0.0/6", "200.0.0.0/5", "208.0.0.0/4",
)
} }
} }
@@ -17,10 +17,6 @@ interface AppStateRepository {
suspend fun setBatteryOptimizationDisableShown(shown: Boolean) suspend fun setBatteryOptimizationDisableShown(shown: Boolean)
suspend fun getCurrentSsid(): String?
suspend fun setCurrentSsid(ssid: String)
suspend fun isTunnelStatsExpanded(): Boolean suspend fun isTunnelStatsExpanded(): Boolean
suspend fun setTunnelStatsExpanded(expanded: Boolean) suspend fun setTunnelStatsExpanded(expanded: Boolean)
@@ -38,14 +38,6 @@ class DataStoreAppStateRepository(
dataStoreManager.saveToDataStore(DataStoreManager.batteryDisableShown, shown) dataStoreManager.saveToDataStore(DataStoreManager.batteryDisableShown, shown)
} }
override suspend fun getCurrentSsid(): String? {
return dataStoreManager.getFromStore(DataStoreManager.currentSSID)
}
override suspend fun setCurrentSsid(ssid: String) {
dataStoreManager.saveToDataStore(DataStoreManager.currentSSID, ssid)
}
override suspend fun isTunnelStatsExpanded(): Boolean { override suspend fun isTunnelStatsExpanded(): Boolean {
return dataStoreManager.getFromStore(DataStoreManager.tunnelStatsExpanded) return dataStoreManager.getFromStore(DataStoreManager.tunnelStatsExpanded)
?: GeneralState.IS_TUNNEL_STATS_EXPANDED ?: GeneralState.IS_TUNNEL_STATS_EXPANDED
@@ -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) { override suspend fun delete(tunnelConfig: TunnelConfig) {
withContext(ioDispatcher) { withContext(ioDispatcher) {
tunnelConfigDao.delete(tunnelConfig) tunnelConfigDao.delete(tunnelConfig)
@@ -15,6 +15,8 @@ interface TunnelConfigRepository {
suspend fun updateMobileDataTunnel(tunnelConfig: TunnelConfig?) suspend fun updateMobileDataTunnel(tunnelConfig: TunnelConfig?)
suspend fun updateEthernetTunnel(tunnelConfig: TunnelConfig?)
suspend fun delete(tunnelConfig: TunnelConfig) suspend fun delete(tunnelConfig: TunnelConfig)
suspend fun getById(id: Int): TunnelConfig? suspend fun getById(id: Int): TunnelConfig?
@@ -2,7 +2,9 @@ package com.zaneschepke.wireguardautotunnel.module
import android.content.Context import android.content.Context
import com.zaneschepke.logcatter.LogReader import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.logcatter.LogcatCollector import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.notification.WireGuardNotification
import com.zaneschepke.logcatter.LogcatReader
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@@ -25,6 +27,12 @@ class AppModule {
@Singleton @Singleton
@Provides @Provides
fun provideLogCollect(@ApplicationContext context: Context): LogReader { fun provideLogCollect(@ApplicationContext context: Context): LogReader {
return LogcatCollector.init(context = context) return LogcatReader.init(storageDir = context.filesDir.absolutePath)
}
@Singleton
@Provides
fun provideNotificationService(@ApplicationContext context: Context): NotificationService {
return WireGuardNotification(context)
} }
} }
@@ -4,8 +4,6 @@ import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
import com.zaneschepke.wireguardautotunnel.service.network.WifiService import com.zaneschepke.wireguardautotunnel.service.network.WifiService
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.notification.WireGuardNotification
import dagger.Binds import dagger.Binds
import dagger.Module import dagger.Module
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@@ -15,10 +13,6 @@ import dagger.hilt.android.scopes.ServiceScoped
@Module @Module
@InstallIn(ServiceComponent::class) @InstallIn(ServiceComponent::class)
abstract class ServiceModule { abstract class ServiceModule {
@Binds
@ServiceScoped
abstract fun provideNotificationService(wireGuardNotification: WireGuardNotification): NotificationService
@Binds @Binds
@ServiceScoped @ServiceScoped
abstract fun provideWifiService(wifiService: WifiService): NetworkService<WifiService> abstract fun provideWifiService(wifiService: WifiService): NetworkService<WifiService>
@@ -10,6 +10,7 @@ import com.wireguard.android.util.ToolsInstaller
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.WireGuardTunnel import com.zaneschepke.wireguardautotunnel.service.tunnel.WireGuardTunnel
import dagger.Module import dagger.Module
@@ -76,6 +77,7 @@ class TunnelModule {
@ApplicationScope applicationScope: CoroutineScope, @ApplicationScope applicationScope: CoroutineScope,
@IoDispatcher ioDispatcher: CoroutineDispatcher, @IoDispatcher ioDispatcher: CoroutineDispatcher,
serviceManager: ServiceManager, serviceManager: ServiceManager,
notificationService: NotificationService,
): TunnelService { ): TunnelService {
return WireGuardTunnel( return WireGuardTunnel(
amneziaBackend, amneziaBackend,
@@ -85,12 +87,17 @@ class TunnelModule {
applicationScope, applicationScope,
ioDispatcher, ioDispatcher,
serviceManager, serviceManager,
notificationService,
) )
} }
@Singleton @Singleton
@Provides @Provides
fun provideServiceManager(@ApplicationContext context: Context): ServiceManager { fun provideServiceManager(
return ServiceManager.getInstance(context) @ApplicationContext context: Context,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
appDataRepository: AppDataRepository,
): ServiceManager {
return ServiceManager(context, ioDispatcher, appDataRepository)
} }
} }
@@ -5,8 +5,8 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -26,6 +26,9 @@ class KernelReceiver : BroadcastReceiver() {
@Inject @Inject
lateinit var tunnelConfigRepository: TunnelConfigRepository lateinit var tunnelConfigRepository: TunnelConfigRepository
@Inject
lateinit var serviceManager: ServiceManager
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
val action = intent.action ?: return val action = intent.action ?: return
applicationScope.launch { applicationScope.launch {
@@ -37,7 +40,7 @@ class KernelReceiver : BroadcastReceiver() {
tunnelConfigRepository.save(it.copy(isActive = true)) tunnelConfigRepository.save(it.copy(isActive = true))
} }
} }
context.requestTunnelTileServiceStateUpdate() serviceManager.updateTunnelTile()
} }
} }
} }
@@ -0,0 +1,36 @@
package com.zaneschepke.wireguardautotunnel.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationAction
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint
class NotificationActionReceiver : BroadcastReceiver() {
@Inject
lateinit var serviceManager: ServiceManager
@Inject
lateinit var tunnelService: TunnelService
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
override fun onReceive(context: Context, intent: Intent) {
applicationScope.launch {
when (intent.action) {
NotificationAction.AUTO_TUNNEL_OFF.name -> serviceManager.stopAutoTunnel()
NotificationAction.TUNNEL_OFF.name -> tunnelService.stopTunnel()
}
}
}
}
@@ -1,8 +0,0 @@
package com.zaneschepke.wireguardautotunnel.service.foreground
enum class Action {
START,
START_FOREGROUND,
STOP,
STOP_FOREGROUND,
}
@@ -1,556 +0,0 @@
package com.zaneschepke.wireguardautotunnel.service.foreground
import android.content.Intent
import android.net.NetworkCapabilities
import android.os.IBinder
import android.os.PowerManager
import androidx.core.app.ServiceCompat
import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope
import com.wireguard.android.util.RootShell
import com.zaneschepke.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.network.EthernetService
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
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
import com.zaneschepke.wireguardautotunnel.util.extensions.isReachable
import com.zaneschepke.wireguardautotunnel.util.extensions.onNotRunning
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.net.InetAddress
import javax.inject.Inject
import javax.inject.Provider
@AndroidEntryPoint
class AutoTunnelService : LifecycleService() {
private val foregroundId = 122
@Inject
@AppShell
lateinit var rootShell: Provider<RootShell>
@Inject
lateinit var wifiService: NetworkService<WifiService>
@Inject
lateinit var mobileDataService: NetworkService<MobileDataService>
@Inject
lateinit var ethernetService: NetworkService<EthernetService>
@Inject
lateinit var appDataRepository: AppDataRepository
@Inject
lateinit var notificationService: NotificationService
@Inject
lateinit var tunnelService: Provider<TunnelService>
@Inject
@IoDispatcher
lateinit var ioDispatcher: CoroutineDispatcher
@Inject
lateinit var serviceManager: ServiceManager
@Inject
@MainImmediateDispatcher
lateinit var mainImmediateDispatcher: CoroutineDispatcher
private val autoTunnelStateFlow = MutableStateFlow(AutoTunnelState())
private var wakeLock: PowerManager.WakeLock? = null
private var wifiJob: Job? = null
private var mobileDataJob: Job? = null
private var ethernetJob: Job? = null
private var pingJob: Job? = null
private var networkEventJob: Job? = null
override fun onCreate() {
super.onCreate()
lifecycleScope.launch(mainImmediateDispatcher) {
kotlin.runCatching {
launchWatcherNotification()
}.onFailure {
Timber.e(it)
}
}
}
override fun onBind(intent: Intent): IBinder? {
super.onBind(intent)
// We don't provide binding, so return null
return null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Timber.d("onStartCommand executed with startId: $startId")
serviceManager.autoTunnelService.complete(this)
return super.onStartCommand(intent, flags, startId)
}
fun start() {
kotlin.runCatching {
lifecycleScope.launch(mainImmediateDispatcher) {
launchWatcherNotification()
initWakeLock()
}
startSettingsJob()
startVpnStateJob()
}.onFailure {
Timber.e(it)
}
}
fun stop() {
wakeLock?.let {
if (it.isHeld) {
it.release()
}
}
stopSelf()
}
override fun onDestroy() {
cancelAndResetNetworkJobs()
cancelAndResetPingJob()
serviceManager.autoTunnelService = CompletableDeferred()
super.onDestroy()
}
private fun launchWatcherNotification(description: String = getString(R.string.monitoring_state_changes)) {
val notification =
notificationService.createNotification(
channelId = getString(R.string.watcher_channel_id),
channelName = getString(R.string.watcher_channel_name),
title = getString(R.string.auto_tunnel_title),
description = description,
)
ServiceCompat.startForeground(
this,
foregroundId,
notification,
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
)
}
private fun initWakeLock() {
wakeLock =
(getSystemService(POWER_SERVICE) as PowerManager).run {
val tag = this.javaClass.name
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
try {
Timber.i("Initiating wakelock with 10 min timeout")
acquire(Constants.BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT)
} finally {
release()
}
}
}
}
private fun startSettingsJob() = lifecycleScope.launch {
watchForSettingsChanges()
}
private fun startVpnStateJob() = lifecycleScope.launch {
watchForVpnStateChanges()
}
private fun startWifiJob() = lifecycleScope.launch {
watchForWifiConnectivityChanges()
}
private fun startMobileDataJob() = lifecycleScope.launch {
watchForMobileDataConnectivityChanges()
}
private fun startEthernetJob() = lifecycleScope.launch {
watchForEthernetConnectivityChanges()
}
private fun startPingJob() = lifecycleScope.launch {
watchForPingFailure()
}
private fun startNetworkEventJob() = lifecycleScope.launch {
handleNetworkEventChanges()
}
private suspend fun watchForMobileDataConnectivityChanges() {
withContext(ioDispatcher) {
Timber.i("Starting mobile data watcher")
mobileDataService.networkStatus.collect { status ->
when (status) {
is NetworkStatus.Available -> {
Timber.i("Gained Mobile data connection")
emitMobileDataConnected(true)
}
is NetworkStatus.CapabilitiesChanged -> {
emitMobileDataConnected(true)
Timber.i("Mobile data capabilities changed")
}
is NetworkStatus.Unavailable -> {
emitMobileDataConnected(false)
Timber.i("Lost mobile data connection")
}
}
}
}
}
private suspend fun watchForPingFailure() {
withContext(ioDispatcher) {
Timber.i("Starting ping watcher")
runCatching {
do {
val vpnState = tunnelService.get().vpnState.value
if (vpnState.status == TunnelState.UP) {
if (vpnState.tunnelConfig != null) {
val config = TunnelConfig.configFromWgQuick(vpnState.tunnelConfig.wgQuick)
val results = if (vpnState.tunnelConfig.pingIp != null) {
Timber.d("Pinging custom ip : ${vpnState.tunnelConfig.pingIp}")
listOf(InetAddress.getByName(vpnState.tunnelConfig.pingIp).isReachable(Constants.PING_TIMEOUT.toInt()))
} else {
Timber.d("Pinging all peers")
config.peers.map { peer ->
peer.isReachable()
}
}
Timber.i("Ping results reachable: $results")
if (results.contains(false)) {
Timber.i("Restarting VPN for ping failure")
val cooldown = vpnState.tunnelConfig.pingCooldown
tunnelService.get().bounceTunnel(vpnState.tunnelConfig)
delay(cooldown ?: Constants.PING_COOLDOWN)
continue
}
}
}
delay(vpnState.tunnelConfig?.pingInterval ?: Constants.PING_INTERVAL)
} while (true)
}.onFailure {
Timber.e(it)
}
}
}
private suspend fun watchForSettingsChanges() {
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 }
},
) { 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)
}
}
}
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 ->
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()
}
}
private fun cancelAndResetPingJob() {
pingJob?.cancelWithMessage("Ping job canceled")
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(
isEthernetConnected = connected,
)
}
}
private fun emitWifiConnected(connected: Boolean) {
autoTunnelStateFlow.update {
it.copy(
isWifiConnected = connected,
)
}
}
private fun emitWifiSSID(ssid: String) {
autoTunnelStateFlow.update {
it.copy(
currentNetworkSSID = ssid,
)
}
}
private fun emitMobileDataConnected(connected: Boolean) {
autoTunnelStateFlow.update {
it.copy(
isMobileDataConnected = connected,
)
}
}
private suspend fun watchForEthernetConnectivityChanges() {
withContext(ioDispatcher) {
Timber.i("Starting ethernet data watcher")
ethernetService.networkStatus.collect { status ->
when (status) {
is NetworkStatus.Available -> {
Timber.i("Gained Ethernet connection")
emitEthernetConnected(true)
}
is NetworkStatus.CapabilitiesChanged -> {
Timber.i("Ethernet capabilities changed")
emitEthernetConnected(true)
}
is NetworkStatus.Unavailable -> {
emitEthernetConnected(false)
Timber.i("Lost Ethernet connection")
}
}
}
}
}
private suspend fun watchForWifiConnectivityChanges() {
withContext(ioDispatcher) {
Timber.i("Starting wifi watcher")
wifiService.networkStatus.collect { status ->
when (status) {
is NetworkStatus.Available -> {
Timber.i("Gained Wi-Fi connection")
emitWifiConnected(true)
}
is NetworkStatus.CapabilitiesChanged -> {
Timber.i("Wifi capabilities changed")
emitWifiConnected(true)
val ssid = getWifiSSID(status.networkCapabilities)
ssid?.let { name ->
if (name.contains(Constants.UNREADABLE_SSID)) {
Timber.w("SSID unreadable: missing permissions")
} else {
Timber.i("Detected valid SSID")
}
appDataRepository.appState.setCurrentSsid(name)
emitWifiSSID(name)
} ?: Timber.w("Failed to read ssid")
}
is NetworkStatus.Unavailable -> {
emitWifiConnected(false)
Timber.i("Lost Wi-Fi connection")
}
}
}
}
}
private suspend fun getWifiSSID(networkCapabilities: NetworkCapabilities): String? {
return withContext(ioDispatcher) {
with(autoTunnelStateFlow.value.settings) {
if (isWifiNameByShellEnabled) return@withContext rootShell.get().getCurrentWifiName()
wifiService.getNetworkName(networkCapabilities)
}
}
}
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")
}
}
}
}
}
}
@@ -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,19 +3,25 @@ package com.zaneschepke.wireguardautotunnel.service.foreground
import android.app.Service import android.app.Service
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import com.zaneschepke.wireguardautotunnel.util.SingletonHolder import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.AutoTunnelService
import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelControlTile
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
import jakarta.inject.Inject import jakarta.inject.Inject
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
class ServiceManager class ServiceManager
@Inject constructor(private val context: Context) { @Inject constructor(private val context: Context, private val ioDispatcher: CoroutineDispatcher, private val appDataRepository: AppDataRepository) {
private val _autoTunnelActive = MutableStateFlow(false) private val _autoTunnelActive = MutableStateFlow(false)
@@ -23,8 +29,8 @@ class ServiceManager
var autoTunnelService = CompletableDeferred<AutoTunnelService>() var autoTunnelService = CompletableDeferred<AutoTunnelService>()
var backgroundService = CompletableDeferred<TunnelBackgroundService>() var backgroundService = CompletableDeferred<TunnelBackgroundService>()
var autoTunnelTile = CompletableDeferred<AutoTunnelControlTile>()
companion object : SingletonHolder<ServiceManager, Context>(::ServiceManager) var tunnelControlTile = CompletableDeferred<TunnelControlTile>()
private fun <T : Service> startService(cls: Class<T>, background: Boolean) { private fun <T : Service> startService(cls: Class<T>, background: Boolean) {
runCatching { runCatching {
@@ -38,12 +44,15 @@ class ServiceManager
} }
suspend fun startAutoTunnel(background: Boolean) { suspend fun startAutoTunnel(background: Boolean) {
val settings = appDataRepository.settings.getSettings()
appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = true))
if (autoTunnelService.isCompleted) return _autoTunnelActive.update { true } if (autoTunnelService.isCompleted) return _autoTunnelActive.update { true }
kotlin.runCatching { kotlin.runCatching {
startService(AutoTunnelService::class.java, background) startService(AutoTunnelService::class.java, background)
autoTunnelService.await() autoTunnelService.await()
autoTunnelService.getCompleted().start() autoTunnelService.getCompleted().start()
_autoTunnelActive.update { true } _autoTunnelActive.update { true }
updateAutoTunnelTile()
}.onFailure { }.onFailure {
Timber.e(it) Timber.e(it)
} }
@@ -69,17 +78,41 @@ class ServiceManager
} }
} }
fun stopAutoTunnel() { suspend fun toggleAutoTunnel(background: Boolean) {
if (!autoTunnelService.isCompleted) return withContext(ioDispatcher) {
runCatching { if (_autoTunnelActive.value) return@withContext stopAutoTunnel()
autoTunnelService.getCompleted().stop() startAutoTunnel(background)
_autoTunnelActive.update { false }
}.onFailure {
Timber.e(it)
} }
} }
fun requestTunnelTileUpdate() { fun updateAutoTunnelTile() {
context.requestTunnelTileServiceStateUpdate() if (autoTunnelTile.isCompleted) {
autoTunnelTile.getCompleted().updateTileState()
} else {
context.requestAutoTunnelTileServiceUpdate()
}
}
fun updateTunnelTile() {
if (tunnelControlTile.isCompleted) {
tunnelControlTile.getCompleted().updateTileState()
} else {
context.requestTunnelTileServiceStateUpdate()
}
}
suspend fun stopAutoTunnel() {
withContext(ioDispatcher) {
val settings = appDataRepository.settings.getSettings()
appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = false))
if (!autoTunnelService.isCompleted) return@withContext
runCatching {
autoTunnelService.getCompleted().stop()
_autoTunnelActive.update { false }
updateAutoTunnelTile()
}.onFailure {
Timber.e(it)
}
}
} }
} }
@@ -7,6 +7,7 @@ import androidx.core.app.ServiceCompat
import androidx.lifecycle.LifecycleService import androidx.lifecycle.LifecycleService
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.notification.WireGuardNotification
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
@@ -21,8 +22,6 @@ class TunnelBackgroundService : LifecycleService() {
@Inject @Inject
lateinit var serviceManager: ServiceManager lateinit var serviceManager: ServiceManager
private val foregroundId = 123
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
start() start()
@@ -42,7 +41,7 @@ class TunnelBackgroundService : LifecycleService() {
fun start() { fun start() {
ServiceCompat.startForeground( ServiceCompat.startForeground(
this, this,
foregroundId, NotificationService.KERNEL_SERVICE_NOTIFICATION_ID,
createNotification(), createNotification(),
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID, Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
) )
@@ -60,8 +59,7 @@ class TunnelBackgroundService : LifecycleService() {
private fun createNotification(): Notification { private fun createNotification(): Notification {
return notificationService.createNotification( return notificationService.createNotification(
getString(R.string.vpn_channel_id), WireGuardNotification.NotificationChannels.VPN,
getString(R.string.vpn_channel_name),
getString(R.string.tunnel_running), getString(R.string.tunnel_running),
description = "", description = "",
) )
@@ -0,0 +1,309 @@
package com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel
import android.content.Intent
import android.net.NetworkCapabilities
import android.os.IBinder
import android.os.PowerManager
import androidx.core.app.ServiceCompat
import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope
import com.wireguard.android.util.RootShell
import com.zaneschepke.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.foreground.autotunnel.model.AutoTunnelEvent
import com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model.AutoTunnelState
import com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model.NetworkState
import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
import com.zaneschepke.wireguardautotunnel.service.network.NetworkStatus
import com.zaneschepke.wireguardautotunnel.service.network.WifiService
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationAction
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.notification.WireGuardNotification
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
import com.zaneschepke.wireguardautotunnel.util.extensions.cancelWithMessage
import com.zaneschepke.wireguardautotunnel.util.extensions.getCurrentWifiName
import com.zaneschepke.wireguardautotunnel.util.extensions.isReachable
import com.zaneschepke.wireguardautotunnel.util.extensions.onNotRunning
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.update
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
@AndroidEntryPoint
class AutoTunnelService : LifecycleService() {
@Inject
@AppShell
lateinit var rootShell: Provider<RootShell>
@Inject
lateinit var wifiService: NetworkService<WifiService>
@Inject
lateinit var mobileDataService: NetworkService<MobileDataService>
@Inject
lateinit var ethernetService: NetworkService<EthernetService>
@Inject
lateinit var appDataRepository: Provider<AppDataRepository>
@Inject
lateinit var notificationService: NotificationService
@Inject
lateinit var tunnelService: Provider<TunnelService>
@Inject
@IoDispatcher
lateinit var ioDispatcher: CoroutineDispatcher
@Inject
lateinit var serviceManager: ServiceManager
@Inject
@MainImmediateDispatcher
lateinit var mainImmediateDispatcher: CoroutineDispatcher
private val autoTunnelStateFlow = MutableStateFlow(AutoTunnelState())
private var wakeLock: PowerManager.WakeLock? = null
private val pingTunnelRestartActive = AtomicBoolean(false)
private var pingJob: Job? = null
override fun onCreate() {
super.onCreate()
serviceManager.autoTunnelService.complete(this)
lifecycleScope.launch(mainImmediateDispatcher) {
kotlin.runCatching {
launchWatcherNotification()
}.onFailure {
Timber.e(it)
}
}
}
override fun onBind(intent: Intent): IBinder? {
super.onBind(intent)
return null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Timber.d("onStartCommand executed with startId: $startId")
serviceManager.autoTunnelService.complete(this)
return super.onStartCommand(intent, flags, startId)
}
fun start() {
kotlin.runCatching {
lifecycleScope.launch(mainImmediateDispatcher) {
launchWatcherNotification()
initWakeLock()
}
startAutoTunnelJob()
startAutoTunnelStateJob()
startPingStateJob()
}.onFailure {
Timber.e(it)
}
}
fun stop() {
wakeLock?.let { if (it.isHeld) it.release() }
stopSelf()
}
override fun onDestroy() {
cancelAndResetPingJob()
serviceManager.autoTunnelService = CompletableDeferred()
super.onDestroy()
}
private fun launchWatcherNotification(description: String = getString(R.string.monitoring_state_changes)) {
val notification =
notificationService.createNotification(
WireGuardNotification.NotificationChannels.AUTO_TUNNEL,
title = getString(R.string.auto_tunnel_title),
description = description,
actions = listOf(
notificationService.createNotificationAction(NotificationAction.AUTO_TUNNEL_OFF),
),
)
ServiceCompat.startForeground(
this,
NotificationService.AUTO_TUNNEL_NOTIFICATION_ID,
notification,
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
)
}
private fun initWakeLock() {
wakeLock = (getSystemService(POWER_SERVICE) as PowerManager).run {
val tag = this.javaClass.name
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
try {
Timber.i("Initiating wakelock with 10 min timeout")
acquire(Constants.BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT)
} finally {
release()
}
}
}
}
private fun startPingJob() = lifecycleScope.launch {
watchForPingFailure()
}
private fun startPingStateJob() = lifecycleScope.launch {
autoTunnelStateFlow.collect {
if (it.isPingEnabled()) {
pingJob.onNotRunning { pingJob = startPingJob() }
} else {
if (!pingTunnelRestartActive.get()) cancelAndResetPingJob()
}
}
}
private suspend fun watchForPingFailure() {
withContext(ioDispatcher) {
Timber.i("Starting ping watcher")
runCatching {
do {
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) {
Timber.d("Pinging custom ip : ${vpnState.tunnelConfig.pingIp}")
listOf(InetAddress.getByName(vpnState.tunnelConfig.pingIp).isReachable(Constants.PING_TIMEOUT.toInt()))
} else {
Timber.d("Pinging all peers")
config.peers.map { peer ->
peer.isReachable()
}
}
Timber.i("Ping results reachable: $results")
if (results.contains(false)) {
Timber.i("Restarting VPN for ping failure")
val cooldown = vpnState.tunnelConfig.pingCooldown
pingTunnelRestartActive.set(true)
tunnelService.get().bounceTunnel()
pingTunnelRestartActive.set(false)
delay(cooldown ?: Constants.PING_COOLDOWN)
continue
}
}
}
delay(vpnState.tunnelConfig?.pingInterval ?: Constants.PING_INTERVAL)
} while (true)
}.onFailure {
Timber.e(it)
}
}
}
private fun startAutoTunnelStateJob() = lifecycleScope.launch(ioDispatcher) {
combine(
combineSettings(),
combineNetworkEventsJob(),
) { double, networkState ->
AutoTunnelState(tunnelService.get().vpnState.value, networkState, double.first, double.second)
}.collect { state ->
autoTunnelStateFlow.update {
it.copy(state.vpnState, state.networkState, state.settings, state.tunnels)
}
}
}
private fun cancelAndResetPingJob() {
pingJob?.cancelWithMessage("Ping job canceled")
pingJob = null
}
private fun combineNetworkEventsJob(): Flow<NetworkState> {
return combine(
wifiService.networkStatus,
mobileDataService.networkStatus,
ethernetService.networkStatus,
) { wifi, mobileData, ethernet ->
NetworkState(
wifi.isConnected,
mobileData.isConnected,
ethernet.isConnected,
when (wifi) {
is NetworkStatus.CapabilitiesChanged -> getWifiSSID(wifi.networkCapabilities)
is NetworkStatus.Available -> autoTunnelStateFlow.value.networkState.wifiName
is NetworkStatus.Unavailable -> null
},
)
}.distinctUntilChanged().filterNot { it.isWifiConnected && it.wifiName == null }
}
private fun combineSettings(): Flow<Pair<Settings, TunnelConfigs>> {
return combine(
appDataRepository.get().settings.getSettingsFlow(),
appDataRepository.get().tunnels.getTunnelConfigsFlow().distinctUntilChanged { old, new ->
old.map { it.isActive } != new.map { it.isActive }
},
) { settings, tunnels ->
Pair(settings, tunnels)
}.distinctUntilChanged()
}
private suspend fun getWifiSSID(networkCapabilities: NetworkCapabilities): String? {
return withContext(ioDispatcher) {
with(autoTunnelStateFlow.value.settings) {
if (isWifiNameByShellEnabled) return@withContext rootShell.get().getCurrentWifiName()
wifiService.getNetworkName(networkCapabilities)
}.also {
if (it?.contains(Constants.UNREADABLE_SSID) == true) {
Timber.w("SSID unreadable: missing permissions")
} else {
Timber.i("Detected valid SSID")
}
}
}
}
private fun startAutoTunnelJob() = lifecycleScope.launch(ioDispatcher) {
Timber.i("Starting auto-tunnel network event watcher")
autoTunnelStateFlow.collect { watcherState ->
Timber.d("New auto tunnel state emitted")
when (val event = watcherState.asAutoTunnelEvent()) {
is AutoTunnelEvent.Start -> tunnelService.get().startTunnel(
event.tunnelConfig
?: appDataRepository.get().getPrimaryOrFirstTunnel(),
)
is AutoTunnelEvent.Stop -> tunnelService.get().stopTunnel()
AutoTunnelEvent.DoNothing -> Timber.i("Auto-tunneling: no condition met")
}
}
}
}
@@ -0,0 +1,9 @@
package com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model
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()
}
@@ -0,0 +1,147 @@
package com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model
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
import timber.log.Timber
data class AutoTunnelState(
val vpnState: VpnState = VpnState(),
val networkState: NetworkState = NetworkState(),
val settings: Settings = Settings(),
val tunnels: TunnelConfigs = emptyList(),
) {
private fun isMobileDataActive(): Boolean {
return !networkState.isEthernetConnected && !networkState.isWifiConnected && networkState.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 !networkState.isEthernetConnected && networkState.isWifiConnected
}
private fun startOnEthernet(): Boolean {
return networkState.isEthernetConnected && settings.isTunnelOnEthernetEnabled && vpnState.status.isDown()
}
private fun stopOnEthernet(): Boolean {
return networkState.isEthernetConnected && !settings.isTunnelOnEthernetEnabled && vpnState.status.isUp()
}
fun isNoConnectivity(): Boolean {
return !networkState.isEthernetConnected && !networkState.isWifiConnected && !networkState.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 networkState.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 {
Timber.d("Is tunnel on wifi enabled ${settings.isTunnelOnWifiEnabled}")
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 networkState.wifiName?.let {
hasTrustedWifiName(it)
} == true
}
private fun hasTrustedWifiName(wifiName: String, wifiNames: List<String> = settings.trustedNetworkSSIDs): Boolean {
return if (settings.isWildcardsEnabled) {
wifiNames.isMatchingToWildcardList(wifiName)
} else {
wifiNames.contains(wifiName)
}
}
private fun getTunnelWithMatchingTunnelNetwork(): TunnelConfig? {
return networkState.wifiName?.let { wifiName ->
tunnels.firstOrNull {
hasTrustedWifiName(wifiName, it.tunnelNetworks)
}
}
}
fun isPingEnabled(): Boolean {
return settings.isPingEnabled ||
(vpnState.status.isUp() && vpnState.tunnelConfig != null && tunnels.first { it.id == vpnState.tunnelConfig.id }.isPingEnabled)
}
}
@@ -0,0 +1,8 @@
package com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model
data class NetworkState(
val isWifiConnected: Boolean = false,
val isMobileDataConnected: Boolean = false,
val isEthernetConnected: Boolean = false,
val wifiName: String? = null,
)
@@ -10,7 +10,10 @@ import android.os.Build
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import timber.log.Timber
abstract class BaseNetworkService<T : BaseNetworkService<T>>( abstract class BaseNetworkService<T : BaseNetworkService<T>>(
val context: Context, val context: Context,
@@ -22,8 +25,17 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(
val wifiManager = val wifiManager =
context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
fun checkHasCapability(networkCapability: Int): Boolean {
val network = connectivityManager.activeNetwork
val networkCapabilities = connectivityManager.getNetworkCapabilities(network)
return networkCapabilities?.hasTransport(networkCapability) == true
}
override val networkStatus = override val networkStatus =
callbackFlow { callbackFlow {
if (!checkHasCapability(networkCapability)) {
trySend(NetworkStatus.Unavailable())
}
val networkStatusCallback = val networkStatusCallback =
when (Build.VERSION.SDK_INT) { when (Build.VERSION.SDK_INT) {
in Build.VERSION_CODES.S..Int.MAX_VALUE -> { in Build.VERSION_CODES.S..Int.MAX_VALUE -> {
@@ -36,7 +48,7 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(
} }
override fun onLost(network: Network) { override fun onLost(network: Network) {
trySend(NetworkStatus.Unavailable(network)) trySend(NetworkStatus.Unavailable())
} }
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) { override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
@@ -57,7 +69,7 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(
} }
override fun onLost(network: Network) { override fun onLost(network: Network) {
trySend(NetworkStatus.Unavailable(network)) trySend(NetworkStatus.Unavailable())
} }
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) { override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
@@ -80,17 +92,20 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(
connectivityManager.registerNetworkCallback(request, networkStatusCallback) connectivityManager.registerNetworkCallback(request, networkStatusCallback)
awaitClose { connectivityManager.unregisterNetworkCallback(networkStatusCallback) } awaitClose { connectivityManager.unregisterNetworkCallback(networkStatusCallback) }
} }.catch {
Timber.e(it)
// conflate for backpressure
}.conflate()
} }
inline fun <Result> Flow<NetworkStatus>.map( inline fun <Result> Flow<NetworkStatus>.map(
crossinline onUnavailable: suspend (network: Network) -> Result, crossinline onUnavailable: suspend () -> Result,
crossinline onAvailable: suspend (network: Network) -> Result, crossinline onAvailable: suspend (network: Network) -> Result,
crossinline onCapabilitiesChanged: crossinline onCapabilitiesChanged:
suspend (network: Network, networkCapabilities: NetworkCapabilities) -> Result, suspend (network: Network, networkCapabilities: NetworkCapabilities) -> Result,
): Flow<Result> = map { status -> ): Flow<Result> = map { status ->
when (status) { when (status) {
is NetworkStatus.Unavailable -> onUnavailable(status.network) is NetworkStatus.Unavailable -> onUnavailable()
is NetworkStatus.Available -> onAvailable(status.network) is NetworkStatus.Available -> onAvailable(status.network)
is NetworkStatus.CapabilitiesChanged -> is NetworkStatus.CapabilitiesChanged ->
onCapabilitiesChanged( onCapabilitiesChanged(
@@ -4,10 +4,11 @@ import android.net.Network
import android.net.NetworkCapabilities import android.net.NetworkCapabilities
sealed class NetworkStatus { sealed class NetworkStatus {
class Available(val network: Network) : NetworkStatus() abstract val isConnected: Boolean
class Available(val network: Network, override val isConnected: Boolean = true) : NetworkStatus()
class Unavailable(val network: Network) : NetworkStatus() class Unavailable(override val isConnected: Boolean = false) : NetworkStatus()
class CapabilitiesChanged(val network: Network, val networkCapabilities: NetworkCapabilities) : class CapabilitiesChanged(val network: Network, val networkCapabilities: NetworkCapabilities, override val isConnected: Boolean = true) :
NetworkStatus() NetworkStatus()
} }
@@ -0,0 +1,17 @@
package com.zaneschepke.wireguardautotunnel.service.notification
import android.content.Context
import com.zaneschepke.wireguardautotunnel.R
enum class NotificationAction {
TUNNEL_OFF,
AUTO_TUNNEL_OFF,
;
fun title(context: Context): String {
return when (this) {
TUNNEL_OFF -> context.getString(R.string.stop)
AUTO_TUNNEL_OFF -> context.getString(R.string.stop)
}
}
}
@@ -2,21 +2,32 @@ package com.zaneschepke.wireguardautotunnel.service.notification
import android.app.Notification import android.app.Notification
import android.app.NotificationManager import android.app.NotificationManager
import android.app.PendingIntent import android.content.Context
import androidx.core.app.NotificationCompat
import com.zaneschepke.wireguardautotunnel.service.notification.WireGuardNotification.NotificationChannels
interface NotificationService { interface NotificationService {
val context: Context
fun createNotification( fun createNotification(
channelId: String, channel: NotificationChannels,
channelName: String,
title: String = "", title: String = "",
action: PendingIntent? = null, actions: Collection<NotificationCompat.Action> = emptyList(),
actionText: String? = null, description: String = "",
description: String,
showTimestamp: Boolean = false, showTimestamp: Boolean = false,
importance: Int = NotificationManager.IMPORTANCE_HIGH, importance: Int = NotificationManager.IMPORTANCE_HIGH,
vibration: Boolean = false,
onGoing: Boolean = true, onGoing: Boolean = true,
lights: Boolean = true,
onlyAlertOnce: Boolean = true, onlyAlertOnce: Boolean = true,
): Notification ): Notification
fun createNotificationAction(action: NotificationAction): NotificationCompat.Action
fun remove(notificationId: Int)
fun show(notificationId: Int, notification: Notification)
companion object {
const val KERNEL_SERVICE_NOTIFICATION_ID = 123
const val AUTO_TUNNEL_NOTIFICATION_ID = 122
const val VPN_NOTIFICATION_ID = 100
}
} }
@@ -1,105 +1,134 @@
package com.zaneschepke.wireguardautotunnel.service.notification package com.zaneschepke.wireguardautotunnel.service.notification
import android.Manifest
import android.app.Notification import android.app.Notification
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Color import android.graphics.Color
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.MainActivity import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject import javax.inject.Inject
class WireGuardNotification class WireGuardNotification
@Inject @Inject
constructor( constructor(
@ApplicationContext private val context: Context, @ApplicationContext override val context: Context,
) : ) : NotificationService {
NotificationService {
private val notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
private val watcherBuilder: NotificationCompat.Builder = enum class NotificationChannels {
NotificationCompat.Builder( VPN,
context, AUTO_TUNNEL,
context.getString(R.string.watcher_channel_id), }
)
private val tunnelBuilder: NotificationCompat.Builder = private val notificationManager = NotificationManagerCompat.from(context)
NotificationCompat.Builder(
context,
context.getString(R.string.vpn_channel_id),
)
override fun createNotification( override fun createNotification(
channelId: String, channel: NotificationChannels,
channelName: String,
title: String, title: String,
action: PendingIntent?, actions: Collection<NotificationCompat.Action>,
actionText: String?,
description: String, description: String,
showTimestamp: Boolean, showTimestamp: Boolean,
importance: Int, importance: Int,
vibration: Boolean,
onGoing: Boolean, onGoing: Boolean,
lights: Boolean,
onlyAlertOnce: Boolean, onlyAlertOnce: Boolean,
): Notification { ): Notification {
val channel = notificationManager.createNotificationChannel(channel.asChannel())
NotificationChannel( return channel.asBuilder().apply {
channelId, actions.forEach {
channelName, addAction(it)
importance, }
) setContentTitle(title)
.let { setContentText(description)
it.description = title setOnlyAlertOnce(onlyAlertOnce)
it.enableLights(lights) setOngoing(onGoing)
it.lightColor = Color.RED setPriority(NotificationCompat.PRIORITY_HIGH)
it.enableVibration(vibration) setShowWhen(showTimestamp)
it.vibrationPattern = longArrayOf(100, 200, 300) setSmallIcon(R.drawable.ic_launcher)
it }.build()
} }
notificationManager.createNotificationChannel(channel)
val pendingIntent: PendingIntent = override fun createNotificationAction(notificationAction: NotificationAction): NotificationCompat.Action {
Intent(context, MainActivity::class.java).let { notificationIntent -> val pendingIntent = PendingIntent.getBroadcast(
PendingIntent.getActivity( context,
0,
Intent(context, NotificationActionReceiver::class.java).apply {
action = notificationAction.name
},
PendingIntent.FLAG_IMMUTABLE,
)
return NotificationCompat.Action.Builder(
R.drawable.ic_launcher,
notificationAction.title(context).uppercase(),
pendingIntent,
).build()
}
override fun remove(notificationId: Int) {
notificationManager.cancel(notificationId)
}
override fun show(notificationId: Int, notification: Notification) {
with(notificationManager) {
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
return
}
notify(notificationId, notification)
}
}
fun NotificationChannels.asBuilder(): NotificationCompat.Builder {
return when (this) {
NotificationChannels.VPN -> {
NotificationCompat.Builder(
context, context,
0, context.getString(R.string.auto_tunnel_channel_id),
notificationIntent,
PendingIntent.FLAG_IMMUTABLE,
) )
} }
NotificationChannels.AUTO_TUNNEL -> {
NotificationCompat.Builder(
context,
context.getString(R.string.vpn_channel_id),
)
}
}
}
val builder = fun NotificationChannels.asChannel(): NotificationChannel {
when (channelId) { return when (this) {
context.getString(R.string.watcher_channel_id) -> watcherBuilder NotificationChannels.VPN -> {
context.getString(R.string.vpn_channel_id) -> tunnelBuilder NotificationChannel(
else -> { context.getString(R.string.vpn_channel_id),
NotificationCompat.Builder( context.getString(R.string.vpn_channel_name),
context, NotificationManager.IMPORTANCE_HIGH,
channelId, ).apply {
) description = context.getString(R.string.vpn_channel_description)
enableLights(true)
lightColor = Color.WHITE
enableVibration(false)
vibrationPattern = longArrayOf(100, 200, 300)
} }
} }
NotificationChannels.AUTO_TUNNEL -> {
return builder.let { NotificationChannel(
if (action != null && actionText != null) { context.getString(R.string.auto_tunnel_channel_id),
it.addAction( context.getString(R.string.auto_tunnel_channel_name),
NotificationCompat.Action.Builder(0, actionText, action).build(), NotificationManager.IMPORTANCE_HIGH,
) ).apply {
it.setAutoCancel(true) description = context.getString(R.string.auto_tunnel_channel_description)
enableLights(true)
lightColor = Color.WHITE
enableVibration(false)
vibrationPattern = longArrayOf(100, 200, 300)
}
} }
it.setContentTitle(title)
.setContentText(description)
.setOnlyAlertOnce(onlyAlertOnce)
.setContentIntent(pendingIntent)
.setOngoing(onGoing)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setShowWhen(showTimestamp)
.setSmallIcon(R.drawable.ic_launcher)
.build()
} }
} }
} }
@@ -4,9 +4,8 @@ import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope 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.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.AutoTunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -47,7 +46,7 @@ class ShortcutsActivity : ComponentActivity() {
tunnelConfig?.let { tunnelConfig?.let {
when (intent.action) { when (intent.action) {
Action.START.name -> tunnelService.get().startTunnel(it, true) Action.START.name -> tunnelService.get().startTunnel(it, true)
Action.STOP.name -> tunnelService.get().stopTunnel(it) Action.STOP.name -> tunnelService.get().stopTunnel()
else -> Unit else -> Unit
} }
} }
@@ -64,6 +63,11 @@ class ShortcutsActivity : ComponentActivity() {
finish() finish()
} }
enum class Action {
START,
STOP,
}
companion object { companion object {
const val LEGACY_TUNNEL_SERVICE_NAME = "WireGuardTunnelService" const val LEGACY_TUNNEL_SERVICE_NAME = "WireGuardTunnelService"
const val LEGACY_AUTO_TUNNEL_SERVICE_NAME = "WireGuardConnectivityWatcherService" const val LEGACY_AUTO_TUNNEL_SERVICE_NAME = "WireGuardConnectivityWatcherService"
@@ -1,24 +1,18 @@
package com.zaneschepke.wireguardautotunnel.service.tile package com.zaneschepke.wireguardautotunnel.service.tile
import android.content.Intent
import android.os.IBinder
import android.service.quicksettings.Tile import android.service.quicksettings.Tile
import android.service.quicksettings.TileService import android.service.quicksettings.TileService
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.lifecycleScope
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class AutoTunnelControlTile : TileService(), LifecycleOwner { class AutoTunnelControlTile : TileService() {
@Inject @Inject
lateinit var appDataRepository: AppDataRepository lateinit var appDataRepository: AppDataRepository
@@ -29,32 +23,26 @@ class AutoTunnelControlTile : TileService(), LifecycleOwner {
@ApplicationScope @ApplicationScope
lateinit var applicationScope: CoroutineScope lateinit var applicationScope: CoroutineScope
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) serviceManager.autoTunnelTile.complete(this)
}
override fun onStopListening() {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
} }
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) serviceManager.autoTunnelTile = CompletableDeferred()
} }
override fun onStartListening() { override fun onStartListening() {
super.onStartListening() super.onStartListening()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START) serviceManager.autoTunnelTile.complete(this)
lifecycleScope.launch { applicationScope.launch {
if (appDataRepository.tunnels.getAll().isEmpty()) return@launch setUnavailable() if (appDataRepository.tunnels.getAll().isEmpty()) return@launch setUnavailable()
updateTileState() updateTileState()
} }
} }
private fun updateTileState() { fun updateTileState() {
serviceManager.autoTunnelActive.value.let { serviceManager.autoTunnelActive.value.let {
if (it) setActive() else setInactive() if (it) setActive() else setInactive()
} }
@@ -63,7 +51,7 @@ class AutoTunnelControlTile : TileService(), LifecycleOwner {
override fun onClick() { override fun onClick() {
super.onClick() super.onClick()
unlockAndRun { unlockAndRun {
lifecycleScope.launch { applicationScope.launch {
if (serviceManager.autoTunnelActive.value) { if (serviceManager.autoTunnelActive.value) {
serviceManager.stopAutoTunnel() serviceManager.stopAutoTunnel()
setInactive() setInactive()
@@ -95,18 +83,4 @@ class AutoTunnelControlTile : TileService(), LifecycleOwner {
qsTile.updateTile() qsTile.updateTile()
} }
} }
/* This works around an annoying unsolved frameworks bug some people are hitting. */
override fun onBind(intent: Intent): IBinder? {
var ret: IBinder? = null
try {
ret = super.onBind(intent)
} catch (_: Throwable) {
Timber.e("Failed to bind to TunnelControlTile")
}
return ret
}
override val lifecycle: Lifecycle
get() = lifecycleRegistry
} }
@@ -1,27 +1,22 @@
package com.zaneschepke.wireguardautotunnel.service.tile package com.zaneschepke.wireguardautotunnel.service.tile
import android.content.Intent
import android.os.Build import android.os.Build
import android.os.IBinder
import android.service.quicksettings.Tile import android.service.quicksettings.Tile
import android.service.quicksettings.TileService import android.service.quicksettings.TileService
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.lifecycleScope
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider import javax.inject.Provider
@AndroidEntryPoint @AndroidEntryPoint
class TunnelControlTile : TileService(), LifecycleOwner { class TunnelControlTile : TileService() {
@Inject @Inject
lateinit var appDataRepository: AppDataRepository lateinit var appDataRepository: AppDataRepository
@@ -32,33 +27,29 @@ class TunnelControlTile : TileService(), LifecycleOwner {
@ApplicationScope @ApplicationScope
lateinit var applicationScope: CoroutineScope lateinit var applicationScope: CoroutineScope
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this) @Inject
lateinit var serviceManager: ServiceManager
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) serviceManager.tunnelControlTile.complete(this)
}
override fun onStopListening() {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
} }
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) serviceManager.tunnelControlTile = CompletableDeferred()
} }
override fun onStartListening() { override fun onStartListening() {
super.onStartListening() super.onStartListening()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START) serviceManager.tunnelControlTile.complete(this)
Timber.d("Updating tile!") applicationScope.launch {
lifecycleScope.launch {
if (appDataRepository.tunnels.getAll().isEmpty()) return@launch setUnavailable() if (appDataRepository.tunnels.getAll().isEmpty()) return@launch setUnavailable()
updateTileState() updateTileState()
} }
} }
private suspend fun updateTileState() { fun updateTileState() = applicationScope.launch {
val lastActive = appDataRepository.getStartTunnelConfig() val lastActive = appDataRepository.getStartTunnelConfig()
lastActive?.let { lastActive?.let {
updateTile(it) updateTile(it)
@@ -68,11 +59,11 @@ class TunnelControlTile : TileService(), LifecycleOwner {
override fun onClick() { override fun onClick() {
super.onClick() super.onClick()
unlockAndRun { unlockAndRun {
lifecycleScope.launch { applicationScope.launch {
val lastActive = appDataRepository.getStartTunnelConfig() val lastActive = appDataRepository.getStartTunnelConfig()
lastActive?.let { tunnel -> lastActive?.let { tunnel ->
if (tunnel.isActive) { if (tunnel.isActive) {
tunnelService.get().stopTunnel(tunnel) tunnelService.get().stopTunnel()
} else { } else {
tunnelService.get().startTunnel(tunnel, true) tunnelService.get().startTunnel(tunnel, true)
} }
@@ -125,18 +116,4 @@ class TunnelControlTile : TileService(), LifecycleOwner {
} }
} }
} }
/* This works around an annoying unsolved frameworks bug some people are hitting. */
override fun onBind(intent: Intent): IBinder? {
var ret: IBinder? = null
try {
ret = super.onBind(intent)
} catch (_: Throwable) {
Timber.e("Failed to bind to TunnelControlTile")
}
return ret
}
override val lifecycle: Lifecycle
get() = lifecycleRegistry
} }
@@ -0,0 +1,7 @@
package com.zaneschepke.wireguardautotunnel.service.tunnel
enum class BackendState {
KILL_SWITCH_ACTIVE,
SERVICE_ACTIVE,
INACTIVE,
}
@@ -5,11 +5,16 @@ import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
interface TunnelService : Tunnel, org.amnezia.awg.backend.Tunnel { 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()
suspend fun getBackendState(): BackendState
suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>)
val vpnState: StateFlow<VpnState> val vpnState: StateFlow<VpnState>
@@ -17,7 +22,7 @@ interface TunnelService : Tunnel, org.amnezia.awg.backend.Tunnel {
suspend fun getState(): TunnelState suspend fun getState(): TunnelState
fun cancelStatsJob() fun cancelActiveTunnelJobs()
fun startStatsJob() fun startActiveTunnelJobs()
} }
@@ -24,6 +24,14 @@ enum class TunnelState {
} }
} }
fun isDown(): Boolean {
return this == DOWN
}
fun isUp(): Boolean {
return this == UP
}
companion object { companion object {
fun from(state: Tunnel.State): TunnelState { fun from(state: Tunnel.State): TunnelState {
return when (state) { return when (state) {
@@ -9,23 +9,30 @@ import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.module.Kernel import com.zaneschepke.wireguardautotunnel.module.Kernel
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.notification.WireGuardNotification
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.AmneziaStatistics import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.AmneziaStatistics
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.WireGuardStatistics import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.WireGuardStatistics
import com.zaneschepke.wireguardautotunnel.util.extensions.asAmBackendState
import com.zaneschepke.wireguardautotunnel.util.extensions.asBackendState
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.amnezia.awg.backend.Tunnel import org.amnezia.awg.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationAction
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService.Companion.VPN_NOTIFICATION_ID
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider import javax.inject.Provider
@@ -33,31 +40,38 @@ class WireGuardTunnel
@Inject @Inject
constructor( constructor(
private val amneziaBackend: Provider<org.amnezia.awg.backend.Backend>, private val amneziaBackend: Provider<org.amnezia.awg.backend.Backend>,
tunnelConfigRepository: TunnelConfigRepository, private val tunnelConfigRepository: TunnelConfigRepository,
@Kernel private val kernelBackend: Provider<Backend>, @Kernel private val kernelBackend: Provider<Backend>,
private val appDataRepository: AppDataRepository, private val appDataRepository: AppDataRepository,
@ApplicationScope private val applicationScope: CoroutineScope, @ApplicationScope private val applicationScope: CoroutineScope,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher, @IoDispatcher private val ioDispatcher: CoroutineDispatcher,
private val serviceManager: ServiceManager, private val serviceManager: ServiceManager,
private val notificationService: NotificationService,
) : TunnelService { ) : TunnelService {
private val _vpnState = MutableStateFlow(VpnState()) private val _vpnState = MutableStateFlow(VpnState())
override val vpnState: StateFlow<VpnState> = _vpnState.combine( override val vpnState: StateFlow<VpnState> = _vpnState.asStateFlow()
tunnelConfigRepository.getTunnelConfigsFlow(),
) {
vpnState, tunnels ->
vpnState.copy(
tunnelConfig = tunnels.firstOrNull { it.id == vpnState.tunnelConfig?.id },
)
}.stateIn(applicationScope, SharingStarted.Eagerly, VpnState())
private var statsJob: Job? = null private var statsJob: Job? = null
private var tunnelChangesJob: Job? = null
private val runningHandle = AtomicBoolean(false) @get:Synchronized @set:Synchronized
private var isKernelBackend: Boolean? = null
private val tunnelControlMutex = Mutex()
init {
applicationScope.launch(ioDispatcher) {
appDataRepository.settings.getSettingsFlow().collect {
isKernelBackend = it.isKernelEnabled
}
}
}
private suspend fun backend(): Any { private suspend fun backend(): Any {
val settings = appDataRepository.settings.getSettings() val isKernelEnabled = isKernelBackend
if (settings.isKernelEnabled) return kernelBackend.get() ?: appDataRepository.settings.getSettings().isKernelEnabled
if (isKernelEnabled) return kernelBackend.get()
return amneziaBackend.get() return amneziaBackend.get()
} }
@@ -92,136 +106,169 @@ constructor(
} }
} }
override suspend fun startTunnel(tunnelConfig: TunnelConfig, background: Boolean): Result<TunnelState> { private fun isTunnelAlreadyRunning(tunnelConfig: TunnelConfig): Boolean {
return withContext(ioDispatcher) { val isRunning = tunnelConfig == _vpnState.value.tunnelConfig && _vpnState.value.status.isUp()
if (runningHandle.get() && tunnelConfig == vpnState.value.tunnelConfig) { if (isRunning) Timber.w("Tunnel already running")
Timber.w("Tunnel already running") return isRunning
return@withContext Result.success(vpnState.value.status) }
}
runningHandle.set(true) override suspend fun startTunnel(tunnelConfig: TunnelConfig?, background: Boolean) {
onBeforeStart(tunnelConfig) if (tunnelConfig == null) return
val settings = appDataRepository.settings.getSettings() withContext(ioDispatcher) {
if (background || settings.isKernelEnabled) startBackgroundService() if (isTunnelAlreadyRunning(tunnelConfig)) return@withContext
setState(tunnelConfig, TunnelState.UP).onSuccess { withServiceActive {
updateTunnelState(it) onBeforeStart(background)
}.onFailure { tunnelControlMutex.withLock {
Timber.e(it) setState(tunnelConfig, TunnelState.UP).onSuccess {
onStartFailed() startActiveTunnelJobs()
if (it.isUp()) appDataRepository.tunnels.save(tunnelConfig.copy(isActive = true))
with(notificationService) {
val notification = createNotification(
WireGuardNotification.NotificationChannels.VPN,
title = "${context.getString(R.string.tunnel_running)} - ${tunnelConfig.name}",
actions = listOf(
notificationService.createNotificationAction(NotificationAction.TUNNEL_OFF),
),
)
show(VPN_NOTIFICATION_ID, notification)
}
updateTunnelState(it, tunnelConfig)
}
}.onFailure {
Timber.e(it)
}
} }
} }
} }
override suspend fun stopTunnel(tunnelConfig: TunnelConfig): Result<TunnelState> { override suspend fun stopTunnel() {
return withContext(ioDispatcher) { withContext(ioDispatcher) {
onBeforeStop(tunnelConfig) if (_vpnState.value.status.isDown()) return@withContext
setState(tunnelConfig, TunnelState.DOWN).onSuccess { with(_vpnState.value) {
updateTunnelState(it) if (tunnelConfig == null) return@withContext
}.onFailure { tunnelControlMutex.withLock {
Timber.e(it) setState(tunnelConfig, TunnelState.DOWN).onSuccess {
onStopFailed() updateTunnelState(it, null)
}.also { onStop(tunnelConfig)
stopBackgroundService() notificationService.remove(VPN_NOTIFICATION_ID)
runningHandle.set(false) stopBackgroundService()
}.onFailure {
Timber.e(it)
}
}
} }
} }
} }
// use this when we just want to bounce tunnel and not change tunnelConfig active state private suspend fun toggleTunnel(tunnelConfig: TunnelConfig) {
override suspend fun bounceTunnel(tunnelConfig: TunnelConfig): Result<TunnelState> { withContext(ioDispatcher) {
toggleTunnel(tunnelConfig) tunnelControlMutex.withLock {
delay(VPN_RESTART_DELAY) setState(tunnelConfig, TunnelState.TOGGLE)
return toggleTunnel(tunnelConfig)
}
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() { // utility to keep vpnService alive during rapid changes to prevent bad states
private suspend fun withServiceActive(callback: suspend () -> Unit) {
when (val backend = backend()) {
is org.amnezia.awg.backend.Backend -> {
val backendState = backend.backendState
if (backendState == org.amnezia.awg.backend.Backend.BackendState.INACTIVE) {
backend.setBackendState(org.amnezia.awg.backend.Backend.BackendState.SERVICE_ACTIVE, emptyList())
}
callback()
}
is Backend -> callback()
}
}
override suspend fun bounceTunnel() {
_vpnState.value.tunnelConfig?.let { _vpnState.value.tunnelConfig?.let {
appDataRepository.tunnels.save(it.copy(isActive = true)) withServiceActive {
toggleTunnel(it)
toggleTunnel(it)
}
} }
} }
private suspend fun onStartFailed() { override suspend fun getBackendState(): BackendState {
_vpnState.value.tunnelConfig?.let { return when (val backend = backend()) {
appDataRepository.tunnels.save(it.copy(isActive = false)) is org.amnezia.awg.backend.Backend -> {
backend.backendState.asBackendState()
}
is Backend -> BackendState.SERVICE_ACTIVE
else -> BackendState.INACTIVE
} }
cancelStatsJob()
resetBackendStatistics()
runningHandle.set(false)
} }
private suspend fun shutDownActiveTunnel(config: TunnelConfig) { override suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
kotlin.runCatching {
when (val backend = backend()) {
is org.amnezia.awg.backend.Backend -> {
backend.setBackendState(backendState.asAmBackendState(), allowedIps)
}
is Backend -> {
// TODO not yet implemented
Timber.d("Kernel backend state not yet implemented")
}
else -> Unit
}
}
}
private suspend fun shutDownActiveTunnel() {
with(_vpnState.value) { with(_vpnState.value) {
if (status == TunnelState.UP && tunnelConfig != config) { if (status.isUp()) {
tunnelConfig?.let { stopTunnel(it) } stopTunnel()
} }
} }
} }
private suspend fun startBackgroundService() { private suspend fun startBackgroundService() {
serviceManager.startBackgroundService() serviceManager.startBackgroundService()
serviceManager.requestTunnelTileUpdate() serviceManager.updateTunnelTile()
} }
private fun stopBackgroundService() { private fun stopBackgroundService() {
serviceManager.stopBackgroundService() serviceManager.stopBackgroundService()
serviceManager.requestTunnelTileUpdate() serviceManager.updateTunnelTile()
} }
private suspend fun onBeforeStart(tunnelConfig: TunnelConfig) { private suspend fun onBeforeStart(background: Boolean) {
shutDownActiveTunnel(tunnelConfig) shutDownActiveTunnel()
appDataRepository.tunnels.save(tunnelConfig.copy(isActive = true))
emitVpnStateConfig(tunnelConfig)
resetBackendStatistics() 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)) appDataRepository.tunnels.save(tunnelConfig.copy(isActive = false))
cancelStatsJob() cancelActiveTunnelJobs()
resetBackendStatistics() resetBackendStatistics()
} }
private fun updateTunnelState(state: TunnelState) { private fun updateTunnelState(state: TunnelState, tunnelConfig: TunnelConfig?) {
_vpnState.tryEmit( _vpnState.update {
_vpnState.value.copy( it.copy(status = state, tunnelConfig = tunnelConfig)
status = state, }
),
)
serviceManager.requestTunnelTileUpdate()
} }
private fun emitBackendStatistics(statistics: TunnelStatistics) { private fun updateTunnelConfig(tunnelConfig: TunnelConfig?) {
_vpnState.tryEmit( _vpnState.update {
_vpnState.value.copy( it.copy(tunnelConfig = tunnelConfig)
statistics = statistics, }
),
)
} }
private fun emitVpnStateConfig(tunnelConfig: TunnelConfig) { private fun updateBackendStatistics(statistics: TunnelStatistics) {
_vpnState.tryEmit( _vpnState.update {
_vpnState.value.copy( it.copy(statistics = statistics)
tunnelConfig = tunnelConfig, }
),
)
} }
private fun resetBackendStatistics() { private fun resetBackendStatistics() {
_vpnState.tryEmit( _vpnState.update {
_vpnState.value.copy( it.copy(statistics = null)
statistics = null, }
),
)
} }
override suspend fun getState(): TunnelState { override suspend fun getState(): TunnelState {
@@ -232,12 +279,14 @@ constructor(
} }
} }
override fun cancelStatsJob() { override fun cancelActiveTunnelJobs() {
statsJob?.cancel() statsJob?.cancel()
tunnelChangesJob?.cancel()
} }
override fun startStatsJob() { override fun startActiveTunnelJobs() {
statsJob = startTunnelStatisticsJob() statsJob = startTunnelStatisticsJob()
tunnelChangesJob = startTunnelConfigChangesJob()
} }
override fun getName(): String { override fun getName(): String {
@@ -249,11 +298,11 @@ constructor(
delay(STATS_START_DELAY) delay(STATS_START_DELAY)
while (true) { while (true) {
when (backend) { when (backend) {
is Backend -> emitBackendStatistics( is Backend -> updateBackendStatistics(
WireGuardStatistics(backend.getStatistics(this@WireGuardTunnel)), WireGuardStatistics(backend.getStatistics(this@WireGuardTunnel)),
) )
is org.amnezia.awg.backend.Backend -> { is org.amnezia.awg.backend.Backend -> {
emitBackendStatistics( updateBackendStatistics(
AmneziaStatistics( AmneziaStatistics(
backend.getStatistics(this@WireGuardTunnel), backend.getStatistics(this@WireGuardTunnel),
), ),
@@ -264,17 +313,38 @@ constructor(
} }
} }
private fun startTunnelConfigChangesJob() = applicationScope.launch(ioDispatcher) {
tunnelConfigRepository.getTunnelConfigsFlow().collect {
with(_vpnState.value) {
if (status.isDown() || tunnelConfig == null) return@collect
val vpnConfigFromStorage = it.first { it.id == tunnelConfig.id }
val isRestartNeeded = vpnConfigFromStorage.wgQuick != tunnelConfig.wgQuick ||
vpnConfigFromStorage.amQuick != tunnelConfig.amQuick
updateTunnelConfig(vpnConfigFromStorage)
if (isRestartNeeded) {
Timber.d("Bouncing tunnel on config change")
bounceTunnel()
}
}
}
}
override fun onStateChange(newState: Tunnel.State) { override fun onStateChange(newState: Tunnel.State) {
updateTunnelState(TunnelState.from(newState)) _vpnState.update {
it.copy(status = TunnelState.from(newState))
}
serviceManager.updateTunnelTile()
} }
override fun onStateChange(state: State) { override fun onStateChange(state: State) {
updateTunnelState(TunnelState.from(state)) _vpnState.update {
it.copy(status = TunnelState.from(state))
}
serviceManager.updateTunnelTile()
} }
companion object { companion object {
const val STATS_START_DELAY = 1_000L const val STATS_START_DELAY = 1_000L
const val VPN_STATISTIC_CHECK_INTERVAL = 1_000L const val VPN_STATISTIC_CHECK_INTERVAL = 1_000L
const val VPN_RESTART_DELAY = 1_000L
} }
} }
@@ -1,7 +1,5 @@
package com.zaneschepke.wireguardautotunnel.ui package com.zaneschepke.wireguardautotunnel.ui
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.LocaleListCompat
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.wireguard.android.backend.WgQuickBackend import com.wireguard.android.backend.WgQuickBackend
@@ -9,16 +7,17 @@ import com.wireguard.android.util.RootShell
import com.zaneschepke.logcatter.LogReader import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.AppShell import com.zaneschepke.wireguardautotunnel.module.AppShell
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.BackendState
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil.OPTION_PHONE_LANGUAGE
import com.zaneschepke.wireguardautotunnel.util.StringValue import com.zaneschepke.wireguardautotunnel.util.StringValue
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
@@ -34,6 +33,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import timber.log.Timber
import xyz.teamgravity.pin_lock_compose.PinManager import xyz.teamgravity.pin_lock_compose.PinManager
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider import javax.inject.Provider
@@ -80,7 +80,7 @@ constructor(
init { init {
viewModelScope.launch { viewModelScope.launch {
initPin() initPin()
initAutoTunnel() initServices()
initTunnel() initTunnel()
appReadyCheck() appReadyCheck()
} }
@@ -94,7 +94,7 @@ constructor(
} }
private suspend fun initTunnel() { private suspend fun initTunnel() {
if (tunnelService.get().getState() == TunnelState.UP) tunnelService.get().startStatsJob() if (tunnelService.get().getState() == TunnelState.UP) tunnelService.get().startActiveTunnelJobs()
val activeTunnels = appDataRepository.tunnels.getActive() val activeTunnels = appDataRepository.tunnels.getActive()
if (activeTunnels.isNotEmpty() && if (activeTunnels.isNotEmpty() &&
tunnelService.get().getState() == TunnelState.DOWN tunnelService.get().getState() == TunnelState.DOWN
@@ -108,9 +108,12 @@ constructor(
if (isPinEnabled) PinManager.initialize(WireGuardAutoTunnel.instance) if (isPinEnabled) PinManager.initialize(WireGuardAutoTunnel.instance)
} }
private suspend fun initAutoTunnel() { private suspend fun initServices() {
val settings = appDataRepository.settings.getSettings() withContext(ioDispatcher) {
if (settings.isAutoTunnelEnabled) serviceManager.startAutoTunnel(false) val settings = appDataRepository.settings.getSettings()
handleVpnKillSwitchChange(settings.isVpnKillSwitchEnabled)
if (settings.isAutoTunnelEnabled) serviceManager.startAutoTunnel(false)
}
} }
fun onPinLockDisabled() = viewModelScope.launch(ioDispatcher) { fun onPinLockDisabled() = viewModelScope.launch(ioDispatcher) {
@@ -138,7 +141,6 @@ constructor(
} }
private suspend fun onLoggerStop() { private suspend fun onLoggerStop() {
logReader.stop()
logReader.deleteAndClearLogs() logReader.deleteAndClearLogs()
} }
@@ -153,11 +155,11 @@ constructor(
} }
fun onLocaleChange(localeTag: String) = viewModelScope.launch { fun onLocaleChange(localeTag: String) = viewModelScope.launch {
val locale = LocaleUtil.getLocaleFromPrefCode(localeTag) appDataRepository.appState.setLocale(localeTag)
val storageLocale = if (localeTag == OPTION_PHONE_LANGUAGE) OPTION_PHONE_LANGUAGE else locale LocaleUtil.changeLocale(localeTag)
appDataRepository.appState.setLocale(storageLocale) _configurationChange.update {
val appLocale: LocaleListCompat = LocaleListCompat.forLanguageTags(locale) true
AppCompatDelegate.setApplicationLocales(appLocale) }
} }
fun onToggleRestartAtBoot() = viewModelScope.launch { fun onToggleRestartAtBoot() = viewModelScope.launch {
@@ -170,10 +172,50 @@ constructor(
} }
} }
fun onToggleVpnKillSwitch(enabled: Boolean) = viewModelScope.launch {
with(uiState.value.settings) {
appDataRepository.settings.save(
copy(
isVpnKillSwitchEnabled = enabled,
isLanOnKillSwitchEnabled = if (enabled) isLanOnKillSwitchEnabled else false,
),
)
}
handleVpnKillSwitchChange(enabled)
}
private suspend fun handleVpnKillSwitchChange(enabled: Boolean) {
withContext(ioDispatcher) {
if (enabled) {
Timber.d("Starting kill switch")
val allowedIps = if (appDataRepository.settings.getSettings().isLanOnKillSwitchEnabled) {
TunnelConfig.IPV4_PUBLIC_NETWORKS
} else {
emptySet()
}
tunnelService.get().setBackendState(BackendState.KILL_SWITCH_ACTIVE, allowedIps)
} else {
Timber.d("Sending shutdown of kill switch")
tunnelService.get().setBackendState(BackendState.SERVICE_ACTIVE, emptySet())
}
}
}
fun onToggleLanOnKillSwitch(enabled: Boolean) = viewModelScope.launch(ioDispatcher) {
appDataRepository.settings.save(
uiState.value.settings.copy(
isLanOnKillSwitchEnabled = enabled,
),
)
val allowedIps = if (enabled) TunnelConfig.IPV4_PUBLIC_NETWORKS else emptySet()
Timber.d("Setting allowedIps $allowedIps")
tunnelService.get().setBackendState(BackendState.KILL_SWITCH_ACTIVE, allowedIps)
}
fun onToggleShortcutsEnabled() = viewModelScope.launch { fun onToggleShortcutsEnabled() = viewModelScope.launch {
with(uiState.value.settings) { with(uiState.value.settings) {
appDataRepository.settings.save( appDataRepository.settings.save(
this.copy( copy(
isShortcutsEnabled = !isShortcutsEnabled, isShortcutsEnabled = !isShortcutsEnabled,
), ),
) )
@@ -52,6 +52,7 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.displa
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.language.LanguageScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.language.LanguageScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.AutoTunnelScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.AutoTunnelScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.disclosure.LocationDisclosureScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.disclosure.LocationDisclosureScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.killswitch.KillSwitchScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.logs.LogsScreen import com.zaneschepke.wireguardautotunnel.ui.screens.support.logs.LogsScreen
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
@@ -144,13 +145,13 @@ class MainActivity : AppCompatActivity() {
), ),
) )
}, },
) { ) { padding ->
Box(modifier = Modifier.fillMaxSize().padding(it)) { Box(modifier = Modifier.fillMaxSize().padding(padding)) {
NavHost( NavHost(
navController, navController,
enterTransition = { fadeIn(tween(Constants.TRANSITION_ANIMATION_TIME)) }, enterTransition = { fadeIn(tween(Constants.TRANSITION_ANIMATION_TIME)) },
exitTransition = { fadeOut(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> { composable<Route.Main> {
MainScreen( MainScreen(
@@ -207,6 +208,9 @@ class MainActivity : AppCompatActivity() {
composable<Route.Scanner> { composable<Route.Scanner> {
ScannerScreen() ScannerScreen()
} }
composable<Route.KillSwitch> {
KillSwitchScreen(appUiState, viewModel)
}
} }
} }
} }
@@ -218,6 +222,6 @@ class MainActivity : AppCompatActivity() {
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
// save battery by not polling stats while app is closed // save battery by not polling stats while app is closed
tunnelService.cancelStatsJob() tunnelService.cancelActiveTunnelJobs()
} }
} }
@@ -21,6 +21,9 @@ sealed class Route {
@Serializable @Serializable
data object Display : Route() data object Display : Route()
@Serializable
data object KillSwitch : Route()
@Serializable @Serializable
data object Language : Route() data object Language : Route()
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main.components package com.zaneschepke.wireguardautotunnel.ui.common.permission.vpn
import androidx.compose.foundation.text.ClickableText import androidx.compose.foundation.text.ClickableText
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -0,0 +1,38 @@
package com.zaneschepke.wireguardautotunnel.ui.common.permission.vpn
import android.net.VpnService
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity.RESULT_OK
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
@Composable
inline fun <T> withVpnPermission(crossinline onSuccess: (t: T) -> Unit): (t: T) -> Unit {
val context = LocalContext.current
var showVpnPermissionDialog by remember { mutableStateOf(false) }
val vpnActivity =
rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult(),
onResult = {
if (it.resultCode != RESULT_OK) showVpnPermissionDialog = true
},
)
VpnDeniedDialog(showVpnPermissionDialog, onDismiss = { showVpnPermissionDialog = false })
return {
val intent = VpnService.prepare(context)
if (intent != null) {
vpnActivity.launch(intent)
} else {
onSuccess(it)
}
}
}
@@ -0,0 +1,35 @@
package com.zaneschepke.wireguardautotunnel.ui.common.permission
import android.content.Intent
import android.net.Uri
import android.provider.Settings
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import com.zaneschepke.wireguardautotunnel.util.extensions.isBatteryOptimizationsDisabled
@Composable
inline fun withIgnoreBatteryOpt(ignore: Boolean, crossinline callback: () -> Unit): () -> Unit {
val context = LocalContext.current
val batteryActivity =
rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult(),
) { result: ActivityResult ->
// we only ask once
callback()
}
return {
if (ignore || context.isBatteryOptimizationsDisabled()) {
callback()
} else {
batteryActivity.launch(
Intent().apply {
action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
data = Uri.parse("package:${context.packageName}")
},
)
}
}
}
@@ -1,13 +1,7 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main package com.zaneschepke.wireguardautotunnel.ui.screens.main
import android.content.Intent
import android.net.Uri
import android.net.VpnService
import android.provider.Settings
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity.RESULT_OK
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.ScrollableDefaults import androidx.compose.foundation.gestures.ScrollableDefaults
import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.gestures.detectTapGestures
@@ -41,6 +35,7 @@ import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.os.ConfigurationCompat
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
@@ -51,18 +46,19 @@ import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberFileImportLauncherForResult import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberFileImportLauncherForResult
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
import com.zaneschepke.wireguardautotunnel.ui.common.permission.vpn.withVpnPermission
import com.zaneschepke.wireguardautotunnel.ui.common.permission.withIgnoreBatteryOpt
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.AutoTunnelRowItem import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.AutoTunnelRowItem
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.GettingStartedLabel import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.GettingStartedLabel
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.ScrollDismissFab import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.ScrollDismissFab
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.TunnelImportSheet import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.TunnelImportSheet
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.TunnelRowItem import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.TunnelRowItem
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.VpnDeniedDialog
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.isBatteryOptimizationsDisabled
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import java.text.Collator
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
@@ -73,30 +69,35 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
val snackbar = SnackbarController.current val snackbar = SnackbarController.current
var showBottomSheet by remember { mutableStateOf(false) } var showBottomSheet by remember { mutableStateOf(false) }
var showVpnPermissionDialog by remember { mutableStateOf(false) }
var isFabVisible by rememberSaveable { mutableStateOf(true) } var isFabVisible by rememberSaveable { mutableStateOf(true) }
var showDeleteTunnelAlertDialog by remember { mutableStateOf(false) } var showDeleteTunnelAlertDialog by remember { mutableStateOf(false) }
var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) } var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) }
val isRunningOnTv = remember { context.isRunningOnTv() } val isRunningOnTv = remember { context.isRunningOnTv() }
val currentLocale = ConfigurationCompat.getLocales(context.resources.configuration)[0]
val collator = Collator.getInstance(currentLocale)
val sortedTunnels = remember(uiState.tunnels) {
uiState.tunnels.sortedWith(compareBy(collator) { it.name })
}
val startAutoTunnel = withVpnPermission<Unit> { viewModel.onToggleAutoTunnel() }
val startTunnel = withVpnPermission<TunnelConfig> {
viewModel.onTunnelStart(it, uiState.settings.isKernelEnabled)
}
val autoTunnelToggleBattery = withIgnoreBatteryOpt(uiState.generalState.isBatteryOptimizationDisableShown) {
if (!uiState.generalState.isBatteryOptimizationDisableShown) viewModel.setBatteryOptimizeDisableShown()
if (uiState.settings.isKernelEnabled) {
viewModel.onToggleAutoTunnel()
} else {
startAutoTunnel.invoke(Unit)
}
}
val nestedScrollConnection = remember { val nestedScrollConnection = remember {
NestedScrollListener({ isFabVisible = false }, { isFabVisible = true }) NestedScrollListener({ isFabVisible = false }, { isFabVisible = true })
} }
val vpnActivity =
rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult(),
onResult = {
if (it.resultCode != RESULT_OK) showVpnPermissionDialog = true
},
)
val batteryActivity =
rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult(),
) { result: ActivityResult ->
viewModel.setBatteryOptimizeDisableShown()
}
val tunnelFileImportResultLauncher = rememberFileImportLauncherForResult(onNoFileExplorer = { val tunnelFileImportResultLauncher = rememberFileImportLauncherForResult(onNoFileExplorer = {
snackbar.showMessage( snackbar.showMessage(
context.getString(R.string.error_no_file_explorer), context.getString(R.string.error_no_file_explorer),
@@ -112,8 +113,6 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
navController.navigate(Route.Scanner) navController.navigate(Route.Scanner)
} }
VpnDeniedDialog(showVpnPermissionDialog, onDismiss = { showVpnPermissionDialog = false })
if (showDeleteTunnelAlertDialog) { if (showDeleteTunnelAlertDialog) {
InfoDialog( InfoDialog(
onDismiss = { showDeleteTunnelAlertDialog = false }, onDismiss = { showDeleteTunnelAlertDialog = false },
@@ -128,35 +127,13 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
) )
} }
fun requestBatteryOptimizationsDisabled() {
val intent =
Intent().apply {
action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
data = Uri.parse("package:${context.packageName}")
}
batteryActivity.launch(intent)
}
fun onAutoTunnelToggle() {
if (!uiState.generalState.isBatteryOptimizationDisableShown &&
!context.isBatteryOptimizationsDisabled() && !isRunningOnTv
) {
return requestBatteryOptimizationsDisabled()
}
val intent = if (!uiState.settings.isKernelEnabled) {
VpnService.prepare(context)
} else {
null
}
if (intent != null) return vpnActivity.launch(intent)
viewModel.onToggleAutoTunnel()
}
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) { fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) {
val intent = if (uiState.settings.isKernelEnabled) null else VpnService.prepare(context) if (!checked) viewModel.onTunnelStop().also { return }
if (intent != null) return vpnActivity.launch(intent) if (uiState.settings.isKernelEnabled) {
if (!checked) viewModel.onTunnelStop(tunnel).also { return } viewModel.onTunnelStart(tunnel, uiState.settings.isKernelEnabled)
viewModel.onTunnelStart(tunnel, uiState.settings.isKernelEnabled) } else {
startTunnel.invoke(tunnel)
}
} }
Scaffold( Scaffold(
@@ -239,22 +216,19 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
} }
} else { } else {
item { item {
AutoTunnelRowItem(uiState, { AutoTunnelRowItem(uiState) {
onAutoTunnelToggle() autoTunnelToggleBattery.invoke()
}) }
} }
} }
items( items(
uiState.tunnels, sortedTunnels,
key = { tunnel -> tunnel.id }, key = { tunnel -> tunnel.id },
) { tunnel -> ) { tunnel ->
val isActive = uiState.tunnels.any {
it.id == tunnel.id &&
it.isActive
}
val expanded = uiState.generalState.isTunnelStatsExpanded val expanded = uiState.generalState.isTunnelStatsExpanded
TunnelRowItem( TunnelRowItem(
isActive, tunnel.id == uiState.vpnState.tunnelConfig?.id &&
uiState.vpnState.status.isUp(),
expanded, expanded,
selectedTunnel?.id == tunnel.id, selectedTunnel?.id == tunnel.id,
tunnel, tunnel,
@@ -72,9 +72,9 @@ constructor(
tunnelService.get().startTunnel(tunnelConfig, background) tunnelService.get().startTunnel(tunnelConfig, background)
} }
fun onTunnelStop(tunnel: TunnelConfig) = viewModelScope.launch { fun onTunnelStop() = viewModelScope.launch {
Timber.i("Stopping active tunnel") Timber.i("Stopping active tunnel")
tunnelService.get().stopTunnel(tunnel) tunnelService.get().stopTunnel()
} }
private fun generateQrCodeDefaultName(config: String): String { private fun generateQrCodeDefaultName(config: String): String {
@@ -159,18 +159,7 @@ constructor(
} }
fun onToggleAutoTunnel() = viewModelScope.launch { fun onToggleAutoTunnel() = viewModelScope.launch {
val settings = appDataRepository.settings.getSettings() serviceManager.toggleAutoTunnel(false)
val toggled = !settings.isAutoTunnelEnabled
if (toggled) {
serviceManager.startAutoTunnel(false)
} else {
serviceManager.stopAutoTunnel()
}
appDataRepository.settings.save(
settings.copy(
isAutoTunnelEnabled = toggled,
),
)
} }
private suspend fun saveTunnelsFromZipUri(uri: Uri, context: Context) { private suspend fun saveTunnelsFromZipUri(uri: Uri, context: Context) {
@@ -252,13 +241,7 @@ constructor(
fun onCopyTunnel(tunnel: TunnelConfig) = viewModelScope.launch { fun onCopyTunnel(tunnel: TunnelConfig) = viewModelScope.launch {
saveTunnel( saveTunnel(
tunnel.copy( TunnelConfig(name = makeTunnelNameUnique(tunnel.name), wgQuick = tunnel.wgQuick, amQuick = tunnel.amQuick),
id = 0,
isPrimaryTunnel = false,
isMobileDataTunnel = false,
isActive = false,
name = makeTunnelNameUnique(tunnel.name),
),
) )
} }
@@ -9,6 +9,7 @@ import androidx.compose.material.icons.rounded.CopyAll
import androidx.compose.material.icons.rounded.Delete import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material.icons.rounded.Info import androidx.compose.material.icons.rounded.Info
import androidx.compose.material.icons.rounded.Settings 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.Smartphone
import androidx.compose.material.icons.rounded.Star import androidx.compose.material.icons.rounded.Star
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@@ -55,14 +56,12 @@ fun TunnelRowItem(
val itemFocusRequester = remember { FocusRequester() } val itemFocusRequester = remember { FocusRequester() }
ExpandingRowListItem( ExpandingRowListItem(
leading = { leading = {
val icon = val icon = when {
if (tunnel.isPrimaryTunnel) { tunnel.isPrimaryTunnel -> Icons.Rounded.Star
Icons.Rounded.Star tunnel.isMobileDataTunnel -> Icons.Rounded.Smartphone
} else if (tunnel.isMobileDataTunnel) { tunnel.isEthernetTunnel -> Icons.Rounded.SettingsEthernet
Icons.Rounded.Smartphone else -> Icons.Rounded.Circle
} else { }
Icons.Rounded.Circle
}
Icon( Icon(
icon, icon,
icon.name, 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.NetworkPing
import androidx.compose.material.icons.outlined.PhoneAndroid import androidx.compose.material.icons.outlined.PhoneAndroid
import androidx.compose.material.icons.outlined.Security import androidx.compose.material.icons.outlined.Security
import androidx.compose.material.icons.outlined.SettingsEthernet
import androidx.compose.material.icons.outlined.Star import androidx.compose.material.icons.outlined.Star
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@@ -139,6 +140,28 @@ fun OptionsScreen(optionsViewModel: OptionsViewModel = hiltViewModel(), appUiSta
}, },
onClick = { optionsViewModel.onToggleIsMobileDataTunnel(config) }, 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( SelectionItem(
Icons.Outlined.NetworkPing, Icons.Outlined.NetworkPing,
title = { 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,13 +12,13 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ViewQuilt import androidx.compose.material.icons.automirrored.outlined.ViewQuilt
import androidx.compose.material.icons.filled.AppShortcut import androidx.compose.material.icons.filled.AppShortcut
import androidx.compose.material.icons.outlined.AdminPanelSettings
import androidx.compose.material.icons.outlined.Bolt import androidx.compose.material.icons.outlined.Bolt
import androidx.compose.material.icons.outlined.Code import androidx.compose.material.icons.outlined.Code
import androidx.compose.material.icons.outlined.FolderZip import androidx.compose.material.icons.outlined.FolderZip
import androidx.compose.material.icons.outlined.Notifications import androidx.compose.material.icons.outlined.Notifications
import androidx.compose.material.icons.outlined.Pin import androidx.compose.material.icons.outlined.Pin
import androidx.compose.material.icons.outlined.Restore import androidx.compose.material.icons.outlined.Restore
import androidx.compose.material.icons.outlined.VpnKeyOff
import androidx.compose.material.icons.outlined.VpnLock import androidx.compose.material.icons.outlined.VpnLock
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -49,7 +49,6 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.Forwar
import com.zaneschepke.wireguardautotunnel.ui.theme.topPadding import com.zaneschepke.wireguardautotunnel.ui.theme.topPadding
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import com.zaneschepke.wireguardautotunnel.util.extensions.launchNotificationSettings import com.zaneschepke.wireguardautotunnel.util.extensions.launchNotificationSettings
import com.zaneschepke.wireguardautotunnel.util.extensions.launchVpnSettings
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
import com.zaneschepke.wireguardautotunnel.util.extensions.showToast import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
@@ -179,18 +178,18 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
onClick = { appViewModel.onToggleAlwaysOnVPN() }, onClick = { appViewModel.onToggleAlwaysOnVPN() },
), ),
SelectionItem( SelectionItem(
Icons.Outlined.AdminPanelSettings, Icons.Outlined.VpnKeyOff,
title = { title = {
Text( Text(
stringResource(R.string.kill_switch), stringResource(R.string.kill_switch_options),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
) )
}, },
onClick = { onClick = {
context.launchVpnSettings() navController.navigate(Route.KillSwitch)
}, },
trailing = { trailing = {
ForwardButton { context.launchVpnSettings() } ForwardButton { navController.navigate(Route.KillSwitch) }
}, },
), ),
), ),
@@ -2,11 +2,8 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.langu
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
@@ -53,7 +50,7 @@ fun LanguageScreen(appUiState: AppUiState, appViewModel: AppViewModel) {
modifier = modifier =
Modifier Modifier
.fillMaxSize().padding(padding) .fillMaxSize().padding(padding)
.padding(horizontal = 24.dp.scaledWidth()).windowInsetsPadding(WindowInsets.navigationBars), .padding(horizontal = 24.dp.scaledWidth()),
) { ) {
item { item {
Box(modifier = Modifier.padding(top = 24.dp.scaledHeight())) { Box(modifier = Modifier.padding(top = 24.dp.scaledHeight())) {
@@ -6,12 +6,14 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons 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.Code
import androidx.compose.material.icons.outlined.Filter1 import androidx.compose.material.icons.outlined.Filter1
import androidx.compose.material.icons.outlined.NetworkPing import androidx.compose.material.icons.outlined.NetworkPing
@@ -71,16 +73,18 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV
isBackgroundLocationGranted = fineLocationState.status.isGranted isBackgroundLocationGranted = fineLocationState.status.isGranted
} }
fun onAutoTunnelWifiChecked() { fun isWifiNameReadable(): Boolean {
if (uiState.settings.isTunnelOnWifiEnabled) viewModel.onToggleTunnelOnWifi().also { return } return when {
when (false) { !isBackgroundLocationGranted ||
isBackgroundLocationGranted -> showLocationDialog = true !fineLocationState.status.isGranted -> {
fineLocationState.status.isGranted -> showLocationDialog = true showLocationDialog = true
context.isLocationServicesEnabled() -> false
showLocationServicesAlertDialog = true
else -> {
viewModel.onToggleTunnelOnWifi()
} }
!context.isLocationServicesEnabled() -> {
showLocationServicesAlertDialog = true
false
}
else -> true
} }
} }
@@ -113,18 +117,18 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV
) )
Scaffold( Scaffold(
contentWindowInsets = WindowInsets(0.dp),
topBar = { topBar = {
TopNavBar(stringResource(R.string.auto_tunneling)) TopNavBar(stringResource(R.string.auto_tunneling))
}, },
) { ) { padding ->
Column( Column(
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top), verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top),
modifier = modifier =
Modifier Modifier
.fillMaxSize() .fillMaxSize()
.padding(it) .padding(padding)
.verticalScroll(rememberScrollState())
.padding(top = 24.dp.scaledHeight()) .padding(top = 24.dp.scaledHeight())
.padding(horizontal = 24.dp.scaledWidth()), .padding(horizontal = 24.dp.scaledWidth()),
) { ) {
@@ -147,14 +151,12 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV
enabled = !uiState.settings.isAlwaysOnVpnEnabled, enabled = !uiState.settings.isAlwaysOnVpnEnabled,
checked = uiState.settings.isTunnelOnWifiEnabled, checked = uiState.settings.isTunnelOnWifiEnabled,
onClick = { onClick = {
if (uiState.settings.isWifiNameByShellEnabled) viewModel.onToggleTunnelOnWifi().also { return@ScaledSwitch } viewModel.onToggleTunnelOnWifi()
onAutoTunnelWifiChecked()
}, },
) )
}, },
onClick = { onClick = {
if (uiState.settings.isWifiNameByShellEnabled) viewModel.onToggleTunnelOnWifi().also { return@SelectionItem } viewModel.onToggleTunnelOnWifi()
onAutoTunnelWifiChecked()
}, },
), ),
SelectionItem( SelectionItem(
@@ -250,7 +252,9 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV
uiState.settings.trustedNetworkSSIDs, uiState.settings.trustedNetworkSSIDs,
onDelete = viewModel::onDeleteTrustedSSID, onDelete = viewModel::onDeleteTrustedSSID,
currentText = currentText, currentText = currentText,
onSave = viewModel::onSaveTrustedSSID, onSave = { ssid ->
if (uiState.settings.isWifiNameByShellEnabled || isWifiNameReadable()) viewModel.onSaveTrustedSSID(ssid)
},
onValueChange = { currentText = it }, onValueChange = { currentText = it },
supporting = { supporting = {
if (uiState.settings.isWildcardsEnabled) { if (uiState.settings.isWildcardsEnabled) {
@@ -323,6 +327,30 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV
viewModel.onToggleRestartOnPing() 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),
)
}
}
} }
@@ -0,0 +1,142 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.killswitch
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.AdminPanelSettings
import androidx.compose.material.icons.outlined.Lan
import androidx.compose.material.icons.outlined.VpnKey
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.AppUiState
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
import com.zaneschepke.wireguardautotunnel.ui.common.permission.vpn.withVpnPermission
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.ForwardButton
import com.zaneschepke.wireguardautotunnel.util.extensions.launchVpnSettings
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
@Composable
fun KillSwitchScreen(uiState: AppUiState, appViewModel: AppViewModel) {
val context = LocalContext.current
val toggleVpnSwitch = withVpnPermission<Boolean> { appViewModel.onToggleVpnKillSwitch(it) }
fun toggleVpnKillSwitch() {
with(uiState.settings) {
if (isVpnKillSwitchEnabled) {
appViewModel.onToggleVpnKillSwitch(false)
} else {
toggleVpnSwitch.invoke(true)
}
}
}
fun toggleLanOnKillSwitch() {
with(uiState.settings) {
appViewModel.onToggleLanOnKillSwitch(!isLanOnKillSwitchEnabled)
}
}
Scaffold(
topBar = {
TopNavBar(stringResource(R.string.kill_switch))
},
) { padding ->
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top),
modifier =
Modifier
.fillMaxSize().padding(padding)
.padding(top = 24.dp.scaledHeight())
.padding(horizontal = 24.dp.scaledWidth()),
) {
SurfaceSelectionGroupButton(
listOf(
SelectionItem(
Icons.Outlined.AdminPanelSettings,
title = {
Text(
stringResource(R.string.native_kill_switch),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
onClick = { context.launchVpnSettings() },
trailing = {
ForwardButton { context.launchVpnSettings() }
},
),
),
)
SurfaceSelectionGroupButton(
buildList {
add(
SelectionItem(
Icons.Outlined.VpnKey,
title = {
Text(
stringResource(R.string.vpn_kill_switch),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
onClick = {
toggleVpnKillSwitch()
},
trailing = {
ScaledSwitch(
uiState.settings.isVpnKillSwitchEnabled,
onClick = {
toggleVpnKillSwitch()
},
)
},
),
)
if (uiState.settings.isVpnKillSwitchEnabled) {
add(
SelectionItem(
Icons.Outlined.Lan,
title = {
Text(
stringResource(R.string.allow_lan_traffic),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
onClick = { toggleLanOnKillSwitch() },
description = {
Text(
stringResource(R.string.bypass_lan_for_kill_switch),
style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline),
)
},
trailing = {
ScaledSwitch(
uiState.settings.isLanOnKillSwitchEnabled,
onClick = {
toggleLanOnKillSwitch()
},
)
},
),
)
}
},
)
}
}
}
@@ -1,30 +1,20 @@
package com.zaneschepke.wireguardautotunnel.util package com.zaneschepke.wireguardautotunnel.util
import android.content.res.Resources import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.ConfigurationCompat import androidx.core.os.LocaleListCompat
import com.zaneschepke.wireguardautotunnel.BuildConfig import com.zaneschepke.wireguardautotunnel.BuildConfig
object LocaleUtil { object LocaleUtil {
private const val DEFAULT_LANG = "en"
val supportedLocales: Array<String> = BuildConfig.LANGUAGES val supportedLocales: Array<String> = BuildConfig.LANGUAGES
const val OPTION_PHONE_LANGUAGE = "sys_def" const val OPTION_PHONE_LANGUAGE = "sys_def"
/** fun changeLocale(locale: String) {
* returns the locale to use depending on the preference value if (locale == OPTION_PHONE_LANGUAGE) return resetToSystemLanguage()
* when preference value = "sys_def" returns the locale of current system val appLocale: LocaleListCompat = LocaleListCompat.forLanguageTags(locale)
* else it returns the locale code e.g. "en", "bn" etc. AppCompatDelegate.setApplicationLocales(appLocale)
*/ }
fun getLocaleFromPrefCode(prefCode: String): String {
val localeCode = if (prefCode != OPTION_PHONE_LANGUAGE) { private fun resetToSystemLanguage() {
prefCode AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList())
} else {
val systemLang = ConfigurationCompat.getLocales(Resources.getSystem().configuration).get(0)?.language ?: DEFAULT_LANG
if (systemLang in supportedLocales) {
systemLang
} else {
DEFAULT_LANG
}
}
return localeCode
} }
} }
@@ -3,12 +3,14 @@ package com.zaneschepke.wireguardautotunnel.util.extensions
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import com.wireguard.android.util.RootShell import com.wireguard.android.util.RootShell
import com.wireguard.config.Peer import com.wireguard.config.Peer
import com.zaneschepke.wireguardautotunnel.service.tunnel.BackendState
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree
import com.zaneschepke.wireguardautotunnel.ui.theme.Straw import com.zaneschepke.wireguardautotunnel.ui.theme.Straw
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.NumberUtils import com.zaneschepke.wireguardautotunnel.util.NumberUtils
import org.amnezia.awg.backend.Backend
import org.amnezia.awg.config.Config import org.amnezia.awg.config.Config
import timber.log.Timber import timber.log.Timber
import java.net.InetAddress import java.net.InetAddress
@@ -85,3 +87,11 @@ fun RootShell.getCurrentWifiName(): String? {
this.run(response, "dumpsys wifi | grep -o \"SSID: [^,]*\" | cut -d ' ' -f2- | tr -d '\"'") this.run(response, "dumpsys wifi | grep -o \"SSID: [^,]*\" | cut -d ' ' -f2- | tr -d '\"'")
return response.lastOrNull() return response.lastOrNull()
} }
fun Backend.BackendState.asBackendState(): BackendState {
return BackendState.valueOf(this.name)
}
fun BackendState.asAmBackendState(): Backend.BackendState {
return Backend.BackendState.valueOf(this.name)
}
+35 -4
View File
@@ -5,7 +5,7 @@
<string name="no_tunnels">Noch keine Tunnel hinzugefügt!</string> <string name="no_tunnels">Noch keine Tunnel hinzugefügt!</string>
<string name="tunnels">Tunnel</string> <string name="tunnels">Tunnel</string>
<string name="tunnel_mobile_data">Tunnel für mobile Daten</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="okay">Ok</string>
<string name="tunnel_on_ethernet">Tunnel für Ethernet</string> <string name="tunnel_on_ethernet">Tunnel für Ethernet</string>
<string name="auto_tunneling">Auto-Tunneln</string> <string name="auto_tunneling">Auto-Tunneln</string>
@@ -29,7 +29,7 @@
<string name="addresses">Adressen</string> <string name="addresses">Adressen</string>
<string name="dns_servers">DNS-Server</string> <string name="dns_servers">DNS-Server</string>
<string name="mtu">MTU</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="allowed_ips">Erlaubte IPs</string>
<string name="endpoint">Endpunkt</string> <string name="endpoint">Endpunkt</string>
<string name="name">Name</string> <string name="name">Name</string>
@@ -81,7 +81,6 @@
<string name="version">Version</string> <string name="version">Version</string>
<string name="settings">Einstellungen</string> <string name="settings">Einstellungen</string>
<string name="support">Unterstützung</string> <string name="support">Unterstützung</string>
<string name="watcher_channel_id">Wächterkanal</string>
<string name="error_authentication_failed">Authentifizierung fehlgeschlagen</string> <string name="error_authentication_failed">Authentifizierung fehlgeschlagen</string>
<string name="export_configs">Konfigurationen exportieren</string> <string name="export_configs">Konfigurationen exportieren</string>
<string name="unknown_error">Unbekannter Fehler aufgetreten</string> <string name="unknown_error">Unbekannter Fehler aufgetreten</string>
@@ -99,7 +98,6 @@
<string name="set_primary_tunnel">Als Primären Tunnel setzen</string> <string name="set_primary_tunnel">Als Primären Tunnel setzen</string>
<string name="vpn_channel_id">VPN Kanal</string> <string name="vpn_channel_id">VPN Kanal</string>
<string name="vpn_channel_name">VPN Benachrichtigungskanal</string> <string name="vpn_channel_name">VPN Benachrichtigungskanal</string>
<string name="watcher_channel_name">Wächterbenachrichtigungskanal</string>
<string name="turn_off_tunnel">Aktion erfordert deaktivierten Tunnel</string> <string name="turn_off_tunnel">Aktion erfordert deaktivierten Tunnel</string>
<string name="kernel">Kernel</string> <string name="kernel">Kernel</string>
<string name="use_kernel">Kernelmodul verwenden</string> <string name="use_kernel">Kernelmodul verwenden</string>
@@ -138,4 +136,37 @@
<string name="handshake">Handshake</string> <string name="handshake">Handshake</string>
<string name="vpn_denied_dialog_title">Genehmigung verweigert</string> <string name="vpn_denied_dialog_title">Genehmigung verweigert</string>
<string name="logs">Logs</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> </resources>
+2 -3
View File
@@ -101,9 +101,8 @@
<string name="app_name">WG Tunnel</string> <string name="app_name">WG Tunnel</string>
<string name="vpn_channel_id">Canal VPN</string> <string name="vpn_channel_id">Canal VPN</string>
<string name="vpn_channel_name">Canal de notificación VPN</string> <string name="vpn_channel_name">Canal de notificación VPN</string>
<string name="watcher_channel_id">Canal del obvervador</string> <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="watcher_channel_name">Canal de notificación del obvervador</string>
<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_count">Recuento de paquetes basura</string>
<string name="junk_packet_minimum_size">Tamaño mínimo del paquete 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> </resources>
+34 -3
View File
@@ -1,13 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="vpn_channel_id">Canal VPN</string> <string name="vpn_channel_id">Canal VPN</string>
<string name="watcher_channel_id">Canal de surveillance</string>
<string name="watcher_channel_name">Canal de notification de surveillance</string>
<string name="turn_off_tunnel">Cette action nécessite la désactivation du tunnel</string> <string name="turn_off_tunnel">Cette action nécessite la désactivation du tunnel</string>
<string name="no_tunnels">Aucun tunnel n\'a été ajouté pour le moment!</string> <string name="no_tunnels">Aucun tunnel n\'a été ajouté pour le moment!</string>
<string name="tunnels">Tunnels</string> <string name="tunnels">Tunnels</string>
<string name="tunnel_mobile_data">Tunnel sur données mobiles</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="tunnel_on_ethernet">Tunnel sur Ethernet</string>
<string name="prominent_background_location_title">Divulgation de la localisation en arrière-plan</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> <string name="thank_you">Merci d\'utiliser WG Tunnel!</string>
@@ -138,4 +136,37 @@
<string name="set_custom_ping_internal">Intervalle de ping (sec)</string> <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="set_custom_ping_cooldown">Temps d\'attente avant redémarrage du ping (sec)</string>
<string name="sec">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> </resources>
-2
View File
@@ -6,7 +6,6 @@
<string name="kernel">Kernel</string> <string name="kernel">Kernel</string>
<string name="init_packet_junk_size">Ukuran sampah paket init</string> <string name="init_packet_junk_size">Ukuran sampah paket init</string>
<string name="app_name">WG Tunnel</string> <string name="app_name">WG Tunnel</string>
<string name="watcher_channel_name">Notifikasi Saluran Pengamat</string>
<string name="error_file_extension">File bukan .conf atau .zip</string> <string name="error_file_extension">File bukan .conf atau .zip</string>
<string name="turn_off_tunnel">Aksi memerlukan tunnel mati</string> <string name="turn_off_tunnel">Aksi memerlukan tunnel mati</string>
<string name="no_tunnels">Belum ada tunnel yang ditambahkan!</string> <string name="no_tunnels">Belum ada tunnel yang ditambahkan!</string>
@@ -99,7 +98,6 @@
<string name="handshake">handshake</string> <string name="handshake">handshake</string>
<string name="vpn_channel_id">Saluran VPN</string> <string name="vpn_channel_id">Saluran VPN</string>
<string name="vpn_channel_name">Notifikasi Saluran VPN</string> <string name="vpn_channel_name">Notifikasi Saluran VPN</string>
<string name="watcher_channel_id">Saluran Pengamat</string>
<string name="prominent_background_location_message">Fitur ini memerlukan izin lokasi latar belakang untuk mengaktifkan pemantauan SSID Wi-Fi bahkan saat aplikasi ditutup. Untuk detail lebih lanjut, silakan lihat Kebijakan Privasi yang ditautkan di layar Dukungan.</string> <string name="prominent_background_location_message">Fitur ini memerlukan izin lokasi latar belakang untuk mengaktifkan pemantauan SSID Wi-Fi bahkan saat aplikasi ditutup. Untuk detail lebih lanjut, silakan lihat Kebijakan Privasi yang ditautkan di layar Dukungan.</string>
<string name="copy_public_key">Salin kunci publik</string> <string name="copy_public_key">Salin kunci publik</string>
<string name="base64_key">kunci base64</string> <string name="base64_key">kunci base64</string>
-2
View File
@@ -15,8 +15,6 @@
<string name="icon">Icoon</string> <string name="icon">Icoon</string>
<string name="include">Meenemen</string> <string name="include">Meenemen</string>
<string name="addresses">Adres</string> <string name="addresses">Adres</string>
<string name="watcher_channel_id">Watcher Kanaal</string>
<string name="watcher_channel_name">Watcher Notificatiekanaal</string>
<string name="peer">Peer (extern systeem)</string> <string name="peer">Peer (extern systeem)</string>
<string name="allowed_ips">Allowed IPs</string> <string name="allowed_ips">Allowed IPs</string>
<string name="always_on_vpn_support">Altijd-aan VPN toestaan</string> <string name="always_on_vpn_support">Altijd-aan VPN toestaan</string>
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -99,8 +99,6 @@
<string name="app_name">WG Tunnel</string> <string name="app_name">WG Tunnel</string>
<string name="vpn_channel_id">Canal de VPN</string> <string name="vpn_channel_id">Canal de VPN</string>
<string name="vpn_channel_name">Canal de notificações VPN</string> <string name="vpn_channel_name">Canal de notificações VPN</string>
<string name="watcher_channel_id">Canal de vigia</string>
<string name="watcher_channel_name">Canal de notificações de vigia</string>
<string name="prominent_background_location_message">Este recurso precisa de permissões de localização em segundo plano para ativar o monitoramento do SSID da rede Wi-Fi mesmo quando a aplicação está fechado. Para mais pormenores, por favor veja a Política de Privacidade no ecrã de Suporte.</string> <string name="prominent_background_location_message">Este recurso precisa de permissões de localização em segundo plano para ativar o monitoramento do SSID da rede Wi-Fi mesmo quando a aplicação está fechado. Para mais pormenores, por favor veja a Política de Privacidade no ecrã de Suporte.</string>
<string name="trusted_ssid_value_description">Envie o SSID</string> <string name="trusted_ssid_value_description">Envie o SSID</string>
<string name="add_tunnels_text">Adicionar a partir de ficheiro ou zip</string> <string name="add_tunnels_text">Adicionar a partir de ficheiro ou zip</string>
-2
View File
@@ -110,8 +110,6 @@
<string name="error_file_format">Formato de configuração inválido</string> <string name="error_file_format">Formato de configuração inválido</string>
<string name="vpn_channel_id">Canal de VPN</string> <string name="vpn_channel_id">Canal de VPN</string>
<string name="vpn_channel_name">Canal de notificações VPN</string> <string name="vpn_channel_name">Canal de notificações VPN</string>
<string name="watcher_channel_id">Canal de vigia</string>
<string name="watcher_channel_name">Canal de notificações de vigia</string>
<string name="db_name">wg-tunnel-db</string> <string name="db_name">wg-tunnel-db</string>
<string name="set_custom_ping_ip">Definir ip ping personalizado</string> <string name="set_custom_ping_ip">Definir ip ping personalizado</string>
<string name="vpn_denied_dialog_title">Permissão negada</string> <string name="vpn_denied_dialog_title">Permissão negada</string>
+33 -2
View File
@@ -63,8 +63,6 @@
<string name="app_name">WG Tunnel</string> <string name="app_name">WG Tunnel</string>
<string name="vpn_channel_id">Канал VPN</string> <string name="vpn_channel_id">Канал VPN</string>
<string name="vpn_channel_name">Канал уведомлений VPN</string> <string name="vpn_channel_name">Канал уведомлений VPN</string>
<string name="watcher_channel_id">Канал наблюдателя</string>
<string name="watcher_channel_name">Канал уведомлений наблюдателя</string>
<string name="tunnels">Туннели</string> <string name="tunnels">Туннели</string>
<string name="okay">Хорошо</string> <string name="okay">Хорошо</string>
<string name="prominent_background_location_title">Фоновая передача местоположения</string> <string name="prominent_background_location_title">Фоновая передача местоположения</string>
@@ -138,4 +136,37 @@
<string name="sec">сек.</string> <string name="sec">сек.</string>
<string name="handshake">рукопожатие</string> <string name="handshake">рукопожатие</string>
<string name="logs">Журналы</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> </resources>
-2
View File
@@ -3,8 +3,6 @@
<string name="app_name">WG Tunnel</string> <string name="app_name">WG Tunnel</string>
<string name="vpn_channel_id">VPN Kanalı</string> <string name="vpn_channel_id">VPN Kanalı</string>
<string name="vpn_channel_name">VPN Bildirim Kanalı</string> <string name="vpn_channel_name">VPN Bildirim Kanalı</string>
<string name="watcher_channel_id">İzleyici Kanalı</string>
<string name="watcher_channel_name">İzleyici Bildirim Kanalı</string>
<string name="github_url" translatable="false">https://github.com/zaneschepke/wgtunnel/issues</string> <string name="github_url" translatable="false">https://github.com/zaneschepke/wgtunnel/issues</string>
<string name="docs_url" translatable="false">https://zaneschepke.com/wgtunnel-docs/overview.html</string> <string name="docs_url" translatable="false">https://zaneschepke.com/wgtunnel-docs/overview.html</string>
<string name="privacy_policy_url" translatable="false">https://zaneschepke.com/wgtunnel-docs/privacypolicy.html</string> <string name="privacy_policy_url" translatable="false">https://zaneschepke.com/wgtunnel-docs/privacypolicy.html</string>
+73 -34
View File
@@ -1,14 +1,14 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="turn_off_tunnel">请关闭连接再操作</string> <string name="turn_off_tunnel">请关闭连接再操作</string>
<string name="no_tunnels">你还没有添加隧道!</string> <string name="no_tunnels">你还没有添加隧道</string>
<string name="tunnels">连接列表</string> <string name="tunnels">连接列表</string>
<string name="privacy_policy">查看隐私政策</string> <string name="privacy_policy">查看隐私政策</string>
<string name="tunnel_mobile_data">隧道使用手机数据流量</string> <string name="tunnel_mobile_data">允许隧道使用手机数据流量</string>
<string name="okay"></string> <string name="okay"></string>
<string name="tunnel_on_ethernet">在局域网中使用隧道</string> <string name="tunnel_on_ethernet">在局域网中使用隧道</string>
<string name="error_file_extension">文件类型不是 .conf 或 .zip</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_tunnels_text">从文件或 zip 添加</string>
<string name="add_from_qr">从二维码添加</string> <string name="add_from_qr">从二维码添加</string>
<string name="qr_scan">扫描二维码</string> <string name="qr_scan">扫描二维码</string>
@@ -18,15 +18,15 @@
<string name="icon">图标</string> <string name="icon">图标</string>
<string name="public_key">公钥</string> <string name="public_key">公钥</string>
<string name="addresses">地址</string> <string name="addresses">地址</string>
<string name="dns_servers">DNS服务器</string> <string name="dns_servers">DNS 服务器</string>
<string name="mtu">MTU</string> <string name="mtu">MTU</string>
<string name="allowed_ips">允许的IP</string> <string name="allowed_ips">允许的 IP</string>
<string name="name">名称</string> <string name="name">名称</string>
<string name="peer">节点</string> <string name="peer">节点</string>
<string name="always_on_vpn_support">允许VPN始终在线</string> <string name="always_on_vpn_support">允许VPN始终在线</string>
<string name="location_services_not_detected">定位服务未开启</string> <string name="location_services_not_detected">定位服务未开启</string>
<string name="hint_search_packages">查找软件包</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="done">完成</string>
<string name="rotate_keys">轮换秘钥</string> <string name="rotate_keys">轮换秘钥</string>
<string name="create_import">手动创建</string> <string name="create_import">手动创建</string>
@@ -39,23 +39,23 @@
<string name="export_configs">导出设置</string> <string name="export_configs">导出设置</string>
<string name="docs_description">阅读文档</string> <string name="docs_description">阅读文档</string>
<string name="email_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_no_file_explorer">没有安装任何的文件管理器</string>
<string name="error_invalid_code">无效的二维码</string> <string name="error_invalid_code">无效的二维码</string>
<string name="copy_public_key">复制公钥</string> <string name="copy_public_key">复制公钥</string>
<string name="email_chooser">发送邮件…</string> <string name="email_chooser">发送邮件…</string>
<string name="persistent_keepalive">连接保活</string> <string name="persistent_keepalive">连接保活</string>
<string name="turn_on_tunnel">此操作需要一个已建立的隧道</string> <string name="turn_on_tunnel">此操作需要一个已建立的隧道</string>
<string name="tunnel_on_wifi">在不受信任的wifi上建立隧道</string> <string name="tunnel_on_wifi">在不受信任的 WiFi 上建立隧道</string>
<string name="exclude"></string> <string name="exclude"></string>
<string name="comma_separated_list">逗号分隔列表</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="use_kernel">使用内核模块</string>
<string name="endpoint"></string> <string name="endpoint"></string>
<string name="thank_you">谢谢使用WG Tunnel!</string> <string name="thank_you">谢谢使用 WG Tunnel!</string>
<string name="prominent_background_location_message">此功能是在app关闭时,后台自动扫描Wi-Fi SSID,需要开启后台位置信息访问权限。更多信息,请在支持页面查看隐私政策。</string> <string name="prominent_background_location_message">此功能是在应用关闭时,后台自动扫描 Wi-Fi SSID,需要开启后台位置信息访问权限。更多信息,请在支持页面查看隐私政策。</string>
<string name="vpn_on">VPN已连接</string> <string name="vpn_on">VPN 已连接</string>
<string name="vpn_off">VPN已关闭</string> <string name="vpn_off">VPN 已关闭</string>
<string name="auto_tunneling">自动建立隧道</string> <string name="auto_tunneling">自动建立隧道</string>
<string name="add_peer">添加节点</string> <string name="add_peer">添加节点</string>
<string name="random">(随机)</string> <string name="random">(随机)</string>
@@ -73,13 +73,13 @@
<string name="auto_tunnel_title">自动连接服务</string> <string name="auto_tunnel_title">自动连接服务</string>
<string name="delete_tunnel">删除隧道</string> <string name="delete_tunnel">删除隧道</string>
<string name="delete_tunnel_message">确定删除这个隧道吗?</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="yes"></string>
<string name="tunneling_apps">正使用隧道的app</string> <string name="tunneling_apps">正使用隧道的应用</string>
<string name="included">已包含</string> <string name="included">已包含</string>
<string name="excluded">排除</string> <string name="excluded">排除</string>
<string name="all">全部</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="enable_app_lock">锁定应用</string>
<string name="use_tunnel_on_wifi_name">在指定的wifi上使用此隧道</string> <string name="use_tunnel_on_wifi_name">在指定的wifi上使用此隧道</string>
<string name="version">版本</string> <string name="version">版本</string>
@@ -88,21 +88,19 @@
<string name="kernel">内核</string> <string name="kernel">内核</string>
<string name="junk_packet_minimum_size">无效包最小值</string> <string name="junk_packet_minimum_size">无效包最小值</string>
<string name="chat_description">加入社区</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="default_ping_ip">(可选,默认选择节点)</string>
<string name="set_custom_ping_internal">Ping 间隔(秒)</string> <string name="set_custom_ping_internal">Ping 间隔(秒)</string>
<string name="optional_default">"可选,默认: "</string> <string name="optional_default">"可选,默认: "</string>
<string name="set_custom_ping_cooldown">Ping 重启间隔(秒)</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="no_browser_detected">没有安装浏览器</string>
<string name="incorrect_pin">密码不正确</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="junk_packet_count">无效包计数</string>
<string name="app_name">WG Tunnel</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="open_issue">查看问题</string>
<string name="read_logs">查看日志</string> <string name="read_logs">查看日志</string>
<string name="auto">(自动)</string> <string name="auto">(自动)</string>
@@ -110,24 +108,65 @@
<string name="enter_pin">输入密码</string> <string name="enter_pin">输入密码</string>
<string name="create_pin">创建密码</string> <string name="create_pin">创建密码</string>
<string name="set_primary_tunnel">设置为主隧道</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="init_packet_junk_size">初始化无效包大小</string>
<string name="junk_packet_maximum_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="response_packet_junk_size">无效的响应包大小</string>
<string name="vpn_denied_dialog_title">拒绝访问</string> <string name="vpn_denied_dialog_title">拒绝访问</string>
<string name="tunnel_required">此功能需要至少一个隧道</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="background_location_message2">请确保这些权限已开启。</string>
<string name="logs">日志</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="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="error_file_format">无效的隧道配置文件格式</string>
<string name="always_on_message">VPN连接被拒绝,请检查</string> <string name="always_on_message">VPN 连接被拒绝,请检查</string>
<string name="vpn_settings">VPN系统设置</string> <string name="vpn_settings">系统 VPN 设置</string>
<string name="always_on_message2">始终开启VPN功能是否关闭,然后再尝试连接</string> <string name="always_on_message2">确保始终开启 VPN 功能关闭,然后再尝试连接</string>
<string name="never">从不</string> <string name="never">从不</string>
<string name="sec"></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> </resources>
+17 -5
View File
@@ -2,9 +2,7 @@
<string name="app_name">WG Tunnel</string> <string name="app_name">WG Tunnel</string>
<string name="vpn_channel_id">VPN Channel</string> <string name="vpn_channel_id">VPN Channel</string>
<string name="vpn_channel_name">VPN Notification Channel</string> <string name="vpn_channel_name">VPN Notification Channel</string>
<string name="watcher_channel_id">Watcher Channel</string> <string name="github_url" translatable="false">https://github.com/zaneschepke/wgtunnel/issues</string>
<string name="watcher_channel_name">Watcher Notification Channel</string>
<string name="github_url" translatable="false">https://github.com/zaneschepke/wgtunnel/issues</string>
<string name="docs_url" translatable="false">https://zaneschepke.com/wgtunnel-docs/overview.html</string> <string name="docs_url" translatable="false">https://zaneschepke.com/wgtunnel-docs/overview.html</string>
<string name="privacy_policy_url" translatable="false">https://zaneschepke.com/wgtunnel-docs/privacypolicy.html</string> <string name="privacy_policy_url" translatable="false">https://zaneschepke.com/wgtunnel-docs/privacypolicy.html</string>
<string name="docs_wildcards" translatable="false" >https://zaneschepke.com/wgtunnel-docs/features.html#wildcard-wi-fi-name-support</string> <string name="docs_wildcards" translatable="false" >https://zaneschepke.com/wgtunnel-docs/features.html#wildcard-wi-fi-name-support</string>
@@ -82,7 +80,7 @@
<string name="error_no_file_explorer">No file explorer installed</string> <string name="error_no_file_explorer">No file explorer installed</string>
<string name="error_invalid_code">Invalid QR code</string> <string name="error_invalid_code">Invalid QR code</string>
<string name="location_services_missing_message">The app is not detecting any location services enabled on your device. Depending on the device, this could cause the untrusted wifi feature to fail to read the wifi name. Would you like to continue anyways?</string> <string name="location_services_missing_message">The app is not detecting any location services enabled on your device. Depending on the device, this could cause the untrusted wifi feature to fail to read the wifi name. Would you like to continue anyways?</string>
<string name="auto_tunnel_title">Auto-tunnel Service</string> <string name="auto_tunnel_title">Auto-tunnel service</string>
<string name="delete_tunnel">Delete tunnel</string> <string name="delete_tunnel">Delete tunnel</string>
<string name="delete_tunnel_message">Are you sure you would like to delete this tunnel?</string> <string name="delete_tunnel_message">Are you sure you would like to delete this tunnel?</string>
<string name="yes">Yes</string> <string name="yes">Yes</string>
@@ -133,7 +131,7 @@
<string name="tunnel_required">Feature requires at least one tunnel</string> <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="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="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="root_accepted">Root shell accepted</string>
<string name="set_custom_ping_ip">Set custom ping ip</string> <string name="set_custom_ping_ip">Set custom ping ip</string>
<string name="default_ping_ip">(optional, defaults to peers)</string> <string name="default_ping_ip">(optional, defaults to peers)</string>
@@ -178,4 +176,18 @@
<string name="configuration_change">Configuration change</string> <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="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="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>
<string name="native_kill_switch">Native kill switch</string>
<string name="vpn_kill_switch">VPN kill switch</string>
<string name="kill_switch_options">Kill switch options</string>
<string name="allow_lan_traffic">Allow LAN traffic</string>
<string name="bypass_lan_for_kill_switch">Bypass LAN for kill switch</string>
<string name="vpn_channel_description">A channel for VPN state notifications</string>
<string name="auto_tunnel_channel_id">Auto-tunnel Channel</string>
<string name="auto_tunnel_channel_name">Auto-tunnel Notification Channel</string>
<string name="auto_tunnel_channel_description">A channel for auto-tunnel state notifications</string>
<string name="stop">stop</string>
</resources> </resources>
+2 -2
View File
@@ -1,7 +1,7 @@
object Constants { object Constants {
const val VERSION_NAME = "3.6.1" const val VERSION_NAME = "3.6.3"
const val JVM_TARGET = "17" const val JVM_TARGET = "17"
const val VERSION_CODE = 36100 const val VERSION_CODE = 36300
const val TARGET_SDK = 35 const val TARGET_SDK = 35
const val MIN_SDK = 26 const val MIN_SDK = 26
const val APP_ID = "com.zaneschepke.wireguardautotunnel" const val APP_ID = "com.zaneschepke.wireguardautotunnel"
@@ -1,5 +1,5 @@
Was ist neu: Was ist neu:
- Auto-Tunnel Auswahl nach WLAN-Name - 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 - Automatischer Neustart des Manuellen Tunnels nach einem Systemneustart
- Verschiedene Fehlerbehebungen und Performanceverbesserungen - Verschiedene Fehlerbehebungen und Performanceverbesserungen
@@ -1,5 +1,5 @@
Was ist neu: Was ist neu:
- Verbesserung der Benennung von Tunnel-Importen - Verbesserung der Benennung von Tunnel-Importen
- Fehler des Anfangszustand beim automatischen Tunneln behoben - Fehler des Anfangszustand beim Auto-Tunneln behoben
- Verbesserte Fehlerhandhabung - Verbesserte Fehlerhandhabung
- Fehler beim Import von Amnezia zip behoben - Fehler beim Import von Amnezia zip behoben
@@ -1,5 +1,5 @@
Was ist neu Was ist neu
- Unterstützung für weitere Sprachen - 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 - für AndroidTV: Bug bei schwebendem Aktionsknopf behoben
- andere Optimisation und Erweiterungen - 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 - 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 - Automatische Verbindung zum VPN basierend auf der WLAN-SSID, Ethernet und mobilen Daten
- Geteilter Tunnel für Anwendungen mit Suche - 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 - Amnezia Unterstützung für Benutzeroberflächen-Modus zur DPI/Zensurschutz
- Always-On VPN Unterstützung - Always-On VPN Unterstützung
- Export von Amnezia- und WireGuard-Tunnel ins Zip Format - Export von Amnezia- und WireGuard-Tunnel ins Zip Format
- Quicktiles zum aktivieren/deaktivieren der VPN Verbindung - 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 - Absichtlicher Automationssupport für alle Tunnel
- Automatischer Servicestart nach Geräteneustart - Automatischer Servicestart nach Geräteneustart
- Akkuerhaltungsfunktionen - Akkuerhaltungsfunktionen
@@ -0,0 +1,5 @@
What's new:
- Added VPN kill switch w/LAN bypass
- Improved auto tunnel speed and reliability
- Improved tile sync
- Various bug fixes and improvements
@@ -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 @@
WG Tunnel
@@ -0,0 +1,6 @@
Что нового:
- Обновление интерфейса
- Улучшения навигации AndroidTV
- Исправление ошибки разряда батареи
- Исправление подстановочных знаков дополнительной настройкой
- Другие улучшения
@@ -0,0 +1,7 @@
Что нового:
- Добавление туннеля из буфера обмена
- Добавление переводов
- Исправлена ошибка разряда батареи
- Исправлена ошибка удаления
- Улучшена синхронизация плиток туннеля
- Другие исправления и улучшения
+6 -6
View File
@@ -1,7 +1,7 @@
[versions] [versions]
accompanist = "0.36.0" accompanist = "0.36.0"
activityCompose = "1.9.3" activityCompose = "1.9.3"
amneziawgAndroid = "1.2.2" amneziawgAndroid = "1.2.3"
androidx-junit = "1.2.1" androidx-junit = "1.2.1"
appcompat = "1.7.0" appcompat = "1.7.0"
biometricKtx = "1.2.0-alpha05" biometricKtx = "1.2.0-alpha05"
@@ -10,7 +10,7 @@ coreKtx = "1.15.0"
datastorePreferences = "1.1.1" datastorePreferences = "1.1.1"
desugar_jdk_libs = "2.1.3" desugar_jdk_libs = "2.1.3"
espressoCore = "3.6.1" espressoCore = "3.6.1"
hiltAndroid = "2.52" hiltAndroid = "2.53"
hiltNavigationCompose = "1.2.0" hiltNavigationCompose = "1.2.0"
junit = "4.13.2" junit = "4.13.2"
kotlinx-serialization-json = "1.7.3" kotlinx-serialization-json = "1.7.3"
@@ -21,9 +21,9 @@ pinLockCompose = "1.0.4"
roomVersion = "2.6.1" roomVersion = "2.6.1"
timber = "5.0.1" timber = "5.0.1"
tunnel = "1.2.1" tunnel = "1.2.1"
androidGradlePlugin = "8.7.2" androidGradlePlugin = "8.8.0-rc01"
kotlin = "2.0.21" kotlin = "2.1.0"
ksp = "2.0.21-1.0.28" ksp = "2.1.0-1.0.29"
composeBom = "2024.11.00" composeBom = "2024.11.00"
compose = "1.7.5" compose = "1.7.5"
zxingAndroidEmbedded = "4.3.0" zxingAndroidEmbedded = "4.3.0"
@@ -32,7 +32,7 @@ gradlePlugins-grgit = "5.3.0"
#plugins #plugins
material = "1.12.0" material = "1.12.0"
gradlePlugins-ktlint="12.1.1" gradlePlugins-ktlint="12.1.2"
[libraries] [libraries]
+2 -2
View File
@@ -1,8 +1,8 @@
#Wed Oct 11 22:39:21 EDT 2023 #Wed Oct 11 22:39:21 EDT 2023
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab distributionSha256Sum=31c55713e40233a8303827ceb42ca48a47267a0ad4bab9177123121e71524c26
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
+1
View File
@@ -49,6 +49,7 @@ dependencies {
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.espresso.core)
implementation(libs.androidx.lifecycle.process)
// logging // logging
implementation(libs.timber) implementation(libs.timber)
} }
@@ -4,8 +4,7 @@ import com.zaneschepke.logcatter.model.LogMessage
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface LogReader { interface LogReader {
suspend fun start(onLogMessage: ((message: LogMessage) -> Unit)? = null) fun initialize(onLogMessage: ((message: LogMessage) -> Unit)? = null)
fun stop()
fun zipLogFiles(path: String) fun zipLogFiles(path: String)
suspend fun deleteAndClearLogs() suspend fun deleteAndClearLogs()
val bufferedLogs: Flow<LogMessage> val bufferedLogs: Flow<LogMessage>
@@ -1,26 +1,30 @@
package com.zaneschepke.logcatter package com.zaneschepke.logcatter
import android.content.Context import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.zaneschepke.logcatter.model.LogMessage import com.zaneschepke.logcatter.model.LogMessage
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
import java.io.BufferedOutputStream import java.io.BufferedOutputStream
import java.io.BufferedReader import java.io.BufferedReader
import java.io.File import java.io.File
import java.io.FileNotFoundException
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.IOException import java.io.IOException
import java.io.InputStreamReader import java.io.InputStreamReader
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream import java.util.zip.ZipOutputStream
object LogcatCollector { object LogcatReader {
private const val MAX_FILE_SIZE = 2097152L // 2MB private const val MAX_FILE_SIZE = 2097152L // 2MB
private const val MAX_FOLDER_SIZE = 10485760L // 10MB private const val MAX_FOLDER_SIZE = 10485760L // 10MB
@@ -40,7 +44,7 @@ object LogcatCollector {
var logcatPath = "" var logcatPath = ""
} }
fun init(maxFileSize: Long = MAX_FILE_SIZE, maxFolderSize: Long = MAX_FOLDER_SIZE, context: Context): LogReader { fun init(maxFileSize: Long = MAX_FILE_SIZE, maxFolderSize: Long = MAX_FOLDER_SIZE, storageDir: String): LogReader {
if (maxFileSize > maxFolderSize) { if (maxFileSize > maxFolderSize) {
throw IllegalStateException("maxFileSize must be less than maxFolderSize") throw IllegalStateException("maxFileSize must be less than maxFolderSize")
} }
@@ -48,13 +52,11 @@ object LogcatCollector {
LogcatHelperInit.maxFileSize = maxFileSize LogcatHelperInit.maxFileSize = maxFileSize
LogcatHelperInit.maxFolderSize = maxFolderSize LogcatHelperInit.maxFolderSize = maxFolderSize
LogcatHelperInit.pID = android.os.Process.myPid() LogcatHelperInit.pID = android.os.Process.myPid()
context.getExternalFilesDir(null)?.let { LogcatHelperInit.publicAppDirectory = storageDir
LogcatHelperInit.publicAppDirectory = it.absolutePath LogcatHelperInit.logcatPath = LogcatHelperInit.publicAppDirectory + File.separator + "logs"
LogcatHelperInit.logcatPath = LogcatHelperInit.publicAppDirectory + File.separator + "logs" val logDirectory = File(LogcatHelperInit.logcatPath)
val logDirectory = File(LogcatHelperInit.logcatPath) if (!logDirectory.exists()) {
if (!logDirectory.exists()) { logDirectory.mkdir()
logDirectory.mkdir()
}
} }
return Logcat return Logcat
} }
@@ -62,7 +64,12 @@ object LogcatCollector {
internal object Logcat : LogReader { internal object Logcat : LogReader {
private var logcatReader: LogcatReader? = null private lateinit var logcatReader: LogcatReader
override fun initialize(onLogMessage: ((message: LogMessage) -> Unit)?) {
logcatReader = LogcatReader(LogcatHelperInit.pID.toString(), LogcatHelperInit.logcatPath, onLogMessage)
ProcessLifecycleOwner.get().lifecycle.addObserver(logcatReader)
}
private fun obfuscator(log: String): String { private fun obfuscator(log: String): String {
return findKeyRegex.replace(log, "<crypto-key>").let { first -> return findKeyRegex.replace(log, "<crypto-key>").let { first ->
@@ -72,22 +79,10 @@ object LogcatCollector {
}.let { last -> findIpv4AddressRegex.replace(last, "<ipv4-address>") } }.let { last -> findIpv4AddressRegex.replace(last, "<ipv4-address>") }
} }
override suspend fun start(onLogMessage: ((message: LogMessage) -> Unit)?) {
logcatReader ?: run {
logcatReader = LogcatReader(LogcatHelperInit.pID.toString(), LogcatHelperInit.logcatPath, onLogMessage)
}
logcatReader?.run()
}
override fun stop() {
logcatReader?.stop()
logcatReader = null
}
override fun zipLogFiles(path: String) { override fun zipLogFiles(path: String) {
logcatReader?.pause() logcatReader.cancel()
zipAll(path) zipAll(path)
logcatReader?.resume() logcatReader.onCreate(ProcessLifecycleOwner.get())
} }
private fun zipAll(zipFilePath: String) { private fun zipAll(zipFilePath: String) {
@@ -110,10 +105,10 @@ object LogcatCollector {
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
override suspend fun deleteAndClearLogs() { override suspend fun deleteAndClearLogs() {
withContext(ioDispatcher) { withContext(ioDispatcher) {
logcatReader?.pause() logcatReader.cancel()
_bufferedLogs.resetReplayCache() _bufferedLogs.resetReplayCache()
logcatReader?.deleteAllFiles() logcatReader.deleteAllFiles()
logcatReader?.resume() logcatReader.onCreate(ProcessLifecycleOwner.get())
} }
} }
@@ -134,57 +129,25 @@ object LogcatCollector {
pID: String, pID: String,
private val logcatPath: String, private val logcatPath: String,
private val callback: ((input: LogMessage) -> Unit)?, private val callback: ((input: LogMessage) -> Unit)?,
) { ) : DefaultLifecycleObserver {
private var logcatProc: Process? = null private var logcatProc: Process? = null
private var reader: BufferedReader? = null private var reader: BufferedReader? = null
@get:Synchronized @set:Synchronized private val command = "logcat -v epoch | grep \"($pID)\""
private var paused = false private val clearLogCommand = "logcat -c"
private var logJob: Job? = null
@get:Synchronized @set:Synchronized
private var stopped = false
private var command = ""
private var clearLogCommand = ""
private var outputStream: FileOutputStream? = null private var outputStream: FileOutputStream? = null
init { override fun onCreate(owner: LifecycleOwner) {
try { super.onCreate(owner)
outputStream = FileOutputStream(createLogFile(logcatPath)) logJob = owner.lifecycleScope.launch(ioDispatcher) {
} catch (e: FileNotFoundException) {
Timber.e(e)
}
command = "logcat -v epoch | grep \"($pID)\""
clearLogCommand = "logcat -c"
}
fun pause() {
paused = true
}
fun stop() {
stopped = true
}
fun resume() {
paused = false
}
fun clear() {
Runtime.getRuntime().exec(clearLogCommand)
}
suspend fun run() {
withContext(ioDispatcher) {
paused = false
stopped = false
if (outputStream == null) return@withContext
try { try {
if (outputStream == null) outputStream = createNewLogFileStream()
clear() clear()
logcatProc = Runtime.getRuntime().exec(command) logcatProc = Runtime.getRuntime().exec(command)
reader = BufferedReader(InputStreamReader(logcatProc!!.inputStream), 1024) reader = BufferedReader(InputStreamReader(logcatProc!!.inputStream), 1024)
var line: String? var line: String? = null
while (!stopped) { while (true) {
if (paused) continue
line = reader?.readLine() line = reader?.readLine()
if (line.isNullOrEmpty()) continue if (line.isNullOrEmpty()) continue
outputStream?.let { outputStream?.let {
@@ -196,8 +159,8 @@ object LogcatCollector {
deleteOldestFile() deleteOldestFile()
} }
line.let { text -> line.let { text ->
val obfuscated = obfuscator(text) val sanitized = obfuscator(text)
it.write((obfuscated + System.lineSeparator()).toByteArray()) it.write((sanitized + System.lineSeparator()).toByteArray())
try { try {
val logMessage = LogMessage.from(text) val logMessage = LogMessage.from(text)
_bufferedLogs.tryEmit(logMessage) _bufferedLogs.tryEmit(logMessage)
@@ -214,19 +177,34 @@ object LogcatCollector {
} catch (e: IOException) { } catch (e: IOException) {
Timber.e(e) Timber.e(e)
} finally { } finally {
logcatProc?.destroy() reset()
logcatProc = null
try {
reader?.close()
outputStream?.close()
reader = null
outputStream = null
} catch (e: IOException) {
Timber.e(e)
}
} }
} }
logJob?.invokeOnCompletion {
reset()
}
}
override fun onDestroy(owner: LifecycleOwner) {
super.onDestroy(owner)
logJob?.cancel()
}
fun cancel() {
logJob?.cancel()
}
private fun reset() {
logcatProc?.destroy()
logcatProc = null
reader?.close()
outputStream?.close()
reader = null
outputStream = null
}
fun clear() {
Runtime.getRuntime().exec(clearLogCommand)
} }
private fun getFolderSize(path: String): Long { private fun getFolderSize(path: String): Long {
@@ -266,7 +244,6 @@ object LogcatCollector {
directory.listFiles()?.toMutableList()?.run { directory.listFiles()?.toMutableList()?.run {
this.forEach { it.delete() } this.forEach { it.delete() }
} }
outputStream = createNewLogFileStream()
} }
} }
} }
+1 -1
View File
@@ -1 +1 @@
8 13