Compare commits

...

23 Commits

Author SHA1 Message Date
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 2504 additions and 886 deletions
+17 -17
View File
@@ -8,6 +8,7 @@ plugins {
alias(libs.plugins.compose.compiler)
alias(libs.plugins.grgit)
alias(libs.plugins.licensee)
alias(libs.plugins.aboutlibraries)
}
ksp {
@@ -17,9 +18,15 @@ ksp {
licensee {
allowedLicenses().forEach { allow(it) }
allowedLicenseUrls().forEach { allowUrl(it) }
// foss, but missing licenses
ignoreDependencies("com.github.T8RIN.QuickieExtended")
ignoreDependencies("com.github.topjohnwu.libsu")
allowDependency("com.github.T8RIN.QuickieExtended", "quickie-foss", "1.18.1") {
because("FOSS library, but JitPack doesn't publish license metadata")
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> {
@@ -94,21 +101,24 @@ configure<ApplicationExtension> {
"proguard-rules.pro",
)
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 {
applicationIdSuffix = ".debug"
resValue("string", "app_name", "WG Tunnel Debug")
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) {
initWith(buildTypes.getByName(Constants.RELEASE))
applicationIdSuffix = ".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.lottie.compose)
implementation(libs.sonner)
implementation(libs.aboutlibraries.compose)
// Misc utilities
implementation(libs.bundles.misc.utilities)
@@ -271,17 +282,6 @@ dependencies {
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
tasks.configureEach {
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:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.App.Start"
tools:targetApi="tiramisu">
@@ -157,7 +156,7 @@
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="@string/provider"
android:authorities="${providerAuthority}"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
@@ -178,7 +177,7 @@
<service
android:name=".service.tile.TunnelControlTile"
android:exported="true"
android:icon="@drawable/ic_notification"
android:icon="@drawable/qs_logo"
android:label="@string/tunnel_control"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<meta-data
@@ -195,7 +194,7 @@
<service
android:name=".service.tile.AutoTunnelControlTile"
android:exported="true"
android:icon="@drawable/ic_notification"
android:icon="@drawable/qs_logo"
android:label="@string/auto_tunnel"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<meta-data
View File
@@ -1,12 +1,14 @@
package com.zaneschepke.wireguardautotunnel
import ProxySettingsScreen
import android.Manifest
import android.content.Intent
import android.graphics.Color
import android.net.Uri
import android.net.VpnService
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.view.WindowManager
import androidx.activity.SystemBarStyle
import androidx.activity.compose.rememberLauncherForActivityResult
@@ -50,6 +52,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
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.dp
import androidx.compose.ui.zIndex
import androidx.core.app.ActivityCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.lifecycleScope
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.common.banner.AppAlertBanner
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.navigation.Route
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.openWebUrl
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.SharedAppViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.SplitTunnelViewModel
@@ -202,6 +208,48 @@ class MainActivity : AppCompatActivity() {
var requestingTunnelMode by remember {
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 {
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 ->
val host = Uri.parse(url).host ?: url
InfoDialog(
@@ -48,47 +48,51 @@ class RemoteControlReceiver : BroadcastReceiver(), KoinComponent {
}
override fun onReceive(context: Context, intent: Intent) {
val action = intent.action ?: return
val appAction = Action.fromAction(action) ?: return
val pendingResult = goAsync()
applicationScope.launch {
val settings = settingsRepository.getGeneralSettings()
try {
val action = intent.action ?: return@launch
val appAction = Action.fromAction(action) ?: return@launch
if (!settings.isRemoteControlEnabled) return@launch
val settings = settingsRepository.getGeneralSettings()
if (!validateKey(settings, intent)) return@launch
if (!settings.isRemoteControlEnabled) return@launch
when (appAction) {
Action.START_TUNNEL -> {
val tunnel =
resolveTunnel(intent)
?: tunnelsRepository.getDefaultTunnel()
?: return@launch
if (!validateKey(settings, intent)) return@launch
tunnelCoordinator.startTunnel(tunnel)
}
when (appAction) {
Action.START_TUNNEL -> {
val tunnel =
resolveTunnel(intent)
?: tunnelsRepository.getDefaultTunnel()
?: return@launch
Action.STOP_TUNNEL -> {
val tunnelName = intent.getStringExtra(EXTRA_TUN_NAME)
if (tunnelName == null) {
tunnelCoordinator.stopActiveTunnels()
return@launch
tunnelCoordinator.startTunnel(tunnel)
}
val tunnel = tunnelsRepository.findByTunnelName(tunnelName) ?: return@launch
Action.STOP_TUNNEL -> {
val tunnelName = intent.getStringExtra(EXTRA_TUN_NAME)
tunnelCoordinator.stopTunnel(tunnel.id)
}
if (tunnelName == null) {
tunnelCoordinator.stopActiveTunnels()
return@launch
}
Action.START_AUTO_TUNNEL -> {
autoTunnelCoordinator.enable()
}
val tunnel = tunnelsRepository.findByTunnelName(tunnelName) ?: return@launch
Action.STOP_AUTO_TUNNEL -> {
autoTunnelCoordinator.disable()
tunnelCoordinator.stopTunnel(tunnel.id)
}
Action.START_AUTO_TUNNEL -> {
autoTunnelCoordinator.enable()
}
Action.STOP_AUTO_TUNNEL -> {
autoTunnelCoordinator.disable()
}
}
} finally {
pendingResult.finish()
}
}
}
@@ -27,19 +27,31 @@ class RestartReceiver : BroadcastReceiver(), KoinComponent {
override fun onReceive(context: Context, intent: Intent) {
Timber.d("RestartReceiver triggered with action: ${intent.action}")
val pendingResult = goAsync()
applicationScope.launch {
when (intent.action) {
Intent.ACTION_BOOT_COMPLETED,
"android.intent.action.QUICKBOOT_POWERON",
"com.htc.intent.action.QUICKBOOT_POWERON" -> {
startupCoordinator.applyStartupPolicy()
}
Intent.ACTION_MY_PACKAGE_REPLACED -> {
Timber.i("Restoring state on package upgrade")
startupCoordinator.applyStartupPolicy()
logReader.deleteAndClearLogs()
appStateRepository.setShouldShowDonationSnackbar(true)
try {
when (intent.action) {
Intent.ACTION_BOOT_COMPLETED,
"android.intent.action.QUICKBOOT_POWERON",
"com.htc.intent.action.QUICKBOOT_POWERON" -> {
startupCoordinator.applyStartupPolicy()
}
Intent.ACTION_MY_PACKAGE_REPLACED -> {
Timber.i("Restoring state on package upgrade")
startupCoordinator.applyStartupPolicy()
logReader.deleteAndClearLogs()
appStateRepository.setShouldShowDonationSnackbar(true)
}
else -> {
Timber.w("Unhandled action in RestartReceiver: ${intent.action}")
}
}
} finally {
pendingResult.finish()
}
}
}
@@ -39,7 +39,7 @@ class AppBoostrapCoordinator(
listOf(
async { bootstrapDns() },
async { ensureGlobalConfig() },
async { restoreLockdown() },
async { restoreBackendConfiguration() },
)
try {
@@ -73,9 +73,13 @@ class AppBoostrapCoordinator(
tunnelRepository.ensureGlobalConfigExists()
}
private suspend fun restoreLockdown() {
private suspend fun restoreBackendConfiguration() {
val settings = settingsRepository.getGeneralSettings()
if (settings.seamlessRoamingEnabled) {
tunnelProvider.setSeamlessRoaming(true)
}
when (settings.tunnelMode) {
TunnelMode.LOCK_DOWN -> {
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.LockdownSettingsRepository
class TunnelModeCoordinator(
class TunnelBackendCoordinator(
private val tunnelProvider: TunnelProvider,
private val settingsRepository: GeneralSettingRepository,
private val lockdownRepository: LockdownSettingsRepository,
@@ -42,7 +42,6 @@ class TunnelModeCoordinator(
when (newMode) {
TunnelMode.LOCK_DOWN -> {
val lockdownSettings = lockdownRepository.getLockdownSettings()
tunnelProvider.setLockDown(lockdownSettings).getOrThrow()
}
@@ -50,4 +49,9 @@ class TunnelModeCoordinator(
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.model.DnsSettings
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.ProxySettings
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
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.ProxySettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
@@ -44,6 +46,7 @@ class TunnelCoordinator(
dnsSettingsRepository: RoomDnsSettingsRepository,
monitoringSettingsRepository: MonitoringSettingsRepository,
proxyRepository: ProxySettingsRepository,
lockdownModeRepository: LockdownSettingsRepository,
scope: CoroutineScope,
) {
@@ -66,6 +69,7 @@ class TunnelCoordinator(
val dns: DnsSettings,
val monitoring: MonitoringSettings,
val proxy: ProxySettings,
val lockdown: LockdownSettings,
)
private val runtimeSettingsSnapshot =
@@ -74,12 +78,14 @@ class TunnelCoordinator(
dnsSettingsRepository.flow,
monitoringSettingsRepository.flow,
proxyRepository.flow,
) { general, dns, monitoring, proxy ->
lockdownModeRepository.flow,
) { general, dns, monitoring, proxy, lockdown ->
RuntimeSettingsSnapshot(
general = general,
dns = dns,
monitoring = monitoring,
proxy = proxy,
lockdown = lockdown,
)
}
@@ -117,7 +123,7 @@ class TunnelCoordinator(
// enforce single tunnel, for now
if (backendStatus.value.activeTunnels.isNotEmpty()) {
stopActiveTunnelsInternal()
stopActiveTunnelsInternal(source)
}
startTunnelInternal(config, source)
@@ -131,7 +137,13 @@ class TunnelCoordinator(
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(
tunnelConfig: TunnelConfig,
@@ -143,6 +155,7 @@ class TunnelCoordinator(
val dnsSettings = snapshot.dns
val proxySettings = snapshot.proxy
val monitoringSettings = snapshot.monitoring
val lockdownSettings = snapshot.lockdown
val config = tunnelConfig.getConfig()
val policy =
@@ -178,8 +191,10 @@ class TunnelCoordinator(
}
TunnelMode.LOCK_DOWN -> {
BackendMode.Proxy.KillSwitchPrimary(runConfig)
BackendMode.Proxy.KillSwitchPrimary(
runConfig,
lockdownSettings.toKillSwitchConfig(),
)
}
}
@@ -218,7 +233,7 @@ class TunnelCoordinator(
_actions.emit(TunnelActionEvent.Stopped(tunnelId = id, source = source))
}
stopActiveTunnelsInternal()
stopActiveTunnelsInternal(source)
return@withLock
}
@@ -243,7 +258,15 @@ class TunnelCoordinator(
.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()
}
}
@@ -19,9 +19,8 @@ class ShortcutsActivity : ComponentActivity() {
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.model.BackendMode
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 kotlin.concurrent.atomics.ExperimentalAtomicApi
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
@@ -53,9 +50,7 @@ class TunnelBackendProvider(
return backend.disableKillSwitch()
}
@OptIn(ExperimentalCoroutinesApi::class)
private val localErrorEvents = MutableSharedFlow<Pair<String?, BackendCoreException>>()
@OptIn(ExperimentalCoroutinesApi::class)
private val localMessageEvents = MutableSharedFlow<Pair<String?, BackendMessage>>()
override suspend fun setSeamlessRoaming(enabled: Boolean): Result<Unit> {
return backend.setSeamlessRoaming(enabled)
}
}
@@ -20,6 +20,8 @@ interface TunnelProvider {
suspend fun disableLockDown(): Result<Unit>
suspend fun setSeamlessRoaming(enabled: Boolean): Result<Unit>
val backendStatus: StateFlow<BackendStatus>
val events: Flow<TunnelEvent>
@@ -34,7 +34,7 @@ import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
DnsSettings::class,
LockdownSettings::class,
],
version = 30,
version = 32,
autoMigrations =
[
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 = 27, to = 28, spec = DonationMigration::class),
AutoMigration(from = 29, to = 30, spec = SingleConfigMigration::class),
AutoMigration(from = 30, to = 31),
AutoMigration(from = 31, to = 32),
],
exportSchema = true,
)
@@ -18,4 +18,7 @@ interface AutoTunnelSettingsDao {
@Query("UPDATE auto_tunnel_settings SET is_tunnel_enabled = :enabled")
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")
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")
val wifiDetectionMethod: WifiDetectionMethod = WifiDetectionMethod.fromValue(0),
@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,
@ColumnInfo(name = "tunnel_scripting_enabled", defaultValue = "0")
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,
wifiDetectionMethod = wifiDetectionMethod,
startOnBoot = startOnBoot,
disableTunnelOnCaptivePortal = disableTunnelOnCaptivePortal
)
fun Domain.toEntity(): Entity =
@@ -31,4 +32,5 @@ fun Domain.toEntity(): Entity =
isTunnelOnUnsecureEnabled = isTunnelOnUnsecureEnabled,
wifiDetectionMethod = wifiDetectionMethod,
startOnBoot = startOnBoot,
disableTunnelOnCaptivePortal = disableTunnelOnCaptivePortal
)
@@ -22,6 +22,7 @@ fun Entity.toDomain(): Domain =
screenRecordingSecurityEnabled = screenRecordingSecurityEnabled,
isGlobalAmneziaEnabled = isGlobalAmneziaEnabled,
tunnelScriptingEnabled = tunnelScriptingEnabled,
seamlessRoamingEnabled = seamlessRoamingEnabled,
)
fun Domain.toEntity(): Entity =
@@ -42,4 +43,5 @@ fun Domain.toEntity(): Entity =
screenRecordingSecurityEnabled = screenRecordingSecurityEnabled,
isGlobalAmneziaEnabled = isGlobalAmneziaEnabled,
tunnelScriptingEnabled = tunnelScriptingEnabled,
seamlessRoamingEnabled = seamlessRoamingEnabled,
)
@@ -1,15 +1,25 @@
package com.zaneschepke.wireguardautotunnel.data.network
import com.zaneschepke.wireguardautotunnel.BuildConfig
import io.ktor.client.HttpClient
import io.ktor.client.engine.okhttp.OkHttp
import io.ktor.client.plugins.DefaultRequest
import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.http.HttpHeaders
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json
object KtorClient {
fun create(): HttpClient {
return HttpClient(OkHttp) {
install(DefaultRequest) {
headers {
append(HttpHeaders.UserAgent, "wgtunnel/${BuildConfig.VERSION_NAME} (Android)")
append(HttpHeaders.Accept, "*/*")
}
}
install(ContentNegotiation) {
json(
Json {
@@ -18,10 +28,11 @@ object KtorClient {
}
)
}
install(HttpTimeout) {
requestTimeoutMillis = 15000
connectTimeoutMillis = 15000
socketTimeoutMillis = 15000
requestTimeoutMillis = 120_000L
connectTimeoutMillis = 30_000L
socketTimeoutMillis = 120_000L
}
}
}
@@ -26,4 +26,8 @@ class RoomAutoTunnelSettingsRepository(private val autoTunnelSettingsDao: AutoTu
override suspend fun updateAutoTunnelEnabled(enabled: Boolean) {
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) {
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.ConfigEditViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.DnsViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.LicenseViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.LockdownViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.LoggerViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.MonitoringViewModel
@@ -80,7 +79,6 @@ val appModule = module {
viewModelOf(::AutoTunnelViewModel)
viewModel { (id: Int?) -> ConfigEditViewModel(get(), get(), get(), get(), get(), id) }
viewModelOf(::DnsViewModel)
viewModelOf(::LicenseViewModel)
viewModelOf(::LockdownViewModel)
viewModelOf(::LoggerViewModel)
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.ShortcutCoordinator
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.TunnelModeCoordinator
import org.koin.core.module.dsl.singleOf
import org.koin.core.qualifier.named
import org.koin.dsl.module
val coordinatorModule = module {
singleOf(::ShortcutCoordinator)
singleOf(::TunnelModeCoordinator)
singleOf(::TunnelBackendCoordinator)
singleOf(::StartupCoordinator)
singleOf(::AutoTunnelCoordinator)
singleOf(::DnsSettingsCoordinator)
@@ -27,6 +27,7 @@ val coordinatorModule = module {
get(),
get(),
get(),
get(),
get(named(Scope.APPLICATION)),
)
}
@@ -14,4 +14,5 @@ data class AutoTunnelSettings(
val isTunnelOnUnsecureEnabled: Boolean = false,
val wifiDetectionMethod: WifiDetectionMethod = WifiDetectionMethod.fromValue(0),
val startOnBoot: Boolean = false,
val disableTunnelOnCaptivePortal: Boolean = true,
)
@@ -20,5 +20,6 @@ data class GeneralSettings(
val alreadyDonated: Boolean = false,
val screenRecordingSecurityEnabled: Boolean = true,
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 updateAutoTunnelEnabled(enabled: Boolean)
suspend fun updateDisableOnCaptivePortal(enabled: Boolean)
}
@@ -23,4 +23,6 @@ interface GeneralSettingRepository {
suspend fun updateGlobalAmneziaEnabled(enabled: Boolean)
suspend fun updateScreenRecordingSecurity(enabled: Boolean)
suspend fun updateSeamlessRoaming(enabled: Boolean)
}
@@ -11,16 +11,20 @@ sealed class 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(
val activeNetwork: ActiveNetwork = ActiveNetwork.Disconnected,
val locationServicesEnabled: Boolean = false,
val locationPermissionGranted: Boolean = false,
) {
fun hasInternet(): Boolean = activeNetwork !is ActiveNetwork.Disconnected
}
// Has a network that can actually transfer data (not suspended)
val hasUsableNetwork: Boolean = false,
)
fun ConnectivityState.toDomain(): NetworkState {
val domainNetwork: ActiveNetwork =
@@ -33,7 +37,11 @@ fun ConnectivityState.toDomain(): NetworkState {
null -> null
else -> true
}
ActiveNetwork.Wifi(ssid = network.ssid, isSecure = isSecure)
ActiveNetwork.Wifi(
ssid = network.ssid,
isSecure = isSecure,
requiresCaptivePortalLogin(),
)
}
is MonitorActiveNetwork.Cellular -> ActiveNetwork.Cellular
is MonitorActiveNetwork.Ethernet -> ActiveNetwork.Ethernet
@@ -44,5 +52,6 @@ fun ConnectivityState.toDomain(): NetworkState {
activeNetwork = domainNetwork,
locationPermissionGranted = this.locationPermissionsGranted,
locationServicesEnabled = this.locationServicesEnabled,
hasUsableNetwork = hasUsableNetwork(),
)
}
@@ -28,7 +28,19 @@ class AutoTunnelEngine {
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) {
Decision.StopDueToNoInternet
} else {
@@ -192,11 +192,11 @@ class AutoTunnelService : LifecycleService() {
reconciliationMutex.withLock {
val currentNetworkState = networkEngine.stableState.value?.state?.toDomain()
val stillNoInternet = currentNetworkState?.hasInternet() == false
val stillNoUsableNetwork = currentNetworkState?.hasUsableNetwork == false
val stopOnNoInternetEnabled =
autoTunnelRepository.flow.firstOrNull()?.isStopOnNoInternetEnabled == true
if (stillNoInternet && stopOnNoInternetEnabled) {
if (stillNoUsableNetwork && stopOnNoInternetEnabled) {
val currentActiveIds =
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.outlined.Filter1
import androidx.compose.material.icons.outlined.Map
import androidx.compose.material.icons.outlined.PublicOff
import androidx.compose.material.icons.outlined.WifiFind
import androidx.compose.material3.Icon
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 {
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.automirrored.outlined.ViewQuilt
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.ExpandMore
import androidx.compose.material.icons.outlined.MonitorHeart
@@ -234,6 +235,23 @@ fun SettingsScreen(
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(
leading = { Icon(Icons.Outlined.MonitorHeart, null) },
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
import androidx.compose.foundation.layout.Box
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.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.ui.screens.support.license.components.LicenseList
import com.zaneschepke.wireguardautotunnel.viewmodel.LicenseViewModel
import org.koin.androidx.compose.koinViewModel
import org.orbitmvi.orbit.compose.collectAsState
import androidx.compose.ui.platform.LocalContext
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
import com.mikepenz.aboutlibraries.ui.compose.variant.LibraryDetailMode
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun LicenseScreen(viewModel: LicenseViewModel = koinViewModel()) {
val licenseUiState by viewModel.collectAsState()
fun LicenseScreen() {
val context = LocalContext.current
val libs = remember { buildLibsWithAdditionalLibraries(context) }
if (licenseUiState.isLoading) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularWavyProgressIndicator(waveSpeed = 60.dp, modifier = Modifier.size(48.dp))
}
} else {
LicenseList(licenseUiState.licenses)
}
LibrariesContainer(
libraries = libs,
modifier = Modifier.fillMaxSize(),
detailMode = LibraryDetailMode.Sheet,
)
}
@@ -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 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 QR_CODE_NAME_PROPERTY = "# Name ="
const val FDROID_FLAVOR = "fdroid"
const val GOOGLE_PLAY_FLAVOR = "google"
const val STANDALONE_FLAVOR = "standalone"
const val RELEASE = "release"
const val BASE_RELEASE_URL = "https://github.com/wgtunnel/wgtunnel/releases/tag/"
}
@@ -81,7 +81,7 @@ object DnsValidator {
return Result.Valid
}
private fun validateUdp(value: String): DnsValidator.Result {
private fun validateUdp(value: String): Result {
val parts = value.split(":")
val host = parts.getOrNull(0)?.trim()
@@ -93,14 +93,14 @@ object DnsValidator {
// basic IP/hostname sanity check
if (!isValidHostOrIp(host)) {
return DnsValidator.Result.Invalid(DnsError.InvalidIpOrHost)
return Result.Invalid(DnsError.InvalidIpOrHost)
}
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 {
@@ -1,6 +1,5 @@
package com.zaneschepke.wireguardautotunnel.util
import LicenseFileEntry
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
@@ -23,7 +22,6 @@ import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import timber.log.Timber
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
}
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 {
const val CONF_FILE_EXTENSION = ".conf"
const val ZIP_FILE_EXTENSION = ".zip"
@@ -1,22 +1,9 @@
package com.zaneschepke.wireguardautotunnel.util
import com.vdurmont.semver4j.Semver
import java.math.BigDecimal
import kotlin.math.pow
import timber.log.Timber
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 {
return "tunnel${randomFive()}"
@@ -1,26 +1,18 @@
package com.zaneschepke.wireguardautotunnel.util.extensions
import android.Manifest
import android.app.Activity
import android.content.ComponentName
import android.content.Context
import android.content.Context.POWER_SERVICE
import android.content.Intent
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.PowerManager
import android.provider.Settings
import android.service.quicksettings.TileService
import androidx.core.content.FileProvider
import androidx.core.net.toUri
import com.zaneschepke.wireguardautotunnel.BuildConfig
import com.zaneschepke.wireguardautotunnel.MainActivity
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.FileUtils
import java.io.File
@@ -36,11 +28,6 @@ fun Context.openWebUrl(url: String): Result<Unit> = runCatching {
startActivity(intent)
}
fun Context.isBatteryOptimizationsDisabled(): Boolean {
val pm = getSystemService(POWER_SERVICE) as PowerManager
return pm.isIgnoringBatteryOptimizations(packageName)
}
fun Context.launchNotificationSettings() {
if (isRunningOnTv()) return launchAppSettings()
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 {
val intent =
Intent(Intent.ACTION_SENDTO).apply {
@@ -128,7 +100,7 @@ fun Context.isRunningOnTv(): Boolean {
fun Context.launchVpnSettings(): Result<Unit> {
return kotlin.runCatching {
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)
}
}
@@ -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() {
kotlin
.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 {
return packageManager.canRequestPackageInstalls()
}
@@ -227,7 +149,7 @@ fun Context.requestInstallPackagesPermission() {
}
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 =
Intent(Intent.ACTION_VIEW).apply {
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) }
}
fun Int.toMillis(): Long {
return this * 1_000L
}
fun Double.round(decimals: Int): Double {
val factor = 10.0.pow(decimals)
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 {
tunnelsRepository.save(
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.zaneschepke.tunnel.util.RootShell
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelBackendCoordinator
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
@@ -27,6 +28,7 @@ class SettingsViewModel(
private val monitoringRepository: MonitoringSettingsRepository,
private val globalEffectRepository: GlobalEffectRepository,
private val tunnelCoordinator: TunnelCoordinator,
private val tunnelBackendCoordinator: TunnelBackendCoordinator,
) : ContainerHost<SettingUiState, Nothing>, ViewModel() {
override val container =
@@ -114,6 +116,10 @@ class SettingsViewModel(
settingsRepository.upsert(state.settings.copy(tunnelScriptingEnabled = to))
}
fun setSeamlessNetworkRoaming(enabled: Boolean) = intent {
tunnelBackendCoordinator.changeSeamlessRoaming(enabled)
}
fun setAlreadyDonated(to: Boolean) = intent {
settingsRepository.upsert(state.settings.copy(alreadyDonated = to))
}
@@ -5,8 +5,8 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.dokar.sonner.ToastType
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.TunnelModeCoordinator
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
@@ -61,7 +61,7 @@ class SharedAppViewModel(
private val settingsRepository: GeneralSettingRepository,
private val autoTunnelStateHolder: AutoTunnelStateHolder,
private val selectedTunnelsRepository: SelectedTunnelsRepository,
private val tunnelModeCoordinator: TunnelModeCoordinator,
private val tunnelBackendCoordinator: TunnelBackendCoordinator,
private val httpClient: HttpClient,
private val fileUtils: FileUtils,
private val networkUtils: NetworkUtils,
@@ -172,7 +172,7 @@ class SharedAppViewModel(
}
}
tunnelModeCoordinator.changeMode(mode)
tunnelBackendCoordinator.changeMode(mode)
}
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"?>
<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_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="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>
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"?>
<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"/>
<monochrome android:drawable="@mipmap/ic_launcher_foreground"/>
</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