Compare commits

..

24 Commits

Author SHA1 Message Date
zaneschepke 186f063e23 chore(deps): bump deps
closes #1327
closes #1326
closes #1328
closes #1329
2026-07-03 04:56:54 -04:00
zaneschepke 2c78e9fcbd refactor: app icons and assets, change nightly launcher color
Remove old lincensee task
2026-07-03 04:43:10 -04:00
zaneschepke 8bdeff515e fix: aboutlibraries r8 crash 2026-07-03 02:56:19 -04:00
zaneschepke fa89715ede fix: disable on captive portal toggle bug
closes #1325
2026-07-03 01:35:50 -04:00
zaneschepke 4a94905893 feat: seamless roaming
Fix for broadcast receivers to goAsync to fix issues on slower devices
Add "seamless roaming" to bind wireguard UDP sockets to the active network to help resolve handshake issues on network transitions
2026-07-03 01:30:20 -04:00
zaneschepke cf184f2042 refactor: remove old license viewmodel 2026-07-01 03:15:58 -04:00
zaneschepke e0ddb8730d refactor: switch license view to aboutlibraries, add native dep licenses 2026-07-01 02:37:00 -04:00
zaneschepke c8c041b872 fix: improve ktor client to prevent download failures from github
Add user agent headers with default request and increased timeout to prevent download denial from GitHub
2026-06-30 00:35:38 -04:00
zaneschepke 7c8adb380b chore: release 5.0.7 2026-06-29 12:56:55 -04:00
zaneschepke 614f97fd14 feat: add disable tunnel on captive portal to auto tunnel
Refactor of network monitoring and auto tunnel logic to adapt to the lest strict network monitoring of active network to support system DNS.

Add disable tunnel on captive portal feature to allow auto disable of vpn while captive portal is not completed.
2026-06-29 12:08:34 -04:00
zaneschepke fbd470f5d2 fix: make network monitor less strict for network capabilities
Network monitor was too strict with capability checks, impacting our DNS resolver which needs to bind to underlying network. Capabilities have been separated out into a separate state property so we always pass the active network to connectivity state for system dns

Improve system dns by supporting DnsResolver on modern devices

#1270
2026-06-28 13:41:08 -04:00
zaneschepke 5f89b2ed31 refactor: improve mobile network detection, cleanup network monitor
#1270
2026-06-28 04:52:41 -04:00
zaneschepke 9503a3284b fix: kill switch should restore properly on tunnel up if it was killed by system or another app
closes #1313
2026-06-28 02:52:24 -04:00
zaneschepke 68c1a19bd3 fix: remove ipv6 address from lockdown causing routing issues ipv4 only tunnels 2026-06-28 02:15:45 -04:00
zaneschepke f3bb6667c3 fix: private dns to use network bind, bootstrap custom with system dns
closes #1312
closes #1311

