Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot] df716d4035 build(deps): bump compose from 1.7.6 to 1.7.7
Bumps `compose` from 1.7.6 to 1.7.7.

Updates `androidx.compose.ui:ui-test-junit4` from 1.7.6 to 1.7.7

Updates `androidx.compose.ui:ui-tooling` from 1.7.6 to 1.7.7

Updates `androidx.compose.ui:ui-test-manifest` from 1.7.6 to 1.7.7

Updates `androidx.compose.ui:ui-graphics` from 1.7.6 to 1.7.7

Updates `androidx.compose.ui:ui-tooling-preview` from 1.7.6 to 1.7.7

Updates `androidx.compose.ui:ui` from 1.7.6 to 1.7.7

Updates `androidx.compose.material:material-icons-extended` from 1.7.6 to 1.7.7

---
updated-dependencies:
- dependency-name: androidx.compose.ui:ui-test-junit4
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: androidx.compose.ui:ui-tooling
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: androidx.compose.ui:ui-test-manifest
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: androidx.compose.ui:ui-graphics
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: androidx.compose.ui:ui-tooling-preview
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: androidx.compose.ui:ui
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: androidx.compose.material:material-icons-extended
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-30 13:26:48 +00:00
335 changed files with 7933 additions and 10790 deletions
+7 -7
View File
@@ -4,7 +4,7 @@ WG Tunnel
<div align="center">
An alternative Android client app for [WireGuard](https://www.wireguard.com/)
An alternative Android client app for [WireGuard®](https://www.wireguard.com/)
and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/)
<br />
<br />
@@ -23,14 +23,14 @@ and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/)
[![Google Play](https://img.shields.io/badge/Google_Play-414141?style=for-the-badge&logo=google-play&logoColor=white)](https://play.google.com/store/apps/details?id=com.zaneschepke.wireguardautotunnel)
[![F-Droid](https://img.shields.io/static/v1?style=for-the-badge&message=F-Droid&color=1976D2&logo=F-Droid&logoColor=FFFFFF&label=)](https://f-droid.org/packages/com.zaneschepke.wireguardautotunnel/)
[![Personal](https://img.shields.io/static/v1?style=for-the-badge&message=Personal&color=1976D2&logo=F-Droid&logoColor=FFFFFF&label=)](https://github.com/zaneschepke/fdroid)
[![Obtainium](https://img.shields.io/badge/Obtainium-414141?style=for-the-badge&logo=Obtainium&logoColor=white)](https://apps.obtainium.imranr.dev/redirect?r=obtainium://app/%7B%22id%22%3A%22com.zaneschepke.wireguardautotunnel%22%2C%22url%22%3A%22https%3A%2F%2Fgithub.com%2Fzaneschepke%2Fwgtunnel%22%2C%22author%22%3A%22zaneschepke%22%2C%22name%22%3A%22WG%20Tunnel%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Atrue%2C%5C%22sortMethodChoice%5C%22%3A%5C%22date%5C%22%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Atrue%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22WG%20Tunnel%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22Zane%20Schepke%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%7D%22%2C%22overrideSource%22%3Anull%7D)
</div>
<div align="center">
[<img src="https://img.shields.io/badge/Telegram-26A5E4.svg?style=for-the-badge&logo=Telegram&logoColor=white">](https://t.me/wgtunnel)
[<img src="https://img.shields.io/badge/Matrix-000000.svg?style=for-the-badge&logo=Matrix&logoColor=white">](https://matrix.to/#/#wg-tunnel-space:matrix.org)
[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?style=for-the-badge&logo=discord&logoColor=white)](https://discord.gg/rbRRNh6H7V)
[![Telegram](https://img.shields.io/badge/Telegram-2CA5E0?style=for-the-badge&logo=telegram&logoColor=white)](https://t.me/wgtunnel)
</div>
<details open="open">
@@ -49,7 +49,7 @@ and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/)
<div style="text-align: left;">
## About
Inspired by the official [wireguard-android](https://github.com/WireGuard/wireguard-android) app, WG Tunnel was created to address features and support missing from the official app. This app combines support for both [WireGuard](https://www.wireguard.com/)
Inspired by the official [wireguard-android](https://github.com/WireGuard/wireguard-android) app, WG Tunnel was created to address features and support missing from the official app. This app combines support for both [WireGuard®](https://www.wireguard.com/)
and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/), with its primary feature of auto-tunneling (on-demand tunneling).
</div>
@@ -61,14 +61,14 @@ and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/), with its pr
Thank you to the following:
- All of the users that have helped contribute to the project with ideas, translations, feedback, bug reports, testing, and donations.
- [WireGuard](https://www.wireguard.com/) - Jason A. Donenfeld (https://github.com/WireGuard/wireguard-android)
- [WireGuard®](https://www.wireguard.com/) - © Jason A. Donenfeld (https://github.com/WireGuard/wireguard-android)
- [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) - Amnezia Team (https://github.com/amnezia-vpn/amneziawg-android)
## Screenshots
</div>
<div style="display: flex; flex-wrap: wrap; justify-content: left; gap: 10px;">
<div style="display: flex; flex-wrap: wrap; justify-content: center; gap: 10px;">
<img label="Main" src="fastlane/metadata/android/en-US/images/phoneScreenshots/main_screen.png" width="200" />
<img label="Settings" src="fastlane/metadata/android/en-US/images/phoneScreenshots/settings_screen.png" width="200" />
<img label="Auto" src="fastlane/metadata/android/en-US/images/phoneScreenshots/auto_screen.png" width="200" />
+9 -10
View File
@@ -86,6 +86,7 @@ android {
}
debug {
applicationIdSuffix = ".debug"
versionNameSuffix = "-debug"
resValue("string", "app_name", "WG Tunnel - Debug")
isDebuggable = true
resValue("string", "provider", "\"${Constants.APP_NAME}.provider.debug\"")
@@ -94,6 +95,7 @@ android {
create(Constants.PRERELEASE) {
initWith(buildTypes.getByName(Constants.RELEASE))
applicationIdSuffix = ".prerelease"
versionNameSuffix = "-pre"
resValue("string", "app_name", "WG Tunnel - Pre")
resValue("string", "provider", "\"${Constants.APP_NAME}.provider.pre\"")
}
@@ -101,6 +103,7 @@ android {
create(Constants.NIGHTLY) {
initWith(buildTypes.getByName(Constants.RELEASE))
applicationIdSuffix = ".nightly"
versionNameSuffix = "-nightly"
resValue("string", "app_name", "WG Tunnel - Nightly")
resValue("string", "provider", "\"${Constants.APP_NAME}.provider.nightly\"")
}
@@ -141,8 +144,8 @@ android {
}
dependencies {
implementation(project(":logcatter"))
implementation(project(":networkmonitor"))
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
@@ -157,7 +160,6 @@ dependencies {
implementation(libs.androidx.material3)
implementation(libs.androidx.appcompat)
implementation(libs.material)
implementation(libs.androidx.storage)
// test
testImplementation(libs.junit)
@@ -170,7 +172,8 @@ dependencies {
debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.manifest)
// tunnel
// get tunnel lib from github packages or mavenLocal
// implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar"))))
implementation(libs.tunnel)
implementation(libs.amneziawg.android)
coreLibraryDesugaring(libs.desugar.jdk.libs)
@@ -185,7 +188,6 @@ dependencies {
// hilt
implementation(libs.hilt.android)
ksp(libs.hilt.android.compiler)
ksp(libs.androidx.hilt.compiler)
// accompanist
implementation(libs.accompanist.permissions)
@@ -202,12 +204,13 @@ dependencies {
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.process)
// icons
implementation(libs.material.icons.extended)
// serialization
implementation(libs.kotlinx.serialization.json)
// ui
// barcode scanning
implementation(libs.zxing.android.embedded)
implementation(libs.material.icons.extended)
// bio
implementation(libs.androidx.biometric.ktx)
@@ -218,10 +221,6 @@ dependencies {
// splash
implementation(libs.androidx.core.splashscreen)
// worker
implementation(libs.androidx.work.runtime)
implementation(libs.androidx.hilt.work)
}
fun determineVersionName(): String {
+1 -38
View File
@@ -2,41 +2,4 @@
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
<fields>;
}
# Keep all classes in the org.xbill.DNS package and subpackages
-keep class org.xbill.DNS.** { *; }
-dontwarn org.xbill.DNS.**
# Preserve JNA classes if used (e.g., for IPHlpAPI on Windows)
-keep class com.sun.jna.** { *; }
-dontwarn com.sun.jna.**
# Keep DNS resolver configuration classes that might be loaded dynamically
-keep class org.xbill.DNS.config.** { *; }
-dontwarn org.xbill.DNS.config.**
-keep class org.xbill.DNS.** { *; }
# Prevent optimization issues with native or reflection-based calls
-dontoptimize
-dontshrink
# Uncomment the above if errors persist, but use sparingly as theyre broad
# Suppress warnings about missing classes if not all features are used
-dontwarn java.lang.management.**
-dontwarn sun.nio.ch.**
-dontwarn com.google.api.client.http.GenericUrl
-dontwarn com.google.api.client.http.HttpHeaders
-dontwarn com.google.api.client.http.HttpRequest
-dontwarn com.google.api.client.http.HttpRequestFactory
-dontwarn com.google.api.client.http.HttpResponse
-dontwarn com.google.api.client.http.HttpTransport
-dontwarn com.google.api.client.http.javanet.NetHttpTransport$Builder
-dontwarn com.google.api.client.http.javanet.NetHttpTransport
-dontwarn javax.lang.model.element.Modifier
-dontwarn org.joda.time.Instant
-dontwarn org.slf4j.impl.StaticLoggerBinder
-dontwarn org.slf4j.impl.StaticMDCBinder
-dontwarn org.slf4j.impl.StaticMarkerBinder
}
+1 -38
View File
@@ -21,41 +21,4 @@
#-renamesourcefileattribute SourceFile
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
<fields>;
}
# Keep all classes in the org.xbill.DNS package and subpackages
-keep class org.xbill.DNS.** { *; }
-dontwarn org.xbill.DNS.**
# Preserve JNA classes if used (e.g., for IPHlpAPI on Windows)
-keep class com.sun.jna.** { *; }
-dontwarn com.sun.jna.**
# Keep DNS resolver configuration classes that might be loaded dynamically
-keep class org.xbill.DNS.config.** { *; }
-dontwarn org.xbill.DNS.config.**
-keep class org.xbill.DNS.** { *; }
# Prevent optimization issues with native or reflection-based calls
-dontoptimize
-dontshrink
# Uncomment the above if errors persist, but use sparingly as theyre broad
# Suppress warnings about missing classes if not all features are used
-dontwarn java.lang.management.**
-dontwarn sun.nio.ch.**
-dontwarn com.google.api.client.http.GenericUrl
-dontwarn com.google.api.client.http.HttpHeaders
-dontwarn com.google.api.client.http.HttpRequest
-dontwarn com.google.api.client.http.HttpRequestFactory
-dontwarn com.google.api.client.http.HttpResponse
-dontwarn com.google.api.client.http.HttpTransport
-dontwarn com.google.api.client.http.javanet.NetHttpTransport$Builder
-dontwarn com.google.api.client.http.javanet.NetHttpTransport
-dontwarn javax.lang.model.element.Modifier
-dontwarn org.joda.time.Instant
-dontwarn org.slf4j.impl.StaticLoggerBinder
-dontwarn org.slf4j.impl.StaticMDCBinder
-dontwarn org.slf4j.impl.StaticMarkerBinder
}
@@ -1,288 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 16,
"identityHash": "ae51793c4d09ea3194ecd26f0606f35c",
"entities": [
{
"tableName": "Settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `is_amnezia_enabled` INTEGER NOT NULL DEFAULT false, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT false, `is_wifi_by_shell_enabled` INTEGER NOT NULL DEFAULT false, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT false, `is_vpn_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_lan_on_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `debounce_delay_seconds` INTEGER NOT NULL DEFAULT 3, `is_disable_kill_switch_on_trusted_enabled` INTEGER NOT NULL DEFAULT false)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isAutoTunnelEnabled",
"columnName": "is_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnMobileDataEnabled",
"columnName": "is_tunnel_on_mobile_data_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "trustedNetworkSSIDs",
"columnName": "trusted_network_ssids",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isAlwaysOnVpnEnabled",
"columnName": "is_always_on_vpn_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnEthernetEnabled",
"columnName": "is_tunnel_on_ethernet_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isShortcutsEnabled",
"columnName": "is_shortcuts_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isTunnelOnWifiEnabled",
"columnName": "is_tunnel_on_wifi_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isKernelEnabled",
"columnName": "is_kernel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isRestoreOnBootEnabled",
"columnName": "is_restore_on_boot_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isMultiTunnelEnabled",
"columnName": "is_multi_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isAmneziaEnabled",
"columnName": "is_amnezia_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isWildcardsEnabled",
"columnName": "is_wildcards_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isWifiNameByShellEnabled",
"columnName": "is_wifi_by_shell_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isStopOnNoInternetEnabled",
"columnName": "is_stop_on_no_internet_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isVpnKillSwitchEnabled",
"columnName": "is_vpn_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isKernelKillSwitchEnabled",
"columnName": "is_kernel_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isLanOnKillSwitchEnabled",
"columnName": "is_lan_on_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "debounceDelaySeconds",
"columnName": "debounce_delay_seconds",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "3"
},
{
"fieldPath": "isDisableKillSwitchOnTrustedEnabled",
"columnName": "is_disable_kill_switch_on_trusted_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "TunnelConfig",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false, `is_primary_tunnel` INTEGER NOT NULL DEFAULT false, `am_quick` TEXT NOT NULL DEFAULT '', `is_Active` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `ping_interval` INTEGER DEFAULT null, `ping_cooldown` INTEGER DEFAULT null, `ping_ip` TEXT DEFAULT null, `is_ethernet_tunnel` INTEGER NOT NULL DEFAULT false, `is_ipv4_preferred` INTEGER NOT NULL DEFAULT true)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "wgQuick",
"columnName": "wg_quick",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tunnelNetworks",
"columnName": "tunnel_networks",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isMobileDataTunnel",
"columnName": "is_mobile_data_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPrimaryTunnel",
"columnName": "is_primary_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "amQuick",
"columnName": "am_quick",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isActive",
"columnName": "is_Active",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "pingInterval",
"columnName": "ping_interval",
"affinity": "INTEGER",
"notNull": false,
"defaultValue": "null"
},
{
"fieldPath": "pingCooldown",
"columnName": "ping_cooldown",
"affinity": "INTEGER",
"notNull": false,
"defaultValue": "null"
},
{
"fieldPath": "pingIp",
"columnName": "ping_ip",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "null"
},
{
"fieldPath": "isEthernetTunnel",
"columnName": "is_ethernet_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isIpv4Preferred",
"columnName": "is_ipv4_preferred",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "true"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_TunnelConfig_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ae51793c4d09ea3194ecd26f0606f35c')"
]
}
}
+42 -21
View File
@@ -3,8 +3,13 @@
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<!--foreground service exempt android 14-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"
@@ -61,9 +66,9 @@
android:theme="@style/Theme.App.Start"
tools:targetApi="tiramisu">
<activity
android:name=".MainActivity"
android:name=".ui.MainActivity"
android:exported="true"
android:windowSoftInputMode="adjustNothing"
android:windowSoftInputMode="adjustResize"
android:theme="@style/Theme.WireguardAutoTunnel"
android:configChanges="orientation|screenSize|keyboardHidden"
>
@@ -82,7 +87,7 @@
tools:replace="screenOrientation" />
<activity
android:name=".core.shortcut.ShortcutsActivity"
android:name=".service.shortcut.ShortcutsActivity"
android:enabled="true"
android:exported="true"
android:noHistory="true"
@@ -101,15 +106,8 @@
android:resource="@xml/file_paths" />
</provider>
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:multiprocess="true"
tools:node="remove">
</provider>
<service
android:name=".core.service.tile.TunnelControlTile"
android:name=".service.tile.TunnelControlTile"
android:exported="true"
android:icon="@drawable/ic_launcher"
android:label="@string/tunnel_control"
@@ -126,7 +124,7 @@
</intent-filter>
</service>
<service
android:name=".core.service.tile.AutoTunnelControlTile"
android:name=".service.tile.AutoTunnelControlTile"
android:exported="true"
android:icon="@drawable/ic_launcher"
android:label="@string/auto_tunnel"
@@ -142,8 +140,23 @@
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
<service
android:name=".service.tunnel.AlwaysOnVpnService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="systemExempted"
android:permission="android.permission.BIND_VPN_SERVICE"
android:persistent="true"
tools:node="merge">
<intent-filter>
<action android:name="android.net.VpnService" />
</intent-filter>
<meta-data
android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
android:value="true" />
</service>
<service
android:name=".core.service.autotunnel.AutoTunnelService"
android:name=".service.foreground.autotunnel.AutoTunnelService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="systemExempted"
@@ -152,7 +165,7 @@
tools:node="merge" />
<service
android:name=".core.service.TunnelForegroundService"
android:name=".service.foreground.TunnelForegroundService"
android:exported="false"
android:persistent="true"
android:foregroundServiceType="systemExempted"
@@ -163,19 +176,27 @@
</service>
<receiver
android:name=".core.broadcast.RestartReceiver"
android:name=".receiver.BootReceiver"
android:enabled="true"
android:exported="true">
android:exported="false">
<intent-filter>
<category android:name="android.intent.category.DEFAULT" />
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.ACTION_BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
</receiver>
<receiver
android:name=".core.broadcast.KernelReceiver"
android:name=".receiver.AppUpdateReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
</receiver>
<receiver
android:name=".receiver.KernelReceiver"
android:exported="false"
android:permission="${applicationId}.permission.CONTROL_TUNNELS">
<intent-filter>
@@ -183,7 +204,7 @@
</intent-filter>
</receiver>
<receiver
android:name=".core.broadcast.NotificationActionReceiver"
android:name=".receiver.NotificationActionReceiver"
android:exported="false"
android:permission="${applicationId}.permission.CONTROL_TUNNELS">
</receiver>
@@ -1,390 +0,0 @@
package com.zaneschepke.wireguardautotunnel
import android.Manifest
import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Color
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import androidx.activity.SystemBarStyle
import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Bolt
import androidx.compose.material.icons.rounded.Home
import androidx.compose.material.icons.rounded.QuestionMark
import androidx.compose.material.icons.rounded.Settings
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarData
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import androidx.navigation.toRoute
import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.VpnDeniedDialog
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.CustomBottomNavbar
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.DynamicTopAppBar
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.currentNavBackStackEntryAsNavBarState
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.CustomSnackBar
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.main.autotunnel.TunnelAutoTunnelScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.ConfigScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.main.scanner.ScannerScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.main.splittunnel.SplitTunnelScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.main.tunneloptions.TunnelOptionsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.pin.PinLockScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.AppearanceScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.display.DisplayScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.language.LanguageScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.AutoTunnelScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.advanced.AdvancedScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.disclosure.LocationDisclosureScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.killswitch.KillSwitchScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.logs.LogsScreen
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
import com.zaneschepke.wireguardautotunnel.util.extensions.goFromRoot
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
import dagger.hilt.android.AndroidEntryPoint
import org.amnezia.awg.backend.GoBackend.VpnService
import timber.log.Timber
import javax.inject.Inject
import kotlin.system.exitProcess
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var appStateRepository: AppStateRepository
@Inject
lateinit var tunnelManager: TunnelManager
@Inject
lateinit var networkMonitor: NetworkMonitor
private var lastLocationPermissionState: Boolean? = null
@SuppressLint("BatteryLife")
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge(
statusBarStyle = SystemBarStyle.Companion.auto(Color.TRANSPARENT, Color.TRANSPARENT),
navigationBarStyle = SystemBarStyle.Companion.auto(Color.TRANSPARENT, Color.TRANSPARENT),
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
window.isNavigationBarContrastEnforced = false
}
super.onCreate(savedInstanceState)
val viewModel by viewModels<AppViewModel>()
installSplashScreen().apply {
setKeepOnScreenCondition {
!viewModel.appViewState.value.isAppReady
}
}
setContent {
val appUiState by viewModel.uiState.collectAsStateWithLifecycle()
val appViewState by viewModel.appViewState.collectAsStateWithLifecycle()
val navController = rememberNavController()
val backStackEntry by navController.currentBackStackEntryAsState()
val navBarState by currentNavBackStackEntryAsNavBarState(navController, backStackEntry, viewModel, appUiState)
val snackbar = remember { SnackbarHostState() }
var showVpnPermissionDialog by remember { mutableStateOf(false) }
var vpnPermissionDenied by remember { mutableStateOf(false) }
val vpnActivity =
rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult(),
onResult = {
if (it.resultCode != RESULT_OK) {
showVpnPermissionDialog = true
vpnPermissionDenied = true
} else {
vpnPermissionDenied = false
}
},
)
LaunchedEffect(appUiState.tunnels) {
if (!appViewState.isAppReady) {
viewModel.handleEvent(AppEvent.AppReadyCheck(appUiState.tunnels))
}
}
val batteryActivity = rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult(),
) { _: ActivityResult ->
viewModel.handleEvent(AppEvent.SetBatteryOptimizeDisableShown)
}
with(appViewState) {
LaunchedEffect(isConfigChanged) {
if (isConfigChanged) {
Intent(this@MainActivity, MainActivity::class.java).also {
startActivity(it)
exitProcess(0)
}
}
}
LaunchedEffect(errorMessage) {
errorMessage?.let {
snackbar.showSnackbar(it.asString(this@MainActivity))
viewModel.handleEvent(AppEvent.MessageShown)
}
}
LaunchedEffect(appUiState.activeTunnels) {
appUiState.activeTunnels
.mapNotNull { (tunnelConf, tunnelState) ->
(tunnelState.status as? TunnelStatus.Error)?.let { error ->
val message = error.error.toStringRes()
val context = this@MainActivity
snackbar.showSnackbar(context.getString(R.string.tunnel_error_template, context.getString(message)))
viewModel.handleEvent(AppEvent.ClearTunnelError(tunnelConf))
}
}
}
LaunchedEffect(popBackStack) {
if (popBackStack) {
navController.popBackStack()
viewModel.handleEvent(AppEvent.PopBackStack(false))
}
}
LaunchedEffect(requestVpnPermission) {
if (requestVpnPermission) {
if (!vpnPermissionDenied) {
vpnActivity.launch(VpnService.prepare(this@MainActivity))
} else {
showVpnPermissionDialog = true
}
viewModel.handleEvent(AppEvent.VpnPermissionRequested)
}
}
LaunchedEffect(requestBatteryPermission) {
if (requestBatteryPermission) {
batteryActivity.launch(
Intent().apply {
action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
data = Uri.parse("package:${this@MainActivity.packageName}")
},
)
}
}
}
CompositionLocalProvider(LocalNavController provides navController) {
WireguardAutoTunnelTheme(theme = appUiState.generalState.theme) {
VpnDeniedDialog(showVpnPermissionDialog, onDismiss = { showVpnPermissionDialog = false })
Scaffold(
modifier = Modifier.pointerInput(Unit) {
detectTapGestures {
viewModel.handleEvent(AppEvent.SetSelectedTunnel(null))
}
},
snackbarHost = {
SnackbarHost(snackbar) { snackbarData: SnackbarData ->
CustomSnackBar(
snackbarData.visuals.message,
isRtl = false,
containerColor =
MaterialTheme.colorScheme.surfaceColorAtElevation(
2.dp,
),
)
}
},
topBar = {
DynamicTopAppBar(navBarState)
},
bottomBar = {
AnimatedVisibility(
visible = navBarState.showBottom,
enter = slideInVertically(initialOffsetY = { it }),
exit = slideOutVertically(targetOffsetY = { it }),
) {
CustomBottomNavbar(
listOf(
BottomNavItem(
name = stringResource(R.string.tunnels),
route = Route.Main,
icon = Icons.Rounded.Home,
onClick = { navController.goFromRoot(Route.Main) },
),
BottomNavItem(
name = stringResource(R.string.auto_tunnel),
route = Route.AutoTunnel,
icon = Icons.Rounded.Bolt,
onClick = {
val route = if (appUiState.generalState.isLocationDisclosureShown) Route.AutoTunnel else Route.LocationDisclosure
navController.goFromRoot(route)
},
active = appUiState.isAutoTunnelActive,
),
BottomNavItem(
name = stringResource(R.string.settings),
route = Route.Settings,
icon = Icons.Rounded.Settings,
onClick = { navController.goFromRoot(Route.Settings) },
),
BottomNavItem(
name = stringResource(R.string.support),
route = Route.Support,
icon = Icons.Rounded.QuestionMark,
onClick = { navController.goFromRoot(Route.Support) },
),
),
navBarState = navBarState,
)
}
},
) { padding ->
Box(
modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.surface)
.padding(padding)
.consumeWindowInsets(padding)
.imePadding(),
) {
NavHost(
navController,
startDestination = (if (appUiState.generalState.isPinLockEnabled) Route.Lock else Route.Main),
) {
composable<Route.Main> {
MainScreen(appUiState, appViewState, viewModel)
}
composable<Route.Settings> {
SettingsScreen(appUiState, appViewState, viewModel)
}
composable<Route.LocationDisclosure> {
LocationDisclosureScreen(appUiState, viewModel)
}
composable<Route.AutoTunnel> {
AutoTunnelScreen(appUiState, viewModel)
}
composable<Route.Appearance> {
AppearanceScreen()
}
composable<Route.Language> {
LanguageScreen(appUiState, viewModel)
}
composable<Route.Display> {
DisplayScreen(appUiState, viewModel)
}
composable<Route.Support> {
SupportScreen()
}
composable<Route.AutoTunnelAdvanced> {
AdvancedScreen(appUiState)
}
composable<Route.Logs> {
LogsScreen(appViewState, viewModel)
}
composable<Route.Config> { backStack ->
val args = backStack.toRoute<Route.Config>()
val config = appUiState.tunnels.firstOrNull { it.id == args.id }
ConfigScreen(config, viewModel)
}
composable<Route.TunnelOptions> { backStack ->
val args = backStack.toRoute<Route.TunnelOptions>()
appUiState.tunnels.firstOrNull { it.id == args.id }?.let { config ->
TunnelOptionsScreen(config, appUiState, viewModel)
}
}
composable<Route.Lock> {
PinLockScreen(viewModel)
}
composable<Route.Scanner> {
ScannerScreen(viewModel)
}
composable<Route.KillSwitch> {
KillSwitchScreen(appUiState, viewModel)
}
composable<Route.SplitTunnel> {
SplitTunnelScreen(viewModel)
}
composable<Route.TunnelAutoTunnel> { backStack ->
val args = backStack.toRoute<Route.TunnelOptions>()
appUiState.tunnels.firstOrNull { it.id == args.id }?.let {
TunnelAutoTunnelScreen(it, appUiState.appSettings, viewModel)
}
}
}
BackHandler {
if (navController.currentDestination?.route != Route.Main::class.qualifiedName) {
navController.popBackStack()
} else {
this@MainActivity.finish()
}
}
}
}
}
}
}
}
override fun onResume() {
super.onResume()
checkPermissionAndNotify()
}
private fun checkPermissionAndNotify() {
val hasLocation = ContextCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_FINE_LOCATION,
) == PackageManager.PERMISSION_GRANTED
if (lastLocationPermissionState != hasLocation) {
Timber.d("Location permission changed to: $hasLocation")
if (hasLocation) {
networkMonitor.sendLocationPermissionsGrantedBroadcast()
}
lastLocationPermissionState = hasLocation
}
}
}
@@ -3,22 +3,20 @@ package com.zaneschepke.wireguardautotunnel
import android.app.Application
import android.os.StrictMode
import android.os.StrictMode.ThreadPolicy
import androidx.hilt.work.HiltWorkerFactory
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.work.Configuration
import com.wireguard.android.backend.GoBackend
import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.core.worker.ServiceWorker
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.di.MainDispatcher
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.module.MainDispatcher
import com.zaneschepke.wireguardautotunnel.service.tunnel.BackendState
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
@@ -28,15 +26,7 @@ import timber.log.Timber
import javax.inject.Inject
@HiltAndroidApp
class WireGuardAutoTunnel : Application(), Configuration.Provider {
@Inject
lateinit var workerFactory: HiltWorkerFactory
override val workManagerConfiguration: Configuration
get() = Configuration.Builder()
.setWorkerFactory(workerFactory)
.build()
class WireGuardAutoTunnel : Application() {
@Inject
@ApplicationScope
@@ -46,7 +36,13 @@ class WireGuardAutoTunnel : Application(), Configuration.Provider {
lateinit var logReader: LogReader
@Inject
lateinit var appDataRepository: AppDataRepository
lateinit var appStateRepository: AppStateRepository
@Inject
lateinit var settingsRepository: SettingsRepository
@Inject
lateinit var tunnelService: TunnelService
@Inject
@IoDispatcher
@@ -56,9 +52,6 @@ class WireGuardAutoTunnel : Application(), Configuration.Provider {
@MainDispatcher
lateinit var mainDispatcher: CoroutineDispatcher
@Inject
lateinit var tunnelManager: TunnelManager
override fun onCreate() {
super.onCreate()
instance = this
@@ -77,40 +70,24 @@ class WireGuardAutoTunnel : Application(), Configuration.Provider {
Timber.plant(ReleaseTree())
}
GoBackend.setAlwaysOnCallback {
applicationScope.launch {
val settings = appDataRepository.settings.get()
if (settings.isAlwaysOnVpnEnabled) {
val tunnel = appDataRepository.getPrimaryOrFirstTunnel()
tunnel?.let {
tunnelManager.startTunnel(it)
}
} else {
Timber.w("Always-on VPN is not enabled in app settings")
}
}
}
ServiceWorker.start(this)
applicationScope.launch {
if (!appDataRepository.settings.get().isKernelEnabled) {
tunnelManager.setBackendState(BackendState.SERVICE_ACTIVE, emptyList())
withContext(mainDispatcher) {
if (appStateRepository.isLocalLogsEnabled() && !isRunningOnTv()) logReader.initialize()
}
appDataRepository.appState.getLocale()?.let {
if (!settingsRepository.getSettings().isKernelEnabled) {
tunnelService.setBackendState(BackendState.SERVICE_ACTIVE, emptyList())
}
appStateRepository.getLocale()?.let {
withContext(mainDispatcher) {
LocaleUtil.changeLocale(it)
}
}
appDataRepository.appState.isLocalLogsEnabled().let { enabled ->
if (enabled) logReader.start()
}
}
}
override fun onTerminate() {
applicationScope.launch {
tunnelManager.setBackendState(BackendState.SERVICE_ACTIVE, emptyList())
tunnelService.setBackendState(BackendState.INACTIVE, emptyList())
}
super.onTerminate()
}
@@ -134,19 +111,6 @@ class WireGuardAutoTunnel : Application(), Configuration.Provider {
return foreground
}
@Volatile
private var lastActiveTunnels: List<Int> = emptyList()
@Synchronized
fun getLastActiveTunnels(): List<Int> {
return lastActiveTunnels
}
@Synchronized
fun setLastActiveTunnels(newTunnels: List<Int>) {
lastActiveTunnels = newTunnels
}
lateinit var instance: WireGuardAutoTunnel
private set
}
@@ -1,50 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.broadcast
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint
class NotificationActionReceiver : BroadcastReceiver() {
@Inject
lateinit var serviceManager: ServiceManager
@Inject
lateinit var tunnelManager: TunnelManager
@Inject
lateinit var tunnelRepository: TunnelRepository
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
override fun onReceive(context: Context, intent: Intent) {
applicationScope.launch {
when (intent.action) {
NotificationAction.AUTO_TUNNEL_OFF.name -> serviceManager.stopAutoTunnel()
NotificationAction.TUNNEL_OFF.name -> {
val tunnelId = intent.getIntExtra(NotificationManager.EXTRA_ID, 0)
if (tunnelId == STOP_ALL_TUNNELS_ID) return@launch tunnelManager.stopTunnel()
val tunnel = tunnelRepository.getById(tunnelId)
tunnelManager.stopTunnel(tunnel)
}
}
}
}
companion object {
const val STOP_ALL_TUNNELS_ID = 0
}
}
@@ -1,64 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.broadcast
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class RestartReceiver : BroadcastReceiver() {
@Inject
lateinit var appDataRepository: AppDataRepository
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
@Inject
lateinit var serviceManager: ServiceManager
@Inject
lateinit var tunnelManager: TunnelManager
@Inject
@IoDispatcher
lateinit var ioDispatcher: CoroutineDispatcher
override fun onReceive(context: Context, intent: Intent) {
val action = intent.action ?: return
if (action != Intent.ACTION_BOOT_COMPLETED &&
action != Intent.ACTION_MY_PACKAGE_REPLACED &&
action != "com.htc.intent.action.QUICKBOOT_POWERON"
) {
return
}
Timber.d("RestartReceiver triggered with action: ${intent.action}")
serviceManager.updateTunnelTile()
serviceManager.updateAutoTunnelTile()
applicationScope.launch(ioDispatcher) {
val settings = appDataRepository.settings.get()
if (settings.isRestoreOnBootEnabled) {
if (settings.isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) {
Timber.d("Starting auto-tunnel on boot/update")
serviceManager.startAutoTunnel()
} else {
Timber.d("Restoring previous tunnel state")
tunnelManager.restorePreviousState()
}
} else {
Timber.d("Restore on boot disabled, skipping")
}
}
}
}
@@ -1,130 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.service
import android.app.Service
import android.content.Context
import android.content.Intent
import android.net.VpnService
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelService
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.di.MainDispatcher
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
import jakarta.inject.Inject
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import timber.log.Timber
class ServiceManager @Inject constructor(
private val context: Context,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
@ApplicationScope private val applicationScope: CoroutineScope,
@MainDispatcher private val mainDispatcher: CoroutineDispatcher,
private val appDataRepository: AppDataRepository,
) {
private val autoTunnelMutex = Mutex()
private val _autoTunnelActive = MutableStateFlow(false)
val autoTunnelActive = _autoTunnelActive.asStateFlow()
var autoTunnelService = CompletableDeferred<AutoTunnelService>()
var backgroundService = CompletableDeferred<TunnelForegroundService>()
private fun <T : Service> startService(cls: Class<T>, background: Boolean) {
runCatching {
val intent = Intent(context, cls)
if (background) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
}.onFailure { Timber.e(it) }
}
fun hasVpnPermission(): Boolean {
return VpnService.prepare(context) == null
}
suspend fun startAutoTunnel() {
autoTunnelMutex.withLock {
val settings = appDataRepository.settings.get()
appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = true))
if (autoTunnelService.isCompleted) {
_autoTunnelActive.update { true }
return
}
runCatching {
autoTunnelService = CompletableDeferred()
startService(AutoTunnelService::class.java, !WireGuardAutoTunnel.isForeground())
_autoTunnelActive.update { true }
}.onFailure {
Timber.e(it)
_autoTunnelActive.update { false }
}
withContext(mainDispatcher) { updateAutoTunnelTile() }
}
}
suspend fun stopAutoTunnel() {
autoTunnelMutex.withLock {
val settings = appDataRepository.settings.get()
appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = false))
if (!autoTunnelService.isCompleted) return
runCatching {
val service = autoTunnelService.await()
service.stop()
_autoTunnelActive.update { false }
autoTunnelService = CompletableDeferred()
}.onFailure {
Timber.e(it)
}
withContext(mainDispatcher) { updateAutoTunnelTile() }
}
}
fun startTunnelForegroundService() {
if (backgroundService.isCompleted) return
runCatching {
backgroundService = CompletableDeferred()
startService(TunnelForegroundService::class.java, !WireGuardAutoTunnel.isForeground())
}.onFailure {
Timber.e(it)
}
}
suspend fun stopTunnelForegroundService() {
if (!backgroundService.isCompleted) return
runCatching {
val service = backgroundService.await()
service.stop()
backgroundService = CompletableDeferred()
}.onFailure {
Timber.e(it)
}
}
fun toggleAutoTunnel() {
applicationScope.launch(ioDispatcher) {
if (_autoTunnelActive.value) stopAutoTunnel() else startAutoTunnel()
}
}
fun updateAutoTunnelTile() {
context.requestAutoTunnelTileServiceUpdate()
}
fun updateTunnelTile() {
context.requestTunnelTileServiceStateUpdate()
}
}
@@ -1,272 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.service
import android.app.Notification
import android.content.Intent
import android.os.IBinder
import androidx.core.app.ServiceCompat
import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope
import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.networkmonitor.NetworkStatus
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.distinctByKeys
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Job
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
@AndroidEntryPoint
class TunnelForegroundService : LifecycleService() {
@Inject
lateinit var notificationManager: NotificationManager
@Inject
lateinit var serviceManager: ServiceManager
@Inject
lateinit var networkMonitor: NetworkMonitor
@Inject
@IoDispatcher
lateinit var ioDispatcher: CoroutineDispatcher
@Inject
lateinit var tunnelRepo: TunnelRepository
@Inject
lateinit var tunnelManager: TunnelManager
private val isNetworkConnected = MutableStateFlow(true)
private val tunnelJobs = ConcurrentHashMap<TunnelConf, Job>()
override fun onCreate() {
super.onCreate()
serviceManager.backgroundService.complete(this)
ServiceCompat.startForeground(
this@TunnelForegroundService,
NotificationManager.VPN_NOTIFICATION_ID,
onCreateNotification(),
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
)
}
override fun onBind(intent: Intent): IBinder? {
super.onBind(intent)
return null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
serviceManager.backgroundService.complete(this)
ServiceCompat.startForeground(
this@TunnelForegroundService,
NotificationManager.VPN_NOTIFICATION_ID,
onCreateNotification(),
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
)
start()
return START_STICKY
}
fun start() = lifecycleScope.launch {
tunnelManager.activeTunnels.distinctByKeys().collect { tuns ->
if (tuns.isEmpty() && tunnelJobs.isEmpty()) return@collect
if (tuns.isEmpty() && tunnelJobs.isNotEmpty()) {
return@collect tunnelJobs.forEach { (key, _) ->
Timber.d("Stopping all tunnel jobs")
tunnelJobs[key]?.cancel()
tunnelJobs.remove(key)
}
}
val (jobsToStop, jobsToStart) = findMissingKeys(tuns, tunnelJobs)
if (jobsToStop.isEmpty() && jobsToStart.isEmpty()) return@collect
jobsToStop.forEach { tun ->
Timber.d("Stopping tunnel jobs for ${tun.tunName}")
tunnelJobs[tun]?.cancel()
tunnelJobs.remove(tun)
}
jobsToStart.forEach { tun ->
Timber.d("Starting tunnel jobs for ${tun.tunName}")
tunnelJobs += (tun to startTunnelJobs(tun))
}
updateServiceNotification()
}
}
// TODO Would be cool to have this include kill switch
// TODO also we need to include errors
private fun updateServiceNotification() {
val notification = when (tunnelJobs.size) {
0 -> onCreateNotification()
1 -> createTunnelNotification(tunnelJobs.keys.first())
else -> createTunnelsNotification()
}
ServiceCompat.startForeground(
this@TunnelForegroundService,
NotificationManager.VPN_NOTIFICATION_ID,
notification,
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
)
}
// use same scope so we can cancel all of these
private fun startTunnelJobs(tunnelConf: TunnelConf) = lifecycleScope.launch {
// monitor if we have internet connectivity
launch { startNetworkMonitorJob() }
// job to trigger stats emit on interval
launch { startTunnelStatsJob(tunnelConf) }
// monitor changes to the tunnel config
launch { startTunnelConfChangesJob(tunnelConf) }
// monitor tunnel ping
launch { startPingJob(tunnelConf) }
}
private fun findMissingKeys(map1: Map<TunnelConf, Any>, map2: Map<TunnelConf, Any>): Pair<Set<TunnelConf>, Set<TunnelConf>> {
val missingMap1 = map2.keys - map1.keys
val missingMap2 = map1.keys - map2.keys
return missingMap1 to missingMap2
}
private suspend fun startTunnelConfChangesJob(tunnelConf: TunnelConf) {
tunnelRepo.flow
.flowOn(ioDispatcher)
.map { storedTunnels ->
storedTunnels.firstOrNull { it.id == tunnelConf.id }
}
.filterNotNull()
// only emit when one of these 3 values change
.distinctUntilChanged { old, new ->
old == new
}
.collect { storedTunnel ->
if (tunnelConf != storedTunnel) {
Timber.d("Config changed for ${storedTunnel.tunName}, bouncing")
// let this complete, even after cancel
withContext(NonCancellable) {
tunnelManager.bounceTunnel(storedTunnel, TunnelStatus.StopReason.CONFIG_CHANGED)
}
}
}
}
private suspend fun startNetworkMonitorJob() {
networkMonitor.networkStatusFlow
.flowOn(ioDispatcher)
.collectLatest { status ->
val isAvailable = status !is NetworkStatus.Disconnected
isNetworkConnected.value = isAvailable
Timber.d("Network available: $status")
}
}
private suspend fun startTunnelStatsJob(tunnel: TunnelConf) = coroutineScope {
while (isActive) {
tunnelManager.updateTunnelStatistics(tunnel)
delay(STATS_DELAY)
}
}
// TODO fix cooldown
private suspend fun startPingJob(tunnel: TunnelConf) = coroutineScope {
delay(PING_START_DELAY)
while (isActive) {
val shouldBounce = shouldBounceTunnel(tunnel)
val delayMs = if (shouldBounce) {
// let this complete, even after cancel
withContext(NonCancellable) {
tunnelManager.bounceTunnel(tunnel, TunnelStatus.StopReason.PING)
}
tunnel.pingCooldown ?: Constants.PING_COOLDOWN
} else {
tunnel.pingInterval ?: Constants.PING_INTERVAL
}
delay(delayMs)
}
}
private suspend fun shouldBounceTunnel(tunnel: TunnelConf): Boolean {
if (!isNetworkConnected.value) {
Timber.d("Network disconnected, skipping ping for ${tunnel.tunName}")
return false
}
return runCatching {
!tunnel.isTunnelPingable(ioDispatcher)
}.onFailure { e ->
Timber.e(e, "Ping check failed for ${tunnel.tunName}")
}.getOrDefault(true)
}
fun stop() {
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
stopSelf()
}
override fun onDestroy() {
serviceManager.backgroundService = CompletableDeferred()
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
super.onDestroy()
}
private fun createTunnelNotification(tunnelConf: TunnelConf): Notification {
return notificationManager.createNotification(
WireGuardNotification.NotificationChannels.VPN,
title = "${getString(R.string.tunnel_running)} - ${tunnelConf.tunName}",
actions = listOf(
notificationManager.createNotificationAction(NotificationAction.TUNNEL_OFF, tunnelConf.id),
),
)
}
private fun createTunnelsNotification(): Notification {
return notificationManager.createNotification(
WireGuardNotification.NotificationChannels.VPN,
title = "${getString(R.string.tunnel_running)} - ${getString(R.string.multiple)}",
actions = listOf(
notificationManager.createNotificationAction(NotificationAction.TUNNEL_OFF, 0),
),
)
}
private fun onCreateNotification(): Notification {
return notificationManager.createNotification(
WireGuardNotification.NotificationChannels.VPN,
title = getString(R.string.tunnel_starting),
)
}
// TODO add notification handling and optional log reading for restart on handshake failures
companion object {
const val STATS_DELAY = 1_000L
const val PING_START_DELAY = 30_000L
// ipv6 disabled or block on network
// const val userspaceStartFailed = "Failed to send handshake initiation: write udp [::]"
// const val ipv6Fails = "Failed to send data packets: write udp [::]"
// const val ipv4Fails = "Failed to send data packets: write udp 0.0.0.0:51820"
}
}
@@ -1,191 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.service.tile
import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.lifecycleScope
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class TunnelControlTile : TileService(), LifecycleOwner {
@Inject
lateinit var appDataRepository: AppDataRepository
@Inject
lateinit var serviceManager: ServiceManager
@Inject
lateinit var tunnelManager: TunnelManager
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
private var isCollecting = false
override fun onCreate() {
super.onCreate()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
}
override fun onDestroy() {
super.onDestroy()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
}
override fun onStartListening() {
super.onStartListening()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
Timber.d("Start listening called for tunnel tile")
if (isCollecting) return
isCollecting = true
lifecycleScope.launch {
tunnelManager.activeTunnels.collect {
updateTileState()
}
}
}
private suspend fun updateTileState() {
try {
val tunnels = appDataRepository.tunnels.getAll()
if (tunnels.isEmpty()) {
setUnavailable()
return
}
val activeTunnels = tunnelManager.activeTunnels.value
.filter { it.value.status.isUpOrStarting() }
when {
activeTunnels.isNotEmpty() -> {
val activeIds = activeTunnels.map { it.key.id }
// TODO improvements would be needed to make this work well with toggling multiple tunnels
// this would be better managed elsewhere
WireGuardAutoTunnel.setLastActiveTunnels(activeIds)
updateTileForActiveTunnels(activeTunnels)
}
else -> updateTileForLastActiveTunnels()
}
} catch (e: Exception) {
setUnavailable()
}
}
private fun updateTileForActiveTunnels(activeTunnels: Map<TunnelConf, TunnelState>) {
val tileName = when (activeTunnels.size) {
1 -> activeTunnels.keys.first().tunName
else -> getString(R.string.multiple)
}
updateTile(tileName, true)
}
private suspend fun updateTileForLastActiveTunnels() {
val lastActiveIds = WireGuardAutoTunnel.getLastActiveTunnels()
when {
lastActiveIds.isEmpty() -> {
appDataRepository.getStartTunnelConfig()?.let { config ->
updateTile(config.tunName, false)
} ?: setUnavailable()
}
lastActiveIds.size > 1 -> updateTile(getString(R.string.multiple), false)
else -> {
val tunnelId = lastActiveIds.first()
appDataRepository.tunnels.getById(tunnelId)?.let { tunnel ->
updateTile(tunnel.tunName, false)
} ?: setUnavailable()
}
}
}
override fun onClick() {
super.onClick()
unlockAndRun {
lifecycleScope.launch {
if (tunnelManager.activeTunnels.value.isNotEmpty()) return@launch tunnelManager.stopTunnel()
val lastActive = WireGuardAutoTunnel.getLastActiveTunnels()
if (lastActive.isEmpty()) {
appDataRepository.getStartTunnelConfig()?.let {
tunnelManager.startTunnel(it)
}
} else {
lastActive.forEach { id ->
appDataRepository.tunnels.getById(id)?.let {
tunnelManager.startTunnel(it)
}
}
}
}
}
}
private fun setActive() {
runCatching {
qsTile.state = Tile.STATE_ACTIVE
qsTile.updateTile()
}
}
private fun setInactive() {
runCatching {
qsTile.state = Tile.STATE_INACTIVE
qsTile.updateTile()
}
}
private fun setUnavailable() {
runCatching {
qsTile.state = Tile.STATE_UNAVAILABLE
setTileDescription("")
qsTile.updateTile()
}
}
private fun setTileDescription(description: String) {
runCatching {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
qsTile.subtitle = description
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
qsTile.stateDescription = description
}
qsTile.updateTile()
}
}
/* This works around an annoying unsolved frameworks bug some people are hitting. */
override fun onBind(intent: Intent): IBinder? {
var ret: IBinder? = null
try {
ret = super.onBind(intent)
} catch (_: Throwable) {
Timber.e("Failed to bind to TunnelControlTile")
}
return ret
}
private fun updateTile(name: String, active: Boolean) {
runCatching {
setTileDescription(name)
if (active) return setActive()
setInactive()
}.onFailure {
Timber.e(it)
}
}
override val lifecycle: Lifecycle
get() = lifecycleRegistry
}
@@ -1,204 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendError
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import timber.log.Timber
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.concurrent.thread
abstract class BaseTunnel(
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
@ApplicationScope private val applicationScope: CoroutineScope,
private val appDataRepository: AppDataRepository,
private val serviceManager: ServiceManager,
) : TunnelProvider {
private val activeTuns = MutableStateFlow<Map<TunnelConf, TunnelState>>(emptyMap())
private val tunThreads = ConcurrentHashMap<Int, Thread>()
override val activeTunnels = activeTuns.asStateFlow()
private val tunMutex = Mutex()
private val tunStatusMutex = Mutex()
private val isBouncing = AtomicBoolean(false)
abstract suspend fun startBackend(tunnel: TunnelConf)
abstract fun stopBackend(tunnel: TunnelConf)
override suspend fun clearError(tunnelConf: TunnelConf) = updateTunnelStatus(tunnelConf, TunnelStatus.Down)
override fun hasVpnPermission(): Boolean {
return serviceManager.hasVpnPermission()
}
protected suspend fun updateTunnelStatus(tunnelConf: TunnelConf, state: TunnelStatus? = null, stats: TunnelStatistics? = null) {
tunStatusMutex.withLock {
activeTuns.update { current ->
val originalConf = current.getKeyById(tunnelConf.id) ?: tunnelConf
val existingState = current.getValueById(tunnelConf.id) ?: TunnelState()
val newState = state ?: existingState.status
if (newState == TunnelStatus.Down) {
Timber.d("Removing tunnel ${tunnelConf.id} from activeTunnels as state is DOWN")
current - originalConf
} else if (existingState.status == newState && stats == null) {
Timber.d("Skipping redundant state update for ${tunnelConf.id}: $newState")
current
} else {
val updated = existingState.copy(
status = newState,
statistics = stats ?: existingState.statistics,
)
current + (originalConf to updated)
}
}
}
}
private suspend fun stopActiveTunnels() {
activeTunnels.value.forEach { (config, state) ->
if (state.status.isUpOrStarting()) {
stopTunnel(config)
delay(300)
}
}
}
private fun configureTunnelCallbacks(tunnelConf: TunnelConf) {
Timber.d("Configuring TunnelConf instance: ${tunnelConf.hashCode()}")
tunnelConf.setStateChangeCallback { state ->
Timber.d("State change callback triggered for tunnel ${tunnelConf.id}: ${tunnelConf.tunName} with state $state at ${System.currentTimeMillis()}")
when (state) {
is Tunnel.State -> applicationScope.launch { updateTunnelStatus(tunnelConf, state.asTunnelState()) }
is org.amnezia.awg.backend.Tunnel.State -> applicationScope.launch { updateTunnelStatus(tunnelConf, state.asTunnelState()) }
}
serviceManager.updateTunnelTile()
}
}
override suspend fun updateTunnelStatistics(tunnel: TunnelConf) {
val stats = getStatistics(tunnel)
updateTunnelStatus(tunnel, null, stats)
}
override suspend fun startTunnel(tunnelConf: TunnelConf) {
if (activeTuns.exists(tunnelConf.id) || tunThreads.containsKey(tunnelConf.id)) return
// stop active tunnels if we are userspace
if (this@BaseTunnel is UserspaceTunnel) stopActiveTunnels()
tunMutex.withLock {
// use thread to interrupt java backend if stuck (like in dns resolution)
tunThreads += tunnelConf.id to thread {
runBlocking {
try {
Timber.d("Starting tunnel ${tunnelConf.id}...")
startTunnelInner(tunnelConf)
Timber.d("Started complete for tunnel ${tunnelConf.name}...")
} catch (e: BackendError) {
Timber.e(e, "Failed to start tunnel ${tunnelConf.name} userspace")
updateTunnelStatus(tunnelConf, TunnelStatus.Error(e))
} catch (e: InterruptedException) {
Timber.i("Tunnel start has been interrupted as ${tunnelConf.name} failed to start")
}
}
}
}
}
private suspend fun startTunnelInner(tunnelConf: TunnelConf) {
configureTunnelCallbacks(tunnelConf)
Timber.d("Started backend for tunnel ${tunnelConf.id}...")
startBackend(tunnelConf)
updateTunnelStatus(tunnelConf, TunnelStatus.Up)
Timber.d("DONE for tun ${tunnelConf.id}...")
saveTunnelActiveState(tunnelConf, true)
serviceManager.startTunnelForegroundService()
}
private suspend fun saveTunnelActiveState(tunnelConf: TunnelConf, active: Boolean) {
val tunnelCopy = tunnelConf.copyWithCallback(isActive = active)
appDataRepository.tunnels.save(tunnelCopy)
}
override suspend fun stopTunnel(tunnelConf: TunnelConf?, reason: TunnelStatus.StopReason) {
if (tunnelConf == null) return stopActiveTunnels()
tunMutex.withLock {
try {
val stuckStarting = activeTuns.isStarting(tunnelConf.id)
handleTunnelThreadCleanup(tunnelConf)
if (stuckStarting) return Timber.d("Stuck in starting, so just shutting down tunnel thread")
updateTunnelStatus(tunnelConf, TunnelStatus.Stopping(reason))
stopTunnelInner(tunnelConf)
} catch (e: BackendError) {
Timber.e(e, "Failed to stop tunnel ${tunnelConf.id}")
updateTunnelStatus(tunnelConf, TunnelStatus.Error(e))
}
}
}
private suspend fun stopTunnelInner(tunnelConf: TunnelConf) {
val tunnel = activeTuns.findTunnel(tunnelConf.id) ?: return
stopBackend(tunnel)
saveTunnelActiveState(tunnelConf, false)
removeActiveTunnel(tunnel)
handleServiceChangesOnStop()
}
private suspend fun handleServiceChangesOnStop() {
if (activeTuns.value.isEmpty() && !isBouncing.get()) return serviceManager.stopTunnelForegroundService()
}
private suspend fun handleTunnelThreadCleanup(tunnel: TunnelConf) {
Timber.d("Cleaning up thread for ${tunnel.name}")
try {
tunThreads[tunnel.id]?.let {
if (it.state != Thread.State.TERMINATED) {
it.interrupt()
updateTunnelStatus(tunnel, TunnelStatus.Down)
} else {
Timber.d("Thread already terminated")
}
}
} catch (e: Exception) {
Timber.e(e, "Failed to stop tunnel thread for ${tunnel.name}")
}
Timber.d("Removing thread for ${tunnel.name}")
tunThreads -= tunnel.id
}
private fun removeActiveTunnel(tunnelConf: TunnelConf) {
activeTuns.update { current ->
current.toMutableMap().apply { remove(tunnelConf) }
}
}
override suspend fun bounceTunnel(tunnelConf: TunnelConf, reason: TunnelStatus.StopReason) {
Timber.i("Bounce tunnel ${tunnelConf.name}")
isBouncing.set(true)
stopTunnel(tunnelConf, reason)
startTunnel(tunnelConf)
isBouncing.set(false)
}
override suspend fun runningTunnelNames(): Set<String> = activeTuns.value.keys.map { it.tunName }.toSet()
}
@@ -1,51 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import kotlinx.coroutines.flow.MutableStateFlow
fun Map<TunnelConf, TunnelState>.allDown(): Boolean {
return this.all { it.value.status.isDown() }
}
fun Map<TunnelConf, TunnelState>.hasActive(): Boolean {
return this.any { it.value.status.isUp() }
}
fun Map<TunnelConf, TunnelState>.getValueById(id: Int): TunnelState? {
val key = this.keys.find { it.id == id }
return key?.let { this@getValueById[it] }
}
fun Map<TunnelConf, TunnelState>.getKeyById(id: Int): TunnelConf? {
return this.keys.find { it.id == id }
}
fun Map<TunnelConf, TunnelState>.isUp(tunnelConf: TunnelConf): Boolean {
return this.getValueById(tunnelConf.id)?.status?.isUp() ?: false
}
fun MutableStateFlow<Map<TunnelConf, TunnelState>>.exists(id: Int): Boolean {
return this.value.any { it.key.id == id }
}
fun MutableStateFlow<Map<TunnelConf, TunnelState>>.isUp(id: Int): Boolean {
return this.value.any { it.key.id == id && it.value.status == TunnelStatus.Up }
}
fun MutableStateFlow<Map<TunnelConf, TunnelState>>.isStarting(id: Int): Boolean {
return this.value.any { it.key.id == id && it.value.status == TunnelStatus.Starting }
}
fun MutableStateFlow<Map<TunnelConf, TunnelState>>.findTunnel(id: Int): TunnelConf? {
return this.value.keys.find { it.id == id }
}
private val URL_PATTERN = Regex(
"""^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}:[0-9]{1,5}$""",
)
fun String.isUrl(): Boolean {
return URL_PATTERN.matches(this)
}
@@ -1,67 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.wireguard.android.backend.Backend
import com.wireguard.android.backend.BackendException
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.domain.state.WireGuardStatistics
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendError
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import timber.log.Timber
import javax.inject.Inject
class KernelTunnel @Inject constructor(
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
@ApplicationScope private val applicationScope: CoroutineScope,
serviceManager: ServiceManager,
appDataRepository: AppDataRepository,
private val backend: Backend,
) : BaseTunnel(ioDispatcher, applicationScope, appDataRepository, serviceManager) {
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics? {
return try {
WireGuardStatistics(backend.getStatistics(tunnelConf))
} catch (e: Exception) {
Timber.e(e)
null
}
}
override suspend fun startBackend(tunnel: TunnelConf) {
try {
updateTunnelStatus(tunnel, TunnelStatus.Starting)
backend.setState(tunnel, Tunnel.State.UP, tunnel.toWgConfig())
} catch (e: BackendException) {
throw e.toBackendError()
}
}
override fun stopBackend(tunnel: TunnelConf) {
Timber.i("Stopping tunnel ${tunnel.id} kernel")
try {
backend.setState(tunnel, Tunnel.State.DOWN, tunnel.toWgConfig())
} catch (e: BackendException) {
throw e.toBackendError()
}
}
override fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
Timber.w("Not yet implemented for kernel")
}
override fun getBackendState(): BackendState {
return BackendState.INACTIVE
}
override suspend fun runningTunnelNames(): Set<String> {
return backend.runningTunnelNames
}
}
@@ -1,114 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.di.Kernel
import com.zaneschepke.wireguardautotunnel.di.Userspace
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import javax.inject.Inject
class TunnelManager @Inject constructor(
@Kernel private val kernelTunnel: TunnelProvider,
@Userspace private val userspaceTunnel: TunnelProvider,
private val appDataRepository: AppDataRepository,
@ApplicationScope private val applicationScope: CoroutineScope,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : TunnelProvider {
@OptIn(ExperimentalCoroutinesApi::class)
private val tunnelProviderFlow = appDataRepository.settings.flow
.filterNotNull()
.flatMapLatest { settings ->
MutableStateFlow(if (settings.isKernelEnabled) kernelTunnel else userspaceTunnel)
}
.stateIn(
scope = applicationScope.plus(ioDispatcher),
started = SharingStarted.Eagerly,
initialValue = userspaceTunnel,
)
@OptIn(ExperimentalCoroutinesApi::class)
override val activeTunnels = appDataRepository.settings.flow
.filterNotNull()
.flatMapLatest { settings ->
if (settings.isKernelEnabled) {
kernelTunnel.activeTunnels
} else {
userspaceTunnel.activeTunnels
}
}
.stateIn(
scope = applicationScope.plus(ioDispatcher),
started = SharingStarted.Eagerly,
initialValue = emptyMap(),
)
override fun hasVpnPermission(): Boolean {
return userspaceTunnel.hasVpnPermission()
}
override suspend fun clearError(tunnelConf: TunnelConf) {
tunnelProviderFlow.value.clearError(tunnelConf)
}
override suspend fun updateTunnelStatistics(tunnel: TunnelConf) {
tunnelProviderFlow.value.updateTunnelStatistics(tunnel)
}
override suspend fun startTunnel(tunnelConf: TunnelConf) {
tunnelProviderFlow.value.startTunnel(tunnelConf)
}
override suspend fun stopTunnel(tunnelConf: TunnelConf?, reason: TunnelStatus.StopReason) {
tunnelProviderFlow.value.stopTunnel(tunnelConf)
}
override suspend fun bounceTunnel(tunnelConf: TunnelConf, reason: TunnelStatus.StopReason) {
tunnelProviderFlow.value.bounceTunnel(tunnelConf)
}
override fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
tunnelProviderFlow.value.setBackendState(backendState, allowedIps)
}
override fun getBackendState(): BackendState {
return tunnelProviderFlow.value.getBackendState()
}
override suspend fun runningTunnelNames(): Set<String> {
return tunnelProviderFlow.value.runningTunnelNames()
}
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics? {
return tunnelProviderFlow.value.getStatistics(tunnelConf)
}
fun restorePreviousState() = applicationScope.launch(ioDispatcher) {
val settings = appDataRepository.settings.get()
if (settings.isRestoreOnBootEnabled) {
val previouslyActiveTuns = appDataRepository.tunnels.getActive()
val tunsToStart = previouslyActiveTuns.filterNot { tun -> activeTunnels.value.any { tun.id == it.key.id } }
if (settings.isKernelEnabled) {
return@launch tunsToStart.forEach {
startTunnel(it)
}
} else {
tunsToStart.firstOrNull()?.let { startTunnel(it) }
}
}
}
}
@@ -1,22 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import kotlinx.coroutines.flow.StateFlow
interface TunnelProvider {
suspend fun startTunnel(tunnelConf: TunnelConf)
suspend fun stopTunnel(tunnelConf: TunnelConf? = null, reason: TunnelStatus.StopReason = TunnelStatus.StopReason.USER)
suspend fun bounceTunnel(tunnelConf: TunnelConf, reason: TunnelStatus.StopReason = TunnelStatus.StopReason.USER)
fun setBackendState(backendState: BackendState, allowedIps: Collection<String>)
fun getBackendState(): BackendState
suspend fun runningTunnelNames(): Set<String>
fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics?
val activeTunnels: StateFlow<Map<TunnelConf, TunnelState>>
fun hasVpnPermission(): Boolean
suspend fun clearError(tunnelConf: TunnelConf)
suspend fun updateTunnelStatistics(tunnel: TunnelConf)
}
@@ -1,107 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.state.AmneziaStatistics
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.util.extensions.asAmBackendState
import com.zaneschepke.wireguardautotunnel.util.extensions.asBackendState
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendError
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import org.amnezia.awg.backend.Backend
import org.amnezia.awg.backend.BackendException
import org.amnezia.awg.backend.Tunnel
import org.amnezia.awg.config.Config
import timber.log.Timber
import javax.inject.Inject
import kotlin.jvm.optionals.getOrNull
class UserspaceTunnel @Inject constructor(
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
@ApplicationScope private val applicationScope: CoroutineScope,
val serviceManager: ServiceManager,
val appDataRepository: AppDataRepository,
private val backend: Backend,
) : BaseTunnel(ioDispatcher, applicationScope, appDataRepository, serviceManager) {
private var previousBackendState: Pair<BackendState, Boolean>? = null
override suspend fun startBackend(tunnel: TunnelConf) {
try {
updateTunnelStatus(tunnel, TunnelStatus.Starting)
val amConfig = tunnel.toAmConfig()
handleVpnKillSwitchWithDomainEndpoints(amConfig)
backend.setState(tunnel, Tunnel.State.UP, amConfig)
} catch (e: BackendException) {
Timber.e(e, "Failed to start up backend for tunnel ${tunnel.name}")
throw e.toBackendError()
}
}
override fun stopBackend(tunnel: TunnelConf) {
Timber.i("Stopping tunnel ${tunnel.name} userspace")
try {
backend.setState(tunnel, Tunnel.State.DOWN, tunnel.toAmConfig())
} catch (e: BackendException) {
Timber.e(e, "Failed to stop tunnel ${tunnel.id}")
throw e.toBackendError()
}
handlePreviouslyEnabledVpnKillSwitch()
}
// stop vpn kill switch if we need to resolve DNS for peer endpoints
private suspend fun handleVpnKillSwitchWithDomainEndpoints(config: Config) {
if (config.peers.any { it.endpoint.getOrNull()?.toString()?.isUrl() == true } &&
backend.backendState.asBackendState() == BackendState.KILL_SWITCH_ACTIVE
) {
val bypassLan = appDataRepository.settings.get().isLanOnKillSwitchEnabled
previousBackendState = Pair(BackendState.KILL_SWITCH_ACTIVE, bypassLan)
setBackendState(BackendState.SERVICE_ACTIVE, emptyList())
}
}
// restore vpn kill switch if needed
private fun handlePreviouslyEnabledVpnKillSwitch() {
// let auto tunnel handle this if it is active
if (!serviceManager.autoTunnelActive.value) {
previousBackendState?.let { (state, lanEnabled) ->
Timber.d("Restoring kill switch configuration")
val lan = if (lanEnabled) TunnelConf.LAN_BYPASS_ALLOWED_IPS else emptyList()
backend.setBackendState(state.asAmBackendState(), lan)
}
}
previousBackendState = null
}
override fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
Timber.d("Setting backend state: $backendState with allowedIps: $allowedIps")
try {
backend.setBackendState(backendState.asAmBackendState(), allowedIps)
} catch (e: BackendException) {
throw e.toBackendError()
}
}
override fun getBackendState(): BackendState {
return backend.backendState.asBackendState()
}
override suspend fun runningTunnelNames(): Set<String> {
return backend.runningTunnelNames
}
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics? {
return try {
AmneziaStatistics(backend.getStatistics(tunnelConf))
} catch (e: Exception) {
Timber.e(e, "Failed to get stats for ${tunnelConf.tunName}")
null
}
}
}
@@ -1,60 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.worker
import android.content.Context
import androidx.hilt.work.HiltWorker
import androidx.work.CoroutineWorker
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.util.concurrent.TimeUnit
@HiltWorker
class ServiceWorker @AssistedInject constructor(
@Assisted private val context: Context,
@Assisted private val params: WorkerParameters,
private val serviceManager: ServiceManager,
private val appDataRepository: AppDataRepository,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
private val tunnelManager: TunnelManager,
) : CoroutineWorker(context, params) {
companion object {
private const val TAG = "service_worker"
fun stop(context: Context) {
WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
}
fun start(context: Context) {
val periodicWorkRequest = PeriodicWorkRequestBuilder<ServiceWorker>(
repeatInterval = 15,
repeatIntervalTimeUnit = TimeUnit.MINUTES,
).build()
WorkManager.getInstance(context)
.enqueueUniquePeriodicWork(
TAG,
ExistingPeriodicWorkPolicy.KEEP,
periodicWorkRequest,
)
}
}
override suspend fun doWork(): Result = withContext(ioDispatcher) {
Timber.i("Service worker started")
with(appDataRepository.settings.get()) {
if (isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) return@with serviceManager.startAutoTunnel()
if (tunnelManager.activeTunnels.value.isEmpty()) tunnelManager.restorePreviousState()
}
Result.success()
}
}
@@ -6,14 +6,12 @@ import androidx.room.DeleteColumn
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import androidx.room.migration.AutoMigrationSpec
import com.zaneschepke.wireguardautotunnel.data.dao.SettingsDao
import com.zaneschepke.wireguardautotunnel.data.dao.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.data.model.Settings
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
@Database(
entities = [Settings::class, TunnelConfig::class],
version = 16,
version = 15,
autoMigrations =
[
AutoMigration(from = 1, to = 2),
@@ -59,10 +57,6 @@ import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
from = 14,
to = 15,
),
AutoMigration(
from = 15,
to = 16,
),
],
exportSchema = true,
)
@@ -1,6 +1,7 @@
package com.zaneschepke.wireguardautotunnel.data
import androidx.room.TypeConverter
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
class DatabaseListConverters {
@@ -1,11 +1,11 @@
package com.zaneschepke.wireguardautotunnel.data.dao
package com.zaneschepke.wireguardautotunnel.data
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.zaneschepke.wireguardautotunnel.data.model.Settings
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import kotlinx.coroutines.flow.Flow
@Dao
@@ -1,11 +1,11 @@
package com.zaneschepke.wireguardautotunnel.data.dao
package com.zaneschepke.wireguardautotunnel.data
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
import kotlinx.coroutines.flow.Flow
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.data
package com.zaneschepke.wireguardautotunnel.data.datastore
import android.content.Context
import androidx.datastore.preferences.core.Preferences
@@ -6,11 +6,10 @@ import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
@@ -44,7 +43,7 @@ class DataStoreManager(
try {
context.dataStore.data.first()
} catch (e: IOException) {
Timber.Forest.e(e)
Timber.e(e)
}
}
}
@@ -54,9 +53,9 @@ class DataStoreManager(
try {
context.dataStore.edit { it[key] = value }
} catch (e: IOException) {
Timber.Forest.e(e)
Timber.e(e)
} catch (e: Exception) {
Timber.Forest.e(e)
Timber.e(e)
}
}
}
@@ -66,9 +65,9 @@ class DataStoreManager(
try {
context.dataStore.edit { it.remove(key) }
} catch (e: IOException) {
Timber.Forest.e(e)
Timber.e(e)
} catch (e: Exception) {
Timber.Forest.e(e)
Timber.e(e)
}
}
}
@@ -80,7 +79,7 @@ class DataStoreManager(
try {
context.dataStore.data.map { it[key] }.first()
} catch (e: IOException) {
Timber.Forest.e(e)
Timber.e(e)
null
}
}
@@ -90,5 +89,5 @@ class DataStoreManager(
context.dataStore.data.map { it[key] }.first()
}
val preferencesFlow: Flow<Preferences?> = context.dataStore.data.flowOn(ioDispatcher)
val preferencesFlow: Flow<Preferences?> = context.dataStore.data
}
@@ -1,6 +1,5 @@
package com.zaneschepke.wireguardautotunnel.data.model
package com.zaneschepke.wireguardautotunnel.data.domain
import com.zaneschepke.wireguardautotunnel.domain.entity.AppState
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
data class GeneralState(
@@ -12,31 +11,7 @@ data class GeneralState(
val locale: String? = null,
val theme: Theme = Theme.AUTOMATIC,
) {
fun toAppState(): AppState = AppState(
isLocationDisclosureShown,
isBatteryOptimizationDisableShown,
isPinLockEnabled,
isTunnelStatsExpanded,
isLocationDisclosureShown,
locale,
theme,
)
companion object {
fun from(appState: AppState): GeneralState {
return with(appState) {
GeneralState(
isLocationDisclosureShown,
isBatteryOptimizationDisableShown,
isPinLockEnabled,
isTunnelStatsExpanded,
isLocationDisclosureShown,
locale,
theme,
)
}
}
const val LOCATION_DISCLOSURE_SHOWN_DEFAULT = false
const val BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT = false
const val PIN_LOCK_ENABLED_DEFAULT = false
@@ -1,9 +1,8 @@
package com.zaneschepke.wireguardautotunnel.data.model
package com.zaneschepke.wireguardautotunnel.data.domain
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
@Entity
data class Settings(
@@ -92,27 +91,7 @@ data class Settings(
)
val isDisableKillSwitchOnTrustedEnabled: Boolean = false,
) {
fun toAppSettings(): AppSettings {
return AppSettings(
id, isAutoTunnelEnabled, isTunnelOnMobileDataEnabled, trustedNetworkSSIDs, isAlwaysOnVpnEnabled, isTunnelOnEthernetEnabled,
isShortcutsEnabled, isTunnelOnWifiEnabled, isKernelEnabled, isRestoreOnBootEnabled, isMultiTunnelEnabled, isPingEnabled,
isAmneziaEnabled, isWildcardsEnabled, isWifiNameByShellEnabled, isStopOnNoInternetEnabled, isVpnKillSwitchEnabled,
isKernelKillSwitchEnabled, isLanOnKillSwitchEnabled, debounceDelaySeconds, isDisableKillSwitchOnTrustedEnabled,
)
}
companion object {
fun from(appSettings: AppSettings): Settings {
return with(appSettings) {
Settings(
id, isAutoTunnelEnabled, isTunnelOnMobileDataEnabled, trustedNetworkSSIDs.toMutableList(), isAlwaysOnVpnEnabled,
isTunnelOnEthernetEnabled, isShortcutsEnabled, isTunnelOnWifiEnabled, isKernelEnabled, isRestoreOnBootEnabled,
isMultiTunnelEnabled, isPingEnabled, isAmneziaEnabled, isWildcardsEnabled, isWifiNameByShellEnabled,
isStopOnNoInternetEnabled, isVpnKillSwitchEnabled, isKernelKillSwitchEnabled, isLanOnKillSwitchEnabled,
debounceDelaySeconds, isDisableKillSwitchOnTrustedEnabled,
)
}
}
fun debounceDelayMillis(): Long {
return debounceDelaySeconds * 1000L
}
}
@@ -0,0 +1,113 @@
package com.zaneschepke.wireguardautotunnel.data.domain
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import com.wireguard.config.Config
import com.zaneschepke.wireguardautotunnel.util.extensions.toWgQuickString
import java.io.InputStream
@Entity(indices = [Index(value = ["name"], unique = true)])
data class TunnelConfig(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
@ColumnInfo(name = "name") val name: String,
@ColumnInfo(name = "wg_quick") val wgQuick: String,
@ColumnInfo(
name = "tunnel_networks",
defaultValue = "",
)
val tunnelNetworks: MutableList<String> = mutableListOf(),
@ColumnInfo(
name = "is_mobile_data_tunnel",
defaultValue = "false",
)
val isMobileDataTunnel: Boolean = false,
@ColumnInfo(
name = "is_primary_tunnel",
defaultValue = "false",
)
val isPrimaryTunnel: Boolean = false,
@ColumnInfo(
name = "am_quick",
defaultValue = "",
)
val amQuick: String = AM_QUICK_DEFAULT,
@ColumnInfo(
name = "is_Active",
defaultValue = "false",
)
val isActive: Boolean = false,
@ColumnInfo(
name = "is_ping_enabled",
defaultValue = "false",
)
val isPingEnabled: Boolean = false,
@ColumnInfo(
name = "ping_interval",
defaultValue = "null",
)
val pingInterval: Long? = null,
@ColumnInfo(
name = "ping_cooldown",
defaultValue = "null",
)
val pingCooldown: Long? = null,
@ColumnInfo(
name = "ping_ip",
defaultValue = "null",
)
var pingIp: String? = null,
@ColumnInfo(
name = "is_ethernet_tunnel",
defaultValue = "false",
)
var isEthernetTunnel: Boolean = false,
) {
fun toAmConfig(): org.amnezia.awg.config.Config {
return configFromAmQuick(if (amQuick.isNotBlank()) amQuick else wgQuick)
}
fun toWgConfig(): Config {
return configFromWgQuick(wgQuick)
}
companion object {
fun configFromWgQuick(wgQuick: String): Config {
val inputStream: InputStream = wgQuick.byteInputStream()
return inputStream.bufferedReader(Charsets.UTF_8).use {
Config.parse(it)
}
}
fun configFromAmQuick(amQuick: String): org.amnezia.awg.config.Config {
val inputStream: InputStream = amQuick.byteInputStream()
return inputStream.bufferedReader(Charsets.UTF_8).use {
org.amnezia.awg.config.Config.parse(it)
}
}
fun tunnelConfigFromAmConfig(config: org.amnezia.awg.config.Config, name: String): TunnelConfig {
val amQuick = config.toAwgQuickString(true)
val wgQuick = config.toWgQuickString()
return TunnelConfig(name = name, wgQuick = wgQuick, amQuick = amQuick)
}
const val AM_QUICK_DEFAULT = ""
const val IPV6_ALL_NETWORKS = "::/0"
const val IPV4_ALL_NETWORKS = "0.0.0.0/0"
val ALL_IPS = setOf(IPV4_ALL_NETWORKS, IPV6_ALL_NETWORKS)
val IPV4_PUBLIC_NETWORKS = setOf(
"0.0.0.0/5", "8.0.0.0/7", "11.0.0.0/8", "12.0.0.0/6", "16.0.0.0/4", "32.0.0.0/3",
"64.0.0.0/2", "128.0.0.0/3", "160.0.0.0/5", "168.0.0.0/6", "172.0.0.0/12",
"172.32.0.0/11", "172.64.0.0/10", "172.128.0.0/9", "173.0.0.0/8", "174.0.0.0/7",
"176.0.0.0/4", "192.0.0.0/9", "192.128.0.0/11", "192.160.0.0/13", "192.169.0.0/16",
"192.170.0.0/15", "192.172.0.0/14", "192.176.0.0/12", "192.192.0.0/10",
"193.0.0.0/8", "194.0.0.0/7", "196.0.0.0/6", "200.0.0.0/5", "208.0.0.0/4",
)
val LAN_BYPASS_ALLOWED_IPS = setOf(IPV6_ALL_NETWORKS) + IPV4_PUBLIC_NETWORKS
}
}
@@ -1,93 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.model
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
@Entity(indices = [Index(value = ["name"], unique = true)])
data class TunnelConfig(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
@ColumnInfo(name = "name") val name: String,
@ColumnInfo(name = "wg_quick") val wgQuick: String,
@ColumnInfo(
name = "tunnel_networks",
defaultValue = "",
)
val tunnelNetworks: MutableList<String> = mutableListOf(),
@ColumnInfo(
name = "is_mobile_data_tunnel",
defaultValue = "false",
)
val isMobileDataTunnel: Boolean = false,
@ColumnInfo(
name = "is_primary_tunnel",
defaultValue = "false",
)
val isPrimaryTunnel: Boolean = false,
@ColumnInfo(
name = "am_quick",
defaultValue = "",
)
val amQuick: String = AM_QUICK_DEFAULT,
@ColumnInfo(
name = "is_Active",
defaultValue = "false",
)
val isActive: Boolean = false,
@ColumnInfo(
name = "is_ping_enabled",
defaultValue = "false",
)
val isPingEnabled: Boolean = false,
@ColumnInfo(
name = "ping_interval",
defaultValue = "null",
)
val pingInterval: Long? = null,
@ColumnInfo(
name = "ping_cooldown",
defaultValue = "null",
)
val pingCooldown: Long? = null,
@ColumnInfo(
name = "ping_ip",
defaultValue = "null",
)
var pingIp: String? = null,
@ColumnInfo(
name = "is_ethernet_tunnel",
defaultValue = "false",
)
var isEthernetTunnel: Boolean = false,
@ColumnInfo(
name = "is_ipv4_preferred",
defaultValue = "true",
)
var isIpv4Preferred: Boolean = true,
) {
fun toTunnel(): TunnelConf {
return TunnelConf(
id, name, wgQuick, tunnelNetworks, isMobileDataTunnel,
isPrimaryTunnel, amQuick, isActive, isPingEnabled, pingInterval,
pingCooldown, pingIp, isEthernetTunnel, isIpv4Preferred,
)
}
companion object {
const val AM_QUICK_DEFAULT = ""
fun from(tunnelConf: TunnelConf): TunnelConfig {
return with(tunnelConf) {
return TunnelConfig(
id, tunName, wgQuick, tunnelNetworks.toMutableList(), isMobileDataTunnel,
isPrimaryTunnel, amQuick, isActive, isPingEnabled, pingInterval,
pingCooldown, pingIp, isEthernetTunnel, isIpv4Preferred,
)
}
}
}
}
@@ -0,0 +1,13 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
interface AppDataRepository {
suspend fun getPrimaryOrFirstTunnel(): TunnelConfig?
suspend fun getStartTunnelConfig(): TunnelConfig?
val settings: SettingsRepository
val tunnels: TunnelConfigRepository
val appState: AppStateRepository
}
@@ -1,25 +1,20 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.AppSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import javax.inject.Inject
class AppDataRoomRepository
@Inject
constructor(
override val settings: AppSettingRepository,
override val tunnels: TunnelRepository,
override val settings: SettingsRepository,
override val tunnels: TunnelConfigRepository,
override val appState: AppStateRepository,
) : AppDataRepository {
override suspend fun getPrimaryOrFirstTunnel(): TunnelConf? {
override suspend fun getPrimaryOrFirstTunnel(): TunnelConfig? {
return tunnels.findPrimary().firstOrNull() ?: tunnels.getAll().firstOrNull()
}
override suspend fun getStartTunnelConfig(): TunnelConf? {
override suspend fun getStartTunnelConfig(): TunnelConfig? {
tunnels.getActive().let {
if (it.isNotEmpty()) return it.first()
return getPrimaryOrFirstTunnel()
@@ -1,6 +1,6 @@
package com.zaneschepke.wireguardautotunnel.domain.repository
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.model.GeneralState
import com.zaneschepke.wireguardautotunnel.data.domain.GeneralState
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
import kotlinx.coroutines.flow.Flow
@@ -33,5 +33,5 @@ interface AppStateRepository {
suspend fun getLocale(): String?
val flow: Flow<GeneralState>
val generalStateFlow: Flow<GeneralState>
}
@@ -1,8 +1,7 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.DataStoreManager
import com.zaneschepke.wireguardautotunnel.data.model.GeneralState
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
import com.zaneschepke.wireguardautotunnel.data.domain.GeneralState
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
@@ -78,7 +77,7 @@ class DataStoreAppStateRepository(
return dataStoreManager.getFromStore(DataStoreManager.locale)
}
override val flow: Flow<GeneralState> =
override val generalStateFlow: Flow<GeneralState> =
dataStoreManager.preferencesFlow.map { prefs ->
prefs?.let { pref ->
try {
@@ -1,31 +1,30 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.dao.SettingsDao
import com.zaneschepke.wireguardautotunnel.data.model.Settings
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
import com.zaneschepke.wireguardautotunnel.domain.repository.AppSettingRepository
import com.zaneschepke.wireguardautotunnel.data.SettingsDao
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext
class RoomSettingsRepository(
private val settingsDoa: SettingsDao,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : AppSettingRepository {
override suspend fun save(appSettings: AppSettings) {
class RoomSettingsRepository(private val settingsDoa: SettingsDao, @IoDispatcher private val ioDispatcher: CoroutineDispatcher) : SettingsRepository {
override suspend fun save(settings: Settings) {
withContext(ioDispatcher) {
settingsDoa.save(Settings.from(appSettings))
settingsDoa.save(settings)
}
}
override val flow = settingsDoa.getSettingsFlow().flowOn(ioDispatcher).map { it.toAppSettings() }
override fun getSettingsFlow(): Flow<Settings> {
return settingsDoa.getSettingsFlow()
}
override suspend fun get(): AppSettings {
override suspend fun getSettings(): Settings {
return withContext(ioDispatcher) {
(settingsDoa.getAll().firstOrNull() ?: Settings()).toAppSettings()
settingsDoa.getAll().firstOrNull() ?: Settings()
}
}
override suspend fun getAll(): List<Settings> {
return withContext(ioDispatcher) { settingsDoa.getAll() }
}
}
@@ -0,0 +1,104 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext
class RoomTunnelConfigRepository(
private val tunnelConfigDao: TunnelConfigDao,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) :
TunnelConfigRepository {
override fun getTunnelConfigsFlow(): Flow<TunnelConfigs> {
return tunnelConfigDao.getAllFlow()
}
override suspend fun getAll(): TunnelConfigs {
return withContext(ioDispatcher) { tunnelConfigDao.getAll() }
}
override suspend fun save(tunnelConfig: TunnelConfig) {
withContext(ioDispatcher) {
tunnelConfigDao.save(tunnelConfig)
}
}
override suspend fun updatePrimaryTunnel(tunnelConfig: TunnelConfig?) {
withContext(ioDispatcher) {
tunnelConfigDao.resetPrimaryTunnel()
tunnelConfig?.let {
save(
it.copy(
isPrimaryTunnel = true,
),
)
}
}
}
override suspend fun updateMobileDataTunnel(tunnelConfig: TunnelConfig?) {
withContext(ioDispatcher) {
tunnelConfigDao.resetMobileDataTunnel()
tunnelConfig?.let {
save(
it.copy(
isMobileDataTunnel = true,
),
)
}
}
}
override suspend fun updateEthernetTunnel(tunnelConfig: TunnelConfig?) {
withContext(ioDispatcher) {
tunnelConfigDao.resetEthernetTunnel()
tunnelConfig?.let {
save(
it.copy(
isEthernetTunnel = true,
),
)
}
}
}
override suspend fun delete(tunnelConfig: TunnelConfig) {
withContext(ioDispatcher) {
tunnelConfigDao.delete(tunnelConfig)
}
}
override suspend fun getById(id: Int): TunnelConfig? {
return withContext(ioDispatcher) { tunnelConfigDao.getById(id.toLong()) }
}
override suspend fun getActive(): TunnelConfigs {
return withContext(ioDispatcher) {
tunnelConfigDao.getActive()
}
}
override suspend fun count(): Int {
return withContext(ioDispatcher) { tunnelConfigDao.count().toInt() }
}
override suspend fun findByTunnelName(name: String): TunnelConfig? {
return withContext(ioDispatcher) { tunnelConfigDao.getByName(name) }
}
override suspend fun findByTunnelNetworksName(name: String): TunnelConfigs {
return withContext(ioDispatcher) { tunnelConfigDao.findByTunnelNetworkName(name) }
}
override suspend fun findByMobileDataTunnel(): TunnelConfigs {
return withContext(ioDispatcher) { tunnelConfigDao.findByMobileDataTunnel() }
}
override suspend fun findPrimary(): TunnelConfigs {
return withContext(ioDispatcher) { tunnelConfigDao.findByPrimary() }
}
}
@@ -1,113 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.dao.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.util.extensions.Tunnels
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
class RoomTunnelRepository(
private val tunnelConfigDao: TunnelConfigDao,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : TunnelRepository {
override val flow = tunnelConfigDao.getAllFlow().flowOn(ioDispatcher).map { it.map { it.toTunnel() } }
override suspend fun getAll(): Tunnels {
return withContext(ioDispatcher) {
tunnelConfigDao.getAll().map { it.toTunnel() }
}
}
override suspend fun save(tunnelConf: TunnelConf) {
withContext(ioDispatcher) {
tunnelConfigDao.save(TunnelConfig.from(tunnelConf))
}
}
override suspend fun saveAll(tunnelConfs: List<TunnelConf>) {
withContext(ioDispatcher) {
tunnelConfigDao.saveAll(tunnelConfs.map(TunnelConfig::from))
}
}
override suspend fun updatePrimaryTunnel(tunnelConf: TunnelConf?) {
withContext(ioDispatcher) {
tunnelConfigDao.resetPrimaryTunnel()
tunnelConf?.let {
save(
it.copy(
isPrimaryTunnel = true,
),
)
}
}
}
override suspend fun updateMobileDataTunnel(tunnelConf: TunnelConf?) {
withContext(ioDispatcher) {
tunnelConfigDao.resetMobileDataTunnel()
tunnelConf?.let {
save(
it.copy(
isMobileDataTunnel = true,
),
)
}
}
}
override suspend fun updateEthernetTunnel(tunnelConf: TunnelConf?) {
withContext(ioDispatcher) {
tunnelConfigDao.resetEthernetTunnel()
tunnelConf?.let {
save(
it.copy(
isEthernetTunnel = true,
),
)
}
}
}
override suspend fun delete(tunnelConf: TunnelConf) {
withContext(ioDispatcher) {
tunnelConfigDao.delete(TunnelConfig.from(tunnelConf))
}
}
override suspend fun getById(id: Int): TunnelConf? {
return withContext(ioDispatcher) { tunnelConfigDao.getById(id.toLong())?.toTunnel() }
}
override suspend fun getActive(): Tunnels {
return withContext(ioDispatcher) {
tunnelConfigDao.getActive().map { it.toTunnel() }
}
}
override suspend fun count(): Int {
return withContext(ioDispatcher) { tunnelConfigDao.count().toInt() }
}
override suspend fun findByTunnelName(name: String): TunnelConf? {
return withContext(ioDispatcher) { tunnelConfigDao.getByName(name)?.toTunnel() }
}
override suspend fun findByTunnelNetworksName(name: String): Tunnels {
return withContext(ioDispatcher) { tunnelConfigDao.findByTunnelNetworkName(name).map { it.toTunnel() } }
}
override suspend fun findByMobileDataTunnel(): Tunnels {
return withContext(ioDispatcher) { tunnelConfigDao.findByMobileDataTunnel().map { it.toTunnel() } }
}
override suspend fun findPrimary(): Tunnels {
return withContext(ioDispatcher) { tunnelConfigDao.findByPrimary().map { it.toTunnel() } }
}
}
@@ -0,0 +1,14 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import kotlinx.coroutines.flow.Flow
interface SettingsRepository {
suspend fun save(settings: Settings)
fun getSettingsFlow(): Flow<Settings>
suspend fun getSettings(): Settings
suspend fun getAll(): List<Settings>
}
@@ -0,0 +1,35 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
import kotlinx.coroutines.flow.Flow
interface TunnelConfigRepository {
fun getTunnelConfigsFlow(): Flow<TunnelConfigs>
suspend fun getAll(): TunnelConfigs
suspend fun save(tunnelConfig: TunnelConfig)
suspend fun updatePrimaryTunnel(tunnelConfig: TunnelConfig?)
suspend fun updateMobileDataTunnel(tunnelConfig: TunnelConfig?)
suspend fun updateEthernetTunnel(tunnelConfig: TunnelConfig?)
suspend fun delete(tunnelConfig: TunnelConfig)
suspend fun getById(id: Int): TunnelConfig?
suspend fun getActive(): TunnelConfigs
suspend fun count(): Int
suspend fun findByTunnelName(name: String): TunnelConfig?
suspend fun findByTunnelNetworksName(name: String): TunnelConfigs
suspend fun findByMobileDataTunnel(): TunnelConfigs
suspend fun findPrimary(): TunnelConfigs
}
@@ -1,116 +0,0 @@
package com.zaneschepke.wireguardautotunnel.di
import android.content.Context
import com.wireguard.android.backend.WgQuickBackend
import com.wireguard.android.util.RootShell
import com.wireguard.android.util.ToolsInstaller
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.KernelTunnel
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
import com.zaneschepke.wireguardautotunnel.core.tunnel.UserspaceTunnel
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.AppSettingRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.runBlocking
import org.amnezia.awg.backend.Backend
import org.amnezia.awg.backend.GoBackend
import org.amnezia.awg.backend.RootTunnelActionHandler
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class TunnelModule {
@Provides
@Singleton
@TunnelShell
fun provideTunnelRootShell(@ApplicationContext context: Context): RootShell {
return RootShell(context)
}
@Provides
@Singleton
@AppShell
fun provideAppRootShell(@ApplicationContext context: Context): RootShell {
return RootShell(context)
}
@Provides
@Singleton
fun provideAmneziaBackend(@ApplicationContext context: Context): Backend {
return GoBackend(context, RootTunnelActionHandler(org.amnezia.awg.util.RootShell(context)))
}
@Provides
@Singleton
fun provideKernelBackend(@ApplicationContext context: Context, @TunnelShell shell: RootShell): com.wireguard.android.backend.Backend {
return WgQuickBackend(context, shell, ToolsInstaller(context, shell), com.wireguard.android.backend.RootTunnelActionHandler(shell)).also {
it.setMultipleTunnels(true)
}
}
@Provides
@Singleton
@Kernel
fun provideKernelProvider(
@IoDispatcher ioDispatcher: CoroutineDispatcher,
@ApplicationScope applicationScope: CoroutineScope,
serviceManager: ServiceManager,
appDataRepository: AppDataRepository,
backend: com.wireguard.android.backend.Backend,
): TunnelProvider {
return KernelTunnel(ioDispatcher, applicationScope, serviceManager, appDataRepository, backend)
}
@Provides
@Singleton
@Userspace
fun provideUserspaceProvider(
@IoDispatcher ioDispatcher: CoroutineDispatcher,
@ApplicationScope applicationScope: CoroutineScope,
serviceManager: ServiceManager,
appDataRepository: AppDataRepository,
backend: Backend,
): TunnelProvider {
return UserspaceTunnel(ioDispatcher, applicationScope, serviceManager, appDataRepository, backend)
}
@Provides
@Singleton
fun provideTunnelManager(
@Kernel kernelTunnel: TunnelProvider,
@Userspace userspaceTunnel: TunnelProvider,
appDataRepository: AppDataRepository,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
@ApplicationScope applicationScope: CoroutineScope,
): TunnelManager {
return TunnelManager(kernelTunnel, userspaceTunnel, appDataRepository, applicationScope, ioDispatcher)
}
@Provides
@Singleton
fun provideNetworkMonitor(@ApplicationContext context: Context, settingsRepository: AppSettingRepository): NetworkMonitor {
return AndroidNetworkMonitor(context) { runBlocking { settingsRepository.get().isWifiNameByShellEnabled } }
}
@Singleton
@Provides
fun provideServiceManager(
@ApplicationContext context: Context,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
@MainDispatcher mainCoroutineDispatcher: CoroutineDispatcher,
@ApplicationScope applicationScope: CoroutineScope,
appDataRepository: AppDataRepository,
): ServiceManager {
return ServiceManager(context, ioDispatcher, applicationScope, mainCoroutineDispatcher, appDataRepository)
}
}
@@ -1,29 +0,0 @@
package com.zaneschepke.wireguardautotunnel.domain.entity
data class AppSettings(
val id: Int = 0,
val isAutoTunnelEnabled: Boolean = false,
val isTunnelOnMobileDataEnabled: Boolean = false,
val trustedNetworkSSIDs: List<String> = emptyList(),
val isAlwaysOnVpnEnabled: Boolean = false,
val isTunnelOnEthernetEnabled: Boolean = false,
val isShortcutsEnabled: Boolean = false,
val isTunnelOnWifiEnabled: Boolean = false,
val isKernelEnabled: Boolean = false,
val isRestoreOnBootEnabled: Boolean = false,
val isMultiTunnelEnabled: Boolean = false,
val isPingEnabled: Boolean = false,
val isAmneziaEnabled: Boolean = false,
val isWildcardsEnabled: Boolean = false,
val isWifiNameByShellEnabled: Boolean = false,
val isStopOnNoInternetEnabled: Boolean = false,
val isVpnKillSwitchEnabled: Boolean = false,
val isKernelKillSwitchEnabled: Boolean = false,
val isLanOnKillSwitchEnabled: Boolean = false,
val debounceDelaySeconds: Int = 3,
val isDisableKillSwitchOnTrustedEnabled: Boolean = false,
) {
fun debounceDelayMillis(): Long {
return debounceDelaySeconds * 1000L
}
}
@@ -1,13 +0,0 @@
package com.zaneschepke.wireguardautotunnel.domain.entity
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
data class AppState(
val isLocationDisclosureShown: Boolean,
val isBatteryOptimizationDisableShown: Boolean,
val isPinLockEnabled: Boolean,
val isTunnelStatsExpanded: Boolean,
val isLocalLogsEnabled: Boolean,
val locale: String?,
val theme: Theme,
)
@@ -1,184 +0,0 @@
package com.zaneschepke.wireguardautotunnel.domain.entity
import com.wireguard.android.backend.Tunnel
import com.wireguard.config.Config
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.defaultName
import com.zaneschepke.wireguardautotunnel.util.extensions.extractNameAndNumber
import com.zaneschepke.wireguardautotunnel.util.extensions.hasNumberInParentheses
import com.zaneschepke.wireguardautotunnel.util.extensions.isReachable
import com.zaneschepke.wireguardautotunnel.util.extensions.toWgQuickString
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.InputStream
import java.net.InetAddress
import java.nio.charset.StandardCharsets
import kotlin.coroutines.CoroutineContext
data class TunnelConf(
val id: Int = 0,
val tunName: String,
val wgQuick: String,
val tunnelNetworks: List<String> = emptyList(),
val isMobileDataTunnel: Boolean = false,
val isPrimaryTunnel: Boolean = false,
val amQuick: String,
val isActive: Boolean = false,
val isPingEnabled: Boolean = false,
val pingInterval: Long? = null,
val pingCooldown: Long? = null,
val pingIp: String? = null,
val isEthernetTunnel: Boolean = false,
val isIpv4Preferred: Boolean = true,
@Transient
private var stateChangeCallback: ((Any) -> Unit)? = null,
) : Tunnel, org.amnezia.awg.backend.Tunnel {
fun setStateChangeCallback(callback: (Any) -> Unit) {
stateChangeCallback = callback
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is TunnelConf) return false
return id == other.id && tunName == other.tunName && wgQuick == other.wgQuick && amQuick == other.amQuick &&
isPrimaryTunnel == other.isPrimaryTunnel && isMobileDataTunnel == other.isMobileDataTunnel &&
isEthernetTunnel == other.isEthernetTunnel && isPingEnabled == other.isPingEnabled && pingIp == other.pingIp &&
pingCooldown == other.pingCooldown && pingInterval == other.pingInterval && tunnelNetworks == other.tunnelNetworks &&
isIpv4Preferred == other.isIpv4Preferred
}
override fun hashCode(): Int {
var result = id
result = 31 * result + tunName.hashCode()
result = 31 * result + wgQuick.hashCode()
result = 31 * result + amQuick.hashCode()
return result
}
fun copyWithCallback(
id: Int = this.id,
tunName: String = this.tunName,
wgQuick: String = this.wgQuick,
tunnelNetworks: List<String> = this.tunnelNetworks,
isMobileDataTunnel: Boolean = this.isMobileDataTunnel,
isPrimaryTunnel: Boolean = this.isPrimaryTunnel,
amQuick: String = this.amQuick,
isActive: Boolean = this.isActive,
isPingEnabled: Boolean = this.isPingEnabled,
pingInterval: Long? = this.pingInterval,
pingCooldown: Long? = this.pingCooldown,
pingIp: String? = this.pingIp,
isEthernetTunnel: Boolean = this.isEthernetTunnel,
isIpv4Preferred: Boolean = this.isIpv4Preferred,
): TunnelConf {
return TunnelConf(
id, tunName, wgQuick, tunnelNetworks, isMobileDataTunnel, isPrimaryTunnel,
amQuick, isActive, isPingEnabled, pingInterval, pingCooldown, pingIp,
isEthernetTunnel, isIpv4Preferred,
).apply {
stateChangeCallback = this@TunnelConf.stateChangeCallback
// tunnelStatsCallback = this@TunnelConf.tunnelStatsCallback
// bounceTunnelCallback = this@TunnelConf.bounceTunnelCallback
}
}
// fun onUpdateStatistics() {
// tunnelStatsCallback?.invoke()
// }
//
// fun bounceTunnel(tunnelConf: TunnelConf, reason: TunnelStatus.StopReason) {
// bounceTunnelCallback?.invoke(tunnelConf, reason)
// }
fun toAmConfig(): org.amnezia.awg.config.Config {
return configFromAmQuick(amQuick.ifBlank { wgQuick })
}
fun toWgConfig(): Config {
return configFromWgQuick(wgQuick)
}
override fun getName(): String = tunName
override fun isIpv4ResolutionPreferred(): Boolean = isIpv4Preferred
override fun onStateChange(newState: org.amnezia.awg.backend.Tunnel.State) {
stateChangeCallback?.invoke(newState)
}
override fun onStateChange(newState: Tunnel.State) {
stateChangeCallback?.invoke(newState)
}
fun isTunnelConfigChanged(updatedConf: TunnelConf): Boolean {
return updatedConf.wgQuick != wgQuick || updatedConf.amQuick != amQuick || updatedConf.name != name
}
fun generateUniqueName(tunnelNames: List<String>): String {
var tunnelName = this.tunName
var num = 1
while (tunnelNames.any { it == tunnelName }) {
tunnelName = if (!tunnelName.hasNumberInParentheses()) {
"$name($num)"
} else {
val pair = tunnelName.extractNameAndNumber()
"${pair?.first}($num)"
}
num++
}
return tunnelName
}
suspend fun isTunnelPingable(context: CoroutineContext): Boolean {
return withContext(context) {
val config = toWgConfig()
if (pingIp != null) {
return@withContext InetAddress.getByName(pingIp)
.isReachable(Constants.PING_TIMEOUT.toInt()).also {
Timber.i("Ping reachable $pingIp: $it")
}
}
config.peers.map { peer ->
peer.isReachable()
}.all { true }.also {
Timber.i("Ping of all peers reachable: $it")
}
}
}
companion object {
fun configFromWgQuick(wgQuick: String): Config {
val inputStream: InputStream = wgQuick.byteInputStream()
return inputStream.bufferedReader(StandardCharsets.UTF_8).use {
Config.parse(it)
}
}
fun configFromAmQuick(amQuick: String): org.amnezia.awg.config.Config {
val inputStream: InputStream = amQuick.byteInputStream()
return inputStream.bufferedReader(StandardCharsets.UTF_8).use {
org.amnezia.awg.config.Config.parse(it)
}
}
fun tunnelConfigFromAmConfig(config: org.amnezia.awg.config.Config, name: String? = null): TunnelConf {
val amQuick = config.toAwgQuickString(true)
val wgQuick = config.toWgQuickString()
return TunnelConf(tunName = name ?: config.defaultName(), wgQuick = wgQuick, amQuick = amQuick)
}
private const val IPV6_ALL_NETWORKS = "::/0"
private const val IPV4_ALL_NETWORKS = "0.0.0.0/0"
val ALL_IPS = listOf(IPV4_ALL_NETWORKS, IPV6_ALL_NETWORKS)
private val IPV4_PUBLIC_NETWORKS = listOf(
"0.0.0.0/5", "8.0.0.0/7", "11.0.0.0/8", "12.0.0.0/6", "16.0.0.0/4", "32.0.0.0/3",
"64.0.0.0/2", "128.0.0.0/3", "160.0.0.0/5", "168.0.0.0/6", "172.0.0.0/12",
"172.32.0.0/11", "172.64.0.0/10", "172.128.0.0/9", "173.0.0.0/8", "174.0.0.0/7",
"176.0.0.0/4", "192.0.0.0/9", "192.128.0.0/11", "192.160.0.0/13", "192.169.0.0/16",
"192.170.0.0/15", "192.172.0.0/14", "192.176.0.0/12", "192.192.0.0/10",
"193.0.0.0/8", "194.0.0.0/7", "196.0.0.0/6", "200.0.0.0/5", "208.0.0.0/4",
)
val LAN_BYPASS_ALLOWED_IPS = listOf(IPV6_ALL_NETWORKS) + IPV4_PUBLIC_NETWORKS
}
}
@@ -1,24 +0,0 @@
package com.zaneschepke.wireguardautotunnel.domain.enums
import com.zaneschepke.wireguardautotunnel.R
sealed class BackendError : Exception() {
data object DNS : BackendError()
data object Unauthorized : BackendError()
data object Config : BackendError()
data object KernelModuleName : BackendError()
data object InvalidConfig : BackendError()
data object NotAuthorized : BackendError()
data object ServiceNotRunning : BackendError()
data object Unknown : BackendError()
fun toStringRes() = when (this) {
Config -> R.string.config_error
DNS -> R.string.dns_resolve_error
InvalidConfig -> R.string.invalid_config_error
KernelModuleName -> R.string.kernel_name_error
NotAuthorized, Unauthorized -> R.string.auth_error
ServiceNotRunning -> R.string.service_running_error
Unknown -> R.string.unknown_error
}
}
@@ -1,6 +0,0 @@
package com.zaneschepke.wireguardautotunnel.domain.enums
enum class ConfigType {
AMNEZIA,
WG,
}
@@ -1,31 +0,0 @@
package com.zaneschepke.wireguardautotunnel.domain.enums
sealed class TunnelStatus {
data class Error(val error: BackendError) : TunnelStatus()
data object Up : TunnelStatus()
data object Down : TunnelStatus()
data class Stopping(val reason: StopReason) : TunnelStatus()
data object Starting : TunnelStatus()
enum class StopReason {
USER,
PING,
CONFIG_CHANGED,
}
fun isDown(): Boolean {
return this == Down
}
fun isUp(): Boolean {
return this == Up
}
fun isUpOrStarting(): Boolean {
return this == Up || this == Starting
}
fun isDownOrStopping(): Boolean {
return this == Down || this is Stopping
}
}
@@ -1,9 +0,0 @@
package com.zaneschepke.wireguardautotunnel.domain.events
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
sealed class AutoTunnelEvent {
data class Start(val tunnelConf: TunnelConf? = null) : AutoTunnelEvent()
data object Stop : AutoTunnelEvent()
data object DoNothing : AutoTunnelEvent()
}
@@ -1,7 +0,0 @@
package com.zaneschepke.wireguardautotunnel.domain.events
sealed class KillSwitchEvent {
data class Start(val allowedIps: List<String>) : KillSwitchEvent()
data object Stop : KillSwitchEvent()
data object DoNothing : KillSwitchEvent()
}
@@ -1,13 +0,0 @@
package com.zaneschepke.wireguardautotunnel.domain.repository
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
interface AppDataRepository {
suspend fun getPrimaryOrFirstTunnel(): TunnelConf?
suspend fun getStartTunnelConfig(): TunnelConf?
val settings: AppSettingRepository
val tunnels: TunnelRepository
val appState: AppStateRepository
}
@@ -1,10 +0,0 @@
package com.zaneschepke.wireguardautotunnel.domain.repository
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
import kotlinx.coroutines.flow.Flow
interface AppSettingRepository {
suspend fun save(appSettings: AppSettings)
val flow: Flow<AppSettings>
suspend fun get(): AppSettings
}
@@ -1,37 +0,0 @@
package com.zaneschepke.wireguardautotunnel.domain.repository
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.util.extensions.Tunnels
import kotlinx.coroutines.flow.Flow
interface TunnelRepository {
val flow: Flow<List<TunnelConf>>
suspend fun getAll(): Tunnels
suspend fun save(tunnelConf: TunnelConf)
suspend fun saveAll(tunnelConfList: List<TunnelConf>)
suspend fun updatePrimaryTunnel(tunnelConf: TunnelConf?)
suspend fun updateMobileDataTunnel(tunnelConf: TunnelConf?)
suspend fun updateEthernetTunnel(tunnelConf: TunnelConf?)
suspend fun delete(tunnelConf: TunnelConf)
suspend fun getById(id: Int): TunnelConf?
suspend fun getActive(): Tunnels
suspend fun count(): Int
suspend fun findByTunnelName(name: String): TunnelConf?
suspend fun findByTunnelNetworksName(name: String): Tunnels
suspend fun findByMobileDataTunnel(): Tunnels
suspend fun findPrimary(): Tunnels
}
@@ -1,10 +0,0 @@
package com.zaneschepke.wireguardautotunnel.domain.state
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
data class TunnelState(
val status: TunnelStatus = TunnelStatus.Down,
val backendState: BackendState = BackendState.INACTIVE,
val statistics: TunnelStatistics? = null,
)
@@ -1,12 +1,12 @@
package com.zaneschepke.wireguardautotunnel.di
package com.zaneschepke.wireguardautotunnel.module
import android.content.Context
import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.logcatter.LogcatReader
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
import com.zaneschepke.wireguardautotunnel.core.shortcut.DynamicShortcutManager
import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.notification.WireGuardNotification
import com.zaneschepke.wireguardautotunnel.service.shortcut.DynamicShortcutManager
import com.zaneschepke.wireguardautotunnel.service.shortcut.ShortcutManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -34,7 +34,7 @@ class AppModule {
@Singleton
@Provides
fun provideNotificationService(@ApplicationContext context: Context): NotificationManager {
fun provideNotificationService(@ApplicationContext context: Context): NotificationService {
return WireGuardNotification(context)
}
@@ -1,15 +1,7 @@
package com.zaneschepke.wireguardautotunnel.di
package com.zaneschepke.wireguardautotunnel.module
import javax.inject.Qualifier
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class TunnelShell
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AppShell
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class Kernel
@@ -17,3 +9,11 @@ annotation class Kernel
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class Userspace
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class TunnelShell
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AppShell
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.di
package com.zaneschepke.wireguardautotunnel.module
import javax.inject.Qualifier
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.di
package com.zaneschepke.wireguardautotunnel.module
import dagger.Module
import dagger.Provides
@@ -1,21 +1,21 @@
package com.zaneschepke.wireguardautotunnel.di
package com.zaneschepke.wireguardautotunnel.module
import android.content.Context
import androidx.room.Room
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
import com.zaneschepke.wireguardautotunnel.data.DataStoreManager
import com.zaneschepke.wireguardautotunnel.data.DatabaseCallback
import com.zaneschepke.wireguardautotunnel.data.dao.SettingsDao
import com.zaneschepke.wireguardautotunnel.data.dao.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.data.SettingsDao
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRoomRepository
import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.data.repository.DataStoreAppStateRepository
import com.zaneschepke.wireguardautotunnel.data.repository.RoomSettingsRepository
import com.zaneschepke.wireguardautotunnel.data.repository.RoomTunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.AppSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.data.repository.RoomTunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -54,13 +54,13 @@ class RepositoryModule {
@Singleton
@Provides
fun provideTunnelConfigRepository(tunnelConfigDao: TunnelConfigDao, @IoDispatcher ioDispatcher: CoroutineDispatcher): TunnelRepository {
return RoomTunnelRepository(tunnelConfigDao, ioDispatcher)
fun provideTunnelConfigRepository(tunnelConfigDao: TunnelConfigDao, @IoDispatcher ioDispatcher: CoroutineDispatcher): TunnelConfigRepository {
return RoomTunnelConfigRepository(tunnelConfigDao, ioDispatcher)
}
@Singleton
@Provides
fun provideSettingsRepository(settingsDao: SettingsDao, @IoDispatcher ioDispatcher: CoroutineDispatcher): AppSettingRepository {
fun provideSettingsRepository(settingsDao: SettingsDao, @IoDispatcher ioDispatcher: CoroutineDispatcher): SettingsRepository {
return RoomSettingsRepository(settingsDao, ioDispatcher)
}
@@ -79,10 +79,10 @@ class RepositoryModule {
@Provides
@Singleton
fun provideAppDataRepository(
settingsRepository: AppSettingRepository,
tunnelRepository: TunnelRepository,
settingsRepository: SettingsRepository,
tunnelConfigRepository: TunnelConfigRepository,
appStateRepository: AppStateRepository,
): AppDataRepository {
return AppDataRoomRepository(settingsRepository, tunnelRepository, appStateRepository)
return AppDataRoomRepository(settingsRepository, tunnelConfigRepository, appStateRepository)
}
}
@@ -0,0 +1,16 @@
package com.zaneschepke.wireguardautotunnel.module
import com.zaneschepke.wireguardautotunnel.service.network.InternetConnectivityService
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
abstract class ServiceModule {
@Binds
abstract fun provideInternetConnectivityService(wifiService: InternetConnectivityService): NetworkService
}
@@ -0,0 +1,106 @@
package com.zaneschepke.wireguardautotunnel.module
import android.content.Context
import com.wireguard.android.backend.Backend
import com.wireguard.android.backend.GoBackend
import com.wireguard.android.backend.RootTunnelActionHandler
import com.wireguard.android.backend.WgQuickBackend
import com.wireguard.android.util.RootShell
import com.wireguard.android.util.ToolsInstaller
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.WireGuardTunnel
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import javax.inject.Provider
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class TunnelModule {
@Provides
@Singleton
@TunnelShell
fun provideTunnelRootShell(@ApplicationContext context: Context): RootShell {
return RootShell(context)
}
@Provides
@Singleton
@AppShell
fun provideAppRootShell(@ApplicationContext context: Context): RootShell {
return RootShell(context)
}
@Provides
@Singleton
fun provideRootShellAm(@ApplicationContext context: Context): org.amnezia.awg.util.RootShell {
return org.amnezia.awg.util.RootShell(context)
}
@Provides
@Singleton
@Userspace
fun provideUserspaceBackend(@ApplicationContext context: Context, @TunnelShell rootShell: RootShell): Backend {
return GoBackend(context, RootTunnelActionHandler(rootShell))
}
@Provides
@Singleton
@Kernel
fun provideKernelBackend(@ApplicationContext context: Context, @TunnelShell rootShell: RootShell): Backend {
return WgQuickBackend(context, rootShell, ToolsInstaller(context, rootShell), RootTunnelActionHandler(rootShell))
}
@Provides
@Singleton
fun provideAmneziaBackend(@ApplicationContext context: Context, rootShell: org.amnezia.awg.util.RootShell): org.amnezia.awg.backend.Backend {
return org.amnezia.awg.backend.GoBackend(context, org.amnezia.awg.backend.RootTunnelActionHandler(rootShell))
}
@Provides
@Singleton
fun provideVpnService(
amneziaBackend: Provider<org.amnezia.awg.backend.Backend>,
@Kernel kernelBackend: Provider<Backend>,
appDataRepository: AppDataRepository,
tunnelConfigRepository: TunnelConfigRepository,
@ApplicationScope applicationScope: CoroutineScope,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
serviceManager: ServiceManager,
notificationService: NotificationService,
internetConnectivityService: NetworkService,
): TunnelService {
return WireGuardTunnel(
amneziaBackend,
tunnelConfigRepository,
kernelBackend,
appDataRepository,
applicationScope,
ioDispatcher,
serviceManager,
notificationService,
internetConnectivityService,
)
}
@Singleton
@Provides
fun provideServiceManager(
@ApplicationContext context: Context,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
appDataRepository: AppDataRepository,
): ServiceManager {
return ServiceManager(context, ioDispatcher, appDataRepository)
}
}
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.di
package com.zaneschepke.wireguardautotunnel.module
import android.content.Context
import com.zaneschepke.wireguardautotunnel.util.FileUtils
@@ -0,0 +1,49 @@
package com.zaneschepke.wireguardautotunnel.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Provider
@AndroidEntryPoint
class AppUpdateReceiver : BroadcastReceiver() {
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
@Inject
lateinit var appDataRepository: AppDataRepository
@Inject
lateinit var tunnelService: Provider<TunnelService>
@Inject
lateinit var serviceManager: ServiceManager
override fun onReceive(context: Context, intent: Intent) {
if (intent.action != Intent.ACTION_MY_PACKAGE_REPLACED) return
serviceManager.updateTunnelTile()
serviceManager.updateAutoTunnelTile()
applicationScope.launch {
val settings = appDataRepository.settings.getSettings()
if (settings.isAutoTunnelEnabled) {
Timber.i("Restarting services after upgrade")
serviceManager.startAutoTunnel(true)
}
if (!settings.isAutoTunnelEnabled) {
val tunnels = appDataRepository.tunnels.getAll().filter { it.isActive }
if (tunnels.isNotEmpty()) tunnelService.get().startTunnel(tunnels.first())
}
}
}
}
@@ -0,0 +1,54 @@
package com.zaneschepke.wireguardautotunnel.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Provider
@AndroidEntryPoint
class BootReceiver : BroadcastReceiver() {
@Inject
lateinit var appDataRepository: AppDataRepository
@Inject
lateinit var tunnelService: Provider<TunnelService>
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
@Inject
lateinit var serviceManager: ServiceManager
override fun onReceive(context: Context, intent: Intent) {
if (Intent.ACTION_BOOT_COMPLETED != intent.action) return
serviceManager.updateTunnelTile()
serviceManager.updateAutoTunnelTile()
applicationScope.launch {
with(appDataRepository.settings.getSettings()) {
if (isRestoreOnBootEnabled) {
val activeTunnels = appDataRepository.tunnels.getActive()
val tunState = tunnelService.get().vpnState.value.status
if (activeTunnels.isNotEmpty() && tunState != TunnelState.UP) {
Timber.i("Starting previously active tunnel")
tunnelService.get().startTunnel(activeTunnels.first())
}
if (isAutoTunnelEnabled) {
Timber.i("Starting watcher service from boot")
serviceManager.startAutoTunnel(true)
}
}
}
}
}
}
@@ -1,41 +1,43 @@
package com.zaneschepke.wireguardautotunnel.core.broadcast
package com.zaneschepke.wireguardautotunnel.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject
import javax.inject.Provider
@AndroidEntryPoint
class KernelReceiver : BroadcastReceiver() {
@Inject
lateinit var tunnelService: Provider<TunnelService>
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
@Inject
lateinit var tunnelRepository: TunnelRepository
lateinit var tunnelConfigRepository: TunnelConfigRepository
@Inject
lateinit var serviceManager: ServiceManager
@Inject
lateinit var tunnelManager: TunnelManager
override fun onReceive(context: Context, intent: Intent) {
val action = intent.action ?: return
applicationScope.launch {
if (action == REFRESH_TUNNELS_ACTION) {
tunnelManager.runningTunnelNames().forEach { name ->
val tunnel = tunnelRepository.findByTunnelName(name)
tunnelService.get().runningTunnelNames().forEach { name ->
// TODO can optimize later
val tunnel = tunnelConfigRepository.findByTunnelName(name)
tunnel?.let {
tunnelRepository.save(it.copy(isActive = true))
tunnelConfigRepository.save(it.copy(isActive = true))
}
}
serviceManager.updateTunnelTile()
@@ -0,0 +1,36 @@
package com.zaneschepke.wireguardautotunnel.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationAction
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint
class NotificationActionReceiver : BroadcastReceiver() {
@Inject
lateinit var serviceManager: ServiceManager
@Inject
lateinit var tunnelService: TunnelService
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
override fun onReceive(context: Context, intent: Intent) {
applicationScope.launch {
when (intent.action) {
NotificationAction.AUTO_TUNNEL_OFF.name -> serviceManager.stopAutoTunnel()
NotificationAction.TUNNEL_OFF.name -> tunnelService.stopTunnel()
}
}
}
}
@@ -0,0 +1,119 @@
package com.zaneschepke.wireguardautotunnel.service.foreground
import android.app.Service
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.AutoTunnelService
import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelControlTile
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
import jakarta.inject.Inject
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.withContext
import timber.log.Timber
@OptIn(ExperimentalCoroutinesApi::class)
class ServiceManager
@Inject constructor(private val context: Context, private val ioDispatcher: CoroutineDispatcher, private val appDataRepository: AppDataRepository) {
private val _autoTunnelActive = MutableStateFlow(false)
val autoTunnelActive = _autoTunnelActive.asStateFlow()
var autoTunnelService = CompletableDeferred<AutoTunnelService>()
var backgroundService = CompletableDeferred<TunnelForegroundService>()
var autoTunnelTile = CompletableDeferred<AutoTunnelControlTile>()
var tunnelControlTile = CompletableDeferred<TunnelControlTile>()
private fun <T : Service> startService(cls: Class<T>, background: Boolean) {
runCatching {
val intent = Intent(context, cls)
if (background) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
}.onFailure { Timber.e(it) }
}
suspend fun startAutoTunnel(background: Boolean) {
val settings = appDataRepository.settings.getSettings()
appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = true))
if (autoTunnelService.isCompleted) return _autoTunnelActive.update { true }
kotlin.runCatching {
startService(AutoTunnelService::class.java, background)
autoTunnelService.await()
autoTunnelService.getCompleted().start()
_autoTunnelActive.update { true }
updateAutoTunnelTile()
}.onFailure {
Timber.e(it)
}
}
suspend fun startBackgroundService(tunnelConfig: TunnelConfig) {
if (backgroundService.isCompleted) return
kotlin.runCatching {
startService(TunnelForegroundService::class.java, true)
backgroundService.await()
backgroundService.getCompleted().start(tunnelConfig)
}.onFailure {
Timber.e(it)
}
}
fun stopBackgroundService() {
if (!backgroundService.isCompleted) return
runCatching {
backgroundService.getCompleted().stop()
}.onFailure {
Timber.e(it)
}
}
suspend fun toggleAutoTunnel(background: Boolean) {
withContext(ioDispatcher) {
if (_autoTunnelActive.value) return@withContext stopAutoTunnel()
startAutoTunnel(background)
}
}
fun updateAutoTunnelTile() {
if (autoTunnelTile.isCompleted) {
autoTunnelTile.getCompleted().updateTileState()
} else {
context.requestAutoTunnelTileServiceUpdate()
}
}
fun updateTunnelTile() {
if (tunnelControlTile.isCompleted) {
tunnelControlTile.getCompleted().updateTileState()
} else {
context.requestTunnelTileServiceStateUpdate()
}
}
suspend fun stopAutoTunnel() {
withContext(ioDispatcher) {
val settings = appDataRepository.settings.getSettings()
appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = false))
if (!autoTunnelService.isCompleted) return@withContext
runCatching {
autoTunnelService.getCompleted().stop()
_autoTunnelActive.update { false }
updateAutoTunnelTile()
}.onFailure {
Timber.e(it)
}
}
}
}
@@ -0,0 +1,70 @@
package com.zaneschepke.wireguardautotunnel.service.foreground
import android.app.Notification
import android.content.Intent
import android.os.IBinder
import androidx.core.app.ServiceCompat
import androidx.lifecycle.LifecycleService
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationAction
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.notification.WireGuardNotification
import com.zaneschepke.wireguardautotunnel.util.Constants
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CompletableDeferred
import javax.inject.Inject
@AndroidEntryPoint
class TunnelForegroundService : LifecycleService() {
@Inject
lateinit var notificationService: NotificationService
@Inject
lateinit var serviceManager: ServiceManager
override fun onCreate() {
super.onCreate()
serviceManager.backgroundService.complete(this)
}
override fun onBind(intent: Intent): IBinder? {
super.onBind(intent)
return null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
serviceManager.backgroundService.complete(this)
return super.onStartCommand(intent, flags, startId)
}
fun start(tunnelConfig: TunnelConfig) {
ServiceCompat.startForeground(
this@TunnelForegroundService,
NotificationService.KERNEL_SERVICE_NOTIFICATION_ID,
createNotification(tunnelConfig),
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
)
}
fun stop() {
stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf()
}
override fun onDestroy() {
serviceManager.backgroundService = CompletableDeferred()
super.onDestroy()
}
private fun createNotification(tunnelConfig: TunnelConfig?): Notification {
return notificationService.createNotification(
WireGuardNotification.NotificationChannels.VPN,
title = "${getString(R.string.tunnel_running)} - ${tunnelConfig?.name}",
actions = listOf(
notificationService.createNotificationAction(NotificationAction.TUNNEL_OFF),
),
)
}
}
@@ -1,43 +1,44 @@
package com.zaneschepke.wireguardautotunnel.core.service.autotunnel
package com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel
import android.content.Intent
import android.net.NetworkCapabilities
import android.os.IBinder
import android.os.PowerManager
import androidx.core.app.ServiceCompat
import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope
import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.networkmonitor.NetworkStatus
import com.wireguard.android.util.RootShell
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
import com.zaneschepke.wireguardautotunnel.domain.events.KillSwitchEvent
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.state.AutoTunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.NetworkState
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.AppShell
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.module.MainImmediateDispatcher
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model.AutoTunnelEvent
import com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model.AutoTunnelState
import com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model.KillSwitchEvent
import com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model.NetworkState
import com.zaneschepke.wireguardautotunnel.service.network.InternetConnectivityService
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
import com.zaneschepke.wireguardautotunnel.service.network.NetworkStatus
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationAction
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.notification.WireGuardNotification
import com.zaneschepke.wireguardautotunnel.service.tunnel.BackendState
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.Tunnels
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
import com.zaneschepke.wireguardautotunnel.util.extensions.getCurrentWifiName
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
@@ -49,13 +50,20 @@ import javax.inject.Provider
class AutoTunnelService : LifecycleService() {
@Inject
lateinit var networkMonitor: NetworkMonitor
@AppShell
lateinit var rootShell: Provider<RootShell>
@Inject
lateinit var networkService: NetworkService
@Inject
lateinit var appDataRepository: Provider<AppDataRepository>
@Inject
lateinit var notificationManager: NotificationManager
lateinit var notificationService: NotificationService
@Inject
lateinit var tunnelService: Provider<TunnelService>
@Inject
@IoDispatcher
@@ -65,7 +73,8 @@ class AutoTunnelService : LifecycleService() {
lateinit var serviceManager: ServiceManager
@Inject
lateinit var tunnelManager: TunnelManager
@MainImmediateDispatcher
lateinit var mainImmediateDispatcher: CoroutineDispatcher
private val defaultState = AutoTunnelState()
@@ -73,12 +82,16 @@ class AutoTunnelService : LifecycleService() {
private var wakeLock: PowerManager.WakeLock? = null
private var killSwitchJob: Job? = null
override fun onCreate() {
super.onCreate()
serviceManager.autoTunnelService.complete(this)
launchWatcherNotification()
lifecycleScope.launch(mainImmediateDispatcher) {
kotlin.runCatching {
launchWatcherNotification()
}.onFailure {
Timber.e(it)
}
}
}
override fun onBind(intent: Intent): IBinder? {
@@ -87,20 +100,20 @@ class AutoTunnelService : LifecycleService() {
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
Timber.d("onStartCommand executed with startId: $startId")
serviceManager.autoTunnelService.complete(this)
start()
return START_STICKY
return super.onStartCommand(intent, flags, startId)
}
fun start() {
kotlin.runCatching {
launchWatcherNotification()
initWakeLock()
lifecycleScope.launch(mainImmediateDispatcher) {
launchWatcherNotification()
initWakeLock()
}
startAutoTunnelJob()
startAutoTunnelStateJob()
killSwitchJob = startKillSwitchJob()
startKillSwitchJob()
}.onFailure {
Timber.e(it)
}
@@ -113,33 +126,22 @@ class AutoTunnelService : LifecycleService() {
override fun onDestroy() {
serviceManager.autoTunnelService = CompletableDeferred()
restoreVpnKillSwitch()
super.onDestroy()
}
private fun restoreVpnKillSwitch() {
with(autoTunnelStateFlow.value) {
if (settings.isVpnKillSwitchEnabled && tunnelManager.getBackendState() != BackendState.KILL_SWITCH_ACTIVE) {
killSwitchJob?.cancel()
val allowedIps = if (settings.isLanOnKillSwitchEnabled) TunnelConf.LAN_BYPASS_ALLOWED_IPS else emptyList()
tunnelManager.setBackendState(BackendState.KILL_SWITCH_ACTIVE, allowedIps)
}
}
}
private fun launchWatcherNotification(description: String = getString(R.string.monitoring_state_changes)) {
val notification =
notificationManager.createNotification(
notificationService.createNotification(
WireGuardNotification.NotificationChannels.AUTO_TUNNEL,
title = getString(R.string.auto_tunnel_title),
description = description,
actions = listOf(
notificationManager.createNotificationAction(NotificationAction.AUTO_TUNNEL_OFF),
notificationService.createNotificationAction(NotificationAction.AUTO_TUNNEL_OFF),
),
)
ServiceCompat.startForeground(
this,
NotificationManager.AUTO_TUNNEL_NOTIFICATION_ID,
NotificationService.AUTO_TUNNEL_NOTIFICATION_ID,
notification,
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
)
@@ -159,58 +161,48 @@ class AutoTunnelService : LifecycleService() {
}
}
private fun buildNetworkState(networkStatus: NetworkStatus): NetworkState {
suspend fun buildNetworkState(networkStatus: NetworkStatus): NetworkState {
return with(autoTunnelStateFlow.value.networkState) {
val wifiName = when (networkStatus) {
is NetworkStatus.Connected -> {
networkStatus.wifiSsid
val wifiName = when {
networkStatus.wifiAvailable &&
(wifiName == null || wifiName == Constants.UNREADABLE_SSID || networkService.didWifiChangeSinceLastCapabilitiesQuery) -> {
networkService.getWifiCapabilities()?.let { getWifiName(it) } ?: wifiName
}
else -> null
!networkStatus.wifiAvailable -> null
else -> wifiName
}
copy(
isWifiConnected = networkStatus.wifiConnected,
isMobileDataConnected = networkStatus.cellularConnected,
isEthernetConnected = networkStatus.ethernetConnected,
wifiName = wifiName,
)
copy(networkStatus.wifiAvailable, networkStatus.cellularAvailable, networkStatus.ethernetAvailable, wifiName)
}
}
@OptIn(ExperimentalCoroutinesApi::class)
private fun startAutoTunnelStateJob() = lifecycleScope.launch(ioDispatcher) {
combine(
combineSettings(),
appDataRepository.get().settings.flow
.distinctUntilChanged { old, new -> old.isKernelEnabled == new.isKernelEnabled } // Only emit when isKernelEnabled changes
.flatMapLatest {
networkMonitor.networkStatusFlow
.flowOn(ioDispatcher)
.map { buildNetworkState(it) }
}
.distinctUntilChanged(),
networkService.status.map {
buildNetworkState(it)
}.distinctUntilChanged(),
) { double, networkState ->
AutoTunnelState(
tunnelManager.activeTunnels.value,
networkState,
double.first,
double.second,
)
AutoTunnelState(tunnelService.get().vpnState.value, networkState, double.first, double.second)
}.collect { state ->
autoTunnelStateFlow.update {
it.copy(
activeTunnels = state.activeTunnels,
networkState = state.networkState,
settings = state.settings,
tunnels = state.tunnels,
)
it.copy(vpnState = state.vpnState, networkState = state.networkState, settings = state.settings, tunnels = state.tunnels)
}
}
}
private fun combineSettings(): Flow<Pair<AppSettings, Tunnels>> {
private suspend fun getWifiName(wifiCapabilities: NetworkCapabilities): String? {
val setting = appDataRepository.get().settings.getSettings()
return if (setting.isWifiNameByShellEnabled) {
rootShell.get().getCurrentWifiName()
} else {
InternetConnectivityService.getNetworkName(wifiCapabilities, this@AutoTunnelService)
}
}
private fun combineSettings(): Flow<Pair<Settings, TunnelConfigs>> {
return combine(
appDataRepository.get().settings.flow,
appDataRepository.get().tunnels.flow.map { tunnels ->
appDataRepository.get().settings.getSettingsFlow(),
appDataRepository.get().tunnels.getTunnelConfigsFlow().map { tunnels ->
// isActive is ignored for equality checks so user can manually toggle off tunnel with auto-tunnel
tunnels.map { it.copy(isActive = false) }
},
@@ -226,11 +218,11 @@ class AutoTunnelService : LifecycleService() {
KillSwitchEvent.DoNothing -> Unit
is KillSwitchEvent.Start -> {
Timber.d("Starting kill switch")
tunnelManager.setBackendState(BackendState.KILL_SWITCH_ACTIVE, event.allowedIps)
tunnelService.get().setBackendState(BackendState.KILL_SWITCH_ACTIVE, event.allowedIps)
}
KillSwitchEvent.Stop -> {
Timber.d("Stopping kill switch")
tunnelManager.setBackendState(BackendState.SERVICE_ACTIVE, emptySet())
tunnelService.get().setBackendState(BackendState.SERVICE_ACTIVE, emptySet())
}
}
}
@@ -239,17 +231,17 @@ class AutoTunnelService : LifecycleService() {
@OptIn(FlowPreview::class)
private fun startAutoTunnelJob() = lifecycleScope.launch(ioDispatcher) {
Timber.i("Starting auto-tunnel network event watcher")
val settings = appDataRepository.get().settings.get()
val settings = appDataRepository.get().settings.getSettings()
Timber.d("Starting with debounce delay of: ${settings.debounceDelaySeconds} seconds")
autoTunnelStateFlow.debounce(settings.debounceDelayMillis()).collect { watcherState ->
if (watcherState == defaultState) return@collect
Timber.d("New auto tunnel state emitted ${watcherState.networkState}")
Timber.d("New auto tunnel state emitted")
when (val event = watcherState.asAutoTunnelEvent()) {
is AutoTunnelEvent.Start -> (event.tunnelConf ?: appDataRepository.get().getPrimaryOrFirstTunnel())?.let {
tunnelManager.startTunnel(it)
}
// TODO improve this to target specific tunnels to better support multi-tunnel
is AutoTunnelEvent.Stop -> tunnelManager.stopTunnel()
is AutoTunnelEvent.Start -> tunnelService.get().startTunnel(
event.tunnelConfig
?: appDataRepository.get().getPrimaryOrFirstTunnel(),
)
is AutoTunnelEvent.Stop -> tunnelService.get().stopTunnel()
AutoTunnelEvent.DoNothing -> Timber.i("Auto-tunneling: no condition met")
}
}
@@ -0,0 +1,9 @@
package com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
sealed class AutoTunnelEvent {
data class Start(val tunnelConfig: TunnelConfig? = null) : AutoTunnelEvent()
data object Stop : AutoTunnelEvent()
data object DoNothing : AutoTunnelEvent()
}
@@ -1,19 +1,17 @@
package com.zaneschepke.wireguardautotunnel.domain.state
package com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model
import com.zaneschepke.wireguardautotunnel.core.tunnel.allDown
import com.zaneschepke.wireguardautotunnel.core.tunnel.hasActive
import com.zaneschepke.wireguardautotunnel.core.tunnel.isUp
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
import com.zaneschepke.wireguardautotunnel.domain.events.KillSwitchEvent
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.tunnel.BackendState
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
import com.zaneschepke.wireguardautotunnel.util.extensions.isMatchingToWildcardList
data class AutoTunnelState(
val activeTunnels: Map<TunnelConf, TunnelState> = emptyMap(),
val vpnState: VpnState = VpnState(),
val networkState: NetworkState = NetworkState(),
val settings: AppSettings = AppSettings(),
val tunnels: List<TunnelConf> = emptyList(),
val settings: Settings = Settings(),
val tunnels: TunnelConfigs = emptyList(),
) {
private fun isMobileDataActive(): Boolean {
@@ -23,23 +21,23 @@ data class AutoTunnelState(
private fun isMobileTunnelDataChangeNeeded(): Boolean {
val preferredTunnel = preferredMobileDataTunnel()
return preferredTunnel != null &&
activeTunnels.isNotEmpty() && !activeTunnels.isUp(preferredTunnel)
vpnState.status.isUp() && preferredTunnel.id != vpnState.tunnelConfig?.id
}
private fun isEthernetTunnelChangeNeeded(): Boolean {
val preferredTunnel = preferredEthernetTunnel()
return preferredTunnel != null && activeTunnels.isNotEmpty() && !activeTunnels.isUp(preferredTunnel)
return preferredTunnel != null && vpnState.status.isUp() && preferredTunnel.id != vpnState.tunnelConfig?.id
}
private fun preferredMobileDataTunnel(): TunnelConf? {
private fun preferredMobileDataTunnel(): TunnelConfig? {
return tunnels.firstOrNull { it.isMobileDataTunnel } ?: tunnels.firstOrNull { it.isPrimaryTunnel }
}
private fun preferredEthernetTunnel(): TunnelConf? {
private fun preferredEthernetTunnel(): TunnelConfig? {
return tunnels.firstOrNull { it.isEthernetTunnel } ?: tunnels.firstOrNull { it.isPrimaryTunnel }
}
private fun preferredWifiTunnel(): TunnelConf? {
private fun preferredWifiTunnel(): TunnelConfig? {
return getTunnelWithMatchingTunnelNetwork() ?: tunnels.firstOrNull { it.isPrimaryTunnel }
}
@@ -48,33 +46,31 @@ data class AutoTunnelState(
}
private fun startOnEthernet(): Boolean {
return networkState.isEthernetConnected && settings.isTunnelOnEthernetEnabled && activeTunnels.allDown()
return networkState.isEthernetConnected && settings.isTunnelOnEthernetEnabled && vpnState.status.isDown()
}
private fun stopOnEthernet(): Boolean {
return networkState.isEthernetConnected && !settings.isTunnelOnEthernetEnabled && activeTunnels.hasActive()
return networkState.isEthernetConnected && !settings.isTunnelOnEthernetEnabled && vpnState.status.isUp()
}
// TODO test removed kill switch state check
private fun stopKillSwitchOnTrusted(): Boolean {
return networkState.isWifiConnected && settings.isVpnKillSwitchEnabled && settings.isDisableKillSwitchOnTrustedEnabled && isCurrentSSIDTrusted()
return networkState.isWifiConnected && settings.isVpnKillSwitchEnabled && settings.isDisableKillSwitchOnTrustedEnabled && isCurrentSSIDTrusted() && vpnState.backendState == BackendState.KILL_SWITCH_ACTIVE
}
// TODO test, removed kill switch state check
private fun startKillSwitch(): Boolean {
return settings.isVpnKillSwitchEnabled && (!settings.isDisableKillSwitchOnTrustedEnabled || !isCurrentSSIDTrusted())
return settings.isVpnKillSwitchEnabled && vpnState.backendState != BackendState.KILL_SWITCH_ACTIVE && (!settings.isDisableKillSwitchOnTrustedEnabled || !isCurrentSSIDTrusted())
}
private fun isNoConnectivity(): Boolean {
fun isNoConnectivity(): Boolean {
return !networkState.isEthernetConnected && !networkState.isWifiConnected && !networkState.isMobileDataConnected
}
private fun stopOnMobileData(): Boolean {
return isMobileDataActive() && !settings.isTunnelOnMobileDataEnabled && activeTunnels.hasActive()
return isMobileDataActive() && !settings.isTunnelOnMobileDataEnabled && vpnState.status.isUp()
}
private fun startOnMobileData(): Boolean {
return isMobileDataActive() && settings.isTunnelOnMobileDataEnabled && activeTunnels.allDown()
return isMobileDataActive() && settings.isTunnelOnMobileDataEnabled && vpnState.status.isDown()
}
private fun changeOnMobileData(): Boolean {
@@ -86,24 +82,29 @@ data class AutoTunnelState(
}
private fun stopOnWifi(): Boolean {
return isWifiActive() && !settings.isTunnelOnWifiEnabled && activeTunnels.hasActive()
return isWifiActive() && !settings.isTunnelOnWifiEnabled && vpnState.status.isUp()
}
private fun stopOnTrustedWifi(): Boolean {
return isWifiActive() && settings.isTunnelOnWifiEnabled && activeTunnels.hasActive() && isCurrentSSIDTrusted()
return isWifiActive() && settings.isTunnelOnWifiEnabled && vpnState.status.isUp() && isCurrentSSIDTrusted()
}
private fun startOnUntrustedWifi(): Boolean {
return isWifiActive() && settings.isTunnelOnWifiEnabled && activeTunnels.allDown() && !isCurrentSSIDTrusted()
return isWifiActive() && settings.isTunnelOnWifiEnabled && vpnState.status.isDown() && !isCurrentSSIDTrusted()
}
private fun changeOnUntrustedWifi(): Boolean {
return isWifiActive() && settings.isTunnelOnWifiEnabled && activeTunnels.hasActive() && !isCurrentSSIDTrusted() && !isWifiTunnelPreferred()
return isWifiActive() && settings.isTunnelOnWifiEnabled && vpnState.status.isUp() && !isCurrentSSIDTrusted() && !isWifiTunnelPreferred()
}
private fun isWifiTunnelPreferred(): Boolean {
val preferred = preferredWifiTunnel()
return preferred?.let { activeTunnels.isUp(it) } ?: true
val vpnTunnel = vpnState.tunnelConfig
return if (preferred != null && vpnTunnel != null) {
preferred.id == vpnTunnel.id
} else {
true
}
}
fun asAutoTunnelEvent(): AutoTunnelEvent {
@@ -128,7 +129,7 @@ data class AutoTunnelState(
return when {
stopKillSwitchOnTrusted() -> KillSwitchEvent.Stop
startKillSwitch() -> {
val allowedIps = if (settings.isLanOnKillSwitchEnabled) TunnelConf.LAN_BYPASS_ALLOWED_IPS else emptyList()
val allowedIps = if (settings.isLanOnKillSwitchEnabled) TunnelConfig.LAN_BYPASS_ALLOWED_IPS else emptySet()
KillSwitchEvent.Start(allowedIps)
}
else -> KillSwitchEvent.DoNothing
@@ -149,11 +150,16 @@ data class AutoTunnelState(
}
}
private fun getTunnelWithMatchingTunnelNetwork(): TunnelConf? {
private fun getTunnelWithMatchingTunnelNetwork(): TunnelConfig? {
return networkState.wifiName?.let { wifiName ->
tunnels.firstOrNull {
hasTrustedWifiName(wifiName, it.tunnelNetworks)
}
}
}
fun isPingEnabled(): Boolean {
return settings.isPingEnabled ||
(vpnState.status.isUp() && vpnState.tunnelConfig != null && tunnels.first { it.id == vpnState.tunnelConfig.id }.isPingEnabled)
}
}
@@ -0,0 +1,7 @@
package com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model
sealed class KillSwitchEvent {
data class Start(val allowedIps: Set<String>) : KillSwitchEvent()
data object Stop : KillSwitchEvent()
data object DoNothing : KillSwitchEvent()
}
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.domain.state
package com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model
data class NetworkState(
val isWifiConnected: Boolean = false,
@@ -0,0 +1,161 @@
package com.zaneschepke.wireguardautotunnel.service.network
import android.content.Context
import android.net.ConnectivityManager
import android.net.ConnectivityManager.NetworkCallback.FLAG_INCLUDE_LOCATION_INFO
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.net.wifi.SupplicantState
import android.net.wifi.WifiManager
import android.os.Build
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.flowOn
import javax.inject.Inject
class InternetConnectivityService
@Inject
constructor(
@ApplicationContext private val context: Context,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : NetworkService {
private val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
@get:Synchronized @set:Synchronized
private var wifiCapabilities: NetworkCapabilities? = null
@get:Synchronized @set:Synchronized
private var wifiNetworkChanged: Boolean = false
override val didWifiChangeSinceLastCapabilitiesQuery: Boolean
get() = wifiNetworkChanged
override val status = callbackFlow {
var wifiState: Boolean = false
var ethernetState: Boolean = false
var cellularState: Boolean = false
fun emitState() {
trySend(NetworkStatus(wifiState, ethernetState, cellularState))
}
val currentNetwork = connectivityManager.activeNetwork
if (currentNetwork == null) {
emitState()
}
fun updateCapabilityState(up: Boolean, network: Network) {
with(connectivityManager.getNetworkCapabilities(network)) {
when {
this == null -> return
hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> wifiState = up
hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ->
cellularState = up
hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) ->
ethernetState = up
}
}
}
fun onWifiChange(network: Network, callback: () -> Unit) {
if (connectivityManager.getNetworkCapabilities(network)?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true) {
callback()
}
}
fun onAvailable(network: Network) {
onWifiChange(network) {
wifiNetworkChanged = true
}
}
fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
onWifiChange(network) {
wifiCapabilities = networkCapabilities
}
updateCapabilityState(true, network)
emitState()
}
val networkStatusCallback =
when (Build.VERSION.SDK_INT) {
in Build.VERSION_CODES.S..Int.MAX_VALUE -> {
object :
ConnectivityManager.NetworkCallback(
FLAG_INCLUDE_LOCATION_INFO,
) {
override fun onAvailable(network: Network) {
onAvailable(network)
}
override fun onLost(network: Network) {
updateCapabilityState(false, network)
emitState()
}
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
onCapabilitiesChanged(network, networkCapabilities)
}
}
}
else -> {
object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
onAvailable(network)
}
override fun onLost(network: Network) {
updateCapabilityState(false, network)
emitState()
}
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
onCapabilitiesChanged(network, networkCapabilities)
}
}
}
}
val request =
NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
.build()
connectivityManager.registerNetworkCallback(request, networkStatusCallback)
awaitClose { connectivityManager.unregisterNetworkCallback(networkStatusCallback) }
}.flowOn(ioDispatcher)
override fun getWifiCapabilities(): NetworkCapabilities? {
wifiNetworkChanged = false
return wifiCapabilities
}
companion object {
fun getNetworkName(networkCapabilities: NetworkCapabilities, context: Context): String? {
var ssid = networkCapabilities.getWifiName()
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S) {
val wifiManager =
context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
@Suppress("DEPRECATION")
val info = wifiManager.connectionInfo
if (info.supplicantState === SupplicantState.COMPLETED) {
ssid = info.ssid
}
}
return ssid?.trim('"')
}
}
}
@@ -0,0 +1,16 @@
package com.zaneschepke.wireguardautotunnel.service.network
import android.net.NetworkCapabilities
import android.net.wifi.WifiInfo
import android.os.Build
fun NetworkCapabilities.getWifiName(): String? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val info: WifiInfo
if (transportInfo is WifiInfo) {
info = transportInfo as WifiInfo
return info.ssid
}
}
return null
}
@@ -0,0 +1,12 @@
package com.zaneschepke.wireguardautotunnel.service.network
import android.net.NetworkCapabilities
import kotlinx.coroutines.flow.Flow
interface NetworkService {
val status: Flow<NetworkStatus>
// util to help limit location queries
val didWifiChangeSinceLastCapabilitiesQuery: Boolean
fun getWifiCapabilities(): NetworkCapabilities?
}
@@ -1,6 +1,6 @@
package com.zaneschepke.wireguardautotunnel.domain.state
package com.zaneschepke.wireguardautotunnel.service.network
data class ConnectivityState(
data class NetworkStatus(
val wifiAvailable: Boolean,
val ethernetAvailable: Boolean,
val cellularAvailable: Boolean,
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.domain.enums
package com.zaneschepke.wireguardautotunnel.service.notification
import android.content.Context
import com.zaneschepke.wireguardautotunnel.R
@@ -1,14 +1,12 @@
package com.zaneschepke.wireguardautotunnel.core.notification
package com.zaneschepke.wireguardautotunnel.service.notification
import android.app.Notification
import android.app.NotificationManager
import android.content.Context
import androidx.core.app.NotificationCompat
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification.NotificationChannels
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
import com.zaneschepke.wireguardautotunnel.util.StringValue
import com.zaneschepke.wireguardautotunnel.service.notification.WireGuardNotification.NotificationChannels
interface NotificationManager {
interface NotificationService {
val context: Context
fun createNotification(
channel: NotificationChannels,
@@ -21,26 +19,15 @@ interface NotificationManager {
onlyAlertOnce: Boolean = true,
): Notification
fun createNotification(
channel: NotificationChannels,
title: StringValue,
actions: Collection<NotificationCompat.Action> = emptyList(),
description: StringValue,
showTimestamp: Boolean = false,
importance: Int = NotificationManager.IMPORTANCE_HIGH,
onGoing: Boolean = true,
onlyAlertOnce: Boolean = true,
): Notification
fun createNotificationAction(notificationAction: NotificationAction, extraId: Int? = null): NotificationCompat.Action
fun createNotificationAction(notificationAction: NotificationAction): NotificationCompat.Action
fun remove(notificationId: Int)
fun show(notificationId: Int, notification: Notification)
companion object {
const val KERNEL_SERVICE_NOTIFICATION_ID = 123
const val AUTO_TUNNEL_NOTIFICATION_ID = 122
const val VPN_NOTIFICATION_ID = 100
const val EXTRA_ID = "id"
}
}
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.core.notification
package com.zaneschepke.wireguardautotunnel.service.notification
import android.Manifest
import android.app.Notification
@@ -12,12 +12,9 @@ import android.graphics.Color
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.zaneschepke.wireguardautotunnel.MainActivity
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.core.broadcast.NotificationActionReceiver
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager.Companion.EXTRA_ID
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
import com.zaneschepke.wireguardautotunnel.util.StringValue
import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver
import com.zaneschepke.wireguardautotunnel.ui.MainActivity
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
@@ -25,7 +22,7 @@ class WireGuardNotification
@Inject
constructor(
@ApplicationContext override val context: Context,
) : com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager {
) : NotificationService {
enum class NotificationChannels {
VPN,
@@ -67,35 +64,12 @@ constructor(
}.build()
}
override fun createNotification(
channel: NotificationChannels,
title: StringValue,
actions: Collection<NotificationCompat.Action>,
description: StringValue,
showTimestamp: Boolean,
importance: Int,
onGoing: Boolean,
onlyAlertOnce: Boolean,
): Notification {
return createNotification(
channel,
title.asString(context),
actions,
description.asString(context),
showTimestamp,
importance,
onGoing,
onlyAlertOnce,
)
}
override fun createNotificationAction(notificationAction: NotificationAction, extraId: Int?): NotificationCompat.Action {
override fun createNotificationAction(notificationAction: NotificationAction): NotificationCompat.Action {
val pendingIntent = PendingIntent.getBroadcast(
context,
0,
Intent(context, NotificationActionReceiver::class.java).apply {
action = notificationAction.name
if (extraId != null) putExtra(EXTRA_ID, extraId)
},
PendingIntent.FLAG_IMMUTABLE,
)
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.core.shortcut
package com.zaneschepke.wireguardautotunnel.service.shortcut
import android.content.Context
import android.content.Intent
@@ -6,7 +6,7 @@ import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.core.shortcut
package com.zaneschepke.wireguardautotunnel.service.shortcut
interface ShortcutManager {
suspend fun addShortcuts()
@@ -1,18 +1,18 @@
package com.zaneschepke.wireguardautotunnel.core.shortcut
package com.zaneschepke.wireguardautotunnel.service.shortcut
import android.os.Bundle
import androidx.activity.ComponentActivity
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelService
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.AutoTunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Provider
@AndroidEntryPoint
class ShortcutsActivity : ComponentActivity() {
@@ -20,10 +20,10 @@ class ShortcutsActivity : ComponentActivity() {
lateinit var appDataRepository: AppDataRepository
@Inject
lateinit var serviceManager: ServiceManager
lateinit var tunnelService: Provider<TunnelService>
@Inject
lateinit var tunnelManager: TunnelManager
lateinit var serviceManager: ServiceManager
@Inject
@ApplicationScope
@@ -32,28 +32,28 @@ class ShortcutsActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
applicationScope.launch {
val settings = appDataRepository.settings.get()
val settings = appDataRepository.settings.getSettings()
if (settings.isShortcutsEnabled) {
when (intent.getStringExtra(CLASS_NAME_EXTRA_KEY)) {
LEGACY_TUNNEL_SERVICE_NAME, TunnelProvider::class.java.simpleName -> {
LEGACY_TUNNEL_SERVICE_NAME, TunnelService::class.java.simpleName -> {
val tunnelName = intent.getStringExtra(TUNNEL_NAME_EXTRA_KEY)
Timber.d("Tunnel name extra: $tunnelName")
val tunnelConfig = tunnelName?.let {
appDataRepository.tunnels.getAll()
.firstOrNull { it.tunName == tunnelName }
.firstOrNull { it.name == tunnelName }
} ?: appDataRepository.getStartTunnelConfig()
Timber.d("Shortcut action on name: ${tunnelConfig?.tunName}")
Timber.d("Shortcut action on name: ${tunnelConfig?.name}")
tunnelConfig?.let {
when (intent.action) {
Action.START.name -> tunnelManager.startTunnel(it)
Action.STOP.name -> tunnelManager.stopTunnel()
Action.START.name -> tunnelService.get().startTunnel(it)
Action.STOP.name -> tunnelService.get().stopTunnel()
else -> Unit
}
}
}
AutoTunnelService::class.java.simpleName, LEGACY_AUTO_TUNNEL_SERVICE_NAME -> {
when (intent.action) {
Action.START.name -> serviceManager.startAutoTunnel()
Action.START.name -> serviceManager.startAutoTunnel(true)
Action.STOP.name -> serviceManager.stopAutoTunnel()
}
}
@@ -1,68 +1,65 @@
package com.zaneschepke.wireguardautotunnel.core.service.tile
package com.zaneschepke.wireguardautotunnel.service.tile
import android.content.Intent
import android.os.IBinder
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.lifecycleScope
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class AutoTunnelControlTile : TileService(), LifecycleOwner {
class AutoTunnelControlTile : TileService() {
@Inject
lateinit var appDataRepository: AppDataRepository
@Inject
lateinit var serviceManager: ServiceManager
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
override fun onCreate() {
super.onCreate()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
serviceManager.autoTunnelTile.complete(this)
}
override fun onDestroy() {
super.onDestroy()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
serviceManager.autoTunnelTile = CompletableDeferred()
}
override fun onStartListening() {
super.onStartListening()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
Timber.d("Start listening called for auto tunnel tile")
lifecycleScope.launch {
serviceManager.autoTunnelActive.collect {
if (it) return@collect setActive()
setInactive()
}
serviceManager.autoTunnelTile.complete(this)
applicationScope.launch {
if (appDataRepository.tunnels.getAll().isEmpty()) return@launch setUnavailable()
updateTileState()
}
lifecycleScope.launch {
appDataRepository.tunnels.flow.collect {
if (it.isEmpty()) {
setUnavailable()
}
}
}
fun updateTileState() {
serviceManager.autoTunnelActive.value.let {
if (it) setActive() else setInactive()
}
}
override fun onClick() {
super.onClick()
unlockAndRun {
lifecycleScope.launch {
applicationScope.launch {
if (serviceManager.autoTunnelActive.value) {
serviceManager.stopAutoTunnel()
setInactive()
} else {
serviceManager.startAutoTunnel()
serviceManager.startAutoTunnel(true)
setActive()
}
}
@@ -70,14 +67,14 @@ class AutoTunnelControlTile : TileService(), LifecycleOwner {
}
private fun setActive() {
runCatching {
kotlin.runCatching {
qsTile.state = Tile.STATE_ACTIVE
qsTile.updateTile()
}
}
private fun setInactive() {
runCatching {
kotlin.runCatching {
qsTile.state = Tile.STATE_INACTIVE
qsTile.updateTile()
}
@@ -95,12 +92,9 @@ class AutoTunnelControlTile : TileService(), LifecycleOwner {
}
private fun setUnavailable() {
runCatching {
kotlin.runCatching {
qsTile.state = Tile.STATE_UNAVAILABLE
qsTile.updateTile()
}
}
override val lifecycle: Lifecycle
get() = lifecycleRegistry
}
@@ -0,0 +1,129 @@
package com.zaneschepke.wireguardautotunnel.service.tile
import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class TunnelControlTile : TileService() {
@Inject
lateinit var appDataRepository: AppDataRepository
@Inject
lateinit var tunnelService: TunnelService
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
@Inject
lateinit var serviceManager: ServiceManager
override fun onCreate() {
super.onCreate()
serviceManager.tunnelControlTile.complete(this)
}
override fun onDestroy() {
super.onDestroy()
serviceManager.tunnelControlTile = CompletableDeferred()
}
override fun onStartListening() {
super.onStartListening()
Timber.d("Start listening called")
serviceManager.tunnelControlTile.complete(this)
applicationScope.launch {
updateTileState()
}
}
fun updateTileState() = applicationScope.launch {
if (appDataRepository.tunnels.getAll().isEmpty()) return@launch setUnavailable()
with(tunnelService.vpnState.value) {
if (status.isUp() && tunnelConfig != null) return@launch updateTile(tunnelConfig.name, true)
}
appDataRepository.getStartTunnelConfig()?.let {
updateTile(it.name, false)
}
}
override fun onClick() {
super.onClick()
unlockAndRun {
applicationScope.launch {
if (tunnelService.vpnState.value.status.isUp()) return@launch tunnelService.stopTunnel()
appDataRepository.getStartTunnelConfig()?.let {
tunnelService.startTunnel(it)
}
}
}
}
private fun setActive() {
kotlin.runCatching {
qsTile.state = Tile.STATE_ACTIVE
qsTile.updateTile()
}
}
private fun setInactive() {
kotlin.runCatching {
qsTile.state = Tile.STATE_INACTIVE
qsTile.updateTile()
}
}
private fun setUnavailable() {
kotlin.runCatching {
qsTile.state = Tile.STATE_UNAVAILABLE
setTileDescription("")
qsTile.updateTile()
}
}
private fun setTileDescription(description: String) {
kotlin.runCatching {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
qsTile.subtitle = description
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
qsTile.stateDescription = description
}
qsTile.updateTile()
}
}
/* This works around an annoying unsolved frameworks bug some people are hitting. */
override fun onBind(intent: Intent): IBinder? {
var ret: IBinder? = null
try {
ret = super.onBind(intent)
} catch (_: Throwable) {
Timber.e("Failed to bind to TunnelControlTile")
}
return ret
}
private fun updateTile(name: String, active: Boolean) {
kotlin.runCatching {
setTileDescription(name)
if (active) return setActive()
setInactive()
}.onFailure {
Timber.e(it)
}
}
}
@@ -0,0 +1,46 @@
package com.zaneschepke.wireguardautotunnel.service.tunnel
import android.content.Intent
import android.os.IBinder
import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Provider
@AndroidEntryPoint
class AlwaysOnVpnService : LifecycleService() {
@Inject
lateinit var tunnelService: Provider<TunnelService>
@Inject
lateinit var appDataRepository: AppDataRepository
override fun onBind(intent: Intent): IBinder? {
super.onBind(intent)
// We don't provide binding, so return null
return null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent == null || intent.component == null || intent.component!!.packageName != packageName) {
Timber.i("Always-on VPN requested started")
lifecycleScope.launch {
val settings = appDataRepository.settings.getSettings()
if (settings.isAlwaysOnVpnEnabled) {
val tunnel = appDataRepository.getPrimaryOrFirstTunnel()
tunnel?.let {
tunnelService.get().startTunnel(it)
}
} else {
Timber.w("Always-on VPN is not enabled in app settings")
}
}
}
return super.onStartCommand(intent, flags, startId)
}
}
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.domain.enums
package com.zaneschepke.wireguardautotunnel.service.tunnel
enum class BackendState {
KILL_SWITCH_ACTIVE,
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.domain.enums
package com.zaneschepke.wireguardautotunnel.service.tunnel
enum class HandshakeStatus {
HEALTHY,
@@ -0,0 +1,28 @@
package com.zaneschepke.wireguardautotunnel.service.tunnel
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import kotlinx.coroutines.flow.StateFlow
interface TunnelService : Tunnel, org.amnezia.awg.backend.Tunnel {
suspend fun startTunnel(tunnelConfig: TunnelConfig?)
suspend fun stopTunnel()
suspend fun bounceTunnel()
suspend fun getBackendState(): BackendState
suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>)
val vpnState: StateFlow<VpnState>
suspend fun runningTunnelNames(): Set<String>
suspend fun getState(): TunnelState
fun cancelActiveTunnelJobs()
fun startActiveTunnelJobs()
}
@@ -0,0 +1,52 @@
package com.zaneschepke.wireguardautotunnel.service.tunnel
import com.wireguard.android.backend.Tunnel
enum class TunnelState {
UP,
DOWN,
TOGGLE,
;
fun toWgState(): Tunnel.State {
return when (this) {
UP -> Tunnel.State.UP
DOWN -> Tunnel.State.DOWN
TOGGLE -> Tunnel.State.TOGGLE
}
}
fun toAmState(): org.amnezia.awg.backend.Tunnel.State {
return when (this) {
UP -> org.amnezia.awg.backend.Tunnel.State.UP
DOWN -> org.amnezia.awg.backend.Tunnel.State.DOWN
TOGGLE -> org.amnezia.awg.backend.Tunnel.State.TOGGLE
}
}
fun isDown(): Boolean {
return this == DOWN
}
fun isUp(): Boolean {
return this == UP
}
companion object {
fun from(state: Tunnel.State): TunnelState {
return when (state) {
Tunnel.State.DOWN -> DOWN
Tunnel.State.TOGGLE -> TOGGLE
Tunnel.State.UP -> UP
}
}
fun from(state: org.amnezia.awg.backend.Tunnel.State): TunnelState {
return when (state) {
org.amnezia.awg.backend.Tunnel.State.DOWN -> DOWN
org.amnezia.awg.backend.Tunnel.State.TOGGLE -> TOGGLE
org.amnezia.awg.backend.Tunnel.State.UP -> UP
}
}
}
}
@@ -0,0 +1,11 @@
package com.zaneschepke.wireguardautotunnel.service.tunnel
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
data class VpnState(
val status: TunnelState = TunnelState.DOWN,
val backendState: BackendState = BackendState.INACTIVE,
val tunnelConfig: TunnelConfig? = null,
val statistics: TunnelStatistics? = null,
)
@@ -0,0 +1,450 @@
package com.zaneschepke.wireguardautotunnel.service.tunnel
import com.wireguard.android.backend.Backend
import com.wireguard.android.backend.Tunnel.State
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.module.Kernel
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService.Companion.VPN_NOTIFICATION_ID
import com.zaneschepke.wireguardautotunnel.service.notification.WireGuardNotification
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.AmneziaStatistics
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.WireGuardStatistics
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.StringValue
import com.zaneschepke.wireguardautotunnel.util.extensions.asAmBackendState
import com.zaneschepke.wireguardautotunnel.util.extensions.asBackendState
import com.zaneschepke.wireguardautotunnel.util.extensions.cancelWithMessage
import com.zaneschepke.wireguardautotunnel.util.extensions.isReachable
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.amnezia.awg.backend.Tunnel
import timber.log.Timber
import java.net.InetAddress
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
import javax.inject.Provider
class WireGuardTunnel
@Inject
constructor(
private val amneziaBackend: Provider<org.amnezia.awg.backend.Backend>,
private val tunnelConfigRepository: TunnelConfigRepository,
@Kernel private val kernelBackend: Provider<Backend>,
private val appDataRepository: AppDataRepository,
@ApplicationScope private val applicationScope: CoroutineScope,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
private val serviceManager: ServiceManager,
private val notificationService: NotificationService,
private val internetConnectivityService: NetworkService,
) : TunnelService {
private val _vpnState = MutableStateFlow(VpnState())
override val vpnState: StateFlow<VpnState> = _vpnState.asStateFlow()
private var statsJob: Job? = null
private var tunnelChangesJob: Job? = null
private var pingJob: Job? = null
private var networkJob: Job? = null
@get:Synchronized @set:Synchronized
private var isKernelBackend: Boolean? = null
private val isNetworkAvailable = AtomicBoolean(false)
private val tunnelControlMutex = Mutex()
init {
applicationScope.launch(ioDispatcher) {
appDataRepository.settings.getSettingsFlow().collect {
isKernelBackend = it.isKernelEnabled
}
}
}
private suspend fun backend(): Any {
val isKernelEnabled = isKernelBackend
?: appDataRepository.settings.getSettings().isKernelEnabled
if (isKernelEnabled) return kernelBackend.get()
return amneziaBackend.get()
}
override suspend fun runningTunnelNames(): Set<String> {
return when (val backend = backend()) {
is Backend -> backend.runningTunnelNames
is org.amnezia.awg.backend.Backend -> backend.runningTunnelNames
else -> emptySet()
}
}
private suspend fun setState(tunnelConfig: TunnelConfig, tunnelState: TunnelState): Result<TunnelState> {
return runCatching {
when (val backend = backend()) {
is Backend -> backend.setState(this, tunnelState.toWgState(), tunnelConfig.toWgConfig()).let { TunnelState.from(it) }
is org.amnezia.awg.backend.Backend -> {
backend.setState(this, tunnelState.toAmState(), tunnelConfig.toAmConfig()).let {
TunnelState.from(it)
}
}
else -> throw NotImplementedError()
}
}.onFailure {
// TODO add better error message to user, especially for kernel as exceptions contain no details
Timber.e(it)
}
}
private fun isTunnelAlreadyRunning(tunnelConfig: TunnelConfig): Boolean {
return with(_vpnState.value) {
this.tunnelConfig?.id == tunnelConfig.id && status.isUp().also {
if (it) Timber.w("Tunnel already running")
}
}
}
override suspend fun startTunnel(tunnelConfig: TunnelConfig?) {
withContext(ioDispatcher) {
if (tunnelConfig == null || isTunnelAlreadyRunning(tunnelConfig)) return@withContext
onBeforeStart(tunnelConfig)
updateTunnelConfig(tunnelConfig) // need to update this here
appDataRepository.tunnels.save(tunnelConfig.copy(isActive = true))
withServiceActive {
setState(tunnelConfig, TunnelState.UP).onSuccess {
updateTunnelState(it, tunnelConfig)
startActiveTunnelJobs()
}.onFailure {
onTunnelStop(tunnelConfig)
// TODO improve this with better statuses and handling
showTunnelStartFailed()
}
}
}
}
private fun showTunnelStartFailed() {
if (WireGuardAutoTunnel.isForeground()) {
SnackbarController.showMessage(StringValue.StringResource(R.string.error_tunnel_start))
} else {
launchStartFailedNotification()
}
}
private fun launchStartFailedNotification() {
with(notificationService) {
val notification = createNotification(
WireGuardNotification.NotificationChannels.VPN,
title = context.getString(R.string.error_tunnel_start),
)
show(VPN_NOTIFICATION_ID, notification)
}
}
override suspend fun stopTunnel() {
withContext(ioDispatcher) {
if (_vpnState.value.status.isDown()) return@withContext
with(_vpnState.value) {
if (tunnelConfig == null) return@withContext
setState(tunnelConfig, TunnelState.DOWN).onSuccess {
onTunnelStop(tunnelConfig)
updateTunnelState(it, null)
}.onFailure {
clearJobsAndStats()
Timber.e(it)
}
}
}
}
private suspend fun toggleTunnel(tunnelConfig: TunnelConfig) {
withContext(ioDispatcher) {
tunnelControlMutex.withLock {
setState(tunnelConfig, TunnelState.TOGGLE)
}
}
}
// utility to keep vpnService alive during rapid changes to prevent bad states
private suspend fun withServiceActive(callback: suspend () -> Unit) {
when (val backend = backend()) {
is org.amnezia.awg.backend.Backend -> {
val backendState = backend.backendState
if (backendState == org.amnezia.awg.backend.Backend.BackendState.INACTIVE) {
backend.setBackendState(org.amnezia.awg.backend.Backend.BackendState.SERVICE_ACTIVE, emptyList())
}
callback()
}
is Backend -> callback()
}
}
override suspend fun bounceTunnel() {
with(_vpnState.value) {
if (tunnelConfig != null && status.isUp()) {
withServiceActive {
toggleTunnel(tunnelConfig)
toggleTunnel(tunnelConfig)
}
}
}
}
override suspend fun getBackendState(): BackendState {
return when (val backend = backend()) {
is org.amnezia.awg.backend.Backend -> {
backend.backendState.asBackendState()
}
is Backend -> BackendState.SERVICE_ACTIVE
else -> BackendState.INACTIVE
}
}
override suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
kotlin.runCatching {
when (val backend = backend()) {
is org.amnezia.awg.backend.Backend -> {
backend.setBackendState(backendState.asAmBackendState(), allowedIps)
_vpnState.update {
it.copy(backendState = backendState)
}
}
is Backend -> {
// TODO not yet implemented
Timber.d("Kernel backend state not yet implemented")
}
else -> Unit
}
}
}
private suspend fun onBeforeStart(tunnelConfig: TunnelConfig) {
with(_vpnState.value) {
if (status.isUp()) stopTunnel() else clearJobsAndStats()
serviceManager.startBackgroundService(tunnelConfig)
}
}
private suspend fun onTunnelStop(tunnelConfig: TunnelConfig) {
runCatching {
appDataRepository.tunnels.save(tunnelConfig.copy(isActive = false))
serviceManager.stopBackgroundService()
notificationService.remove(VPN_NOTIFICATION_ID)
clearJobsAndStats()
}
}
private fun clearJobsAndStats() {
cancelActiveTunnelJobs()
resetBackendStatistics()
}
private fun updateTunnelState(state: TunnelState, tunnelConfig: TunnelConfig?) {
_vpnState.update {
it.copy(status = state, tunnelConfig = tunnelConfig)
}
}
private fun updateTunnelConfig(tunnelConfig: TunnelConfig?) {
_vpnState.update {
it.copy(tunnelConfig = tunnelConfig)
}
}
private fun updateBackendStatistics(statistics: TunnelStatistics) {
_vpnState.update {
it.copy(statistics = statistics)
}
}
private fun resetBackendStatistics() {
_vpnState.update {
it.copy(statistics = null)
}
}
override suspend fun getState(): TunnelState {
return when (val backend = backend()) {
is Backend -> backend.getState(this).let { TunnelState.from(it) }
is org.amnezia.awg.backend.Backend -> backend.getState(this).let { TunnelState.from(it) }
else -> TunnelState.DOWN
}
}
override fun cancelActiveTunnelJobs() {
statsJob?.cancelWithMessage("Tunnel stats job cancelled")
tunnelChangesJob?.cancelWithMessage("Tunnel changes job cancelled")
cancelPingJobs()
}
override fun startActiveTunnelJobs() {
statsJob = startTunnelStatisticsJob()
tunnelChangesJob = startTunnelConfigChangesJob()
if (_vpnState.value.tunnelConfig?.isPingEnabled == true) {
startPingJobs()
}
}
private fun startPingJobs() {
cancelPingJobs()
pingJob = startPingJob()
networkJob = startNetworkJob()
}
override fun getName(): String {
return _vpnState.value.tunnelConfig?.name ?: ""
}
private fun startTunnelStatisticsJob() = applicationScope.launch(ioDispatcher) {
val backend = backend()
delay(STATS_START_DELAY)
while (true) {
when (backend) {
is Backend -> updateBackendStatistics(
WireGuardStatistics(backend.getStatistics(this@WireGuardTunnel)),
)
is org.amnezia.awg.backend.Backend -> updateBackendStatistics(
AmneziaStatistics(
backend.getStatistics(this@WireGuardTunnel),
),
)
}
delay(VPN_STATISTIC_CHECK_INTERVAL)
}
}
private fun isQuickConfigChanged(config: TunnelConfig): Boolean {
return with(_vpnState.value) {
if (tunnelConfig == null) return false
config.wgQuick != tunnelConfig.wgQuick ||
config.amQuick != tunnelConfig.amQuick
}
}
private fun isPingConfigMatching(config: TunnelConfig): Boolean {
return with(_vpnState.value.tunnelConfig) {
if (this == null) return true
config.isPingEnabled == isPingEnabled &&
pingIp == config.pingIp &&
config.pingCooldown == pingCooldown &&
config.pingInterval == pingInterval
}
}
private fun handlePingConfigChanges() {
with(_vpnState.value.tunnelConfig) {
if (this == null) return
if (!isPingEnabled && pingJob?.isActive == true) {
cancelPingJobs()
return
}
restartPingJob()
}
}
private fun restartPingJob() {
cancelPingJobs()
startPingJobs()
}
private fun cancelPingJobs() {
pingJob?.cancelWithMessage("Ping job cancelled")
networkJob?.cancelWithMessage("Network job cancelled")
}
private fun startTunnelConfigChangesJob() = applicationScope.launch(ioDispatcher) {
tunnelConfigRepository.getTunnelConfigsFlow().collect { tunnels ->
with(_vpnState.value) {
if (tunnelConfig == null) return@collect
val storageConfig = tunnels.firstOrNull { it.id == tunnelConfig.id }
if (storageConfig == null) return@collect
val quickChanged = isQuickConfigChanged(storageConfig)
val pingMatching = isPingConfigMatching(storageConfig)
updateTunnelConfig(storageConfig)
if (quickChanged) bounceTunnel()
if (!pingMatching) handlePingConfigChanges()
}
}
}
private suspend fun pingTunnel(tunnelConfig: TunnelConfig): List<Boolean> {
return withContext(ioDispatcher) {
val config = tunnelConfig.toWgConfig()
if (tunnelConfig.pingIp != null) {
Timber.i("Pinging custom ip")
listOf(InetAddress.getByName(tunnelConfig.pingIp).isReachable(Constants.PING_TIMEOUT.toInt()))
} else {
Timber.i("Pinging all peers")
config.peers.map { peer ->
peer.isReachable()
}
}
}
}
private fun startPingJob() = applicationScope.launch(ioDispatcher) {
do {
run {
with(_vpnState.value) {
if (status.isUp() && tunnelConfig != null && isNetworkAvailable.get()) {
val reachable = pingTunnel(tunnelConfig)
if (reachable.contains(false)) {
if (isNetworkAvailable.get()) {
Timber.i("Ping result: target was not reachable, bouncing the tunnel")
bounceTunnel()
delay(tunnelConfig.pingCooldown ?: Constants.PING_COOLDOWN)
} else {
Timber.i("Ping result: target was not reachable, but not network available")
}
return@run
} else {
Timber.i("Ping result: all ping targets were reached successfully")
}
}
delay(tunnelConfig?.pingInterval ?: Constants.PING_INTERVAL)
}
}
} while (true)
}
private fun startNetworkJob() = applicationScope.launch(ioDispatcher) {
internetConnectivityService.status.distinctUntilChanged().collect {
isNetworkAvailable.set(!it.allOffline)
}
}
override fun onStateChange(newState: Tunnel.State) {
_vpnState.update {
it.copy(status = TunnelState.from(newState))
}
serviceManager.updateTunnelTile()
}
override fun onStateChange(state: State) {
_vpnState.update {
it.copy(status = TunnelState.from(state))
}
serviceManager.updateTunnelTile()
}
companion object {
const val STATS_START_DELAY = 1_000L
const val VPN_STATISTIC_CHECK_INTERVAL = 1_000L
}
}
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.domain.state
package com.zaneschepke.wireguardautotunnel.service.tunnel.statistics
import org.amnezia.awg.backend.Statistics
import org.amnezia.awg.crypto.Key
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.domain.state
package com.zaneschepke.wireguardautotunnel.service.tunnel.statistics
import org.amnezia.awg.crypto.Key
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.domain.state
package com.zaneschepke.wireguardautotunnel.service.tunnel.statistics
import com.wireguard.android.backend.Statistics
import org.amnezia.awg.crypto.Key
@@ -0,0 +1,14 @@
package com.zaneschepke.wireguardautotunnel.ui
import com.zaneschepke.wireguardautotunnel.data.domain.GeneralState
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
data class AppUiState(
val settings: Settings = Settings(),
val tunnels: List<TunnelConfig> = emptyList(),
val vpnState: VpnState = VpnState(),
val generalState: GeneralState = GeneralState(),
val autoTunnelActive: Boolean = false,
)
@@ -0,0 +1,420 @@
package com.zaneschepke.wireguardautotunnel.ui
import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wireguard.android.backend.WgQuickBackend
import com.wireguard.android.util.RootShell
import com.wireguard.config.Config
import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.AppShell
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.BackendState
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.ui.screens.tunneloptions.config.model.InterfaceProxy
import com.zaneschepke.wireguardautotunnel.ui.screens.tunneloptions.config.model.PeerProxy
import com.zaneschepke.wireguardautotunnel.ui.screens.tunneloptions.splittunnel.SplitTunnelApp
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
import com.zaneschepke.wireguardautotunnel.util.StringValue
import com.zaneschepke.wireguardautotunnel.util.extensions.getAllInternetCapablePackages
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import kotlinx.coroutines.withContext
import timber.log.Timber
import xyz.teamgravity.pin_lock_compose.PinManager
import javax.inject.Inject
import javax.inject.Provider
@HiltViewModel
class AppViewModel
@Inject
constructor(
private val appDataRepository: AppDataRepository,
private val tunnelService: Provider<TunnelService>,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
@AppShell private val rootShell: Provider<RootShell>,
private val serviceManager: ServiceManager,
private val logReader: LogReader,
) : ViewModel() {
private val _popBackStack = MutableSharedFlow<Boolean>()
val popBackStack = _popBackStack.asSharedFlow()
private val _isAppReady = MutableStateFlow(false)
val isAppReady = _isAppReady.asStateFlow()
private val _configurationChange = MutableStateFlow(false)
val configurationChange = _configurationChange.asStateFlow()
private val _splitTunnelApps = MutableStateFlow<List<SplitTunnelApp>>(emptyList())
val splitTunnelApps = _splitTunnelApps.asStateFlow()
val uiState =
combine(
appDataRepository.settings.getSettingsFlow(),
appDataRepository.tunnels.getTunnelConfigsFlow(),
tunnelService.get().vpnState,
appDataRepository.appState.generalStateFlow,
serviceManager.autoTunnelActive,
) { settings, tunnels, tunnelState, generalState, autoTunnel ->
AppUiState(
settings,
tunnels,
tunnelState,
generalState,
autoTunnel,
)
}.stateIn(
viewModelScope + ioDispatcher,
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
AppUiState(),
)
init {
viewModelScope.launch {
initPin()
initServices()
launch {
initTunnel()
}
appReadyCheck()
}
}
private suspend fun appReadyCheck() {
val tunnelCount = appDataRepository.tunnels.count()
uiState.takeWhile { it.tunnels.size != tunnelCount }.onCompletion {
_isAppReady.emit(true)
}.collect()
}
private suspend fun initTunnel() {
withContext(ioDispatcher) {
if (tunnelService.get().getState() == TunnelState.UP) tunnelService.get().startActiveTunnelJobs()
val activeTunnels = appDataRepository.tunnels.getActive()
if (activeTunnels.isNotEmpty() &&
tunnelService.get().getState() == TunnelState.DOWN
) {
tunnelService.get().startTunnel(activeTunnels.first())
}
}
}
private suspend fun initPin() {
val isPinEnabled = appDataRepository.appState.isPinLockEnabled()
if (isPinEnabled) PinManager.initialize(WireGuardAutoTunnel.instance)
}
private suspend fun initServices() {
withContext(ioDispatcher) {
val settings = appDataRepository.settings.getSettings()
handleVpnKillSwitchChange(settings.isVpnKillSwitchEnabled)
if (settings.isAutoTunnelEnabled) serviceManager.startAutoTunnel(false)
}
}
fun saveSettings(settings: Settings) = viewModelScope.launch {
appDataRepository.settings.save(settings)
}
fun onPinLockDisabled() = viewModelScope.launch(ioDispatcher) {
PinManager.clearPin()
appDataRepository.appState.setPinLockEnabled(false)
}
fun onPinLockEnabled() = viewModelScope.launch {
appDataRepository.appState.setPinLockEnabled(true)
}
fun setLocationDisclosureShown() = viewModelScope.launch {
appDataRepository.appState.setLocationDisclosureShown(true)
}
fun onToggleLocalLogging() = viewModelScope.launch(ioDispatcher) {
with(uiState.value.generalState) {
val toggledOn = !isLocalLogsEnabled
appDataRepository.appState.setLocalLogsEnabled(toggledOn)
if (!toggledOn) onLoggerStop()
_configurationChange.update {
true
}
}
}
private suspend fun onLoggerStop() {
logReader.deleteAndClearLogs()
}
fun onToggleAlwaysOnVPN() = viewModelScope.launch {
with(uiState.value.settings) {
appDataRepository.settings.save(
copy(
isAlwaysOnVpnEnabled = !isAlwaysOnVpnEnabled,
),
)
}
}
fun onLocaleChange(localeTag: String) = viewModelScope.launch {
appDataRepository.appState.setLocale(localeTag)
LocaleUtil.changeLocale(localeTag)
_configurationChange.update {
true
}
}
fun onToggleRestartAtBoot() = viewModelScope.launch {
with(uiState.value.settings) {
appDataRepository.settings.save(
copy(
isRestoreOnBootEnabled = !isRestoreOnBootEnabled,
),
)
}
}
fun onToggleVpnKillSwitch(enabled: Boolean) = viewModelScope.launch {
with(uiState.value.settings) {
appDataRepository.settings.save(
copy(
isVpnKillSwitchEnabled = enabled,
isLanOnKillSwitchEnabled = if (enabled) isLanOnKillSwitchEnabled else false,
),
)
}
handleVpnKillSwitchChange(enabled)
}
private suspend fun handleVpnKillSwitchChange(enabled: Boolean) {
withContext(ioDispatcher) {
if (!enabled) return@withContext tunnelService.get().setBackendState(BackendState.SERVICE_ACTIVE, emptySet())
Timber.d("Starting kill switch")
val allowedIps = if (appDataRepository.settings.getSettings().isLanOnKillSwitchEnabled) {
TunnelConfig.LAN_BYPASS_ALLOWED_IPS
} else {
emptySet()
}
tunnelService.get().setBackendState(BackendState.KILL_SWITCH_ACTIVE, allowedIps)
}
}
fun onToggleLanOnKillSwitch(enabled: Boolean) = viewModelScope.launch(ioDispatcher) {
appDataRepository.settings.save(
uiState.value.settings.copy(
isLanOnKillSwitchEnabled = enabled,
),
)
val allowedIps = if (enabled) TunnelConfig.LAN_BYPASS_ALLOWED_IPS else emptySet()
Timber.d("Setting allowedIps $allowedIps")
tunnelService.get().setBackendState(BackendState.KILL_SWITCH_ACTIVE, allowedIps)
}
fun onToggleShortcutsEnabled() = viewModelScope.launch {
with(uiState.value.settings) {
appDataRepository.settings.save(
copy(
isShortcutsEnabled = !isShortcutsEnabled,
),
)
}
}
private fun saveKernelMode(enabled: Boolean) = viewModelScope.launch {
with(uiState.value.settings) {
appDataRepository.settings.save(
this.copy(
isKernelEnabled = enabled,
),
)
}
}
fun onToggleKernelMode() = viewModelScope.launch {
with(uiState.value.settings) {
if (!isKernelEnabled) {
requestRoot().onSuccess {
if (!isKernelSupported()) return@onSuccess SnackbarController.showMessage(StringValue.StringResource(R.string.kernel_not_supported))
tunnelService.get().setBackendState(BackendState.INACTIVE, emptyList())
appDataRepository.settings.save(
copy(
isKernelEnabled = true,
isAmneziaEnabled = false,
),
)
}
} else {
saveKernelMode(enabled = false)
}
}
}
private suspend fun isKernelSupported(): Boolean {
return withContext(ioDispatcher) {
WgQuickBackend.hasKernelSupport()
}
}
suspend fun getEmitSplitTunnelApps(context: Context) {
withContext(ioDispatcher) {
val apps = context.getAllInternetCapablePackages().filter { it.applicationInfo != null }.map { pack ->
SplitTunnelApp(
context.packageManager.getApplicationIcon(pack.applicationInfo!!),
context.packageManager.getApplicationLabel(pack.applicationInfo!!).toString(),
pack.packageName,
)
}
_splitTunnelApps.emit(apps)
}
}
suspend fun requestRoot(): Result<Unit> {
return withContext(ioDispatcher) {
kotlin.runCatching {
rootShell.get().start()
SnackbarController.showMessage(StringValue.StringResource(R.string.root_accepted))
}.onFailure {
SnackbarController.showMessage(StringValue.StringResource(R.string.error_root_denied))
}
}
}
fun updateExistingTunnelConfig(
tunnelConfig: TunnelConfig,
tunnelName: String? = null,
peers: List<PeerProxy>? = null,
`interface`: InterfaceProxy? = null,
) = viewModelScope.launch {
runCatching {
val amConfig = tunnelConfig.toAmConfig()
val wgConfig = tunnelConfig.toWgConfig()
updateTunnelConfig(tunnelConfig, tunnelName, amConfig, wgConfig, peers, `interface`)
_popBackStack.emit(true)
SnackbarController.showMessage(StringValue.StringResource(R.string.config_changes_saved))
}.onFailure {
onConfigSaveError(it)
}
}
fun saveNewTunnel(tunnelName: String, peers: List<PeerProxy>, `interface`: InterfaceProxy) = viewModelScope.launch {
runCatching {
val config = buildConfigs(peers, `interface`)
appDataRepository.tunnels.save(
TunnelConfig(
name = tunnelName,
wgQuick = config.first.toWgQuickString(true),
amQuick = config.second.toAwgQuickString(true),
),
)
_popBackStack.emit(true)
SnackbarController.showMessage(StringValue.StringResource(R.string.config_changes_saved))
}.onFailure {
onConfigSaveError(it)
}
}
private fun onConfigSaveError(throwable: Throwable) {
Timber.e(throwable)
SnackbarController.showMessage(
throwable.message?.let { message ->
(StringValue.DynamicString(message))
} ?: StringValue.StringResource(R.string.unknown_error),
)
}
private suspend fun updateTunnelConfig(
tunnelConfig: TunnelConfig,
tunnelName: String? = null,
amConfig: org.amnezia.awg.config.Config,
wgConfig: Config,
peers: List<PeerProxy>? = null,
`interface`: InterfaceProxy? = null,
) {
val configs = rebuildConfigs(amConfig, wgConfig, peers, `interface`)
appDataRepository.tunnels.save(
tunnelConfig.copy(
name = tunnelName ?: tunnelConfig.name,
amQuick = configs.second.toAwgQuickString(true),
wgQuick = configs.first.toWgQuickString(true),
),
)
}
fun cleanUpUninstalledApps(tunnelConfig: TunnelConfig, packages: List<String>) = viewModelScope.launch(ioDispatcher) {
runCatching {
val amConfig = tunnelConfig.toAmConfig()
val wgConfig = tunnelConfig.toWgConfig()
val proxy = InterfaceProxy.from(amConfig.`interface`)
if (proxy.includedApplications.isEmpty() && proxy.excludedApplications.isEmpty()) return@launch
if (proxy.includedApplications.retainAll(packages.toSet()) || proxy.excludedApplications.retainAll(packages.toSet())) {
updateTunnelConfig(tunnelConfig, amConfig = amConfig, wgConfig = wgConfig, `interface` = proxy)
Timber.i("Removed split tunnel package for app that no longer exists on the device")
}
}.onFailure {
Timber.e(it)
}
}
fun bounceAutoTunnel() = viewModelScope.launch(ioDispatcher) {
serviceManager.stopAutoTunnel()
delay(1000L)
serviceManager.startAutoTunnel(true)
}
private suspend fun rebuildConfigs(
amConfig: org.amnezia.awg.config.Config,
wgConfig: Config,
peers: List<PeerProxy>? = null,
`interface`: InterfaceProxy? = null,
): Pair<Config, org.amnezia.awg.config.Config> {
return withContext(ioDispatcher) {
Pair(
Config.Builder().apply {
addPeers(peers?.map { it.toWgPeer() } ?: wgConfig.peers)
setInterface(`interface`?.toWgInterface() ?: wgConfig.`interface`)
}.build(),
org.amnezia.awg.config.Config.Builder().apply {
addPeers(peers?.map { it.toAmPeer() } ?: amConfig.peers)
setInterface(`interface`?.toAmInterface() ?: amConfig.`interface`)
}.build(),
)
}
}
private suspend fun buildConfigs(peers: List<PeerProxy>, `interface`: InterfaceProxy): Pair<Config, org.amnezia.awg.config.Config> {
return withContext(ioDispatcher) {
Pair(
Config.Builder().apply {
addPeers(peers.map { it.toWgPeer() })
setInterface(`interface`.toWgInterface())
}.build(),
org.amnezia.awg.config.Config.Builder().apply {
addPeers(peers.map { it.toAmPeer() })
setInterface(`interface`.toAmInterface())
}.build(),
)
}
}
}
@@ -0,0 +1,264 @@
package com.zaneschepke.wireguardautotunnel.ui
import android.content.Intent
import android.graphics.Color.TRANSPARENT
import android.os.Build
import android.os.Bundle
import androidx.activity.SystemBarStyle
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Home
import androidx.compose.material.icons.rounded.QuestionMark
import androidx.compose.material.icons.rounded.Settings
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarData
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.toRoute
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.service.shortcut.ShortcutManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.CustomSnackBar
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarControllerProvider
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.pinlock.PinLockScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.scanner.ScannerScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.AppearanceScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.display.DisplayScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.language.LanguageScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.AutoTunnelScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.advanced.AdvancedScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.disclosure.LocationDisclosureScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.killswitch.KillSwitchScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.logs.LogsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunneloptions.OptionsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunneloptions.config.ConfigScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunneloptions.splittunnel.SplitTunnelScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunneloptions.tunnelautotunnel.TunnelAutoTunnelScreen
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlin.system.exitProcess
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var appStateRepository: AppStateRepository
@Inject
lateinit var tunnelService: TunnelService
@Inject
lateinit var shortcutManager: ShortcutManager
@OptIn(ExperimentalLayoutApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge(
statusBarStyle = SystemBarStyle.auto(TRANSPARENT, TRANSPARENT),
navigationBarStyle = SystemBarStyle.auto(TRANSPARENT, TRANSPARENT),
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
window.isNavigationBarContrastEnforced = false
}
super.onCreate(savedInstanceState)
val viewModel by viewModels<AppViewModel>()
installSplashScreen().apply {
setKeepOnScreenCondition {
!viewModel.isAppReady.value
}
}
setContent {
val appUiState by viewModel.uiState.collectAsStateWithLifecycle()
val configurationChange by viewModel.configurationChange.collectAsStateWithLifecycle()
val navController = rememberNavController()
LaunchedEffect(configurationChange) {
if (configurationChange) {
Intent(this@MainActivity, MainActivity::class.java).also {
startActivity(it)
exitProcess(0)
}
}
}
LaunchedEffect(Unit) {
viewModel.getEmitSplitTunnelApps(this@MainActivity)
}
LaunchedEffect(appUiState.autoTunnelActive) {
requestAutoTunnelTileServiceUpdate()
}
with(appUiState.settings) {
LaunchedEffect(isAutoTunnelEnabled) {
this@MainActivity.requestAutoTunnelTileServiceUpdate()
}
LaunchedEffect(isShortcutsEnabled) {
if (!isShortcutsEnabled) return@LaunchedEffect shortcutManager.removeShortcuts()
shortcutManager.addShortcuts()
}
}
CompositionLocalProvider(LocalNavController provides navController) {
SnackbarControllerProvider { host ->
WireguardAutoTunnelTheme(theme = appUiState.generalState.theme) {
Scaffold(
contentWindowInsets = WindowInsets(0),
snackbarHost = {
SnackbarHost(host) { snackbarData: SnackbarData ->
CustomSnackBar(
snackbarData.visuals.message,
isRtl = false,
containerColor =
MaterialTheme.colorScheme.surfaceColorAtElevation(
2.dp,
),
)
}
},
bottomBar = {
BottomNavBar(
navController,
listOf(
BottomNavItem(
name = stringResource(R.string.tunnels),
route = Route.Main,
icon = Icons.Rounded.Home,
),
BottomNavItem(
name = stringResource(R.string.settings),
route = Route.Settings,
icon = Icons.Rounded.Settings,
),
BottomNavItem(
name = stringResource(R.string.support),
route = Route.Support,
icon = Icons.Rounded.QuestionMark,
),
),
)
},
) { padding ->
Box(modifier = Modifier.fillMaxSize().padding(padding)) {
NavHost(
navController,
enterTransition = { fadeIn(tween(Constants.TRANSITION_ANIMATION_TIME)) },
exitTransition = { fadeOut(tween(Constants.TRANSITION_ANIMATION_TIME)) },
startDestination = (if (appUiState.generalState.isPinLockEnabled) Route.Lock else Route.Main),
) {
composable<Route.Main> {
MainScreen(
uiState = appUiState,
)
}
composable<Route.Settings> {
SettingsScreen(
appViewModel = viewModel,
uiState = appUiState,
)
}
composable<Route.LocationDisclosure> {
LocationDisclosureScreen(viewModel, appUiState)
}
composable<Route.AutoTunnel> {
AutoTunnelScreen(
appUiState.settings,
)
}
composable<Route.Appearance> {
AppearanceScreen()
}
composable<Route.Language> {
LanguageScreen(appUiState, viewModel)
}
composable<Route.Display> {
DisplayScreen(appUiState)
}
composable<Route.Support> {
SupportScreen(appUiState, viewModel)
}
composable<Route.AutoTunnelAdvanced> {
AdvancedScreen(appUiState.settings, viewModel)
}
composable<Route.Logs> {
LogsScreen()
}
composable<Route.Config> {
val args = it.toRoute<Route.Config>()
val config = appUiState.tunnels.firstOrNull { it.id == args.id }
ConfigScreen(config, viewModel)
}
composable<Route.TunnelOptions> {
val args = it.toRoute<Route.TunnelOptions>()
val config = appUiState.tunnels.first { it.id == args.id }
OptionsScreen(config)
}
composable<Route.Lock> {
PinLockScreen(viewModel)
}
composable<Route.Scanner> {
ScannerScreen()
}
composable<Route.KillSwitch> {
KillSwitchScreen(appUiState, viewModel)
}
composable<Route.SplitTunnel> {
val args = it.toRoute<Route.SplitTunnel>()
val config = appUiState.tunnels.first { it.id == args.id }
SplitTunnelScreen(config, viewModel)
}
composable<Route.TunnelAutoTunnel> {
val args = it.toRoute<Route.TunnelOptions>()
val config = appUiState.tunnels.first { it.id == args.id }
TunnelAutoTunnelScreen(config, appUiState.settings)
}
}
BackHandler {
if (navController.previousBackStackEntry == null || !navController.popBackStack()) {
this@MainActivity.finish()
}
}
}
}
}
}
}
}
}
}
@@ -52,11 +52,7 @@ sealed class Route {
@Serializable
data class SplitTunnel(
val id: Int,
) : Route() {
companion object {
const val KEY_ID = "id"
}
}
) : Route()
@Serializable
data class TunnelAutoTunnel(
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.ui.common.button
package com.zaneschepke.wireguardautotunnel.ui.common
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Spacer
@@ -1,59 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.common
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
@Composable
fun <T> DropdownSelector(
currentValue: T,
options: List<T>,
onValueSelected: (T) -> Unit,
modifier: Modifier = Modifier,
label: @Composable (() -> Unit)? = null,
isExpanded: Boolean = false,
onDismiss: () -> Unit = {},
) {
Row(
horizontalArrangement = Arrangement.spacedBy(5.dp),
verticalAlignment = Alignment.CenterVertically,
) {
if (label != null) label()
Text(
text = currentValue.toString(),
style = MaterialTheme.typography.bodyMedium,
)
Icon(Icons.Default.ArrowDropDown, contentDescription = stringResource(R.string.dropdown))
}
DropdownMenu(
modifier = modifier.height(250.dp),
scrollState = rememberScrollState(),
containerColor = MaterialTheme.colorScheme.surface,
expanded = isExpanded,
onDismissRequest = onDismiss,
) {
options.forEach { option ->
DropdownMenuItem(
text = { Text(text = option.toString()) },
onClick = {
onValueSelected(option)
onDismiss() // Close dropdown after selection
},
)
}
}
}
@@ -0,0 +1,13 @@
package com.zaneschepke.wireguardautotunnel.ui.common
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
class NestedScrollListener(val onUp: () -> Unit, val onDown: () -> Unit) : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
if (available.y < -1) onDown()
if (available.y > 1) onUp()
return Offset.Zero
}
}
@@ -0,0 +1,82 @@
package com.zaneschepke.wireguardautotunnel.ui.common
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Clear
import androidx.compose.material.icons.rounded.Search
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import com.zaneschepke.wireguardautotunnel.R
@Composable
fun SearchBar(onQuery: (queryString: String) -> Unit) {
// Immediately update and keep track of query from text field changes.
var query: String by rememberSaveable { mutableStateOf("") }
var showClearIcon by rememberSaveable { mutableStateOf(false) }
if (query.isEmpty()) {
showClearIcon = false
} else if (query.isNotEmpty()) {
showClearIcon = true
}
TextField(
value = query,
onValueChange = { onQueryChanged ->
// If user makes changes to text, immediately updated it.
query = onQueryChanged
onQuery(onQueryChanged)
},
leadingIcon = {
val icon = Icons.Rounded.Search
Icon(
imageVector = icon,
tint = MaterialTheme.colorScheme.onBackground,
contentDescription = icon.name,
)
},
trailingIcon = {
if (showClearIcon) {
IconButton(onClick = { query = "" }) {
val icon = Icons.Rounded.Clear
Icon(
imageVector = icon,
tint = MaterialTheme.colorScheme.onBackground,
contentDescription = icon.name,
)
}
}
},
maxLines = 1,
colors =
TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
disabledContainerColor = Color.Transparent,
),
placeholder = { Text(text = stringResource(R.string.hint_search_packages)) },
textStyle = MaterialTheme.typography.bodySmall,
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
modifier =
Modifier
.fillMaxWidth()
.background(color = MaterialTheme.colorScheme.background, shape = RectangleShape),
)
}

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