mirror of
https://github.com/wgtunnel/android.git
synced 2026-06-02 08:33:40 +02:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9124fcc133 | |||
| fed9537f5c | |||
| 9f4e801aad | |||
| 9cb5796f79 | |||
| b3ab9f6aae | |||
| c1760fda10 | |||
| 53278243e8 | |||
| 0963626164 | |||
| 82bda83464 | |||
| 7e264a6f19 | |||
| c18b3b7ba0 | |||
| 5f03b190dd | |||
| f3a5f14b0e | |||
| 70ce1adda4 | |||
| 87be6fa9ea | |||
| 03df457b55 | |||
| c14556a347 | |||
| f83559f910 | |||
| bf432cca0d | |||
| 68dc57422c | |||
| d528c9b56d | |||
| a42178258e | |||
| fee2878fa0 | |||
| 9d312afdba | |||
| 49f0d7f272 | |||
| 947cd23960 | |||
| d8521bc4c7 | |||
| 82afe54b99 | |||
| f20355e0f8 | |||
| 7d6e55e06e | |||
| 42221da443 | |||
| db920555ce | |||
| 0a3acf04c5 | |||
| 6a9370966c | |||
| 80d63db31a | |||
| 560f6a998b | |||
| 208df1914b |
@@ -73,6 +73,7 @@ jobs:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v5
|
||||
@@ -86,7 +87,7 @@ jobs:
|
||||
|
||||
- name: Decode Keystore
|
||||
id: decode_keystore
|
||||
uses: timheuer/base64-to-file@v1.2
|
||||
uses: timheuer/base64-to-file@v2.0
|
||||
with:
|
||||
fileName: ${{ env.KEY_STORE_FILE }}
|
||||
fileDir: ${{ env.KEY_STORE_LOCATION }}
|
||||
|
||||
@@ -75,6 +75,7 @@ jobs:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
@@ -85,7 +86,7 @@ jobs:
|
||||
run: chmod +x gradlew
|
||||
- name: Decode Keystore
|
||||
id: decode_keystore
|
||||
uses: timheuer/base64-to-file@v1.2
|
||||
uses: timheuer/base64-to-file@v2.0
|
||||
with:
|
||||
fileName: ${{ env.KEY_STORE_FILE }}
|
||||
fileDir: ${{ env.KEY_STORE_LOCATION }}
|
||||
|
||||
@@ -44,6 +44,8 @@ jobs:
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
@@ -101,7 +103,7 @@ jobs:
|
||||
|
||||
- name: Create nightly release
|
||||
id: create_release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
body: |
|
||||
${{ env.RELEASE_NOTES }}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
name: notifications
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
@@ -12,6 +13,9 @@ on:
|
||||
jobs:
|
||||
notify:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
PROJECT_NAME: Android
|
||||
|
||||
steps:
|
||||
- name: Send to Telegram - New Issue
|
||||
if: github.event_name == 'issues' && github.event.action == 'opened'
|
||||
@@ -22,8 +26,8 @@ jobs:
|
||||
BODY: ${{ github.event.issue.body || 'No body provided' }}
|
||||
URL: ${{ github.event.issue.html_url }}
|
||||
run: |
|
||||
BODY_TRUNC="${BODY:0:200}" # Truncate to avoid spam
|
||||
TEXT=$(echo -e "🆕 New Issue #$NUMBER: *$TITLE* by $USER\n\n$BODY_TRUNC\n\n[View Issue]($URL)")
|
||||
BODY_TRUNC="${BODY:0:200}"
|
||||
TEXT=$(echo -e "🆕 **${PROJECT_NAME}** — New Issue #$NUMBER: *$TITLE* by $USER\n\n$BODY_TRUNC\n\n[View Issue]($URL)")
|
||||
curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN }}/sendMessage" \
|
||||
-d chat_id="${{ vars.TELEGRAM_CHAT_ID }}" \
|
||||
${{ vars.TELEGRAM_THREAD_ID && format('-d message_thread_id="{0}"', vars.TELEGRAM_THREAD_ID) || '' }} \
|
||||
@@ -38,7 +42,7 @@ jobs:
|
||||
USER: ${{ github.event.issue.user.login }}
|
||||
URL: ${{ github.event.issue.html_url }}
|
||||
run: |
|
||||
TEXT=$(echo -e "✅ Issue Closed #$NUMBER: *$TITLE* by $USER\n\n[View Issue]($URL)")
|
||||
TEXT=$(echo -e "✅ **${PROJECT_NAME}** — Issue Closed #$NUMBER: *$TITLE* by $USER\n\n[View Issue]($URL)")
|
||||
curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN }}/sendMessage" \
|
||||
-d chat_id="${{ vars.TELEGRAM_CHAT_ID }}" \
|
||||
${{ vars.TELEGRAM_THREAD_ID && format('-d message_thread_id="{0}"', vars.TELEGRAM_THREAD_ID) || '' }} \
|
||||
@@ -54,7 +58,7 @@ jobs:
|
||||
URL: ${{ github.event.release.html_url }}
|
||||
ACTION: ${{ github.event.action }}
|
||||
run: |
|
||||
BODY_TRUNC="${BODY:0:200}" # Truncate to avoid spam
|
||||
BODY_TRUNC="${BODY:0:200}"
|
||||
if [ "$ACTION" == "prereleased" ]; then
|
||||
ICON="🌙"
|
||||
PREFIX="New Nightly Release"
|
||||
@@ -62,7 +66,7 @@ jobs:
|
||||
ICON="🚀"
|
||||
PREFIX="New Release"
|
||||
fi
|
||||
TEXT=$(echo -e "$ICON $PREFIX *$NAME* ($TAG)\n\n$BODY_TRUNC\n\n[View Release]($URL)")
|
||||
TEXT=$(echo -e "$ICON **${PROJECT_NAME}** — $PREFIX *$NAME* ($TAG)\n\n$BODY_TRUNC\n\n[View Release]($URL)")
|
||||
curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN }}/sendMessage" \
|
||||
-d chat_id="${{ vars.TELEGRAM_CHAT_ID }}" \
|
||||
${{ vars.TELEGRAM_THREAD_ID && format('-d message_thread_id="{0}"', vars.TELEGRAM_THREAD_ID) || '' }} \
|
||||
@@ -78,8 +82,8 @@ jobs:
|
||||
BODY: ${{ github.event.issue.body || 'No body provided' }}
|
||||
URL: ${{ github.event.issue.html_url }}
|
||||
run: |
|
||||
PLAIN_MESSAGE=$(echo -e "🆕 New Issue #$NUMBER: $TITLE by $USER\n\n$BODY\n\nView Issue: $URL")
|
||||
HTML_MESSAGE=$(echo -e "<p>🆕 New Issue #$NUMBER: <strong>$TITLE</strong> by $USER</p><p>$BODY</p><p><a href=\"$URL\">View Issue</a></p>")
|
||||
PLAIN_MESSAGE=$(echo -e "🆕 **${PROJECT_NAME}** — New Issue #$NUMBER: $TITLE by $USER\n\n$BODY\n\nView Issue: $URL")
|
||||
HTML_MESSAGE=$(echo -e "<p>🆕 <strong>${PROJECT_NAME}</strong> — New Issue #$NUMBER: <strong>$TITLE</strong> by $USER</p><p>$BODY</p><p><a href=\"$URL\">View Issue</a></p>")
|
||||
PLAIN_MESSAGE="${PLAIN_MESSAGE:0:220}"
|
||||
PAYLOAD=$(jq -n --arg body "$PLAIN_MESSAGE" --arg formatted "$HTML_MESSAGE" '{
|
||||
"msgtype": "m.text",
|
||||
@@ -101,8 +105,8 @@ jobs:
|
||||
USER: ${{ github.event.issue.user.login }}
|
||||
URL: ${{ github.event.issue.html_url }}
|
||||
run: |
|
||||
PLAIN_MESSAGE=$(echo -e "✅ Issue Closed #$NUMBER: $TITLE by $USER\n\nView Issue: $URL")
|
||||
HTML_MESSAGE=$(echo -e "<p>✅ Issue Closed #$NUMBER: <strong>$TITLE</strong> by $USER</p><p><a href=\"$URL\">View Issue</a></p>")
|
||||
PLAIN_MESSAGE=$(echo -e "✅ **${PROJECT_NAME}** — Issue Closed #$NUMBER: $TITLE by $USER\n\nView Issue: $URL")
|
||||
HTML_MESSAGE=$(echo -e "<p>✅ <strong>${PROJECT_NAME}</strong> — Issue Closed #$NUMBER: <strong>$TITLE</strong> by $USER</p><p><a href=\"$URL\">View Issue</a></p>")
|
||||
PLAIN_MESSAGE="${PLAIN_MESSAGE:0:220}"
|
||||
PAYLOAD=$(jq -n --arg body "$PLAIN_MESSAGE" --arg formatted "$HTML_MESSAGE" '{
|
||||
"msgtype": "m.text",
|
||||
@@ -132,8 +136,8 @@ jobs:
|
||||
ICON="🚀"
|
||||
PREFIX="New Release"
|
||||
fi
|
||||
PLAIN_MESSAGE=$(echo -e "$ICON $PREFIX $NAME ($TAG)\n\n$BODY\n\nView Release: $URL")
|
||||
HTML_MESSAGE=$(echo -e "<p>$ICON $PREFIX <strong>$NAME</strong> ($TAG)</p><p>$BODY</p><p><a href=\"$URL\">View Release</a></p>")
|
||||
PLAIN_MESSAGE=$(echo -e "$ICON **${PROJECT_NAME}** — $PREFIX $NAME ($TAG)\n\n$BODY\n\nView Release: $URL")
|
||||
HTML_MESSAGE=$(echo -e "<p>$ICON <strong>${PROJECT_NAME}</strong> — $PREFIX <strong>$NAME</strong> ($TAG)</p><p>$BODY</p><p><a href=\"$URL\">View Release</a></p>")
|
||||
PLAIN_MESSAGE="${PLAIN_MESSAGE:0:220}"
|
||||
PAYLOAD=$(jq -n --arg body "$PLAIN_MESSAGE" --arg formatted "$HTML_MESSAGE" '{
|
||||
"msgtype": "m.text",
|
||||
|
||||
@@ -143,7 +143,7 @@ jobs:
|
||||
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
body: |
|
||||
${{ env.RELEASE_NOTES }}
|
||||
@@ -215,7 +215,7 @@ jobs:
|
||||
# in the folder specified in the release signing configuration
|
||||
- name: Decode Keystore
|
||||
id: decode_keystore
|
||||
uses: timheuer/base64-to-file@v1.2
|
||||
uses: timheuer/base64-to-file@v2.0
|
||||
with:
|
||||
fileName: ${{ env.KEY_STORE_FILE }}
|
||||
fileDir: ${{ env.KEY_STORE_LOCATION }}
|
||||
@@ -240,4 +240,4 @@ jobs:
|
||||
- name: Distribute app to Prod track 🚀
|
||||
run: |
|
||||
track=${{ github.event_name == 'push' && 'production' || inputs.track }}
|
||||
(cd ${{ github.workspace }} && bundle install && bundle exec fastlane $track)
|
||||
(cd ${{ github.workspace }} && bundle install && bundle exec fastlane $track --verbose)
|
||||
@@ -0,0 +1,6 @@
|
||||
[submodule "hevtunnel/src/main/jni/hev-socks5-tunnel"]
|
||||
path = hevtunnel/src/main/jni/hev-socks5-tunnel
|
||||
url = https://github.com/heiher/hev-socks5-tunnel
|
||||
[submodule "tunnel/tools/amneziawg-tools"]
|
||||
path = tunnel/tools/amneziawg-tools
|
||||
url = https://github.com/amnezia-vpn/amneziawg-tools
|
||||
@@ -21,7 +21,7 @@ and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/)
|
||||
<div align="center">
|
||||
|
||||
[](https://play.google.com/store/apps/details?id=com.zaneschepke.wireguardautotunnel)
|
||||
[](https://github.com/zaneschepke/fdroid)
|
||||
[](https://apt.izzysoft.de/fdroid/index/apk/com.zaneschepke.wireguardautotunnel)
|
||||
[](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>
|
||||
|
||||
+36
-30
@@ -1,9 +1,9 @@
|
||||
import com.android.build.gradle.internal.api.BaseVariantOutputImpl
|
||||
import com.android.build.api.dsl.ApplicationExtension
|
||||
import com.android.build.api.variant.FilterConfiguration
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlinxSerialization)
|
||||
alias(libs.plugins.ksp)
|
||||
alias(libs.plugins.compose.compiler)
|
||||
@@ -11,7 +11,26 @@ plugins {
|
||||
alias(libs.plugins.licensee)
|
||||
}
|
||||
|
||||
android {
|
||||
ksp {
|
||||
arg("room.schemaLocation", "$projectDir/schemas")
|
||||
}
|
||||
|
||||
licensee {
|
||||
allowedLicenses().forEach { allow(it) }
|
||||
allowedLicenseUrls().forEach { allowUrl(it) }
|
||||
// foss, but missing licenses
|
||||
ignoreDependencies("com.github.T8RIN.QuickieExtended")
|
||||
ignoreDependencies("com.github.topjohnwu.libsu")
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget = JvmTarget.JVM_17
|
||||
freeCompilerArgs = listOf("-XXLanguage:+PropertyParamAnnotationDefaultTargetMode")
|
||||
}
|
||||
}
|
||||
|
||||
configure<ApplicationExtension> {
|
||||
namespace = Constants.APP_ID
|
||||
compileSdk = Constants.TARGET_SDK
|
||||
|
||||
@@ -22,8 +41,6 @@ android {
|
||||
includeInBundle = false
|
||||
}
|
||||
|
||||
ksp { arg("room.schemaLocation", "$projectDir/schemas") }
|
||||
|
||||
// fix okhttp proguard issue
|
||||
packaging { resources { pickFirsts.add("okhttp3/internal/publicsuffix/publicsuffixes.gz") } }
|
||||
|
||||
@@ -119,28 +136,16 @@ android {
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget = JvmTarget.JVM_17
|
||||
freeCompilerArgs = listOf("-XXLanguage:+PropertyParamAnnotationDefaultTargetMode")
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
compose = true
|
||||
buildConfig = true
|
||||
resValues = true
|
||||
}
|
||||
packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } }
|
||||
}
|
||||
|
||||
licensee {
|
||||
allowedLicenses().forEach { allow(it) }
|
||||
allowedLicenseUrls().forEach { allowUrl(it) }
|
||||
// foss, but missing license
|
||||
ignoreDependencies("com.github.T8RIN.QuickieExtended")
|
||||
}
|
||||
|
||||
android.applicationVariants.all {
|
||||
val variant = this
|
||||
androidComponents {
|
||||
onVariants { variant ->
|
||||
|
||||
val abiNameMap =
|
||||
mapOf(
|
||||
@@ -150,11 +155,14 @@ android {
|
||||
"x86_64" to "x64",
|
||||
)
|
||||
|
||||
variant.outputs.all {
|
||||
val output = this as BaseVariantOutputImpl
|
||||
val abi = output.getFilter("ABI")
|
||||
variant.outputs.forEach { output ->
|
||||
val abi = output.filters.find { it.filterType == FilterConfiguration.FilterType.ABI }?.identifier
|
||||
|
||||
val baseFileName = "${Constants.APP_NAME}-${variant.flavorName}-v${variant.versionName}"
|
||||
val flavorName = variant.productFlavors.joinToString("-") { it.second }
|
||||
|
||||
val versionName = output.versionName.get()
|
||||
|
||||
val baseFileName = "${Constants.APP_NAME}-${flavorName}-v${versionName}"
|
||||
|
||||
val outputFileName =
|
||||
if (!abi.isNullOrEmpty()) {
|
||||
@@ -164,7 +172,7 @@ android {
|
||||
"${baseFileName}.apk"
|
||||
}
|
||||
|
||||
output.outputFileName = outputFileName
|
||||
output.outputFileName.set(outputFileName)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -172,6 +180,7 @@ android {
|
||||
dependencies {
|
||||
implementation(project(":logcatter"))
|
||||
implementation(project(":networkmonitor"))
|
||||
implementation(project(":tunnel"))
|
||||
|
||||
// Core foundations
|
||||
implementation(libs.bundles.androidx.core.full)
|
||||
@@ -208,9 +217,6 @@ dependencies {
|
||||
// State management
|
||||
implementation(libs.bundles.orbit.mvi)
|
||||
|
||||
// Tunnel
|
||||
implementation(libs.bundles.wireguard.tunnel)
|
||||
|
||||
// Shizuku
|
||||
implementation(libs.bundles.shizuku)
|
||||
|
||||
@@ -268,7 +274,7 @@ tasks.register<Copy>("copyLicenseeJsonToAssets") {
|
||||
tasks.named("preBuild") { dependsOn("copyLicenseeJsonToAssets") }
|
||||
|
||||
// https://gist.github.com/obfusk/61046e09cee352ae6dd109911534b12e#fix-proposed-by-linsui-disable-baseline-profiles
|
||||
tasks.whenTaskAdded {
|
||||
tasks.configureEach {
|
||||
if (name.contains("ArtProfile")) {
|
||||
enabled = false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,506 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 30,
|
||||
"identityHash": "28560c6b408d8f5ef28844723e940395",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "tunnel_config",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false, `is_primary_tunnel` INTEGER NOT NULL DEFAULT false, `quick_config` TEXT NOT NULL DEFAULT '', `dynamic_dns` INTEGER NOT NULL DEFAULT false, `is_ethernet_tunnel` INTEGER NOT NULL DEFAULT false, `prefer_ipv6` INTEGER NOT NULL DEFAULT false, `position` INTEGER NOT NULL DEFAULT 0, `auto_tunnel_apps` TEXT NOT NULL DEFAULT '[]', `is_metered` INTEGER NOT NULL DEFAULT false, `ipv4_fallback` INTEGER NOT NULL DEFAULT false, `ipv6_restore` INTEGER NOT NULL DEFAULT false)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "tunnelNetworks",
|
||||
"columnName": "tunnel_networks",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "''"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isMobileDataTunnel",
|
||||
"columnName": "is_mobile_data_tunnel",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isPrimaryTunnel",
|
||||
"columnName": "is_primary_tunnel",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "quickConfig",
|
||||
"columnName": "quick_config",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "''"
|
||||
},
|
||||
{
|
||||
"fieldPath": "dynamicDnsEnabled",
|
||||
"columnName": "dynamic_dns",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isEthernetTunnel",
|
||||
"columnName": "is_ethernet_tunnel",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isIpv6Preferred",
|
||||
"columnName": "prefer_ipv6",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "position",
|
||||
"columnName": "position",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "autoTunnelApps",
|
||||
"columnName": "auto_tunnel_apps",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "'[]'"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isMetered",
|
||||
"columnName": "is_metered",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "ipv4FallbackEnabled",
|
||||
"columnName": "ipv4_fallback",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "ipv6RestoreEnabled",
|
||||
"columnName": "ipv6_restore",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_tunnel_config_name",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"name"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_tunnel_config_name` ON `${TABLE_NAME}` (`name`)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "proxy_settings",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `socks5_proxy_enabled` INTEGER NOT NULL DEFAULT 0, `socks5_proxy_bind_address` TEXT, `http_proxy_enable` INTEGER NOT NULL DEFAULT 0, `http_proxy_bind_address` TEXT, `proxy_username` TEXT, `proxy_password` TEXT)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "socks5ProxyEnabled",
|
||||
"columnName": "socks5_proxy_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "socks5ProxyBindAddress",
|
||||
"columnName": "socks5_proxy_bind_address",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "httpProxyEnabled",
|
||||
"columnName": "http_proxy_enable",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "httpProxyBindAddress",
|
||||
"columnName": "http_proxy_bind_address",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "proxyUsername",
|
||||
"columnName": "proxy_username",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "proxyPassword",
|
||||
"columnName": "proxy_password",
|
||||
"affinity": "TEXT"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "general_settings",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT 0, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT 0, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `global_split_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `app_mode` INTEGER NOT NULL DEFAULT 0, `theme` TEXT NOT NULL DEFAULT 'AUTOMATIC', `locale` TEXT, `remote_key` TEXT, `is_remote_control_enabled` INTEGER NOT NULL DEFAULT 0, `is_pin_lock_enabled` INTEGER NOT NULL DEFAULT 0, `is_always_on_vpn_enabled` INTEGER NOT NULL DEFAULT 0, `already_donated` INTEGER NOT NULL DEFAULT 0, `screen_recording_security` INTEGER NOT NULL DEFAULT 1, `global_amnezia_enabled` INTEGER NOT NULL DEFAULT 0, `tunnel_scripting_enabled` INTEGER NOT NULL DEFAULT 0)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isShortcutsEnabled",
|
||||
"columnName": "is_shortcuts_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isRestoreOnBootEnabled",
|
||||
"columnName": "is_restore_on_boot_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isMultiTunnelEnabled",
|
||||
"columnName": "is_multi_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isGlobalSplitTunnelEnabled",
|
||||
"columnName": "global_split_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "tunnelMode",
|
||||
"columnName": "app_mode",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "theme",
|
||||
"columnName": "theme",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "'AUTOMATIC'"
|
||||
},
|
||||
{
|
||||
"fieldPath": "locale",
|
||||
"columnName": "locale",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "remoteKey",
|
||||
"columnName": "remote_key",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isRemoteControlEnabled",
|
||||
"columnName": "is_remote_control_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isPinLockEnabled",
|
||||
"columnName": "is_pin_lock_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAlwaysOnVpnEnabled",
|
||||
"columnName": "is_always_on_vpn_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "alreadyDonated",
|
||||
"columnName": "already_donated",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "screenRecordingSecurityEnabled",
|
||||
"columnName": "screen_recording_security",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "1"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isGlobalAmneziaEnabled",
|
||||
"columnName": "global_amnezia_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "tunnelScriptingEnabled",
|
||||
"columnName": "tunnel_scripting_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "auto_tunnel_settings",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL DEFAULT 0, `trusted_network_ssids` TEXT NOT NULL DEFAULT '', `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT 0, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT 0, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_unsecure_enabled` INTEGER NOT NULL DEFAULT 0, `wifi_detection_method` INTEGER NOT NULL DEFAULT 0, `start_on_boot` INTEGER NOT NULL DEFAULT 0)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAutoTunnelEnabled",
|
||||
"columnName": "is_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnMobileDataEnabled",
|
||||
"columnName": "is_tunnel_on_mobile_data_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "trustedNetworkSSIDs",
|
||||
"columnName": "trusted_network_ssids",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "''"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnEthernetEnabled",
|
||||
"columnName": "is_tunnel_on_ethernet_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnWifiEnabled",
|
||||
"columnName": "is_tunnel_on_wifi_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isWildcardsEnabled",
|
||||
"columnName": "is_wildcards_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isStopOnNoInternetEnabled",
|
||||
"columnName": "is_stop_on_no_internet_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnUnsecureEnabled",
|
||||
"columnName": "is_tunnel_on_unsecure_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "wifiDetectionMethod",
|
||||
"columnName": "wifi_detection_method",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "startOnBoot",
|
||||
"columnName": "start_on_boot",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "monitoring_settings",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_local_logs_enabled` INTEGER NOT NULL DEFAULT 0, `tunnel_statistics_enabled` INTEGER NOT NULL DEFAULT 1, `tunnel_statistics_poll_interval` INTEGER NOT NULL DEFAULT 3)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isLocalLogsEnabled",
|
||||
"columnName": "is_local_logs_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "tunnelStatisticsEnabled",
|
||||
"columnName": "tunnel_statistics_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "1"
|
||||
},
|
||||
{
|
||||
"fieldPath": "tunnelStatisticsPollInterval",
|
||||
"columnName": "tunnel_statistics_poll_interval",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "3"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "dns_settings",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `dns_protocol` INTEGER NOT NULL DEFAULT 0, `dns_endpoint` TEXT, `global_tunnel_dns_enabled` INTEGER NOT NULL DEFAULT 0)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "dnsProtocol",
|
||||
"columnName": "dns_protocol",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "dnsEndpoint",
|
||||
"columnName": "dns_endpoint",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isGlobalTunnelDnsEnabled",
|
||||
"columnName": "global_tunnel_dns_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "lockdown_settings",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `bypass_lan` INTEGER NOT NULL DEFAULT 0, `metered` INTEGER NOT NULL DEFAULT 0, `dual_stack` INTEGER NOT NULL DEFAULT 0)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "bypassLan",
|
||||
"columnName": "bypass_lan",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "metered",
|
||||
"columnName": "metered",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "dualStack",
|
||||
"columnName": "dual_stack",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '28560c6b408d8f5ef28844723e940395')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -6,13 +6,11 @@
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<!--for split tunneling-->
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:ignore="QueryAllPackagesPermission" />
|
||||
|
||||
<!--foreground service special use for non VPN service tunnels, android 14-->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
<!--foreground service special use for VPN service tunnels, android 14-->
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
|
||||
|
||||
<!--foreground service permissions-->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
@@ -60,6 +58,9 @@
|
||||
android:theme="@style/Theme.App.Start"
|
||||
tools:targetApi="tiramisu">
|
||||
|
||||
<meta-data android:name="android.telephony.PROPERTY_SATELLITE_DATA_OPTIMIZED"
|
||||
android:value="${applicationId}" />
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
@@ -147,40 +148,6 @@
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service
|
||||
android:name=".core.service.tile.TunnelControlTile"
|
||||
android:exported="true"
|
||||
android:icon="@drawable/ic_notification"
|
||||
android:label="@string/tunnel_control"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
<meta-data
|
||||
android:name="android.service.quicksettings.ACTIVE_TILE"
|
||||
android:value="true" />
|
||||
<meta-data
|
||||
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
|
||||
android:value="true" />
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service
|
||||
android:name=".core.service.tile.AutoTunnelControlTile"
|
||||
android:exported="true"
|
||||
android:icon="@drawable/ic_notification"
|
||||
android:label="@string/auto_tunnel"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
<meta-data
|
||||
android:name="android.service.quicksettings.ACTIVE_TILE"
|
||||
android:value="true" />
|
||||
<meta-data
|
||||
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
|
||||
android:value="true" />
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service
|
||||
android:name=".core.service.autotunnel.AutoTunnelService"
|
||||
android:enabled="true"
|
||||
@@ -197,33 +164,6 @@
|
||||
network connectivity monitoring."/>
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".core.service.TunnelForegroundService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="specialUse"
|
||||
android:persistent="true"
|
||||
android:stopWithTask="false"
|
||||
tools:node="merge">
|
||||
<property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="This service sustains non-VpnService virtual tunnels (using gVisor/netstack for
|
||||
isolated networking), keeping connections alive for continuous secure data routing.
|
||||
Persistent foreground operation is essential to handle
|
||||
low-level tunnel maintenance and avoid interruptions, beyond the capabilities of other
|
||||
service types or background work."/>
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".core.service.VpnForegroundService"
|
||||
android:exported="false"
|
||||
android:persistent="true"
|
||||
android:foregroundServiceType="systemExempted"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE">
|
||||
<intent-filter>
|
||||
<action android:name="android.net.VpnService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<receiver
|
||||
android:name=".core.broadcast.RestartReceiver"
|
||||
android:enabled="true"
|
||||
@@ -238,14 +178,6 @@
|
||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver
|
||||
android:name=".core.broadcast.KernelReceiver"
|
||||
android:exported="false"
|
||||
android:permission="${applicationId}.permission.CONTROL_TUNNELS">
|
||||
<intent-filter>
|
||||
<action android:name="com.wireguard.android.action.REFRESH_TUNNEL_STATES" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<!--custom security solution for easier user integration-->
|
||||
<receiver
|
||||
android:name=".core.broadcast.RemoteControlReceiver"
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.graphics.Color
|
||||
import android.net.VpnService
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.SystemBarStyle
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.compose.setContent
|
||||
@@ -23,7 +24,6 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
@@ -52,7 +52,6 @@ import androidx.compose.ui.text.withLink
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.zIndex
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
|
||||
import androidx.navigation3.runtime.entryProvider
|
||||
@@ -61,7 +60,7 @@ import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
||||
import androidx.navigation3.ui.NavDisplay
|
||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
@@ -75,13 +74,13 @@ import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarInfo
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarType
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.rememberCustomSnackbarState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.SecureRoute
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.Tab
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.components.BottomNavbar
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.components.DynamicTopAppBar
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.components.currentRouteAsNavbarState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.functions.rememberNavController
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.AutoTunnelScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.advanced.AutoTunnelAdvancedScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.detection.WifiDetectionMethodScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.disclosure.LocationDisclosureScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.preferred.PreferredTunnelScreen
|
||||
@@ -92,18 +91,21 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.Appear
|
||||
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.dns.DnsSettingsScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.globals.TunnelGlobalsScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.integrations.AndroidIntegrationsScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.lockdown.LockdownSettingsScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.TunnelMonitoringScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.MonitoringScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.logs.LogsScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.ping.PingTargetScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.security.SecurityScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.donate.DonateScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.donate.crypto.AddressesScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.license.LicenseScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.TunnelsScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.config.ConfigScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.settings.TunnelSettingsScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.settings.config.ConfigScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.settings.config.edit.ConfigEditScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.settings.ipv6.IPv6Screen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.sort.SortScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.splittunnel.SplitTunnelScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.AlertRed
|
||||
@@ -115,17 +117,20 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.restartApp
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.ConfigViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.ConfigEditViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SplitTunnelViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelViewModel
|
||||
import de.raphaelebner.roomdatabasebackup.core.RoomBackup
|
||||
import kotlinx.coroutines.awaitCancellation
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
import org.koin.core.parameter.parametersOf
|
||||
import org.orbitmvi.orbit.compose.collectAsState
|
||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
@@ -158,7 +163,7 @@ class MainActivity : AppCompatActivity() {
|
||||
setContent {
|
||||
val context = LocalContext.current
|
||||
val isTv = isRunningOnTv()
|
||||
val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
|
||||
val uiState by viewModel.collectAsState()
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
LaunchedEffect(uiState.isAppLoaded) {
|
||||
@@ -170,8 +175,8 @@ class MainActivity : AppCompatActivity() {
|
||||
val snackbarState = rememberCustomSnackbarState()
|
||||
var showVpnPermissionDialog by remember { mutableStateOf(false) }
|
||||
var vpnPermissionDenied by remember { mutableStateOf(false) }
|
||||
var requestingAppMode by remember {
|
||||
mutableStateOf<Pair<AppMode?, TunnelConfig?>>(Pair(null, null))
|
||||
var requestingTunnelMode by remember {
|
||||
mutableStateOf<Pair<TunnelMode?, TunnelConfig?>>(Pair(null, null))
|
||||
}
|
||||
|
||||
val startingStack = buildList {
|
||||
@@ -201,14 +206,14 @@ class MainActivity : AppCompatActivity() {
|
||||
} else {
|
||||
vpnPermissionDenied = false
|
||||
showVpnPermissionDialog = false
|
||||
val (appMode, config) = requestingAppMode
|
||||
val (appMode, config) = requestingTunnelMode
|
||||
when (appMode) {
|
||||
AppMode.VPN -> if (config != null) viewModel.startTunnel(config)
|
||||
AppMode.LOCK_DOWN -> viewModel.setAppMode(AppMode.LOCK_DOWN)
|
||||
TunnelMode.VPN -> if (config != null) viewModel.startTunnel(config)
|
||||
TunnelMode.LOCK_DOWN -> viewModel.setAppMode(TunnelMode.LOCK_DOWN)
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
requestingAppMode = Pair(null, null)
|
||||
requestingTunnelMode = Pair(null, null)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -218,7 +223,8 @@ class MainActivity : AppCompatActivity() {
|
||||
GlobalSideEffect.ConfigChanged -> restartApp()
|
||||
GlobalSideEffect.PopBackStack -> navController.pop()
|
||||
is GlobalSideEffect.RequestVpnPermission -> {
|
||||
requestingAppMode = Pair(sideEffect.requestingMode, sideEffect.config)
|
||||
requestingTunnelMode =
|
||||
Pair(sideEffect.requestingMode, sideEffect.config)
|
||||
vpnActivity.launch(VpnService.prepare(this@MainActivity))
|
||||
}
|
||||
|
||||
@@ -313,13 +319,40 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
val isPinVisible by remember { derivedStateOf { showLock } }
|
||||
|
||||
val currentRoute by remember {
|
||||
derivedStateOf { backStack.lastOrNull() as? Route }
|
||||
}
|
||||
|
||||
LaunchedEffect(
|
||||
uiState.isScreenRecordingProtectionEnabled,
|
||||
currentRoute,
|
||||
isPinVisible,
|
||||
) {
|
||||
val isSecureRoute = currentRoute is SecureRoute
|
||||
|
||||
val shouldProtect =
|
||||
uiState.isScreenRecordingProtectionEnabled &&
|
||||
(isSecureRoute || isPinVisible)
|
||||
|
||||
if (shouldProtect) {
|
||||
window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_SECURE,
|
||||
WindowManager.LayoutParams.FLAG_SECURE,
|
||||
)
|
||||
} else {
|
||||
delay(500L)
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
awaitCancellation()
|
||||
}
|
||||
}
|
||||
|
||||
if (showLock) {
|
||||
PinManager.initialize(context = this@MainActivity)
|
||||
PinLockScreen()
|
||||
} else {
|
||||
val currentRoute by remember {
|
||||
derivedStateOf { backStack.lastOrNull() as? Route }
|
||||
}
|
||||
|
||||
val currentTab by remember {
|
||||
derivedStateOf { Tab.fromRoute(currentRoute ?: Route.Tunnels) }
|
||||
}
|
||||
@@ -332,7 +365,7 @@ class MainActivity : AppCompatActivity() {
|
||||
)
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
if (uiState.appMode == AppMode.LOCK_DOWN) {
|
||||
if (uiState.tunnelMode == TunnelMode.LOCK_DOWN) {
|
||||
AppAlertBanner(
|
||||
stringResource(R.string.locked_down)
|
||||
.uppercase(Locale.current.platformLocale),
|
||||
@@ -346,11 +379,7 @@ class MainActivity : AppCompatActivity() {
|
||||
snackbarState.SnackbarHost(
|
||||
modifier =
|
||||
Modifier.align(Alignment.BottomCenter)
|
||||
.padding(
|
||||
bottom =
|
||||
if (LocalIsAndroidTV.current) 120.dp
|
||||
else 80.dp
|
||||
)
|
||||
.padding(bottom = 80.dp)
|
||||
) { info ->
|
||||
CustomSnackBar(
|
||||
message = info.message,
|
||||
@@ -387,7 +416,6 @@ class MainActivity : AppCompatActivity() {
|
||||
bottom = padding.calculateBottomPadding(),
|
||||
)
|
||||
.consumeWindowInsets(padding)
|
||||
.imePadding()
|
||||
) {
|
||||
NavDisplay(
|
||||
backStack = backStack,
|
||||
@@ -438,6 +466,13 @@ class MainActivity : AppCompatActivity() {
|
||||
)
|
||||
TunnelSettingsScreen(viewModel)
|
||||
}
|
||||
entry<Route.Config> { key ->
|
||||
val viewModel: TunnelViewModel =
|
||||
koinViewModel(
|
||||
parameters = { parametersOf(key.id) }
|
||||
)
|
||||
ConfigScreen(viewModel, key.live)
|
||||
}
|
||||
entry<Route.SplitTunnel> { key ->
|
||||
val viewModel: SplitTunnelViewModel =
|
||||
koinViewModel(
|
||||
@@ -445,12 +480,12 @@ class MainActivity : AppCompatActivity() {
|
||||
)
|
||||
SplitTunnelScreen(viewModel)
|
||||
}
|
||||
entry<Route.Config> { key ->
|
||||
val viewModel: ConfigViewModel =
|
||||
entry<Route.ConfigEdit> { key ->
|
||||
val viewModel: ConfigEditViewModel =
|
||||
koinViewModel(
|
||||
parameters = { parametersOf(key.id) }
|
||||
)
|
||||
ConfigScreen(viewModel)
|
||||
ConfigEditScreen(viewModel)
|
||||
}
|
||||
entry<Route.LocationDisclosure> {
|
||||
LocationDisclosureScreen()
|
||||
@@ -459,26 +494,20 @@ class MainActivity : AppCompatActivity() {
|
||||
entry<Route.WifiPreferences> {
|
||||
WifiSettingsScreen()
|
||||
}
|
||||
entry<Route.AdvancedAutoTunnel> {
|
||||
AutoTunnelAdvancedScreen()
|
||||
}
|
||||
entry<Route.WifiDetectionMethod> {
|
||||
WifiDetectionMethodScreen()
|
||||
}
|
||||
entry<Route.Settings> { SettingsScreen() }
|
||||
entry<Route.TunnelMonitoring> {
|
||||
TunnelMonitoringScreen()
|
||||
}
|
||||
entry<Route.AndroidIntegrations> {
|
||||
AndroidIntegrationsScreen()
|
||||
}
|
||||
entry<Route.Dns> { DnsSettingsScreen() }
|
||||
entry<Route.ConfigGlobal> { key ->
|
||||
val viewModel: ConfigViewModel =
|
||||
val viewModel: ConfigEditViewModel =
|
||||
koinViewModel(
|
||||
parameters = { parametersOf(key.id) }
|
||||
)
|
||||
ConfigScreen(viewModel)
|
||||
ConfigEditScreen(viewModel)
|
||||
}
|
||||
entry<Route.SplitTunnelGlobal> { key ->
|
||||
val viewModel: SplitTunnelViewModel =
|
||||
@@ -487,6 +516,13 @@ class MainActivity : AppCompatActivity() {
|
||||
)
|
||||
SplitTunnelScreen(viewModel)
|
||||
}
|
||||
entry<Route.IPv6> { key ->
|
||||
val viewModel: TunnelViewModel =
|
||||
koinViewModel(
|
||||
parameters = { parametersOf(key.id) }
|
||||
)
|
||||
IPv6Screen(viewModel)
|
||||
}
|
||||
entry<Route.LockdownSettings> {
|
||||
LockdownSettingsScreen()
|
||||
}
|
||||
@@ -502,7 +538,9 @@ class MainActivity : AppCompatActivity() {
|
||||
entry<Route.PreferredTunnel> { key ->
|
||||
PreferredTunnelScreen(key.tunnelNetwork)
|
||||
}
|
||||
entry<Route.PingTarget> { PingTargetScreen() }
|
||||
entry<Route.TunnelGlobals> { TunnelGlobalsScreen() }
|
||||
entry<Route.Security> { SecurityScreen() }
|
||||
entry<Route.Monitoring> { MonitoringScreen() }
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -514,70 +552,54 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
networkMonitor.checkPermissionsAndUpdateState()
|
||||
WireGuardAutoTunnel.setUiActive(true)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
WireGuardAutoTunnel.setUiActive(false)
|
||||
}
|
||||
|
||||
fun performBackup() =
|
||||
lifecycleScope.launch {
|
||||
// reset active tuns before backup to prevent trying to start them without permission on
|
||||
// restore
|
||||
tunnelRepository.resetActiveTunnels()
|
||||
roomBackup
|
||||
.database(appDatabase)
|
||||
.backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG)
|
||||
.enableLogDebug(true)
|
||||
.maxFileCount(5)
|
||||
.apply {
|
||||
onCompleteListener { success, _, _ ->
|
||||
lifecycleScope.launch {
|
||||
if (success) {
|
||||
showToast(
|
||||
getString(
|
||||
R.string.backup_success,
|
||||
getString(R.string.restarting_app),
|
||||
)
|
||||
fun performBackup() = lifecycleScope.launch {
|
||||
roomBackup
|
||||
.database(appDatabase)
|
||||
.backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG)
|
||||
.enableLogDebug(true)
|
||||
.maxFileCount(5)
|
||||
.apply {
|
||||
onCompleteListener { success, _, _ ->
|
||||
lifecycleScope.launch {
|
||||
if (success) {
|
||||
showToast(
|
||||
getString(
|
||||
R.string.backup_success,
|
||||
getString(R.string.restarting_app),
|
||||
)
|
||||
restartApp()
|
||||
} else {
|
||||
showToast(R.string.backup_failed)
|
||||
}
|
||||
)
|
||||
restartApp()
|
||||
} else {
|
||||
showToast(R.string.backup_failed)
|
||||
}
|
||||
}
|
||||
}
|
||||
.backup()
|
||||
}
|
||||
}
|
||||
.backup()
|
||||
}
|
||||
|
||||
fun performRestore() =
|
||||
lifecycleScope.launch {
|
||||
roomBackup
|
||||
.database(appDatabase)
|
||||
.enableLogDebug(true)
|
||||
.backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG)
|
||||
.apply {
|
||||
onCompleteListener { success, _, _ ->
|
||||
lifecycleScope.launch {
|
||||
if (success) {
|
||||
showToast(
|
||||
getString(
|
||||
R.string.restore_success,
|
||||
getString(R.string.restarting_app),
|
||||
)
|
||||
fun performRestore() = lifecycleScope.launch {
|
||||
roomBackup
|
||||
.database(appDatabase)
|
||||
.enableLogDebug(true)
|
||||
.backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG)
|
||||
.apply {
|
||||
onCompleteListener { success, _, _ ->
|
||||
lifecycleScope.launch {
|
||||
if (success) {
|
||||
showToast(
|
||||
getString(
|
||||
R.string.restore_success,
|
||||
getString(R.string.restarting_app),
|
||||
)
|
||||
restartApp()
|
||||
} else {
|
||||
showToast(R.string.restore_failed)
|
||||
}
|
||||
)
|
||||
restartApp()
|
||||
} else {
|
||||
showToast(R.string.restore_failed)
|
||||
}
|
||||
}
|
||||
}
|
||||
.restore()
|
||||
}
|
||||
}
|
||||
.restore()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,29 +2,33 @@ package com.zaneschepke.wireguardautotunnel
|
||||
|
||||
import android.app.Application
|
||||
import android.os.StrictMode
|
||||
import com.zaneschepke.logcatter.LogReader
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationMonitor
|
||||
import com.zaneschepke.tunnel.backend.Backend
|
||||
import com.zaneschepke.tunnel.di.tunnelModule
|
||||
import com.zaneschepke.tunnel.service.VpnService
|
||||
import com.zaneschepke.wireguardautotunnel.core.event.TunnelEventDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.AppBoostrapCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
||||
import com.zaneschepke.wireguardautotunnel.di.Dispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.di.Scope
|
||||
import com.zaneschepke.wireguardautotunnel.di.appModule
|
||||
import com.zaneschepke.wireguardautotunnel.di.coordinatorModule
|
||||
import com.zaneschepke.wireguardautotunnel.di.databaseModule
|
||||
import com.zaneschepke.wireguardautotunnel.di.dispatchersModule
|
||||
import com.zaneschepke.wireguardautotunnel.di.networkModule
|
||||
import com.zaneschepke.wireguardautotunnel.di.tunnelModule
|
||||
import com.zaneschepke.wireguardautotunnel.di.tunnelBackendProviderModule
|
||||
import com.zaneschepke.wireguardautotunnel.di.workerModule
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.android.ext.android.get
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.android.ext.koin.androidLogger
|
||||
import org.koin.androidx.workmanager.koin.workManagerFactory
|
||||
import org.koin.core.annotation.KoinViewModelScopeApi
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.context.GlobalContext.startKoin
|
||||
import org.koin.core.lazyModules
|
||||
@@ -36,22 +40,36 @@ class WireGuardAutoTunnel : Application(), KoinComponent {
|
||||
|
||||
private val applicationScope: CoroutineScope by inject(named(Scope.APPLICATION))
|
||||
private val ioDispatcher: CoroutineDispatcher by inject(named(Dispatcher.IO))
|
||||
private val logReader: LogReader by inject()
|
||||
|
||||
private val monitoringRepository: MonitoringSettingsRepository by inject()
|
||||
private val notificationMonitor: NotificationMonitor by inject()
|
||||
private val boostrapCoordinator: AppBoostrapCoordinator by inject()
|
||||
|
||||
private val notificationService: NotificationService by inject()
|
||||
|
||||
private val tunnelCoordinator: TunnelCoordinator by inject()
|
||||
|
||||
private val backend: Backend by inject()
|
||||
|
||||
@OptIn(KoinViewModelScopeApi::class)
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
startKoin {
|
||||
androidContext(this@WireGuardAutoTunnel)
|
||||
if (BuildConfig.DEBUG) androidLogger()
|
||||
workManagerFactory()
|
||||
modules(dispatchersModule, appModule, databaseModule, tunnelModule, workerModule)
|
||||
modules(
|
||||
dispatchersModule,
|
||||
appModule,
|
||||
databaseModule,
|
||||
tunnelBackendProviderModule,
|
||||
tunnelModule,
|
||||
workerModule,
|
||||
coordinatorModule,
|
||||
)
|
||||
options(viewModelScopeFactory())
|
||||
lazyModules(networkModule)
|
||||
}
|
||||
instance = this
|
||||
notificationService.createAllChannels()
|
||||
if (BuildConfig.DEBUG) {
|
||||
Timber.plant(Timber.DebugTree())
|
||||
StrictMode.setThreadPolicy(
|
||||
@@ -66,44 +84,30 @@ class WireGuardAutoTunnel : Application(), KoinComponent {
|
||||
Timber.plant(ReleaseTree())
|
||||
}
|
||||
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
launch {
|
||||
monitoringRepository.flow
|
||||
.distinctUntilChangedBy { it.isLocalLogsEnabled }
|
||||
.collect { settings ->
|
||||
if (settings.isLocalLogsEnabled) {
|
||||
logReader.start()
|
||||
} else {
|
||||
logReader.stop()
|
||||
}
|
||||
}
|
||||
backend.setAlwaysOnCallback(
|
||||
object : VpnService.AlwaysOnCallback {
|
||||
override fun alwaysOnTriggered() {
|
||||
applicationScope.launch { tunnelCoordinator.startDefault() }
|
||||
}
|
||||
}
|
||||
launch { notificationMonitor.handleApplicationNotifications() }
|
||||
}
|
||||
)
|
||||
|
||||
val dispatcher = get<TunnelEventDispatcher>()
|
||||
val coordinator = get<TunnelCoordinator>()
|
||||
val provider = get<TunnelProvider>()
|
||||
|
||||
// for notifications
|
||||
dispatcher.bind(
|
||||
applicationScope,
|
||||
provider.events,
|
||||
provider.backendStatus,
|
||||
coordinator.errors,
|
||||
)
|
||||
|
||||
applicationScope.launch(ioDispatcher) { boostrapCoordinator.bootstrap() }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val _uiActive = MutableStateFlow(false)
|
||||
|
||||
val uiActive: StateFlow<Boolean>
|
||||
get() = _uiActive
|
||||
|
||||
fun setUiActive(active: Boolean) {
|
||||
_uiActive.update { active }
|
||||
}
|
||||
|
||||
@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
|
||||
}
|
||||
|
||||
-36
@@ -1,36 +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.tunnel.TunnelManager
|
||||
import com.zaneschepke.wireguardautotunnel.di.Scope
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import org.koin.core.qualifier.named
|
||||
|
||||
class KernelReceiver : BroadcastReceiver(), KoinComponent {
|
||||
|
||||
private val applicationScope: CoroutineScope by inject(named(Scope.APPLICATION))
|
||||
private val tunnelRepository: TunnelRepository by inject()
|
||||
private val tunnelManager: TunnelManager by inject()
|
||||
|
||||
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)
|
||||
tunnel?.let { tunnelRepository.save(it.copy(isActive = true)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val REFRESH_TUNNELS_ACTION = "com.wireguard.android.action.REFRESH_TUNNEL_STATES"
|
||||
}
|
||||
}
|
||||
+21
-11
@@ -3,11 +3,11 @@ 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.tunnel.TunnelManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.AutoTunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.di.Scope
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.component.KoinComponent
|
||||
@@ -17,20 +17,30 @@ import org.koin.core.qualifier.named
|
||||
|
||||
class NotificationActionReceiver : BroadcastReceiver(), KoinComponent {
|
||||
|
||||
private val tunnelManager: TunnelManager by inject()
|
||||
private val autoTunnelRepository: AutoTunnelSettingsRepository by inject()
|
||||
private val tunnelCoordinator: TunnelCoordinator by inject()
|
||||
|
||||
private val autoTunnelCoordinator: AutoTunnelCoordinator by inject()
|
||||
|
||||
private val applicationScope: CoroutineScope = get(named(Scope.APPLICATION))
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
|
||||
applicationScope.launch {
|
||||
when (intent.action) {
|
||||
NotificationAction.AUTO_TUNNEL_OFF.name ->
|
||||
autoTunnelRepository.updateAutoTunnelEnabled(false)
|
||||
NotificationAction.AUTO_TUNNEL_OFF.name -> {
|
||||
autoTunnelCoordinator.disable()
|
||||
}
|
||||
|
||||
NotificationAction.TUNNEL_OFF.name -> {
|
||||
val tunnelId = intent.getIntExtra(NotificationManager.EXTRA_ID, 0)
|
||||
if (tunnelId == STOP_ALL_TUNNELS_ID)
|
||||
return@launch tunnelManager.stopActiveTunnels()
|
||||
tunnelManager.stopTunnel(tunnelId)
|
||||
|
||||
val tunnelId =
|
||||
intent.getIntExtra(NotificationService.EXTRA_ID, STOP_ALL_TUNNELS_ID)
|
||||
|
||||
if (tunnelId == STOP_ALL_TUNNELS_ID) {
|
||||
tunnelCoordinator.stopActiveTunnels()
|
||||
return@launch
|
||||
}
|
||||
tunnelCoordinator.stopTunnel(tunnelId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+48
-29
@@ -3,9 +3,10 @@ package com.zaneschepke.wireguardautotunnel.core.broadcast
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.AutoTunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.di.Scope
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
@@ -14,15 +15,15 @@ import kotlinx.coroutines.launch
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import org.koin.core.qualifier.named
|
||||
import timber.log.Timber
|
||||
|
||||
class RemoteControlReceiver : BroadcastReceiver(), KoinComponent {
|
||||
|
||||
private val applicationScope: CoroutineScope by inject(named(Scope.APPLICATION))
|
||||
|
||||
private val settingsRepository: GeneralSettingRepository by inject()
|
||||
private val tunnelsRepository: TunnelRepository by inject()
|
||||
private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository by inject()
|
||||
private val tunnelManager: TunnelManager by inject()
|
||||
private val tunnelCoordinator: TunnelCoordinator by inject()
|
||||
private val autoTunnelCoordinator: AutoTunnelCoordinator by inject()
|
||||
|
||||
enum class Action(private val suffix: String) {
|
||||
START_TUNNEL("START_TUNNEL"),
|
||||
@@ -47,45 +48,63 @@ class RemoteControlReceiver : BroadcastReceiver(), KoinComponent {
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
Timber.i("onReceive")
|
||||
|
||||
val action = intent.action ?: return
|
||||
val appAction = Action.fromAction(action) ?: return Timber.w("Unknown action $action")
|
||||
val appAction = Action.fromAction(action) ?: return
|
||||
|
||||
applicationScope.launch {
|
||||
val settings = settingsRepository.getGeneralSettings()
|
||||
if (!settings.isRemoteControlEnabled) return@launch Timber.w("Remote control disabled")
|
||||
val key = settings.remoteKey ?: return@launch Timber.w("Remote control key missing")
|
||||
if (key != intent.getStringExtra(EXTRA_KEY)?.trim())
|
||||
return@launch Timber.w("Invalid remote control key")
|
||||
|
||||
if (!settings.isRemoteControlEnabled) return@launch
|
||||
|
||||
if (!validateKey(settings, intent)) return@launch
|
||||
|
||||
when (appAction) {
|
||||
Action.START_TUNNEL -> {
|
||||
val tunnelName =
|
||||
intent.getStringExtra(EXTRA_TUN_NAME) ?: return@launch startDefaultTunnel()
|
||||
val tunnel =
|
||||
tunnelsRepository.findByTunnelName(tunnelName)
|
||||
?: return@launch startDefaultTunnel()
|
||||
tunnelManager.startTunnel(tunnel)
|
||||
resolveTunnel(intent)
|
||||
?: tunnelsRepository.getDefaultTunnel()
|
||||
?: return@launch
|
||||
|
||||
tunnelCoordinator.startTunnel(tunnel)
|
||||
}
|
||||
|
||||
Action.STOP_TUNNEL -> {
|
||||
val tunnelName =
|
||||
intent.getStringExtra(EXTRA_TUN_NAME)
|
||||
?: return@launch tunnelManager.stopActiveTunnels()
|
||||
val tunnel =
|
||||
tunnelsRepository.findByTunnelName(tunnelName)
|
||||
?: return@launch tunnelManager.stopActiveTunnels()
|
||||
tunnelManager.stopTunnel(tunnel.id)
|
||||
val tunnelName = intent.getStringExtra(EXTRA_TUN_NAME)
|
||||
|
||||
if (tunnelName == null) {
|
||||
tunnelCoordinator.stopActiveTunnels()
|
||||
return@launch
|
||||
}
|
||||
|
||||
val tunnel = tunnelsRepository.findByTunnelName(tunnelName) ?: return@launch
|
||||
|
||||
tunnelCoordinator.stopTunnel(tunnel.id)
|
||||
}
|
||||
|
||||
Action.START_AUTO_TUNNEL -> {
|
||||
autoTunnelCoordinator.enable()
|
||||
}
|
||||
|
||||
Action.STOP_AUTO_TUNNEL -> {
|
||||
autoTunnelCoordinator.disable()
|
||||
}
|
||||
Action.START_AUTO_TUNNEL ->
|
||||
autoTunnelSettingsRepository.updateAutoTunnelEnabled(true)
|
||||
Action.STOP_AUTO_TUNNEL ->
|
||||
autoTunnelSettingsRepository.updateAutoTunnelEnabled(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun startDefaultTunnel() {
|
||||
tunnelsRepository.getDefaultTunnel()?.let { tunnel -> tunnelManager.startTunnel(tunnel) }
|
||||
private fun validateKey(settings: GeneralSettings, intent: Intent): Boolean {
|
||||
|
||||
val expected = settings.remoteKey?.trim() ?: return false
|
||||
|
||||
val actual = intent.getStringExtra(EXTRA_KEY)?.trim()
|
||||
|
||||
return expected == actual
|
||||
}
|
||||
|
||||
private suspend fun resolveTunnel(intent: Intent) =
|
||||
intent.getStringExtra(EXTRA_TUN_NAME)?.let { tunnelsRepository.findByTunnelName(it) }
|
||||
|
||||
companion object {
|
||||
const val EXTRA_TUN_NAME = "tunnelName"
|
||||
const val EXTRA_KEY = "key"
|
||||
|
||||
+4
-4
@@ -4,7 +4,7 @@ import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.zaneschepke.logcatter.LogReader
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.StartupCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.di.Scope
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -19,7 +19,7 @@ class RestartReceiver : BroadcastReceiver(), KoinComponent {
|
||||
|
||||
private val applicationScope: CoroutineScope = get(named(Scope.APPLICATION))
|
||||
|
||||
private val tunnelManager: TunnelManager by inject()
|
||||
private val startupCoordinator: StartupCoordinator by inject()
|
||||
|
||||
private val appStateRepository: AppStateRepository by inject()
|
||||
|
||||
@@ -32,11 +32,11 @@ class RestartReceiver : BroadcastReceiver(), KoinComponent {
|
||||
Intent.ACTION_BOOT_COMPLETED,
|
||||
"android.intent.action.QUICKBOOT_POWERON",
|
||||
"com.htc.intent.action.QUICKBOOT_POWERON" -> {
|
||||
tunnelManager.handleReboot()
|
||||
startupCoordinator.applyStartupPolicy()
|
||||
}
|
||||
Intent.ACTION_MY_PACKAGE_REPLACED -> {
|
||||
Timber.i("Restoring state on package upgrade")
|
||||
tunnelManager.handleRestore()
|
||||
startupCoordinator.applyStartupPolicy()
|
||||
logReader.deleteAndClearLogs()
|
||||
appStateRepository.setShouldShowDonationSnackbar(true)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.event
|
||||
|
||||
import com.zaneschepke.tunnel.util.BackendException
|
||||
|
||||
sealed interface TunnelErrorEvent {
|
||||
data class VpnPermissionDenied(val tunnelId: Int) : TunnelErrorEvent
|
||||
|
||||
data class StateConflict(val tunnelId: Int, val message: String) : TunnelErrorEvent
|
||||
|
||||
data class InternalFailure(val tunnelId: Int, val message: String) : TunnelErrorEvent
|
||||
|
||||
data class Socks5PortUnavailable(val tunnelId: Int, val port: Int) : TunnelErrorEvent
|
||||
|
||||
data class HttpPortUnavailable(val tunnelId: Int, val port: Int) : TunnelErrorEvent
|
||||
|
||||
companion object {
|
||||
fun from(throwable: Throwable, id: Int): TunnelErrorEvent {
|
||||
return when (throwable) {
|
||||
is BackendException.StateConflict -> {
|
||||
StateConflict(id, throwable.message)
|
||||
}
|
||||
is BackendException.Unauthorized -> {
|
||||
VpnPermissionDenied(id)
|
||||
}
|
||||
is BackendException.InternalError -> {
|
||||
InternalFailure(id, throwable.message)
|
||||
}
|
||||
is BackendException.Socks5PortUnavailable -> {
|
||||
Socks5PortUnavailable(id, throwable.port)
|
||||
}
|
||||
is BackendException.HttpPortUnavailable -> {
|
||||
HttpPortUnavailable(id, throwable.port)
|
||||
}
|
||||
else -> InternalFailure(id, throwable.message ?: "Unknown")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.event
|
||||
|
||||
import com.zaneschepke.tunnel.event.TunnelEvent
|
||||
import com.zaneschepke.tunnel.state.BackendStatus
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.TunnelNotificationService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
class TunnelEventDispatcher(private val notificationManager: TunnelNotificationService) {
|
||||
|
||||
fun bind(
|
||||
scope: CoroutineScope,
|
||||
providerEvents: Flow<TunnelEvent>,
|
||||
providerStatus: StateFlow<BackendStatus>,
|
||||
coordinatorErrors: Flow<TunnelErrorEvent>,
|
||||
) {
|
||||
|
||||
// informational events
|
||||
providerEvents
|
||||
.distinctUntilChanged()
|
||||
.onEach { event ->
|
||||
when (event) {
|
||||
is TunnelEvent.FallbackToIpv4 -> {
|
||||
notificationManager.showIpv4Fallback(event.tunnelId)
|
||||
}
|
||||
|
||||
is TunnelEvent.RecoveredToIpv6 -> {
|
||||
notificationManager.showIpv6Recovery(event.tunnelId)
|
||||
}
|
||||
|
||||
is TunnelEvent.DynamicDnsUpdate -> {
|
||||
notificationManager.showDynamicDnsUpdate(event.tunnelId)
|
||||
}
|
||||
|
||||
is TunnelEvent.NoRootShellAccess -> {
|
||||
notificationManager.showRootShellAccess()
|
||||
}
|
||||
}
|
||||
}
|
||||
.launchIn(scope)
|
||||
|
||||
// errors from the coordinator
|
||||
coordinatorErrors
|
||||
.distinctUntilChanged()
|
||||
.onEach { error ->
|
||||
when (error) {
|
||||
is TunnelErrorEvent.VpnPermissionDenied -> {
|
||||
notificationManager.showVpnRequired()
|
||||
}
|
||||
|
||||
is TunnelErrorEvent.StateConflict -> {
|
||||
notificationManager.showStateConflict(error.tunnelId)
|
||||
}
|
||||
|
||||
is TunnelErrorEvent.InternalFailure -> {
|
||||
notificationManager.showError(error.message)
|
||||
}
|
||||
|
||||
is TunnelErrorEvent.Socks5PortUnavailable -> {
|
||||
notificationManager.showSocks5PortUnavailable(error.port)
|
||||
}
|
||||
|
||||
is TunnelErrorEvent.HttpPortUnavailable -> {
|
||||
notificationManager.showHttpPortUnavailable(error.port)
|
||||
}
|
||||
}
|
||||
}
|
||||
.launchIn(scope)
|
||||
|
||||
// update persistent notification for services with the tunnel states
|
||||
providerStatus
|
||||
.map { it.activeTunnels }
|
||||
.distinctUntilChangedBy { map ->
|
||||
val stateSignature =
|
||||
map.entries
|
||||
.sortedBy { it.key }
|
||||
.map { (_, tunnel) -> tunnel.transportState to tunnel.bootstrapState }
|
||||
map.size to stateSignature
|
||||
}
|
||||
.onEach { status -> notificationManager.updatePersistentNotifications(status) }
|
||||
.launchIn(scope)
|
||||
}
|
||||
}
|
||||
+47
-42
@@ -3,26 +3,24 @@ package com.zaneschepke.wireguardautotunnel.core.notification
|
||||
import android.Manifest
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager.IMPORTANCE_LOW
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationCompat.Action
|
||||
import androidx.core.app.NotificationCompat.Builder
|
||||
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.core.notification.NotificationService.Companion.EXTRA_ID
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
|
||||
class WireGuardNotification(override val context: Context) : NotificationManager {
|
||||
|
||||
enum class NotificationChannels {
|
||||
VPN,
|
||||
AUTO_TUNNEL,
|
||||
}
|
||||
class AndroidNotificationService(override val context: Context) : NotificationService {
|
||||
|
||||
private val notificationManager = NotificationManagerCompat.from(context)
|
||||
|
||||
@@ -30,16 +28,15 @@ class WireGuardNotification(override val context: Context) : NotificationManager
|
||||
channel: NotificationChannels,
|
||||
title: String,
|
||||
subText: String?,
|
||||
actions: Collection<NotificationCompat.Action>,
|
||||
actions: Collection<Action>,
|
||||
description: String,
|
||||
showTimestamp: Boolean,
|
||||
importance: Int,
|
||||
onGoing: Boolean,
|
||||
onlyAlertOnce: Boolean,
|
||||
groupKey: String?,
|
||||
isGroupSummary: Boolean,
|
||||
): Notification {
|
||||
notificationManager.createNotificationChannel(channel.asChannel(importance))
|
||||
notificationManager.createNotificationChannel(channel.asChannel())
|
||||
return channel
|
||||
.asBuilder()
|
||||
.apply {
|
||||
@@ -58,7 +55,6 @@ class WireGuardNotification(override val context: Context) : NotificationManager
|
||||
setContentText(description)
|
||||
setOnlyAlertOnce(onlyAlertOnce)
|
||||
setOngoing(onGoing)
|
||||
setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
setShowWhen(showTimestamp)
|
||||
setSmallIcon(R.drawable.ic_notification)
|
||||
if (groupKey != null) {
|
||||
@@ -75,10 +71,9 @@ class WireGuardNotification(override val context: Context) : NotificationManager
|
||||
channel: NotificationChannels,
|
||||
title: StringValue,
|
||||
subText: String?,
|
||||
actions: Collection<NotificationCompat.Action>,
|
||||
actions: Collection<Action>,
|
||||
description: StringValue,
|
||||
showTimestamp: Boolean,
|
||||
importance: Int,
|
||||
onGoing: Boolean,
|
||||
onlyAlertOnce: Boolean,
|
||||
groupKey: String?,
|
||||
@@ -91,16 +86,17 @@ class WireGuardNotification(override val context: Context) : NotificationManager
|
||||
actions,
|
||||
description.asString(context),
|
||||
showTimestamp,
|
||||
importance,
|
||||
onGoing,
|
||||
onlyAlertOnce,
|
||||
groupKey,
|
||||
isGroupSummary,
|
||||
)
|
||||
}
|
||||
|
||||
override fun createNotificationAction(
|
||||
notificationAction: NotificationAction,
|
||||
extraId: Int?,
|
||||
): NotificationCompat.Action {
|
||||
): Action {
|
||||
val pendingIntent =
|
||||
PendingIntent.getBroadcast(
|
||||
context,
|
||||
@@ -139,40 +135,49 @@ class WireGuardNotification(override val context: Context) : NotificationManager
|
||||
|
||||
private fun NotificationChannels.asBuilder(): NotificationCompat.Builder {
|
||||
return when (this) {
|
||||
NotificationChannels.AUTO_TUNNEL -> {
|
||||
NotificationCompat.Builder(
|
||||
context,
|
||||
context.getString(R.string.auto_tunnel_channel_id),
|
||||
)
|
||||
}
|
||||
NotificationChannels.AUTO_TUNNEL ->
|
||||
Builder(context, context.getString(R.string.auto_tunnel_channel_id))
|
||||
NotificationChannels.VPN -> Builder(context, context.getString(R.string.vpn_channel_id))
|
||||
|
||||
NotificationChannels.VPN -> {
|
||||
NotificationCompat.Builder(context, context.getString(R.string.vpn_channel_id))
|
||||
}
|
||||
NotificationChannels.PROXY ->
|
||||
Builder(context, context.getString(R.string.proxy_channel_id))
|
||||
}
|
||||
}
|
||||
|
||||
private fun NotificationChannels.asChannel(importance: Int): NotificationChannel {
|
||||
return when (this) {
|
||||
NotificationChannels.VPN -> {
|
||||
NotificationChannel(
|
||||
context.getString(R.string.vpn_channel_id),
|
||||
context.getString(R.string.vpn_channel_name),
|
||||
importance,
|
||||
)
|
||||
.apply { description = context.getString(R.string.vpn_channel_description) }
|
||||
}
|
||||
enum class NotificationChannels(val channelId: Int, val importance: Int) {
|
||||
VPN(R.string.vpn_channel_id, IMPORTANCE_LOW),
|
||||
AUTO_TUNNEL(R.string.auto_tunnel_channel_id, IMPORTANCE_LOW),
|
||||
PROXY(R.string.proxy_channel_id, IMPORTANCE_LOW),
|
||||
}
|
||||
|
||||
NotificationChannels.AUTO_TUNNEL -> {
|
||||
NotificationChannel(
|
||||
context.getString(R.string.auto_tunnel_channel_id),
|
||||
context.getString(R.string.auto_tunnel_channel_name),
|
||||
importance,
|
||||
)
|
||||
.apply {
|
||||
description = context.getString(R.string.auto_tunnel_channel_description)
|
||||
fun NotificationChannels.asChannel(): NotificationChannel {
|
||||
return NotificationChannel(
|
||||
context.getString(channelId),
|
||||
context.getString(
|
||||
when (this) {
|
||||
NotificationChannels.VPN -> R.string.vpn
|
||||
NotificationChannels.AUTO_TUNNEL -> R.string.auto_tunnel
|
||||
NotificationChannels.PROXY -> R.string.proxy
|
||||
}
|
||||
),
|
||||
importance,
|
||||
)
|
||||
.apply {
|
||||
description =
|
||||
context.getString(
|
||||
when (this@asChannel) {
|
||||
NotificationChannels.VPN -> R.string.vpn_channel_description
|
||||
NotificationChannels.AUTO_TUNNEL ->
|
||||
R.string.auto_tunnel_channel_description
|
||||
NotificationChannels.PROXY -> R.string.proxy_channel_description
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun createAllChannels() {
|
||||
NotificationChannels.entries.forEach { channel ->
|
||||
notificationManager.createNotificationChannel(channel.asChannel())
|
||||
}
|
||||
}
|
||||
}
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.notification
|
||||
|
||||
import com.zaneschepke.tunnel.model.BackendMode
|
||||
import com.zaneschepke.tunnel.state.ActiveTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.AndroidNotificationService.NotificationChannels
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.PROXY_GROUP_KEY
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.PROXY_NOTIFICATION_ID
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.TUNNEL_ERROR_NOTIFICATION_ID
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.TUNNEL_MESSAGES_NOTIFICATION_ID
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.VPN_GROUP_KEY
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.VPN_NOTIFICATION_ID
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState
|
||||
|
||||
class AndroidTunnelNotificationService(
|
||||
private val notificationService: NotificationService,
|
||||
private val tunnelRepository: TunnelRepository,
|
||||
) : TunnelNotificationService {
|
||||
|
||||
override suspend fun updatePersistentNotifications(activeTunnels: Map<Int, ActiveTunnel>) {
|
||||
|
||||
val vpnTunnels = activeTunnels.filterValues { it.mode is BackendMode.Vpn }
|
||||
|
||||
val proxyTunnels = activeTunnels.filterValues { it.mode is BackendMode.Proxy }
|
||||
|
||||
updateGroupNotification(
|
||||
tunnels = vpnTunnels,
|
||||
notificationId = VPN_NOTIFICATION_ID,
|
||||
channel = NotificationChannels.VPN,
|
||||
groupKey = VPN_GROUP_KEY,
|
||||
)
|
||||
|
||||
updateGroupNotification(
|
||||
tunnels = proxyTunnels,
|
||||
notificationId = PROXY_NOTIFICATION_ID,
|
||||
channel = NotificationChannels.PROXY,
|
||||
groupKey = PROXY_GROUP_KEY,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun updateGroupNotification(
|
||||
tunnels: Map<Int, ActiveTunnel>,
|
||||
notificationId: Int,
|
||||
channel: NotificationChannels,
|
||||
groupKey: String,
|
||||
) {
|
||||
|
||||
if (tunnels.isEmpty()) {
|
||||
notificationService.remove(notificationId)
|
||||
return
|
||||
}
|
||||
|
||||
val context = notificationService.context
|
||||
|
||||
val lines = tunnels.mapNotNull { (id, activeTunnel) ->
|
||||
val tunnel = tunnelRepository.getById(id) ?: return@mapNotNull null
|
||||
val display = DisplayTunnelState.from(activeTunnel)
|
||||
|
||||
context.getString(
|
||||
R.string.notification_tunnel_status_format,
|
||||
tunnel.name,
|
||||
display.asLocalizedString(context),
|
||||
)
|
||||
}
|
||||
|
||||
val description = lines.joinToString("\n")
|
||||
|
||||
val stopActions =
|
||||
tunnels.keys.map {
|
||||
notificationService.createNotificationAction(
|
||||
notificationAction = NotificationAction.TUNNEL_OFF,
|
||||
extraId = it,
|
||||
)
|
||||
}
|
||||
|
||||
val title =
|
||||
when (channel) {
|
||||
NotificationChannels.VPN -> context.getString(R.string.vpn)
|
||||
|
||||
NotificationChannels.PROXY -> context.getString(R.string.proxy)
|
||||
|
||||
NotificationChannels.AUTO_TUNNEL -> context.getString(R.string.auto_tunnel)
|
||||
}
|
||||
|
||||
val notification =
|
||||
notificationService.createNotification(
|
||||
channel = channel,
|
||||
title = title,
|
||||
description = description,
|
||||
actions = stopActions,
|
||||
onGoing = true,
|
||||
onlyAlertOnce = true,
|
||||
groupKey = groupKey,
|
||||
)
|
||||
|
||||
notificationService.show(notificationId, notification)
|
||||
}
|
||||
|
||||
override suspend fun showIpv4Fallback(tunnelId: Int) {
|
||||
|
||||
val context = notificationService.context
|
||||
val name = tunnelName(tunnelId)
|
||||
|
||||
showMessage(
|
||||
title = context.getString(R.string.ipv4_fallback),
|
||||
message = context.getString(R.string.notification_ipv4_fallback_message, name),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun showIpv6Recovery(tunnelId: Int) {
|
||||
|
||||
val context = notificationService.context
|
||||
val name = tunnelName(tunnelId)
|
||||
|
||||
showMessage(
|
||||
title = context.getString(R.string.ipv6_recovery),
|
||||
message = context.getString(R.string.notification_ipv6_recovery_message, name),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun showDynamicDnsUpdate(tunnelId: Int) {
|
||||
|
||||
val context = notificationService.context
|
||||
val name = tunnelName(tunnelId)
|
||||
|
||||
showMessage(
|
||||
title = context.getString(R.string.dynamic_dns_update),
|
||||
message = context.getString(R.string.notification_dynamic_dns_message, name),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun showVpnRequired() {
|
||||
|
||||
showError(notificationService.context.getString(R.string.vpn_permission_required))
|
||||
}
|
||||
|
||||
override suspend fun showStateConflict(tunnelId: Int) {
|
||||
|
||||
val context = notificationService.context
|
||||
val name = tunnelName(tunnelId)
|
||||
|
||||
showError(context.getString(R.string.notification_tunnel_already_running, name))
|
||||
}
|
||||
|
||||
override suspend fun showRootShellAccess() {
|
||||
// TODO could improve with fix action
|
||||
val context = notificationService.context
|
||||
showError(context.getString(R.string.error_root_denied))
|
||||
}
|
||||
|
||||
override suspend fun showSocks5PortUnavailable(port: Int) {
|
||||
val context = notificationService.context
|
||||
val message = context.getString(R.string.error_socks5_port_unavailable, port)
|
||||
|
||||
showError(message)
|
||||
}
|
||||
|
||||
override suspend fun showHttpPortUnavailable(port: Int) {
|
||||
val context = notificationService.context
|
||||
val message = context.getString(R.string.error_http_port_unavailable, port)
|
||||
|
||||
showError(message)
|
||||
}
|
||||
|
||||
override suspend fun showError(message: String) {
|
||||
|
||||
val notification =
|
||||
notificationService.createNotification(
|
||||
channel = NotificationChannels.VPN,
|
||||
title = notificationService.context.getString(R.string.error),
|
||||
description = message,
|
||||
onGoing = false,
|
||||
onlyAlertOnce = true,
|
||||
groupKey = VPN_GROUP_KEY,
|
||||
)
|
||||
|
||||
notificationService.show(TUNNEL_ERROR_NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
private fun showMessage(title: String, message: String) {
|
||||
|
||||
val notification =
|
||||
notificationService.createNotification(
|
||||
channel = NotificationChannels.VPN,
|
||||
title = title,
|
||||
description = message,
|
||||
onGoing = false,
|
||||
onlyAlertOnce = true,
|
||||
groupKey = VPN_GROUP_KEY,
|
||||
)
|
||||
|
||||
notificationService.show(TUNNEL_MESSAGES_NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
private suspend fun tunnelName(id: Int): String {
|
||||
|
||||
val context = notificationService.context
|
||||
|
||||
return tunnelRepository.getById(id)?.name ?: context.getString(R.string.unknown, id)
|
||||
}
|
||||
}
|
||||
-61
@@ -1,61 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.notification
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class NotificationMonitor(
|
||||
private val tunnelManager: TunnelManager,
|
||||
private val notificationManager: NotificationManager,
|
||||
) {
|
||||
suspend fun handleApplicationNotifications() = coroutineScope {
|
||||
launch { handleTunnelErrors() }
|
||||
launch { handleTunnelMessages() }
|
||||
}
|
||||
|
||||
private suspend fun handleTunnelErrors() =
|
||||
tunnelManager.errorEvents.collectLatest { (tunName, error) ->
|
||||
if (!WireGuardAutoTunnel.uiActive.value) {
|
||||
val notification =
|
||||
notificationManager.createNotification(
|
||||
WireGuardNotification.NotificationChannels.VPN,
|
||||
title =
|
||||
tunName?.let { StringValue.DynamicString(it) }
|
||||
?: StringValue.StringResource(R.string.tunnel),
|
||||
description =
|
||||
StringValue.StringResource(
|
||||
R.string.tunnel_error_template,
|
||||
error.stringRes,
|
||||
),
|
||||
groupKey = NotificationManager.VPN_GROUP_KEY,
|
||||
)
|
||||
notificationManager.show(
|
||||
NotificationManager.TUNNEL_ERROR_NOTIFICATION_ID,
|
||||
notification,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleTunnelMessages() =
|
||||
tunnelManager.messageEvents.collectLatest { (tunName, message) ->
|
||||
if (!WireGuardAutoTunnel.uiActive.value) {
|
||||
val notification =
|
||||
notificationManager.createNotification(
|
||||
WireGuardNotification.NotificationChannels.VPN,
|
||||
title =
|
||||
tunName?.let { StringValue.DynamicString(it) }
|
||||
?: StringValue.StringResource(R.string.tunnel),
|
||||
description = message.toStringValue(),
|
||||
groupKey = NotificationManager.VPN_GROUP_KEY,
|
||||
)
|
||||
notificationManager.show(
|
||||
NotificationManager.TUNNEL_MESSAGES_NOTIFICATION_ID,
|
||||
notification,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
-5
@@ -1,14 +1,13 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.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.core.notification.AndroidNotificationService.NotificationChannels
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
|
||||
interface NotificationManager {
|
||||
interface NotificationService {
|
||||
val context: Context
|
||||
|
||||
fun createNotification(
|
||||
@@ -18,7 +17,6 @@ interface NotificationManager {
|
||||
actions: Collection<NotificationCompat.Action> = emptyList(),
|
||||
description: String = "",
|
||||
showTimestamp: Boolean = true,
|
||||
importance: Int = NotificationManager.IMPORTANCE_LOW,
|
||||
onGoing: Boolean = false,
|
||||
onlyAlertOnce: Boolean = true,
|
||||
groupKey: String? = null,
|
||||
@@ -32,13 +30,14 @@ interface NotificationManager {
|
||||
actions: Collection<NotificationCompat.Action> = emptyList(),
|
||||
description: StringValue,
|
||||
showTimestamp: Boolean = true,
|
||||
importance: Int = NotificationManager.IMPORTANCE_LOW,
|
||||
onGoing: Boolean = false,
|
||||
onlyAlertOnce: Boolean = true,
|
||||
groupKey: String? = null,
|
||||
isGroupSummary: Boolean = false,
|
||||
): Notification
|
||||
|
||||
fun createAllChannels()
|
||||
|
||||
fun createNotificationAction(
|
||||
notificationAction: NotificationAction,
|
||||
extraId: Int? = null,
|
||||
@@ -50,6 +49,7 @@ interface NotificationManager {
|
||||
|
||||
companion object {
|
||||
const val VPN_GROUP_KEY = "VPN_GROUP"
|
||||
const val PROXY_GROUP_KEY = "PROXY_GROUP"
|
||||
const val AUTO_TUNNEL_GROUP_KEY = "AUTO_TUNNEL_GROUP"
|
||||
const val AUTO_TUNNEL_LOCATION_PERMISSION_ID = 123
|
||||
const val AUTO_TUNNEL_LOCATION_SERVICES_ID = 124
|
||||
@@ -57,6 +57,7 @@ interface NotificationManager {
|
||||
const val AUTO_TUNNEL_NOTIFICATION_ID = 122
|
||||
// for tunnel foreground notification
|
||||
const val VPN_NOTIFICATION_ID = 100
|
||||
const val PROXY_NOTIFICATION_ID = 103
|
||||
const val TUNNEL_ERROR_NOTIFICATION_ID = 101
|
||||
const val TUNNEL_MESSAGES_NOTIFICATION_ID = 102
|
||||
const val EXTRA_ID = "id"
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.notification
|
||||
|
||||
import com.zaneschepke.tunnel.state.ActiveTunnel
|
||||
|
||||
interface TunnelNotificationService {
|
||||
|
||||
suspend fun updatePersistentNotifications(activeTunnels: Map<Int, ActiveTunnel>)
|
||||
|
||||
suspend fun showIpv4Fallback(tunnelId: Int)
|
||||
|
||||
suspend fun showIpv6Recovery(tunnelId: Int)
|
||||
|
||||
suspend fun showDynamicDnsUpdate(tunnelId: Int)
|
||||
|
||||
suspend fun showVpnRequired()
|
||||
|
||||
suspend fun showStateConflict(tunnelId: Int)
|
||||
|
||||
suspend fun showSocks5PortUnavailable(port: Int)
|
||||
|
||||
suspend fun showHttpPortUnavailable(port: Int)
|
||||
|
||||
suspend fun showRootShellAccess()
|
||||
|
||||
suspend fun showError(message: String)
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.orchestration
|
||||
|
||||
import com.zaneschepke.logcatter.LogReader
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.DnsSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.LockdownSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
class AppBoostrapCoordinator(
|
||||
private val monitoringRepository: MonitoringSettingsRepository,
|
||||
private val settingsRepository: GeneralSettingRepository,
|
||||
private val dnsRepository: DnsSettingsRepository,
|
||||
private val tunnelRepository: TunnelRepository,
|
||||
private val lockdownRepository: LockdownSettingsRepository,
|
||||
private val tunnelProvider: TunnelProvider,
|
||||
private val dnsSettingsCoordinator: DnsSettingsCoordinator,
|
||||
private val logReader: LogReader,
|
||||
) {
|
||||
|
||||
private val _isReady = MutableStateFlow(false)
|
||||
val isReady: StateFlow<Boolean> = _isReady.asStateFlow()
|
||||
|
||||
suspend fun bootstrap() = coroutineScope {
|
||||
launch { bootstrapLogging() }
|
||||
|
||||
val criticalTasks =
|
||||
listOf(
|
||||
async { bootstrapDns() },
|
||||
async { ensureGlobalConfig() },
|
||||
async { restoreLockdown() },
|
||||
)
|
||||
|
||||
try {
|
||||
criticalTasks.awaitAll()
|
||||
_isReady.value = true
|
||||
Timber.d("App bootstrap completed successfully")
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "One or more critical bootstrap tasks failed")
|
||||
_isReady.value = true
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun bootstrapDns() {
|
||||
val dnsSettings = dnsRepository.getDnsSettings()
|
||||
dnsSettingsCoordinator.appyDnsSettings(dnsSettings)
|
||||
}
|
||||
|
||||
private suspend fun bootstrapLogging() {
|
||||
monitoringRepository.flow
|
||||
.distinctUntilChangedBy { it.isLocalLogsEnabled }
|
||||
.collect { settings ->
|
||||
if (settings.isLocalLogsEnabled) {
|
||||
logReader.start()
|
||||
} else {
|
||||
logReader.stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun ensureGlobalConfig() {
|
||||
tunnelRepository.ensureGlobalConfigExists()
|
||||
}
|
||||
|
||||
private suspend fun restoreLockdown() {
|
||||
val settings = settingsRepository.getGeneralSettings()
|
||||
|
||||
when (settings.tunnelMode) {
|
||||
TunnelMode.LOCK_DOWN -> {
|
||||
val lockdownSettings = lockdownRepository.getLockdownSettings()
|
||||
tunnelProvider.setLockDown(lockdownSettings).onFailure {
|
||||
Timber.w(it, "Failed to restore lockdown/kill-switch on startup")
|
||||
}
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.orchestration
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelStateHolder
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
||||
|
||||
class AutoTunnelCoordinator(
|
||||
private val repository: AutoTunnelSettingsRepository,
|
||||
private val serviceManager: ServiceManager,
|
||||
private val autoTunnelStateHolder: AutoTunnelStateHolder,
|
||||
) {
|
||||
|
||||
suspend fun shouldRestore(): Boolean {
|
||||
val settings = repository.getAutoTunnelSettings()
|
||||
return settings.startOnBoot && settings.isAutoTunnelEnabled
|
||||
}
|
||||
|
||||
fun start() {
|
||||
serviceManager.startAutoTunnelService()
|
||||
}
|
||||
|
||||
suspend fun enable() {
|
||||
repository.updateAutoTunnelEnabled(true)
|
||||
serviceManager.startAutoTunnelService()
|
||||
}
|
||||
|
||||
suspend fun toggle() {
|
||||
val running = autoTunnelStateHolder.active.value
|
||||
if (running) {
|
||||
disable()
|
||||
} else enable()
|
||||
}
|
||||
|
||||
suspend fun disable() {
|
||||
repository.updateAutoTunnelEnabled(false)
|
||||
serviceManager.stopAutoTunnelService()
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.orchestration
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.parser.Config
|
||||
import com.zaneschepke.wireguardautotunnel.parser.InterfaceSection
|
||||
|
||||
object ConfigReconciler {
|
||||
private fun mergeInterface(
|
||||
base: InterfaceSection,
|
||||
global: InterfaceSection,
|
||||
policy: ConfigReconcilePolicy,
|
||||
): InterfaceSection {
|
||||
return base.copy(
|
||||
dns = if (policy.dns) global.dns else base.dns,
|
||||
includedApplications =
|
||||
if (policy.splitTunnel) global.includedApplications else base.includedApplications,
|
||||
excludedApplications =
|
||||
if (policy.splitTunnel) global.excludedApplications else base.excludedApplications,
|
||||
jC = if (policy.amnezia) global.jC else base.jC,
|
||||
jMin = if (policy.amnezia) global.jMin else base.jMin,
|
||||
jMax = if (policy.amnezia) global.jMax else base.jMax,
|
||||
s1 = if (policy.amnezia) global.s1 else base.s1,
|
||||
s2 = if (policy.amnezia) global.s2 else base.s2,
|
||||
s3 = if (policy.amnezia) global.s3 else base.s3,
|
||||
s4 = if (policy.amnezia) global.s4 else base.s4,
|
||||
h1 = if (policy.amnezia) global.h1 else base.h1,
|
||||
h2 = if (policy.amnezia) global.h2 else base.h2,
|
||||
h3 = if (policy.amnezia) global.h3 else base.h3,
|
||||
h4 = if (policy.amnezia) global.h4 else base.h4,
|
||||
i1 = if (policy.amnezia) global.i1 else base.i1,
|
||||
i2 = if (policy.amnezia) global.i2 else base.i2,
|
||||
i3 = if (policy.amnezia) global.i3 else base.i3,
|
||||
i4 = if (policy.amnezia) global.i4 else base.i4,
|
||||
i5 = if (policy.amnezia) global.i5 else base.i5,
|
||||
)
|
||||
}
|
||||
|
||||
fun reconcileConfig(base: Config, global: Config?, policy: ConfigReconcilePolicy): Config {
|
||||
if (global == null) return base
|
||||
if (!policy.hasAnyOverrides) return base
|
||||
|
||||
return base.copy(`interface` = mergeInterface(base.`interface`, global.`interface`, policy))
|
||||
}
|
||||
|
||||
data class ConfigReconcilePolicy(
|
||||
val dns: Boolean,
|
||||
val splitTunnel: Boolean,
|
||||
val amnezia: Boolean,
|
||||
) {
|
||||
val hasAnyOverrides
|
||||
get() = dns || splitTunnel || amnezia
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.orchestration
|
||||
|
||||
import com.zaneschepke.tunnel.backend.Backend
|
||||
import com.zaneschepke.tunnel.model.DnsBoostrapConfig
|
||||
import com.zaneschepke.tunnel.model.DnsBoostrapMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.DnsProtocol
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.DnsSettings
|
||||
|
||||
class DnsSettingsCoordinator(private val backend: Backend) {
|
||||
|
||||
suspend fun appyDnsSettings(dnsSettings: DnsSettings) {
|
||||
val mode =
|
||||
when (dnsSettings.dnsProtocol) {
|
||||
DnsProtocol.SYSTEM -> DnsBoostrapMode.System
|
||||
DnsProtocol.DOH ->
|
||||
DnsBoostrapMode.Custom(DnsBoostrapConfig.DoH(dnsSettings.dnsEndpoint))
|
||||
DnsProtocol.DOT ->
|
||||
DnsBoostrapMode.Custom(DnsBoostrapConfig.DoT(dnsSettings.dnsEndpoint))
|
||||
DnsProtocol.UDP ->
|
||||
DnsBoostrapMode.Custom(DnsBoostrapConfig.Plain(dnsSettings.dnsEndpoint))
|
||||
}
|
||||
|
||||
backend.setBootstrapDnsMode(mode)
|
||||
}
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.orchestration
|
||||
|
||||
import android.content.Intent
|
||||
import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutContract
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
|
||||
class ShortcutCoordinator(
|
||||
private val settingsRepository: GeneralSettingRepository,
|
||||
private val tunnelsRepository: TunnelRepository,
|
||||
private val tunnelCoordinator: TunnelCoordinator,
|
||||
private val autoTunnelCoordinator: AutoTunnelCoordinator,
|
||||
) {
|
||||
|
||||
suspend fun handle(intent: Intent) {
|
||||
|
||||
val settings = settingsRepository.getGeneralSettings()
|
||||
|
||||
if (!settings.isShortcutsEnabled) return
|
||||
|
||||
val shortcutType =
|
||||
intent.getStringExtra(ShortcutContract.EXTRA_SHORTCUT_TYPE)
|
||||
?: legacyShortcutType(intent)
|
||||
|
||||
when (shortcutType) {
|
||||
ShortcutContract.ShortcutType.TUNNEL.value -> {
|
||||
handleTunnelShortcut(intent)
|
||||
}
|
||||
|
||||
ShortcutContract.ShortcutType.AUTO_TUNNEL.value -> {
|
||||
handleAutoTunnelShortcut(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleAutoTunnelShortcut(intent: Intent) {
|
||||
|
||||
when (intent.action) {
|
||||
ShortcutContract.Action.START.name -> {
|
||||
autoTunnelCoordinator.enable()
|
||||
}
|
||||
|
||||
ShortcutContract.Action.STOP.name -> {
|
||||
autoTunnelCoordinator.disable()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun legacyShortcutType(intent: Intent): String? {
|
||||
|
||||
return when (intent.getStringExtra(ShortcutContract.EXTRA_CLASS_NAME)) {
|
||||
ShortcutContract.Legacy.AUTO_TUNNEL_SERVICE_CLASS_NAME,
|
||||
ShortcutContract.Legacy.AUTO_TUNNEL_SERVICE_NAME ->
|
||||
ShortcutContract.ShortcutType.AUTO_TUNNEL.value
|
||||
|
||||
ShortcutContract.Legacy.TUNNEL_PROVIDER_NAME,
|
||||
ShortcutContract.Legacy.TUNNEL_SERVICE_NAME ->
|
||||
ShortcutContract.ShortcutType.TUNNEL.value
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleTunnelShortcut(intent: Intent) {
|
||||
|
||||
val tunnelName = intent.getStringExtra(ShortcutContract.EXTRA_TUNNEL_NAME)
|
||||
|
||||
val tunnel =
|
||||
tunnelName?.let { tunnelsRepository.findByTunnelName(it) }
|
||||
?: tunnelsRepository.getDefaultTunnel()
|
||||
|
||||
tunnel ?: return
|
||||
|
||||
when (intent.action) {
|
||||
ShortcutContract.Action.START.name -> {
|
||||
tunnelCoordinator.startTunnel(config = tunnel)
|
||||
}
|
||||
|
||||
ShortcutContract.Action.STOP.name -> {
|
||||
tunnelCoordinator.stopActiveTunnels()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.orchestration
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
class StartupCoordinator(
|
||||
private val tunnelCoordinator: TunnelCoordinator,
|
||||
private val settingsRepository: GeneralSettingRepository,
|
||||
private val autoTunnelCoordinator: AutoTunnelCoordinator,
|
||||
private val tunnelRepository: TunnelRepository,
|
||||
private val bootstrapCoordinator: AppBoostrapCoordinator,
|
||||
) {
|
||||
|
||||
suspend fun applyStartupPolicy(): Result<Unit> = runCatching {
|
||||
val shouldRestoreAutoTunnel = autoTunnelCoordinator.shouldRestore()
|
||||
val settings = settingsRepository.getGeneralSettings()
|
||||
val shouldRestoreDefaultTunnel = settings.isRestoreOnBootEnabled
|
||||
|
||||
if (shouldRestoreAutoTunnel || shouldRestoreDefaultTunnel) {
|
||||
// Wait for app critical bootstrap to finish
|
||||
bootstrapCoordinator.isReady.first { it }
|
||||
} else {
|
||||
return Result.success(Unit)
|
||||
}
|
||||
|
||||
if (shouldRestoreAutoTunnel) {
|
||||
autoTunnelCoordinator.start()
|
||||
return Result.success(Unit)
|
||||
}
|
||||
|
||||
val defaultTunnel = tunnelRepository.getDefaultTunnel() ?: return Result.success(Unit)
|
||||
tunnelCoordinator.startTunnel(defaultTunnel)
|
||||
return Result.success(Unit)
|
||||
}
|
||||
}
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.orchestration
|
||||
|
||||
import com.zaneschepke.tunnel.model.BackendMode
|
||||
import com.zaneschepke.wireguardautotunnel.core.event.TunnelErrorEvent
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.RoomDnsSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelActionSource
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.TunnelActionEvent
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.DnsSettings
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.MonitoringSettings
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.ProxySettings
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.ProxySettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
class TunnelCoordinator(
|
||||
private val tunnelProvider: TunnelProvider,
|
||||
private val serviceManager: ServiceManager,
|
||||
private val bootstrapCoordinator: AppBoostrapCoordinator,
|
||||
settingsRepository: GeneralSettingRepository,
|
||||
private val tunnelRepository: TunnelRepository,
|
||||
dnsSettingsRepository: RoomDnsSettingsRepository,
|
||||
monitoringSettingsRepository: MonitoringSettingsRepository,
|
||||
proxyRepository: ProxySettingsRepository,
|
||||
scope: CoroutineScope,
|
||||
) {
|
||||
|
||||
data class RuntimeSettingsSnapshot(
|
||||
val general: GeneralSettings,
|
||||
val dns: DnsSettings,
|
||||
val monitoring: MonitoringSettings,
|
||||
val proxy: ProxySettings,
|
||||
)
|
||||
|
||||
private val runtimeSettingsSnapshot =
|
||||
combine(
|
||||
settingsRepository.flow,
|
||||
dnsSettingsRepository.flow,
|
||||
monitoringSettingsRepository.flow,
|
||||
proxyRepository.flow,
|
||||
) { general, dns, monitoring, proxy ->
|
||||
RuntimeSettingsSnapshot(
|
||||
general = general,
|
||||
dns = dns,
|
||||
monitoring = monitoring,
|
||||
proxy = proxy,
|
||||
)
|
||||
}
|
||||
|
||||
private val _actions = MutableSharedFlow<TunnelActionEvent>()
|
||||
val actions = _actions.asSharedFlow()
|
||||
|
||||
private val runtimeSettingsSnapshotState =
|
||||
runtimeSettingsSnapshot.stateIn(
|
||||
scope = scope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = null,
|
||||
)
|
||||
|
||||
private suspend fun getSnapshot(): RuntimeSettingsSnapshot {
|
||||
return runtimeSettingsSnapshotState.filterNotNull().first()
|
||||
}
|
||||
|
||||
private var lastActiveTunnels: List<Int> = emptyList()
|
||||
private val tunnelMutex = Mutex()
|
||||
private val _errors = MutableSharedFlow<TunnelErrorEvent>()
|
||||
val errors = _errors.asSharedFlow()
|
||||
|
||||
val backendStatus = tunnelProvider.backendStatus
|
||||
|
||||
suspend fun startTunnel(
|
||||
config: TunnelConfig,
|
||||
source: TunnelActionSource = TunnelActionSource.USER,
|
||||
) = tunnelMutex.withLock {
|
||||
// wait for app to be bootstrapped
|
||||
bootstrapCoordinator.isReady.first { it }
|
||||
startTunnelInternal(config, source)
|
||||
}
|
||||
|
||||
suspend fun stopTunnel(id: Int, source: TunnelActionSource = TunnelActionSource.USER) =
|
||||
tunnelMutex.withLock {
|
||||
stopTunnelInternal(id, source)
|
||||
}
|
||||
|
||||
suspend fun stopActiveTunnels() = tunnelMutex.withLock { stopActiveTunnelsInternal() }
|
||||
|
||||
private suspend fun startTunnelInternal(
|
||||
tunnelConfig: TunnelConfig,
|
||||
source: TunnelActionSource,
|
||||
) {
|
||||
|
||||
val snapshot = getSnapshot()
|
||||
val settings = snapshot.general
|
||||
val dnsSettings = snapshot.dns
|
||||
val proxySettings = snapshot.proxy
|
||||
val monitoringSettings = snapshot.monitoring
|
||||
|
||||
val config = tunnelConfig.getConfig()
|
||||
val policy =
|
||||
ConfigReconciler.ConfigReconcilePolicy(
|
||||
dnsSettings.isGlobalTunnelDnsEnabled,
|
||||
settings.isGlobalSplitTunnelEnabled,
|
||||
settings.isGlobalAmneziaEnabled,
|
||||
)
|
||||
|
||||
val runConfig =
|
||||
if (policy.hasAnyOverrides) {
|
||||
val globalConfig = tunnelRepository.globalTunnelFlow.firstOrNull()?.getConfig()
|
||||
ConfigReconciler.reconcileConfig(config, globalConfig, policy)
|
||||
} else config
|
||||
|
||||
val backendMode =
|
||||
when (settings.tunnelMode) {
|
||||
TunnelMode.VPN -> {
|
||||
|
||||
if (!serviceManager.hasVpnPermission()) {
|
||||
_errors.emit(TunnelErrorEvent.VpnPermissionDenied(tunnelConfig.id))
|
||||
return
|
||||
}
|
||||
|
||||
BackendMode.Vpn(runConfig)
|
||||
}
|
||||
|
||||
TunnelMode.PROXY -> {
|
||||
BackendMode.Proxy.Standard(
|
||||
config = runConfig,
|
||||
proxyConfig = proxySettings.toProxyConfig(),
|
||||
)
|
||||
}
|
||||
|
||||
TunnelMode.LOCK_DOWN -> {
|
||||
|
||||
BackendMode.Proxy.KillSwitchPrimary(runConfig)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO for now, enforce single tunnel until multi-tunneling is implement
|
||||
stopActiveTunnelsInternal()
|
||||
|
||||
tunnelProvider
|
||||
.startTunnel(
|
||||
tunnel =
|
||||
tunnelConfig.toBackendTunnel(
|
||||
monitoringSettings,
|
||||
settings.tunnelScriptingEnabled,
|
||||
),
|
||||
mode = backendMode,
|
||||
)
|
||||
.onSuccess {
|
||||
_actions.emit(
|
||||
TunnelActionEvent.Started(tunnelId = tunnelConfig.id, source = source)
|
||||
)
|
||||
}
|
||||
.onFailure { _errors.emit(TunnelErrorEvent.from(it, tunnelConfig.id)) }
|
||||
}
|
||||
|
||||
suspend fun startDefault() {
|
||||
tunnelRepository.getDefaultTunnel()?.let { tunnel -> startTunnel(tunnel) }
|
||||
}
|
||||
|
||||
suspend fun toggleTunnels(source: TunnelActionSource = TunnelActionSource.USER) =
|
||||
tunnelMutex.withLock {
|
||||
val active = tunnelProvider.backendStatus.value.activeTunnels
|
||||
|
||||
if (active.isNotEmpty()) {
|
||||
lastActiveTunnels = active.keys.toList()
|
||||
stopActiveTunnelsInternal()
|
||||
return@withLock
|
||||
}
|
||||
|
||||
val tunnelsToStart =
|
||||
when {
|
||||
lastActiveTunnels.isNotEmpty() -> {
|
||||
lastActiveTunnels.mapNotNull { tunnelRepository.getById(it) }
|
||||
}
|
||||
|
||||
else -> {
|
||||
tunnelRepository.getDefaultTunnel()?.let(::listOf) ?: emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
tunnelsToStart.forEach { startTunnelInternal(it, source) }
|
||||
}
|
||||
|
||||
private suspend fun stopTunnelInternal(id: Int, source: TunnelActionSource) {
|
||||
tunnelProvider
|
||||
.stopTunnel(id)
|
||||
.onSuccess { _actions.emit(TunnelActionEvent.Stopped(tunnelId = id, source = source)) }
|
||||
.onFailure { _errors.emit(TunnelErrorEvent.from(it, id)) }
|
||||
}
|
||||
|
||||
private suspend fun stopActiveTunnelsInternal() {
|
||||
tunnelProvider.stopActiveTunnels()
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.orchestration
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.LockdownSettingsRepository
|
||||
|
||||
class TunnelModeCoordinator(
|
||||
private val tunnelProvider: TunnelProvider,
|
||||
private val settingsRepository: GeneralSettingRepository,
|
||||
private val lockdownRepository: LockdownSettingsRepository,
|
||||
) {
|
||||
|
||||
suspend fun changeMode(newMode: TunnelMode): Result<Unit> {
|
||||
|
||||
val settings = settingsRepository.getGeneralSettings()
|
||||
val oldMode = settings.tunnelMode
|
||||
|
||||
if (oldMode == newMode) {
|
||||
return Result.success(Unit)
|
||||
}
|
||||
|
||||
return runCatching {
|
||||
tunnelProvider.stopActiveTunnels().getOrThrow()
|
||||
exitMode(oldMode)
|
||||
enterMode(newMode)
|
||||
|
||||
settingsRepository.upsert(settings.copy(tunnelMode = newMode))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun exitMode(oldMode: TunnelMode) {
|
||||
when (oldMode) {
|
||||
TunnelMode.LOCK_DOWN -> {
|
||||
tunnelProvider.disableLockDown().getOrThrow()
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun enterMode(newMode: TunnelMode) {
|
||||
when (newMode) {
|
||||
TunnelMode.LOCK_DOWN -> {
|
||||
val lockdownSettings = lockdownRepository.getLockdownSettings()
|
||||
|
||||
tunnelProvider.setLockDown(lockdownSettings).getOrThrow()
|
||||
}
|
||||
|
||||
TunnelMode.VPN,
|
||||
TunnelMode.PROXY -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
-232
@@ -1,232 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service
|
||||
|
||||
import android.app.Notification
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import android.text.format.Formatter
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.lifecycle.LifecycleService
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
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.Dispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.distinctByKeys
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.core.qualifier.named
|
||||
import timber.log.Timber
|
||||
|
||||
abstract class BaseTunnelForegroundService : LifecycleService(), TunnelService {
|
||||
|
||||
private val notificationManager: NotificationManager by inject()
|
||||
private val serviceManager: ServiceManager by inject()
|
||||
private val tunnelManager: TunnelManager by inject()
|
||||
private val ioDispatcher: CoroutineDispatcher by inject(named(Dispatcher.IO))
|
||||
private val settingsRepository: GeneralSettingRepository by inject()
|
||||
private val tunnelsRepository: TunnelRepository by inject()
|
||||
|
||||
protected abstract val fgsType: Int
|
||||
|
||||
private var currentSingleTunnelId: Int? = null
|
||||
|
||||
private var statsJob: Job? = null
|
||||
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
super.onBind(intent)
|
||||
return LocalBinder(this)
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
ServiceCompat.startForeground(
|
||||
this,
|
||||
NotificationManager.VPN_NOTIFICATION_ID,
|
||||
onCreateNotification(),
|
||||
fgsType,
|
||||
)
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
super.onStartCommand(intent, flags, startId)
|
||||
|
||||
ServiceCompat.startForeground(
|
||||
this,
|
||||
NotificationManager.VPN_NOTIFICATION_ID,
|
||||
onCreateNotification(),
|
||||
fgsType,
|
||||
)
|
||||
|
||||
if (
|
||||
intent == null ||
|
||||
intent.component == null ||
|
||||
(intent.component?.packageName != this.packageName)
|
||||
) {
|
||||
Timber.d("Service started by Always-on VPN feature")
|
||||
lifecycleScope.launch {
|
||||
val settings = settingsRepository.getGeneralSettings()
|
||||
if (settings.isAlwaysOnVpnEnabled) {
|
||||
val tunnel = tunnelsRepository.getDefaultTunnel()
|
||||
tunnel?.let { tunnelManager.startTunnel(it) }
|
||||
} else {
|
||||
Timber.w("Always-on VPN is not enabled in app settings")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
start()
|
||||
}
|
||||
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
override fun start() {
|
||||
lifecycleScope.launch(ioDispatcher) {
|
||||
tunnelManager.activeTunnels.distinctByKeys().collect { activeTunnels ->
|
||||
val activeTunIds = activeTunnels.keys
|
||||
val tunnels = tunnelsRepository.getAll()
|
||||
val activeConfigs = tunnels.filter { activeTunIds.contains(it.id) }
|
||||
|
||||
updateServiceNotification(activeConfigs)
|
||||
restartStatsUpdaterIfNeeded(activeConfigs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun restartStatsUpdaterIfNeeded(activeConfigs: List<TunnelConfig>) {
|
||||
val single = activeConfigs.singleOrNull()
|
||||
|
||||
if (single == null) {
|
||||
statsJob?.cancel()
|
||||
statsJob = null
|
||||
currentSingleTunnelId = null
|
||||
return
|
||||
}
|
||||
|
||||
if (currentSingleTunnelId == single.id && statsJob?.isActive == true) return
|
||||
|
||||
statsJob?.cancel()
|
||||
statsJob = null
|
||||
currentSingleTunnelId = single.id
|
||||
|
||||
statsJob =
|
||||
lifecycleScope.launch(ioDispatcher) {
|
||||
while (isActive) {
|
||||
val traffic = readTraffic(single.id)
|
||||
|
||||
notificationManager.show(
|
||||
NotificationManager.VPN_NOTIFICATION_ID,
|
||||
createTunnelNotification(single, consumedTraffic = traffic),
|
||||
)
|
||||
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun readTraffic(tunnelId: Int): Pair<Long, Long>? {
|
||||
val active = tunnelManager.activeTunnels.value[tunnelId] ?: return null
|
||||
val stats = active.statistics ?: return null
|
||||
return stats.rx() to stats.tx()
|
||||
}
|
||||
|
||||
private fun updateServiceNotification(activeConfigs: List<TunnelConfig>) {
|
||||
val notification =
|
||||
when (activeConfigs.size) {
|
||||
0 -> onCreateNotification()
|
||||
1 -> createTunnelNotification(activeConfigs.first(), consumedTraffic = null)
|
||||
else -> createTunnelsNotification()
|
||||
}
|
||||
|
||||
ServiceCompat.startForeground(
|
||||
this,
|
||||
NotificationManager.VPN_NOTIFICATION_ID,
|
||||
notification,
|
||||
fgsType,
|
||||
)
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
Timber.d("Stop called")
|
||||
statsJob?.cancel()
|
||||
statsJob = null
|
||||
currentSingleTunnelId = null
|
||||
|
||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
serviceManager.handleTunnelServiceDestroy()
|
||||
|
||||
statsJob?.cancel()
|
||||
statsJob = null
|
||||
currentSingleTunnelId = null
|
||||
|
||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||
Timber.d("onDestroy")
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun createTunnelNotification(
|
||||
tunnelConfig: TunnelConfig,
|
||||
consumedTraffic: Pair<Long, Long>?,
|
||||
): Notification {
|
||||
|
||||
val subText =
|
||||
consumedTraffic?.let { traffic ->
|
||||
val formattedRx = "↓ ${formatBytes(traffic.first)}"
|
||||
val formattedTx = "↑ ${formatBytes(traffic.second)}"
|
||||
"$formattedRx $formattedTx"
|
||||
}
|
||||
|
||||
return notificationManager.createNotification(
|
||||
WireGuardNotification.NotificationChannels.VPN,
|
||||
title = tunnelConfig.name,
|
||||
description = getString(R.string.tunnel_running),
|
||||
subText = subText,
|
||||
actions =
|
||||
listOf(
|
||||
notificationManager.createNotificationAction(
|
||||
NotificationAction.TUNNEL_OFF,
|
||||
tunnelConfig.id,
|
||||
)
|
||||
),
|
||||
onGoing = true,
|
||||
groupKey = NotificationManager.VPN_GROUP_KEY,
|
||||
isGroupSummary = true,
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
),
|
||||
groupKey = NotificationManager.VPN_GROUP_KEY,
|
||||
isGroupSummary = true,
|
||||
)
|
||||
}
|
||||
|
||||
private fun onCreateNotification(): Notification {
|
||||
return notificationManager.createNotification(
|
||||
WireGuardNotification.NotificationChannels.VPN,
|
||||
title = getString(R.string.tunnel_starting),
|
||||
groupKey = NotificationManager.VPN_GROUP_KEY,
|
||||
isGroupSummary = true,
|
||||
)
|
||||
}
|
||||
|
||||
private fun formatBytes(bytes: Long) = Formatter.formatFileSize(this, bytes)
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service
|
||||
|
||||
import android.os.Binder
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
class LocalBinder(service: TunnelService) : Binder() {
|
||||
private val serviceRef = WeakReference(service)
|
||||
|
||||
val service: TunnelService?
|
||||
get() = serviceRef.get()
|
||||
}
|
||||
+6
-177
@@ -1,192 +1,21 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.net.VpnService
|
||||
import android.os.IBinder
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelService
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import timber.log.Timber
|
||||
|
||||
class ServiceManager(
|
||||
private val context: Context,
|
||||
ioDispatcher: CoroutineDispatcher,
|
||||
applicationScope: CoroutineScope,
|
||||
private val mainDispatcher: CoroutineDispatcher,
|
||||
private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
|
||||
) {
|
||||
class ServiceManager(private val context: Context) {
|
||||
|
||||
private val autoTunnelMutex = Mutex()
|
||||
private val tunnelMutex = Mutex()
|
||||
|
||||
private val _tunnelService = MutableStateFlow<TunnelService?>(null)
|
||||
private val _autoTunnelService = MutableStateFlow<AutoTunnelService?>(null)
|
||||
val autoTunnelService = _autoTunnelService.asStateFlow()
|
||||
val tunnelService = _tunnelService.asStateFlow()
|
||||
|
||||
init {
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
_autoTunnelService
|
||||
.onEach { _ -> withContext(mainDispatcher) { updateAutoTunnelTile() } }
|
||||
.launchIn(this)
|
||||
}
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
combine(
|
||||
autoTunnelSettingsRepository.flow
|
||||
.map { it.isAutoTunnelEnabled }
|
||||
.distinctUntilChanged(),
|
||||
_autoTunnelService,
|
||||
) { enabled, service ->
|
||||
enabled to (service != null)
|
||||
}
|
||||
.collect { (enabled, isRunning) ->
|
||||
when {
|
||||
enabled && !isRunning -> {
|
||||
autoTunnelMutex.withLock { startServiceInternal() }
|
||||
}
|
||||
!enabled && isRunning -> {
|
||||
autoTunnelMutex.withLock { stopServiceInternal() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fun startAutoTunnelService() {
|
||||
context.startForegroundService(Intent(context, AutoTunnelService::class.java))
|
||||
}
|
||||
|
||||
private val tunnelServiceConnection =
|
||||
object : ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName, service: IBinder) {
|
||||
val binder = service as? LocalBinder
|
||||
_tunnelService.update { binder?.service }
|
||||
val serviceClass =
|
||||
when {
|
||||
name.className.contains("VpnForegroundService") -> "VpnForegroundService"
|
||||
name.className.contains("TunnelForegroundService") ->
|
||||
"TunnelForegroundService"
|
||||
else -> "Unknown"
|
||||
}
|
||||
Timber.d("$serviceClass connected")
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName) {
|
||||
_tunnelService.update { null }
|
||||
val serviceClass =
|
||||
when {
|
||||
name.className.contains("VpnForegroundService") -> "VpnForegroundService"
|
||||
name.className.contains("TunnelForegroundService") ->
|
||||
"TunnelForegroundService"
|
||||
else -> "Unknown"
|
||||
}
|
||||
Timber.d("$serviceClass disconnected")
|
||||
}
|
||||
}
|
||||
|
||||
private val autoTunnelServiceConnection =
|
||||
object : ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName, service: IBinder) {
|
||||
val binder = service as? AutoTunnelService.LocalBinder
|
||||
_autoTunnelService.update { binder?.service }
|
||||
Timber.d("AutoTunnelService connected")
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName) {
|
||||
_autoTunnelService.update { null }
|
||||
Timber.d("AutoTunnelService disconnected")
|
||||
}
|
||||
}
|
||||
fun stopAutoTunnelService() {
|
||||
context.stopService(Intent(context, AutoTunnelService::class.java))
|
||||
}
|
||||
|
||||
fun hasVpnPermission(): Boolean {
|
||||
return VpnService.prepare(context) == null
|
||||
}
|
||||
|
||||
private fun startServiceInternal() {
|
||||
if (autoTunnelService.value == null) {
|
||||
val intent = Intent(context, AutoTunnelService::class.java)
|
||||
context.startForegroundService(intent)
|
||||
context.bindService(intent, autoTunnelServiceConnection, Context.BIND_AUTO_CREATE)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun startAutoTunnelService() = autoTunnelMutex.withLock { startServiceInternal() }
|
||||
|
||||
private fun stopServiceInternal() {
|
||||
_autoTunnelService.value?.stop()
|
||||
try {
|
||||
context.unbindService(autoTunnelServiceConnection)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Failed to unbind AutoTunnelService")
|
||||
}
|
||||
_autoTunnelService.update { null }
|
||||
}
|
||||
|
||||
suspend fun startTunnelService(appMode: AppMode) =
|
||||
tunnelMutex.withLock {
|
||||
if (_tunnelService.value != null) {
|
||||
Timber.d("Service already exists, waiting for disconnect")
|
||||
withTimeoutOrNull(2000L) { _tunnelService.first { it == null } }
|
||||
?: Timber.w("Timeout waiting for existing service to disconnect")
|
||||
}
|
||||
if (_tunnelService.value == null) {
|
||||
val serviceClass =
|
||||
when (appMode) {
|
||||
AppMode.VPN,
|
||||
AppMode.LOCK_DOWN -> VpnForegroundService::class.java
|
||||
AppMode.KERNEL,
|
||||
AppMode.PROXY -> TunnelForegroundService::class.java
|
||||
}
|
||||
val intent = Intent(context, serviceClass)
|
||||
context.startForegroundService(intent)
|
||||
context.bindService(intent, tunnelServiceConnection, Context.BIND_AUTO_CREATE)
|
||||
} else {
|
||||
Timber.e("Service still not null after timeout")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun stopTunnelService() =
|
||||
tunnelMutex.withLock {
|
||||
_tunnelService.value?.let { service ->
|
||||
service.stop()
|
||||
try {
|
||||
context.unbindService(tunnelServiceConnection)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Failed to unbind Tunnel Service")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateAutoTunnelTile() {
|
||||
context.requestAutoTunnelTileServiceUpdate()
|
||||
}
|
||||
|
||||
fun updateTunnelTile() {
|
||||
context.requestTunnelTileServiceStateUpdate()
|
||||
}
|
||||
|
||||
fun handleTunnelServiceDestroy() {
|
||||
_tunnelService.update { null }
|
||||
}
|
||||
|
||||
fun handleAutoTunnelServiceDestroy() {
|
||||
_autoTunnelService.update { null }
|
||||
}
|
||||
}
|
||||
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
|
||||
class TunnelForegroundService(override val fgsType: Int = Constants.SPECIAL_USE_SERVICE_TYPE_ID) :
|
||||
BaseTunnelForegroundService()
|
||||
@@ -1,7 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service
|
||||
|
||||
interface TunnelService {
|
||||
fun start()
|
||||
|
||||
fun stop()
|
||||
}
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
|
||||
class VpnForegroundService(override val fgsType: Int = Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID) :
|
||||
BaseTunnelForegroundService()
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.autotunnel
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.ActiveNetwork
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.AutoTunnelState
|
||||
|
||||
class AutoTunnelEngine {
|
||||
|
||||
fun evaluate(state: AutoTunnelState): AutoTunnelEvent {
|
||||
return when (val decision = decide(state)) {
|
||||
is Decision.Sync -> {
|
||||
if (decision.start.isEmpty() && decision.stop.isEmpty()) {
|
||||
AutoTunnelEvent.DoNothing
|
||||
} else {
|
||||
AutoTunnelEvent.Sync(start = decision.start, stop = decision.stop)
|
||||
}
|
||||
}
|
||||
Decision.None -> AutoTunnelEvent.DoNothing
|
||||
}
|
||||
}
|
||||
|
||||
private fun decide(state: AutoTunnelState): Decision {
|
||||
val network = state.networkState
|
||||
val settings = state.settings
|
||||
val backend = state.backendStatus
|
||||
|
||||
val activeTunnelIds = backend.activeTunnels.keys.toSet()
|
||||
|
||||
val desiredTunnels = resolveDesiredTunnels(state).map { it.id }.toSet()
|
||||
|
||||
// stop condition overrides everything
|
||||
if (!network.hasInternet() && settings.isStopOnNoInternetEnabled) {
|
||||
return Decision.Sync(start = emptySet(), stop = activeTunnelIds)
|
||||
}
|
||||
|
||||
val toStart = desiredTunnels - activeTunnelIds
|
||||
val toStop = activeTunnelIds - desiredTunnels
|
||||
|
||||
if (toStart.isEmpty() && toStop.isEmpty()) {
|
||||
return Decision.None
|
||||
}
|
||||
|
||||
return Decision.Sync(
|
||||
start = state.tunnels.filter { it.id in toStart }.toSet(),
|
||||
stop = toStop,
|
||||
)
|
||||
}
|
||||
|
||||
private fun resolveDesiredTunnels(state: AutoTunnelState): List<TunnelConfig> {
|
||||
val network = state.networkState
|
||||
val settings = state.settings
|
||||
|
||||
val wifiActive = network.activeNetwork is ActiveNetwork.Wifi
|
||||
val mobileActive = network.activeNetwork is ActiveNetwork.Cellular
|
||||
val ethernetActive = network.activeNetwork is ActiveNetwork.Ethernet
|
||||
|
||||
return when {
|
||||
ethernetActive && settings.isTunnelOnEthernetEnabled ->
|
||||
resolveByPriority(state) { it.isEthernetTunnel }
|
||||
|
||||
mobileActive && settings.isTunnelOnMobileDataEnabled ->
|
||||
resolveByPriority(state) { it.isMobileDataTunnel }
|
||||
|
||||
wifiActive && settings.isTunnelOnWifiEnabled && !isWifiTrusted(state) ->
|
||||
resolveWifiTunnels(state)
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveByPriority(
|
||||
state: AutoTunnelState,
|
||||
predicate: (TunnelConfig) -> Boolean,
|
||||
): List<TunnelConfig> {
|
||||
return listOfNotNull(state.tunnels.firstOrNull(predicate) ?: defaultTunnel(state))
|
||||
}
|
||||
|
||||
private fun resolveWifiTunnels(state: AutoTunnelState): List<TunnelConfig> {
|
||||
val wifi = state.networkState.activeNetwork as? ActiveNetwork.Wifi ?: return emptyList()
|
||||
|
||||
val matched = state.tunnels.filter { state.matchesNetwork(wifi.ssid, it.tunnelNetworks) }
|
||||
|
||||
return matched.ifEmpty { listOfNotNull(defaultTunnel(state)) }
|
||||
}
|
||||
|
||||
private fun isWifiTrusted(state: AutoTunnelState): Boolean {
|
||||
val wifi = state.networkState.activeNetwork as? ActiveNetwork.Wifi ?: return false
|
||||
return state.matchesNetwork(wifi.ssid, state.settings.trustedNetworkSSIDs)
|
||||
}
|
||||
|
||||
private fun defaultTunnel(state: AutoTunnelState): TunnelConfig? {
|
||||
return state.tunnels.firstOrNull { it.isPrimaryTunnel } ?: state.tunnels.firstOrNull()
|
||||
}
|
||||
|
||||
private sealed interface Decision {
|
||||
data class Sync(val start: Set<TunnelConfig>, val stop: Set<Int>) : Decision
|
||||
|
||||
data object None : Decision
|
||||
}
|
||||
}
|
||||
+220
-261
@@ -1,23 +1,22 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.autotunnel
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Binder
|
||||
import android.os.IBinder
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.lifecycle.LifecycleService
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
|
||||
import com.zaneschepke.networkmonitor.ConnectivityState
|
||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||
import com.zaneschepke.networkmonitor.StableNetworkEngine
|
||||
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.data.model.AppMode
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.AndroidNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.tile.AutoTunnelTileRefresher
|
||||
import com.zaneschepke.wireguardautotunnel.di.Dispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelActionSource
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.TunnelActionEvent
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.AutoTunnelSettings
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
||||
@@ -27,24 +26,15 @@ import com.zaneschepke.wireguardautotunnel.domain.state.AutoTunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.toDomain
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.to
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.toMillis
|
||||
import java.lang.ref.WeakReference
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
@@ -54,49 +44,67 @@ import timber.log.Timber
|
||||
|
||||
class AutoTunnelService : LifecycleService() {
|
||||
|
||||
private val networkMonitor: NetworkMonitor by inject()
|
||||
private val engine = AutoTunnelEngine()
|
||||
|
||||
private val notificationManager: NotificationManager by inject()
|
||||
private val reconciliationMutex = Mutex()
|
||||
|
||||
private val networkEngine: StableNetworkEngine by inject()
|
||||
|
||||
private val notificationService: NotificationService by inject()
|
||||
|
||||
private val ioDispatcher: CoroutineDispatcher by inject(named(Dispatcher.IO))
|
||||
|
||||
private val serviceManager: ServiceManager by inject()
|
||||
|
||||
private val tunnelManager: TunnelManager by inject()
|
||||
private val stateHolder: AutoTunnelStateHolder by inject()
|
||||
|
||||
private val autoTunnelRepository: AutoTunnelSettingsRepository by inject()
|
||||
private val settingsRepository: GeneralSettingRepository by inject()
|
||||
private val tunnelsRepository: TunnelRepository by inject()
|
||||
|
||||
private val defaultState = AutoTunnelState()
|
||||
|
||||
private val autoTunMutex = Mutex()
|
||||
|
||||
private val autoTunnelStateFlow = MutableStateFlow(defaultState)
|
||||
|
||||
private val tunnelCoordinator: TunnelCoordinator by inject()
|
||||
private var autoTunnelJob: Job? = null
|
||||
private var permissionsJob: Job? = null
|
||||
private var autoTunnelFailoverJob: Job? = null
|
||||
private var overridesJob: Job? = null
|
||||
|
||||
class LocalBinder(service: AutoTunnelService) : Binder() {
|
||||
private val serviceRef = WeakReference(service)
|
||||
@Volatile private var manualOverrideState = ManualOverrideState()
|
||||
|
||||
val service: AutoTunnelService?
|
||||
get() = serviceRef.get()
|
||||
private data class PermissionWarningState(
|
||||
val detectionMethod: AndroidNetworkMonitor.WifiDetectionMethod,
|
||||
val locationServicesEnabled: Boolean,
|
||||
val locationPermissionsEnabled: Boolean,
|
||||
val ssidReadRequired: Boolean,
|
||||
)
|
||||
|
||||
private data class ManualOverrideState(
|
||||
val fingerprint: AutoTunnelState.NetworkFingerprint? = null,
|
||||
val stoppedTunnelIds: Set<Int> = emptySet(),
|
||||
val startedTunnelIds: Set<Int> = emptySet(),
|
||||
)
|
||||
|
||||
private val autoTunnelStateFlow: Flow<AutoTunnelState> by lazy {
|
||||
val networkFlow = networkEngine.stableState.mapNotNull { it?.state?.toDomain() }
|
||||
|
||||
val settingsFlow = combineSettings()
|
||||
|
||||
val backendFlow =
|
||||
tunnelCoordinator.backendStatus.distinctUntilChangedBy { it.activeTunnels.keys.toSet() }
|
||||
|
||||
combine(networkFlow, settingsFlow, backendFlow) { network, settings, backend ->
|
||||
AutoTunnelState(
|
||||
networkState = network,
|
||||
settings = settings.second,
|
||||
tunnelMode = settings.first,
|
||||
tunnels = settings.third,
|
||||
backendStatus = backend,
|
||||
)
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
private val binder = LocalBinder(this)
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
stateHolder.setActive(true)
|
||||
launchWatcherNotification()
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
super.onBind(intent)
|
||||
return binder
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
super.onStartCommand(intent, flags, startId)
|
||||
Timber.d("onStartCommand executed with startId: $startId")
|
||||
@@ -105,44 +113,90 @@ class AutoTunnelService : LifecycleService() {
|
||||
}
|
||||
|
||||
fun start() {
|
||||
stateHolder.setActive(true)
|
||||
AutoTunnelTileRefresher.refresh(this)
|
||||
launchWatcherNotification()
|
||||
autoTunnelJob?.cancel()
|
||||
autoTunnelJob = startAutoTunnelStateJob()
|
||||
permissionsJob?.cancel()
|
||||
permissionsJob = startLocationPermissionsNotificationJob()
|
||||
overridesJob?.cancel()
|
||||
overridesJob = startOverridesJob()
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
stateHolder.setActive(false)
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
serviceManager.handleAutoTunnelServiceDestroy()
|
||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||
stateHolder.setActive(false)
|
||||
AutoTunnelTileRefresher.refresh(this)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun startOverridesJob(): Job =
|
||||
lifecycleScope.launch(ioDispatcher) {
|
||||
tunnelCoordinator.actions.collect { action ->
|
||||
reconciliationMutex.withLock {
|
||||
manualOverrideState =
|
||||
when (action) {
|
||||
is TunnelActionEvent.Started -> {
|
||||
|
||||
if (action.source != TunnelActionSource.USER) {
|
||||
return@withLock
|
||||
}
|
||||
|
||||
manualOverrideState.copy(
|
||||
startedTunnelIds =
|
||||
manualOverrideState.startedTunnelIds + action.tunnelId,
|
||||
stoppedTunnelIds =
|
||||
manualOverrideState.stoppedTunnelIds - action.tunnelId,
|
||||
)
|
||||
}
|
||||
|
||||
is TunnelActionEvent.Stopped -> {
|
||||
|
||||
if (action.source != TunnelActionSource.USER) {
|
||||
return@withLock
|
||||
}
|
||||
|
||||
manualOverrideState.copy(
|
||||
stoppedTunnelIds =
|
||||
manualOverrideState.stoppedTunnelIds + action.tunnelId,
|
||||
startedTunnelIds =
|
||||
manualOverrideState.startedTunnelIds - action.tunnelId,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Timber.d("Updated manual overrides: $manualOverrideState")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchWatcherNotification(
|
||||
description: String = getString(R.string.monitoring_state_changes)
|
||||
) {
|
||||
val notification =
|
||||
notificationManager.createNotification(
|
||||
WireGuardNotification.NotificationChannels.AUTO_TUNNEL,
|
||||
notificationService.createNotification(
|
||||
AndroidNotificationService.NotificationChannels.AUTO_TUNNEL,
|
||||
title = getString(R.string.auto_tunnel_title),
|
||||
description = description,
|
||||
actions =
|
||||
listOf(
|
||||
notificationManager.createNotificationAction(
|
||||
notificationService.createNotificationAction(
|
||||
NotificationAction.AUTO_TUNNEL_OFF
|
||||
)
|
||||
),
|
||||
onGoing = true,
|
||||
groupKey = NotificationManager.AUTO_TUNNEL_GROUP_KEY,
|
||||
groupKey = NotificationService.AUTO_TUNNEL_GROUP_KEY,
|
||||
isGroupSummary = true,
|
||||
)
|
||||
ServiceCompat.startForeground(
|
||||
this,
|
||||
NotificationManager.AUTO_TUNNEL_NOTIFICATION_ID,
|
||||
NotificationService.AUTO_TUNNEL_NOTIFICATION_ID,
|
||||
notification,
|
||||
Constants.SPECIAL_USE_SERVICE_TYPE_ID,
|
||||
)
|
||||
@@ -150,244 +204,149 @@ class AutoTunnelService : LifecycleService() {
|
||||
|
||||
private fun startAutoTunnelStateJob(): Job =
|
||||
lifecycleScope.launch(ioDispatcher) {
|
||||
val networkFlow =
|
||||
debouncedConnectivityStateFlow
|
||||
.flowOn(ioDispatcher)
|
||||
.map { it.toDomain() }
|
||||
.map(::NetworkChange)
|
||||
.distinctUntilChanged()
|
||||
autoTunnelStateFlow.collectLatest { state ->
|
||||
reconciliationMutex.withLock {
|
||||
updateFingerprintIfNeeded(state)
|
||||
|
||||
val settingsFlow =
|
||||
combineSettings().map { (appMode, settings, tunnels) ->
|
||||
SettingsChange(appMode, settings, tunnels)
|
||||
}
|
||||
val rawEvent = engine.evaluate(state)
|
||||
|
||||
val tunnelsFlow = tunnelManager.activeTunnels.map(::ActiveTunnelsChange)
|
||||
val event = applyOverrides(rawEvent)
|
||||
|
||||
var reevaluationJob: Job? = null
|
||||
Timber.d("AutoTunnel reconciliation event: $event")
|
||||
|
||||
// get everything in sync before we use merge
|
||||
combine(networkFlow, settingsFlow, tunnelsFlow) { network, settings, tunnels ->
|
||||
autoTunnelStateFlow.update {
|
||||
it.copy(
|
||||
activeTunnels = tunnels.activeTunnels,
|
||||
networkState = network.networkState,
|
||||
settings = settings.settings,
|
||||
tunnels = settings.tunnels,
|
||||
)
|
||||
}
|
||||
}
|
||||
.first()
|
||||
|
||||
val initialState = autoTunnelStateFlow.value
|
||||
if (initialState != defaultState) {
|
||||
handleAutoTunnelEvent(
|
||||
initialState.determineAutoTunnelEvent(NetworkChange(initialState.networkState))
|
||||
)
|
||||
}
|
||||
|
||||
// use merge to limit the noise of a combine and also increase the scalability of auto
|
||||
// tunnel handling new states
|
||||
merge(networkFlow, settingsFlow, tunnelsFlow).collect { change ->
|
||||
if (change !is ActiveTunnelsChange) {
|
||||
Timber.d("New state changed to ${change.javaClass.simpleName}")
|
||||
}
|
||||
|
||||
val previousState = autoTunnelStateFlow.value
|
||||
|
||||
when (change) {
|
||||
is NetworkChange -> {
|
||||
Timber.d("Network change: ${change.networkState}")
|
||||
reevaluationJob?.cancel()
|
||||
autoTunnelStateFlow.update { it.copy(networkState = change.networkState) }
|
||||
if (previousState.networkState == change.networkState) {
|
||||
Timber.d("Duplicate network state change detected, ignoring")
|
||||
return@collect
|
||||
}
|
||||
}
|
||||
is SettingsChange -> {
|
||||
reevaluationJob?.cancel()
|
||||
autoTunnelStateFlow.update {
|
||||
it.copy(settings = change.settings, tunnels = change.tunnels)
|
||||
}
|
||||
if (
|
||||
previousState.settings == change.settings &&
|
||||
previousState.tunnels == change.tunnels
|
||||
) {
|
||||
Timber.d("Duplicate settings change detected, ignoring")
|
||||
return@collect
|
||||
}
|
||||
}
|
||||
is ActiveTunnelsChange -> {
|
||||
autoTunnelStateFlow.update { it.copy(activeTunnels = change.activeTunnels) }
|
||||
return@collect
|
||||
}
|
||||
}
|
||||
|
||||
handleAutoTunnelEvent(autoTunnelStateFlow.value.determineAutoTunnelEvent(change))
|
||||
|
||||
// re-evaluate network state after a short duration to prevent missed state changes
|
||||
reevaluationJob = launch {
|
||||
val snapshotNetwork = autoTunnelStateFlow.value.networkState
|
||||
delay(REEVALUATE_CHECK_DELAY)
|
||||
val currentState = autoTunnelStateFlow.value
|
||||
if (
|
||||
currentState != defaultState && currentState.networkState != snapshotNetwork
|
||||
) {
|
||||
Timber.d(
|
||||
"Re-evaluating auto-tunnel state.. (network changed since snapshot)"
|
||||
)
|
||||
handleAutoTunnelEvent(currentState.determineAutoTunnelEvent(change))
|
||||
} else {
|
||||
Timber.d("Skipping re-eval: network unchanged or default state")
|
||||
}
|
||||
handleAutoTunnelEvent(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun combineSettings(): Flow<Triple<AppMode, AutoTunnelSettings, List<TunnelConfig>>> {
|
||||
private fun updateFingerprintIfNeeded(state: AutoTunnelState) {
|
||||
val fingerprint = state.networkFingerPrint
|
||||
|
||||
if (manualOverrideState.fingerprint != fingerprint) {
|
||||
Timber.d("Network changed, clearing overrides")
|
||||
|
||||
manualOverrideState = ManualOverrideState(fingerprint = fingerprint)
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyOverrides(event: AutoTunnelEvent): AutoTunnelEvent {
|
||||
|
||||
if (event !is AutoTunnelEvent.Sync) {
|
||||
return event
|
||||
}
|
||||
|
||||
val filteredStart =
|
||||
event.start.filterNot { it.id in manualOverrideState.stoppedTunnelIds }.toSet()
|
||||
|
||||
val filteredStop =
|
||||
event.stop.filterNot { it in manualOverrideState.startedTunnelIds }.toSet()
|
||||
|
||||
if (filteredStart.isEmpty() && filteredStop.isEmpty()) {
|
||||
return AutoTunnelEvent.DoNothing
|
||||
}
|
||||
|
||||
return event.copy(start = filteredStart, stop = filteredStop)
|
||||
}
|
||||
|
||||
private fun combineSettings():
|
||||
Flow<Triple<TunnelMode, AutoTunnelSettings, List<TunnelConfig>>> {
|
||||
return combine(
|
||||
settingsRepository.flow.map { it.appMode }.distinctUntilChanged(),
|
||||
settingsRepository.flow.map { it.tunnelMode }.distinctUntilChanged(),
|
||||
autoTunnelRepository.flow,
|
||||
tunnelsRepository.userTunnelsFlow.map { tunnels ->
|
||||
// isActive is ignored for equality checks so user can manually toggle off
|
||||
// tunnel with auto-tunnel
|
||||
tunnels.map { it.copy(isActive = false) }
|
||||
},
|
||||
tunnelsRepository.userTunnelsFlow,
|
||||
) { appMode, autoTunnel, tunnels ->
|
||||
Triple(appMode, autoTunnel, tunnels)
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
private fun areAutoTunnelPermissionsRequiredTheSame(
|
||||
old: AutoTunnelState,
|
||||
new: AutoTunnelState,
|
||||
): Boolean {
|
||||
return (old.settings.wifiDetectionMethod == new.settings.wifiDetectionMethod &&
|
||||
old.networkState.locationPermissionGranted ==
|
||||
new.networkState.locationPermissionGranted &&
|
||||
old.networkState.locationServicesEnabled == new.networkState.locationServicesEnabled &&
|
||||
old.tunnels == new.tunnels &&
|
||||
old.settings.trustedNetworkSSIDs == new.settings.trustedNetworkSSIDs)
|
||||
}
|
||||
|
||||
// watch for changes to location permission and notify user it will impact auto-tunneling
|
||||
// TODO or a recheck button for location permission so we dont have to poll it
|
||||
private fun startLocationPermissionsNotificationJob(): Job =
|
||||
lifecycleScope.launch(ioDispatcher) {
|
||||
var locationServicesShown = false
|
||||
var locationPermissionsShown = false
|
||||
|
||||
data class NetworkPermissionState(
|
||||
val detectionMethod: AndroidNetworkMonitor.WifiDetectionMethod,
|
||||
val locationServicesEnabled: Boolean,
|
||||
val locationPermissionsEnabled: Boolean,
|
||||
val ssidReadRequired: Boolean,
|
||||
)
|
||||
|
||||
autoTunnelStateFlow
|
||||
.distinctUntilChanged(::areAutoTunnelPermissionsRequiredTheSame)
|
||||
.map {
|
||||
NetworkPermissionState(
|
||||
it.settings.wifiDetectionMethod.to(),
|
||||
it.networkState.locationServicesEnabled,
|
||||
it.networkState.locationPermissionGranted,
|
||||
(it.tunnels.any { tunnel -> tunnel.tunnelNetworks.isNotEmpty() } ||
|
||||
it.settings.trustedNetworkSSIDs.isNotEmpty()),
|
||||
.map { state ->
|
||||
PermissionWarningState(
|
||||
detectionMethod = state.settings.wifiDetectionMethod.to(),
|
||||
locationServicesEnabled = state.networkState.locationServicesEnabled,
|
||||
locationPermissionsEnabled = state.networkState.locationPermissionGranted,
|
||||
ssidReadRequired =
|
||||
state.tunnels.any { it.tunnelNetworks.isNotEmpty() } ||
|
||||
state.settings.trustedNetworkSSIDs.isNotEmpty(),
|
||||
)
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.collect { state ->
|
||||
when (state.detectionMethod) {
|
||||
AndroidNetworkMonitor.WifiDetectionMethod.DEFAULT,
|
||||
AndroidNetworkMonitor.WifiDetectionMethod.LEGACY -> {
|
||||
if (
|
||||
!state.locationPermissionsEnabled &&
|
||||
!locationPermissionsShown &&
|
||||
state.ssidReadRequired
|
||||
) {
|
||||
locationPermissionsShown = true
|
||||
val notification =
|
||||
notificationManager.createNotification(
|
||||
WireGuardNotification.NotificationChannels.AUTO_TUNNEL,
|
||||
title = getString(R.string.warning),
|
||||
description =
|
||||
getString(R.string.location_permissions_missing),
|
||||
)
|
||||
notificationManager.show(
|
||||
NotificationManager.AUTO_TUNNEL_LOCATION_PERMISSION_ID,
|
||||
notification,
|
||||
)
|
||||
}
|
||||
if (
|
||||
!state.locationServicesEnabled &&
|
||||
!locationServicesShown &&
|
||||
state.ssidReadRequired
|
||||
) {
|
||||
locationServicesShown = true
|
||||
val notification =
|
||||
notificationManager.createNotification(
|
||||
WireGuardNotification.NotificationChannels.AUTO_TUNNEL,
|
||||
title = getString(R.string.warning),
|
||||
description =
|
||||
getString(R.string.location_services_not_detected),
|
||||
)
|
||||
notificationManager.show(
|
||||
NotificationManager.AUTO_TUNNEL_LOCATION_SERVICES_ID,
|
||||
notification,
|
||||
)
|
||||
}
|
||||
if (state.locationServicesEnabled || !state.ssidReadRequired) {
|
||||
notificationManager.remove(
|
||||
NotificationManager.AUTO_TUNNEL_LOCATION_SERVICES_ID
|
||||
)
|
||||
locationServicesShown = false
|
||||
}
|
||||
if (state.locationPermissionsEnabled || !state.ssidReadRequired) {
|
||||
notificationManager.remove(
|
||||
NotificationManager.AUTO_TUNNEL_LOCATION_PERMISSION_ID
|
||||
)
|
||||
locationPermissionsShown = false
|
||||
}
|
||||
val wifiMode = state.detectionMethod
|
||||
|
||||
if (
|
||||
wifiMode == AndroidNetworkMonitor.WifiDetectionMethod.DEFAULT ||
|
||||
wifiMode == AndroidNetworkMonitor.WifiDetectionMethod.LEGACY
|
||||
) {
|
||||
|
||||
if (!state.ssidReadRequired) {
|
||||
notificationService.remove(
|
||||
NotificationService.AUTO_TUNNEL_LOCATION_SERVICES_ID
|
||||
)
|
||||
notificationService.remove(
|
||||
NotificationService.AUTO_TUNNEL_LOCATION_PERMISSION_ID
|
||||
)
|
||||
return@collect
|
||||
}
|
||||
|
||||
if (!state.locationPermissionsEnabled) {
|
||||
val notification =
|
||||
notificationService.createNotification(
|
||||
AndroidNotificationService.NotificationChannels.AUTO_TUNNEL,
|
||||
title = getString(R.string.warning),
|
||||
description = getString(R.string.location_permissions_missing),
|
||||
)
|
||||
|
||||
notificationService.show(
|
||||
NotificationService.AUTO_TUNNEL_LOCATION_PERMISSION_ID,
|
||||
notification,
|
||||
)
|
||||
} else {
|
||||
notificationService.remove(
|
||||
NotificationService.AUTO_TUNNEL_LOCATION_PERMISSION_ID
|
||||
)
|
||||
}
|
||||
|
||||
if (!state.locationServicesEnabled) {
|
||||
val notification =
|
||||
notificationService.createNotification(
|
||||
AndroidNotificationService.NotificationChannels.AUTO_TUNNEL,
|
||||
title = getString(R.string.warning),
|
||||
description = getString(R.string.location_services_not_detected),
|
||||
)
|
||||
|
||||
notificationService.show(
|
||||
NotificationService.AUTO_TUNNEL_LOCATION_SERVICES_ID,
|
||||
notification,
|
||||
)
|
||||
} else {
|
||||
notificationService.remove(
|
||||
NotificationService.AUTO_TUNNEL_LOCATION_SERVICES_ID
|
||||
)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleAutoTunnelEvent(autoTunnelEvent: AutoTunnelEvent) {
|
||||
autoTunMutex.withLock {
|
||||
when (
|
||||
val event =
|
||||
autoTunnelEvent.also {
|
||||
Timber.i("Auto tunnel event: ${it.javaClass.simpleName}")
|
||||
}
|
||||
) {
|
||||
is AutoTunnelEvent.Start ->
|
||||
(event.tunnelConfig ?: tunnelsRepository.getDefaultTunnel())?.let {
|
||||
tunnelManager.startTunnel(it).onFailure { e ->
|
||||
Timber.e(e, "Auto-tunnel start failed for ${it.name}")
|
||||
// TODO notify or retry
|
||||
}
|
||||
}
|
||||
is AutoTunnelEvent.Stop -> tunnelManager.stopActiveTunnels()
|
||||
AutoTunnelEvent.DoNothing -> Timber.i("Auto-tunneling: nothing to do")
|
||||
private suspend fun handleAutoTunnelEvent(event: AutoTunnelEvent) {
|
||||
when (event) {
|
||||
is AutoTunnelEvent.Sync -> {
|
||||
|
||||
event.stop.forEach { tunnelId ->
|
||||
Timber.d("Stopping tunnel: $tunnelId")
|
||||
tunnelCoordinator.stopTunnel(tunnelId, TunnelActionSource.AUTO_TUNNEL)
|
||||
}
|
||||
|
||||
event.start.forEach { config ->
|
||||
Timber.d("Starting tunnel: ${config.name}")
|
||||
tunnelCoordinator.startTunnel(config, TunnelActionSource.AUTO_TUNNEL)
|
||||
}
|
||||
}
|
||||
|
||||
AutoTunnelEvent.DoNothing -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
// restart network flow on debounce changes
|
||||
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
|
||||
private val debouncedConnectivityStateFlow: Flow<ConnectivityState> by lazy {
|
||||
autoTunnelRepository.flow
|
||||
.map { it.debounceDelaySeconds.toMillis() }
|
||||
.distinctUntilChanged()
|
||||
.flatMapLatest { debounceMillis ->
|
||||
networkMonitor.connectivityStateFlow.debounce(debounceMillis)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val REEVALUATE_CHECK_DELAY = 3_000L
|
||||
}
|
||||
}
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.autotunnel
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
class AutoTunnelStateHolder {
|
||||
|
||||
private val _active = MutableStateFlow(false)
|
||||
val active: StateFlow<Boolean> = _active
|
||||
|
||||
fun setActive(active: Boolean) {
|
||||
_active.value = active
|
||||
}
|
||||
}
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.autotunnel
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.AutoTunnelSettings
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.NetworkState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||
|
||||
sealed interface StateChange
|
||||
|
||||
data class NetworkChange(val networkState: NetworkState) : StateChange
|
||||
|
||||
data class SettingsChange(
|
||||
val appMode: AppMode,
|
||||
val settings: AutoTunnelSettings,
|
||||
val tunnels: List<TunnelConfig>,
|
||||
) : StateChange
|
||||
|
||||
data class ActiveTunnelsChange(val activeTunnels: Map<Int, TunnelState>) : StateChange
|
||||
+47
-77
@@ -1,109 +1,79 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.tile
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import android.service.quicksettings.Tile
|
||||
import android.service.quicksettings.TileService
|
||||
import androidx.lifecycle.*
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
||||
import kotlin.concurrent.atomics.AtomicBoolean
|
||||
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.AutoTunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelStateHolder
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.android.ext.android.inject
|
||||
import timber.log.Timber
|
||||
|
||||
class AutoTunnelControlTile : TileService(), LifecycleOwner {
|
||||
class AutoTunnelControlTile : TileService() {
|
||||
|
||||
private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository by inject()
|
||||
private val autoTunnelStateHolder: AutoTunnelStateHolder by inject()
|
||||
private val autoTunnelCoordinator: AutoTunnelCoordinator by inject()
|
||||
|
||||
private val serviceManager: ServiceManager by inject()
|
||||
private val tileScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
|
||||
|
||||
@OptIn(ExperimentalAtomicApi::class) val isCollecting = AtomicBoolean(false)
|
||||
|
||||
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
|
||||
}
|
||||
private var observerJob: Job? = null
|
||||
|
||||
override fun onDestroy() {
|
||||
tileScope.cancel()
|
||||
super.onDestroy()
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
|
||||
}
|
||||
|
||||
override fun onTileAdded() {
|
||||
super.onTileAdded()
|
||||
initTileState()
|
||||
}
|
||||
|
||||
override fun onStopListening() {
|
||||
super.onStopListening()
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalAtomicApi::class)
|
||||
private fun initTileState() {
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
|
||||
Timber.d("Start listening called for auto tunnel tile")
|
||||
if (isCollecting.compareAndSet(expectedValue = false, newValue = true)) {
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
serviceManager.autoTunnelService.collect {
|
||||
if (it != null) return@collect setActive()
|
||||
setInactive()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStartListening() {
|
||||
super.onStartListening()
|
||||
initTileState()
|
||||
updateTileState()
|
||||
startObserving()
|
||||
}
|
||||
|
||||
override fun onStopListening() {
|
||||
super.onStopListening()
|
||||
observerJob?.cancel()
|
||||
observerJob = null
|
||||
}
|
||||
|
||||
override fun onTileAdded() {
|
||||
super.onTileAdded()
|
||||
updateTileState()
|
||||
startObserving()
|
||||
}
|
||||
|
||||
override fun onClick() {
|
||||
super.onClick()
|
||||
unlockAndRun {
|
||||
lifecycleScope.launch {
|
||||
if (serviceManager.autoTunnelService.value != null) {
|
||||
autoTunnelSettingsRepository.updateAutoTunnelEnabled(false)
|
||||
setInactive()
|
||||
} else {
|
||||
autoTunnelSettingsRepository.updateAutoTunnelEnabled(true)
|
||||
setActive()
|
||||
}
|
||||
unlockAndRun { tileScope.launch { autoTunnelCoordinator.toggle() } }
|
||||
}
|
||||
|
||||
private fun updateTileState() {
|
||||
val isActive = autoTunnelStateHolder.active.value
|
||||
if (isActive) setActive() else setInactive()
|
||||
}
|
||||
|
||||
private fun startObserving() {
|
||||
if (observerJob?.isActive == true) return
|
||||
|
||||
observerJob = tileScope.launch {
|
||||
autoTunnelStateHolder.active.collect { active ->
|
||||
if (active) setActive() else setInactive()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setActive() {
|
||||
qsTile?.let {
|
||||
it.state = Tile.STATE_ACTIVE
|
||||
it.updateTile()
|
||||
qsTile?.apply {
|
||||
state = Tile.STATE_ACTIVE
|
||||
updateTile()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setInactive() {
|
||||
qsTile?.let {
|
||||
it.state = Tile.STATE_INACTIVE
|
||||
it.updateTile()
|
||||
qsTile?.apply {
|
||||
state = Tile.STATE_INACTIVE
|
||||
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 AutoTunnelControlTile")
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
override val lifecycle: Lifecycle
|
||||
get() = lifecycleRegistry
|
||||
}
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.tile
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.service.quicksettings.TileService
|
||||
|
||||
object AutoTunnelTileRefresher : TileRefresher {
|
||||
override fun refresh(context: Context) {
|
||||
TileService.requestListeningState(
|
||||
context,
|
||||
ComponentName(context, AutoTunnelControlTile::class.java),
|
||||
)
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.tile
|
||||
|
||||
import android.content.Context
|
||||
|
||||
interface TileRefresher {
|
||||
fun refresh(context: Context)
|
||||
}
|
||||
+109
-162
@@ -1,213 +1,160 @@
|
||||
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 androidx.lifecycle.repeatOnLifecycle
|
||||
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.core.orchestration.TunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import kotlin.concurrent.atomics.AtomicBoolean
|
||||
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.android.ext.android.inject
|
||||
import timber.log.Timber
|
||||
|
||||
class TunnelControlTile : TileService(), LifecycleOwner {
|
||||
class TunnelControlTile : TileService() {
|
||||
|
||||
private val tunnelsRepository: TunnelRepository by inject()
|
||||
private val tunnelCoordinator: TunnelCoordinator by inject()
|
||||
|
||||
private val serviceManager: ServiceManager by inject()
|
||||
private var collectionJob: Job? = null
|
||||
|
||||
private val tunnelManager: TunnelManager by inject()
|
||||
private val tileScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
|
||||
|
||||
@OptIn(ExperimentalAtomicApi::class) val isCollecting = AtomicBoolean(false)
|
||||
|
||||
private val startLock = Mutex()
|
||||
|
||||
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
|
||||
}
|
||||
@Volatile private var observing = false
|
||||
|
||||
override fun onDestroy() {
|
||||
tileScope.cancel()
|
||||
super.onDestroy()
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
|
||||
}
|
||||
|
||||
override fun onTileAdded() {
|
||||
super.onTileAdded()
|
||||
initTileState()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalAtomicApi::class)
|
||||
private fun initTileState() {
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
|
||||
Timber.d("Start listening called for tunnel tile")
|
||||
if (isCollecting.compareAndSet(expectedValue = false, newValue = true)) {
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
tunnelManager.activeTunnels
|
||||
.distinctUntilChangedBy { it.size }
|
||||
.collect { updateTileState() }
|
||||
}
|
||||
}
|
||||
}
|
||||
updateTileState()
|
||||
startObserving()
|
||||
}
|
||||
|
||||
override fun onStartListening() {
|
||||
super.onStartListening()
|
||||
initTileState()
|
||||
updateTileState()
|
||||
startObserving()
|
||||
}
|
||||
|
||||
private fun startObserving() {
|
||||
if (observing) return
|
||||
observing = true
|
||||
|
||||
collectionJob = tileScope.launch {
|
||||
val tunnels = withContext(Dispatchers.IO) { tunnelsRepository.getAll() }
|
||||
|
||||
tunnelCoordinator.backendStatus
|
||||
.distinctUntilChangedBy { it.activeTunnels.keys }
|
||||
.collect { status ->
|
||||
if (tunnels.isEmpty()) {
|
||||
setUnavailable()
|
||||
return@collect
|
||||
}
|
||||
|
||||
val active = status.activeTunnels
|
||||
|
||||
if (active.isNotEmpty()) {
|
||||
val names = tunnels.filter { active.containsKey(it.id) }.map { it.name }
|
||||
|
||||
setActive(names)
|
||||
} else {
|
||||
setInactive()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStopListening() {
|
||||
super.onStopListening()
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
|
||||
}
|
||||
|
||||
private suspend fun updateTileState() {
|
||||
try {
|
||||
val tunnels = tunnelsRepository.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 }
|
||||
// TODO improvements would be needed to make this work well with toggling
|
||||
// multiple tunnels
|
||||
// this would be better managed elsewhere
|
||||
WireGuardAutoTunnel.setLastActiveTunnels(activeIds)
|
||||
val activeTunNames =
|
||||
tunnels.filter { activeTunnels.keys.contains(it.id) }.map { it.name }
|
||||
updateTileForActiveTunnels(activeTunNames)
|
||||
}
|
||||
else -> updateTileForLastActiveTunnels()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Failed to update tunnel state")
|
||||
setUnavailable()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateTileForActiveTunnels(activeTunnelNames: List<String>) {
|
||||
val tileName =
|
||||
when (activeTunnelNames.size) {
|
||||
1 -> activeTunnelNames[0]
|
||||
else -> getString(R.string.multiple)
|
||||
}
|
||||
updateTile(tileName, true)
|
||||
}
|
||||
|
||||
private suspend fun updateTileForLastActiveTunnels() {
|
||||
val lastActiveIds = WireGuardAutoTunnel.getLastActiveTunnels()
|
||||
when {
|
||||
lastActiveIds.isEmpty() -> {
|
||||
tunnelsRepository.getStartTunnel()?.let { config -> updateTile(config.name, false) }
|
||||
?: setUnavailable()
|
||||
}
|
||||
lastActiveIds.size > 1 -> updateTile(getString(R.string.multiple), false)
|
||||
else -> {
|
||||
val tunnelId = lastActiveIds.first()
|
||||
tunnelsRepository.getById(tunnelId)?.let { tunnel ->
|
||||
updateTile(tunnel.name, false)
|
||||
} ?: setUnavailable()
|
||||
}
|
||||
}
|
||||
observing = false
|
||||
collectionJob?.cancel()
|
||||
collectionJob = null
|
||||
}
|
||||
|
||||
override fun onClick() {
|
||||
super.onClick()
|
||||
unlockAndRun {
|
||||
lifecycleScope.launch {
|
||||
startLock.withLock {
|
||||
if (tunnelManager.activeTunnels.value.isNotEmpty())
|
||||
return@launch tunnelManager.stopActiveTunnels()
|
||||
val lastActive = WireGuardAutoTunnel.getLastActiveTunnels()
|
||||
if (lastActive.isEmpty()) {
|
||||
tunnelsRepository.getStartTunnel()?.let { tunnelManager.startTunnel(it) }
|
||||
} else {
|
||||
lastActive.forEach { id ->
|
||||
tunnelsRepository.getById(id)?.let { tunnelManager.startTunnel(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
tileScope.launch {
|
||||
tunnelCoordinator.toggleTunnels()
|
||||
updateTileState()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setActive() {
|
||||
qsTile?.let {
|
||||
it.state = Tile.STATE_ACTIVE
|
||||
it.updateTile()
|
||||
private fun updateTileState() {
|
||||
tileScope.launch {
|
||||
val tunnels = tunnelsRepository.getAll()
|
||||
|
||||
if (tunnels.isEmpty()) {
|
||||
setUnavailable()
|
||||
return@launch
|
||||
}
|
||||
|
||||
val active = tunnelCoordinator.backendStatus.value.activeTunnels
|
||||
|
||||
if (active.isNotEmpty()) {
|
||||
val names = tunnels.filter { active.containsKey(it.id) }.map { it.name }
|
||||
|
||||
setActive(names)
|
||||
} else {
|
||||
setInactive()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setActive(names: List<String>) {
|
||||
val label =
|
||||
when {
|
||||
names.isEmpty() -> ""
|
||||
names.size == 1 -> names.first()
|
||||
names.size <= 3 -> names.joinToString(", ")
|
||||
else -> {
|
||||
val visible = names.take(2).joinToString(", ")
|
||||
"$visible +${names.size - 2}"
|
||||
}
|
||||
}
|
||||
|
||||
qsTile?.apply {
|
||||
state = Tile.STATE_ACTIVE
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
subtitle = label
|
||||
}
|
||||
contentDescription = label
|
||||
|
||||
updateTile()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setInactive() {
|
||||
qsTile?.let {
|
||||
it.state = Tile.STATE_INACTIVE
|
||||
it.updateTile()
|
||||
qsTile?.apply {
|
||||
state = Tile.STATE_INACTIVE
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
subtitle = ""
|
||||
}
|
||||
contentDescription = ""
|
||||
|
||||
updateTile()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setUnavailable() {
|
||||
qsTile?.let {
|
||||
it.state = Tile.STATE_UNAVAILABLE
|
||||
setTileDescription("")
|
||||
it.updateTile()
|
||||
}
|
||||
}
|
||||
qsTile?.apply {
|
||||
state = Tile.STATE_UNAVAILABLE
|
||||
|
||||
private fun setTileDescription(description: String) {
|
||||
qsTile?.let {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
it.subtitle = description
|
||||
it.stateDescription = description
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
it.subtitle = description
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
subtitle = ""
|
||||
}
|
||||
it.updateTile()
|
||||
contentDescription = ""
|
||||
|
||||
updateTile()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateTile(name: String, active: Boolean) {
|
||||
runCatching {
|
||||
setTileDescription(name)
|
||||
if (active) return setActive()
|
||||
setInactive()
|
||||
}
|
||||
.onFailure { Timber.e(it) }
|
||||
}
|
||||
|
||||
/* This works around an annoying unsolved frameworks bug some people are hitting. */
|
||||
override fun onBind(intent: Intent): IBinder? {
|
||||
var ret: IBinder? = null
|
||||
try {
|
||||
ret = super.onBind(intent)
|
||||
} catch (_: Throwable) {
|
||||
Timber.e("Failed to bind to TunnelControlTile")
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
override val lifecycle: Lifecycle
|
||||
get() = lifecycleRegistry
|
||||
}
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.tile
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.service.quicksettings.TileService
|
||||
|
||||
object TunnelTileRefresher : TileRefresher {
|
||||
override fun refresh(context: Context) {
|
||||
TileService.requestListeningState(
|
||||
context,
|
||||
ComponentName(context, TunnelControlTile::class.java),
|
||||
)
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -34,7 +34,7 @@ class DynamicShortcutManager(
|
||||
intent =
|
||||
Intent(context, ShortcutsActivity::class.java).apply {
|
||||
putExtra("className", "WireGuardTunnelService")
|
||||
action = ShortcutsActivity.Action.STOP.name
|
||||
action = ShortcutContract.Action.STOP.name
|
||||
},
|
||||
shortcutIcon = R.drawable.vpn_off,
|
||||
),
|
||||
@@ -45,7 +45,7 @@ class DynamicShortcutManager(
|
||||
intent =
|
||||
Intent(context, ShortcutsActivity::class.java).apply {
|
||||
putExtra("className", "WireGuardTunnelService")
|
||||
action = ShortcutsActivity.Action.START.name
|
||||
action = ShortcutContract.Action.START.name
|
||||
},
|
||||
shortcutIcon = R.drawable.vpn_on,
|
||||
),
|
||||
@@ -56,7 +56,7 @@ class DynamicShortcutManager(
|
||||
intent =
|
||||
Intent(context, ShortcutsActivity::class.java).apply {
|
||||
putExtra("className", "WireGuardConnectivityWatcherService")
|
||||
action = ShortcutsActivity.Action.START.name
|
||||
action = ShortcutContract.Action.START.name
|
||||
},
|
||||
shortcutIcon = R.drawable.auto_play,
|
||||
),
|
||||
@@ -67,7 +67,7 @@ class DynamicShortcutManager(
|
||||
intent =
|
||||
Intent(context, ShortcutsActivity::class.java).apply {
|
||||
putExtra("className", "WireGuardConnectivityWatcherService")
|
||||
action = ShortcutsActivity.Action.STOP.name
|
||||
action = ShortcutContract.Action.STOP.name
|
||||
},
|
||||
shortcutIcon = R.drawable.auto_pause,
|
||||
),
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.shortcut
|
||||
|
||||
object ShortcutContract {
|
||||
|
||||
const val EXTRA_SHORTCUT_TYPE = "com.zaneschepke.wireguardautotunnel.shortcut.TYPE"
|
||||
|
||||
const val EXTRA_TUNNEL_NAME = "tunnelName"
|
||||
|
||||
const val EXTRA_CLASS_NAME = "className"
|
||||
|
||||
enum class ShortcutType(val value: String) {
|
||||
TUNNEL("tunnel"),
|
||||
AUTO_TUNNEL("auto_tunnel"),
|
||||
}
|
||||
|
||||
enum class Action {
|
||||
START,
|
||||
STOP,
|
||||
}
|
||||
|
||||
object Legacy {
|
||||
|
||||
const val TUNNEL_PROVIDER_NAME = "TunnelProvider"
|
||||
|
||||
const val AUTO_TUNNEL_SERVICE_CLASS_NAME = "AutoTunnelService"
|
||||
|
||||
const val TUNNEL_SERVICE_NAME = "WireGuardTunnelService"
|
||||
|
||||
const val AUTO_TUNNEL_SERVICE_NAME = "WireGuardConnectivityWatcherService"
|
||||
}
|
||||
}
|
||||
+7
-54
@@ -2,73 +2,26 @@ package com.zaneschepke.wireguardautotunnel.core.shortcut
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
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.core.orchestration.ShortcutCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.di.Scope
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.core.qualifier.named
|
||||
import timber.log.Timber
|
||||
|
||||
class ShortcutsActivity : ComponentActivity() {
|
||||
|
||||
private val settingsRepository: GeneralSettingRepository by inject()
|
||||
private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository by inject()
|
||||
private val tunnelsRepository: TunnelRepository by inject()
|
||||
private val tunnelManager: TunnelManager by inject()
|
||||
private val shortcutCoordinator: ShortcutCoordinator by inject()
|
||||
|
||||
private val applicationScope: CoroutineScope by inject(named(Scope.APPLICATION))
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
applicationScope.launch {
|
||||
val settings = settingsRepository.getGeneralSettings()
|
||||
if (settings.isShortcutsEnabled) {
|
||||
when (intent.getStringExtra(CLASS_NAME_EXTRA_KEY)) {
|
||||
LEGACY_TUNNEL_SERVICE_NAME,
|
||||
TunnelProvider::class.java.simpleName -> {
|
||||
val tunnelName = intent.getStringExtra(TUNNEL_NAME_EXTRA_KEY)
|
||||
Timber.d("Tunnel name extra: $tunnelName")
|
||||
val tunnelConfig =
|
||||
tunnelName?.let { tunnelsRepository.findByTunnelName(it) }
|
||||
?: tunnelsRepository.getDefaultTunnel()
|
||||
Timber.d("Shortcut action on name: ${tunnelConfig?.name}")
|
||||
tunnelConfig?.let {
|
||||
when (intent.action) {
|
||||
Action.START.name -> tunnelManager.startTunnel(it)
|
||||
Action.STOP.name -> tunnelManager.stopActiveTunnels()
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
AutoTunnelService::class.java.simpleName,
|
||||
LEGACY_AUTO_TUNNEL_SERVICE_NAME -> {
|
||||
when (intent.action) {
|
||||
Action.START.name ->
|
||||
autoTunnelSettingsRepository.updateAutoTunnelEnabled(true)
|
||||
Action.STOP.name ->
|
||||
autoTunnelSettingsRepository.updateAutoTunnelEnabled(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
shortcutCoordinator.handle(intent)
|
||||
finish()
|
||||
}
|
||||
finish()
|
||||
}
|
||||
|
||||
enum class Action {
|
||||
START,
|
||||
STOP,
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val LEGACY_TUNNEL_SERVICE_NAME = "WireGuardTunnelService"
|
||||
const val LEGACY_AUTO_TUNNEL_SERVICE_NAME = "WireGuardConnectivityWatcherService"
|
||||
const val TUNNEL_NAME_EXTRA_KEY = "tunnelName"
|
||||
const val CLASS_NAME_EXTRA_KEY = "className"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
fun Map<TunnelConfig, TunnelState>.allDown(): Boolean {
|
||||
return this.all { it.value.status.isDown() }
|
||||
}
|
||||
|
||||
fun Map<TunnelConfig, TunnelState>.hasActive(): Boolean {
|
||||
return this.any { it.value.status.isUp() }
|
||||
}
|
||||
|
||||
fun Map<TunnelConfig, TunnelState>.getValueById(id: Int): TunnelState? {
|
||||
val key = this.keys.find { it.id == id }
|
||||
return key?.let { this@getValueById[it] }
|
||||
}
|
||||
|
||||
fun Map<TunnelConfig, TunnelState>.getKeyById(id: Int): TunnelConfig? {
|
||||
return this.keys.find { it.id == id }
|
||||
}
|
||||
|
||||
fun Map<TunnelConfig, TunnelState>.isUp(tunnelConfig: TunnelConfig): Boolean {
|
||||
return this.getValueById(tunnelConfig.id)?.status?.isUp() ?: false
|
||||
}
|
||||
|
||||
fun MutableStateFlow<Map<TunnelConfig, TunnelState>>.exists(id: Int): Boolean {
|
||||
return this.value.any { it.key.id == id }
|
||||
}
|
||||
|
||||
fun MutableStateFlow<Map<TunnelConfig, TunnelState>>.isUp(id: Int): Boolean {
|
||||
return this.value.any { it.key.id == id && it.value.status is TunnelStatus.Up }
|
||||
}
|
||||
|
||||
fun MutableStateFlow<Map<TunnelConfig, TunnelState>>.isStarting(id: Int): Boolean {
|
||||
return this.value.any { it.key.id == id && it.value.status == TunnelStatus.Starting }
|
||||
}
|
||||
|
||||
fun MutableStateFlow<Map<TunnelConfig, TunnelState>>.findTunnel(id: Int): TunnelConfig? {
|
||||
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)
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||
|
||||
import com.zaneschepke.tunnel.Tunnel
|
||||
import com.zaneschepke.tunnel.backend.Backend
|
||||
import com.zaneschepke.tunnel.model.BackendMode
|
||||
import com.zaneschepke.tunnel.state.BackendStatus
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.LockdownSettings
|
||||
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.plus
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class, ExperimentalAtomicApi::class)
|
||||
class TunnelBackendProvider(
|
||||
private val backend: Backend,
|
||||
applicationScope: CoroutineScope,
|
||||
ioDispatcher: CoroutineDispatcher,
|
||||
) : TunnelProvider {
|
||||
|
||||
override val backendStatus: StateFlow<BackendStatus> =
|
||||
backend.status.stateIn(
|
||||
scope = applicationScope.plus(ioDispatcher),
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = BackendStatus(),
|
||||
)
|
||||
|
||||
override val events = backend.events
|
||||
|
||||
override suspend fun startTunnel(tunnel: Tunnel, mode: BackendMode): Result<Unit> {
|
||||
return backend.start(tunnel = tunnel, mode = mode)
|
||||
}
|
||||
|
||||
override suspend fun stopTunnel(tunnelId: Int): Result<Unit> {
|
||||
return backend.stop(tunnelId)
|
||||
}
|
||||
|
||||
override suspend fun stopActiveTunnels(): Result<Unit> {
|
||||
return backend.stopAllActiveTunnels()
|
||||
}
|
||||
|
||||
override suspend fun setLockDown(settings: LockdownSettings): Result<Unit> {
|
||||
return backend.setKillSwitch(settings.toKillSwitchConfig())
|
||||
}
|
||||
|
||||
override suspend fun disableLockDown(): Result<Unit> {
|
||||
return backend.disableKillSwitch()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val localErrorEvents = MutableSharedFlow<Pair<String?, BackendCoreException>>()
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val localMessageEvents = MutableSharedFlow<Pair<String?, BackendMessage>>()
|
||||
}
|
||||
-187
@@ -1,187 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.backend.TunnelBackend
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.UnknownError
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.LogHealthState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.PingState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import timber.log.Timber
|
||||
|
||||
class TunnelLifecycleManager(
|
||||
private val backend: TunnelBackend,
|
||||
private val applicationScope: CoroutineScope,
|
||||
private val ioDispatcher: CoroutineDispatcher,
|
||||
private val sharedActiveTunnels: MutableStateFlow<Map<Int, TunnelState>>,
|
||||
) : TunnelProvider {
|
||||
|
||||
override val activeTunnels: StateFlow<Map<Int, TunnelState>> = sharedActiveTunnels.asStateFlow()
|
||||
|
||||
private val _errorEvents = MutableSharedFlow<Pair<String?, BackendCoreException>>()
|
||||
override val errorEvents: SharedFlow<Pair<String?, BackendCoreException>> =
|
||||
_errorEvents.asSharedFlow()
|
||||
|
||||
private val _messageEvents = MutableSharedFlow<Pair<String?, BackendMessage>>()
|
||||
override val messageEvents: SharedFlow<Pair<String?, BackendMessage>> =
|
||||
_messageEvents.asSharedFlow()
|
||||
|
||||
private val tunnelJobs = ConcurrentHashMap<Int, Job>()
|
||||
private val tunMutex = Mutex()
|
||||
private val tunStatusMutex = Mutex()
|
||||
|
||||
override suspend fun startTunnel(tunnelConfig: TunnelConfig): Result<Unit> =
|
||||
tunMutex.withLock {
|
||||
val id = tunnelConfig.id
|
||||
if (sharedActiveTunnels.value.containsKey(id)) {
|
||||
Timber.w("Tunnel is already running: ${tunnelConfig.name}")
|
||||
return Result.failure(IllegalStateException("Tunnel already running"))
|
||||
}
|
||||
|
||||
val startupCompleted = CompletableDeferred<Result<Unit>>()
|
||||
|
||||
val job =
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
try {
|
||||
updateTunnelStatus(id, TunnelStatus.Starting)
|
||||
backend.tunnelStateFlow(tunnelConfig).collect { status ->
|
||||
updateTunnelStatus(id, status)
|
||||
|
||||
if (status != TunnelStatus.Starting && !startupCompleted.isCompleted) {
|
||||
if (status is TunnelStatus.Up) {
|
||||
startupCompleted.complete(Result.success(Unit))
|
||||
} else {
|
||||
startupCompleted.complete(Result.failure(UnknownError()))
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: BackendCoreException) {
|
||||
_errorEvents.emit(tunnelConfig.name to e)
|
||||
updateTunnelStatus(id, TunnelStatus.Down)
|
||||
startupCompleted.complete(Result.failure(e))
|
||||
} catch (_: CancellationException) {} finally {
|
||||
tunnelJobs.remove(id)
|
||||
sharedActiveTunnels.update { it - id }
|
||||
}
|
||||
}
|
||||
|
||||
tunnelJobs[id] = job
|
||||
job.invokeOnCompletion { tunnelJobs.remove(id) }
|
||||
|
||||
try {
|
||||
startupCompleted.await()
|
||||
} catch (e: Throwable) {
|
||||
job.cancel()
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun stopTunnel(tunnelId: Int) =
|
||||
tunMutex.withLock {
|
||||
val currentState = sharedActiveTunnels.value[tunnelId]?.status ?: return@withLock
|
||||
updateTunnelStatus(tunnelId, TunnelStatus.Stopping)
|
||||
tunnelJobs[tunnelId]?.cancel()
|
||||
|
||||
withTimeoutOrNull(STOP_TIMEOUT_MS) {
|
||||
activeTunnels.first {
|
||||
!it.containsKey(tunnelId) || it[tunnelId]!!.status == TunnelStatus.Down
|
||||
}
|
||||
}
|
||||
?: run {
|
||||
Timber.w("Stop timeout for $tunnelId (was $currentState); forcing kill")
|
||||
forceStopTunnel(tunnelId)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun forceStopTunnel(tunnelId: Int) {
|
||||
backend.forceStopTunnel(tunnelId)
|
||||
tunnelJobs[tunnelId]?.cancel()
|
||||
tunnelJobs.remove(tunnelId)
|
||||
sharedActiveTunnels.update { it - tunnelId }
|
||||
updateTunnelStatus(tunnelId, TunnelStatus.Down)
|
||||
}
|
||||
|
||||
override suspend fun stopActiveTunnels() {
|
||||
sharedActiveTunnels.value.forEach { (id, state) ->
|
||||
if (state.status.isUpOrStarting()) {
|
||||
stopTunnel(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun updateTunnelStatus(
|
||||
tunnelId: Int,
|
||||
status: TunnelStatus?,
|
||||
stats: TunnelStatistics?,
|
||||
pingStates: Map<String, PingState>?,
|
||||
logHealthState: LogHealthState?,
|
||||
) =
|
||||
tunStatusMutex.withLock {
|
||||
sharedActiveTunnels.update { currentTuns ->
|
||||
if (!currentTuns.containsKey(tunnelId) && status != TunnelStatus.Starting) {
|
||||
Timber.d("Ignoring update for inactive tunnel $tunnelId")
|
||||
return@update currentTuns
|
||||
}
|
||||
val existingState = currentTuns[tunnelId] ?: TunnelState()
|
||||
val newStatus = status ?: existingState.status
|
||||
if (newStatus == TunnelStatus.Down) {
|
||||
Timber.d("Removing tunnel $tunnelId from activeTunnels as state is DOWN")
|
||||
currentTuns - tunnelId
|
||||
} else if (
|
||||
existingState.status == newStatus &&
|
||||
stats == null &&
|
||||
pingStates == null &&
|
||||
logHealthState == null
|
||||
) {
|
||||
Timber.d("Skipping redundant state update for ${tunnelId}: $newStatus")
|
||||
currentTuns
|
||||
} else {
|
||||
val updated =
|
||||
existingState.copy(
|
||||
status = newStatus,
|
||||
statistics = stats ?: existingState.statistics,
|
||||
pingStates = pingStates ?: existingState.pingStates,
|
||||
logHealthState = logHealthState ?: existingState.logHealthState,
|
||||
)
|
||||
currentTuns + (tunnelId to updated)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun setBackendMode(backendMode: BackendMode) = backend.setBackendMode(backendMode)
|
||||
|
||||
override fun getBackendMode(): BackendMode = backend.getBackendMode()
|
||||
|
||||
override suspend fun runningTunnelNames(): Set<String> = backend.runningTunnelNames()
|
||||
|
||||
override fun handleDnsReresolve(tunnelConfig: TunnelConfig): Boolean =
|
||||
backend.handleDnsReresolve(tunnelConfig)
|
||||
|
||||
override fun getStatistics(tunnelId: Int): TunnelStatistics? = backend.getStatistics(tunnelId)
|
||||
|
||||
companion object {
|
||||
const val STOP_TIMEOUT_MS: Long = 5_000L
|
||||
}
|
||||
}
|
||||
@@ -1,359 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||
|
||||
import android.os.PowerManager
|
||||
import com.zaneschepke.logcatter.LogReader
|
||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.backend.TunnelBackend
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.handler.DynamicDnsHandler
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.handler.TunnelActiveStatePersister
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.handler.TunnelMonitorHandler
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.handler.TunnelServiceHandler
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.NotAuthorized
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.AutoTunnelSettings
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.LockdownSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.LogHealthState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.PingState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
||||
import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils
|
||||
import kotlin.concurrent.atomics.AtomicBoolean
|
||||
import kotlin.concurrent.atomics.AtomicReference
|
||||
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.flow.filterNot
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.plus
|
||||
import kotlinx.coroutines.supervisorScope
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class, ExperimentalAtomicApi::class)
|
||||
class TunnelManager(
|
||||
kernelBackend: TunnelBackend,
|
||||
userspaceBackend: TunnelBackend,
|
||||
proxyUserspaceBackend: TunnelBackend,
|
||||
networkMonitor: NetworkMonitor,
|
||||
networkUtils: NetworkUtils,
|
||||
powerManager: PowerManager,
|
||||
logReader: LogReader,
|
||||
monitoringSettingsRepository: MonitoringSettingsRepository,
|
||||
private val serviceManager: ServiceManager,
|
||||
private val settingsRepository: GeneralSettingRepository,
|
||||
private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
|
||||
private val lockdownSettingsRepository: LockdownSettingsRepository,
|
||||
private val tunnelsRepository: TunnelRepository,
|
||||
private val applicationScope: CoroutineScope,
|
||||
private val ioDispatcher: CoroutineDispatcher,
|
||||
) : TunnelProvider {
|
||||
|
||||
private val _activeTunnels = MutableStateFlow<Map<Int, TunnelState>>(emptyMap())
|
||||
override val activeTunnels: StateFlow<Map<Int, TunnelState>> = _activeTunnels.asStateFlow()
|
||||
|
||||
@OptIn(ExperimentalAtomicApi::class) val currentAppMode = AtomicReference(AppMode.VPN)
|
||||
|
||||
private val defaultManager =
|
||||
TunnelLifecycleManager(userspaceBackend, applicationScope, ioDispatcher, _activeTunnels)
|
||||
|
||||
private val lifecycleManagers: Map<AppMode, TunnelLifecycleManager> =
|
||||
mapOf(
|
||||
AppMode.KERNEL to
|
||||
TunnelLifecycleManager(
|
||||
kernelBackend,
|
||||
applicationScope,
|
||||
ioDispatcher,
|
||||
_activeTunnels,
|
||||
),
|
||||
AppMode.VPN to defaultManager,
|
||||
AppMode.PROXY to
|
||||
TunnelLifecycleManager(
|
||||
proxyUserspaceBackend,
|
||||
applicationScope,
|
||||
ioDispatcher,
|
||||
_activeTunnels,
|
||||
),
|
||||
AppMode.LOCK_DOWN to
|
||||
TunnelLifecycleManager(
|
||||
proxyUserspaceBackend,
|
||||
applicationScope,
|
||||
ioDispatcher,
|
||||
_activeTunnels,
|
||||
),
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalAtomicApi::class)
|
||||
private fun getProvider(): TunnelProvider {
|
||||
return lifecycleManagers[currentAppMode.load()] ?: defaultManager
|
||||
}
|
||||
|
||||
override suspend fun startTunnel(tunnelConfig: TunnelConfig): Result<Unit> =
|
||||
getProvider().startTunnel(tunnelConfig)
|
||||
|
||||
override suspend fun stopTunnel(tunnelId: Int) = getProvider().stopTunnel(tunnelId)
|
||||
|
||||
override suspend fun forceStopTunnel(tunnelId: Int) = getProvider().forceStopTunnel(tunnelId)
|
||||
|
||||
override suspend fun stopActiveTunnels() = getProvider().stopActiveTunnels()
|
||||
|
||||
override fun setBackendMode(backendMode: BackendMode) =
|
||||
getProvider().setBackendMode(backendMode)
|
||||
|
||||
override fun getBackendMode(): BackendMode = getProvider().getBackendMode()
|
||||
|
||||
override suspend fun runningTunnelNames(): Set<String> = getProvider().runningTunnelNames()
|
||||
|
||||
override fun handleDnsReresolve(tunnelConfig: TunnelConfig): Boolean =
|
||||
getProvider().handleDnsReresolve(tunnelConfig)
|
||||
|
||||
override fun getStatistics(tunnelId: Int): TunnelStatistics? =
|
||||
getProvider().getStatistics(tunnelId)
|
||||
|
||||
override suspend fun updateTunnelStatus(
|
||||
tunnelId: Int,
|
||||
status: TunnelStatus?,
|
||||
stats: TunnelStatistics?,
|
||||
pingStates: Map<String, PingState>?,
|
||||
logHealthState: LogHealthState?,
|
||||
) = getProvider().updateTunnelStatus(tunnelId, status, stats, pingStates, logHealthState)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val localErrorEvents = MutableSharedFlow<Pair<String?, BackendCoreException>>()
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val localMessageEvents = MutableSharedFlow<Pair<String?, BackendMessage>>()
|
||||
|
||||
override val errorEvents: SharedFlow<Pair<String?, BackendCoreException>> =
|
||||
merge(localErrorEvents, *lifecycleManagers.values.map { it.errorEvents }.toTypedArray())
|
||||
.shareIn(
|
||||
scope = applicationScope + ioDispatcher,
|
||||
started = SharingStarted.Eagerly,
|
||||
replay = 0,
|
||||
)
|
||||
|
||||
override val messageEvents: SharedFlow<Pair<String?, BackendMessage>> =
|
||||
merge(localMessageEvents, *lifecycleManagers.values.map { it.messageEvents }.toTypedArray())
|
||||
.shareIn(
|
||||
scope = applicationScope.plus(ioDispatcher),
|
||||
started = SharingStarted.Eagerly,
|
||||
replay = 0,
|
||||
)
|
||||
|
||||
private val tunnelServiceHandler =
|
||||
TunnelServiceHandler(
|
||||
activeTunnels = activeTunnels,
|
||||
settingsRepository = settingsRepository,
|
||||
serviceManager = serviceManager,
|
||||
applicationScope = applicationScope,
|
||||
ioDispatcher = ioDispatcher,
|
||||
)
|
||||
|
||||
private val tunnelActiveStatePersister =
|
||||
TunnelActiveStatePersister(
|
||||
activeTunnels = activeTunnels,
|
||||
tunnelsRepository = tunnelsRepository,
|
||||
applicationScope = applicationScope,
|
||||
ioDispatcher = ioDispatcher,
|
||||
)
|
||||
|
||||
private val dynamicDnsHandler =
|
||||
DynamicDnsHandler(
|
||||
activeTunnels = activeTunnels,
|
||||
tunnelsRepository = tunnelsRepository,
|
||||
settingsRepository = settingsRepository,
|
||||
localMessageEvents = localMessageEvents,
|
||||
handleDnsReresolve = { config -> handleDnsReresolve(config) },
|
||||
applicationScope = applicationScope,
|
||||
ioDispatcher = ioDispatcher,
|
||||
)
|
||||
|
||||
private val fullTunnelMonitorHandler =
|
||||
TunnelMonitorHandler(
|
||||
activeTunnels = activeTunnels,
|
||||
tunnelsRepository = tunnelsRepository,
|
||||
settingsRepository = settingsRepository,
|
||||
monitoringSettingsRepository = monitoringSettingsRepository,
|
||||
networkMonitor = networkMonitor,
|
||||
networkUtils = networkUtils,
|
||||
powerManager = powerManager,
|
||||
logReader = logReader,
|
||||
getStatistics = { id -> getStatistics(id) },
|
||||
updateTunnelStatus = { id, status, stats, pings, logHealth ->
|
||||
updateTunnelStatus(id, status, stats, pings, logHealth)
|
||||
},
|
||||
applicationScope = applicationScope,
|
||||
ioDispatcher = ioDispatcher,
|
||||
)
|
||||
|
||||
init {
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
val initialEmit = AtomicBoolean(true)
|
||||
settingsRepository.flow
|
||||
.filterNotNull()
|
||||
.filterNot { it == GeneralSettings() }
|
||||
.distinctUntilChangedBy { it.appMode }
|
||||
.collect { settings ->
|
||||
val isInitialEmit = initialEmit.exchange(false)
|
||||
val previousMode = currentAppMode.exchange(settings.appMode)
|
||||
|
||||
if (isInitialEmit) {
|
||||
return@collect handleRestore(settings)
|
||||
}
|
||||
|
||||
if (previousMode != settings.appMode) {
|
||||
handleModeChangeCleanup(previousMode)
|
||||
}
|
||||
if (settings.appMode == AppMode.LOCK_DOWN) {
|
||||
handleLockDownModeInit()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO this can crash if we haven't started foreground service yet, especially for
|
||||
// workerManager
|
||||
private suspend fun handleLockDownModeInit() {
|
||||
val lockdownSettings = lockdownSettingsRepository.getLockdownSettings()
|
||||
val allowedIps =
|
||||
if (lockdownSettings.bypassLan) TunnelConfig.IPV4_PUBLIC_NETWORKS else emptySet()
|
||||
try {
|
||||
if (serviceManager.hasVpnPermission()) {
|
||||
setBackendMode(
|
||||
BackendMode.KillSwitch(
|
||||
allowedIps,
|
||||
lockdownSettings.metered,
|
||||
lockdownSettings.dualStack,
|
||||
)
|
||||
)
|
||||
} else {
|
||||
throw NotAuthorized()
|
||||
}
|
||||
} catch (e: BackendCoreException) {
|
||||
localErrorEvents.tryEmit(null to e)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleModeChangeCleanup(previousAppMode: AppMode) {
|
||||
lifecycleManagers[previousAppMode]?.stopActiveTunnels()
|
||||
if (previousAppMode == AppMode.LOCK_DOWN) {
|
||||
lifecycleManagers[previousAppMode]?.setBackendMode(BackendMode.Inactive)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun handleRestore(settings: GeneralSettings? = null) =
|
||||
withContext(ioDispatcher) {
|
||||
val currentSettings = settings ?: settingsRepository.getGeneralSettings()
|
||||
val autoTunnelSettings = autoTunnelSettingsRepository.getAutoTunnelSettings()
|
||||
val tunnels = tunnelsRepository.userTunnelsFlow.firstOrNull()
|
||||
if (autoTunnelSettings.isAutoTunnelEnabled)
|
||||
return@withContext restoreAutoTunnel(autoTunnelSettings)
|
||||
if (currentSettings.appMode == AppMode.LOCK_DOWN) handleLockDownModeInit()
|
||||
if (tunnels?.any { it.isActive } == true) {
|
||||
if (currentSettings.appMode == AppMode.VPN && !serviceManager.hasVpnPermission())
|
||||
return@withContext localErrorEvents.emit(null to NotAuthorized())
|
||||
when (currentSettings.appMode) {
|
||||
AppMode.VPN,
|
||||
AppMode.PROXY,
|
||||
AppMode.LOCK_DOWN -> {
|
||||
tunnels.firstOrNull { it.isActive }?.let { startTunnel(it) }
|
||||
}
|
||||
AppMode.KERNEL ->
|
||||
tunnels.filter { it.isActive }.forEach { conf -> startTunnel(conf) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun restoreAutoTunnel(autoTunnelSettings: AutoTunnelSettings) {
|
||||
autoTunnelSettingsRepository.upsert(autoTunnelSettings.copy(isAutoTunnelEnabled = true))
|
||||
serviceManager.startAutoTunnelService()
|
||||
}
|
||||
|
||||
suspend fun handleReboot() =
|
||||
withContext(ioDispatcher) {
|
||||
val settings = settingsRepository.getGeneralSettings()
|
||||
val autoTunnelSettings = autoTunnelSettingsRepository.getAutoTunnelSettings()
|
||||
val defaultTunnel = tunnelsRepository.getDefaultTunnel()
|
||||
if (autoTunnelSettings.startOnBoot)
|
||||
return@withContext restoreAutoTunnel(autoTunnelSettings)
|
||||
if (settings.isRestoreOnBootEnabled) {
|
||||
tunnelsRepository.resetActiveTunnels()
|
||||
when (settings.appMode) {
|
||||
AppMode.LOCK_DOWN -> handleLockDownModeInit()
|
||||
AppMode.VPN ->
|
||||
if (!serviceManager.hasVpnPermission())
|
||||
return@withContext localErrorEvents.emit(null to NotAuthorized())
|
||||
AppMode.KERNEL,
|
||||
AppMode.PROXY -> Unit
|
||||
}
|
||||
defaultTunnel?.let { startTunnel(it) }
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun restartActiveTunnel(id: Int) =
|
||||
withContext(ioDispatcher) {
|
||||
val activeIds = activeTunnels.value.keys.toList()
|
||||
if (activeIds.isEmpty()) return@withContext
|
||||
if (!activeIds.contains(id)) return@withContext
|
||||
val tunnel = tunnelsRepository.getById(id) ?: return@withContext
|
||||
restartTunnel(tunnel)
|
||||
}
|
||||
|
||||
suspend fun restartActiveTunnels() =
|
||||
withContext(ioDispatcher) {
|
||||
val activeIds = activeTunnels.value.keys.toList()
|
||||
if (activeIds.isEmpty()) return@withContext
|
||||
|
||||
val tunnels = tunnelsRepository.getAll()
|
||||
if (tunnels.isEmpty()) return@withContext
|
||||
|
||||
supervisorScope {
|
||||
activeIds.forEach { id ->
|
||||
val tunnel =
|
||||
tunnels.find { it.id == id }
|
||||
?: run {
|
||||
Timber.w("Tunnel config $id not found; skipping restart")
|
||||
return@forEach
|
||||
}
|
||||
restartTunnel(tunnel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun restartTunnel(tunnel: TunnelConfig) {
|
||||
runCatching { stopTunnel(tunnel.id) }
|
||||
.onFailure { e -> Timber.e(e, "Failed to stop tunnel ${tunnel.id} during restart") }
|
||||
|
||||
delay(RESTART_TUNNEL_DELAY)
|
||||
|
||||
runCatching { startTunnel(tunnel) }
|
||||
.onFailure { e -> Timber.e(e, "Failed to restart tunnel ${tunnel.id}") }
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val RESTART_TUNNEL_DELAY = 300L
|
||||
}
|
||||
}
|
||||
+13
-32
@@ -1,45 +1,26 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.LogHealthState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.PingState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import com.zaneschepke.tunnel.Tunnel
|
||||
import com.zaneschepke.tunnel.event.TunnelEvent
|
||||
import com.zaneschepke.tunnel.model.BackendMode
|
||||
import com.zaneschepke.tunnel.state.BackendStatus
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.LockdownSettings
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
interface TunnelProvider {
|
||||
suspend fun startTunnel(tunnelConfig: TunnelConfig): Result<Unit>
|
||||
|
||||
suspend fun stopTunnel(tunnelId: Int)
|
||||
suspend fun startTunnel(tunnel: Tunnel, mode: BackendMode): Result<Unit>
|
||||
|
||||
suspend fun forceStopTunnel(tunnelId: Int)
|
||||
suspend fun stopTunnel(tunnelId: Int): Result<Unit>
|
||||
|
||||
suspend fun stopActiveTunnels()
|
||||
suspend fun stopActiveTunnels(): Result<Unit>
|
||||
|
||||
fun setBackendMode(backendMode: BackendMode)
|
||||
suspend fun setLockDown(settings: LockdownSettings): Result<Unit>
|
||||
|
||||
fun getBackendMode(): BackendMode
|
||||
suspend fun disableLockDown(): Result<Unit>
|
||||
|
||||
suspend fun runningTunnelNames(): Set<String>
|
||||
val backendStatus: StateFlow<BackendStatus>
|
||||
|
||||
fun handleDnsReresolve(tunnelConfig: TunnelConfig): Boolean
|
||||
|
||||
fun getStatistics(tunnelId: Int): TunnelStatistics?
|
||||
|
||||
val activeTunnels: StateFlow<Map<Int, TunnelState>>
|
||||
val errorEvents: SharedFlow<Pair<String?, BackendCoreException>>
|
||||
val messageEvents: SharedFlow<Pair<String?, BackendMessage>>
|
||||
|
||||
suspend fun updateTunnelStatus(
|
||||
tunnelId: Int,
|
||||
status: TunnelStatus? = null,
|
||||
stats: TunnelStatistics? = null,
|
||||
pingStates: Map<String, PingState>? = null,
|
||||
logHealthState: LogHealthState? = null,
|
||||
)
|
||||
val events: Flow<TunnelEvent>
|
||||
}
|
||||
|
||||
-128
@@ -1,128 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.tunnel.backend
|
||||
|
||||
import com.wireguard.android.backend.Backend
|
||||
import com.wireguard.android.backend.BackendException
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.wireguard.android.backend.WgQuickBackend
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.DnsFailure
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.InvalidConfig
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.KernelTunnelName
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.KernelWireguardNotSupported
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.UnknownError
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.WireGuardStatistics
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendCoreException
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.regex.Pattern
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.consumeAsFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
class KernelTunnel(private val runConfigHelper: RunConfigHelper, private val backend: Backend) :
|
||||
TunnelBackend {
|
||||
|
||||
private val runtimeTunnels = ConcurrentHashMap<Int, Tunnel>()
|
||||
|
||||
private fun validateWireGuardInterfaceName(name: String): Result<Unit> {
|
||||
if (name.isEmpty() || name.length > 15)
|
||||
return Result.failure(KernelTunnelName(R.string.kernel_name_error))
|
||||
if (name == "." || name == "..") {
|
||||
return Result.failure(KernelTunnelName(R.string.kernel_name_dots))
|
||||
}
|
||||
val pattern = Pattern.compile("^[a-zA-Z0-9_=+.-]{1,15}$")
|
||||
if (!pattern.matcher(name).matches()) {
|
||||
return Result.failure(KernelTunnelName(R.string.kernel_name_special_characters))
|
||||
}
|
||||
return Result.success(Unit)
|
||||
}
|
||||
|
||||
override fun tunnelStateFlow(tunnelConfig: TunnelConfig): Flow<TunnelStatus> = callbackFlow {
|
||||
if (!WgQuickBackend.hasKernelSupport()) throw KernelWireguardNotSupported()
|
||||
validateWireGuardInterfaceName(tunnelConfig.name).onFailure { throw it }
|
||||
|
||||
val stateChannel = Channel<Tunnel.State>()
|
||||
|
||||
val runtimeTunnel = RuntimeWgTunnel(tunnelConfig, stateChannel)
|
||||
runtimeTunnels[tunnelConfig.id] = runtimeTunnel
|
||||
|
||||
val consumerJob = launch {
|
||||
stateChannel.consumeAsFlow().collect { state -> trySend(state.asTunnelState()) }
|
||||
}
|
||||
|
||||
try {
|
||||
val runConfig = runConfigHelper.buildWgRunConfig(tunnelConfig)
|
||||
backend.setState(runtimeTunnel, Tunnel.State.UP, runConfig)
|
||||
} catch (e: TimeoutCancellationException) {
|
||||
Timber.Forest.e("Startup timed out for ${tunnelConfig.name}")
|
||||
throw DnsFailure()
|
||||
} catch (e: BackendException) {
|
||||
throw e.toBackendCoreException()
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Timber.Forest.e(e, "Invalid backend arguments")
|
||||
throw InvalidConfig()
|
||||
} catch (e: Exception) {
|
||||
Timber.Forest.e(e, "Error while setting tunnel state")
|
||||
throw UnknownError()
|
||||
}
|
||||
|
||||
awaitClose {
|
||||
try {
|
||||
backend.setState(runtimeTunnel, Tunnel.State.DOWN, null)
|
||||
} catch (e: BackendException) {
|
||||
// Errors are emitted by caller (lifecycle manager)
|
||||
} finally {
|
||||
consumerJob.cancel()
|
||||
stateChannel.close()
|
||||
runtimeTunnels.remove(tunnelConfig.id)
|
||||
trySend(TunnelStatus.Down)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getStatistics(tunnelId: Int): TunnelStatistics? {
|
||||
return try {
|
||||
val runtimeTunnel = runtimeTunnels[tunnelId] ?: return null
|
||||
WireGuardStatistics(backend.getStatistics(runtimeTunnel))
|
||||
} catch (e: Exception) {
|
||||
Timber.Forest.e(e, "Failed to get stats for $tunnelId")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun setBackendMode(backendMode: BackendMode) {
|
||||
Timber.Forest.w("Not yet implemented for kernel")
|
||||
}
|
||||
|
||||
override fun getBackendMode(): BackendMode {
|
||||
return BackendMode.Inactive
|
||||
}
|
||||
|
||||
override fun handleDnsReresolve(tunnelConfig: TunnelConfig): Boolean {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
override suspend fun runningTunnelNames(): Set<String> {
|
||||
return backend.runningTunnelNames
|
||||
}
|
||||
|
||||
override suspend fun forceStopTunnel(tunnelId: Int) {
|
||||
val runtimeTunnel = runtimeTunnels[tunnelId] ?: return
|
||||
try {
|
||||
backend.setState(runtimeTunnel, Tunnel.State.DOWN, null)
|
||||
} catch (e: BackendException) {
|
||||
Timber.Forest.e(e, "Force stop failed for $tunnelId")
|
||||
} finally {
|
||||
runtimeTunnels.remove(tunnelId)
|
||||
}
|
||||
}
|
||||
}
|
||||
-101
@@ -1,101 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.tunnel.backend
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.DnsProtocol
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.InvalidConfig
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.DnsSettings
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.ProxySettings
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.DnsSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.ProxySettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import java.util.Optional
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import org.amnezia.awg.config.Config
|
||||
import org.amnezia.awg.config.proxy.HttpProxy
|
||||
import org.amnezia.awg.config.proxy.Socks5Proxy
|
||||
|
||||
class RunConfigHelper(
|
||||
private val settingsRepository: GeneralSettingRepository,
|
||||
private val proxySettingsRepository: ProxySettingsRepository,
|
||||
private val dnsSettingsRepository: DnsSettingsRepository,
|
||||
private val tunnelsRepository: TunnelRepository,
|
||||
) {
|
||||
|
||||
private data class PrepResult(
|
||||
val effectiveConfig: TunnelConfig,
|
||||
val generalSettings: GeneralSettings,
|
||||
val dnsSettings: DnsSettings,
|
||||
)
|
||||
|
||||
private suspend fun prepare(tunnelConfig: TunnelConfig): PrepResult {
|
||||
val generalSettings = settingsRepository.getGeneralSettings()
|
||||
val dnsSettings = dnsSettingsRepository.getDnsSettings()
|
||||
val effectiveConfig =
|
||||
if (
|
||||
generalSettings.isGlobalSplitTunnelEnabled || dnsSettings.isGlobalTunnelDnsEnabled
|
||||
) {
|
||||
val globalConfig =
|
||||
tunnelsRepository.globalTunnelFlow.firstOrNull() ?: throw InvalidConfig()
|
||||
tunnelConfig.copyWithGlobalValues(
|
||||
globalConfig,
|
||||
dnsSettings.isGlobalTunnelDnsEnabled,
|
||||
generalSettings.isGlobalSplitTunnelEnabled,
|
||||
)
|
||||
} else {
|
||||
tunnelConfig
|
||||
}
|
||||
return PrepResult(effectiveConfig, generalSettings, dnsSettings)
|
||||
}
|
||||
|
||||
suspend fun buildAmRunConfig(tunnelConfig: TunnelConfig): Config {
|
||||
val prep = prepare(tunnelConfig)
|
||||
val proxies =
|
||||
if (prep.generalSettings.appMode == AppMode.PROXY) {
|
||||
val proxySettings = proxySettingsRepository.getProxySettings()
|
||||
buildList {
|
||||
if (proxySettings.socks5ProxyEnabled) {
|
||||
add(
|
||||
Socks5Proxy(
|
||||
proxySettings.socks5ProxyBindAddress
|
||||
?: ProxySettings.DEFAULT_SOCKS_BIND_ADDRESS,
|
||||
proxySettings.proxyUsername,
|
||||
proxySettings.proxyPassword,
|
||||
)
|
||||
)
|
||||
}
|
||||
if (proxySettings.httpProxyEnabled) {
|
||||
add(
|
||||
HttpProxy(
|
||||
proxySettings.httpProxyBindAddress
|
||||
?: ProxySettings.DEFAULT_HTTP_BIND_ADDRESS,
|
||||
proxySettings.proxyUsername,
|
||||
proxySettings.proxyPassword,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
val amConfig = prep.effectiveConfig.toAmConfig()
|
||||
return Config.Builder()
|
||||
.setInterface(amConfig.`interface`)
|
||||
.addPeers(amConfig.peers)
|
||||
.addProxies(proxies)
|
||||
.setDnsSettings(
|
||||
org.amnezia.awg.config.DnsSettings(
|
||||
prep.dnsSettings.dnsProtocol == DnsProtocol.DOH,
|
||||
Optional.ofNullable(prep.dnsSettings.dnsEndpoint),
|
||||
)
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
suspend fun buildWgRunConfig(tunnelConfig: TunnelConfig): com.wireguard.config.Config {
|
||||
val prep = prepare(tunnelConfig)
|
||||
return prep.effectiveConfig.toWgConfig()
|
||||
}
|
||||
}
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.tunnel.backend
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import org.amnezia.awg.backend.Tunnel
|
||||
|
||||
class RuntimeAwgTunnel(
|
||||
private val tunnelConfig: TunnelConfig,
|
||||
private val stateChannel: Channel<Tunnel.State>,
|
||||
) : Tunnel {
|
||||
|
||||
override fun getName() = tunnelConfig.name
|
||||
|
||||
override fun onStateChange(newState: Tunnel.State) {
|
||||
stateChannel.trySend(newState)
|
||||
}
|
||||
|
||||
override fun isIpv4ResolutionPreferred() = tunnelConfig.isIpv4Preferred
|
||||
|
||||
override fun isMetered() = tunnelConfig.isMetered
|
||||
}
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.tunnel.backend
|
||||
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
|
||||
class RuntimeWgTunnel(
|
||||
private val config: TunnelConfig,
|
||||
private val stateChannel: Channel<Tunnel.State>,
|
||||
) : Tunnel {
|
||||
|
||||
override fun getName() = config.name
|
||||
|
||||
override fun onStateChange(newState: Tunnel.State) {
|
||||
stateChannel.trySend(newState)
|
||||
}
|
||||
|
||||
override fun isIpv4ResolutionPreferred() = config.isIpv4Preferred
|
||||
}
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.tunnel.backend
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface TunnelBackend {
|
||||
fun tunnelStateFlow(tunnelConfig: TunnelConfig): Flow<TunnelStatus>
|
||||
|
||||
fun getStatistics(tunnelId: Int): TunnelStatistics?
|
||||
|
||||
fun setBackendMode(backendMode: BackendMode)
|
||||
|
||||
fun getBackendMode(): BackendMode
|
||||
|
||||
fun handleDnsReresolve(tunnelConfig: TunnelConfig): Boolean
|
||||
|
||||
suspend fun runningTunnelNames(): Set<String>
|
||||
|
||||
suspend fun forceStopTunnel(tunnelId: Int)
|
||||
}
|
||||
-119
@@ -1,119 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.tunnel.backend
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.DnsFailure
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.InvalidConfig
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.ServiceNotRunning
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.UnknownError
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.VpnUnauthorized
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.AmneziaStatistics
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asAmBackendMode
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asBackendMode
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendCoreException
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.consumeAsFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import org.amnezia.awg.backend.Backend
|
||||
import org.amnezia.awg.backend.BackendException
|
||||
import org.amnezia.awg.backend.Tunnel
|
||||
import timber.log.Timber
|
||||
|
||||
class UserspaceTunnel(private val backend: Backend, private val runConfigHelper: RunConfigHelper) :
|
||||
TunnelBackend {
|
||||
|
||||
private val runtimeTunnels = ConcurrentHashMap<Int, Tunnel>()
|
||||
|
||||
override fun tunnelStateFlow(tunnelConfig: TunnelConfig): Flow<TunnelStatus> = callbackFlow {
|
||||
val stateChannel = Channel<Tunnel.State>()
|
||||
|
||||
val runtimeTunnel = RuntimeAwgTunnel(tunnelConfig, stateChannel)
|
||||
runtimeTunnels[tunnelConfig.id] = runtimeTunnel
|
||||
|
||||
val consumerJob = launch {
|
||||
stateChannel.consumeAsFlow().collect { awgState -> trySend(awgState.asTunnelState()) }
|
||||
}
|
||||
|
||||
try {
|
||||
val runConfig = runConfigHelper.buildAmRunConfig(tunnelConfig)
|
||||
backend.setState(runtimeTunnel, Tunnel.State.UP, runConfig)
|
||||
} catch (_: TimeoutCancellationException) {
|
||||
Timber.e("Startup timed out for ${tunnelConfig.name} (likely DNS hang)")
|
||||
throw DnsFailure()
|
||||
} catch (e: BackendException) {
|
||||
throw e.toBackendCoreException()
|
||||
} catch (_: IllegalArgumentException) {
|
||||
throw InvalidConfig()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Error while setting tunnel state")
|
||||
throw UnknownError()
|
||||
}
|
||||
|
||||
awaitClose {
|
||||
try {
|
||||
backend.setState(runtimeTunnel, Tunnel.State.DOWN, null)
|
||||
} catch (e: BackendException) {
|
||||
// Errors emitted by caller
|
||||
} finally {
|
||||
consumerJob.cancel()
|
||||
stateChannel.close()
|
||||
runtimeTunnels.remove(tunnelConfig.id)
|
||||
trySend(TunnelStatus.Down)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun setBackendMode(backendMode: BackendMode) {
|
||||
Timber.d("Setting backend mode: $backendMode")
|
||||
try {
|
||||
backend.backendMode = backendMode.asAmBackendMode()
|
||||
} catch (e: BackendException) {
|
||||
throw e.toBackendCoreException()
|
||||
} catch (_: IOException) {
|
||||
throw VpnUnauthorized()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getBackendMode(): BackendMode {
|
||||
return backend.backendMode.asBackendMode()
|
||||
}
|
||||
|
||||
override fun handleDnsReresolve(tunnelConfig: TunnelConfig): Boolean {
|
||||
val tunnel = runtimeTunnels[tunnelConfig.id] ?: throw ServiceNotRunning()
|
||||
return backend.resolveDDNS(tunnelConfig.toAmConfig(), tunnel.isIpv4ResolutionPreferred)
|
||||
}
|
||||
|
||||
override suspend fun runningTunnelNames(): Set<String> {
|
||||
return backend.runningTunnelNames
|
||||
}
|
||||
|
||||
override fun getStatistics(tunnelId: Int): TunnelStatistics? {
|
||||
return try {
|
||||
val runtimeTunnel = runtimeTunnels[tunnelId] ?: return null
|
||||
AmneziaStatistics(backend.getStatistics(runtimeTunnel))
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Failed to get stats for $tunnelId")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun forceStopTunnel(tunnelId: Int) {
|
||||
val runtimeTunnel = runtimeTunnels[tunnelId] ?: return
|
||||
try {
|
||||
backend.setState(runtimeTunnel, Tunnel.State.DOWN, null)
|
||||
} catch (e: BackendException) {
|
||||
Timber.e(e, "Force stop failed for $tunnelId")
|
||||
} finally {
|
||||
runtimeTunnels.remove(tunnelId)
|
||||
}
|
||||
}
|
||||
}
|
||||
-114
@@ -1,114 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.tunnel.handler
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.plus
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import timber.log.Timber
|
||||
|
||||
class DynamicDnsHandler(
|
||||
private val activeTunnels: StateFlow<Map<Int, TunnelState>>,
|
||||
private val tunnelsRepository: TunnelRepository,
|
||||
private val settingsRepository: GeneralSettingRepository,
|
||||
private val localMessageEvents: MutableSharedFlow<Pair<String?, BackendMessage>>,
|
||||
private val handleDnsReresolve: (TunnelConfig) -> Boolean,
|
||||
private val applicationScope: CoroutineScope,
|
||||
private val ioDispatcher: CoroutineDispatcher,
|
||||
) {
|
||||
private val mutex = Mutex()
|
||||
private val jobs = ConcurrentHashMap<Int, Job>()
|
||||
|
||||
init {
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
combine(activeTunnels, settingsRepository.flow.filterNotNull()) { active, settings ->
|
||||
active to settings
|
||||
}
|
||||
.collect { (activeTuns, settings) ->
|
||||
mutex.withLock {
|
||||
val activeIds =
|
||||
activeTuns.keys
|
||||
.filter { id ->
|
||||
val config =
|
||||
tunnelsRepository.getById(id) ?: return@filter false
|
||||
config.restartOnPingFailure &&
|
||||
settings.appMode != AppMode.KERNEL
|
||||
}
|
||||
.toSet()
|
||||
|
||||
(jobs.keys - activeIds).forEach { id ->
|
||||
Timber.d("Shutting down Dynamic DNS monitoring job for tunnelId: $id")
|
||||
jobs.remove(id)?.cancel()
|
||||
}
|
||||
|
||||
activeIds.forEach { id ->
|
||||
if (jobs.containsKey(id)) return@forEach
|
||||
val config = tunnelsRepository.getById(id) ?: return@forEach
|
||||
val tunStateFlow =
|
||||
activeTunnels
|
||||
.map { it[id] }
|
||||
.stateIn(applicationScope + ioDispatcher)
|
||||
Timber.d("Starting Dynamic DNS monitoring job for tunnelId: $id")
|
||||
jobs[id] =
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
monitorDynamicDns(config, tunStateFlow)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun monitorDynamicDns(
|
||||
config: TunnelConfig,
|
||||
tunStateFlow: StateFlow<TunnelState?>,
|
||||
) {
|
||||
var backoff = BASE_BACKOFF
|
||||
while (true) {
|
||||
val state = tunStateFlow.value ?: break
|
||||
if (state.health() != TunnelState.Health.UNHEALTHY) {
|
||||
backoff = BASE_BACKOFF
|
||||
tunStateFlow.first { it?.health() == TunnelState.Health.UNHEALTHY || it == null }
|
||||
continue
|
||||
}
|
||||
|
||||
runCatching {
|
||||
val updated = handleDnsReresolve(config)
|
||||
if (updated) {
|
||||
localMessageEvents.emit(config.name to BackendMessage.DynamicDnsSuccess)
|
||||
backoff = BASE_BACKOFF
|
||||
} else {
|
||||
Timber.i(
|
||||
"Dynamic DNS check completed, current endpoint address is already up to date."
|
||||
)
|
||||
}
|
||||
}
|
||||
.onFailure { Timber.e(it, "Failed to handle dns re-resolution for ${config.name}") }
|
||||
|
||||
delay(backoff)
|
||||
backoff = (backoff * 1.5).toLong().coerceAtMost(MAX_BACKOFF_TIME)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val BASE_BACKOFF = 30_000L
|
||||
const val MAX_BACKOFF_TIME = 300_000L
|
||||
}
|
||||
}
|
||||
-47
@@ -1,47 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.tunnel.handler
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.supervisorScope
|
||||
|
||||
class TunnelActiveStatePersister(
|
||||
private val activeTunnels: StateFlow<Map<Int, TunnelState>>,
|
||||
private val tunnelsRepository: TunnelRepository,
|
||||
applicationScope: CoroutineScope,
|
||||
ioDispatcher: CoroutineDispatcher,
|
||||
) {
|
||||
private var previousActiveIds: Set<Int> = emptySet()
|
||||
|
||||
init {
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
activeTunnels.collect { currentActive ->
|
||||
val currentActiveIds = currentActive.keys
|
||||
if (currentActiveIds == previousActiveIds) return@collect
|
||||
|
||||
val tunnels = tunnelsRepository.userTunnelsFlow.firstOrNull() ?: return@collect
|
||||
val tunnelsById = tunnels.associateBy { it.id }
|
||||
|
||||
val relevantIds = previousActiveIds + currentActiveIds
|
||||
|
||||
supervisorScope {
|
||||
relevantIds.forEach { id ->
|
||||
launch {
|
||||
val config = tunnelsById[id] ?: return@launch
|
||||
val wasActive = previousActiveIds.contains(id)
|
||||
val isActive = currentActiveIds.contains(id)
|
||||
if (wasActive != isActive) {
|
||||
tunnelsRepository.save(config.copy(isActive = isActive))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
previousActiveIds = currentActiveIds.toSet()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-388
@@ -1,388 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.tunnel.handler
|
||||
|
||||
import android.os.PowerManager
|
||||
import com.zaneschepke.logcatter.LogReader
|
||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.FailureReason
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.LogHealthState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.PingState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.toMillis
|
||||
import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils
|
||||
import inet.ipaddr.AddressValueException
|
||||
import inet.ipaddr.IPAddress
|
||||
import inet.ipaddr.IPAddressString
|
||||
import io.ktor.util.collections.ConcurrentMap
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.plus
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import timber.log.Timber
|
||||
|
||||
class TunnelMonitorHandler(
|
||||
private val activeTunnels: StateFlow<Map<Int, TunnelState>>,
|
||||
private val tunnelsRepository: TunnelRepository,
|
||||
private val settingsRepository: GeneralSettingRepository,
|
||||
private val monitoringSettingsRepository: MonitoringSettingsRepository,
|
||||
private val networkMonitor: NetworkMonitor,
|
||||
private val networkUtils: NetworkUtils,
|
||||
private val logReader: LogReader,
|
||||
private val powerManager: PowerManager,
|
||||
private val getStatistics: (Int) -> TunnelStatistics?,
|
||||
private val updateTunnelStatus:
|
||||
suspend (
|
||||
Int, TunnelStatus?, TunnelStatistics?, Map<String, PingState>?, LogHealthState?,
|
||||
) -> Unit,
|
||||
private val applicationScope: CoroutineScope,
|
||||
private val ioDispatcher: CoroutineDispatcher,
|
||||
) {
|
||||
private val mutex = Mutex()
|
||||
private val jobs = ConcurrentHashMap<Int, Job>()
|
||||
|
||||
init {
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
activeTunnels.collect { activeTuns ->
|
||||
mutex.withLock {
|
||||
val activeIds = activeTuns.keys.toSet()
|
||||
(jobs.keys - activeIds).forEach { id ->
|
||||
Timber.d("Shutting down tunnel monitoring job for tunnelId: $id")
|
||||
jobs.remove(id)?.cancel()
|
||||
}
|
||||
|
||||
val tunnels = tunnelsRepository.flow.firstOrNull() ?: return@collect
|
||||
val tunnelsById = tunnels.associateBy { it.id }
|
||||
|
||||
activeIds.forEach { id ->
|
||||
if (jobs.containsKey(id)) return@forEach
|
||||
val config = tunnelsById[id] ?: return@forEach
|
||||
val settings = settingsRepository.flow.filterNotNull().first()
|
||||
val tunStateFlow =
|
||||
activeTunnels.map { it[id] }.stateIn(applicationScope + ioDispatcher)
|
||||
jobs[id] =
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
Timber.d("Starting tunnel monitoring job for tunnelId: $id")
|
||||
startMonitoring(
|
||||
config = config,
|
||||
withLogs = settings.appMode != AppMode.KERNEL,
|
||||
tunStateFlow = tunStateFlow,
|
||||
getStatistics = { tunnelId -> getStatistics(tunnelId) },
|
||||
updateTunnelStatus = { tid, _, stats, pings, logHealth ->
|
||||
updateTunnelStatus(tid, null, stats, pings, logHealth)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
private suspend fun startMonitoring(
|
||||
config: TunnelConfig,
|
||||
withLogs: Boolean,
|
||||
tunStateFlow: StateFlow<TunnelState?>,
|
||||
getStatistics: suspend (Int) -> TunnelStatistics?,
|
||||
updateTunnelStatus:
|
||||
suspend (
|
||||
Int, TunnelStatus?, TunnelStatistics?, Map<String, PingState>?, LogHealthState?,
|
||||
) -> Unit,
|
||||
) = coroutineScope {
|
||||
launch { startPingMonitor(config, tunStateFlow, updateTunnelStatus) }
|
||||
launch { startWgStatsPoll(config.id, getStatistics, updateTunnelStatus) }
|
||||
if (withLogs) launch { startLogsMonitor(config, updateTunnelStatus) }
|
||||
}
|
||||
|
||||
private suspend fun startLogsMonitor(
|
||||
tunnelConfig: TunnelConfig,
|
||||
updateTunnelStatus:
|
||||
suspend (
|
||||
Int, TunnelStatus?, TunnelStatistics?, Map<String, PingState>?, LogHealthState?,
|
||||
) -> Unit,
|
||||
) {
|
||||
logReader.liveLogs
|
||||
.filter { log -> log.tag.contains(tunnelConfig.name) }
|
||||
.mapNotNull { log ->
|
||||
val now = System.currentTimeMillis()
|
||||
|
||||
when {
|
||||
successLogRegex.containsMatchIn(log.message) ->
|
||||
LogHealthState(isHealthy = true, timestamp = now)
|
||||
|
||||
failureLogRegex.containsMatchIn(log.message) ->
|
||||
LogHealthState(isHealthy = false, timestamp = now)
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
.distinctUntilChangedBy { it.isHealthy }
|
||||
.collect { logHealthState ->
|
||||
Timber.d("Tunnel log health updated for ${tunnelConfig.name}: $logHealthState")
|
||||
updateTunnelStatus(tunnelConfig.id, null, null, null, logHealthState)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun startPingMonitor(
|
||||
tunnelConfig: TunnelConfig,
|
||||
tunStateFlow: StateFlow<TunnelState?>,
|
||||
updateTunnelStatus:
|
||||
suspend (
|
||||
Int, TunnelStatus?, TunnelStatistics?, Map<String, PingState>?, LogHealthState?,
|
||||
) -> Unit,
|
||||
) = coroutineScope {
|
||||
val pingStatsFlow = MutableStateFlow<Map<String, PingState>>(emptyMap())
|
||||
|
||||
val connectivityStateFlow = networkMonitor.connectivityStateFlow.stateIn(this)
|
||||
|
||||
val isNetworkConnected = connectivityStateFlow.map { it.hasInternet() }.stateIn(this)
|
||||
|
||||
combine(
|
||||
settingsRepository.flow.distinctUntilChangedBy { it.appMode },
|
||||
monitoringSettingsRepository.flow,
|
||||
) { settings, monitorSettings ->
|
||||
Pair(settings.appMode, monitorSettings)
|
||||
}
|
||||
.collectLatest { (appMode, settings) ->
|
||||
if (!settings.isPingEnabled) return@collectLatest
|
||||
// TODO for now until we get monitoring for these modes
|
||||
if (appMode == AppMode.LOCK_DOWN || appMode == AppMode.PROXY) return@collectLatest
|
||||
|
||||
Timber.d("Starting pinger for ${tunnelConfig.name} with settings")
|
||||
|
||||
val config = tunnelConfig.toAmConfig()
|
||||
|
||||
val pingablePeers = config.peers.filter { it.allowedIps.isNotEmpty() }
|
||||
if (pingablePeers.isEmpty()) return@collectLatest
|
||||
|
||||
suspend fun performPing() {
|
||||
val updates = ConcurrentMap<String, PingState>()
|
||||
|
||||
pingablePeers
|
||||
.map { it.publicKey.toBase64() to it }
|
||||
.forEach { (key, peer) ->
|
||||
ensureActive()
|
||||
val previousState = pingStatsFlow.value[key] ?: PingState()
|
||||
|
||||
val allowedIpStr = peer.allowedIps.firstOrNull()?.toString()
|
||||
if (allowedIpStr == null) {
|
||||
updates[key] =
|
||||
previousState.copy(
|
||||
isReachable = false,
|
||||
failureReason = FailureReason.NoResolvedEndpoint,
|
||||
lastPingAttemptMillis = System.currentTimeMillis(),
|
||||
)
|
||||
return@forEach
|
||||
}
|
||||
|
||||
val host =
|
||||
tunnelConfig.pingTarget
|
||||
?: run {
|
||||
val parts = allowedIpStr.split("/")
|
||||
val internalIp =
|
||||
if (parts.size == 2) parts[0] else allowedIpStr
|
||||
val prefix =
|
||||
if (parts.size == 2) parts[1].toIntOrNull() ?: 32
|
||||
else 32
|
||||
val cleanedIp = internalIp.removeSurrounding("[", "]")
|
||||
val defaultCloudflare =
|
||||
if (cleanedIp.contains(":")) CLOUDFLARE_IPV6_IP
|
||||
else CLOUDFLARE_IPV4_IP
|
||||
|
||||
if (prefix <= 1) {
|
||||
defaultCloudflare
|
||||
} else {
|
||||
try {
|
||||
val addrStr = IPAddressString(cleanedIp)
|
||||
val addr: IPAddress =
|
||||
addrStr.address
|
||||
?: throw AddressValueException(
|
||||
"Invalid IP: $cleanedIp"
|
||||
)
|
||||
val isIpv6 = addr.isIPv6
|
||||
val cloudflareIp =
|
||||
if (isIpv6) CLOUDFLARE_IPV6_IP
|
||||
else CLOUDFLARE_IPV4_IP
|
||||
val max = if (isIpv6) 128 else 32
|
||||
|
||||
if (prefix == max) {
|
||||
addr.toCanonicalString()
|
||||
} else {
|
||||
val nextAddr: IPAddress? = addr.increment(1)
|
||||
nextAddr?.toCanonicalString() ?: cloudflareIp
|
||||
}
|
||||
} catch (e: AddressValueException) {
|
||||
Timber.e(
|
||||
e,
|
||||
"Failed to parse or increment IP: $cleanedIp",
|
||||
)
|
||||
defaultCloudflare
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val attemptTime = System.currentTimeMillis()
|
||||
val timeout = settings.tunnelPingTimeoutSeconds?.toMillis() ?: 5000L
|
||||
runCatching {
|
||||
withTimeout(
|
||||
settings.tunnelPingTimeoutSeconds?.toMillis() ?: 5000L
|
||||
) {
|
||||
val pingStats =
|
||||
settings.tunnelPingTimeoutSeconds?.let {
|
||||
networkUtils.pingWithStats(
|
||||
host,
|
||||
settings.tunnelPingAttempts,
|
||||
it.toMillis(),
|
||||
)
|
||||
}
|
||||
?: networkUtils.pingWithStats(
|
||||
host,
|
||||
settings.tunnelPingAttempts,
|
||||
)
|
||||
|
||||
updates[key] =
|
||||
previousState.copy(
|
||||
transmitted = pingStats.transmitted,
|
||||
received = pingStats.received,
|
||||
packetLoss = pingStats.packetLoss,
|
||||
rttMin = pingStats.rttMin,
|
||||
rttMax = pingStats.rttMax,
|
||||
rttAvg = pingStats.rttAvg,
|
||||
rttStddev = pingStats.rttStddev,
|
||||
isReachable = pingStats.isReachable,
|
||||
failureReason =
|
||||
if (pingStats.isReachable) null
|
||||
else FailureReason.PingFailed,
|
||||
lastSuccessfulPingMillis =
|
||||
pingStats.lastSuccessfulPingMillis
|
||||
?: previousState.lastSuccessfulPingMillis,
|
||||
pingTarget = host,
|
||||
lastPingAttemptMillis = attemptTime,
|
||||
)
|
||||
Timber.d(
|
||||
"Ping completed for peer ${peer.publicKey.toBase64().substring(0, 5)}.. to host $host with stats: $pingStats"
|
||||
)
|
||||
}
|
||||
}
|
||||
.onFailure {
|
||||
Timber.e(
|
||||
it,
|
||||
"Ping failed for peer ${peer.publicKey} in ${tunnelConfig.name} to host $host",
|
||||
)
|
||||
updates[key] =
|
||||
previousState.copy(
|
||||
isReachable = false,
|
||||
failureReason = FailureReason.PingFailed,
|
||||
pingTarget = host,
|
||||
lastPingAttemptMillis = attemptTime,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (updates.isNotEmpty()) {
|
||||
ensureActive()
|
||||
pingStatsFlow.update { updates }
|
||||
updateTunnelStatus(tunnelConfig.id, null, null, updates, null)
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for the tunnel to be fully active
|
||||
tunStateFlow.filter { state -> state?.status is TunnelStatus.Up }.first()
|
||||
|
||||
// small delay to make sure tunnel is fully up before we actively monitor
|
||||
delay(PING_MONITOR_START_DELAY)
|
||||
|
||||
while (isActive) {
|
||||
ensureActive()
|
||||
if (!powerManager.isDeviceIdleMode) {
|
||||
if (isNetworkConnected.value) {
|
||||
performPing()
|
||||
} else {
|
||||
pingStatsFlow.update { current ->
|
||||
current.mapValues { entry ->
|
||||
entry.value.copy(
|
||||
isReachable = false,
|
||||
failureReason = FailureReason.NoConnectivity,
|
||||
lastPingAttemptMillis = System.currentTimeMillis(),
|
||||
)
|
||||
}
|
||||
}
|
||||
ensureActive()
|
||||
updateTunnelStatus(
|
||||
tunnelConfig.id,
|
||||
null,
|
||||
null,
|
||||
pingStatsFlow.value,
|
||||
null,
|
||||
)
|
||||
}
|
||||
}
|
||||
delay(settings.tunnelPingIntervalSeconds.toMillis())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun startWgStatsPoll(
|
||||
tunnelId: Int,
|
||||
getStatistics: suspend (Int) -> TunnelStatistics?,
|
||||
updateTunnelStatus:
|
||||
suspend (
|
||||
Int, TunnelStatus?, TunnelStatistics?, Map<String, PingState>?, LogHealthState?,
|
||||
) -> Unit,
|
||||
) = coroutineScope {
|
||||
while (isActive) {
|
||||
ensureActive()
|
||||
val stats = getStatistics(tunnelId)
|
||||
ensureActive()
|
||||
updateTunnelStatus(tunnelId, null, stats, null, null)
|
||||
delay(STATS_DELAY)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val successLogRegex =
|
||||
Regex("Received handshake response|Receiving keepalive packet", RegexOption.IGNORE_CASE)
|
||||
|
||||
private val failureLogRegex =
|
||||
Regex(
|
||||
"Failed to send handshake initiation: write udp|" +
|
||||
"Handshake did not complete after 5 seconds, retrying|" +
|
||||
"Failed to send data packets",
|
||||
RegexOption.IGNORE_CASE,
|
||||
)
|
||||
|
||||
const val CLOUDFLARE_IPV6_IP = "2606:4700:4700::1111"
|
||||
const val CLOUDFLARE_IPV4_IP = "1.1.1.1"
|
||||
const val STATS_DELAY = 1_000L
|
||||
const val PING_MONITOR_START_DELAY = 5_000L
|
||||
}
|
||||
}
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.tunnel.handler
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
class TunnelServiceHandler(
|
||||
private val activeTunnels: StateFlow<Map<Int, TunnelState>>,
|
||||
private val settingsRepository: GeneralSettingRepository,
|
||||
private val serviceManager: ServiceManager,
|
||||
applicationScope: CoroutineScope,
|
||||
ioDispatcher: CoroutineDispatcher,
|
||||
) {
|
||||
init {
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
activeTunnels.collect { activeTuns ->
|
||||
if (activeTuns.isEmpty()) {
|
||||
Timber.d("Stopping tunnel service, no tunnels active.")
|
||||
serviceManager.stopTunnelService()
|
||||
} else if (serviceManager.tunnelService.value == null) {
|
||||
val settings = settingsRepository.flow.firstOrNull() ?: GeneralSettings()
|
||||
Timber.d("Starting tunnel foreground service for active tunnel.")
|
||||
serviceManager.startTunnelService(settings.appMode)
|
||||
}
|
||||
serviceManager.updateTunnelTile()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkerParameters
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelStateHolder
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
||||
import java.util.concurrent.TimeUnit
|
||||
import timber.log.Timber
|
||||
@@ -16,6 +17,7 @@ class ServiceWorker(
|
||||
params: WorkerParameters,
|
||||
private val serviceManager: ServiceManager,
|
||||
private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
|
||||
private val autoTunnelStateHolder: AutoTunnelStateHolder,
|
||||
) : CoroutineWorker(context, params) {
|
||||
|
||||
companion object {
|
||||
@@ -42,14 +44,18 @@ class ServiceWorker(
|
||||
}
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
Timber.i("Service worker started")
|
||||
with(autoTunnelSettingsRepository.getAutoTunnelSettings()) {
|
||||
Timber.i("Checking to see if auto-tunnel has been killed by system")
|
||||
if (isAutoTunnelEnabled && serviceManager.autoTunnelService.value == null) {
|
||||
Timber.i("Service has been killed by system, restoring.")
|
||||
serviceManager.startAutoTunnelService()
|
||||
}
|
||||
Timber.i("AutoTunnel reconciliation worker running")
|
||||
|
||||
val settings = autoTunnelSettingsRepository.getAutoTunnelSettings()
|
||||
|
||||
if (!settings.isAutoTunnelEnabled) {
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
if (autoTunnelStateHolder.active.value) return Result.success()
|
||||
|
||||
serviceManager.startAutoTunnelService()
|
||||
|
||||
return Result.success()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,27 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data
|
||||
|
||||
import androidx.room.*
|
||||
import androidx.room.AutoMigration
|
||||
import androidx.room.Database
|
||||
import androidx.room.DeleteColumn
|
||||
import androidx.room.RenameColumn
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import androidx.room.migration.AutoMigrationSpec
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import com.zaneschepke.wireguardautotunnel.data.dao.*
|
||||
import com.zaneschepke.wireguardautotunnel.data.entity.*
|
||||
import com.zaneschepke.wireguardautotunnel.data.dao.AutoTunnelSettingsDao
|
||||
import com.zaneschepke.wireguardautotunnel.data.dao.DnsSettingsDao
|
||||
import com.zaneschepke.wireguardautotunnel.data.dao.GeneralSettingsDao
|
||||
import com.zaneschepke.wireguardautotunnel.data.dao.LockdownSettingsDao
|
||||
import com.zaneschepke.wireguardautotunnel.data.dao.MonitoringSettingsDao
|
||||
import com.zaneschepke.wireguardautotunnel.data.dao.ProxySettingsDao
|
||||
import com.zaneschepke.wireguardautotunnel.data.dao.TunnelConfigDao
|
||||
import com.zaneschepke.wireguardautotunnel.data.entity.AutoTunnelSettings
|
||||
import com.zaneschepke.wireguardautotunnel.data.entity.DnsSettings
|
||||
import com.zaneschepke.wireguardautotunnel.data.entity.GeneralSettings
|
||||
import com.zaneschepke.wireguardautotunnel.data.entity.LockdownSettings
|
||||
import com.zaneschepke.wireguardautotunnel.data.entity.MonitoringSettings
|
||||
import com.zaneschepke.wireguardautotunnel.data.entity.ProxySettings
|
||||
import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
|
||||
|
||||
@Database(
|
||||
entities =
|
||||
@@ -17,7 +34,7 @@ import com.zaneschepke.wireguardautotunnel.data.entity.*
|
||||
DnsSettings::class,
|
||||
LockdownSettings::class,
|
||||
],
|
||||
version = 29,
|
||||
version = 30,
|
||||
autoMigrations =
|
||||
[
|
||||
AutoMigration(from = 1, to = 2),
|
||||
@@ -45,6 +62,7 @@ import com.zaneschepke.wireguardautotunnel.data.entity.*
|
||||
AutoMigration(from = 24, to = 25),
|
||||
AutoMigration(from = 26, to = 27, spec = GlobalsMigration::class),
|
||||
AutoMigration(from = 27, to = 28, spec = DonationMigration::class),
|
||||
AutoMigration(from = 29, to = 30, spec = SingleConfigMigration::class),
|
||||
],
|
||||
exportSchema = true,
|
||||
)
|
||||
@@ -129,3 +147,60 @@ class GlobalsMigration : AutoMigrationSpec
|
||||
|
||||
@DeleteColumn(tableName = "general_settings", columnName = "custom_split_packages")
|
||||
class DonationMigration : AutoMigrationSpec
|
||||
|
||||
@RenameColumn.Entries(
|
||||
RenameColumn(
|
||||
tableName = "tunnel_config",
|
||||
fromColumnName = "is_ipv4_preferred",
|
||||
toColumnName = "prefer_ipv6",
|
||||
),
|
||||
RenameColumn(
|
||||
tableName = "tunnel_config",
|
||||
fromColumnName = "am_quick",
|
||||
toColumnName = "quick_config",
|
||||
),
|
||||
RenameColumn(
|
||||
tableName = "tunnel_config",
|
||||
fromColumnName = "restart_on_ping_failure",
|
||||
toColumnName = "dynamic_dns",
|
||||
),
|
||||
)
|
||||
@DeleteColumn.Entries(
|
||||
DeleteColumn(tableName = "tunnel_config", columnName = "wg_quick"),
|
||||
DeleteColumn(tableName = "tunnel_config", columnName = "ping_target"),
|
||||
DeleteColumn(tableName = "tunnel_config", columnName = "is_Active"),
|
||||
DeleteColumn(tableName = "monitoring_settings", columnName = "is_ping_enabled"),
|
||||
DeleteColumn(tableName = "monitoring_settings", columnName = "is_ping_monitoring_enabled"),
|
||||
DeleteColumn(tableName = "monitoring_settings", columnName = "tunnel_ping_interval_sec"),
|
||||
DeleteColumn(tableName = "monitoring_settings", columnName = "tunnel_ping_attempts"),
|
||||
DeleteColumn(tableName = "monitoring_settings", columnName = "tunnel_ping_timeout_sec"),
|
||||
DeleteColumn(tableName = "monitoring_settings", columnName = "show_detailed_ping_stats"),
|
||||
DeleteColumn(tableName = "auto_tunnel_settings", columnName = "debounce_delay_seconds"),
|
||||
)
|
||||
class SingleConfigMigration : AutoMigrationSpec {
|
||||
|
||||
override fun onPostMigrate(db: SupportSQLiteDatabase) {
|
||||
db.execSQL(
|
||||
"""
|
||||
UPDATE tunnel_config
|
||||
SET prefer_ipv6 =
|
||||
CASE prefer_ipv6
|
||||
WHEN 1 THEN 0
|
||||
WHEN 0 THEN 1
|
||||
ELSE 0
|
||||
END
|
||||
"""
|
||||
)
|
||||
|
||||
db.execSQL(
|
||||
"""
|
||||
UPDATE general_settings
|
||||
SET app_mode = CASE app_mode
|
||||
WHEN 3 THEN 0
|
||||
ELSE app_mode
|
||||
END
|
||||
"""
|
||||
.trimIndent()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data
|
||||
|
||||
import androidx.room.TypeConverter
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.DnsProtocol
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.WifiDetectionMethod
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.DnsProtocol
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.WifiDetectionMethod
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
class DatabaseConverters {
|
||||
@@ -57,9 +57,9 @@ class DatabaseConverters {
|
||||
@TypeConverter
|
||||
fun toStatus(value: Int): WifiDetectionMethod = WifiDetectionMethod.fromValue(value)
|
||||
|
||||
@TypeConverter fun toMode(value: Int): AppMode = AppMode.fromValue(value)
|
||||
@TypeConverter fun toMode(value: Int): TunnelMode = TunnelMode.fromValue(value)
|
||||
|
||||
@TypeConverter fun fromMode(mode: AppMode): Int = mode.value
|
||||
@TypeConverter fun fromMode(mode: TunnelMode): Int = mode.value
|
||||
|
||||
@TypeConverter fun toDnsProtocol(value: Int): DnsProtocol = DnsProtocol.fromValue(value)
|
||||
|
||||
|
||||
@@ -13,4 +13,7 @@ interface DnsSettingsDao {
|
||||
@Upsert suspend fun upsert(dnsSettings: DnsSettings)
|
||||
|
||||
@Query("SELECT * FROM dns_settings LIMIT 1") fun getDnsSettingsFlow(): Flow<DnsSettings?>
|
||||
|
||||
@Query("UPDATE dns_settings SET global_tunnel_dns_enabled = :enabled")
|
||||
suspend fun updateGlobalDnsEnabled(enabled: Boolean)
|
||||
}
|
||||
|
||||
+9
-3
@@ -4,7 +4,7 @@ import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import androidx.room.Upsert
|
||||
import com.zaneschepke.wireguardautotunnel.data.entity.GeneralSettings
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
@@ -26,6 +26,12 @@ interface GeneralSettingsDao {
|
||||
@Query("UPDATE general_settings SET is_pin_lock_enabled = :enabled WHERE id = 1")
|
||||
suspend fun updatePinLockEnabled(enabled: Boolean)
|
||||
|
||||
@Query("UPDATE general_settings SET app_mode = :appMode WHERE id = 1")
|
||||
suspend fun updateAppMode(appMode: AppMode)
|
||||
@Query("UPDATE general_settings SET app_mode = :tunnelMode WHERE id = 1")
|
||||
suspend fun updateAppMode(tunnelMode: TunnelMode)
|
||||
|
||||
@Query("UPDATE general_settings SET global_amnezia_enabled = :enabled")
|
||||
suspend fun updateGlobalAmneziaEnabled(enabled: Boolean)
|
||||
|
||||
@Query("UPDATE general_settings SET screen_recording_security = :enabled")
|
||||
suspend fun updateScreenRecordingSecurity(enabled: Boolean)
|
||||
}
|
||||
|
||||
+22
@@ -15,4 +15,26 @@ interface MonitoringSettingsDao {
|
||||
|
||||
@Query("SELECT * FROM monitoring_settings LIMIT 1")
|
||||
fun getMonitoringSettingsFlow(): Flow<MonitoringSettings?>
|
||||
|
||||
@Query(
|
||||
"""
|
||||
UPDATE monitoring_settings
|
||||
SET tunnel_statistics_poll_interval = :interval
|
||||
WHERE id = (
|
||||
SELECT id FROM monitoring_settings LIMIT 1
|
||||
)
|
||||
"""
|
||||
)
|
||||
suspend fun updateStatisticsInterval(interval: Int)
|
||||
|
||||
@Query(
|
||||
"""
|
||||
UPDATE monitoring_settings
|
||||
SET tunnel_statistics_enabled = :enabled
|
||||
WHERE id = (
|
||||
SELECT id FROM monitoring_settings LIMIT 1
|
||||
)
|
||||
"""
|
||||
)
|
||||
suspend fun updateStatisticsEnabled(enabled: Boolean)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.dao
|
||||
|
||||
import androidx.room.*
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Upsert
|
||||
import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@@ -11,17 +16,17 @@ interface TunnelConfigDao {
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun saveAll(t: List<TunnelConfig>)
|
||||
|
||||
@Query("SELECT * FROM tunnel_config WHERE id=:id") suspend fun getById(id: Long): TunnelConfig?
|
||||
@Query("UPDATE tunnel_config SET is_metered = :value WHERE id = :id")
|
||||
suspend fun setMetered(id: Int, value: Boolean)
|
||||
|
||||
@Query("UPDATE tunnel_config SET is_Active = 0 WHERE is_Active = 1")
|
||||
suspend fun resetActiveTunnels()
|
||||
@Query("UPDATE tunnel_config SET dynamic_dns = :value WHERE id = :id")
|
||||
suspend fun setDynamicDns(id: Int, value: Boolean)
|
||||
|
||||
@Query("SELECT * FROM tunnel_config WHERE id=:id") suspend fun getById(id: Long): TunnelConfig?
|
||||
|
||||
@Query("SELECT * FROM tunnel_config WHERE name=:name")
|
||||
suspend fun getByName(name: String): TunnelConfig?
|
||||
|
||||
@Query("SELECT * FROM tunnel_config WHERE is_Active=1")
|
||||
suspend fun getActive(): List<TunnelConfig>
|
||||
|
||||
@Query("SELECT * FROM tunnel_config") suspend fun getAll(): List<TunnelConfig>
|
||||
|
||||
@Delete suspend fun delete(t: TunnelConfig)
|
||||
@@ -50,30 +55,15 @@ interface TunnelConfigDao {
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM tunnel_config
|
||||
WHERE name != '${TunnelConfig.GLOBAL_CONFIG_NAME}'
|
||||
ORDER BY
|
||||
CASE WHEN is_primary_tunnel = 1 THEN 0 ELSE 1 END,
|
||||
position ASC
|
||||
LIMIT 1
|
||||
"""
|
||||
SELECT *
|
||||
FROM tunnel_config
|
||||
WHERE name != '${TunnelConfig.GLOBAL_CONFIG_NAME}'
|
||||
ORDER BY is_primary_tunnel DESC, position ASC
|
||||
LIMIT 1
|
||||
"""
|
||||
)
|
||||
suspend fun getDefaultTunnel(): TunnelConfig?
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM tunnel_config
|
||||
WHERE name != '${TunnelConfig.GLOBAL_CONFIG_NAME}'
|
||||
ORDER BY
|
||||
CASE WHEN is_Active = 1 THEN 0
|
||||
WHEN is_primary_tunnel = 1 THEN 1
|
||||
ELSE 2 END,
|
||||
position ASC
|
||||
LIMIT 1
|
||||
"""
|
||||
)
|
||||
suspend fun getStartTunnel(): TunnelConfig?
|
||||
|
||||
@Query("SELECT * FROM tunnel_config ORDER BY position")
|
||||
fun getAllFlow(): Flow<List<TunnelConfig>>
|
||||
|
||||
|
||||
+1
-3
@@ -3,7 +3,7 @@ package com.zaneschepke.wireguardautotunnel.data.entity
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.WifiDetectionMethod
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.WifiDetectionMethod
|
||||
|
||||
@Entity(tableName = "auto_tunnel_settings")
|
||||
data class AutoTunnelSettings(
|
||||
@@ -22,8 +22,6 @@ data class AutoTunnelSettings(
|
||||
val isWildcardsEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "is_stop_on_no_internet_enabled", defaultValue = "0")
|
||||
val isStopOnNoInternetEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "debounce_delay_seconds", defaultValue = "3")
|
||||
val debounceDelaySeconds: Int = 3,
|
||||
@ColumnInfo(name = "is_tunnel_on_unsecure_enabled", defaultValue = "0")
|
||||
val isTunnelOnUnsecureEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "wifi_detection_method", defaultValue = "0")
|
||||
|
||||
@@ -3,7 +3,7 @@ package com.zaneschepke.wireguardautotunnel.data.entity
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.DnsProtocol
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.DnsProtocol
|
||||
|
||||
@Entity(tableName = "dns_settings")
|
||||
data class DnsSettings(
|
||||
|
||||
+9
-2
@@ -3,7 +3,7 @@ package com.zaneschepke.wireguardautotunnel.data.entity
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||
|
||||
@Entity(tableName = "general_settings")
|
||||
data class GeneralSettings(
|
||||
@@ -16,7 +16,8 @@ data class GeneralSettings(
|
||||
val isMultiTunnelEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "global_split_tunnel_enabled", defaultValue = "0")
|
||||
val isGlobalSplitTunnelEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "app_mode", defaultValue = "0") val appMode: AppMode = AppMode.fromValue(0),
|
||||
@ColumnInfo(name = "app_mode", defaultValue = "0")
|
||||
val tunnelMode: TunnelMode = TunnelMode.fromValue(0),
|
||||
@ColumnInfo(name = "theme", defaultValue = "AUTOMATIC") val theme: String = "AUTOMATIC",
|
||||
@ColumnInfo(name = "locale") val locale: String? = null,
|
||||
@ColumnInfo(name = "remote_key") val remoteKey: String? = null,
|
||||
@@ -27,4 +28,10 @@ data class GeneralSettings(
|
||||
@ColumnInfo(name = "is_always_on_vpn_enabled", defaultValue = "0")
|
||||
val isAlwaysOnVpnEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "already_donated", defaultValue = "0") val alreadyDonated: Boolean = false,
|
||||
@ColumnInfo(name = "screen_recording_security", defaultValue = "1")
|
||||
val screenRecordingSecurityEnabled: Boolean = true,
|
||||
@ColumnInfo(name = "global_amnezia_enabled", defaultValue = "0")
|
||||
val isGlobalAmneziaEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "tunnel_scripting_enabled", defaultValue = "0")
|
||||
val tunnelScriptingEnabled: Boolean = true,
|
||||
)
|
||||
|
||||
+4
-9
@@ -7,15 +7,10 @@ import androidx.room.PrimaryKey
|
||||
@Entity(tableName = "monitoring_settings")
|
||||
data class MonitoringSettings(
|
||||
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
||||
@ColumnInfo(name = "is_ping_enabled", defaultValue = "0") val isPingEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "is_ping_monitoring_enabled", defaultValue = "1")
|
||||
val isPingMonitoringEnabled: Boolean = true,
|
||||
@ColumnInfo(name = "tunnel_ping_interval_sec", defaultValue = "30")
|
||||
val tunnelPingIntervalSeconds: Int = 30,
|
||||
@ColumnInfo(name = "tunnel_ping_attempts", defaultValue = "3") val tunnelPingAttempts: Int = 3,
|
||||
@ColumnInfo(name = "tunnel_ping_timeout_sec") val tunnelPingTimeoutSeconds: Int? = null,
|
||||
@ColumnInfo(name = "show_detailed_ping_stats", defaultValue = "0")
|
||||
val showDetailedPingStats: Boolean = false,
|
||||
@ColumnInfo(name = "is_local_logs_enabled", defaultValue = "0")
|
||||
val isLocalLogsEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "tunnel_statistics_enabled", defaultValue = "1")
|
||||
val tunnelStatisticsEnabled: Boolean = true,
|
||||
@ColumnInfo(name = "tunnel_statistics_poll_interval", defaultValue = "3")
|
||||
val tunnelStatisticsPollInterval: Int = 3,
|
||||
)
|
||||
|
||||
@@ -9,26 +9,26 @@ import androidx.room.PrimaryKey
|
||||
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: Set<String> = setOf(),
|
||||
@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 = "",
|
||||
@ColumnInfo(name = "is_Active", defaultValue = "false") val isActive: Boolean = false,
|
||||
@ColumnInfo(name = "restart_on_ping_failure", defaultValue = "false")
|
||||
val restartOnPingFailure: Boolean = false,
|
||||
@ColumnInfo(name = "ping_target", defaultValue = "null") var pingTarget: String? = null,
|
||||
@ColumnInfo(name = "quick_config", defaultValue = "") val quickConfig: String = "",
|
||||
@ColumnInfo(name = "dynamic_dns", defaultValue = "false")
|
||||
val dynamicDnsEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "is_ethernet_tunnel", defaultValue = "false")
|
||||
val isEthernetTunnel: Boolean = false,
|
||||
@ColumnInfo(name = "is_ipv4_preferred", defaultValue = "true")
|
||||
val isIpv4Preferred: Boolean = true,
|
||||
@ColumnInfo(name = "prefer_ipv6", defaultValue = "false") val isIpv6Preferred: Boolean = false,
|
||||
@ColumnInfo(name = "position", defaultValue = "0") val position: Int = 0,
|
||||
@ColumnInfo(name = "auto_tunnel_apps", defaultValue = "[]")
|
||||
val autoTunnelApps: Set<String> = emptySet(),
|
||||
@ColumnInfo(name = "is_metered", defaultValue = "false") val isMetered: Boolean = false,
|
||||
@ColumnInfo(name = "ipv4_fallback", defaultValue = "false")
|
||||
val ipv4FallbackEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "ipv6_restore", defaultValue = "false")
|
||||
val ipv6RestoreEnabled: Boolean = false,
|
||||
) {
|
||||
companion object {
|
||||
const val GLOBAL_CONFIG_NAME = "4675ab06-903a-438b-8485-6ea4187a9512"
|
||||
|
||||
-2
@@ -13,7 +13,6 @@ fun Entity.toDomain(): Domain =
|
||||
isTunnelOnWifiEnabled = isTunnelOnWifiEnabled,
|
||||
isWildcardsEnabled = isWildcardsEnabled,
|
||||
isStopOnNoInternetEnabled = isStopOnNoInternetEnabled,
|
||||
debounceDelaySeconds = debounceDelaySeconds,
|
||||
isTunnelOnUnsecureEnabled = isTunnelOnUnsecureEnabled,
|
||||
wifiDetectionMethod = wifiDetectionMethod,
|
||||
startOnBoot = startOnBoot,
|
||||
@@ -29,7 +28,6 @@ fun Domain.toEntity(): Entity =
|
||||
isTunnelOnWifiEnabled = isTunnelOnWifiEnabled,
|
||||
isWildcardsEnabled = isWildcardsEnabled,
|
||||
isStopOnNoInternetEnabled = isStopOnNoInternetEnabled,
|
||||
debounceDelaySeconds = debounceDelaySeconds,
|
||||
isTunnelOnUnsecureEnabled = isTunnelOnUnsecureEnabled,
|
||||
wifiDetectionMethod = wifiDetectionMethod,
|
||||
startOnBoot = startOnBoot,
|
||||
|
||||
+4
-12
@@ -6,23 +6,15 @@ import com.zaneschepke.wireguardautotunnel.domain.model.MonitoringSettings as Do
|
||||
fun Entity.toDomain(): Domain =
|
||||
Domain(
|
||||
id = id,
|
||||
isPingEnabled = isPingEnabled,
|
||||
isPingMonitoringEnabled = isPingMonitoringEnabled,
|
||||
tunnelPingIntervalSeconds = tunnelPingIntervalSeconds,
|
||||
tunnelPingAttempts = tunnelPingAttempts,
|
||||
tunnelPingTimeoutSeconds = tunnelPingTimeoutSeconds,
|
||||
showDetailedPingStats = showDetailedPingStats,
|
||||
tunnelStatisticsEnabled = tunnelStatisticsEnabled,
|
||||
tunnelStatisticsPollInterval = tunnelStatisticsPollInterval,
|
||||
isLocalLogsEnabled = isLocalLogsEnabled,
|
||||
)
|
||||
|
||||
fun Domain.toEntity(): Entity =
|
||||
Entity(
|
||||
id = id,
|
||||
isPingEnabled = isPingEnabled,
|
||||
isPingMonitoringEnabled = isPingMonitoringEnabled,
|
||||
tunnelPingIntervalSeconds = tunnelPingIntervalSeconds,
|
||||
tunnelPingAttempts = tunnelPingAttempts,
|
||||
tunnelPingTimeoutSeconds = tunnelPingTimeoutSeconds,
|
||||
showDetailedPingStats = showDetailedPingStats,
|
||||
tunnelStatisticsEnabled = tunnelStatisticsEnabled,
|
||||
tunnelStatisticsPollInterval = tunnelStatisticsPollInterval,
|
||||
isLocalLogsEnabled = isLocalLogsEnabled,
|
||||
)
|
||||
|
||||
@@ -11,7 +11,7 @@ fun Entity.toDomain(): Domain =
|
||||
isRestoreOnBootEnabled = isRestoreOnBootEnabled,
|
||||
isMultiTunnelEnabled = isMultiTunnelEnabled,
|
||||
isGlobalSplitTunnelEnabled = isGlobalSplitTunnelEnabled,
|
||||
appMode = appMode,
|
||||
tunnelMode = tunnelMode,
|
||||
theme = Theme.valueOf(theme.uppercase()),
|
||||
locale = locale,
|
||||
remoteKey = remoteKey,
|
||||
@@ -19,6 +19,9 @@ fun Entity.toDomain(): Domain =
|
||||
isPinLockEnabled = isPinLockEnabled,
|
||||
isAlwaysOnVpnEnabled = isAlwaysOnVpnEnabled,
|
||||
alreadyDonated = alreadyDonated,
|
||||
screenRecordingSecurityEnabled = screenRecordingSecurityEnabled,
|
||||
isGlobalAmneziaEnabled = isGlobalAmneziaEnabled,
|
||||
tunnelScriptingEnabled = tunnelScriptingEnabled,
|
||||
)
|
||||
|
||||
fun Domain.toEntity(): Entity =
|
||||
@@ -28,7 +31,7 @@ fun Domain.toEntity(): Entity =
|
||||
isRestoreOnBootEnabled = isRestoreOnBootEnabled,
|
||||
isMultiTunnelEnabled = isMultiTunnelEnabled,
|
||||
isGlobalSplitTunnelEnabled = isGlobalSplitTunnelEnabled,
|
||||
appMode = appMode,
|
||||
tunnelMode = tunnelMode,
|
||||
theme = theme.name,
|
||||
locale = locale,
|
||||
remoteKey = remoteKey,
|
||||
@@ -36,4 +39,7 @@ fun Domain.toEntity(): Entity =
|
||||
isPinLockEnabled = isPinLockEnabled,
|
||||
isAlwaysOnVpnEnabled = isAlwaysOnVpnEnabled,
|
||||
alreadyDonated = alreadyDonated,
|
||||
screenRecordingSecurityEnabled = screenRecordingSecurityEnabled,
|
||||
isGlobalAmneziaEnabled = isGlobalAmneziaEnabled,
|
||||
tunnelScriptingEnabled = tunnelScriptingEnabled,
|
||||
)
|
||||
|
||||
+10
-12
@@ -7,36 +7,34 @@ fun Entity.toDomain(): Domain =
|
||||
Domain(
|
||||
id = id,
|
||||
name = name,
|
||||
wgQuick = wgQuick,
|
||||
tunnelNetworks = tunnelNetworks,
|
||||
isMobileDataTunnel = isMobileDataTunnel,
|
||||
isPrimaryTunnel = isPrimaryTunnel,
|
||||
amQuick = amQuick,
|
||||
isActive = isActive,
|
||||
restartOnPingFailure = restartOnPingFailure,
|
||||
pingTarget = pingTarget,
|
||||
quickConfig = quickConfig,
|
||||
dynamicDnsEnabled = dynamicDnsEnabled,
|
||||
isEthernetTunnel = isEthernetTunnel,
|
||||
isIpv4Preferred = isIpv4Preferred,
|
||||
isIpv6Preferred = isIpv6Preferred,
|
||||
position = position,
|
||||
autoTunnelApps = autoTunnelApps,
|
||||
isMetered = isMetered,
|
||||
ipv4FallbackEnabled = ipv4FallbackEnabled,
|
||||
ipv6RestoreEnabled = ipv6RestoreEnabled,
|
||||
)
|
||||
|
||||
fun Domain.toEntity(): Entity =
|
||||
Entity(
|
||||
id = id,
|
||||
name = name,
|
||||
wgQuick = wgQuick,
|
||||
tunnelNetworks = tunnelNetworks,
|
||||
isMobileDataTunnel = isMobileDataTunnel,
|
||||
isPrimaryTunnel = isPrimaryTunnel,
|
||||
amQuick = amQuick,
|
||||
isActive = isActive,
|
||||
restartOnPingFailure = restartOnPingFailure,
|
||||
pingTarget = pingTarget,
|
||||
quickConfig = quickConfig,
|
||||
dynamicDnsEnabled = dynamicDnsEnabled,
|
||||
isEthernetTunnel = isEthernetTunnel,
|
||||
isIpv4Preferred = isIpv4Preferred,
|
||||
isIpv6Preferred = isIpv6Preferred,
|
||||
position = position,
|
||||
autoTunnelApps = autoTunnelApps,
|
||||
isMetered = isMetered,
|
||||
ipv4FallbackEnabled = ipv4FallbackEnabled,
|
||||
ipv6RestoreEnabled = ipv6RestoreEnabled,
|
||||
)
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.model
|
||||
|
||||
enum class AppMode(val value: Int) {
|
||||
VPN(0),
|
||||
PROXY(1),
|
||||
LOCK_DOWN(2),
|
||||
KERNEL(3);
|
||||
|
||||
companion object {
|
||||
fun fromValue(value: Int): AppMode = entries.find { it.value == value } ?: VPN
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.model
|
||||
|
||||
import android.content.Context
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
|
||||
enum class DnsProtocol(val value: Int) {
|
||||
SYSTEM(0),
|
||||
DOH(1);
|
||||
|
||||
fun asString(context: Context): String {
|
||||
return when (this) {
|
||||
SYSTEM -> context.getString(R.string.system)
|
||||
DOH -> context.getString(R.string.doh)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromValue(value: Int): DnsProtocol =
|
||||
DnsProtocol.entries.find { it.value == value } ?: SYSTEM
|
||||
}
|
||||
}
|
||||
|
||||
data class DnsSettings(val protocol: DnsProtocol = DnsProtocol.SYSTEM, val endpoint: String? = null)
|
||||
|
||||
enum class DnsProvider(private val systemAddress: String, private val dohAddress: String) {
|
||||
CLOUDFLARE("1.1.1.1", "https://1.1.1.1/dns-query"),
|
||||
ADGUARD("94.140.14.14", "https://94.140.14.14/dns-query");
|
||||
|
||||
fun asAddress(protocol: DnsProtocol): String {
|
||||
return when (protocol) {
|
||||
DnsProtocol.SYSTEM -> systemAddress
|
||||
DnsProtocol.DOH -> dohAddress
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromAddress(address: String): DnsProvider {
|
||||
return entries.find { it.systemAddress == address || it.dohAddress == address }
|
||||
?: CLOUDFLARE
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.network
|
||||
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.engine.okhttp.*
|
||||
import io.ktor.client.plugins.*
|
||||
import io.ktor.client.plugins.contentnegotiation.*
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.okhttp.OkHttp
|
||||
import io.ktor.client.plugins.HttpTimeout
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
object KtorClient {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.network
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.entity.GitHubRelease
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.plugins.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.plugins.ClientRequestException
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.http.HttpStatusCode
|
||||
|
||||
class KtorGitHubApi(private val client: HttpClient) : GitHubApi {
|
||||
override suspend fun getLatestRelease(owner: String, repo: String): Result<GitHubRelease> {
|
||||
@@ -32,10 +32,9 @@ class KtorGitHubApi(private val client: HttpClient) : GitHubApi {
|
||||
client.get("https://api.github.com/repos/$owner/$repo/releases").body()
|
||||
|
||||
// Find the first release with "nightly" in the tag_name (case-insensitive)
|
||||
val nightlyRelease =
|
||||
releases.firstOrNull { release ->
|
||||
release.tagName.contains("nightly", ignoreCase = true)
|
||||
}
|
||||
val nightlyRelease = releases.firstOrNull { release ->
|
||||
release.tagName.contains("nightly", ignoreCase = true)
|
||||
}
|
||||
|
||||
if (nightlyRelease != null) {
|
||||
Result.success(nightlyRelease)
|
||||
|
||||
+17
-18
@@ -28,25 +28,24 @@ class InstalledAndroidPackageRepository(
|
||||
withContext(ioDispatcher) {
|
||||
val packages = context.packageManager.getInstalledPackages(0)
|
||||
|
||||
val installedPackages =
|
||||
packages.mapNotNull { packageInfo ->
|
||||
try {
|
||||
val appInfo =
|
||||
context.packageManager.getApplicationInfo(packageInfo.packageName, 0)
|
||||
InstalledPackage(
|
||||
name =
|
||||
context.packageManager.getFriendlyAppName(
|
||||
packageInfo.packageName,
|
||||
appInfo,
|
||||
),
|
||||
packageName = packageInfo.packageName,
|
||||
uId = appInfo.uid,
|
||||
)
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
Timber.e(e)
|
||||
null
|
||||
}
|
||||
val installedPackages = packages.mapNotNull { packageInfo ->
|
||||
try {
|
||||
val appInfo =
|
||||
context.packageManager.getApplicationInfo(packageInfo.packageName, 0)
|
||||
InstalledPackage(
|
||||
name =
|
||||
context.packageManager.getFriendlyAppName(
|
||||
packageInfo.packageName,
|
||||
appInfo,
|
||||
),
|
||||
packageName = packageInfo.packageName,
|
||||
uId = appInfo.uid,
|
||||
)
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
Timber.e(e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
cachedPackages = installedPackages
|
||||
|
||||
|
||||
+4
@@ -21,4 +21,8 @@ class RoomDnsSettingsRepository(private val dnsSettingsDao: DnsSettingsDao) :
|
||||
override suspend fun getDnsSettings(): Domain {
|
||||
return (dnsSettingsDao.getDnsSettings() ?: Entity()).toDomain()
|
||||
}
|
||||
|
||||
override suspend fun updateGlobalDnsEnabled(enabled: Boolean) {
|
||||
dnsSettingsDao.updateGlobalDnsEnabled(enabled)
|
||||
}
|
||||
}
|
||||
|
||||
+8
@@ -22,4 +22,12 @@ class RoomMonitoringSettingsRepository(private val monitoringSettingsDao: Monito
|
||||
override suspend fun getMonitoringSettings(): Domain {
|
||||
return (monitoringSettingsDao.getMonitoringSettings() ?: Entity()).toDomain()
|
||||
}
|
||||
|
||||
override suspend fun updateStatisticRefresh(statisticRefresh: Int) {
|
||||
monitoringSettingsDao.updateStatisticsInterval(statisticRefresh)
|
||||
}
|
||||
|
||||
override suspend fun updateStatisticsEnabled(enabled: Boolean) {
|
||||
monitoringSettingsDao.updateStatisticsEnabled(enabled)
|
||||
}
|
||||
}
|
||||
|
||||
+11
-3
@@ -4,7 +4,7 @@ import com.zaneschepke.wireguardautotunnel.data.dao.GeneralSettingsDao
|
||||
import com.zaneschepke.wireguardautotunnel.data.entity.GeneralSettings as Entity
|
||||
import com.zaneschepke.wireguardautotunnel.data.mapper.toDomain
|
||||
import com.zaneschepke.wireguardautotunnel.data.mapper.toEntity
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings as Domain
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
|
||||
@@ -34,7 +34,15 @@ class RoomSettingsRepository(private val settingsDao: GeneralSettingsDao) :
|
||||
settingsDao.updatePinLockEnabled(enabled)
|
||||
}
|
||||
|
||||
override suspend fun updateAppMode(appMode: AppMode) {
|
||||
settingsDao.updateAppMode(appMode)
|
||||
override suspend fun updateAppMode(tunnelMode: TunnelMode) {
|
||||
settingsDao.updateAppMode(tunnelMode)
|
||||
}
|
||||
|
||||
override suspend fun updateGlobalAmneziaEnabled(enabled: Boolean) {
|
||||
settingsDao.updateGlobalAmneziaEnabled(enabled)
|
||||
}
|
||||
|
||||
override suspend fun updateScreenRecordingSecurity(enabled: Boolean) {
|
||||
settingsDao.updateScreenRecordingSecurity(enabled)
|
||||
}
|
||||
}
|
||||
|
||||
+15
-12
@@ -6,6 +6,7 @@ import com.zaneschepke.wireguardautotunnel.data.mapper.toEntity
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig as Domain
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class RoomTunnelRepository(private val tunnelConfigDao: TunnelConfigDao) : TunnelRepository {
|
||||
@@ -25,6 +26,14 @@ class RoomTunnelRepository(private val tunnelConfigDao: TunnelConfigDao) : Tunne
|
||||
return tunnelConfigDao.getAll().map { it.toDomain() }
|
||||
}
|
||||
|
||||
override suspend fun setMetered(tunnelId: Int, value: Boolean) {
|
||||
tunnelConfigDao.setMetered(tunnelId, value)
|
||||
}
|
||||
|
||||
override suspend fun setDynamicDns(tunnelId: Int, value: Boolean) {
|
||||
tunnelConfigDao.setDynamicDns(tunnelId, value)
|
||||
}
|
||||
|
||||
override suspend fun save(tunnelConfig: Domain) {
|
||||
tunnelConfigDao.upsert(tunnelConfig.toEntity())
|
||||
}
|
||||
@@ -38,10 +47,6 @@ class RoomTunnelRepository(private val tunnelConfigDao: TunnelConfigDao) : Tunne
|
||||
tunnelConfig?.let { save(it.copy(isPrimaryTunnel = true)) }
|
||||
}
|
||||
|
||||
override suspend fun resetActiveTunnels() {
|
||||
tunnelConfigDao.resetActiveTunnels()
|
||||
}
|
||||
|
||||
override suspend fun updateMobileDataTunnel(tunnelConfig: Domain?) {
|
||||
tunnelConfigDao.resetMobileDataTunnel()
|
||||
tunnelConfig?.let { save(it.copy(isMobileDataTunnel = true)) }
|
||||
@@ -60,18 +65,10 @@ class RoomTunnelRepository(private val tunnelConfigDao: TunnelConfigDao) : Tunne
|
||||
return tunnelConfigDao.getById(id.toLong())?.toDomain()
|
||||
}
|
||||
|
||||
override suspend fun getActive(): List<Domain> {
|
||||
return tunnelConfigDao.getActive().map { it.toDomain() }
|
||||
}
|
||||
|
||||
override suspend fun getDefaultTunnel(): Domain? {
|
||||
return tunnelConfigDao.getDefaultTunnel()?.toDomain()
|
||||
}
|
||||
|
||||
override suspend fun getStartTunnel(): Domain? {
|
||||
return tunnelConfigDao.getStartTunnel()?.toDomain()
|
||||
}
|
||||
|
||||
override suspend fun count(): Int {
|
||||
return tunnelConfigDao.count().toInt()
|
||||
}
|
||||
@@ -95,4 +92,10 @@ class RoomTunnelRepository(private val tunnelConfigDao: TunnelConfigDao) : Tunne
|
||||
override suspend fun delete(tunnels: List<Domain>) {
|
||||
tunnelConfigDao.delete(tunnels.map { it.toEntity() })
|
||||
}
|
||||
|
||||
override suspend fun ensureGlobalConfigExists() {
|
||||
if (globalTunnelFlow.firstOrNull() == null) {
|
||||
save(Domain.generateDefaultGlobalConfig())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,19 +4,31 @@ import android.content.Context
|
||||
import android.os.PowerManager
|
||||
import com.zaneschepke.logcatter.LogReader
|
||||
import com.zaneschepke.logcatter.LogcatReader
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationMonitor
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.AndroidNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelStateHolder
|
||||
import com.zaneschepke.wireguardautotunnel.core.shortcut.DynamicShortcutManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.SelectedTunnelsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||
import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.*
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AutoTunnelViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.ConfigEditViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.DnsViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.LicenseViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.LockdownViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.LoggerViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.MonitoringViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.ProxySettingsViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SettingsViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SplitTunnelViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SupportViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelViewModel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.core.annotation.KoinExperimentalAPI
|
||||
@@ -30,25 +42,15 @@ import org.koin.dsl.module
|
||||
@OptIn(KoinExperimentalAPI::class)
|
||||
val appModule = module {
|
||||
single<CoroutineScope>(named(Scope.APPLICATION)) {
|
||||
CoroutineScope(SupervisorJob() + get<CoroutineDispatcher>(named(Dispatcher.DEFAULT)))
|
||||
CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
}
|
||||
|
||||
single<LogReader> { LogcatReader.init(storageDir = androidContext().filesDir.absolutePath) }
|
||||
|
||||
single<PowerManager> {
|
||||
androidContext().getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
}
|
||||
singleOf(::NotificationMonitor)
|
||||
singleOf(::WireGuardNotification) bind NotificationManager::class
|
||||
single {
|
||||
ServiceManager(
|
||||
androidContext(),
|
||||
get(named(Dispatcher.IO)),
|
||||
get(named(Scope.APPLICATION)),
|
||||
get(named(Dispatcher.MAIN)),
|
||||
get(),
|
||||
)
|
||||
}
|
||||
singleOf(::AndroidNotificationService) bind NotificationService::class
|
||||
single { ServiceManager(androidContext()) }
|
||||
|
||||
singleOf(::GlobalEffectRepository)
|
||||
|
||||
@@ -59,7 +61,7 @@ val appModule = module {
|
||||
single { NetworkUtils(get(named(Dispatcher.IO))) }
|
||||
|
||||
viewModelOf(::AutoTunnelViewModel)
|
||||
viewModel { (id: Int?) -> ConfigViewModel(get(), get(), get(), id) }
|
||||
viewModel { (id: Int?) -> ConfigEditViewModel(get(), get(), get(), get(), get(), id) }
|
||||
viewModelOf(::DnsViewModel)
|
||||
viewModelOf(::LicenseViewModel)
|
||||
viewModelOf(::LockdownViewModel)
|
||||
@@ -71,4 +73,6 @@ val appModule = module {
|
||||
viewModel { (id: Int) -> SplitTunnelViewModel(get(), get(), get(), id) }
|
||||
viewModel { SupportViewModel(get(), get(named(Dispatcher.MAIN)), get()) }
|
||||
viewModel { (id: Int) -> TunnelViewModel(get(), get(), id) }
|
||||
|
||||
singleOf(::AutoTunnelStateHolder)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.zaneschepke.wireguardautotunnel.di
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.AppBoostrapCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.AutoTunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.DnsSettingsCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.ShortcutCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.StartupCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelModeCoordinator
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
|
||||
val coordinatorModule = module {
|
||||
singleOf(::ShortcutCoordinator)
|
||||
singleOf(::TunnelModeCoordinator)
|
||||
singleOf(::StartupCoordinator)
|
||||
singleOf(::AutoTunnelCoordinator)
|
||||
singleOf(::DnsSettingsCoordinator)
|
||||
single {
|
||||
TunnelCoordinator(
|
||||
get(),
|
||||
get(),
|
||||
get(),
|
||||
get(),
|
||||
get(),
|
||||
get(),
|
||||
get(),
|
||||
get(),
|
||||
get(named(Scope.APPLICATION)),
|
||||
)
|
||||
}
|
||||
singleOf(::AppBoostrapCoordinator)
|
||||
}
|
||||
@@ -12,14 +12,3 @@ enum class Dispatcher {
|
||||
enum class Scope {
|
||||
APPLICATION
|
||||
}
|
||||
|
||||
enum class Shell {
|
||||
APP,
|
||||
TUNNEL,
|
||||
}
|
||||
|
||||
enum class Core {
|
||||
KERNEL,
|
||||
PROXY_USERSPACE,
|
||||
USERSPACE,
|
||||
}
|
||||
|
||||
@@ -1,108 +1,112 @@
|
||||
package com.zaneschepke.wireguardautotunnel.di
|
||||
|
||||
import com.wireguard.android.backend.WgQuickBackend
|
||||
import com.wireguard.android.util.RootShell
|
||||
import com.wireguard.android.util.ToolsInstaller
|
||||
import android.app.Notification
|
||||
import android.content.Context
|
||||
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
|
||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.backend.KernelTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.backend.RunConfigHelper
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.backend.TunnelBackend
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.backend.UserspaceTunnel
|
||||
import com.zaneschepke.networkmonitor.StableNetworkEngine
|
||||
import com.zaneschepke.tunnel.NotificationProvider
|
||||
import com.zaneschepke.tunnel.backend.RootShell
|
||||
import com.zaneschepke.tunnel.util.RootShellException
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.event.TunnelEventDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.AndroidNotificationService.NotificationChannels
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.AndroidTunnelNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.PROXY_GROUP_KEY
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.VPN_GROUP_KEY
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.TunnelNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.tile.TunnelTileRefresher
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelBackendProvider
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.util.RootShellUtils
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.to
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.amnezia.awg.backend.Backend
|
||||
import org.amnezia.awg.backend.GoBackend
|
||||
import org.amnezia.awg.backend.ProxyGoBackend
|
||||
import org.amnezia.awg.backend.RootTunnelActionHandler
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
import timber.log.Timber
|
||||
|
||||
val tunnelModule = module {
|
||||
single(named(Shell.TUNNEL)) { RootShell(androidContext()) }
|
||||
single(named(Shell.APP)) { RootShell(androidContext()) }
|
||||
val tunnelBackendProviderModule = module {
|
||||
single<TunnelNotificationService> { AndroidTunnelNotificationService(get(), get()) }
|
||||
singleOf(::TunnelEventDispatcher)
|
||||
|
||||
single { RootShellUtils(get(named(Shell.APP)), get(named(Dispatcher.IO))) }
|
||||
single<NotificationProvider> {
|
||||
val notificationService = get<NotificationService>()
|
||||
val context = androidContext()
|
||||
object : NotificationProvider {
|
||||
override val vpnInitNotification: Notification
|
||||
get() =
|
||||
notificationService.createNotification(
|
||||
channel = NotificationChannels.VPN,
|
||||
title = context.getString(R.string.initializing),
|
||||
onGoing = true,
|
||||
groupKey = VPN_GROUP_KEY,
|
||||
)
|
||||
|
||||
singleOf(::RunConfigHelper)
|
||||
override val proxyInitNotification: Notification
|
||||
get() =
|
||||
notificationService.createNotification(
|
||||
channel = NotificationChannels.PROXY,
|
||||
title = context.getString(R.string.initializing),
|
||||
onGoing = true,
|
||||
groupKey = PROXY_GROUP_KEY,
|
||||
)
|
||||
|
||||
single<Backend>(named(Core.USERSPACE)) {
|
||||
GoBackend(
|
||||
androidContext(),
|
||||
RootTunnelActionHandler(org.amnezia.awg.util.RootShell(androidContext())),
|
||||
override val vpnNotificationId: Int
|
||||
get() = NotificationService.VPN_NOTIFICATION_ID
|
||||
|
||||
override val proxyNotificationId: Int
|
||||
get() = NotificationService.PROXY_NOTIFICATION_ID
|
||||
|
||||
override fun refreshTile(context: Context) {
|
||||
TunnelTileRefresher.refresh(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
single {
|
||||
StableNetworkEngine(
|
||||
get<CoroutineScope>(named(Scope.APPLICATION)),
|
||||
get<NetworkMonitor>().connectivityStateFlow,
|
||||
)
|
||||
}
|
||||
|
||||
single<Backend>(named(Core.PROXY_USERSPACE)) {
|
||||
ProxyGoBackend(
|
||||
androidContext(),
|
||||
RootTunnelActionHandler(org.amnezia.awg.util.RootShell(androidContext())),
|
||||
)
|
||||
}
|
||||
|
||||
single<com.wireguard.android.backend.Backend> {
|
||||
val shell = get<RootShell>(named(Shell.TUNNEL))
|
||||
WgQuickBackend(
|
||||
androidContext(),
|
||||
shell,
|
||||
ToolsInstaller(androidContext(), shell),
|
||||
com.wireguard.android.backend.RootTunnelActionHandler(shell),
|
||||
)
|
||||
.apply { setMultipleTunnels(true) }
|
||||
}
|
||||
|
||||
single<TunnelBackend>(named(Core.KERNEL)) {
|
||||
KernelTunnel(get(), get<com.wireguard.android.backend.Backend>())
|
||||
}
|
||||
|
||||
single<TunnelBackend>(qualifier = named(Core.USERSPACE)) {
|
||||
UserspaceTunnel(get<Backend>(named(Core.USERSPACE)), get())
|
||||
}
|
||||
|
||||
single<TunnelBackend>(qualifier = named(Core.PROXY_USERSPACE)) {
|
||||
UserspaceTunnel(get<Backend>(named(Core.PROXY_USERSPACE)), get())
|
||||
}
|
||||
|
||||
single<NetworkMonitor> {
|
||||
AndroidNetworkMonitor(
|
||||
androidContext(),
|
||||
object : AndroidNetworkMonitor.ConfigurationListener {
|
||||
override suspend fun runRootShellCommand(cmd: String): String? {
|
||||
return try {
|
||||
withTimeout(3_000) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val result = RootShell.run(cmd)
|
||||
result.output
|
||||
}
|
||||
}
|
||||
} catch (e: RootShellException) {
|
||||
Timber.e(e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override val detectionMethod =
|
||||
get<AutoTunnelSettingsRepository>()
|
||||
.flow
|
||||
.distinctUntilChangedBy { it.wifiDetectionMethod }
|
||||
.map { it.wifiDetectionMethod.to() }
|
||||
|
||||
override val rootShell = get<RootShell>(named(Shell.APP))
|
||||
},
|
||||
get<CoroutineScope>(named(Scope.APPLICATION)),
|
||||
)
|
||||
}
|
||||
|
||||
single {
|
||||
TunnelManager(
|
||||
get(named(Core.KERNEL)),
|
||||
get(named(Core.USERSPACE)),
|
||||
get(named(Core.PROXY_USERSPACE)),
|
||||
get(),
|
||||
get(),
|
||||
get(),
|
||||
get(),
|
||||
get(),
|
||||
get(),
|
||||
get(),
|
||||
get(),
|
||||
get(),
|
||||
get(),
|
||||
get(named(Scope.APPLICATION)),
|
||||
get(named(Dispatcher.IO)),
|
||||
)
|
||||
single<TunnelProvider> {
|
||||
TunnelBackendProvider(get(), get(named(Scope.APPLICATION)), get(named(Dispatcher.IO)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.zaneschepke.wireguardautotunnel.domain.enums
|
||||
|
||||
import android.content.Context
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
|
||||
enum class DnsProtocol(val value: Int) {
|
||||
SYSTEM(0),
|
||||
DOH(1),
|
||||
DOT(2),
|
||||
UDP(3);
|
||||
|
||||
fun asString(context: Context): String {
|
||||
return when (this) {
|
||||
SYSTEM -> context.getString(R.string.system)
|
||||
DOH -> context.getString(R.string.doh)
|
||||
DOT -> context.getString(R.string.dot)
|
||||
UDP -> context.getString(R.string.plain_dns)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromValue(value: Int): DnsProtocol = entries.find { it.value == value } ?: SYSTEM
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.zaneschepke.wireguardautotunnel.domain.enums
|
||||
|
||||
enum class MimicMode {
|
||||
QUIC,
|
||||
DNS,
|
||||
SIP,
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package com.zaneschepke.wireguardautotunnel.domain.enums
|
||||
|
||||
import android.content.Context
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
|
||||
enum class StatisticRefresh(val value: Int) {
|
||||
LIVE(1),
|
||||
BALANCED(3),
|
||||
BATTERY_SAVER(10);
|
||||
|
||||
fun asString(context: Context): String {
|
||||
return when (this) {
|
||||
LIVE -> context.getString(R.string.live)
|
||||
BALANCED -> context.getString(R.string.balanced)
|
||||
BATTERY_SAVER -> context.getString(R.string.balance_saver)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromValue(value: Int): StatisticRefresh = entries.find { it.value == value } ?: BALANCED
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package com.zaneschepke.wireguardautotunnel.domain.enums
|
||||
|
||||
enum class TunnelActionSource {
|
||||
USER,
|
||||
AUTO_TUNNEL,
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.zaneschepke.wireguardautotunnel.domain.enums
|
||||
|
||||
enum class TunnelMode(val value: Int) {
|
||||
VPN(0),
|
||||
PROXY(1),
|
||||
LOCK_DOWN(2);
|
||||
|
||||
companion object {
|
||||
fun fromValue(value: Int): TunnelMode = entries.find { it.value == value } ?: VPN
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.domain.enums
|
||||
|
||||
sealed class TunnelStatus {
|
||||
|
||||
data class Up(val startTime: Long) : TunnelStatus()
|
||||
|
||||
data object Down : TunnelStatus()
|
||||
|
||||
data object Stopping : TunnelStatus()
|
||||
|
||||
data object Starting : TunnelStatus()
|
||||
|
||||
fun isDown(): Boolean {
|
||||
return this == Down
|
||||
}
|
||||
|
||||
fun isUp(): Boolean {
|
||||
return this is Up
|
||||
}
|
||||
|
||||
fun isUpOrStarting(): Boolean {
|
||||
return this is Up || this == Starting
|
||||
}
|
||||
|
||||
fun isDownOrStopping(): Boolean {
|
||||
return this == Down || this is Stopping
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.model
|
||||
package com.zaneschepke.wireguardautotunnel.domain.enums
|
||||
|
||||
enum class WifiDetectionMethod(val value: Int) {
|
||||
DEFAULT(0),
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user