#1303
#1270
2026-06-27 20:06:06 -04:00
zaneschepke 244a990c37 fix: ddns job logic, respect user dns setting for DDNS with default fallback if cache suspected
#1312
#1303
2026-06-27 03:57:18 -04:00
zaneschepke cbf07600b4 fix: ddns checking logic, force well known DoH to bypass system dns cache
#1303
2026-06-26 12:47:38 -04:00
zaneschepke ec8d90d13d chore: bump ktor and leakcanary
closes #1309
closes #1308
2026-06-26 03:45:38 -04:00
zaneschepke 85acca8604 fix: local network permission dialog theme and wording 2026-06-26 03:36:59 -04:00
zaneschepke 0a9773d202 chore: release 5.0.6 2026-06-25 13:10:15 -04:00
zaneschepke 3cb4480a65 fix: android 17 local devices/network permission requirement
closes #1299
2026-06-25 04:41:18 -04:00
zaneschepke a7f3255a76 refactor: remove legacy round icons 2026-06-25 02:32:03 -04:00
zaneschepke 7d7b99f448 fix: quick tile logo for samsung OneUI
#1301
2026-06-25 01:12:40 -04:00
zaneschepke 74e9e462bb fix: app shortcuts crash
closes #1302
2026-06-24 02:11:02 -04:00
144 changed files with 2508 additions and 890 deletions
+17 -17
View File
@@ -8,6 +8,7 @@ plugins {
alias(libs.plugins.compose.compiler) alias(libs.plugins.compose.compiler)
alias(libs.plugins.grgit) alias(libs.plugins.grgit)
alias(libs.plugins.licensee) alias(libs.plugins.licensee)
alias(libs.plugins.aboutlibraries)
} }
ksp { ksp {
@@ -17,9 +18,15 @@ ksp {
licensee { licensee {
allowedLicenses().forEach { allow(it) } allowedLicenses().forEach { allow(it) }
allowedLicenseUrls().forEach { allowUrl(it) } allowedLicenseUrls().forEach { allowUrl(it) }
// foss, but missing licenses allowDependency("com.github.T8RIN.QuickieExtended", "quickie-foss", "1.18.1") {
ignoreDependencies("com.github.T8RIN.QuickieExtended") because("FOSS library, but JitPack doesn't publish license metadata")
ignoreDependencies("com.github.topjohnwu.libsu") allow("Apache-2.0")
}
allowDependency("com.github.topjohnwu.libsu", "core", "6.0.0") {
because("FOSS library, but JitPack doesn't publish license metadata")
allow("Apache-2.0")
}
} }
configure<ApplicationExtension> { configure<ApplicationExtension> {
@@ -94,21 +101,24 @@ configure<ApplicationExtension> {
"proguard-rules.pro", "proguard-rules.pro",
) )
signingConfig = signingConfigs.getByName(Constants.RELEASE) signingConfig = signingConfigs.getByName(Constants.RELEASE)
resValue("string", "provider", "\"${Constants.APP_NAME}.provider\"") manifestPlaceholders["providerAuthority"] = "${Constants.APP_NAME}.provider"
buildConfigField("String", "FILE_PROVIDER_AUTHORITY", "\"${Constants.APP_NAME}.provider\"")
} }
debug { debug {
applicationIdSuffix = ".debug" applicationIdSuffix = ".debug"
resValue("string", "app_name", "WG Tunnel Debug") resValue("string", "app_name", "WG Tunnel Debug")
isDebuggable = true isDebuggable = true
resValue("string", "provider", "\"${Constants.APP_NAME}.provider.debug\"") manifestPlaceholders["providerAuthority"] = "${Constants.APP_NAME}.provider.debug"
buildConfigField("String", "FILE_PROVIDER_AUTHORITY", "\"${Constants.APP_NAME}.provider.debug\"")
} }
create(Constants.NIGHTLY) { create(Constants.NIGHTLY) {
initWith(buildTypes.getByName(Constants.RELEASE)) initWith(buildTypes.getByName(Constants.RELEASE))
applicationIdSuffix = ".nightly" applicationIdSuffix = ".nightly"
resValue("string", "app_name", "WG Tunnel Nightly") resValue("string", "app_name", "WG Tunnel Nightly")
resValue("string", "provider", "\"${Constants.APP_NAME}.provider.nightly\"") manifestPlaceholders["providerAuthority"] = "${Constants.APP_NAME}.provider.nightly"
buildConfigField("String", "FILE_PROVIDER_AUTHORITY", "\"${Constants.APP_NAME}.provider.nightly\"")
} }
} }
@@ -231,6 +241,7 @@ dependencies {
implementation(libs.bundles.ui.utilities) implementation(libs.bundles.ui.utilities)
implementation(libs.lottie.compose) implementation(libs.lottie.compose)
implementation(libs.sonner) implementation(libs.sonner)
implementation(libs.aboutlibraries.compose)
// Misc utilities // Misc utilities
implementation(libs.bundles.misc.utilities) implementation(libs.bundles.misc.utilities)
@@ -271,17 +282,6 @@ dependencies {
implementation(libs.koin.worker) implementation(libs.koin.worker)
} }
tasks.register<Copy>("copyLicenseeJsonToAssets") {
dependsOn("licensee")
val outputAssets = layout.projectDirectory.dir("src/main/assets")
from(layout.buildDirectory.file("reports/licensee/androidFdroidRelease/artifacts.json")) {
rename("artifacts.json", "licenses.json")
}
into(outputAssets)
}
tasks.named("preBuild") { dependsOn("copyLicenseeJsonToAssets") }
// https://gist.github.com/obfusk/61046e09cee352ae6dd109911534b12e#fix-proposed-by-linsui-disable-baseline-profiles // https://gist.github.com/obfusk/61046e09cee352ae6dd109911534b12e#fix-proposed-by-linsui-disable-baseline-profiles
tasks.configureEach { tasks.configureEach {
if (name.contains("ArtProfile")) { if (name.contains("ArtProfile")) {
@@ -0,0 +1,513 @@
{
"formatVersion": 1,
"database": {
"version": 31,
"identityHash": "1dee3799f1c6526c48723fd2fee58d11",
"entities": [
{
"tableName": "tunnel_config",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` 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, `quick_config` TEXT NOT NULL DEFAULT '', `dynamic_dns` INTEGER NOT NULL DEFAULT false, `is_ethernet_tunnel` INTEGER NOT NULL DEFAULT false, `prefer_ipv6` INTEGER NOT NULL DEFAULT false, `position` INTEGER NOT NULL DEFAULT 0, `auto_tunnel_apps` TEXT NOT NULL DEFAULT '[]', `is_metered` INTEGER NOT NULL DEFAULT false, `ipv4_fallback` INTEGER NOT NULL DEFAULT false, `ipv6_restore` INTEGER NOT NULL DEFAULT false)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"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": "quickConfig",
"columnName": "quick_config",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "dynamicDnsEnabled",
"columnName": "dynamic_dns",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isEthernetTunnel",
"columnName": "is_ethernet_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isIpv6Preferred",
"columnName": "prefer_ipv6",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "autoTunnelApps",
"columnName": "auto_tunnel_apps",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'[]'"
},
{
"fieldPath": "isMetered",
"columnName": "is_metered",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "ipv4FallbackEnabled",
"columnName": "ipv4_fallback",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "ipv6RestoreEnabled",
"columnName": "ipv6_restore",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_tunnel_config_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_tunnel_config_name` ON `${TABLE_NAME}` (`name`)"
}
]
},
{
"tableName": "proxy_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `socks5_proxy_enabled` INTEGER NOT NULL DEFAULT 0, `socks5_proxy_bind_address` TEXT, `http_proxy_enable` INTEGER NOT NULL DEFAULT 0, `http_proxy_bind_address` TEXT, `proxy_username` TEXT, `proxy_password` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "socks5ProxyEnabled",
"columnName": "socks5_proxy_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "socks5ProxyBindAddress",
"columnName": "socks5_proxy_bind_address",
"affinity": "TEXT"
},
{
"fieldPath": "httpProxyEnabled",
"columnName": "http_proxy_enable",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "httpProxyBindAddress",
"columnName": "http_proxy_bind_address",
"affinity": "TEXT"
},
{
"fieldPath": "proxyUsername",
"columnName": "proxy_username",
"affinity": "TEXT"
},
{
"fieldPath": "proxyPassword",
"columnName": "proxy_password",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "general_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT 0, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT 0, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `global_split_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `app_mode` INTEGER NOT NULL DEFAULT 0, `theme` TEXT NOT NULL DEFAULT 'AUTOMATIC', `locale` TEXT, `remote_key` TEXT, `is_remote_control_enabled` INTEGER NOT NULL DEFAULT 0, `is_pin_lock_enabled` INTEGER NOT NULL DEFAULT 0, `is_always_on_vpn_enabled` INTEGER NOT NULL DEFAULT 0, `already_donated` INTEGER NOT NULL DEFAULT 0, `screen_recording_security` INTEGER NOT NULL DEFAULT 1, `global_amnezia_enabled` INTEGER NOT NULL DEFAULT 0, `tunnel_scripting_enabled` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isShortcutsEnabled",
"columnName": "is_shortcuts_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isRestoreOnBootEnabled",
"columnName": "is_restore_on_boot_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isMultiTunnelEnabled",
"columnName": "is_multi_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isGlobalSplitTunnelEnabled",
"columnName": "global_split_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "tunnelMode",
"columnName": "app_mode",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "theme",
"columnName": "theme",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'AUTOMATIC'"
},
{
"fieldPath": "locale",
"columnName": "locale",
"affinity": "TEXT"
},
{
"fieldPath": "remoteKey",
"columnName": "remote_key",
"affinity": "TEXT"
},
{
"fieldPath": "isRemoteControlEnabled",
"columnName": "is_remote_control_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isPinLockEnabled",
"columnName": "is_pin_lock_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isAlwaysOnVpnEnabled",
"columnName": "is_always_on_vpn_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "alreadyDonated",
"columnName": "already_donated",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "screenRecordingSecurityEnabled",
"columnName": "screen_recording_security",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "1"
},
{
"fieldPath": "isGlobalAmneziaEnabled",
"columnName": "global_amnezia_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "tunnelScriptingEnabled",
"columnName": "tunnel_scripting_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "auto_tunnel_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL DEFAULT 0, `trusted_network_ssids` TEXT NOT NULL DEFAULT '', `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT 0, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT 0, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_unsecure_enabled` INTEGER NOT NULL DEFAULT 0, `wifi_detection_method` INTEGER NOT NULL DEFAULT 0, `start_on_boot` INTEGER NOT NULL DEFAULT 0, `disable_on_captive_portal` INTEGER NOT NULL DEFAULT 1)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isAutoTunnelEnabled",
"columnName": "is_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isTunnelOnMobileDataEnabled",
"columnName": "is_tunnel_on_mobile_data_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "trustedNetworkSSIDs",
"columnName": "trusted_network_ssids",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isTunnelOnEthernetEnabled",
"columnName": "is_tunnel_on_ethernet_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isTunnelOnWifiEnabled",
"columnName": "is_tunnel_on_wifi_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isWildcardsEnabled",
"columnName": "is_wildcards_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isStopOnNoInternetEnabled",
"columnName": "is_stop_on_no_internet_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isTunnelOnUnsecureEnabled",
"columnName": "is_tunnel_on_unsecure_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "wifiDetectionMethod",
"columnName": "wifi_detection_method",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "startOnBoot",
"columnName": "start_on_boot",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "disableTunnelOnCaptivePortal",
"columnName": "disable_on_captive_portal",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "1"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "monitoring_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_local_logs_enabled` INTEGER NOT NULL DEFAULT 0, `tunnel_statistics_enabled` INTEGER NOT NULL DEFAULT 1, `tunnel_statistics_poll_interval` INTEGER NOT NULL DEFAULT 3)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isLocalLogsEnabled",
"columnName": "is_local_logs_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "tunnelStatisticsEnabled",
"columnName": "tunnel_statistics_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "1"
},
{
"fieldPath": "tunnelStatisticsPollInterval",
"columnName": "tunnel_statistics_poll_interval",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "3"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "dns_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `dns_protocol` INTEGER NOT NULL DEFAULT 0, `dns_endpoint` TEXT, `global_tunnel_dns_enabled` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dnsProtocol",
"columnName": "dns_protocol",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "dnsEndpoint",
"columnName": "dns_endpoint",
"affinity": "TEXT"
},
{
"fieldPath": "isGlobalTunnelDnsEnabled",
"columnName": "global_tunnel_dns_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "lockdown_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `bypass_lan` INTEGER NOT NULL DEFAULT 0, `metered` INTEGER NOT NULL DEFAULT 0, `dual_stack` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "bypassLan",
"columnName": "bypass_lan",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "metered",
"columnName": "metered",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "dualStack",
"columnName": "dual_stack",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
}
],
"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, '1dee3799f1c6526c48723fd2fee58d11')"
]
}
}
@@ -0,0 +1,520 @@
{
"formatVersion": 1,
"database": {
"version": 32,
"identityHash": "fd4803fc483f41704303be9246dcfb4d",
"entities": [
{
"tableName": "tunnel_config",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` 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, `quick_config` TEXT NOT NULL DEFAULT '', `dynamic_dns` INTEGER NOT NULL DEFAULT false, `is_ethernet_tunnel` INTEGER NOT NULL DEFAULT false, `prefer_ipv6` INTEGER NOT NULL DEFAULT false, `position` INTEGER NOT NULL DEFAULT 0, `auto_tunnel_apps` TEXT NOT NULL DEFAULT '[]', `is_metered` INTEGER NOT NULL DEFAULT false, `ipv4_fallback` INTEGER NOT NULL DEFAULT false, `ipv6_restore` INTEGER NOT NULL DEFAULT false)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"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": "quickConfig",
"columnName": "quick_config",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "dynamicDnsEnabled",
"columnName": "dynamic_dns",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isEthernetTunnel",
"columnName": "is_ethernet_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isIpv6Preferred",
"columnName": "prefer_ipv6",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "autoTunnelApps",
"columnName": "auto_tunnel_apps",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'[]'"
},
{
"fieldPath": "isMetered",
"columnName": "is_metered",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "ipv4FallbackEnabled",
"columnName": "ipv4_fallback",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "ipv6RestoreEnabled",
"columnName": "ipv6_restore",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_tunnel_config_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_tunnel_config_name` ON `${TABLE_NAME}` (`name`)"
}
]
},
{
"tableName": "proxy_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `socks5_proxy_enabled` INTEGER NOT NULL DEFAULT 0, `socks5_proxy_bind_address` TEXT, `http_proxy_enable` INTEGER NOT NULL DEFAULT 0, `http_proxy_bind_address` TEXT, `proxy_username` TEXT, `proxy_password` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "socks5ProxyEnabled",
"columnName": "socks5_proxy_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "socks5ProxyBindAddress",
"columnName": "socks5_proxy_bind_address",
"affinity": "TEXT"
},
{
"fieldPath": "httpProxyEnabled",
"columnName": "http_proxy_enable",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "httpProxyBindAddress",
"columnName": "http_proxy_bind_address",
"affinity": "TEXT"
},
{
"fieldPath": "proxyUsername",
"columnName": "proxy_username",
"affinity": "TEXT"
},
{
"fieldPath": "proxyPassword",
"columnName": "proxy_password",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "general_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT 0, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT 0, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `global_split_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `app_mode` INTEGER NOT NULL DEFAULT 0, `theme` TEXT NOT NULL DEFAULT 'AUTOMATIC', `locale` TEXT, `remote_key` TEXT, `is_remote_control_enabled` INTEGER NOT NULL DEFAULT 0, `is_pin_lock_enabled` INTEGER NOT NULL DEFAULT 0, `is_always_on_vpn_enabled` INTEGER NOT NULL DEFAULT 0, `already_donated` INTEGER NOT NULL DEFAULT 0, `screen_recording_security` INTEGER NOT NULL DEFAULT 1, `global_amnezia_enabled` INTEGER NOT NULL DEFAULT 0, `tunnel_scripting_enabled` INTEGER NOT NULL DEFAULT 0, `seamless_roaming_enabled` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isShortcutsEnabled",
"columnName": "is_shortcuts_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isRestoreOnBootEnabled",
"columnName": "is_restore_on_boot_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isMultiTunnelEnabled",
"columnName": "is_multi_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isGlobalSplitTunnelEnabled",
"columnName": "global_split_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "tunnelMode",
"columnName": "app_mode",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "theme",
"columnName": "theme",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'AUTOMATIC'"
},
{
"fieldPath": "locale",
"columnName": "locale",
"affinity": "TEXT"
},
{
"fieldPath": "remoteKey",
"columnName": "remote_key",
"affinity": "TEXT"
},
{
"fieldPath": "isRemoteControlEnabled",
"columnName": "is_remote_control_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isPinLockEnabled",
"columnName": "is_pin_lock_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isAlwaysOnVpnEnabled",
"columnName": "is_always_on_vpn_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "alreadyDonated",
"columnName": "already_donated",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "screenRecordingSecurityEnabled",
"columnName": "screen_recording_security",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "1"
},
{
"fieldPath": "isGlobalAmneziaEnabled",
"columnName": "global_amnezia_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "tunnelScriptingEnabled",
"columnName": "tunnel_scripting_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "seamlessRoamingEnabled",
"columnName": "seamless_roaming_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "auto_tunnel_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL DEFAULT 0, `trusted_network_ssids` TEXT NOT NULL DEFAULT '', `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT 0, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT 0, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_unsecure_enabled` INTEGER NOT NULL DEFAULT 0, `wifi_detection_method` INTEGER NOT NULL DEFAULT 0, `start_on_boot` INTEGER NOT NULL DEFAULT 0, `disable_on_captive_portal` INTEGER NOT NULL DEFAULT 1)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isAutoTunnelEnabled",
"columnName": "is_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isTunnelOnMobileDataEnabled",
"columnName": "is_tunnel_on_mobile_data_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "trustedNetworkSSIDs",
"columnName": "trusted_network_ssids",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isTunnelOnEthernetEnabled",
"columnName": "is_tunnel_on_ethernet_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isTunnelOnWifiEnabled",
"columnName": "is_tunnel_on_wifi_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isWildcardsEnabled",
"columnName": "is_wildcards_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isStopOnNoInternetEnabled",
"columnName": "is_stop_on_no_internet_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isTunnelOnUnsecureEnabled",
"columnName": "is_tunnel_on_unsecure_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "wifiDetectionMethod",
"columnName": "wifi_detection_method",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "startOnBoot",
"columnName": "start_on_boot",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "disableTunnelOnCaptivePortal",
"columnName": "disable_on_captive_portal",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "1"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "monitoring_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_local_logs_enabled` INTEGER NOT NULL DEFAULT 0, `tunnel_statistics_enabled` INTEGER NOT NULL DEFAULT 1, `tunnel_statistics_poll_interval` INTEGER NOT NULL DEFAULT 3)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isLocalLogsEnabled",
"columnName": "is_local_logs_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "tunnelStatisticsEnabled",
"columnName": "tunnel_statistics_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "1"
},
{
"fieldPath": "tunnelStatisticsPollInterval",
"columnName": "tunnel_statistics_poll_interval",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "3"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "dns_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `dns_protocol` INTEGER NOT NULL DEFAULT 0, `dns_endpoint` TEXT, `global_tunnel_dns_enabled` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dnsProtocol",
"columnName": "dns_protocol",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "dnsEndpoint",
"columnName": "dns_endpoint",
"affinity": "TEXT"
},
{
"fieldPath": "isGlobalTunnelDnsEnabled",
"columnName": "global_tunnel_dns_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "lockdown_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `bypass_lan` INTEGER NOT NULL DEFAULT 0, `metered` INTEGER NOT NULL DEFAULT 0, `dual_stack` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "bypassLan",
"columnName": "bypass_lan",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "metered",
"columnName": "metered",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "dualStack",
"columnName": "dual_stack",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
}
],
"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, 'fd4803fc483f41704303be9246dcfb4d')"
]
}
}
+3 -4
View File
@@ -53,7 +53,6 @@
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.App.Start" android:theme="@style/Theme.App.Start"
tools:targetApi="tiramisu"> tools:targetApi="tiramisu">
@@ -157,7 +156,7 @@
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="@string/provider" android:authorities="${providerAuthority}"
android:exported="false" android:exported="false"
android:grantUriPermissions="true"> android:grantUriPermissions="true">
<meta-data <meta-data
@@ -178,7 +177,7 @@
<service <service
android:name=".service.tile.TunnelControlTile" android:name=".service.tile.TunnelControlTile"
android:exported="true" android:exported="true"
android:icon="@drawable/ic_notification" android:icon="@drawable/qs_logo"
android:label="@string/tunnel_control" android:label="@string/tunnel_control"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"> android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<meta-data <meta-data
@@ -195,7 +194,7 @@
<service <service
android:name=".service.tile.AutoTunnelControlTile" android:name=".service.tile.AutoTunnelControlTile"
android:exported="true" android:exported="true"
android:icon="@drawable/ic_notification" android:icon="@drawable/qs_logo"
android:label="@string/auto_tunnel" android:label="@string/auto_tunnel"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"> android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<meta-data <meta-data
View File
@@ -1,12 +1,14 @@
package com.zaneschepke.wireguardautotunnel package com.zaneschepke.wireguardautotunnel
import ProxySettingsScreen import ProxySettingsScreen
import android.Manifest
import android.content.Intent import android.content.Intent
import android.graphics.Color import android.graphics.Color
import android.net.Uri import android.net.Uri
import android.net.VpnService import android.net.VpnService
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.Settings
import android.view.WindowManager import android.view.WindowManager
import androidx.activity.SystemBarStyle import androidx.activity.SystemBarStyle
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
@@ -50,6 +52,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -61,6 +64,7 @@ import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import androidx.core.app.ActivityCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
@@ -83,6 +87,7 @@ import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.banner.AppAlertBanner import com.zaneschepke.wireguardautotunnel.ui.common.banner.AppAlertBanner
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.LocalNetworkPermissionDialog
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.VpnDeniedDialog import com.zaneschepke.wireguardautotunnel.ui.common.dialog.VpnDeniedDialog
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
import com.zaneschepke.wireguardautotunnel.ui.navigation.SecureRoute import com.zaneschepke.wireguardautotunnel.ui.navigation.SecureRoute
@@ -132,6 +137,7 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.installApk
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.restartApp import com.zaneschepke.wireguardautotunnel.util.extensions.restartApp
import com.zaneschepke.wireguardautotunnel.util.permission.LocalNetworkPermissionHelper
import com.zaneschepke.wireguardautotunnel.viewmodel.ConfigEditViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.ConfigEditViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.SplitTunnelViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.SplitTunnelViewModel
@@ -202,6 +208,48 @@ class MainActivity : AppCompatActivity() {
var requestingTunnelMode by remember { var requestingTunnelMode by remember {
mutableStateOf<Pair<TunnelMode?, TunnelConfig?>>(Pair(null, null)) mutableStateOf<Pair<TunnelMode?, TunnelConfig?>>(Pair(null, null))
} }
var showLocalNetworkRationale by remember { mutableStateOf(false) }
var hasPromptedLocalNetwork by rememberSaveable { mutableStateOf(false) }
val localNetworkPermissionLauncher =
rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestPermission()
) { isGranted ->
if (!isGranted) {
val canAskAgain =
ActivityCompat.shouldShowRequestPermissionRationale(
this,
Manifest.permission.ACCESS_LOCAL_NETWORK,
)
if (!canAskAgain) {
val intent =
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", packageName, null)
}
startActivity(intent)
} else {
toaster.show(
message =
context.getString(R.string.local_network_permission_denied),
type = ToastType.Warning,
duration = 6000.milliseconds,
)
}
}
}
LaunchedEffect(uiState.isAppLoaded) {
if (
uiState.isAppLoaded &&
!hasPromptedLocalNetwork &&
LocalNetworkPermissionHelper.shouldRequestPermission() &&
!LocalNetworkPermissionHelper.isPermissionGranted(context)
) {
hasPromptedLocalNetwork = true
showLocalNetworkRationale = true
}
}
val startingStack = buildList { val startingStack = buildList {
add(Route.Tunnels) add(Route.Tunnels)
@@ -292,6 +340,27 @@ class MainActivity : AppCompatActivity() {
}, },
) )
if (showLocalNetworkRationale) {
LocalNetworkPermissionDialog(
onDismiss = {
showLocalNetworkRationale = false
toaster.show(
message =
context.getString(R.string.local_network_permission_denied),
type = ToastType.Warning,
duration = 6000.milliseconds,
)
},
onAttest = {
showLocalNetworkRationale = false
localNetworkPermissionLauncher.launch(
Manifest.permission.ACCESS_LOCAL_NETWORK
)
},
)
}
uiState.pendingWgImportUrl?.let { url -> uiState.pendingWgImportUrl?.let { url ->
val host = Uri.parse(url).host ?: url val host = Uri.parse(url).host ?: url
InfoDialog( InfoDialog(
@@ -48,11 +48,12 @@ class RemoteControlReceiver : BroadcastReceiver(), KoinComponent {
} }
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
val pendingResult = goAsync()
val action = intent.action ?: return
val appAction = Action.fromAction(action) ?: return
applicationScope.launch { applicationScope.launch {
try {
val action = intent.action ?: return@launch
val appAction = Action.fromAction(action) ?: return@launch
val settings = settingsRepository.getGeneralSettings() val settings = settingsRepository.getGeneralSettings()
if (!settings.isRemoteControlEnabled) return@launch if (!settings.isRemoteControlEnabled) return@launch
@@ -90,6 +91,9 @@ class RemoteControlReceiver : BroadcastReceiver(), KoinComponent {
autoTunnelCoordinator.disable() autoTunnelCoordinator.disable()
} }
} }
} finally {
pendingResult.finish()
}
} }
} }
@@ -27,19 +27,31 @@ class RestartReceiver : BroadcastReceiver(), KoinComponent {
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
Timber.d("RestartReceiver triggered with action: ${intent.action}") Timber.d("RestartReceiver triggered with action: ${intent.action}")
val pendingResult = goAsync()
applicationScope.launch { applicationScope.launch {
try {
when (intent.action) { when (intent.action) {
Intent.ACTION_BOOT_COMPLETED, Intent.ACTION_BOOT_COMPLETED,
"android.intent.action.QUICKBOOT_POWERON", "android.intent.action.QUICKBOOT_POWERON",
"com.htc.intent.action.QUICKBOOT_POWERON" -> { "com.htc.intent.action.QUICKBOOT_POWERON" -> {
startupCoordinator.applyStartupPolicy() startupCoordinator.applyStartupPolicy()
} }
Intent.ACTION_MY_PACKAGE_REPLACED -> { Intent.ACTION_MY_PACKAGE_REPLACED -> {
Timber.i("Restoring state on package upgrade") Timber.i("Restoring state on package upgrade")
startupCoordinator.applyStartupPolicy() startupCoordinator.applyStartupPolicy()
logReader.deleteAndClearLogs() logReader.deleteAndClearLogs()
appStateRepository.setShouldShowDonationSnackbar(true) appStateRepository.setShouldShowDonationSnackbar(true)
} }
else -> {
Timber.w("Unhandled action in RestartReceiver: ${intent.action}")
}
}
} finally {
pendingResult.finish()
} }
} }
} }
@@ -39,7 +39,7 @@ class AppBoostrapCoordinator(
listOf( listOf(
async { bootstrapDns() }, async { bootstrapDns() },
async { ensureGlobalConfig() }, async { ensureGlobalConfig() },
async { restoreLockdown() }, async { restoreBackendConfiguration() },
) )
try { try {
@@ -73,9 +73,13 @@ class AppBoostrapCoordinator(
tunnelRepository.ensureGlobalConfigExists() tunnelRepository.ensureGlobalConfigExists()
} }
private suspend fun restoreLockdown() { private suspend fun restoreBackendConfiguration() {
val settings = settingsRepository.getGeneralSettings() val settings = settingsRepository.getGeneralSettings()
if (settings.seamlessRoamingEnabled) {
tunnelProvider.setSeamlessRoaming(true)
}
when (settings.tunnelMode) { when (settings.tunnelMode) {
TunnelMode.LOCK_DOWN -> { TunnelMode.LOCK_DOWN -> {
val lockdownSettings = lockdownRepository.getLockdownSettings() val lockdownSettings = lockdownRepository.getLockdownSettings()
@@ -5,7 +5,7 @@ import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.LockdownSettingsRepository import com.zaneschepke.wireguardautotunnel.domain.repository.LockdownSettingsRepository
class TunnelModeCoordinator( class TunnelBackendCoordinator(
private val tunnelProvider: TunnelProvider, private val tunnelProvider: TunnelProvider,
private val settingsRepository: GeneralSettingRepository, private val settingsRepository: GeneralSettingRepository,
private val lockdownRepository: LockdownSettingsRepository, private val lockdownRepository: LockdownSettingsRepository,
@@ -42,7 +42,6 @@ class TunnelModeCoordinator(
when (newMode) { when (newMode) {
TunnelMode.LOCK_DOWN -> { TunnelMode.LOCK_DOWN -> {
val lockdownSettings = lockdownRepository.getLockdownSettings() val lockdownSettings = lockdownRepository.getLockdownSettings()
tunnelProvider.setLockDown(lockdownSettings).getOrThrow() tunnelProvider.setLockDown(lockdownSettings).getOrThrow()
} }
@@ -50,4 +49,9 @@ class TunnelModeCoordinator(
TunnelMode.PROXY -> Unit TunnelMode.PROXY -> Unit
} }
} }
suspend fun changeSeamlessRoaming(enabled: Boolean) {
tunnelProvider.setSeamlessRoaming(enabled).getOrThrow()
settingsRepository.updateSeamlessRoaming(enabled)
}
} }
@@ -9,10 +9,12 @@ import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
import com.zaneschepke.wireguardautotunnel.domain.events.TunnelActionEvent import com.zaneschepke.wireguardautotunnel.domain.events.TunnelActionEvent
import com.zaneschepke.wireguardautotunnel.domain.model.DnsSettings import com.zaneschepke.wireguardautotunnel.domain.model.DnsSettings
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings
import com.zaneschepke.wireguardautotunnel.domain.model.LockdownSettings
import com.zaneschepke.wireguardautotunnel.domain.model.MonitoringSettings import com.zaneschepke.wireguardautotunnel.domain.model.MonitoringSettings
import com.zaneschepke.wireguardautotunnel.domain.model.ProxySettings import com.zaneschepke.wireguardautotunnel.domain.model.ProxySettings
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.LockdownSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.ProxySettingsRepository import com.zaneschepke.wireguardautotunnel.domain.repository.ProxySettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
@@ -44,6 +46,7 @@ class TunnelCoordinator(
dnsSettingsRepository: RoomDnsSettingsRepository, dnsSettingsRepository: RoomDnsSettingsRepository,
monitoringSettingsRepository: MonitoringSettingsRepository, monitoringSettingsRepository: MonitoringSettingsRepository,
proxyRepository: ProxySettingsRepository, proxyRepository: ProxySettingsRepository,
lockdownModeRepository: LockdownSettingsRepository,
scope: CoroutineScope, scope: CoroutineScope,
) { ) {
@@ -66,6 +69,7 @@ class TunnelCoordinator(
val dns: DnsSettings, val dns: DnsSettings,
val monitoring: MonitoringSettings, val monitoring: MonitoringSettings,
val proxy: ProxySettings, val proxy: ProxySettings,
val lockdown: LockdownSettings,
) )
private val runtimeSettingsSnapshot = private val runtimeSettingsSnapshot =
@@ -74,12 +78,14 @@ class TunnelCoordinator(
dnsSettingsRepository.flow, dnsSettingsRepository.flow,
monitoringSettingsRepository.flow, monitoringSettingsRepository.flow,
proxyRepository.flow, proxyRepository.flow,
) { general, dns, monitoring, proxy -> lockdownModeRepository.flow,
) { general, dns, monitoring, proxy, lockdown ->
RuntimeSettingsSnapshot( RuntimeSettingsSnapshot(
general = general, general = general,
dns = dns, dns = dns,
monitoring = monitoring, monitoring = monitoring,
proxy = proxy, proxy = proxy,
lockdown = lockdown,
) )
} }
@@ -117,7 +123,7 @@ class TunnelCoordinator(
// enforce single tunnel, for now // enforce single tunnel, for now
if (backendStatus.value.activeTunnels.isNotEmpty()) { if (backendStatus.value.activeTunnels.isNotEmpty()) {
stopActiveTunnelsInternal() stopActiveTunnelsInternal(source)
} }
startTunnelInternal(config, source) startTunnelInternal(config, source)
@@ -131,7 +137,13 @@ class TunnelCoordinator(
stopTunnelInternal(id, source) stopTunnelInternal(id, source)
} }
suspend fun stopActiveTunnels() = tunnelMutex.withLock { stopActiveTunnelsInternal() } suspend fun stopActiveTunnels(source: TunnelActionSource = TunnelActionSource.USER) =
tunnelMutex.withLock {
if (source == TunnelActionSource.USER) {
_userOverrideFlow.tryEmit(Unit)
}
stopActiveTunnelsInternal(source)
}
private suspend fun startTunnelInternal( private suspend fun startTunnelInternal(
tunnelConfig: TunnelConfig, tunnelConfig: TunnelConfig,
@@ -143,6 +155,7 @@ class TunnelCoordinator(
val dnsSettings = snapshot.dns val dnsSettings = snapshot.dns
val proxySettings = snapshot.proxy val proxySettings = snapshot.proxy
val monitoringSettings = snapshot.monitoring val monitoringSettings = snapshot.monitoring
val lockdownSettings = snapshot.lockdown
val config = tunnelConfig.getConfig() val config = tunnelConfig.getConfig()
val policy = val policy =
@@ -178,8 +191,10 @@ class TunnelCoordinator(
} }
TunnelMode.LOCK_DOWN -> { TunnelMode.LOCK_DOWN -> {
BackendMode.Proxy.KillSwitchPrimary(
BackendMode.Proxy.KillSwitchPrimary(runConfig) runConfig,
lockdownSettings.toKillSwitchConfig(),
)
} }
} }
@@ -218,7 +233,7 @@ class TunnelCoordinator(
_actions.emit(TunnelActionEvent.Stopped(tunnelId = id, source = source)) _actions.emit(TunnelActionEvent.Stopped(tunnelId = id, source = source))
} }
stopActiveTunnelsInternal() stopActiveTunnelsInternal(source)
return@withLock return@withLock
} }
@@ -243,7 +258,15 @@ class TunnelCoordinator(
.onFailure { _errors.emit(TunnelErrorEvent.from(it, id)) } .onFailure { _errors.emit(TunnelErrorEvent.from(it, id)) }
} }
private suspend fun stopActiveTunnelsInternal() { private suspend fun stopActiveTunnelsInternal(
source: TunnelActionSource = TunnelActionSource.USER
) {
val active = tunnelProvider.backendStatus.value.activeTunnels
active.keys.forEach { id ->
_actions.emit(TunnelActionEvent.Stopped(tunnelId = id, source = source))
}
tunnelProvider.stopActiveTunnels() tunnelProvider.stopActiveTunnels()
} }
} }
@@ -19,9 +19,8 @@ class ShortcutsActivity : ComponentActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
applicationScope.launch {
shortcutCoordinator.handle(intent)
finish() finish()
}
applicationScope.launch { shortcutCoordinator.handle(intent) }
} }
} }
@@ -4,14 +4,11 @@ import com.zaneschepke.tunnel.Tunnel
import com.zaneschepke.tunnel.backend.Backend import com.zaneschepke.tunnel.backend.Backend
import com.zaneschepke.tunnel.model.BackendMode import com.zaneschepke.tunnel.model.BackendMode
import com.zaneschepke.tunnel.state.BackendStatus import com.zaneschepke.tunnel.state.BackendStatus
import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException
import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage
import com.zaneschepke.wireguardautotunnel.domain.model.LockdownSettings import com.zaneschepke.wireguardautotunnel.domain.model.LockdownSettings
import kotlin.concurrent.atomics.ExperimentalAtomicApi import kotlin.concurrent.atomics.ExperimentalAtomicApi
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
@@ -53,9 +50,7 @@ class TunnelBackendProvider(
return backend.disableKillSwitch() return backend.disableKillSwitch()
} }
@OptIn(ExperimentalCoroutinesApi::class) override suspend fun setSeamlessRoaming(enabled: Boolean): Result<Unit> {
private val localErrorEvents = MutableSharedFlow<Pair<String?, BackendCoreException>>() return backend.setSeamlessRoaming(enabled)
}
@OptIn(ExperimentalCoroutinesApi::class)
private val localMessageEvents = MutableSharedFlow<Pair<String?, BackendMessage>>()
} }
@@ -20,6 +20,8 @@ interface TunnelProvider {
suspend fun disableLockDown(): Result<Unit> suspend fun disableLockDown(): Result<Unit>
suspend fun setSeamlessRoaming(enabled: Boolean): Result<Unit>
val backendStatus: StateFlow<BackendStatus> val backendStatus: StateFlow<BackendStatus>
val events: Flow<TunnelEvent> val events: Flow<TunnelEvent>
@@ -34,7 +34,7 @@ import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
DnsSettings::class, DnsSettings::class,
LockdownSettings::class, LockdownSettings::class,
], ],
version = 30, version = 32,
autoMigrations = autoMigrations =
[ [
AutoMigration(from = 1, to = 2), AutoMigration(from = 1, to = 2),
@@ -63,6 +63,8 @@ import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
AutoMigration(from = 26, to = 27, spec = GlobalsMigration::class), AutoMigration(from = 26, to = 27, spec = GlobalsMigration::class),
AutoMigration(from = 27, to = 28, spec = DonationMigration::class), AutoMigration(from = 27, to = 28, spec = DonationMigration::class),
AutoMigration(from = 29, to = 30, spec = SingleConfigMigration::class), AutoMigration(from = 29, to = 30, spec = SingleConfigMigration::class),
AutoMigration(from = 30, to = 31),
AutoMigration(from = 31, to = 32),
], ],
exportSchema = true, exportSchema = true,
) )
@@ -18,4 +18,7 @@ interface AutoTunnelSettingsDao {
@Query("UPDATE auto_tunnel_settings SET is_tunnel_enabled = :enabled") @Query("UPDATE auto_tunnel_settings SET is_tunnel_enabled = :enabled")
suspend fun updateAutoTunnelEnabled(enabled: Boolean) suspend fun updateAutoTunnelEnabled(enabled: Boolean)
@Query("UPDATE auto_tunnel_settings SET disable_on_captive_portal = :enabled")
suspend fun updateDisableOnCaptivePortal(enabled: Boolean)
} }
@@ -34,4 +34,7 @@ interface GeneralSettingsDao {
@Query("UPDATE general_settings SET screen_recording_security = :enabled") @Query("UPDATE general_settings SET screen_recording_security = :enabled")
suspend fun updateScreenRecordingSecurity(enabled: Boolean) suspend fun updateScreenRecordingSecurity(enabled: Boolean)
@Query("UPDATE general_settings SET seamless_roaming_enabled = :enabled")
suspend fun updateSeamlessRoaming(enabled: Boolean)
} }
@@ -27,4 +27,6 @@ data class AutoTunnelSettings(
@ColumnInfo(name = "wifi_detection_method", defaultValue = "0") @ColumnInfo(name = "wifi_detection_method", defaultValue = "0")
val wifiDetectionMethod: WifiDetectionMethod = WifiDetectionMethod.fromValue(0), val wifiDetectionMethod: WifiDetectionMethod = WifiDetectionMethod.fromValue(0),
@ColumnInfo(name = "start_on_boot", defaultValue = "0") val startOnBoot: Boolean = false, @ColumnInfo(name = "start_on_boot", defaultValue = "0") val startOnBoot: Boolean = false,
@ColumnInfo(name = "disable_on_captive_portal", defaultValue = "1")
val disableTunnelOnCaptivePortal: Boolean = true,
) )
@@ -34,4 +34,6 @@ data class GeneralSettings(
val isGlobalAmneziaEnabled: Boolean = false, val isGlobalAmneziaEnabled: Boolean = false,
@ColumnInfo(name = "tunnel_scripting_enabled", defaultValue = "0") @ColumnInfo(name = "tunnel_scripting_enabled", defaultValue = "0")
val tunnelScriptingEnabled: Boolean = true, val tunnelScriptingEnabled: Boolean = true,
@ColumnInfo(name = "seamless_roaming_enabled", defaultValue = "0")
val seamlessRoamingEnabled: Boolean = true,
) )
@@ -16,6 +16,7 @@ fun Entity.toDomain(): Domain =
isTunnelOnUnsecureEnabled = isTunnelOnUnsecureEnabled, isTunnelOnUnsecureEnabled = isTunnelOnUnsecureEnabled,
wifiDetectionMethod = wifiDetectionMethod, wifiDetectionMethod = wifiDetectionMethod,
startOnBoot = startOnBoot, startOnBoot = startOnBoot,
disableTunnelOnCaptivePortal = disableTunnelOnCaptivePortal
) )
fun Domain.toEntity(): Entity = fun Domain.toEntity(): Entity =
@@ -31,4 +32,5 @@ fun Domain.toEntity(): Entity =
isTunnelOnUnsecureEnabled = isTunnelOnUnsecureEnabled, isTunnelOnUnsecureEnabled = isTunnelOnUnsecureEnabled,
wifiDetectionMethod = wifiDetectionMethod, wifiDetectionMethod = wifiDetectionMethod,
startOnBoot = startOnBoot, startOnBoot = startOnBoot,
disableTunnelOnCaptivePortal = disableTunnelOnCaptivePortal
) )
@@ -22,6 +22,7 @@ fun Entity.toDomain(): Domain =
screenRecordingSecurityEnabled = screenRecordingSecurityEnabled, screenRecordingSecurityEnabled = screenRecordingSecurityEnabled,
isGlobalAmneziaEnabled = isGlobalAmneziaEnabled, isGlobalAmneziaEnabled = isGlobalAmneziaEnabled,
tunnelScriptingEnabled = tunnelScriptingEnabled, tunnelScriptingEnabled = tunnelScriptingEnabled,
seamlessRoamingEnabled = seamlessRoamingEnabled,
) )
fun Domain.toEntity(): Entity = fun Domain.toEntity(): Entity =
@@ -42,4 +43,5 @@ fun Domain.toEntity(): Entity =
screenRecordingSecurityEnabled = screenRecordingSecurityEnabled, screenRecordingSecurityEnabled = screenRecordingSecurityEnabled,
isGlobalAmneziaEnabled = isGlobalAmneziaEnabled, isGlobalAmneziaEnabled = isGlobalAmneziaEnabled,
tunnelScriptingEnabled = tunnelScriptingEnabled, tunnelScriptingEnabled = tunnelScriptingEnabled,
seamlessRoamingEnabled = seamlessRoamingEnabled,
) )
@@ -1,15 +1,25 @@
package com.zaneschepke.wireguardautotunnel.data.network package com.zaneschepke.wireguardautotunnel.data.network
import com.zaneschepke.wireguardautotunnel.BuildConfig
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.engine.okhttp.OkHttp import io.ktor.client.engine.okhttp.OkHttp
import io.ktor.client.plugins.DefaultRequest
import io.ktor.client.plugins.HttpTimeout import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.http.HttpHeaders
import io.ktor.serialization.kotlinx.json.json import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
object KtorClient { object KtorClient {
fun create(): HttpClient { fun create(): HttpClient {
return HttpClient(OkHttp) { return HttpClient(OkHttp) {
install(DefaultRequest) {
headers {
append(HttpHeaders.UserAgent, "wgtunnel/${BuildConfig.VERSION_NAME} (Android)")
append(HttpHeaders.Accept, "*/*")
}
}
install(ContentNegotiation) { install(ContentNegotiation) {
json( json(
Json { Json {
@@ -18,10 +28,11 @@ object KtorClient {
} }
) )
} }
install(HttpTimeout) { install(HttpTimeout) {
requestTimeoutMillis = 15000 requestTimeoutMillis = 120_000L
connectTimeoutMillis = 15000 connectTimeoutMillis = 30_000L
socketTimeoutMillis = 15000 socketTimeoutMillis = 120_000L
} }
} }
} }
@@ -26,4 +26,8 @@ class RoomAutoTunnelSettingsRepository(private val autoTunnelSettingsDao: AutoTu
override suspend fun updateAutoTunnelEnabled(enabled: Boolean) { override suspend fun updateAutoTunnelEnabled(enabled: Boolean) {
autoTunnelSettingsDao.updateAutoTunnelEnabled(enabled) autoTunnelSettingsDao.updateAutoTunnelEnabled(enabled)
} }
override suspend fun updateDisableOnCaptivePortal(enabled: Boolean) {
autoTunnelSettingsDao.updateDisableOnCaptivePortal(enabled)
}
} }
@@ -45,4 +45,8 @@ class RoomSettingsRepository(private val settingsDao: GeneralSettingsDao) :
override suspend fun updateScreenRecordingSecurity(enabled: Boolean) { override suspend fun updateScreenRecordingSecurity(enabled: Boolean) {
settingsDao.updateScreenRecordingSecurity(enabled) settingsDao.updateScreenRecordingSecurity(enabled)
} }
override suspend fun updateSeamlessRoaming(enabled: Boolean) {
settingsDao.updateSeamlessRoaming(enabled)
}
} }
@@ -19,7 +19,6 @@ import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils
import com.zaneschepke.wireguardautotunnel.viewmodel.AutoTunnelViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.AutoTunnelViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.ConfigEditViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.ConfigEditViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.DnsViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.DnsViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.LicenseViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.LockdownViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.LockdownViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.LoggerViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.LoggerViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.MonitoringViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.MonitoringViewModel
@@ -80,7 +79,6 @@ val appModule = module {
viewModelOf(::AutoTunnelViewModel) viewModelOf(::AutoTunnelViewModel)
viewModel { (id: Int?) -> ConfigEditViewModel(get(), get(), get(), get(), get(), id) } viewModel { (id: Int?) -> ConfigEditViewModel(get(), get(), get(), get(), get(), id) }
viewModelOf(::DnsViewModel) viewModelOf(::DnsViewModel)
viewModelOf(::LicenseViewModel)
viewModelOf(::LockdownViewModel) viewModelOf(::LockdownViewModel)
viewModelOf(::LoggerViewModel) viewModelOf(::LoggerViewModel)
viewModelOf(::MonitoringViewModel) viewModelOf(::MonitoringViewModel)
@@ -5,15 +5,15 @@ import com.zaneschepke.wireguardautotunnel.core.orchestration.AutoTunnelCoordina
import com.zaneschepke.wireguardautotunnel.core.orchestration.DnsSettingsCoordinator import com.zaneschepke.wireguardautotunnel.core.orchestration.DnsSettingsCoordinator
import com.zaneschepke.wireguardautotunnel.core.orchestration.ShortcutCoordinator import com.zaneschepke.wireguardautotunnel.core.orchestration.ShortcutCoordinator
import com.zaneschepke.wireguardautotunnel.core.orchestration.StartupCoordinator import com.zaneschepke.wireguardautotunnel.core.orchestration.StartupCoordinator
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelBackendCoordinator
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelModeCoordinator
import org.koin.core.module.dsl.singleOf import org.koin.core.module.dsl.singleOf
import org.koin.core.qualifier.named import org.koin.core.qualifier.named
import org.koin.dsl.module import org.koin.dsl.module
val coordinatorModule = module { val coordinatorModule = module {
singleOf(::ShortcutCoordinator) singleOf(::ShortcutCoordinator)
singleOf(::TunnelModeCoordinator) singleOf(::TunnelBackendCoordinator)
singleOf(::StartupCoordinator) singleOf(::StartupCoordinator)
singleOf(::AutoTunnelCoordinator) singleOf(::AutoTunnelCoordinator)
singleOf(::DnsSettingsCoordinator) singleOf(::DnsSettingsCoordinator)
@@ -27,6 +27,7 @@ val coordinatorModule = module {
get(), get(),
get(), get(),
get(), get(),
get(),
get(named(Scope.APPLICATION)), get(named(Scope.APPLICATION)),
) )
} }
@@ -14,4 +14,5 @@ data class AutoTunnelSettings(
val isTunnelOnUnsecureEnabled: Boolean = false, val isTunnelOnUnsecureEnabled: Boolean = false,
val wifiDetectionMethod: WifiDetectionMethod = WifiDetectionMethod.fromValue(0), val wifiDetectionMethod: WifiDetectionMethod = WifiDetectionMethod.fromValue(0),
val startOnBoot: Boolean = false, val startOnBoot: Boolean = false,
val disableTunnelOnCaptivePortal: Boolean = true,
) )
@@ -20,5 +20,6 @@ data class GeneralSettings(
val alreadyDonated: Boolean = false, val alreadyDonated: Boolean = false,
val screenRecordingSecurityEnabled: Boolean = true, val screenRecordingSecurityEnabled: Boolean = true,
val isGlobalAmneziaEnabled: Boolean = false, val isGlobalAmneziaEnabled: Boolean = false,
val tunnelScriptingEnabled: Boolean = true, val tunnelScriptingEnabled: Boolean = false,
val seamlessRoamingEnabled: Boolean = false,
) )
@@ -11,4 +11,6 @@ interface AutoTunnelSettingsRepository {
suspend fun getAutoTunnelSettings(): AutoTunnelSettings suspend fun getAutoTunnelSettings(): AutoTunnelSettings
suspend fun updateAutoTunnelEnabled(enabled: Boolean) suspend fun updateAutoTunnelEnabled(enabled: Boolean)
suspend fun updateDisableOnCaptivePortal(enabled: Boolean)
} }
@@ -23,4 +23,6 @@ interface GeneralSettingRepository {
suspend fun updateGlobalAmneziaEnabled(enabled: Boolean) suspend fun updateGlobalAmneziaEnabled(enabled: Boolean)
suspend fun updateScreenRecordingSecurity(enabled: Boolean) suspend fun updateScreenRecordingSecurity(enabled: Boolean)
suspend fun updateSeamlessRoaming(enabled: Boolean)
} }
@@ -11,16 +11,20 @@ sealed class ActiveNetwork {
data object Cellular : ActiveNetwork() data object Cellular : ActiveNetwork()
data class Wifi(val ssid: String, val isSecure: Boolean?) : ActiveNetwork() data class Wifi(
val ssid: String,
val isSecure: Boolean?,
val requiresCaptivePortalLogin: Boolean,
) : ActiveNetwork()
} }
data class NetworkState( data class NetworkState(
val activeNetwork: ActiveNetwork = ActiveNetwork.Disconnected, val activeNetwork: ActiveNetwork = ActiveNetwork.Disconnected,
val locationServicesEnabled: Boolean = false, val locationServicesEnabled: Boolean = false,
val locationPermissionGranted: Boolean = false, val locationPermissionGranted: Boolean = false,
) { // Has a network that can actually transfer data (not suspended)
fun hasInternet(): Boolean = activeNetwork !is ActiveNetwork.Disconnected val hasUsableNetwork: Boolean = false,
} )
fun ConnectivityState.toDomain(): NetworkState { fun ConnectivityState.toDomain(): NetworkState {
val domainNetwork: ActiveNetwork = val domainNetwork: ActiveNetwork =
@@ -33,7 +37,11 @@ fun ConnectivityState.toDomain(): NetworkState {
null -> null null -> null
else -> true else -> true
} }
ActiveNetwork.Wifi(ssid = network.ssid, isSecure = isSecure) ActiveNetwork.Wifi(
ssid = network.ssid,
isSecure = isSecure,
requiresCaptivePortalLogin(),
)
} }
is MonitorActiveNetwork.Cellular -> ActiveNetwork.Cellular is MonitorActiveNetwork.Cellular -> ActiveNetwork.Cellular
is MonitorActiveNetwork.Ethernet -> ActiveNetwork.Ethernet is MonitorActiveNetwork.Ethernet -> ActiveNetwork.Ethernet
@@ -44,5 +52,6 @@ fun ConnectivityState.toDomain(): NetworkState {
activeNetwork = domainNetwork, activeNetwork = domainNetwork,
locationPermissionGranted = this.locationPermissionsGranted, locationPermissionGranted = this.locationPermissionsGranted,
locationServicesEnabled = this.locationServicesEnabled, locationServicesEnabled = this.locationServicesEnabled,
hasUsableNetwork = hasUsableNetwork(),
) )
} }
@@ -28,7 +28,19 @@ class AutoTunnelEngine {
val activeTunnelIds = backend.activeTunnels.keys.toSet() val activeTunnelIds = backend.activeTunnels.keys.toSet()
if (!network.hasInternet()) { val isOnCaptivePortalWifi =
network.activeNetwork is ActiveNetwork.Wifi &&
network.activeNetwork.requiresCaptivePortalLogin
if (isOnCaptivePortalWifi && settings.disableTunnelOnCaptivePortal) {
return if (activeTunnelIds.isNotEmpty()) {
Decision.Sync(start = emptySet(), stop = activeTunnelIds)
} else {
Decision.None
}
}
if (!network.hasUsableNetwork) {
return if (settings.isStopOnNoInternetEnabled) { return if (settings.isStopOnNoInternetEnabled) {
Decision.StopDueToNoInternet Decision.StopDueToNoInternet
} else { } else {
@@ -192,11 +192,11 @@ class AutoTunnelService : LifecycleService() {
reconciliationMutex.withLock { reconciliationMutex.withLock {
val currentNetworkState = networkEngine.stableState.value?.state?.toDomain() val currentNetworkState = networkEngine.stableState.value?.state?.toDomain()
val stillNoInternet = currentNetworkState?.hasInternet() == false val stillNoUsableNetwork = currentNetworkState?.hasUsableNetwork == false
val stopOnNoInternetEnabled = val stopOnNoInternetEnabled =
autoTunnelRepository.flow.firstOrNull()?.isStopOnNoInternetEnabled == true autoTunnelRepository.flow.firstOrNull()?.isStopOnNoInternetEnabled == true
if (stillNoInternet && stopOnNoInternetEnabled) { if (stillNoUsableNetwork && stopOnNoInternetEnabled) {
val currentActiveIds = val currentActiveIds =
tunnelCoordinator.backendStatus.value.activeTunnels.keys tunnelCoordinator.backendStatus.value.activeTunnels.keys
@@ -0,0 +1,73 @@
package com.zaneschepke.wireguardautotunnel.ui.common.dialog
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
@Composable
fun LocalNetworkPermissionDialog(onDismiss: () -> Unit, onAttest: () -> Unit) {
InfoDialog(
onAttest = onAttest,
onDismiss = onDismiss,
title = stringResource(R.string.local_network_permission_title),
body = {
Column {
Text(
text = stringResource(R.string.local_network_permission_intro),
style = MaterialTheme.typography.bodyMedium,
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(R.string.local_network_permission_issues_intro),
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Bold,
)
Spacer(modifier = Modifier.height(8.dp))
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(
text = stringResource(R.string.local_network_permission_feature_tunnels),
style = MaterialTheme.typography.bodyMedium,
)
Text(
text = stringResource(R.string.local_network_permission_feature_autotunnel),
style = MaterialTheme.typography.bodyMedium,
)
Text(
text = stringResource(R.string.local_network_permission_feature_proxy),
style = MaterialTheme.typography.bodyMedium,
)
}
Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(R.string.local_network_permission_recommendation),
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Bold,
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(R.string.local_network_permission_nearby_devices),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
},
confirmText = stringResource(R.string._continue),
)
}
@@ -10,6 +10,7 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Filter1 import androidx.compose.material.icons.outlined.Filter1
import androidx.compose.material.icons.outlined.Map import androidx.compose.material.icons.outlined.Map
import androidx.compose.material.icons.outlined.PublicOff
import androidx.compose.material.icons.outlined.WifiFind import androidx.compose.material.icons.outlined.WifiFind
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -191,6 +192,21 @@ fun WifiSettingsScreen(viewModel: AutoTunnelViewModel = koinViewModel()) {
) )
}, },
) )
SurfaceRow(
leading = { Icon(Icons.Outlined.PublicOff, contentDescription = null) },
title = stringResource(R.string.stop_while_captive_portal),
onClick = {
viewModel.setDisabledOnCaptivePortal(
!uiState.autoTunnelSettings.disableTunnelOnCaptivePortal
)
},
trailing = {
ThemedSwitch(
checked = uiState.autoTunnelSettings.disableTunnelOnCaptivePortal,
onClick = { viewModel.setDisabledOnCaptivePortal(it) },
)
},
)
} }
Column { Column {
GroupLabel(stringResource(R.string.tunnels), Modifier.padding(horizontal = 16.dp)) GroupLabel(stringResource(R.string.tunnels), Modifier.padding(horizontal = 16.dp))
@@ -9,6 +9,7 @@ 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.outlined.Android import androidx.compose.material.icons.outlined.Android
import androidx.compose.material.icons.outlined.CellWifi
import androidx.compose.material.icons.outlined.Dns import androidx.compose.material.icons.outlined.Dns
import androidx.compose.material.icons.outlined.ExpandMore import androidx.compose.material.icons.outlined.ExpandMore
import androidx.compose.material.icons.outlined.MonitorHeart import androidx.compose.material.icons.outlined.MonitorHeart
@@ -234,6 +235,23 @@ fun SettingsScreen(
viewModel.setTunnelScriptedEnabled(!uiState.settings.tunnelScriptingEnabled) viewModel.setTunnelScriptedEnabled(!uiState.settings.tunnelScriptingEnabled)
}, },
) )
SurfaceRow(
leading = { Icon(Icons.Outlined.CellWifi, contentDescription = null) },
title = stringResource(R.string.seamless_roaming),
trailing = { modifier ->
ThemedSwitch(
checked = uiState.settings.seamlessRoamingEnabled,
onClick = { viewModel.setSeamlessNetworkRoaming(enabled = it) },
modifier = modifier,
)
},
description = {
DescriptionText(stringResource(R.string.seamless_roaming_description))
},
onClick = {
viewModel.setSeamlessNetworkRoaming(!uiState.settings.seamlessRoamingEnabled)
},
)
SurfaceRow( SurfaceRow(
leading = { Icon(Icons.Outlined.MonitorHeart, null) }, leading = { Icon(Icons.Outlined.MonitorHeart, null) },
title = stringResource(R.string.tunnel_monitoring), title = stringResource(R.string.tunnel_monitoring),
@@ -0,0 +1,213 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.support.license
import android.content.Context
import com.mikepenz.aboutlibraries.Libs
import com.mikepenz.aboutlibraries.entity.Developer
import com.mikepenz.aboutlibraries.entity.Library
import com.mikepenz.aboutlibraries.entity.License
import com.mikepenz.aboutlibraries.entity.Scm
import com.mikepenz.aboutlibraries.util.withContext
fun buildLibsWithAdditionalLibraries(context: Context): Libs {
val baseLibs = Libs.Builder().withContext(context).build()
val cleanedBaseLibs =
baseLibs.libraries.filterNot { library ->
library.uniqueId.contains("com.github.topjohnwu.libsu", ignoreCase = true) ||
library.uniqueId.contains("com.github.T8RIN.QuickieExtended", ignoreCase = true)
}
val nativeLibraries =
listOf(
Library(
uniqueId = "github.com.wgtunnel:amneziawg-go",
artifactVersion = "v0.0.0-20260618075902-e1b699b2104b",
name = "AmneziaWG Go (Fork)",
description = "WireGuard implementation with Amnezia obfuscation",
website = "https://wgtunnel.com",
developers =
listOf(
Developer(
name = "Zane Schepke (Fork Maintainer)",
organisationUrl = "https://wgtunnel.com",
),
Developer(
name = "Jason A. Donenfeld (Original WireGuard)",
organisationUrl = "https://www.wireguard.com/",
),
Developer(
name = "Amnezia VPN Team",
organisationUrl = "https://amnezia.org/",
),
),
organization = null,
scm = Scm(null, null, "https://github.com/wgtunnel/amneziawg-go"),
licenses =
setOf(
License(
name = "MIT License",
url = "https://opensource.org/licenses/MIT",
spdxId = "MIT",
hash = "mit-license-amneziawg-fork",
)
),
funding = emptySet(),
tag = "native",
),
Library(
uniqueId = "github.com.wgtunnel:wireproxy-awg",
artifactVersion = "v0.0.0-20260309043206-ff4200f20ff2",
name = "Wireproxy AWG (Fork)",
description = "WireGuard proxy with Amnezia support",
website = "https://wgtunnel.com",
developers =
listOf(
Developer(
name = "Zane Schepke (Fork Maintainer)",
organisationUrl = "https://wgtunnel.com",
),
Developer(name = "Artem Russkikh (Original)", organisationUrl = null),
),
organization = null,
scm = Scm(null, null, "https://github.com/wgtunnel/wireproxy-awg"),
licenses =
setOf(
License(
name = "MIT License",
url = "https://opensource.org/licenses/MIT",
spdxId = "MIT",
hash = "mit-license-wireproxy-fork",
)
),
funding = emptySet(),
tag = "native",
),
Library(
uniqueId = "github.com.wgtunnel:go-socks5",
artifactVersion = "v0.0.0-20260307052555-86f8d93b9534",
name = "go-socks5 (Fork)",
description = "SOCKS5 proxy server implementation",
website = "https://wgtunnel.com",
developers =
listOf(
Developer(
name = "Zane Schepke (Fork Maintainer)",
organisationUrl = "https://wgtunnel.com",
),
Developer(name = "Things-go Team (Original)", organisationUrl = null),
),
organization = null,
scm = Scm(null, null, "https://github.com/wgtunnel/go-socks5"),
licenses =
setOf(
License(
name = "MIT License",
url = "https://opensource.org/licenses/MIT",
spdxId = "MIT",
hash = "mit-license-go-socks5-fork",
)
),
funding = emptySet(),
tag = "native",
),
Library(
uniqueId = "github.com.miekg:dns",
artifactVersion = "v1.1.69",
name = "miekg/dns",
description = "DNS library for Go",
website = "https://github.com/miekg/dns",
developers = listOf(Developer(name = "Miek Gieben", organisationUrl = null)),
organization = null,
scm = Scm(null, null, "https://github.com/miekg/dns"),
licenses =
setOf(
License(
name = "BSD 3-Clause \"New\" or \"Revised\" License",
url = "https://opensource.org/licenses/BSD-3-Clause",
spdxId = "BSD-3-Clause",
hash = "bsd3-miekg-dns",
)
),
funding = emptySet(),
tag = "go",
),
Library(
uniqueId = "github.com.heiher:hev-socks5-tunnel",
artifactVersion = "2.15.0",
name = "hev-socks5-tunnel",
description = "High performance SOCKS5 tunnel",
website = "https://github.com/heiher/hev-socks5-tunnel",
developers = listOf(Developer(name = "heiher", organisationUrl = null)),
organization = null,
scm = Scm(null, null, "https://github.com/heiher/hev-socks5-tunnel"),
licenses =
setOf(
License(
name = "MIT License",
url = "https://opensource.org/licenses/MIT",
spdxId = "MIT",
hash = "mit-license-hev",
)
),
funding = emptySet(),
tag = "native",
),
)
val additionalLibraries =
listOf(
Library(
uniqueId = "com.github.T8RIN.QuickieExtended:quickie-foss",
artifactVersion = "1.18.1",
name = "QuickieFoss",
description = "Camera QR code scanner",
website = "https://github.com/T8RIN/QuickieExtended",
developers = listOf(Developer(name = "T8RIN", null)),
organization = null,
scm = Scm(null, null, "https://github.com/T8RIN/QuickieExtended"),
licenses =
setOf(
License(
name = "Apache License 2.0",
url = "https://www.apache.org/licenses/LICENSE-2.0",
spdxId = "Apache-2.0",
hash = "apache-2-quickie",
)
),
funding = emptySet(),
tag = "ui",
),
Library(
uniqueId = "com.github.topjohnwu.libsu:core",
artifactVersion = "6.0.0",
name = "libsu",
description = "Root shell library for Android",
website = "https://github.com/topjohnwu/libsu",
developers = listOf(Developer(name = "topjohnwu", null)),
organization = null,
scm = Scm(null, null, "https://github.com/topjohnwu/libsu"),
licenses =
setOf(
License(
name = "Apache License 2.0",
url = "https://www.apache.org/licenses/LICENSE-2.0",
spdxId = "Apache-2.0",
hash = "apache-2-libsu",
)
),
funding = emptySet(),
tag = "system",
),
)
return Libs(
libraries =
(cleanedBaseLibs + nativeLibraries + additionalLibraries).sortedBy {
it.name.lowercase()
},
licenses =
baseLibs.licenses +
nativeLibraries.flatMap { it.licenses } +
additionalLibraries.flatMap { it.licenses },
)
}
@@ -1,15 +0,0 @@
import kotlinx.serialization.Serializable
@Serializable
data class LicenseFileEntry(
val groupId: String,
val artifactId: String,
val version: String,
val name: String? = null,
val spdxLicenses: List<SpdxLicense> = emptyList(),
val scm: Scm? = null,
)
@Serializable data class SpdxLicense(val identifier: String, val name: String, val url: String)
@Serializable data class Scm(val url: String)
@@ -1,30 +1,23 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.support.license package com.zaneschepke.wireguardautotunnel.ui.screens.support.license
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.material3.CircularWavyProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.platform.LocalContext
import com.zaneschepke.wireguardautotunnel.ui.screens.support.license.components.LicenseList import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
import com.zaneschepke.wireguardautotunnel.viewmodel.LicenseViewModel import com.mikepenz.aboutlibraries.ui.compose.variant.LibraryDetailMode
import org.koin.androidx.compose.koinViewModel
import org.orbitmvi.orbit.compose.collectAsState
@OptIn(ExperimentalMaterial3ExpressiveApi::class) @OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable @Composable
fun LicenseScreen(viewModel: LicenseViewModel = koinViewModel()) { fun LicenseScreen() {
val licenseUiState by viewModel.collectAsState() val context = LocalContext.current
val libs = remember { buildLibsWithAdditionalLibraries(context) }
if (licenseUiState.isLoading) { LibrariesContainer(
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { libraries = libs,
CircularWavyProgressIndicator(waveSpeed = 60.dp, modifier = Modifier.size(48.dp)) modifier = Modifier.fillMaxSize(),
} detailMode = LibraryDetailMode.Sheet,
} else { )
LicenseList(licenseUiState.licenses)
}
} }
@@ -1,69 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.support.license.components
import LicenseFileEntry
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.Launch
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.scrollbar
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.unit.dp
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
@Composable
fun LicenseList(licenses: List<LicenseFileEntry>) {
val context = LocalContext.current
val lazyListState = rememberLazyListState()
LazyColumn(
modifier =
Modifier.fillMaxSize()
.scrollbar(
state = lazyListState.scrollIndicatorState,
orientation = Orientation.Vertical,
),
state = lazyListState,
) {
items(licenses) { entry ->
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier =
Modifier.clickable(enabled = entry.scm?.url != null) {
entry.scm?.url?.let { context.openWebUrl(it) }
}
.padding(horizontal = 16.dp, vertical = 8.dp),
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = "${entry.artifactId} (${entry.version})",
style = MaterialTheme.typography.titleSmall,
)
entry.spdxLicenses.forEach { license ->
Text(
text = license.name,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary,
)
}
}
entry.scm?.url?.let { Icon(Icons.AutoMirrored.Outlined.Launch, null) }
}
}
}
}
@@ -1,8 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.state
import LicenseFileEntry
data class LicenseUiState(
val isLoading: Boolean = true,
val licenses: List<LicenseFileEntry> = emptyList(),
)
@@ -6,15 +6,11 @@ object Constants {
const val BASE_LOG_FILE_NAME = "wg_tunnel_logs" const val BASE_LOG_FILE_NAME = "wg_tunnel_logs"
const val VPN_SETTINGS_PACKAGE = "android.net.vpn.SETTINGS" const val VPN_SETTINGS_PACKAGE = "android.net.vpn.SETTINGS"
const val SYSTEM_EXEMPT_SERVICE_TYPE_ID = 1 shl 10
const val SPECIAL_USE_SERVICE_TYPE_ID = 1 shl 30 const val SPECIAL_USE_SERVICE_TYPE_ID = 1 shl 30
const val QR_CODE_NAME_PROPERTY = "# Name ="
const val FDROID_FLAVOR = "fdroid" const val FDROID_FLAVOR = "fdroid"
const val GOOGLE_PLAY_FLAVOR = "google" const val GOOGLE_PLAY_FLAVOR = "google"
const val STANDALONE_FLAVOR = "standalone" const val STANDALONE_FLAVOR = "standalone"
const val RELEASE = "release"
const val BASE_RELEASE_URL = "https://github.com/wgtunnel/wgtunnel/releases/tag/" const val BASE_RELEASE_URL = "https://github.com/wgtunnel/wgtunnel/releases/tag/"
} }
@@ -81,7 +81,7 @@ object DnsValidator {
return Result.Valid return Result.Valid
} }
private fun validateUdp(value: String): DnsValidator.Result { private fun validateUdp(value: String): Result {
val parts = value.split(":") val parts = value.split(":")
val host = parts.getOrNull(0)?.trim() val host = parts.getOrNull(0)?.trim()
@@ -93,14 +93,14 @@ object DnsValidator {
// basic IP/hostname sanity check // basic IP/hostname sanity check
if (!isValidHostOrIp(host)) { if (!isValidHostOrIp(host)) {
return DnsValidator.Result.Invalid(DnsError.InvalidIpOrHost) return Result.Invalid(DnsError.InvalidIpOrHost)
} }
if (port !in 1..65535) { if (port !in 1..65535) {
return DnsValidator.Result.Invalid(DnsError.InvalidPort) return Result.Invalid(DnsError.InvalidPort)
} }
return DnsValidator.Result.Valid return Result.Valid
} }
private fun isValidHostOrIp(value: String): Boolean { private fun isValidHostOrIp(value: String): Boolean {
@@ -1,6 +1,5 @@
package com.zaneschepke.wireguardautotunnel.util package com.zaneschepke.wireguardautotunnel.util
import LicenseFileEntry
import android.content.ContentValues import android.content.ContentValues
import android.content.Context import android.content.Context
import android.database.Cursor import android.database.Cursor
@@ -23,7 +22,6 @@ import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream import java.util.zip.ZipOutputStream
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import timber.log.Timber import timber.log.Timber
class FileUtils(private val context: Context, private val ioDispatcher: CoroutineDispatcher) { class FileUtils(private val context: Context, private val ioDispatcher: CoroutineDispatcher) {
@@ -293,15 +291,6 @@ class FileUtils(private val context: Context, private val ioDispatcher: Coroutin
return@withContext itemUri return@withContext itemUri
} }
suspend fun readLibraryLicensesFromAssets(): List<LicenseFileEntry> =
withContext(ioDispatcher) {
val json = Json { ignoreUnknownKeys = true }
val jsonResult =
context.assets.open("licenses.json").bufferedReader().use { it.readText() }
json.decodeFromString(jsonResult)
}
companion object { companion object {
const val CONF_FILE_EXTENSION = ".conf" const val CONF_FILE_EXTENSION = ".conf"
const val ZIP_FILE_EXTENSION = ".zip" const val ZIP_FILE_EXTENSION = ".zip"
@@ -1,22 +1,9 @@
package com.zaneschepke.wireguardautotunnel.util package com.zaneschepke.wireguardautotunnel.util
import com.vdurmont.semver4j.Semver import com.vdurmont.semver4j.Semver
import java.math.BigDecimal
import kotlin.math.pow
import timber.log.Timber import timber.log.Timber
object NumberUtils { object NumberUtils {
private const val BYTES_IN_KB = 1024.0
private val BYTES_IN_MB = BYTES_IN_KB.pow(2.0)
private val keyValidationRegex = """^[A-Za-z0-9+/]{42}[AEIMQUYcgkosw480]=${'$'}""".toRegex()
fun bytesToMB(bytes: Long): BigDecimal {
return bytes.toBigDecimal().divide(BYTES_IN_MB.toBigDecimal())
}
fun isValidKey(key: String): Boolean {
return key.matches(keyValidationRegex)
}
fun generateRandomTunnelName(): String { fun generateRandomTunnelName(): String {
return "tunnel${randomFive()}" return "tunnel${randomFive()}"
@@ -1,26 +1,18 @@
package com.zaneschepke.wireguardautotunnel.util.extensions package com.zaneschepke.wireguardautotunnel.util.extensions
import android.Manifest
import android.app.Activity import android.app.Activity
import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Context.POWER_SERVICE
import android.content.Intent import android.content.Intent
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.PowerManager
import android.provider.Settings import android.provider.Settings
import android.service.quicksettings.TileService
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.core.net.toUri import androidx.core.net.toUri
import com.zaneschepke.wireguardautotunnel.BuildConfig
import com.zaneschepke.wireguardautotunnel.MainActivity import com.zaneschepke.wireguardautotunnel.MainActivity
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelControlTile
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.splittunnel.state.TunnelApp
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.FileUtils import com.zaneschepke.wireguardautotunnel.util.FileUtils
import java.io.File import java.io.File
@@ -36,11 +28,6 @@ fun Context.openWebUrl(url: String): Result<Unit> = runCatching {
startActivity(intent) startActivity(intent)
} }
fun Context.isBatteryOptimizationsDisabled(): Boolean {
val pm = getSystemService(POWER_SERVICE) as PowerManager
return pm.isIgnoringBatteryOptimizations(packageName)
}
fun Context.launchNotificationSettings() { fun Context.launchNotificationSettings() {
if (isRunningOnTv()) return launchAppSettings() if (isRunningOnTv()) return launchAppSettings()
val settingsIntent: Intent = val settingsIntent: Intent =
@@ -87,21 +74,6 @@ fun Context.hasSAFSupport(mimeType: String): Boolean {
} }
} }
fun Context.launchShareFile(file: File) {
FileProvider.getUriForFile(this, getString(R.string.provider), file)
val shareIntent =
Intent().apply {
action = Intent.ACTION_SEND
type = FileUtils.ALL_FILE_TYPES
putExtra(Intent.EXTRA_STREAM, file)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
val chooserIntent =
Intent.createChooser(shareIntent, "").apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }
this.startActivity(chooserIntent)
}
fun Context.launchSupportEmail(): Result<Unit> = runCatching { fun Context.launchSupportEmail(): Result<Unit> = runCatching {
val intent = val intent =
Intent(Intent.ACTION_SENDTO).apply { Intent(Intent.ACTION_SENDTO).apply {
@@ -128,7 +100,7 @@ fun Context.isRunningOnTv(): Boolean {
fun Context.launchVpnSettings(): Result<Unit> { fun Context.launchVpnSettings(): Result<Unit> {
return kotlin.runCatching { return kotlin.runCatching {
val intent = val intent =
Intent(Constants.VPN_SETTINGS_PACKAGE).apply { setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } Intent(Constants.VPN_SETTINGS_PACKAGE).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK }
startActivity(intent) startActivity(intent)
} }
} }
@@ -147,14 +119,6 @@ fun Context.launchLocationServicesSettings(): Result<Unit> {
} }
} }
fun Context.launchSettings(): Result<Unit> {
return kotlin.runCatching {
val intent =
Intent(Settings.ACTION_SETTINGS).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK }
startActivity(intent)
}
}
fun Context.launchAppSettings() { fun Context.launchAppSettings() {
kotlin kotlin
.runCatching { .runCatching {
@@ -171,48 +135,6 @@ fun Context.launchAppSettings() {
} }
} }
fun Context.requestTunnelTileServiceStateUpdate() =
runCatching {
TileService.requestListeningState(
this,
ComponentName(this, TunnelControlTile::class.java),
)
}
.onFailure { Timber.w(it) }
fun Context.requestAutoTunnelTileServiceUpdate() =
runCatching {
TileService.requestListeningState(
this,
ComponentName(this, AutoTunnelControlTile::class.java),
)
}
.onFailure { Timber.w(it) }
fun Context.getAllInternetCapablePackages(): List<PackageInfo> {
val permissions = arrayOf(Manifest.permission.INTERNET)
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
packageManager.getPackagesHoldingPermissions(
permissions,
PackageManager.PackageInfoFlags.of(0L),
)
} else {
packageManager.getPackagesHoldingPermissions(permissions, 0)
}
}
fun Context.getSplitTunnelApps(): List<TunnelApp> {
val packages = getAllInternetCapablePackages()
return packages
.filter { it.applicationInfo != null }
.map { pkg ->
TunnelApp(
packageManager.getApplicationLabel(pkg.applicationInfo!!).toString(),
pkg.packageName,
)
}
}
fun Context.canInstallPackages(): Boolean { fun Context.canInstallPackages(): Boolean {
return packageManager.canRequestPackageInstalls() return packageManager.canRequestPackageInstalls()
} }
@@ -227,7 +149,7 @@ fun Context.requestInstallPackagesPermission() {
} }
fun Context.installApk(apkFile: File) { fun Context.installApk(apkFile: File) {
val apkUri = FileProvider.getUriForFile(this, getString(R.string.provider), apkFile) val apkUri = FileProvider.getUriForFile(this, BuildConfig.FILE_PROVIDER_AUTHORITY, apkFile)
val intent = val intent =
Intent(Intent.ACTION_VIEW).apply { Intent(Intent.ACTION_VIEW).apply {
setDataAndType(apkUri, "application/vnd.android.package-archive") setDataAndType(apkUri, "application/vnd.android.package-archive")
@@ -1,8 +0,0 @@
package com.zaneschepke.wireguardautotunnel.util.extensions
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
fun <K, V> Flow<Map<K, V>>.distinctByKeys(): Flow<Map<K, V>> {
return distinctUntilChanged { old, new -> old.keys == new.keys }
}
@@ -18,10 +18,6 @@ fun <T, R : Comparable<R>> List<T>.isSortedBy(selector: (T) -> R): Boolean {
return zipWithNext().all { (a, b) -> selector(a) <= selector(b) } return zipWithNext().all { (a, b) -> selector(a) <= selector(b) }
} }
fun Int.toMillis(): Long {
return this * 1_000L
}
fun Double.round(decimals: Int): Double { fun Double.round(decimals: Int): Double {
val factor = 10.0.pow(decimals) val factor = 10.0.pow(decimals)
return (this * factor).roundToInt() / factor return (this * factor).roundToInt() / factor
@@ -0,0 +1,23 @@
package com.zaneschepke.wireguardautotunnel.util.permission
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import androidx.core.content.ContextCompat
object LocalNetworkPermissionHelper {
fun shouldRequestPermission(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.CINNAMON_BUN
}
fun isPermissionGranted(context: Context): Boolean {
return if (shouldRequestPermission()) {
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_LOCAL_NETWORK) ==
PackageManager.PERMISSION_GRANTED
} else {
true
}
}
}
@@ -145,6 +145,10 @@ class AutoTunnelViewModel(
) )
} }
fun setDisabledOnCaptivePortal(enabled: Boolean) = intent {
autoTunnelRepository.updateDisableOnCaptivePortal(enabled)
}
fun removeTunnelNetwork(tunnel: TunnelConfig, ssid: String) = intent { fun removeTunnelNetwork(tunnel: TunnelConfig, ssid: String) = intent {
tunnelsRepository.save( tunnelsRepository.save(
tunnel.copy( tunnel.copy(
@@ -1,23 +0,0 @@
package com.zaneschepke.wireguardautotunnel.viewmodel
import androidx.lifecycle.ViewModel
import com.zaneschepke.wireguardautotunnel.ui.state.LicenseUiState
import com.zaneschepke.wireguardautotunnel.util.FileUtils
import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.orbitmvi.orbit.ContainerHost
import org.orbitmvi.orbit.viewmodel.container
class LicenseViewModel(private val fileUtils: FileUtils) :
ContainerHost<LicenseUiState, Nothing>, ViewModel() {
@OptIn(ExperimentalCoroutinesApi::class)
override val container =
container<LicenseUiState, Nothing>(
LicenseUiState(),
buildSettings = { repeatOnSubscribedStopTimeout = 5000L },
) {
intent {
val licenses = fileUtils.readLibraryLicensesFromAssets()
reduce { state.copy(isLoading = false, licenses = licenses) }
}
}
}
@@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel
import com.dokar.sonner.ToastType import com.dokar.sonner.ToastType
import com.zaneschepke.tunnel.util.RootShell import com.zaneschepke.tunnel.util.RootShell
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelBackendCoordinator
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
@@ -27,6 +28,7 @@ class SettingsViewModel(
private val monitoringRepository: MonitoringSettingsRepository, private val monitoringRepository: MonitoringSettingsRepository,
private val globalEffectRepository: GlobalEffectRepository, private val globalEffectRepository: GlobalEffectRepository,
private val tunnelCoordinator: TunnelCoordinator, private val tunnelCoordinator: TunnelCoordinator,
private val tunnelBackendCoordinator: TunnelBackendCoordinator,
) : ContainerHost<SettingUiState, Nothing>, ViewModel() { ) : ContainerHost<SettingUiState, Nothing>, ViewModel() {
override val container = override val container =
@@ -114,6 +116,10 @@ class SettingsViewModel(
settingsRepository.upsert(state.settings.copy(tunnelScriptingEnabled = to)) settingsRepository.upsert(state.settings.copy(tunnelScriptingEnabled = to))
} }
fun setSeamlessNetworkRoaming(enabled: Boolean) = intent {
tunnelBackendCoordinator.changeSeamlessRoaming(enabled)
}
fun setAlreadyDonated(to: Boolean) = intent { fun setAlreadyDonated(to: Boolean) = intent {
settingsRepository.upsert(state.settings.copy(alreadyDonated = to)) settingsRepository.upsert(state.settings.copy(alreadyDonated = to))
} }
@@ -5,8 +5,8 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.dokar.sonner.ToastType import com.dokar.sonner.ToastType
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelBackendCoordinator
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelModeCoordinator
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
@@ -61,7 +61,7 @@ class SharedAppViewModel(
private val settingsRepository: GeneralSettingRepository, private val settingsRepository: GeneralSettingRepository,
private val autoTunnelStateHolder: AutoTunnelStateHolder, private val autoTunnelStateHolder: AutoTunnelStateHolder,
private val selectedTunnelsRepository: SelectedTunnelsRepository, private val selectedTunnelsRepository: SelectedTunnelsRepository,
private val tunnelModeCoordinator: TunnelModeCoordinator, private val tunnelBackendCoordinator: TunnelBackendCoordinator,
private val httpClient: HttpClient, private val httpClient: HttpClient,
private val fileUtils: FileUtils, private val fileUtils: FileUtils,
private val networkUtils: NetworkUtils, private val networkUtils: NetworkUtils,
@@ -172,7 +172,7 @@ class SharedAppViewModel(
} }
} }
tunnelModeCoordinator.changeMode(mode) tunnelBackendCoordinator.changeMode(mode)
} }
fun setShouldShowDonationSnackbar(to: Boolean) = intent { fun setShouldShowDonationSnackbar(to: Boolean) = intent {
+7
View File
@@ -0,0 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="1024" android:viewportWidth="1024" android:width="24dp">
<path android:fillColor="#FEFEFE" android:pathData="M654.6,237.7C658.8,237.7 663.1,237.7 667.4,237.7C677.4,237.7 687.5,237.7 697.6,237.7C707.9,237.7 718.2,237.7 728.6,237.7C737.5,237.6 746.4,237.6 755.4,237.6C760.7,237.6 766,237.6 771.3,237.6C776.3,237.6 781.3,237.6 786.3,237.6C788.1,237.6 789.9,237.6 791.7,237.6C807.6,237.5 826.2,238.6 838.3,250.2C849.7,262.5 857,276.4 856.4,293.7C855.7,300 853.9,306 852,312C851.7,313.1 851.3,314.1 851,315.3C845.5,331.6 837.9,347.2 830.5,362.7C825.4,373.2 821,383.7 817.2,394.7C815.8,398.5 814.2,402 812.5,405.6C810.4,410.4 808.5,415.1 806.5,419.9C806.1,420.9 805.7,421.9 805.3,422.9C804.5,424.8 803.8,426.7 803,428.6C799.2,438 795.1,447.3 791.1,456.6C787.3,465.3 783.5,474.1 779.8,483C777.8,487.9 775.7,492.8 773.4,497.6C771.5,501.8 769.8,506 768.1,510.3C765.8,516 763.2,521.7 760.8,527.4C757,536.1 753.2,544.8 749.6,553.5C746.9,560 743.9,566.3 740.7,572.6C736.3,581.3 732.6,590.3 729.1,599.4C728.8,600.4 728.8,600.4 728.4,601.4C727.1,604.8 725.8,608.1 724.5,611.5C721.1,620.5 717.2,629.3 713,638C711.6,641.1 710.2,644.1 708.8,647.2C708.4,648 708.1,648.8 707.7,649.7C704.4,656.9 701.3,664.2 698.2,671.6C690.6,689.4 682.8,707.1 674.6,724.6C673.2,727.6 671.8,730.7 670.5,733.8C661.2,755.7 650.6,777.3 627.3,787.1C619.6,789.9 612.7,790.6 604.4,790.5C603.3,790.5 602.2,790.5 601,790.5C589.3,790.5 580.2,787.1 571,780C570.3,779.4 569.5,778.9 568.8,778.3C557.3,768.9 551.3,756.1 545.3,742.9C543,737.9 540.7,732.8 538.3,727.8C537.5,726 536.7,724.2 535.8,722.4C533,716.3 530.1,710.2 527.2,704.1C523.7,696.8 520.4,689.5 517.5,682C516,678.3 514.4,674.7 512.6,671.2C509.4,665 507,658.5 504.6,651.9C502.5,646.6 500.3,641.4 497.7,636.3C493.6,628.4 490.2,620.2 486.9,611.9C486.3,610.4 485.7,608.9 485.1,607.5C483.9,604.6 482.8,601.8 481.6,598.9C477.4,588.5 473,578.2 468.1,568.1C460.7,552.6 454.6,536.7 448.8,520.7C445.8,512.5 442,504.8 438,497.1C432.2,485.5 427.2,473.8 422.8,461.7C420.1,454.6 417.1,447.7 413.8,440.9C411,435 408.4,428.9 405.8,422.9C404.9,420.9 404,418.8 403.1,416.8C399.9,409.5 396.9,402.3 394.1,394.9C392.6,390.9 390.9,387 389.1,383.1C388.8,382.4 388.4,381.6 388.1,380.8C385.6,375.3 383,369.8 380.4,364.3C378.1,359.3 376.1,354.2 374.2,349C373.1,346.3 371.9,343.8 370.6,341.3C367.3,334.9 365.1,328.1 362.8,321.3C362,319 361.1,316.7 360.3,314.5C354.4,298.6 354.5,284.7 361,269C365.9,258.7 375.2,249.3 385.8,244.7C399.3,240.2 417.7,237.5 431,244C455.6,257.2 466.2,288.1 475.7,312.7C477.3,316.9 479.2,320.8 481.1,324.8C487.7,338.7 493.6,352.9 499.3,367.2C503,376.4 507,385.4 511.3,394.3C517.4,407.2 522.6,420.3 527.7,433.6C531,442.1 534.5,450.5 538.5,458.8C540.3,462.6 541.9,466.5 543.5,470.5C545.9,476.3 548.4,482 551.1,487.7C554.6,495.1 557.7,502.6 560.8,510.1C563.6,516.7 566.5,523.3 569.4,529.8C573.7,539.2 577.6,548.7 581.6,558.2C585.6,568 589.7,577.7 593.9,587.4C595.9,592.2 598,596.9 600,601.6C600.4,602.5 600.8,603.4 601.2,604.3C601.5,605.2 601.9,606 602.3,606.9C602.6,607.6 602.9,608.3 603.2,609.1C603.9,610.7 604.4,612.3 605,614C607.7,608.6 610.3,603.2 612.7,597.7C613.1,597 613.4,596.2 613.8,595.4C617.7,586.5 621.4,577.4 625.1,568.4C631.8,551.7 638.9,535.2 646.4,518.9C649.1,513 651.6,507.1 654,501C657.6,491.9 661.3,482.8 665.1,473.7C665.5,472.7 665.9,471.7 666.3,470.7C668.3,465.9 670.3,461.3 672.7,456.7C674.7,452.8 676.1,449 677.4,444.8C679.4,438.4 682.1,432.6 685.2,426.7C687.2,422.6 688.9,418.5 690.6,414.3C692.8,408.9 695.1,403.7 697.7,398.6C700.3,393.3 702.6,387.8 705,382.4C706.1,379.8 707.2,377.2 708.4,374.6C709.2,372.8 710,370.9 710.8,369.1C711.2,368.2 711.6,367.3 712,366.4C715.1,359.2 717.7,352.9 716,345C714.8,345 713.6,345 712.4,345C701.2,344.9 690,344.8 678.7,344.7C673,344.6 667.2,344.6 661.4,344.5C617.2,344.1 617.2,344.1 600.4,343.6C599.3,343.5 598.2,343.5 597.1,343.5C577.7,342.7 563.5,336.2 550.1,322C544.3,315 540.5,305.1 540,296C539.9,295.3 539.9,294.5 539.8,293.8C539.3,280.1 544.1,267.9 553.3,257.8C578.1,231.3 621.5,237.7 654.6,237.7Z"/>
<path android:fillColor="#FEFEFE" android:pathData="M253,250C253.7,250.5 254.4,250.9 255.1,251.4C269.1,262.1 275.9,283.6 282.8,299.1C284.1,302 285.4,304.8 286.7,307.7C291.1,316.8 294.9,326.1 298.7,335.5C301.8,343.2 305.1,350.9 308.4,358.5C308.8,359.4 309.2,360.3 309.6,361.2C311.5,365.6 313.5,370 315.5,374.4C318.1,380.2 320.6,386 323,391.9C323.6,393.4 323.6,393.4 324.3,394.9C326.4,400 328.5,405.2 330.4,410.5C332.9,417 335.9,423.2 339,429.4C345.8,443.2 351.7,457.2 357.5,471.4C361,479.9 364.6,488.3 368.5,496.6C369.1,497.8 369.7,499.1 370.3,500.3C371.4,502.7 372.5,505 373.6,507.4C374.1,508.5 374.6,509.5 375.1,510.6C375.6,511.5 376,512.5 376.4,513.4C378.3,517.8 380,522.2 381.7,526.6C383.7,531.7 385.8,536.8 387.9,541.8C388.4,542.9 388.9,544 389.3,545.1C392,551.4 394.7,557.7 397.5,564C397.8,564.7 398.1,565.3 398.4,566C398.9,567 398.9,567 399.3,568.1C399.9,569.4 400.5,570.8 401.1,572.2C401.4,572.9 401.7,573.5 402,574.2C406.2,583.8 410.2,593.4 413.9,603.1C416.1,608.9 418.6,614.5 421.2,620.1C424.6,627.2 427.7,634.3 430.9,641.5C431.3,642.5 431.3,642.5 431.8,643.6C436.1,653.4 440.2,663.3 444,673.4C446.5,679.7 449.2,685.9 452.1,692C472.4,734.7 472.4,734.7 465.3,755.8C459.9,769.4 451,781 437.5,787.2C436.4,787.7 435.3,788.2 434.2,788.7C422.2,793.5 408.1,791.5 396.4,787C369.2,774.6 356.6,742.9 346.5,716.7C344.7,712.4 342.8,708.2 340.8,704C333,687.8 326,671.2 319,654.6C316.1,647.6 313.1,640.6 310.1,633.7C307.7,628.1 305.4,622.5 303.3,616.8C301.6,612.4 299.7,608.2 297.5,604C291.1,591.1 285.6,577.7 280,564.4C277.2,557.7 274.3,551.1 271.4,544.4C268.9,538.9 266.7,533.3 264.6,527.7C263.4,524.8 262,522 260.6,519.2C258,514.1 255.8,508.9 253.7,503.6C252.9,501.6 252.1,499.7 251.3,497.8C250.9,496.8 250.6,495.8 250.2,494.8C248.4,490.6 246.7,486.3 244.9,482.1C244.6,481.4 244.3,480.7 244,480C242.3,475.9 240.5,471.9 238.6,467.9C234.7,459.7 231.2,451.4 227.8,443.1C226.9,440.9 226,438.7 225.1,436.5C222.4,430 219.8,423.5 217.3,417C215.4,411.9 213.2,407 210.8,402.1C203.8,387.5 197.4,372.6 191.3,357.6C189.5,353.1 187.5,348.6 185.4,344.1C182.4,337.5 179.7,330.9 176.9,324.3C176.4,322.9 175.8,321.5 175.2,320.1C168.5,304.1 166.4,288.4 173,272C179.5,258.3 189.7,249.1 203.7,243.7C220.2,238.1 238.5,240.3 253,250Z"/>
</vector>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_banner_background"/>
<foreground android:drawable="@mipmap/ic_banner_foreground"/>
</adaptive-icon>
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

+3
View File
@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
tools:keep="@raw/aboutlibraries" />
@@ -1,2 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources></resources> <resources>
<color name="ic_banner_background">#21272A</color>
</resources>
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+18
View File
@@ -537,4 +537,22 @@
<string name="restore_failed_wrong_password">Restore failed. Wrong password</string> <string name="restore_failed_wrong_password">Restore failed. Wrong password</string>
<string name="restore_failed_invalid_file">Restore failed. Select a valid backup file (.sqlite3 or .sqlite3.aes)</string> <string name="restore_failed_invalid_file">Restore failed. Select a valid backup file (.sqlite3 or .sqlite3.aes)</string>
<string name="error_invalid_config_url">This link returned an invalid config file. Make sure you are using a direct download link</string> <string name="error_invalid_config_url">This link returned an invalid config file. Make sure you are using a direct download link</string>
<string name="local_network_permission_title">Local Network Access Needed</string>
<string name="local_network_permission_intro">WG Tunnel needs access to your local network for several features to work properly.</string>
<string name="local_network_permission_issues_intro">Without this permission, you may experience issues with:</string>
<string name="local_network_permission_feature_tunnels">- Connection issues with split tunneling, LAN bypass, or servers hosted on your local network</string>
<string name="local_network_permission_feature_autotunnel">- Auto-tunneling and split tunneling features</string>
<string name="local_network_permission_feature_proxy">- Local proxy and bypass functionality</string>
<string name="local_network_permission_recommendation">Granting this permission is strongly recommended.</string>
<string name="local_network_permission_nearby_devices">Note: Android labels this permission as “nearby devices”.</string>
<string name="local_network_permission_denied">Local network access denied. Some features may not work properly</string>
<string name="stop_while_captive_portal">Stop tunnel while captive portal is present</string>
<string name="seamless_roaming">Seamless roaming</string>
<string name="seamless_roaming_description">Bind tunnels to the active network to improve reliability on network handoffs</string>
</resources> </resources>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 871 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 735 B

After

Width:  |  Height:  |  Size: 629 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 576 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 503 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 466 B

After

Width:  |  Height:  |  Size: 423 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 995 B

After

Width:  |  Height:  |  Size: 839 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

+9
View File
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="512" android:viewportWidth="512" android:width="24dp">
<path android:fillColor="#FEFEFE" android:pathData="M387.85,105.96C390.08,105.99 392.31,106.01 394.54,106.02C425.06,106.26 425.06,106.26 435.06,115.31C440.67,121.68 443.41,128.87 443.28,137.35C442.12,148.45 436.55,159.27 432.19,169.44C431.49,171.08 430.79,172.73 430.09,174.37C428.56,177.97 427.03,181.57 425.49,185.17C422.88,191.28 420.28,197.41 417.68,203.53C353.09,355.51 353.09,355.51 338.06,385.06C337.46,386.28 337.46,386.28 336.85,387.51C331.72,397.31 324.43,403.52 314,407C305.35,407.63 297.48,407.88 290,403C289.35,402.61 288.7,402.21 288.03,401.81C267.22,386.65 255.93,343.57 246.12,320.63C243.84,315.3 241.56,309.97 239.27,304.64C235.16,295.07 231.06,285.5 226.96,275.93C216,250.31 216,250.31 205,224.7C195.27,202.04 185.54,179.37 176.31,156.5C176,155.72 175.68,154.95 175.36,154.15C170.97,143.16 169.24,134.79 173.75,123.58C177.5,115.83 183.04,112.05 191,109C199.93,106.88 206.75,108.16 215,112C223.45,117.42 227.57,127.73 231.75,136.56C232.2,137.51 232.65,138.46 233.12,139.44C244.41,163.45 254.74,187.93 265.15,212.33C268.22,219.52 271.33,226.7 274.44,233.88C274.98,235.13 275.53,236.39 276.07,237.65C278.53,243.32 281,248.99 283.47,254.66C289.07,267.5 294.62,280.36 300,293.3C300.54,294.59 301.08,295.88 301.62,297.17C302.36,298.95 303.1,300.73 303.83,302.52C304.25,303.53 304.67,304.54 305.1,305.59C306,308 306,308 306,310C306.66,310 307.32,310 308,310C308.17,309.38 308.35,308.77 308.52,308.13C310.24,302.19 312.3,296.56 314.77,290.89C315.13,290.05 315.49,289.22 315.86,288.36C317.03,285.65 318.2,282.95 319.38,280.25C320.19,278.35 321.01,276.46 321.83,274.56C323.85,269.88 325.88,265.2 327.9,260.53C329.44,256.98 330.98,253.42 332.52,249.87C334.58,245.1 336.65,240.34 338.71,235.57C341.15,229.96 343.58,224.35 346,218.74C346.35,217.95 346.69,217.15 347.05,216.33C348.78,212.33 350.51,208.33 352.23,204.32C355.22,197.41 358.21,190.5 361.24,183.61C361.71,182.53 362.19,181.45 362.67,180.34C363.53,178.38 364.39,176.42 365.26,174.47C365.63,173.63 366,172.79 366.38,171.93C366.85,170.85 366.85,170.85 367.34,169.75C368.12,168 368.12,168 368,166C366.76,165.99 366.76,165.99 365.49,165.98C357.63,165.9 349.78,165.81 341.92,165.71C337.88,165.66 333.84,165.62 329.81,165.58C325.9,165.54 322,165.49 318.1,165.44C316.62,165.42 315.13,165.41 313.65,165.4C300.1,165.29 288.99,164.82 278.75,155.19C273.15,149.32 271.03,143.02 271,135C271.65,126.87 275.22,120.31 281.34,114.95C296.39,103.76 318.78,106.75 336.69,106.44C338.39,106.41 338.39,106.41 340.12,106.37C373.39,105.77 373.39,105.77 387.85,105.96Z"/>
<path android:fillColor="#FEFEFE" android:pathData="M114,113C120.76,117.8 124.22,123.2 127.69,130.63C128.19,131.68 128.7,132.73 129.22,133.81C130.84,137.2 132.43,140.59 134,144C134.5,145.08 135,146.16 135.52,147.28C144.69,167.16 153.19,187.35 161.79,207.48C165.04,215.1 168.33,222.71 171.63,230.31C172.14,231.49 172.65,232.67 173.18,233.89C175.39,239 177.61,244.1 179.83,249.21C189.5,271.38 198.88,293.66 208.11,316.01C211.04,323.11 214.01,330.18 217,337.25C217.43,338.26 217.86,339.28 218.3,340.32C220.39,345.24 222.5,350.16 224.63,355.07C225.05,356.04 225.47,357.01 225.91,358C226.69,359.81 227.48,361.62 228.27,363.42C232.37,372.86 233.7,381.34 229.94,391.19C226.56,398.54 222.59,403.08 215.19,406.44C207.09,409.27 200.15,408.78 192.38,405.23C182.8,400.26 178.73,393.43 174,384C173.52,383.06 173.05,382.12 172.56,381.15C162.64,361.39 153.83,341.17 145.1,320.88C143.13,316.31 141.16,311.74 139.17,307.18C130.23,286.6 121.43,265.97 112.65,245.32C107.35,232.86 102.04,220.41 96.69,207.98C90.69,194.05 84.73,180.11 78.92,166.1C78.32,164.64 77.71,163.18 77.1,161.72C67.42,138.54 67.42,138.54 71.56,126.94C74.89,119.43 79.6,114.1 87.11,110.69C96.25,107.35 105.69,107.97 114,113Z"/>
<path android:fillColor="#FEFEFE" android:pathData="M324,178C331.04,182.91 336.31,189.42 338,198C338.98,206.07 336.97,213.6 332.42,220.34C327.99,225.71 322.98,229.65 316,231C306.84,231.81 299.67,231.33 292,226C289.13,223.5 289.13,223.5 287,221C286.28,220.2 285.56,219.39 284.81,218.56C279.84,211.53 280.29,203.26 281,195C281.85,191.88 283.09,189.59 285,187C285.42,186.39 285.85,185.78 286.29,185.14C295.31,173.35 311.34,170.15 324,178Z"/>
</vector>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_banner_background"/>
<foreground android:drawable="@mipmap/ic_banner_foreground"/>
</adaptive-icon>
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_launcher_background"/> <background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/> <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon> </adaptive-icon>
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 942 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1000 B

After

Width:  |  Height:  |  Size: 1012 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1012 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Some files were not shown because too many files have changed in this diff Show More