Compare commits

..

56 Commits

Author SHA1 Message Date
Zane Schepke b2bd8574ec chore: release 4.1.1 2025-10-19 02:45:22 -04:00
Zane Schepke e8146f0b97 fix: peer stats ui bug
closes #1007
2025-10-19 02:41:33 -04:00
Zane Schepke 706513a5b0 fix: auto tunnel start show battery optimization 2025-10-19 02:15:00 -04:00
Zane Schepke a77aa4d92f ci: fix token 2025-10-17 18:35:14 -04:00
Zane Schepke 21706db668 chore: release 4.1.0 2025-10-17 15:14:15 -04:00
Zane Schepke a160802f81 refactor: monitoring ui 2025-10-17 15:04:55 -04:00
Zane Schepke 01b5298254 fix: android tv config screen actions 2025-10-17 14:04:42 -04:00
Zane Schepke ff5331bea9 refactor: remove nav ripple on mobile, improve key ui 2025-10-17 03:43:41 -04:00
Zane Schepke 2cc71e657b fix: restart on boot, dynamic dns, auto tunnel reliability
Separate settings for starting tunnels vs auto tunnel on boot, fixing logic to make behavior more expected.

Fix a bug where dynamic DNS updater was only running once and not continually monitoring.

Further improvements to prevent spurious network monitoring states. Improved reevaluate job to allow for reliable manual tunnel overrides while auto tunnel is active.

Improved messaging on errors and dynamic dns events.
2025-10-17 02:30:45 -04:00
Zane Schepke 919816588b fix: always-on vpn regression 2025-10-16 16:36:48 -04:00
Zane Schepke 90bdb0fae2 fix: only allow backup/restore when all services are down
closes #948
2025-10-16 15:49:04 -04:00
Zane Schepke de3f43100e refactor: datamanager for migration 2025-10-16 13:20:20 -04:00
Zane Schepke bd3851077c fix: Amnezia QR large text error handling
closes #969
2025-10-16 12:57:23 -04:00
Zane Schepke 9c0a4f117a fix: prevent redundant starts
closes #994
2025-10-16 11:14:42 -04:00
Zane Schepke a2139fa2fc fix: lower notification importance, group notifications
closes #159
2025-10-16 10:18:13 -04:00
Zane Schepke a451e962b0 fix: location warning banner 2025-10-16 01:18:41 -04:00
Zane Schepke 60935c9ff1 fix: auto tunnel service worker 2025-10-16 00:27:02 -04:00
Zane Schepke b30b0f3cd2 refactor: db restructuring 2025-10-15 23:39:42 -04:00
Zane Schepke afbd25f46c refactor: add back active network details, optimize imports and strings 2025-10-14 22:03:39 -04:00
Zane Schepke 5418ddde54 fix: nav/status bar colors 2025-10-14 19:41:45 -04:00
Zane Schepke 2673518fdd fix: wg config parser to allow server configs
closes #998
2025-10-14 04:41:53 -04:00
Zane Schepke f3489e13ed fix: ping default target for split configs 2025-10-14 03:32:41 -04:00
Zane Schepke b0da2982d7 fix: more ui fixes, move ping targets 2025-10-14 02:56:48 -04:00
Zane Schepke 5519f1da8b fix: revert tile binder changes 2025-10-13 18:29:18 -04:00
Zane Schepke ce49e07b2f fix: row click responsiveness issues with trailing components 2025-10-13 18:26:21 -04:00
Zane Schepke 2e726206e0 fix: nav3 tab bug 2025-10-13 10:24:36 -04:00
Zane Schepke 09c417c422 ci: fix token 2025-10-12 22:21:49 -04:00
Zane Schepke 841ed71e90 fix: proguard issue, bump deps 2025-10-12 17:48:30 -04:00
Zane Schepke 50dd2404ae refactor: move some things around 2025-10-11 22:31:22 -04:00
Zane Schepke 5fddf03523 refactor: additional ui changes 2025-10-11 21:46:33 -04:00
Zane Schepke 71461f4d0b refactor: ui improvements 2025-10-11 20:43:43 -04:00
Zane Schepke 3c1e6b5862 fix: improve tile binding 2025-10-08 20:02:17 -04:00
Zane Schepke a6a7b3f770 fix: nav crash bugfix 2025-10-08 18:37:26 -04:00
Zane Schepke 3612cbda08 ci: add telegram and matrix bot notifications 2025-10-07 19:52:53 -04:00
dependabot[bot] 954d0c1c88 chore(deps): bump peter-evans/repository-dispatch from 3 to 4 (#987)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-07 18:23:21 -04:00
Zane Schepke 0827d06f47 ci: add bot notification, switch to PAT 2025-10-07 18:18:18 -04:00
Zane Schepke 449b6c5422 fix: auto tunnel status badge 2025-10-04 19:50:02 -04:00
Zane Schepke f71ec7b33f fix: doh static ip bug 2025-10-04 18:29:47 -04:00
Zane Schepke 4015aa1f28 refactor: attempt to improve network change detection for tecno, redmi, samsung devices
closes #984
2025-10-04 16:15:18 -04:00
Zane Schepke 5ee5ebe736 fix: health unknown if no rx 2025-10-04 13:57:49 -04:00
Zane Schepke 9be5932979 fix: config ui outline bug 2025-10-04 13:45:46 -04:00
Zane Schepke b341870c8b fix: peer stats of same peer key bug, minor status ui improvements 2025-10-04 11:38:22 -04:00
Zane Schepke ea77259653 refactor: deps 2025-10-03 00:54:33 -04:00
Zane Schepke 9735f59abb refactor: animate bottom nav icons 2025-10-03 00:09:34 -04:00
Zane Schepke 9ea608beb5 feat: improved navigation animations 2025-10-02 22:03:43 -04:00
Zane Schepke c86733c572 feat: additional app settings integration 2025-10-02 18:59:59 -04:00
Zane Schepke 91e61c5c31 fix: add timeouts to prevent tunnel hang on slow dns 2025-10-02 17:43:05 -04:00
Zane Schepke ca85000f1d fix: qr modal ui bug 2025-10-02 15:03:15 -04:00
Zane Schepke ae2b28c9f6 fix: split tunnel ui bug, new progress indicators 2025-10-02 14:52:53 -04:00
Zane Schepke f8fe3a4a3a fix: sorting bug 2025-10-02 14:36:03 -04:00
Zane Schepke 77ef317564 refactor: migrate to navigation3 2025-10-01 22:03:36 -04:00
Zane Schepke 0cbd90d4e1 refactor: ui screen content spacing 2025-10-01 12:48:50 -04:00
Zane Schepke 9b77a702d3 ci: refactor 2025-09-30 18:11:02 -04:00
Zane Schepke 4659ed92b6 refactor: update links 2025-09-30 18:03:48 -04:00
Zane Schepke 2cbace5bb5 fix: tunnel performance bug 2025-09-30 13:10:19 -04:00
Zane Schepke 00c2c2ac20 feat: global config overrides (#983) 2025-09-30 12:14:09 -04:00
302 changed files with 9328 additions and 7282 deletions
+5 -5
View File
@@ -20,7 +20,7 @@ jobs:
- name: Check for new commits - name: Check for new commits
id: check id: check
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.PAT }}
run: | run: |
NEW_COMMITS=$(git rev-list --count --after="$(date -Iseconds -d '23 hours ago')" ${{ github.sha }}) NEW_COMMITS=$(git rev-list --count --after="$(date -Iseconds -d '23 hours ago')" ${{ github.sha }})
echo "new_commits=$NEW_COMMITS" >> $GITHUB_OUTPUT echo "new_commits=$NEW_COMMITS" >> $GITHUB_OUTPUT
@@ -53,14 +53,14 @@ jobs:
tag: "latest" tag: "latest"
message: "Automated tag for HEAD commit" message: "Automated tag for HEAD commit"
force_push_tag: true force_push_tag: true
github_token: ${{ secrets.GITHUB_TOKEN }} github_token: ${{ github.token }}
tag_exists_error: false tag_exists_error: false
- name: Generate Changelog - name: Generate Changelog
id: changelog id: changelog
uses: requarks/changelog-action@v1 uses: requarks/changelog-action@v1
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ github.token }}
toTag: "nightly" toTag: "nightly"
fromTag: "latest" fromTag: "latest"
writeToFile: false writeToFile: false
@@ -84,7 +84,7 @@ jobs:
tag_name: "nightly" tag_name: "nightly"
delete_release: true delete_release: true
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ github.token }}
- name: Get checksum - name: Get checksum
id: checksum id: checksum
@@ -124,4 +124,4 @@ jobs:
files: | files: |
${{ github.workspace }}/temp/**/*.apk ${{ github.workspace }}/temp/**/*.apk
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ github.token }}
+73
View File
@@ -0,0 +1,73 @@
name: notifications
on:
issues:
types: [opened, closed]
release:
types: [published]
jobs:
notify:
runs-on: ubuntu-latest
steps:
- name: Send to Telegram
run: |
if [[ "${{ github.event_name }}" == "issues" && "${{ github.event.action }}" == "opened" ]]; then
TITLE="${{ github.event.issue.title }}"
NUMBER=${{ github.event.issue.number }}
USER="${{ github.event.issue.user.login }}"
BODY="${{ github.event.issue.body || 'No body provided' }}"
BODY_TRUNC="${BODY:0:200}" # Truncate to avoid spam
URL="${{ github.event.issue.html_url }}"
TEXT=$(echo -e "🆕 New Issue #$NUMBER: *$TITLE* by $USER\n\n$BODY_TRUNC\n\n[View Issue]($URL)")
elif [[ "${{ github.event_name }}" == "issues" && "${{ github.event.action }}" == "closed" ]]; then
TITLE="${{ github.event.issue.title }}"
NUMBER=${{ github.event.issue.number }}
USER="${{ github.event.issue.user.login }}"
URL="${{ github.event.issue.html_url }}"
TEXT=$(echo -e "✅ Issue Closed #$NUMBER: *$TITLE* by $USER\n\n[View Issue]($URL)")
elif [[ "${{ github.event_name }}" == "release" && "${{ github.event.action }}" == "published" ]]; then
NAME="${{ github.event.release.name }}"
TAG="${{ github.event.release.tag_name }}"
BODY="${{ github.event.release.body || 'No notes provided' }}"
BODY_TRUNC="${BODY:0:200}" # Truncate to avoid spam
URL="${{ github.event.release.html_url }}"
TEXT=$(echo -e "🚀 New Release *$NAME* ($TAG)\n\n$BODY_TRUNC\n\n[View Release]($URL)")
fi
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) || '' }} \
-d parse_mode="Markdown" \
--data-urlencode "text=$TEXT"
- name: Send to Matrix
run: |
PLAIN_MESSAGE=""
HTML_MESSAGE=""
if [[ "${{ github.event_name }}" == "issues" && "${{ github.event.action }}" == "opened" ]]; then
PLAIN_MESSAGE=$(echo -e "🆕 New Issue #${{ github.event.issue.number }}: ${{ github.event.issue.title }} by ${{ github.event.issue.user.login }}\n\n${{ github.event.issue.body || 'No body provided' }}\n\nView Issue: ${{ github.event.issue.html_url }}")
HTML_MESSAGE=$(echo -e "<p>🆕 New Issue #${{ github.event.issue.number }}: <strong>${{ github.event.issue.title }}</strong> by ${{ github.event.issue.user.login }}</p><p>${{ github.event.issue.body || 'No body provided' }}</p><p><a href=\"${{ github.event.issue.html_url }}\">View Issue</a></p>")
elif [[ "${{ github.event_name }}" == "issues" && "${{ github.event.action }}" == "closed" ]]; then
PLAIN_MESSAGE=$(echo -e "✅ Issue Closed #${{ github.event.issue.number }}: ${{ github.event.issue.title }} by ${{ github.event.issue.user.login }}\n\nView Issue: ${{ github.event.issue.html_url }}")
HTML_MESSAGE=$(echo -e "<p>✅ Issue Closed #${{ github.event.issue.number }}: <strong>${{ github.event.issue.title }}</strong> by ${{ github.event.issue.user.login }}</p><p><a href=\"${{ github.event.issue.html_url }}\">View Issue</a></p>")
elif [[ "${{ github.event_name }}" == "release" && "${{ github.event.action }}" == "published" ]]; then
PLAIN_MESSAGE=$(echo -e "🚀 New Release ${{ github.event.release.name }} (${{ github.event.release.tag_name }})\n\n${{ github.event.release.body || 'No notes provided' }}\n\nView Release: ${{ github.event.release.html_url }}")
HTML_MESSAGE=$(echo -e "<p>🚀 New Release <strong>${{ github.event.release.name }}</strong> (${{ github.event.release.tag_name }})</p><p>${{ github.event.release.body || 'No notes provided' }}</p><p><a href=\"${{ github.event.release.html_url }}\">View Release</a></p>")
fi
PLAIN_MESSAGE="${PLAIN_MESSAGE:0:220}"
PAYLOAD=$(jq -n --arg body "$PLAIN_MESSAGE" --arg formatted "$HTML_MESSAGE" '{
"msgtype": "m.text",
"body": $body,
"format": "org.matrix.custom.html",
"formatted_body": $formatted
}')
TXN_ID="${{ github.run_id }}-${{ github.run_attempt }}"
curl -s -X PUT "https://${{ vars.MATRIX_HOMESERVER }}/_matrix/client/v3/rooms/${{ vars.MATRIX_ROOM_ID }}/send/m.room.message/$TXN_ID" \
-H "Authorization: Bearer ${{ secrets.MATRIX_ACCESS_TOKEN }}" \
-H "Content-Type: application/json" \
-d "$PAYLOAD"
+8 -7
View File
@@ -74,7 +74,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
with: with:
ref: ${{ github.event_name == 'push' && github.ref || 'main' }} ref: ${{ github.event_name == 'push' && github.ref || 'master' }}
- name: Install system dependencies - name: Install system dependencies
run: | run: |
sudo apt update && sudo apt install -y gh apksigner sudo apt update && sudo apt install -y gh apksigner
@@ -86,20 +86,21 @@ jobs:
tag: "latest" tag: "latest"
message: "Automated tag for HEAD commit" message: "Automated tag for HEAD commit"
force_push_tag: true force_push_tag: true
github_token: ${{ secrets.GITHUB_TOKEN }} github_token: ${{ github.token }}
tag_exists_error: false tag_exists_error: false
- name: Get latest release - name: Get latest release
id: latest_release id: latest_release
uses: kaliber5/action-get-release@v1 uses: kaliber5/action-get-release@v1
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ github.token }}
latest: true latest: true
- name: Generate Changelog - name: Generate Changelog
id: changelog id: changelog
uses: requarks/changelog-action@v1 uses: requarks/changelog-action@v1
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ github.token }}
toTag: ${{ steps.latest_release.outputs.tag_name }} toTag: ${{ steps.latest_release.outputs.tag_name }}
fromTag: "latest" fromTag: "latest"
writeToFile: false writeToFile: false
@@ -161,7 +162,7 @@ jobs:
files: | files: |
${{ github.workspace }}/temp/**/*.apk ${{ github.workspace }}/temp/**/*.apk
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ github.token }}
publish-fdroid-public: publish-fdroid-public:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -170,9 +171,9 @@ jobs:
if: ${{ github.event_name == 'push' || inputs.release_type == 'release' }} if: ${{ github.event_name == 'push' || inputs.release_type == 'release' }}
steps: steps:
- name: Dispatch update for fdroid repo - name: Dispatch update for fdroid repo
uses: peter-evans/repository-dispatch@v3 uses: peter-evans/repository-dispatch@v4
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.PAT }}
repository: wgtunnel/fdroid repository: wgtunnel/fdroid
event-type: fdroid-update event-type: fdroid-update
+60 -74
View File
@@ -148,19 +148,66 @@ dependencies {
implementation(project(":logcatter")) implementation(project(":logcatter"))
implementation(project(":networkmonitor")) implementation(project(":networkmonitor"))
implementation(libs.androidx.core.ktx) // Core foundations
implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.bundles.androidx.core.full)
implementation(libs.androidx.lifecycle.service) implementation(libs.bundles.androidx.lifecycle.core)
implementation(libs.androidx.activity.compose) implementation(libs.bundles.androidx.appcompat)
implementation(platform(libs.androidx.compose.bom)) implementation(libs.bundles.androidx.storage)
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation(libs.androidx.appcompat)
implementation(libs.material)
implementation(libs.androidx.storage)
// Compose setup
implementation(platform(libs.androidx.compose.bom))
implementation(libs.bundles.androidx.compose.ui)
implementation(libs.bundles.androidx.compose.material)
implementation(libs.androidx.activity.compose)
// Navigation
implementation(libs.bundles.androidx.navigation3)
implementation(libs.bundles.navigation.lifecycle)
implementation(libs.bundles.androidx.hilt)
// Material and icons
implementation(libs.bundles.google.material)
implementation(libs.bundles.material.icons)
// Database
implementation(libs.bundles.androidx.room)
implementation(libs.bundles.androidx.datastore)
ksp(libs.androidx.room.compiler)
// DI and work
implementation(libs.bundles.hilt.android)
implementation(libs.bundles.androidx.work)
ksp(libs.hilt.android.compiler)
ksp(libs.androidx.hilt.compiler)
// Networking and serialization
implementation(libs.bundles.ktor.client)
implementation(libs.bundles.kotlinx.serialization)
implementation(libs.ipaddress)
// State management
implementation(libs.bundles.orbit.mvi)
// Tunnel
implementation(libs.bundles.wireguard.tunnel)
// Shizuku
implementation(libs.bundles.shizuku)
// UI utilities
implementation(libs.bundles.ui.utilities)
// Misc utilities
implementation(libs.bundles.misc.utilities)
coreLibraryDesugaring(libs.desugar.jdk.libs)
// Accompanist
implementation(libs.bundles.accompanist)
// Lifecycle Compose
implementation(libs.lifecycle.runtime.compose)
// Testing
testImplementation(libs.junit) testImplementation(libs.junit)
testImplementation(libs.androidx.junit) testImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)
@@ -171,71 +218,10 @@ dependencies {
debugImplementation(libs.androidx.compose.ui.tooling) debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.manifest) debugImplementation(libs.androidx.compose.manifest)
implementation(libs.tunnel) // Room database backup
implementation(libs.amneziawg.android)
coreLibraryDesugaring(libs.desugar.jdk.libs)
implementation(libs.timber)
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.hilt.navigation.compose)
implementation(libs.hilt.android)
ksp(libs.hilt.android.compiler)
ksp(libs.androidx.hilt.compiler)
implementation(libs.accompanist.permissions)
implementation(libs.accompanist.drawablepainter)
implementation(libs.androidx.room.runtime)
ksp(libs.androidx.room.compiler)
implementation(libs.androidx.room.ktx)
implementation(libs.androidx.datastore.preferences)
implementation(libs.lifecycle.runtime.compose)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.process)
implementation(libs.kotlinx.serialization.json)
implementation(libs.zxing.android.embedded)
implementation(libs.material.icons.core)
implementation(libs.material.icons.extended)
implementation(libs.pin.lock.compose)
implementation(libs.androidx.core)
implementation(libs.androidx.core.splashscreen)
implementation(libs.androidx.work.runtime)
implementation(libs.androidx.hilt.work)
implementation(libs.qrose)
implementation(libs.semver4j)
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.okhttp)
implementation(libs.ktor.client.cio)
implementation(libs.ktor.client.content.negotiation)
implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.slf4j.android)
implementation(libs.icmp4a)
// shizuku
implementation(libs.shizuku.api)
implementation(libs.shizuku.provider)
implementation(libs.reorderable)
implementation(libs.roomdatabasebackup) { implementation(libs.roomdatabasebackup) {
exclude(group = "org.reactivestreams", module = "reactive-streams") exclude(group = "org.reactivestreams", module = "reactive-streams")
} }
// state management
implementation(libs.orbit.compose)
implementation(libs.orbit.viewmodel)
implementation(libs.orbit.core)
} }
tasks.register<Copy>("copyLicenseeJsonToAssets") { tasks.register<Copy>("copyLicenseeJsonToAssets") {
+1
View File
@@ -0,0 +1 @@
-dontwarn javax.lang.model.**
@@ -0,0 +1,371 @@
{
"formatVersion": 1,
"database": {
"version": 23,
"identityHash": "c94fe51e6c318edf8bda81ab854c85e5",
"entities": [
{
"tableName": "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_always_on_vpn_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL DEFAULT 0, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_wifi_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, `is_ping_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_lan_on_kill_switch_enabled` INTEGER NOT NULL DEFAULT 0, `debounce_delay_seconds` INTEGER NOT NULL DEFAULT 3, `is_disable_kill_switch_on_trusted_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_unsecure_enabled` INTEGER NOT NULL DEFAULT 0, `wifi_detection_method` INTEGER NOT NULL DEFAULT 0, `is_ping_monitoring_enabled` INTEGER NOT NULL DEFAULT 1, `tunnel_ping_interval_sec` INTEGER NOT NULL DEFAULT 30, `tunnel_ping_attempts` INTEGER NOT NULL DEFAULT 3, `tunnel_ping_timeout_sec` INTEGER, `app_mode` INTEGER NOT NULL DEFAULT 0, `dns_protocol` INTEGER NOT NULL DEFAULT 0, `dns_endpoint` TEXT, `is_tunnel_globals_enabled` 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": "isAlwaysOnVpnEnabled",
"columnName": "is_always_on_vpn_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isTunnelOnEthernetEnabled",
"columnName": "is_tunnel_on_ethernet_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isShortcutsEnabled",
"columnName": "is_shortcuts_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isTunnelOnWifiEnabled",
"columnName": "is_tunnel_on_wifi_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": "isPingEnabled",
"columnName": "is_ping_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": "isLanOnKillSwitchEnabled",
"columnName": "is_lan_on_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "debounceDelaySeconds",
"columnName": "debounce_delay_seconds",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "3"
},
{
"fieldPath": "isDisableKillSwitchOnTrustedEnabled",
"columnName": "is_disable_kill_switch_on_trusted_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "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": "isPingMonitoringEnabled",
"columnName": "is_ping_monitoring_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "1"
},
{
"fieldPath": "tunnelPingIntervalSeconds",
"columnName": "tunnel_ping_interval_sec",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "30"
},
{
"fieldPath": "tunnelPingAttempts",
"columnName": "tunnel_ping_attempts",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "3"
},
{
"fieldPath": "tunnelPingTimeoutSeconds",
"columnName": "tunnel_ping_timeout_sec",
"affinity": "INTEGER"
},
{
"fieldPath": "appMode",
"columnName": "app_mode",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "dnsProtocol",
"columnName": "dns_protocol",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "dnsEndpoint",
"columnName": "dns_endpoint",
"affinity": "TEXT"
},
{
"fieldPath": "isTunnelGlobalsEnabled",
"columnName": "is_tunnel_globals_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "TunnelConfig",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false, `is_primary_tunnel` INTEGER NOT NULL DEFAULT false, `am_quick` TEXT NOT NULL DEFAULT '', `is_Active` INTEGER NOT NULL DEFAULT false, `restart_on_ping_failure` INTEGER NOT NULL DEFAULT false, `ping_target` TEXT DEFAULT null, `is_ethernet_tunnel` INTEGER NOT NULL DEFAULT false, `is_ipv4_preferred` INTEGER NOT NULL DEFAULT true, `position` INTEGER NOT NULL DEFAULT 0, `auto_tunnel_apps` TEXT NOT NULL DEFAULT '[]')",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "wgQuick",
"columnName": "wg_quick",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tunnelNetworks",
"columnName": "tunnel_networks",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isMobileDataTunnel",
"columnName": "is_mobile_data_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPrimaryTunnel",
"columnName": "is_primary_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "amQuick",
"columnName": "am_quick",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isActive",
"columnName": "is_Active",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "restartOnPingFailure",
"columnName": "restart_on_ping_failure",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "pingTarget",
"columnName": "ping_target",
"affinity": "TEXT",
"defaultValue": "null"
},
{
"fieldPath": "isEthernetTunnel",
"columnName": "is_ethernet_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isIpv4Preferred",
"columnName": "is_ipv4_preferred",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "true"
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "autoTunnelApps",
"columnName": "auto_tunnel_apps",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'[]'"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_TunnelConfig_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
}
]
},
{
"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"
]
}
}
],
"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, 'c94fe51e6c318edf8bda81ab854c85e5')"
]
}
}
@@ -0,0 +1,463 @@
{
"formatVersion": 1,
"database": {
"version": 24,
"identityHash": "545fe5e4cfa7f19ec10911ab5c603339",
"entities": [
{
"tableName": "tunnel_config",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false, `is_primary_tunnel` INTEGER NOT NULL DEFAULT false, `am_quick` TEXT NOT NULL DEFAULT '', `is_Active` INTEGER NOT NULL DEFAULT false, `restart_on_ping_failure` INTEGER NOT NULL DEFAULT false, `ping_target` TEXT DEFAULT null, `is_ethernet_tunnel` INTEGER NOT NULL DEFAULT false, `is_ipv4_preferred` INTEGER NOT NULL DEFAULT true, `position` INTEGER NOT NULL DEFAULT 0, `auto_tunnel_apps` TEXT NOT NULL DEFAULT '[]')",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "wgQuick",
"columnName": "wg_quick",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tunnelNetworks",
"columnName": "tunnel_networks",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isMobileDataTunnel",
"columnName": "is_mobile_data_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPrimaryTunnel",
"columnName": "is_primary_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "amQuick",
"columnName": "am_quick",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isActive",
"columnName": "is_Active",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "restartOnPingFailure",
"columnName": "restart_on_ping_failure",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "pingTarget",
"columnName": "ping_target",
"affinity": "TEXT",
"defaultValue": "null"
},
{
"fieldPath": "isEthernetTunnel",
"columnName": "is_ethernet_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isIpv4Preferred",
"columnName": "is_ipv4_preferred",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "true"
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "autoTunnelApps",
"columnName": "auto_tunnel_apps",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'[]'"
}
],
"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, `is_tunnel_globals_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, `is_lan_on_kill_switch_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": "isTunnelGlobalsEnabled",
"columnName": "is_tunnel_globals_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "appMode",
"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": "isLanOnKillSwitchEnabled",
"columnName": "is_lan_on_kill_switch_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, `debounce_delay_seconds` INTEGER NOT NULL DEFAULT 3, `is_tunnel_on_unsecure_enabled` INTEGER NOT NULL DEFAULT 0, `wifi_detection_method` 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": "debounceDelaySeconds",
"columnName": "debounce_delay_seconds",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "3"
},
{
"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"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "monitoring_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_ping_enabled` INTEGER NOT NULL DEFAULT 0, `is_ping_monitoring_enabled` INTEGER NOT NULL DEFAULT 1, `tunnel_ping_interval_sec` INTEGER NOT NULL DEFAULT 30, `tunnel_ping_attempts` INTEGER NOT NULL DEFAULT 3, `tunnel_ping_timeout_sec` INTEGER, `show_detailed_ping_stats` INTEGER NOT NULL DEFAULT 0, `is_local_logs_enabled` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isPingMonitoringEnabled",
"columnName": "is_ping_monitoring_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "1"
},
{
"fieldPath": "tunnelPingIntervalSeconds",
"columnName": "tunnel_ping_interval_sec",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "30"
},
{
"fieldPath": "tunnelPingAttempts",
"columnName": "tunnel_ping_attempts",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "3"
},
{
"fieldPath": "tunnelPingTimeoutSeconds",
"columnName": "tunnel_ping_timeout_sec",
"affinity": "INTEGER"
},
{
"fieldPath": "showDetailedPingStats",
"columnName": "show_detailed_ping_stats",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isLocalLogsEnabled",
"columnName": "is_local_logs_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"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)",
"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"
}
],
"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, '545fe5e4cfa7f19ec10911ab5c603339')"
]
}
}
@@ -0,0 +1,477 @@
{
"formatVersion": 1,
"database": {
"version": 25,
"identityHash": "2ea437642cca24af74dc57904899909a",
"entities": [
{
"tableName": "tunnel_config",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false, `is_primary_tunnel` INTEGER NOT NULL DEFAULT false, `am_quick` TEXT NOT NULL DEFAULT '', `is_Active` INTEGER NOT NULL DEFAULT false, `restart_on_ping_failure` INTEGER NOT NULL DEFAULT false, `ping_target` TEXT DEFAULT null, `is_ethernet_tunnel` INTEGER NOT NULL DEFAULT false, `is_ipv4_preferred` INTEGER NOT NULL DEFAULT true, `position` INTEGER NOT NULL DEFAULT 0, `auto_tunnel_apps` TEXT NOT NULL DEFAULT '[]')",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "wgQuick",
"columnName": "wg_quick",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tunnelNetworks",
"columnName": "tunnel_networks",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isMobileDataTunnel",
"columnName": "is_mobile_data_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPrimaryTunnel",
"columnName": "is_primary_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "amQuick",
"columnName": "am_quick",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isActive",
"columnName": "is_Active",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "restartOnPingFailure",
"columnName": "restart_on_ping_failure",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "pingTarget",
"columnName": "ping_target",
"affinity": "TEXT",
"defaultValue": "null"
},
{
"fieldPath": "isEthernetTunnel",
"columnName": "is_ethernet_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isIpv4Preferred",
"columnName": "is_ipv4_preferred",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "true"
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "autoTunnelApps",
"columnName": "auto_tunnel_apps",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'[]'"
}
],
"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, `is_tunnel_globals_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, `is_lan_on_kill_switch_enabled` INTEGER NOT NULL DEFAULT 0, `custom_split_packages` TEXT NOT NULL DEFAULT '{}')",
"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": "isTunnelGlobalsEnabled",
"columnName": "is_tunnel_globals_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "appMode",
"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": "isLanOnKillSwitchEnabled",
"columnName": "is_lan_on_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "customSplitPackages",
"columnName": "custom_split_packages",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'{}'"
}
],
"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, `debounce_delay_seconds` INTEGER NOT NULL DEFAULT 3, `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": "debounceDelaySeconds",
"columnName": "debounce_delay_seconds",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "3"
},
{
"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_ping_enabled` INTEGER NOT NULL DEFAULT 0, `is_ping_monitoring_enabled` INTEGER NOT NULL DEFAULT 1, `tunnel_ping_interval_sec` INTEGER NOT NULL DEFAULT 30, `tunnel_ping_attempts` INTEGER NOT NULL DEFAULT 3, `tunnel_ping_timeout_sec` INTEGER, `show_detailed_ping_stats` INTEGER NOT NULL DEFAULT 0, `is_local_logs_enabled` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isPingMonitoringEnabled",
"columnName": "is_ping_monitoring_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "1"
},
{
"fieldPath": "tunnelPingIntervalSeconds",
"columnName": "tunnel_ping_interval_sec",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "30"
},
{
"fieldPath": "tunnelPingAttempts",
"columnName": "tunnel_ping_attempts",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "3"
},
{
"fieldPath": "tunnelPingTimeoutSeconds",
"columnName": "tunnel_ping_timeout_sec",
"affinity": "INTEGER"
},
{
"fieldPath": "showDetailedPingStats",
"columnName": "show_detailed_ping_stats",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isLocalLogsEnabled",
"columnName": "is_local_logs_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"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)",
"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"
}
],
"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, '2ea437642cca24af74dc57904899909a')"
]
}
}
+7 -1
View File
@@ -73,10 +73,16 @@
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:windowSoftInputMode="adjustNothing" android:windowSoftInputMode="adjustResize"
android:theme="@style/Theme.WireguardAutoTunnel" android:theme="@style/Theme.WireguardAutoTunnel"
android:configChanges="orientation|screenSize|keyboardHidden" android:configChanges="orientation|screenSize|keyboardHidden"
> >
<intent-filter>
<action android:name="android.intent.action.APPLICATION_PREFERENCES" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.SHOW_APP_INFO" /> <action android:name="android.intent.action.SHOW_APP_INFO" />
@@ -1,46 +1,43 @@
package com.zaneschepke.wireguardautotunnel package com.zaneschepke.wireguardautotunnel
import ProxySettingsScreen import ProxySettingsScreen
import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.graphics.Color import android.graphics.Color
import android.net.VpnService import android.net.VpnService
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.Settings
import androidx.activity.SystemBarStyle import androidx.activity.SystemBarStyle
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.*
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import androidx.core.net.toUri
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.compose.NavHost import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
import androidx.navigation.compose.composable import androidx.navigation3.runtime.NavKey
import androidx.navigation.compose.rememberNavController import androidx.navigation3.runtime.entryProvider
import androidx.navigation.navigation import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation.toRoute import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
import androidx.navigation3.ui.NavDisplay
import com.zaneschepke.networkmonitor.NetworkMonitor import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.data.AppDatabase import com.zaneschepke.wireguardautotunnel.data.AppDatabase
import com.zaneschepke.wireguardautotunnel.data.model.AppMode import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
@@ -50,28 +47,33 @@ import com.zaneschepke.wireguardautotunnel.ui.common.banner.AppAlertBanner
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.VpnDeniedDialog import com.zaneschepke.wireguardautotunnel.ui.common.dialog.VpnDeniedDialog
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.CustomSnackBar import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.CustomSnackBar
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
import com.zaneschepke.wireguardautotunnel.ui.navigation.Tab
import com.zaneschepke.wireguardautotunnel.ui.navigation.components.BottomNavbar import com.zaneschepke.wireguardautotunnel.ui.navigation.components.BottomNavbar
import com.zaneschepke.wireguardautotunnel.ui.navigation.components.DynamicTopAppBar import com.zaneschepke.wireguardautotunnel.ui.navigation.components.DynamicTopAppBar
import com.zaneschepke.wireguardautotunnel.ui.navigation.components.currentBackStackEntryAsNavbarState 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.AutoTunnelScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.advanced.AutoTunnelAdvancedScreen 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.detection.WifiDetectionMethodScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.disclosure.LocationDisclosureScreen import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.disclosure.LocationDisclosureScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.preferred.PreferredTunnelScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.wifi.WifiSettingsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.pin.PinLockScreen import com.zaneschepke.wireguardautotunnel.ui.screens.pin.PinLockScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.AppearanceScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.AppearanceScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.display.DisplayScreen 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.appearance.language.LanguageScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.dns.DnsSettingsScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.dns.DnsSettingsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.logs.LogsScreen 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.monitoring.TunnelMonitoringScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.TunnelMonitoringScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.system.SystemFeaturesScreen 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.support.SupportScreen 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.DonateScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.donate.crypto.AddressesScreen 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.support.license.LicenseScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.TunnelsScreen import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.TunnelsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.autotunnel.TunnelAutoTunnelScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.config.ConfigScreen import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.config.ConfigScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.sort.SortScreen import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.sort.SortScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.splittunnel.SplitTunnelScreen import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.splittunnel.SplitTunnelScreen
@@ -81,13 +83,16 @@ import com.zaneschepke.wireguardautotunnel.ui.theme.OffWhite
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
import com.zaneschepke.wireguardautotunnel.util.extensions.* import com.zaneschepke.wireguardautotunnel.util.extensions.*
import com.zaneschepke.wireguardautotunnel.viewmodel.* import com.zaneschepke.wireguardautotunnel.viewmodel.ConfigViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.SplitTunnelViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelViewModel
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import de.raphaelebner.roomdatabasebackup.core.RoomBackup import de.raphaelebner.roomdatabasebackup.core.RoomBackup
import java.util.* import java.util.*
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber
import xyz.teamgravity.pin_lock_compose.PinManager import xyz.teamgravity.pin_lock_compose.PinManager
@AndroidEntryPoint @AndroidEntryPoint
@@ -100,7 +105,7 @@ class MainActivity : AppCompatActivity() {
private lateinit var roomBackup: RoomBackup private lateinit var roomBackup: RoomBackup
@SuppressLint("BatteryLife") @OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge( enableEdgeToEdge(
statusBarStyle = SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT), statusBarStyle = SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT),
@@ -123,7 +128,6 @@ class MainActivity : AppCompatActivity() {
val context = LocalContext.current val context = LocalContext.current
val isTv = isRunningOnTv() val isTv = isRunningOnTv()
val appState by viewModel.container.stateFlow.collectAsStateWithLifecycle() val appState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
val navController = rememberNavController()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
LaunchedEffect(appState.isAppLoaded) { LaunchedEffect(appState.isAppLoaded) {
@@ -132,16 +136,27 @@ class MainActivity : AppCompatActivity() {
} }
} }
val navState by
navController.currentBackStackEntryAsNavbarState(viewModel, navController)
val snackbar = remember { SnackbarHostState() } val snackbar = remember { SnackbarHostState() }
var showVpnPermissionDialog by remember { mutableStateOf(false) } var showVpnPermissionDialog by remember { mutableStateOf(false) }
var vpnPermissionDenied by remember { mutableStateOf(false) } var vpnPermissionDenied by remember { mutableStateOf(false) }
var requestingAppMode by remember { var requestingAppMode by remember {
mutableStateOf<Pair<AppMode?, TunnelConf?>>(Pair(null, null)) mutableStateOf<Pair<AppMode?, TunnelConfig?>>(Pair(null, null))
} }
LaunchedEffect(navState) { Timber.d("New navbar state $navState") } val startingStack = buildList {
add(Route.Tunnels)
if (intent?.action == Intent.ACTION_APPLICATION_PREFERENCES) add(Route.Settings)
if (appState.pinLockEnabled) add(Route.Lock)
}
val backStack = rememberNavBackStack(*startingStack.toTypedArray())
var previousRoute by remember { mutableStateOf<Route?>(null) }
val navController =
rememberNavController<NavKey>(backStack, appState.isLocationDisclosureShown) {
previousKey ->
previousRoute = previousKey as? Route
}
val vpnActivity = val vpnActivity =
rememberLauncherForActivityResult( rememberLauncherForActivityResult(
@@ -164,39 +179,24 @@ class MainActivity : AppCompatActivity() {
}, },
) )
val batteryActivity =
rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { _: ActivityResult ->
viewModel.disableBatteryOptimizationsShown()
}
fun requestDisableBatteryOptimizations() {
batteryActivity.launch(
Intent().apply {
action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
data = "package:${this@MainActivity.packageName}".toUri()
}
)
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.globalSideEffect.collect { sideEffect -> viewModel.globalSideEffect.collectLatest { sideEffect ->
when (sideEffect) { when (sideEffect) {
GlobalSideEffect.ConfigChanged -> restartApp() GlobalSideEffect.ConfigChanged -> restartApp()
GlobalSideEffect.PopBackStack -> navController.popBackStack() GlobalSideEffect.PopBackStack -> navController.pop()
GlobalSideEffect.RequestBatteryOptimizationDisabled ->
requestDisableBatteryOptimizations()
is GlobalSideEffect.RequestVpnPermission -> { is GlobalSideEffect.RequestVpnPermission -> {
requestingAppMode = Pair(sideEffect.requestingMode, sideEffect.config) requestingAppMode = Pair(sideEffect.requestingMode, sideEffect.config)
vpnActivity.launch(VpnService.prepare(this@MainActivity)) vpnActivity.launch(VpnService.prepare(this@MainActivity))
} }
is GlobalSideEffect.Snackbar -> is GlobalSideEffect.Snackbar ->
scope.launch { scope.launch {
snackbar.showSnackbar(sideEffect.message.asString(context)) snackbar.showSnackbar(sideEffect.message.asString(context))
} }
is GlobalSideEffect.Toast -> is GlobalSideEffect.Toast ->
scope.launch { context.showToast(sideEffect.message.asString(context)) } scope.launch { context.showToast(sideEffect.message.asString(context)) }
is GlobalSideEffect.LaunchUrl -> context.openWebUrl(sideEffect.url) is GlobalSideEffect.LaunchUrl -> context.openWebUrl(sideEffect.url)
is GlobalSideEffect.InstallApk -> context.installApk(sideEffect.apk) is GlobalSideEffect.InstallApk -> context.installApk(sideEffect.apk)
} }
@@ -205,6 +205,11 @@ class MainActivity : AppCompatActivity() {
if (!appState.isAppLoaded) return@setContent if (!appState.isAppLoaded) return@setContent
var showLock by remember {
mutableStateOf(appState.pinLockEnabled && !appState.isPinVerified)
}
LaunchedEffect(appState.isPinVerified) { if (appState.isPinVerified) showLock = false }
CompositionLocalProvider( CompositionLocalProvider(
LocalIsAndroidTV provides isTv, LocalIsAndroidTV provides isTv,
LocalSharedVm provides viewModel, LocalSharedVm provides viewModel,
@@ -219,174 +224,212 @@ class MainActivity : AppCompatActivity() {
}, },
) )
Box(modifier = Modifier.fillMaxSize()) { if (showLock) {
if (appState.settings.appMode == AppMode.LOCK_DOWN) { PinManager.initialize(context = this@MainActivity)
AppAlertBanner( PinLockScreen()
stringResource(R.string.locked_down).uppercase(Locale.getDefault()), } else {
OffWhite, val currentRoute by remember {
AlertRed, derivedStateOf { backStack.lastOrNull() as? Route }
modifier = Modifier.fillMaxWidth().zIndex(2f),
)
} }
val currentTab by remember {
derivedStateOf { Tab.fromRoute(currentRoute ?: Route.Tunnels) }
}
val navState by
currentRouteAsNavbarState(
appState,
viewModel,
currentRoute,
navController,
)
Scaffold( Box(modifier = Modifier.fillMaxSize()) {
snackbarHost = { if (appState.settings.appMode == AppMode.LOCK_DOWN) {
SnackbarHost(snackbar) { snackbarData -> AppAlertBanner(
CustomSnackBar( stringResource(R.string.locked_down)
snackbarData.visuals.message, .uppercase(Locale.getDefault()),
isRtl = false, OffWhite,
containerColor = AlertRed,
MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp), modifier = Modifier.fillMaxWidth().zIndex(2f),
) )
} }
}, Scaffold(
topBar = { DynamicTopAppBar(navState) }, snackbarHost = {
bottomBar = { SnackbarHost(snackbar) { snackbarData ->
BottomNavbar(appState.isAutoTunnelActive, navState, navController) CustomSnackBar(
}, snackbarData.visuals.message,
modifier = isRtl = false,
Modifier.pointerInput(Unit) { containerColor =
detectTapGestures { viewModel.clearSelectedTunnels() } MaterialTheme.colorScheme.surfaceColorAtElevation(
2.dp
),
)
}
}, },
) { padding -> topBar = { DynamicTopAppBar(navState) },
Box( bottomBar = {
modifier = if (navState.showBottomItems) {
Modifier.fillMaxSize() BottomNavbar(
.background(MaterialTheme.colorScheme.surface) appState.isAutoTunnelActive,
.padding(padding) currentTab,
.consumeWindowInsets(padding) onTabSelected = { tab ->
.imePadding() navController.popUpTo(tab.startRoute)
) { },
NavHost( )
navController = navController, }
startDestination = },
if (appState.pinLockEnabled && !appState.isAuthorized) ) { padding ->
Route.Lock Column(
else Route.TunnelsGraph, modifier =
Modifier.fillMaxSize()
.background(MaterialTheme.colorScheme.surface)
.padding(
top = padding.calculateTopPadding().plus(8.dp),
bottom = padding.calculateBottomPadding(),
)
.consumeWindowInsets(padding)
.imePadding()
) { ) {
composable<Route.Lock> { NavDisplay(
PinManager.initialize(context = this@MainActivity) backStack = backStack,
PinLockScreen() modifier = Modifier.fillMaxSize(),
} onBack = { navController.pop() },
navigation<Route.TunnelsGraph>( transitionSpec = {
startDestination = Route.Tunnels val initialIndex =
) { previousRoute?.let(Tab::fromRoute)?.index ?: 0
composable<Route.Tunnels> { val targetIndex =
val viewModel = currentRoute?.let(Tab::fromRoute)?.index ?: 0
it.sharedViewModel<TunnelsViewModel>(navController) if (initialIndex != targetIndex) {
TunnelsScreen(viewModel) val dir = if (targetIndex > initialIndex) 1 else -1
} (slideInHorizontally { dir * it } +
composable<Route.Sort> { fadeIn()) togetherWith
val viewModel = (slideOutHorizontally { dir * -it } + fadeOut())
it.sharedViewModel<TunnelsViewModel>(navController) } else {
SortScreen(viewModel) (slideInHorizontally { it } + fadeIn()) togetherWith
} (slideOutHorizontally { -it } + fadeOut())
composable<Route.TunnelOptions> { backStackEntry -> }
val args = backStackEntry.toRoute<Route.TunnelOptions>() },
val viewModel = popTransitionSpec = {
backStackEntry.sharedViewModel<TunnelsViewModel>( (slideInHorizontally { -it } + fadeIn()) togetherWith
navController (slideOutHorizontally { it } + fadeOut())
) },
TunnelOptionsScreen(args.id, viewModel) predictivePopTransitionSpec = {
} (slideInHorizontally { -it } + fadeIn()) togetherWith
composable<Route.SplitTunnel> { backStackEntry -> (slideOutHorizontally { it } + fadeOut())
val args = backStackEntry.toRoute<Route.SplitTunnel>() },
SplitTunnelScreen(args.id) entryDecorators =
} listOf(
composable<Route.TunnelAutoTunnel> { backStackEntry -> rememberSaveableStateHolderNavEntryDecorator(),
val args = rememberViewModelStoreNavEntryDecorator(),
backStackEntry.toRoute<Route.TunnelAutoTunnel>() ),
val viewModel = entryProvider =
backStackEntry.sharedViewModel<TunnelsViewModel>( entryProvider {
navController entry<Route.Lock> {
) PinManager.initialize(
TunnelAutoTunnelScreen(args.id, viewModel) context = this@MainActivity
} )
composable<Route.Config> { backStackEntry -> PinLockScreen()
val args = backStackEntry.toRoute<Route.Config>() }
val viewModel = entry<Route.Tunnels> { TunnelsScreen() }
backStackEntry.sharedViewModel<TunnelsViewModel>( entry<Route.Sort> { SortScreen() }
navController entry<Route.TunnelOptions> { key ->
) val viewModel =
ConfigScreen(args.id, viewModel) hiltViewModel<
} TunnelViewModel,
} TunnelViewModel.Factory,
>(
navigation<Route.AutoTunnelGraph>( creationCallback = { factory ->
startDestination = Route.AutoTunnel factory.create(key.id)
) { }
composable<Route.LocationDisclosure> { )
val viewModel = TunnelOptionsScreen(viewModel)
it.sharedViewModel<AutoTunnelViewModel>( }
navController entry<Route.SplitTunnel> { key ->
) val viewModel =
LocationDisclosureScreen(viewModel) hiltViewModel<
} SplitTunnelViewModel,
composable<Route.AutoTunnel> { SplitTunnelViewModel.Factory,
val viewModel = >(
it.sharedViewModel<AutoTunnelViewModel>( creationCallback = { factory ->
navController factory.create(key.id)
) }
AutoTunnelScreen(viewModel) )
} SplitTunnelScreen(viewModel)
composable<Route.AdvancedAutoTunnel> { }
val viewModel = entry<Route.Config> { key ->
it.sharedViewModel<AutoTunnelViewModel>( val viewModel =
navController hiltViewModel<
) ConfigViewModel,
AutoTunnelAdvancedScreen(viewModel) ConfigViewModel.Factory,
} >(
composable<Route.WifiDetectionMethod> { creationCallback = { factory ->
val viewModel = factory.create(key.id)
it.sharedViewModel<AutoTunnelViewModel>( }
navController )
) ConfigScreen(viewModel)
WifiDetectionMethodScreen(viewModel) }
} entry<Route.LocationDisclosure> {
} LocationDisclosureScreen()
}
navigation<Route.SettingsGraph>( entry<Route.AutoTunnel> { AutoTunnelScreen() }
startDestination = Route.Settings entry<Route.WifiPreferences> {
) { WifiSettingsScreen()
composable<Route.Settings> { }
val viewModel = entry<Route.AdvancedAutoTunnel> {
it.sharedViewModel<SettingsViewModel>(navController) AutoTunnelAdvancedScreen()
SettingsScreen(viewModel) }
} entry<Route.WifiDetectionMethod> {
composable<Route.TunnelMonitoring> { WifiDetectionMethodScreen()
val viewModel = }
it.sharedViewModel<SettingsViewModel>(navController) entry<Route.Settings> { SettingsScreen() }
TunnelMonitoringScreen(viewModel) entry<Route.TunnelMonitoring> {
} TunnelMonitoringScreen()
composable<Route.SystemFeatures> { }
val viewModel = entry<Route.AndroidIntegrations> {
it.sharedViewModel<SettingsViewModel>(navController) AndroidIntegrationsScreen()
SystemFeaturesScreen(viewModel) }
} entry<Route.Dns> { DnsSettingsScreen() }
composable<Route.Dns> { entry<Route.TunnelGlobals> { key ->
val viewModel = TunnelGlobalsScreen(key.id)
it.sharedViewModel<SettingsViewModel>(navController) }
DnsSettingsScreen(viewModel) entry<Route.ConfigGlobal> { key ->
} val viewModel =
composable<Route.ProxySettings> { ProxySettingsScreen() } hiltViewModel<
composable<Route.Appearance> { AppearanceScreen() } ConfigViewModel,
composable<Route.Language> { LanguageScreen() } ConfigViewModel.Factory,
composable<Route.Display> { DisplayScreen() } >(
composable<Route.Logs> { LogsScreen() } creationCallback = { factory ->
} factory.create(key.id)
}
navigation<Route.SupportGraph>( )
startDestination = Route.Support ConfigScreen(viewModel)
) { }
composable<Route.Support> { entry<Route.SplitTunnelGlobal> { key ->
val viewModel = val viewModel =
it.sharedViewModel<SupportViewModel>(navController) hiltViewModel<
SupportScreen(viewModel) SplitTunnelViewModel,
} SplitTunnelViewModel.Factory,
composable<Route.License> { LicenseScreen() } >(
composable<Route.Donate> { DonateScreen(navController) } creationCallback = { factory ->
composable<Route.Addresses> { AddressesScreen() } factory.create(key.id)
} }
)
SplitTunnelScreen(viewModel)
}
entry<Route.ProxySettings> { ProxySettingsScreen() }
entry<Route.Appearance> { AppearanceScreen() }
entry<Route.Language> { LanguageScreen() }
entry<Route.Display> { DisplayScreen() }
entry<Route.Logs> { LogsScreen() }
entry<Route.Support> { SupportScreen() }
entry<Route.License> { LicenseScreen() }
entry<Route.Donate> { DonateScreen() }
entry<Route.Addresses> { AddressesScreen() }
entry<Route.PreferredTunnel> { key ->
PreferredTunnelScreen(key.tunnelNetwork)
}
entry<Route.PingTarget> { PingTargetScreen() }
},
)
} }
} }
} }
@@ -12,9 +12,7 @@ import com.zaneschepke.wireguardautotunnel.core.worker.ServiceWorker
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
import javax.inject.Inject import javax.inject.Inject
@@ -25,7 +23,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.amnezia.awg.backend.GoBackend
import timber.log.Timber import timber.log.Timber
@HiltAndroidApp @HiltAndroidApp
@@ -42,9 +39,7 @@ class WireGuardAutoTunnel : Application(), Configuration.Provider {
@Inject @IoDispatcher lateinit var ioDispatcher: CoroutineDispatcher @Inject @IoDispatcher lateinit var ioDispatcher: CoroutineDispatcher
@Inject lateinit var settingsRepository: GeneralSettingRepository @Inject lateinit var monitoringRepository: MonitoringSettingsRepository
@Inject lateinit var tunnelsRepository: TunnelRepository
@Inject lateinit var appStateRepository: AppStateRepository
@Inject lateinit var notificationMonitor: NotificationMonitor @Inject lateinit var notificationMonitor: NotificationMonitor
@@ -68,20 +63,11 @@ class WireGuardAutoTunnel : Application(), Configuration.Provider {
} }
applicationScope.launch(ioDispatcher) { applicationScope.launch(ioDispatcher) {
launch { if (appStateRepository.isLocalLogsEnabled()) logReader.start() } launch {
launch { notificationMonitor.handleApplicationNotifications() } val monitoringSettings = monitoringRepository.getMonitoringSettings()
} if (monitoringSettings.isLocalLogsEnabled) logReader.start()
GoBackend.setAlwaysOnCallback {
applicationScope.launch {
val settings = settingsRepository.get()
if (settings.isAlwaysOnVpnEnabled) {
val tunnel = tunnelsRepository.getDefaultTunnel()
tunnel?.let { tunnelManager.startTunnel(it) }
} else {
Timber.w("Always-on VPN is not enabled in app settings")
}
} }
launch { notificationMonitor.handleApplicationNotifications() }
} }
ServiceWorker.start(this) ServiceWorker.start(this)
@@ -7,7 +7,7 @@ import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject import javax.inject.Inject
@@ -21,7 +21,7 @@ class NotificationActionReceiver : BroadcastReceiver() {
@Inject lateinit var tunnelRepository: TunnelRepository @Inject lateinit var tunnelRepository: TunnelRepository
@Inject lateinit var settingsRepository: GeneralSettingRepository @Inject lateinit var autoTunnelRepository: AutoTunnelSettingsRepository
@Inject @ApplicationScope lateinit var applicationScope: CoroutineScope @Inject @ApplicationScope lateinit var applicationScope: CoroutineScope
@@ -29,7 +29,7 @@ class NotificationActionReceiver : BroadcastReceiver() {
applicationScope.launch { applicationScope.launch {
when (intent.action) { when (intent.action) {
NotificationAction.AUTO_TUNNEL_OFF.name -> NotificationAction.AUTO_TUNNEL_OFF.name ->
settingsRepository.updateAutoTunnelEnabled(false) autoTunnelRepository.updateAutoTunnelEnabled(false)
NotificationAction.TUNNEL_OFF.name -> { NotificationAction.TUNNEL_OFF.name -> {
val tunnelId = intent.getIntExtra(NotificationManager.EXTRA_ID, 0) val tunnelId = intent.getIntExtra(NotificationManager.EXTRA_ID, 0)
if (tunnelId == STOP_ALL_TUNNELS_ID) if (tunnelId == STOP_ALL_TUNNELS_ID)
@@ -5,7 +5,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
@@ -20,9 +20,9 @@ class RemoteControlReceiver : BroadcastReceiver() {
@Inject @ApplicationScope lateinit var applicationScope: CoroutineScope @Inject @ApplicationScope lateinit var applicationScope: CoroutineScope
@Inject lateinit var appStateRepository: AppStateRepository
@Inject lateinit var settingsRepository: GeneralSettingRepository @Inject lateinit var settingsRepository: GeneralSettingRepository
@Inject lateinit var tunnelsRepository: TunnelRepository @Inject lateinit var tunnelsRepository: TunnelRepository
@Inject lateinit var autoTunnelSettingsRepository: AutoTunnelSettingsRepository
@Inject lateinit var tunnelManager: TunnelManager @Inject lateinit var tunnelManager: TunnelManager
@@ -53,11 +53,9 @@ class RemoteControlReceiver : BroadcastReceiver() {
val action = intent.action ?: return val action = intent.action ?: return
val appAction = Action.fromAction(action) ?: return Timber.w("Unknown action $action") val appAction = Action.fromAction(action) ?: return Timber.w("Unknown action $action")
applicationScope.launch { applicationScope.launch {
if (!appStateRepository.isRemoteControlEnabled()) val settings = settingsRepository.getGeneralSettings()
return@launch Timber.w("Remote control disabled") if (!settings.isRemoteControlEnabled) return@launch Timber.w("Remote control disabled")
val key = val key = settings.remoteKey ?: return@launch Timber.w("Remote control key missing")
appStateRepository.getRemoteKey()
?: return@launch Timber.w("Remote control key missing")
if (key != intent.getStringExtra(EXTRA_KEY)?.trim()) if (key != intent.getStringExtra(EXTRA_KEY)?.trim())
return@launch Timber.w("Invalid remote control key") return@launch Timber.w("Invalid remote control key")
when (appAction) { when (appAction) {
@@ -78,8 +76,10 @@ class RemoteControlReceiver : BroadcastReceiver() {
?: return@launch tunnelManager.stopActiveTunnels() ?: return@launch tunnelManager.stopActiveTunnels()
tunnelManager.stopTunnel(tunnel.id) tunnelManager.stopTunnel(tunnel.id)
} }
Action.START_AUTO_TUNNEL -> settingsRepository.updateAutoTunnelEnabled(true) Action.START_AUTO_TUNNEL ->
Action.STOP_AUTO_TUNNEL -> settingsRepository.updateAutoTunnelEnabled(false) autoTunnelSettingsRepository.updateAutoTunnelEnabled(true)
Action.STOP_AUTO_TUNNEL ->
autoTunnelSettingsRepository.updateAutoTunnelEnabled(false)
} }
} }
} }
@@ -6,10 +6,8 @@ import android.content.Intent
import com.zaneschepke.logcatter.LogReader import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
@@ -23,11 +21,20 @@ class RestartReceiver : BroadcastReceiver() {
@Inject lateinit var logReader: LogReader @Inject lateinit var logReader: LogReader
@Inject @IoDispatcher lateinit var ioDispatcher: CoroutineDispatcher
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
Timber.d("RestartReceiver triggered with action: ${intent.action}") Timber.d("RestartReceiver triggered with action: ${intent.action}")
if (intent.action == Intent.ACTION_MY_PACKAGE_REPLACED) applicationScope.launch {
applicationScope.launch(ioDispatcher) { logReader.deleteAndClearLogs() } when (intent.action) {
Intent.ACTION_BOOT_COMPLETED,
"android.intent.action.QUICKBOOT_POWERON",
"com.htc.intent.action.QUICKBOOT_POWERON" -> {
tunnelManager.handleReboot()
}
Intent.ACTION_MY_PACKAGE_REPLACED -> {
tunnelManager.handleRestore()
logReader.deleteAndClearLogs()
}
}
}
} }
} }
@@ -17,9 +17,11 @@ interface NotificationManager {
actions: Collection<NotificationCompat.Action> = emptyList(), actions: Collection<NotificationCompat.Action> = emptyList(),
description: String = "", description: String = "",
showTimestamp: Boolean = true, showTimestamp: Boolean = true,
importance: Int = NotificationManager.IMPORTANCE_HIGH, importance: Int = NotificationManager.IMPORTANCE_LOW,
onGoing: Boolean = false, onGoing: Boolean = false,
onlyAlertOnce: Boolean = true, onlyAlertOnce: Boolean = true,
groupKey: String? = null,
isGroupSummary: Boolean = false,
): Notification ): Notification
fun createNotification( fun createNotification(
@@ -28,9 +30,11 @@ interface NotificationManager {
actions: Collection<NotificationCompat.Action> = emptyList(), actions: Collection<NotificationCompat.Action> = emptyList(),
description: StringValue, description: StringValue,
showTimestamp: Boolean = true, showTimestamp: Boolean = true,
importance: Int = NotificationManager.IMPORTANCE_HIGH, importance: Int = NotificationManager.IMPORTANCE_LOW,
onGoing: Boolean = false, onGoing: Boolean = false,
onlyAlertOnce: Boolean = true, onlyAlertOnce: Boolean = true,
groupKey: String? = null,
isGroupSummary: Boolean = false,
): Notification ): Notification
fun createNotificationAction( fun createNotificationAction(
@@ -43,6 +47,8 @@ interface NotificationManager {
fun show(notificationId: Int, notification: Notification) fun show(notificationId: Int, notification: Notification)
companion object { companion object {
const val VPN_GROUP_KEY = "VPN_GROUP"
const val AUTO_TUNNEL_GROUP_KEY = "AUTO_TUNNEL_GROUP"
const val AUTO_TUNNEL_LOCATION_PERMISSION_ID = 123 const val AUTO_TUNNEL_LOCATION_PERMISSION_ID = 123
const val AUTO_TUNNEL_LOCATION_SERVICES_ID = 124 const val AUTO_TUNNEL_LOCATION_SERVICES_ID = 124
// For auto tunnel foreground notification // For auto tunnel foreground notification
@@ -3,7 +3,6 @@ package com.zaneschepke.wireguardautotunnel.core.notification
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException
import com.zaneschepke.wireguardautotunnel.util.StringValue import com.zaneschepke.wireguardautotunnel.util.StringValue
import jakarta.inject.Inject import jakarta.inject.Inject
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
@@ -27,16 +26,15 @@ constructor(
val notification = val notification =
notificationManager.createNotification( notificationManager.createNotification(
WireGuardNotification.NotificationChannels.VPN, WireGuardNotification.NotificationChannels.VPN,
title = StringValue.DynamicString(tunName), title =
tunName?.let { StringValue.DynamicString(it) }
?: StringValue.StringResource(R.string.tunnel),
description = description =
when (error) { StringValue.StringResource(
is BackendCoreException.BounceFailed -> error.toStringValue() R.string.tunnel_error_template,
else -> error.toStringRes(),
StringValue.StringResource( ),
R.string.tunnel_error_template, groupKey = NotificationManager.VPN_GROUP_KEY,
error.toStringRes(),
)
},
) )
notificationManager.show( notificationManager.show(
NotificationManager.TUNNEL_ERROR_NOTIFICATION_ID, NotificationManager.TUNNEL_ERROR_NOTIFICATION_ID,
@@ -51,8 +49,11 @@ constructor(
val notification = val notification =
notificationManager.createNotification( notificationManager.createNotification(
WireGuardNotification.NotificationChannels.VPN, WireGuardNotification.NotificationChannels.VPN,
title = StringValue.DynamicString(tunName), title =
tunName?.let { StringValue.DynamicString(it) }
?: StringValue.StringResource(R.string.tunnel),
description = message.toStringValue(), description = message.toStringValue(),
groupKey = NotificationManager.VPN_GROUP_KEY,
) )
notificationManager.show( notificationManager.show(
NotificationManager.TUNNEL_MESSAGES_NOTIFICATION_ID, NotificationManager.TUNNEL_MESSAGES_NOTIFICATION_ID,
@@ -3,12 +3,10 @@ package com.zaneschepke.wireguardautotunnel.core.notification
import android.Manifest import android.Manifest
import android.app.Notification import android.app.Notification
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.graphics.Color
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
@@ -22,7 +20,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject import javax.inject.Inject
class WireGuardNotification @Inject constructor(@ApplicationContext override val context: Context) : class WireGuardNotification @Inject constructor(@ApplicationContext override val context: Context) :
com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager { NotificationManager {
enum class NotificationChannels { enum class NotificationChannels {
VPN, VPN,
@@ -40,8 +38,10 @@ class WireGuardNotification @Inject constructor(@ApplicationContext override val
importance: Int, importance: Int,
onGoing: Boolean, onGoing: Boolean,
onlyAlertOnce: Boolean, onlyAlertOnce: Boolean,
groupKey: String?,
isGroupSummary: Boolean,
): Notification { ): Notification {
notificationManager.createNotificationChannel(channel.asChannel()) notificationManager.createNotificationChannel(channel.asChannel(importance))
return channel return channel
.asBuilder() .asBuilder()
.apply { .apply {
@@ -59,9 +59,15 @@ class WireGuardNotification @Inject constructor(@ApplicationContext override val
setContentText(description) setContentText(description)
setOnlyAlertOnce(onlyAlertOnce) setOnlyAlertOnce(onlyAlertOnce)
setOngoing(onGoing) setOngoing(onGoing)
setPriority(NotificationCompat.PRIORITY_HIGH) setPriority(NotificationCompat.PRIORITY_LOW)
setShowWhen(showTimestamp) setShowWhen(showTimestamp)
setSmallIcon(R.drawable.ic_notification) setSmallIcon(R.drawable.ic_notification)
if (groupKey != null) {
setGroup(groupKey)
if (isGroupSummary) {
setGroupSummary(true)
}
}
} }
.build() .build()
} }
@@ -75,6 +81,8 @@ class WireGuardNotification @Inject constructor(@ApplicationContext override val
importance: Int, importance: Int,
onGoing: Boolean, onGoing: Boolean,
onlyAlertOnce: Boolean, onlyAlertOnce: Boolean,
groupKey: String?,
isGroupSummary: Boolean,
): Notification { ): Notification {
return createNotification( return createNotification(
channel, channel,
@@ -142,34 +150,24 @@ class WireGuardNotification @Inject constructor(@ApplicationContext override val
} }
} }
private fun NotificationChannels.asChannel(): NotificationChannel { private fun NotificationChannels.asChannel(importance: Int): NotificationChannel {
return when (this) { return when (this) {
NotificationChannels.VPN -> { NotificationChannels.VPN -> {
NotificationChannel( NotificationChannel(
context.getString(R.string.vpn_channel_id), context.getString(R.string.vpn_channel_id),
context.getString(R.string.vpn_channel_name), context.getString(R.string.vpn_channel_name),
NotificationManager.IMPORTANCE_HIGH, importance,
) )
.apply { .apply { description = context.getString(R.string.vpn_channel_description) }
description = context.getString(R.string.vpn_channel_description)
enableLights(true)
lightColor = Color.WHITE
enableVibration(false)
vibrationPattern = longArrayOf(100, 200, 300)
}
} }
NotificationChannels.AUTO_TUNNEL -> { NotificationChannels.AUTO_TUNNEL -> {
NotificationChannel( NotificationChannel(
context.getString(R.string.auto_tunnel_channel_id), context.getString(R.string.auto_tunnel_channel_id),
context.getString(R.string.auto_tunnel_channel_name), context.getString(R.string.auto_tunnel_channel_name),
NotificationManager.IMPORTANCE_HIGH, importance,
) )
.apply { .apply {
description = context.getString(R.string.auto_tunnel_channel_description) description = context.getString(R.string.auto_tunnel_channel_description)
enableLights(true)
lightColor = Color.WHITE
enableVibration(false)
vibrationPattern = longArrayOf(100, 200, 300)
} }
} }
} }
@@ -13,7 +13,8 @@ import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelMonitor import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelMonitor
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf 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.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.util.extensions.distinctByKeys import com.zaneschepke.wireguardautotunnel.util.extensions.distinctByKeys
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@@ -35,6 +36,8 @@ abstract class BaseTunnelForegroundService : LifecycleService(), TunnelService {
@Inject @IoDispatcher lateinit var ioDispatcher: CoroutineDispatcher @Inject @IoDispatcher lateinit var ioDispatcher: CoroutineDispatcher
@Inject lateinit var settingsRepository: GeneralSettingRepository
@Inject lateinit var tunnelsRepository: TunnelRepository @Inject lateinit var tunnelsRepository: TunnelRepository
protected abstract val fgsType: Int protected abstract val fgsType: Int
@@ -62,7 +65,24 @@ abstract class BaseTunnelForegroundService : LifecycleService(), TunnelService {
onCreateNotification(), onCreateNotification(),
fgsType, fgsType,
) )
start() 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 return START_STICKY
} }
@@ -78,7 +98,7 @@ abstract class BaseTunnelForegroundService : LifecycleService(), TunnelService {
} }
// TODO Would be cool to have this include kill switch // TODO Would be cool to have this include kill switch
private fun updateServiceNotification(activeConfigs: List<TunnelConf>) { private fun updateServiceNotification(activeConfigs: List<TunnelConfig>) {
val notification = val notification =
when (activeConfigs.size) { when (activeConfigs.size) {
0 -> onCreateNotification() 0 -> onCreateNotification()
@@ -106,18 +126,20 @@ abstract class BaseTunnelForegroundService : LifecycleService(), TunnelService {
super.onDestroy() super.onDestroy()
} }
private fun createTunnelNotification(tunnelConf: TunnelConf): Notification { private fun createTunnelNotification(tunnelConfig: TunnelConfig): Notification {
return notificationManager.createNotification( return notificationManager.createNotification(
WireGuardNotification.NotificationChannels.VPN, WireGuardNotification.NotificationChannels.VPN,
title = "${getString(R.string.tunnel_running)} - ${tunnelConf.tunName}", title = "${getString(R.string.tunnel_running)} - ${tunnelConfig.name}",
actions = actions =
listOf( listOf(
notificationManager.createNotificationAction( notificationManager.createNotificationAction(
NotificationAction.TUNNEL_OFF, NotificationAction.TUNNEL_OFF,
tunnelConf.id, tunnelConfig.id,
) )
), ),
onGoing = true, onGoing = true,
groupKey = NotificationManager.VPN_GROUP_KEY,
isGroupSummary = true,
) )
} }
@@ -129,6 +151,8 @@ abstract class BaseTunnelForegroundService : LifecycleService(), TunnelService {
listOf( listOf(
notificationManager.createNotificationAction(NotificationAction.TUNNEL_OFF, 0) notificationManager.createNotificationAction(NotificationAction.TUNNEL_OFF, 0)
), ),
groupKey = NotificationManager.VPN_GROUP_KEY,
isGroupSummary = true,
) )
} }
@@ -136,6 +160,8 @@ abstract class BaseTunnelForegroundService : LifecycleService(), TunnelService {
return notificationManager.createNotification( return notificationManager.createNotification(
WireGuardNotification.NotificationChannels.VPN, WireGuardNotification.NotificationChannels.VPN,
title = getString(R.string.tunnel_starting), title = getString(R.string.tunnel_starting),
groupKey = NotificationManager.VPN_GROUP_KEY,
isGroupSummary = true,
) )
} }
} }
@@ -10,7 +10,7 @@ import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelSer
import com.zaneschepke.wireguardautotunnel.data.model.AppMode import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
import jakarta.inject.Inject import jakarta.inject.Inject
@@ -30,7 +30,7 @@ constructor(
@IoDispatcher ioDispatcher: CoroutineDispatcher, @IoDispatcher ioDispatcher: CoroutineDispatcher,
@ApplicationScope applicationScope: CoroutineScope, @ApplicationScope applicationScope: CoroutineScope,
private val mainDispatcher: CoroutineDispatcher, private val mainDispatcher: CoroutineDispatcher,
private val settingsRepository: GeneralSettingRepository, private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
) { ) {
private val autoTunnelMutex = Mutex() private val autoTunnelMutex = Mutex()
@@ -49,7 +49,9 @@ constructor(
} }
applicationScope.launch(ioDispatcher) { applicationScope.launch(ioDispatcher) {
combine( combine(
settingsRepository.flow.map { it.isAutoTunnelEnabled }.distinctUntilChanged(), autoTunnelSettingsRepository.flow
.map { it.isAutoTunnelEnabled }
.distinctUntilChanged(),
_autoTunnelService, _autoTunnelService,
) { enabled, service -> ) { enabled, service ->
enabled to (service != null) enabled to (service != null)
@@ -71,7 +73,7 @@ constructor(
object : ServiceConnection { object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) { override fun onServiceConnected(name: ComponentName, service: IBinder) {
val binder = service as? LocalBinder val binder = service as? LocalBinder
_tunnelService.value = binder?.service _tunnelService.update { binder?.service }
val serviceClass = val serviceClass =
when { when {
name.className.contains("VpnForegroundService") -> "VpnForegroundService" name.className.contains("VpnForegroundService") -> "VpnForegroundService"
@@ -83,7 +85,7 @@ constructor(
} }
override fun onServiceDisconnected(name: ComponentName) { override fun onServiceDisconnected(name: ComponentName) {
_tunnelService.value = null _tunnelService.update { null }
val serviceClass = val serviceClass =
when { when {
name.className.contains("VpnForegroundService") -> "VpnForegroundService" name.className.contains("VpnForegroundService") -> "VpnForegroundService"
@@ -99,12 +101,12 @@ constructor(
object : ServiceConnection { object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) { override fun onServiceConnected(name: ComponentName, service: IBinder) {
val binder = service as? AutoTunnelService.LocalBinder val binder = service as? AutoTunnelService.LocalBinder
_autoTunnelService.value = binder?.service _autoTunnelService.update { binder?.service }
Timber.d("AutoTunnelService connected") Timber.d("AutoTunnelService connected")
} }
override fun onServiceDisconnected(name: ComponentName) { override fun onServiceDisconnected(name: ComponentName) {
_autoTunnelService.value = null _autoTunnelService.update { null }
Timber.d("AutoTunnelService disconnected") Timber.d("AutoTunnelService disconnected")
} }
} }
@@ -114,11 +116,15 @@ constructor(
} }
private fun startServiceInternal() { private fun startServiceInternal() {
val intent = Intent(context, AutoTunnelService::class.java) if (autoTunnelService.value == null) {
context.startForegroundService(intent) val intent = Intent(context, AutoTunnelService::class.java)
context.bindService(intent, autoTunnelServiceConnection, Context.BIND_AUTO_CREATE) context.startForegroundService(intent)
context.bindService(intent, autoTunnelServiceConnection, Context.BIND_AUTO_CREATE)
}
} }
suspend fun startAutoTunnelService() = autoTunnelMutex.withLock { startServiceInternal() }
private fun stopServiceInternal() { private fun stopServiceInternal() {
_autoTunnelService.value?.stop() _autoTunnelService.value?.stop()
try { try {
@@ -14,16 +14,18 @@ import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings import com.zaneschepke.wireguardautotunnel.domain.model.AutoTunnelSettings
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.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.state.AutoTunnelState import com.zaneschepke.wireguardautotunnel.domain.state.AutoTunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.NetworkState import com.zaneschepke.wireguardautotunnel.domain.state.NetworkState
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.Tunnels
import com.zaneschepke.wireguardautotunnel.util.extensions.to import com.zaneschepke.wireguardautotunnel.util.extensions.to
import com.zaneschepke.wireguardautotunnel.util.extensions.toMillis import com.zaneschepke.wireguardautotunnel.util.extensions.toMillis
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@@ -48,7 +50,8 @@ class AutoTunnelService : LifecycleService() {
@Inject lateinit var tunnelManager: TunnelManager @Inject lateinit var tunnelManager: TunnelManager
@Inject lateinit var settingsRepository: Provider<GeneralSettingRepository> @Inject lateinit var autoTunnelRepository: Provider<AutoTunnelSettingsRepository>
@Inject lateinit var settingsRepository: GeneralSettingRepository
@Inject lateinit var tunnelsRepository: TunnelRepository @Inject lateinit var tunnelsRepository: TunnelRepository
private val defaultState = AutoTunnelState() private val defaultState = AutoTunnelState()
@@ -109,6 +112,8 @@ class AutoTunnelService : LifecycleService() {
) )
), ),
onGoing = true, onGoing = true,
groupKey = NotificationManager.AUTO_TUNNEL_GROUP_KEY,
isGroupSummary = true,
) )
ServiceCompat.startForeground( ServiceCompat.startForeground(
this, this,
@@ -124,14 +129,15 @@ class AutoTunnelService : LifecycleService() {
debouncedConnectivityStateFlow debouncedConnectivityStateFlow
.flowOn(ioDispatcher) .flowOn(ioDispatcher)
.map(NetworkState::from) .map(NetworkState::from)
.map { StateChange.NetworkChange(it) } .map(::NetworkChange)
.distinctUntilChanged() .distinctUntilChanged()
val settingsFlow = val settingsFlow =
combineSettings().map { StateChange.SettingsChange(it.first, it.second) } combineSettings().map { (appMode, settings, tunnels) ->
SettingsChange(appMode, settings, tunnels)
}
val tunnelsFlow = val tunnelsFlow = tunnelManager.activeTunnels.map(::ActiveTunnelsChange)
tunnelManager.activeTunnels.map { StateChange.ActiveTunnelsChange(it) }
var reevaluationJob: Job? = null var reevaluationJob: Job? = null
@@ -148,36 +154,46 @@ class AutoTunnelService : LifecycleService() {
} }
.first() .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 // use merge to limit the noise of a combine and also increase the scalability of auto
// tunnel handling new states // tunnel handling new states
merge(networkFlow, settingsFlow, tunnelsFlow).collect { change -> merge(networkFlow, settingsFlow, tunnelsFlow).collect { change ->
if (change !is StateChange.ActiveTunnelsChange) { if (change !is ActiveTunnelsChange) {
Timber.d("New state changed to ${change.javaClass.simpleName}") Timber.d("New state changed to ${change.javaClass.simpleName}")
} }
val previousState = autoTunnelStateFlow.value
when (change) { when (change) {
is StateChange.NetworkChange -> { is NetworkChange -> {
Timber.d("Network change: ${change.networkState}")
reevaluationJob?.cancel() reevaluationJob?.cancel()
val previousState = autoTunnelStateFlow.value
autoTunnelStateFlow.update { it.copy(networkState = change.networkState) } autoTunnelStateFlow.update { it.copy(networkState = change.networkState) }
// Android late mobile data state change, we can ignore handling this if (previousState.networkState == change.networkState) {
if ( Timber.d("Duplicate network state change detected, ignoring")
isAndroidLateCellularActiveChange(
previousState.networkState,
change.networkState,
)
) {
Timber.d("Android late cellular active state change")
return@collect return@collect
} }
} }
is StateChange.SettingsChange -> { is SettingsChange -> {
reevaluationJob?.cancel() reevaluationJob?.cancel()
autoTunnelStateFlow.update { autoTunnelStateFlow.update {
it.copy(settings = change.settings, tunnels = change.tunnels) 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 StateChange.ActiveTunnelsChange -> { is ActiveTunnelsChange -> {
autoTunnelStateFlow.update { it.copy(activeTunnels = change.activeTunnels) } autoTunnelStateFlow.update { it.copy(activeTunnels = change.activeTunnels) }
return@collect return@collect
} }
@@ -186,51 +202,34 @@ class AutoTunnelService : LifecycleService() {
handleAutoTunnelEvent(autoTunnelStateFlow.value.determineAutoTunnelEvent(change)) handleAutoTunnelEvent(autoTunnelStateFlow.value.determineAutoTunnelEvent(change))
reevaluationJob = launch { reevaluationJob = launch {
val snapshotNetwork = autoTunnelStateFlow.value.networkState
delay(REEVALUATE_CHECK_DELAY) delay(REEVALUATE_CHECK_DELAY)
val currentState = autoTunnelStateFlow.value val currentState = autoTunnelStateFlow.value
if (currentState != defaultState) { if (
Timber.d("Re-evaluating auto-tunnel state..") currentState != defaultState && currentState.networkState != snapshotNetwork
) {
Timber.d(
"Re-evaluating auto-tunnel state.. (network changed since snapshot)"
)
handleAutoTunnelEvent(currentState.determineAutoTunnelEvent(change)) handleAutoTunnelEvent(currentState.determineAutoTunnelEvent(change))
} else {
Timber.d("Skipping re-eval: network unchanged or default state")
} }
} }
} }
} }
private fun isAndroidLateCellularActiveChange( private fun combineSettings(): Flow<Triple<AppMode, AutoTunnelSettings, List<TunnelConfig>>> {
previous: NetworkState,
new: NetworkState,
): Boolean {
return (previous.isWifiConnected != new.isWifiConnected &&
previous.wifiName == new.wifiName &&
previous.isMobileDataConnected != new.isMobileDataConnected)
}
// all relevant settings to auto tunnel
private fun areAutoTunnelSettingsTheSame(old: GeneralSettings, new: GeneralSettings): Boolean {
return (old.isTunnelOnWifiEnabled == new.isTunnelOnWifiEnabled &&
old.isTunnelOnMobileDataEnabled == new.isTunnelOnMobileDataEnabled &&
old.isTunnelOnEthernetEnabled == new.isTunnelOnEthernetEnabled &&
old.trustedNetworkSSIDs == new.trustedNetworkSSIDs &&
old.isPingEnabled == new.isPingEnabled &&
old.debounceDelaySeconds == new.debounceDelaySeconds &&
old.wifiDetectionMethod == new.wifiDetectionMethod &&
old.isVpnKillSwitchEnabled == new.isVpnKillSwitchEnabled &&
old.isLanOnKillSwitchEnabled == new.isLanOnKillSwitchEnabled &&
old.isDisableKillSwitchOnTrustedEnabled == new.isDisableKillSwitchOnTrustedEnabled &&
old.isStopOnNoInternetEnabled == new.isStopOnNoInternetEnabled &&
old.appMode == new.appMode)
}
private fun combineSettings(): Flow<Pair<GeneralSettings, Tunnels>> {
return combine( return combine(
settingsRepository.get().flow.distinctUntilChanged(::areAutoTunnelSettingsTheSame), settingsRepository.flow.map { it.appMode }.distinctUntilChanged(),
autoTunnelRepository.get().flow,
tunnelsRepository.flow.map { tunnels -> tunnelsRepository.flow.map { tunnels ->
// isActive is ignored for equality checks so user can manually toggle off // isActive is ignored for equality checks so user can manually toggle off
// tunnel with auto-tunnel // tunnel with auto-tunnel
tunnels.map { it.copy(isActive = false) } tunnels.map { it.copy(isActive = false) }
}, },
) { settings, tunnels -> ) { appMode, autoTunnel, tunnels ->
Pair(settings, tunnels) Triple(appMode, autoTunnel, tunnels)
} }
.distinctUntilChanged() .distinctUntilChanged()
} }
@@ -339,7 +338,7 @@ class AutoTunnelService : LifecycleService() {
} }
) { ) {
is AutoTunnelEvent.Start -> is AutoTunnelEvent.Start ->
(event.tunnelConf ?: tunnelsRepository.getDefaultTunnel())?.let { (event.tunnelConfig ?: tunnelsRepository.getDefaultTunnel())?.let {
tunnelManager.startTunnel(it) tunnelManager.startTunnel(it)
} }
is AutoTunnelEvent.Stop -> tunnelManager.stopActiveTunnels() is AutoTunnelEvent.Stop -> tunnelManager.stopActiveTunnels()
@@ -350,7 +349,7 @@ class AutoTunnelService : LifecycleService() {
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
private val debouncedConnectivityStateFlow: Flow<ConnectivityState> by lazy { private val debouncedConnectivityStateFlow: Flow<ConnectivityState> by lazy {
settingsRepository autoTunnelRepository
.get() .get()
.flow .flow
.map { it.debounceDelaySeconds.toMillis() } .map { it.debounceDelaySeconds.toMillis() }
@@ -361,7 +360,6 @@ class AutoTunnelService : LifecycleService() {
} }
companion object { companion object {
// try to keep this window short as it will interrupt manual overrides const val REEVALUATE_CHECK_DELAY = 3_000L
const val REEVALUATE_CHECK_DELAY = 2_000L
} }
} }
@@ -1,14 +1,19 @@
package com.zaneschepke.wireguardautotunnel.core.service.autotunnel package com.zaneschepke.wireguardautotunnel.core.service.autotunnel
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings 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.NetworkState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.util.extensions.Tunnels
sealed class StateChange { sealed interface StateChange
data class NetworkChange(val networkState: NetworkState) : StateChange()
data class SettingsChange(val settings: GeneralSettings, val tunnels: Tunnels) : StateChange() data class NetworkChange(val networkState: NetworkState) : StateChange
data class ActiveTunnelsChange(val activeTunnels: Map<Int, TunnelState>) : StateChange() data class SettingsChange(
} val appMode: AppMode,
val settings: AutoTunnelSettings,
val tunnels: List<TunnelConfig>,
) : StateChange
data class ActiveTunnelsChange(val activeTunnels: Map<Int, TunnelState>) : StateChange
@@ -9,7 +9,7 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject import javax.inject.Inject
@@ -19,7 +19,7 @@ import timber.log.Timber
@AndroidEntryPoint @AndroidEntryPoint
class AutoTunnelControlTile : TileService(), LifecycleOwner { class AutoTunnelControlTile : TileService(), LifecycleOwner {
@Inject lateinit var settingsRepository: GeneralSettingRepository @Inject lateinit var autoTunnelSettingsRepository: AutoTunnelSettingsRepository
@Inject lateinit var tunnelsRepository: TunnelRepository @Inject lateinit var tunnelsRepository: TunnelRepository
@Inject lateinit var serviceManager: ServiceManager @Inject lateinit var serviceManager: ServiceManager
@@ -60,10 +60,10 @@ class AutoTunnelControlTile : TileService(), LifecycleOwner {
unlockAndRun { unlockAndRun {
lifecycleScope.launch { lifecycleScope.launch {
if (serviceManager.autoTunnelService.value != null) { if (serviceManager.autoTunnelService.value != null) {
settingsRepository.updateAutoTunnelEnabled(false) autoTunnelSettingsRepository.updateAutoTunnelEnabled(false)
setInactive() setInactive()
} else { } else {
settingsRepository.updateAutoTunnelEnabled(true) autoTunnelSettingsRepository.updateAutoTunnelEnabled(true)
setActive() setActive()
} }
} }
@@ -84,17 +84,6 @@ class AutoTunnelControlTile : TileService(), LifecycleOwner {
} }
} }
/* This works around an annoying unsolved frameworks bug some people are hitting. */
override fun onBind(intent: Intent): IBinder? {
var ret: IBinder? = null
try {
ret = super.onBind(intent)
} catch (_: Throwable) {
Timber.e("Failed to bind to TunnelControlTile")
}
return ret
}
private fun setUnavailable() { private fun setUnavailable() {
runCatching { runCatching {
qsTile.state = Tile.STATE_UNAVAILABLE qsTile.state = Tile.STATE_UNAVAILABLE
@@ -102,6 +91,17 @@ class AutoTunnelControlTile : TileService(), LifecycleOwner {
} }
} }
/* This works around an annoying unsolved frameworks bug some people are hitting. */
override fun onBind(intent: Intent): IBinder? {
var ret: IBinder? = null
try {
ret = super.onBind(intent)
} catch (_: Throwable) {
Timber.e("Failed to bind to AutoTunnelControlTile")
}
return ret
}
override val lifecycle: Lifecycle override val lifecycle: Lifecycle
get() = lifecycleRegistry get() = lifecycleRegistry
} }
@@ -70,7 +70,7 @@ class TunnelControlTile : TileService(), LifecycleOwner {
// this would be better managed elsewhere // this would be better managed elsewhere
WireGuardAutoTunnel.setLastActiveTunnels(activeIds) WireGuardAutoTunnel.setLastActiveTunnels(activeIds)
val activeTunNames = val activeTunNames =
tunnels.filter { activeTunnels.keys.contains(it.id) }.map { it.tunName } tunnels.filter { activeTunnels.keys.contains(it.id) }.map { it.name }
updateTileForActiveTunnels(activeTunNames) updateTileForActiveTunnels(activeTunNames)
} }
else -> updateTileForLastActiveTunnels() else -> updateTileForLastActiveTunnels()
@@ -93,15 +93,14 @@ class TunnelControlTile : TileService(), LifecycleOwner {
val lastActiveIds = WireGuardAutoTunnel.getLastActiveTunnels() val lastActiveIds = WireGuardAutoTunnel.getLastActiveTunnels()
when { when {
lastActiveIds.isEmpty() -> { lastActiveIds.isEmpty() -> {
tunnelsRepository.getStartTunnel()?.let { config -> tunnelsRepository.getStartTunnel()?.let { config -> updateTile(config.name, false) }
updateTile(config.tunName, false) ?: setUnavailable()
} ?: setUnavailable()
} }
lastActiveIds.size > 1 -> updateTile(getString(R.string.multiple), false) lastActiveIds.size > 1 -> updateTile(getString(R.string.multiple), false)
else -> { else -> {
val tunnelId = lastActiveIds.first() val tunnelId = lastActiveIds.first()
tunnelsRepository.getById(tunnelId)?.let { tunnel -> tunnelsRepository.getById(tunnelId)?.let { tunnel ->
updateTile(tunnel.tunName, false) updateTile(tunnel.name, false)
} ?: setUnavailable() } ?: setUnavailable()
} }
} }
@@ -160,6 +159,15 @@ class TunnelControlTile : TileService(), LifecycleOwner {
} }
} }
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. */ /* This works around an annoying unsolved frameworks bug some people are hitting. */
override fun onBind(intent: Intent): IBinder? { override fun onBind(intent: Intent): IBinder? {
var ret: IBinder? = null var ret: IBinder? = null
@@ -171,15 +179,6 @@ class TunnelControlTile : TileService(), LifecycleOwner {
return ret return ret
} }
private fun updateTile(name: String, active: Boolean) {
runCatching {
setTileDescription(name)
if (active) return setActive()
setInactive()
}
.onFailure { Timber.e(it) }
}
override val lifecycle: Lifecycle override val lifecycle: Lifecycle
get() = lifecycleRegistry get() = lifecycleRegistry
} }
@@ -6,6 +6,7 @@ import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelSer
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@@ -18,6 +19,7 @@ import timber.log.Timber
class ShortcutsActivity : ComponentActivity() { class ShortcutsActivity : ComponentActivity() {
@Inject lateinit var settingsRepository: GeneralSettingRepository @Inject lateinit var settingsRepository: GeneralSettingRepository
@Inject lateinit var autoTunnelSettingsRepository: AutoTunnelSettingsRepository
@Inject lateinit var tunnelsRepository: TunnelRepository @Inject lateinit var tunnelsRepository: TunnelRepository
@Inject lateinit var tunnelManager: TunnelManager @Inject lateinit var tunnelManager: TunnelManager
@@ -27,7 +29,7 @@ class ShortcutsActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
applicationScope.launch { applicationScope.launch {
val settings = settingsRepository.get() val settings = settingsRepository.getGeneralSettings()
if (settings.isShortcutsEnabled) { if (settings.isShortcutsEnabled) {
when (intent.getStringExtra(CLASS_NAME_EXTRA_KEY)) { when (intent.getStringExtra(CLASS_NAME_EXTRA_KEY)) {
LEGACY_TUNNEL_SERVICE_NAME, LEGACY_TUNNEL_SERVICE_NAME,
@@ -37,7 +39,7 @@ class ShortcutsActivity : ComponentActivity() {
val tunnelConfig = val tunnelConfig =
tunnelName?.let { tunnelsRepository.findByTunnelName(it) } tunnelName?.let { tunnelsRepository.findByTunnelName(it) }
?: tunnelsRepository.getDefaultTunnel() ?: tunnelsRepository.getDefaultTunnel()
Timber.d("Shortcut action on name: ${tunnelConfig?.tunName}") Timber.d("Shortcut action on name: ${tunnelConfig?.name}")
tunnelConfig?.let { tunnelConfig?.let {
when (intent.action) { when (intent.action) {
Action.START.name -> tunnelManager.startTunnel(it) Action.START.name -> tunnelManager.startTunnel(it)
@@ -49,8 +51,10 @@ class ShortcutsActivity : ComponentActivity() {
AutoTunnelService::class.java.simpleName, AutoTunnelService::class.java.simpleName,
LEGACY_AUTO_TUNNEL_SERVICE_NAME -> { LEGACY_AUTO_TUNNEL_SERVICE_NAME -> {
when (intent.action) { when (intent.action) {
Action.START.name -> settingsRepository.updateAutoTunnelEnabled(true) Action.START.name ->
Action.STOP.name -> settingsRepository.updateAutoTunnelEnabled(false) autoTunnelSettingsRepository.updateAutoTunnelEnabled(true)
Action.STOP.name ->
autoTunnelSettingsRepository.updateAutoTunnelEnabled(false)
} }
} }
} }
@@ -1,28 +1,28 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException
import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.state.LogHealthState import com.zaneschepke.wireguardautotunnel.domain.state.LogHealthState
import com.zaneschepke.wireguardautotunnel.domain.state.PingState import com.zaneschepke.wireguardautotunnel.domain.state.PingState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import kotlin.coroutines.cancellation.CancellationException import kotlin.coroutines.cancellation.CancellationException
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.*
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import org.amnezia.awg.crypto.Key
import timber.log.Timber import timber.log.Timber
abstract class BaseTunnel(@ApplicationScope protected val applicationScope: CoroutineScope) : abstract class BaseTunnel(
TunnelProvider { @ApplicationScope protected val applicationScope: CoroutineScope,
@IoDispatcher protected val ioDispatcher: CoroutineDispatcher,
) : TunnelProvider {
protected val errors = MutableSharedFlow<Pair<String, BackendCoreException>>() protected val errors = MutableSharedFlow<Pair<String, BackendCoreException>>()
override val errorEvents = errors.asSharedFlow() override val errorEvents = errors.asSharedFlow()
@@ -33,17 +33,19 @@ abstract class BaseTunnel(@ApplicationScope protected val applicationScope: Coro
protected val activeTuns = MutableStateFlow<Map<Int, TunnelState>>(emptyMap()) protected val activeTuns = MutableStateFlow<Map<Int, TunnelState>>(emptyMap())
override val activeTunnels = activeTuns.asStateFlow() override val activeTunnels = activeTuns.asStateFlow()
private val tunJobs = ConcurrentHashMap<Int, Job>() protected val tunJobs = ConcurrentHashMap<Int, Job>()
private val tunMutex = Mutex() private val tunMutex = Mutex()
private val tunStatusMutex = Mutex() private val tunStatusMutex = Mutex()
abstract fun tunnelStateFlow(tunnelConf: TunnelConf): Flow<TunnelStatus> abstract fun tunnelStateFlow(tunnelConfig: TunnelConfig): Flow<TunnelStatus>
abstract override fun setBackendMode(backendMode: BackendMode) abstract override fun setBackendMode(backendMode: BackendMode)
abstract override fun getBackendMode(): BackendMode abstract override fun getBackendMode(): BackendMode
abstract override fun handleDnsReresolve(tunnelConf: TunnelConf): Boolean abstract override suspend fun forceStopTunnel(tunnelId: Int)
abstract override fun handleDnsReresolve(tunnelConfig: TunnelConfig): Boolean
abstract override fun getStatistics(tunnelId: Int): TunnelStatistics? abstract override fun getStatistics(tunnelId: Int): TunnelStatistics?
@@ -51,7 +53,7 @@ abstract class BaseTunnel(@ApplicationScope protected val applicationScope: Coro
tunnelId: Int, tunnelId: Int,
status: TunnelStatus?, status: TunnelStatus?,
stats: TunnelStatistics?, stats: TunnelStatistics?,
pingStates: Map<Key, PingState>?, pingStates: Map<String, PingState>?,
logHealthState: LogHealthState?, logHealthState: LogHealthState?,
) { ) {
tunStatusMutex.withLock { tunStatusMutex.withLock {
@@ -96,37 +98,51 @@ abstract class BaseTunnel(@ApplicationScope protected val applicationScope: Coro
} }
} }
override suspend fun startTunnel(tunnelConf: TunnelConf) { override suspend fun startTunnel(tunnelConfig: TunnelConfig) {
tunMutex.withLock { tunMutex.withLock {
if (activeTuns.value.containsKey(tunnelConf.id) || tunJobs.containsKey(tunnelConf.id)) { if (
return Timber.w("Tunnel is already running: ${tunnelConf.tunName}") activeTuns.value.containsKey(tunnelConfig.id) ||
tunJobs.containsKey(tunnelConfig.id)
) {
return Timber.w("Tunnel is already running: ${tunnelConfig.name}")
} }
updateTunnelStatus(tunnelConf.id, TunnelStatus.Starting) updateTunnelStatus(tunnelConfig.id, TunnelStatus.Starting)
val job = val job =
applicationScope.launch { applicationScope.launch(ioDispatcher) {
try { try {
tunnelStateFlow(tunnelConf).collect { status -> tunnelStateFlow(tunnelConfig).collect { status ->
updateTunnelStatus(tunnelConf.id, status) updateTunnelStatus(tunnelConfig.id, status)
} }
} catch (e: BackendCoreException) { } catch (e: BackendCoreException) {
errors.emit(tunnelConf.tunName to e) errors.emit(tunnelConfig.name to e)
updateTunnelStatus(tunnelConf.id, TunnelStatus.Down) updateTunnelStatus(tunnelConfig.id, TunnelStatus.Down)
} catch (_: CancellationException) {} } catch (_: CancellationException) {}
} }
tunJobs[tunnelConf.id] = job tunJobs[tunnelConfig.id] = job
job.invokeOnCompletion { job.invokeOnCompletion {
tunJobs.remove(tunnelConf.id) tunJobs.remove(tunnelConfig.id)
activeTuns.update { it - tunnelConf.id } activeTuns.update { it - tunnelConfig.id }
} }
} }
} }
override suspend fun stopTunnel(tunnelId: Int) { override suspend fun stopTunnel(tunnelId: Int) {
tunMutex.withLock { tunMutex.withLock {
val currentState = activeTuns.value[tunnelId]?.status ?: return@withLock
updateTunnelStatus(tunnelId, TunnelStatus.Stopping) updateTunnelStatus(tunnelId, TunnelStatus.Stopping)
tunJobs[tunnelId]?.cancel() // Triggers awaitClose to stop backend tunJobs[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)
}
} }
} }
@@ -134,4 +150,9 @@ abstract class BaseTunnel(@ApplicationScope protected val applicationScope: Coro
Timber.d("Removing job for $tunnelId") Timber.d("Removing job for $tunnelId")
tunJobs -= tunnelId tunJobs -= tunnelId
} }
companion object {
const val STARTUP_TIMEOUT_MS: Long = 15_000L
const val STOP_TIMEOUT_MS: Long = 5_000L
}
} }
@@ -1,44 +1,44 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
fun Map<TunnelConf, TunnelState>.allDown(): Boolean { fun Map<TunnelConfig, TunnelState>.allDown(): Boolean {
return this.all { it.value.status.isDown() } return this.all { it.value.status.isDown() }
} }
fun Map<TunnelConf, TunnelState>.hasActive(): Boolean { fun Map<TunnelConfig, TunnelState>.hasActive(): Boolean {
return this.any { it.value.status.isUp() } return this.any { it.value.status.isUp() }
} }
fun Map<TunnelConf, TunnelState>.getValueById(id: Int): TunnelState? { fun Map<TunnelConfig, TunnelState>.getValueById(id: Int): TunnelState? {
val key = this.keys.find { it.id == id } val key = this.keys.find { it.id == id }
return key?.let { this@getValueById[it] } return key?.let { this@getValueById[it] }
} }
fun Map<TunnelConf, TunnelState>.getKeyById(id: Int): TunnelConf? { fun Map<TunnelConfig, TunnelState>.getKeyById(id: Int): TunnelConfig? {
return this.keys.find { it.id == id } return this.keys.find { it.id == id }
} }
fun Map<TunnelConf, TunnelState>.isUp(tunnelConf: TunnelConf): Boolean { fun Map<TunnelConfig, TunnelState>.isUp(tunnelConfig: TunnelConfig): Boolean {
return this.getValueById(tunnelConf.id)?.status?.isUp() ?: false return this.getValueById(tunnelConfig.id)?.status?.isUp() ?: false
} }
fun MutableStateFlow<Map<TunnelConf, TunnelState>>.exists(id: Int): Boolean { fun MutableStateFlow<Map<TunnelConfig, TunnelState>>.exists(id: Int): Boolean {
return this.value.any { it.key.id == id } return this.value.any { it.key.id == id }
} }
fun MutableStateFlow<Map<TunnelConf, TunnelState>>.isUp(id: Int): Boolean { fun MutableStateFlow<Map<TunnelConfig, TunnelState>>.isUp(id: Int): Boolean {
return this.value.any { it.key.id == id && it.value.status == TunnelStatus.Up } return this.value.any { it.key.id == id && it.value.status == TunnelStatus.Up }
} }
fun MutableStateFlow<Map<TunnelConf, TunnelState>>.isStarting(id: Int): Boolean { fun MutableStateFlow<Map<TunnelConfig, TunnelState>>.isStarting(id: Int): Boolean {
return this.value.any { it.key.id == id && it.value.status == TunnelStatus.Starting } return this.value.any { it.key.id == id && it.value.status == TunnelStatus.Starting }
} }
fun MutableStateFlow<Map<TunnelConf, TunnelState>>.findTunnel(id: Int): TunnelConf? { fun MutableStateFlow<Map<TunnelConfig, TunnelState>>.findTunnel(id: Int): TunnelConfig? {
return this.value.keys.find { it.id == id } return this.value.keys.find { it.id == id }
} }
@@ -4,51 +4,60 @@ import com.wireguard.android.backend.Backend
import com.wireguard.android.backend.BackendException import com.wireguard.android.backend.BackendException
import com.wireguard.android.backend.Tunnel as WgTunnel import com.wireguard.android.backend.Tunnel as WgTunnel
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.di.Kernel import com.zaneschepke.wireguardautotunnel.di.Kernel
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.domain.state.WireGuardStatistics import com.zaneschepke.wireguardautotunnel.domain.state.WireGuardStatistics
import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendCoreException import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendCoreException
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.flow.update
import timber.log.Timber import timber.log.Timber
class KernelTunnel class KernelTunnel
@Inject @Inject
constructor( constructor(
@ApplicationScope applicationScope: CoroutineScope, @ApplicationScope applicationScope: CoroutineScope,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
@Kernel private val backend: Backend, @Kernel private val backend: Backend,
) : BaseTunnel(applicationScope) { ) : BaseTunnel(applicationScope, ioDispatcher) {
private val runtimeTunnels = ConcurrentHashMap<Int, WgTunnel>() private val runtimeTunnels = ConcurrentHashMap<Int, WgTunnel>()
// TODO Add DNS settings // TODO Add DNS settings
override fun tunnelStateFlow(tunnelConf: TunnelConf): Flow<TunnelStatus> = callbackFlow { override fun tunnelStateFlow(tunnelConfig: TunnelConfig): Flow<TunnelStatus> = callbackFlow {
if (!tunnelConf.isNameKernelCompatible) close(BackendCoreException.TunnelNameTooLong) if (!tunnelConfig.isNameKernelCompatible) close(BackendCoreException.TunnelNameTooLong)
val stateChannel = Channel<WgTunnel.State>() val stateChannel = Channel<WgTunnel.State>()
val runtimeTunnel = RuntimeWgTunnel(tunnelConf, stateChannel) val runtimeTunnel = RuntimeWgTunnel(tunnelConfig, stateChannel)
runtimeTunnels[tunnelConf.id] = runtimeTunnel runtimeTunnels[tunnelConfig.id] = runtimeTunnel
val consumerJob = launch { val consumerJob = launch {
stateChannel.consumeAsFlow().collect { state -> trySend(state.asTunnelState()) } stateChannel.consumeAsFlow().collect { state -> trySend(state.asTunnelState()) }
} }
try { try {
updateTunnelStatus(tunnelConf.id, TunnelStatus.Starting) withTimeout(STARTUP_TIMEOUT_MS) {
backend.setState(runtimeTunnel, WgTunnel.State.UP, tunnelConf.toWgConfig()) updateTunnelStatus(tunnelConfig.id, TunnelStatus.Starting)
backend.setState(runtimeTunnel, WgTunnel.State.UP, tunnelConfig.toWgConfig())
}
} catch (e: TimeoutCancellationException) {
Timber.e("Startup timed out for ${tunnelConfig.name}")
errors.emit(tunnelConfig.name to BackendCoreException.DNS)
forceStopTunnel(tunnelConfig.id)
close()
} catch (e: BackendException) { } catch (e: BackendException) {
close(e.toBackendCoreException()) close(e.toBackendCoreException())
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
@@ -63,11 +72,11 @@ constructor(
try { try {
backend.setState(runtimeTunnel, WgTunnel.State.DOWN, null) backend.setState(runtimeTunnel, WgTunnel.State.DOWN, null)
} catch (e: BackendException) { } catch (e: BackendException) {
errors.tryEmit(tunnelConf.tunName to e.toBackendCoreException()) errors.tryEmit(tunnelConfig.name to e.toBackendCoreException())
} finally { } finally {
consumerJob.cancel() consumerJob.cancel()
stateChannel.close() stateChannel.close()
runtimeTunnels.remove(tunnelConf.id) runtimeTunnels.remove(tunnelConfig.id)
trySend(TunnelStatus.Down) trySend(TunnelStatus.Down)
close() close()
} }
@@ -92,11 +101,26 @@ constructor(
return BackendMode.Inactive return BackendMode.Inactive
} }
override fun handleDnsReresolve(tunnelConf: TunnelConf): Boolean { override fun handleDnsReresolve(tunnelConfig: TunnelConfig): Boolean {
throw NotImplementedError() throw NotImplementedError()
} }
override suspend fun runningTunnelNames(): Set<String> { override suspend fun runningTunnelNames(): Set<String> {
return backend.runningTunnelNames return backend.runningTunnelNames
} }
override suspend fun forceStopTunnel(tunnelId: Int) {
val runtimeTunnel = runtimeTunnels[tunnelId] ?: return
try {
backend.setState(runtimeTunnel, WgTunnel.State.DOWN, null)
} catch (e: BackendException) {
Timber.e(e, "Force stop failed for $tunnelId")
} finally {
tunJobs[tunnelId]?.cancel()
runtimeTunnels.remove(tunnelId)
tunJobs.remove(tunnelId)
activeTuns.update { it - tunnelId }
updateTunnelStatus(tunnelId, TunnelStatus.Down)
}
}
} }
@@ -1,19 +1,19 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import org.amnezia.awg.backend.Tunnel import org.amnezia.awg.backend.Tunnel
class RuntimeAwgTunnel( class RuntimeAwgTunnel(
private val tunnelConf: TunnelConf, private val tunnelConfig: TunnelConfig,
private val stateChannel: Channel<Tunnel.State>, private val stateChannel: Channel<Tunnel.State>,
) : Tunnel { ) : Tunnel {
override fun getName() = tunnelConf.tunName override fun getName() = tunnelConfig.name
override fun onStateChange(newState: Tunnel.State) { override fun onStateChange(newState: Tunnel.State) {
stateChannel.trySend(newState) stateChannel.trySend(newState)
} }
override fun isIpv4ResolutionPreferred() = tunnelConf.isIpv4Preferred override fun isIpv4ResolutionPreferred() = tunnelConfig.isIpv4Preferred
} }
@@ -1,15 +1,15 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.wireguard.android.backend.Tunnel import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
class RuntimeWgTunnel( class RuntimeWgTunnel(
private val config: TunnelConf, private val config: TunnelConfig,
private val stateChannel: Channel<Tunnel.State>, private val stateChannel: Channel<Tunnel.State>,
) : Tunnel { ) : Tunnel {
override fun getName() = config.tunName override fun getName() = config.name
override fun onStateChange(newState: Tunnel.State) { override fun onStateChange(newState: Tunnel.State) {
stateChannel.trySend(newState) stateChannel.trySend(newState)
@@ -1,14 +1,17 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig as Entity
import com.zaneschepke.wireguardautotunnel.data.model.AppMode import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.di.* import com.zaneschepke.wireguardautotunnel.di.*
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException
import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage
import com.zaneschepke.wireguardautotunnel.domain.model.AutoTunnelSettings
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf 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.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.state.LogHealthState import com.zaneschepke.wireguardautotunnel.domain.state.LogHealthState
@@ -24,7 +27,6 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import org.amnezia.awg.crypto.Key
import timber.log.Timber import timber.log.Timber
@OptIn(ExperimentalCoroutinesApi::class, ExperimentalAtomicApi::class) @OptIn(ExperimentalCoroutinesApi::class, ExperimentalAtomicApi::class)
@@ -36,18 +38,28 @@ constructor(
@ProxyUserspace private val proxyUserspaceTunnel: TunnelProvider, @ProxyUserspace private val proxyUserspaceTunnel: TunnelProvider,
private val serviceManager: ServiceManager, private val serviceManager: ServiceManager,
private val settingsRepository: GeneralSettingRepository, private val settingsRepository: GeneralSettingRepository,
private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
private val tunnelsRepository: TunnelRepository, private val tunnelsRepository: TunnelRepository,
private val tunnelMonitor: TunnelMonitor, private val tunnelMonitor: TunnelMonitor,
@ApplicationScope private val applicationScope: CoroutineScope, @ApplicationScope private val applicationScope: CoroutineScope,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher, @IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : TunnelProvider { ) : TunnelProvider {
@OptIn(ExperimentalCoroutinesApi::class)
private val localErrorEvents = MutableSharedFlow<Pair<String?, BackendCoreException>>()
@OptIn(ExperimentalCoroutinesApi::class)
private val localMessageEvents = MutableSharedFlow<Pair<String?, BackendMessage>>()
private val monitoringMutex = Mutex() private val monitoringMutex = Mutex()
private val monitoringJobs = ConcurrentHashMap<Int, Job>() private val monitoringJobs = ConcurrentHashMap<Int, Job>()
private val ddnsMutex = Mutex()
private val ddnsJobs = ConcurrentHashMap<Int, Job>()
private data class SideEffectState( private data class SideEffectState(
val activeTuns: Map<Int, TunnelState>, val activeTuns: Map<Int, TunnelState>,
val tuns: List<TunnelConf>, val tuns: List<TunnelConfig>,
val settings: GeneralSettings, val settings: GeneralSettings,
val previouslyActive: Map<Int, TunnelState>, val previouslyActive: Map<Int, TunnelState>,
) )
@@ -57,6 +69,12 @@ constructor(
val condition: (SideEffectState) -> Boolean, val condition: (SideEffectState) -> Boolean,
) )
private suspend fun getSettings(): GeneralSettings =
settingsRepository.flow.filterNotNull().first { it != GeneralSettings() }
private suspend fun getTunnels(): List<TunnelConfig> =
tunnelsRepository.flow.first { it.isNotEmpty() }
private val tunnelProviderFlow: StateFlow<TunnelProvider> = run { private val tunnelProviderFlow: StateFlow<TunnelProvider> = run {
val currentBackend = AtomicReference(userspaceTunnel) val currentBackend = AtomicReference(userspaceTunnel)
val currentSettings = AtomicReference(GeneralSettings()) val currentSettings = AtomicReference(GeneralSettings())
@@ -115,7 +133,7 @@ constructor(
Triple(activeTuns, tuns, settings) Triple(activeTuns, tuns, settings)
} }
} }
.onStart { handleStateRestore() } .onStart { handleRestore() }
.onEach { (activeTuns, tuns, settings) -> .onEach { (activeTuns, tuns, settings) ->
val previouslyActive = activeTunsReference.exchange(activeTuns) val previouslyActive = activeTunsReference.exchange(activeTuns)
val state = SideEffectState(activeTuns, tuns, settings, previouslyActive) val state = SideEffectState(activeTuns, tuns, settings, previouslyActive)
@@ -144,16 +162,12 @@ constructor(
s.activeTuns.size != s.previouslyActive.size s.activeTuns.size != s.previouslyActive.size
}, },
), ),
// TODO Not for kernel mode for now
SideEffectWithCondition( SideEffectWithCondition(
effect = { s -> effect = { s ->
handleTunnelMonitoringChanges(s.activeTuns, s.tuns) handleDynamicDnsMonitoring(s.activeTuns, s.tuns, s.settings)
}, },
condition = { s -> condition = { s ->
s.tuns.any { s.activeTuns.keys != s.previouslyActive.keys
it.restartOnPingFailure &&
s.activeTuns.keys.contains(it.id)
} && s.settings.appMode != AppMode.KERNEL
}, },
), ),
SideEffectWithCondition( SideEffectWithCondition(
@@ -189,19 +203,17 @@ constructor(
} }
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
override val errorEvents: SharedFlow<Pair<String, BackendCoreException>> = override val errorEvents: SharedFlow<Pair<String?, BackendCoreException>> =
tunnelProviderFlow merge(localErrorEvents, tunnelProviderFlow.flatMapLatest { it.errorEvents })
.flatMapLatest { it.errorEvents }
.shareIn( .shareIn(
scope = applicationScope.plus(ioDispatcher), scope = applicationScope + ioDispatcher,
started = SharingStarted.Eagerly, started = SharingStarted.Eagerly,
replay = 0, replay = 0,
) )
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
override val messageEvents: SharedFlow<Pair<String, BackendMessage>> = override val messageEvents: SharedFlow<Pair<String?, BackendMessage>> =
tunnelProviderFlow merge(localMessageEvents, tunnelProviderFlow.flatMapLatest { it.messageEvents })
.flatMapLatest { it.messageEvents }
.shareIn( .shareIn(
scope = applicationScope.plus(ioDispatcher), scope = applicationScope.plus(ioDispatcher),
started = SharingStarted.Eagerly, started = SharingStarted.Eagerly,
@@ -212,17 +224,38 @@ constructor(
return tunnelProviderFlow.value.getStatistics(tunnelId) return tunnelProviderFlow.value.getStatistics(tunnelId)
} }
override suspend fun startTunnel(tunnelConf: TunnelConf) { override suspend fun startTunnel(tunnelConfig: TunnelConfig) {
// for VPN Mode, we need to stop active tunnels as we can only have one active at a time if (activeTunnels.value.containsKey(tunnelConfig.id)) return
if (activeTunnels.value.isNotEmpty() && tunnelProviderFlow.value == userspaceTunnel) val provider = tunnelProviderFlow.value
val isKernel = provider is KernelTunnel
if (!isKernel && activeTunnels.value.isNotEmpty()) {
stopActiveTunnels() stopActiveTunnels()
tunnelProviderFlow.value.startTunnel(tunnelConf) withTimeoutOrNull(BaseTunnel.STARTUP_TIMEOUT_MS) {
activeTunnels.first { it.isEmpty() }
} ?: run { activeTunnels.value.keys.forEach { id -> provider.forceStopTunnel(id) } }
}
val runConfig =
tunnelConfig.run {
if (getSettings().isTunnelGlobalsEnabled) {
val globalTunnel =
getTunnels().firstOrNull { it.name == Entity.GLOBAL_CONFIG_NAME }
?: return@run this
return@run copyWithGlobalValues(globalTunnel)
}
this
}
tunnelProviderFlow.value.startTunnel(runConfig)
} }
override suspend fun stopTunnel(tunnelId: Int) { override suspend fun stopTunnel(tunnelId: Int) {
tunnelProviderFlow.value.stopTunnel(tunnelId) tunnelProviderFlow.value.stopTunnel(tunnelId)
} }
override suspend fun forceStopTunnel(tunnelId: Int) {
tunnelProviderFlow.value.forceStopTunnel(tunnelId)
}
override suspend fun stopActiveTunnels() { override suspend fun stopActiveTunnels() {
tunnelProviderFlow.value.stopActiveTunnels() tunnelProviderFlow.value.stopActiveTunnels()
} }
@@ -239,15 +272,15 @@ constructor(
return tunnelProviderFlow.value.runningTunnelNames() return tunnelProviderFlow.value.runningTunnelNames()
} }
override fun handleDnsReresolve(tunnelConf: TunnelConf): Boolean { override fun handleDnsReresolve(tunnelConfig: TunnelConfig): Boolean {
return tunnelProviderFlow.value.handleDnsReresolve(tunnelConf) return tunnelProviderFlow.value.handleDnsReresolve(tunnelConfig)
} }
override suspend fun updateTunnelStatus( override suspend fun updateTunnelStatus(
tunnelId: Int, tunnelId: Int,
status: TunnelStatus?, status: TunnelStatus?,
stats: TunnelStatistics?, stats: TunnelStatistics?,
pingStates: Map<Key, PingState>?, pingStates: Map<String, PingState>?,
logHealthState: LogHealthState?, logHealthState: LogHealthState?,
) { ) {
tunnelProviderFlow.value.updateTunnelStatus( tunnelProviderFlow.value.updateTunnelStatus(
@@ -270,15 +303,15 @@ constructor(
} }
private fun handleLockDownModeInit(withLanBypass: Boolean) { private fun handleLockDownModeInit(withLanBypass: Boolean) {
val allowedIps = if (withLanBypass) TunnelConf.IPV4_PUBLIC_NETWORKS else emptySet() val allowedIps = if (withLanBypass) TunnelConfig.IPV4_PUBLIC_NETWORKS else emptySet()
try { try {
// TODO handle situation where they don't have vpn permission, request it
if (serviceManager.hasVpnPermission()) { if (serviceManager.hasVpnPermission()) {
proxyUserspaceTunnel.setBackendMode(BackendMode.KillSwitch(allowedIps)) proxyUserspaceTunnel.setBackendMode(BackendMode.KillSwitch(allowedIps))
} else {
throw BackendCoreException.NotAuthorized
} }
} catch (e: BackendCoreException) { } catch (e: BackendCoreException) {
// TODO expose this error to user localErrorEvents.tryEmit(null to e)
Timber.e(e)
} }
} }
@@ -292,64 +325,64 @@ constructor(
proxyUserspaceTunnel.setBackendMode(BackendMode.Inactive) proxyUserspaceTunnel.setBackendMode(BackendMode.Inactive)
} }
private suspend fun handleStateRestore() { suspend fun handleRestore() =
val settings = settingsRepository.flow.first() withContext(ioDispatcher) {
if (settings.isRestoreOnBootEnabled) { val settings = settingsRepository.getGeneralSettings()
if (settings.isAutoTunnelEnabled) { val autoTunnelSettings = autoTunnelSettingsRepository.getAutoTunnelSettings()
tunnelsRepository.resetActiveTunnels() val tunnels = tunnelsRepository.getAll()
return settingsRepository.updateAutoTunnelEnabled(true) if (autoTunnelSettings.isAutoTunnelEnabled)
} return@withContext restoreAutoTunnel(autoTunnelSettings)
val tunnels = tunnelsRepository.flow.first() when (val mode = settings.appMode) {
when (settings.appMode) {
// TODO eventually, lockdown/proxy can support multi
AppMode.VPN, AppMode.VPN,
AppMode.LOCK_DOWN, AppMode.PROXY,
AppMode.PROXY -> AppMode.LOCK_DOWN -> {
tunnels if (serviceManager.hasVpnPermission()) {
.firstOrNull { it.isActive } if (mode == AppMode.LOCK_DOWN)
?.let { handleLockDownModeInit(settings.isLanOnKillSwitchEnabled)
// clear any duplicates tunnels.firstOrNull { it.isActive }?.let { startTunnel(it) }
tunnelsRepository.resetActiveTunnels() } else localErrorEvents.emit(null to BackendCoreException.NotAuthorized)
startTunnel(it) }
}
// kernel supports multi
AppMode.KERNEL -> AppMode.KERNEL ->
tunnels.filter { it.isActive }.forEach { conf -> startTunnel(conf) } tunnels.filter { it.isActive }.forEach { conf -> startTunnel(conf) }
} }
} }
private suspend fun restoreAutoTunnel(autoTunnelSettings: AutoTunnelSettings) {
autoTunnelSettingsRepository.upsert(autoTunnelSettings.copy(isAutoTunnelEnabled = true))
serviceManager.startAutoTunnelService()
} }
private suspend fun handleTunnelMonitoringChanges( suspend fun handleReboot() =
activeTuns: Map<Int, TunnelState>, withContext(ioDispatcher) {
configs: List<TunnelConf>, val settings = settingsRepository.getGeneralSettings()
) { val autoTunnelSettings = autoTunnelSettingsRepository.getAutoTunnelSettings()
configs val defaultTunnel = tunnelsRepository.getStartTunnel()
.filter { it.restartOnPingFailure && activeTuns.keys.contains(it.id) } if (autoTunnelSettings.startOnBoot)
.forEach { conf -> return@withContext restoreAutoTunnel(autoTunnelSettings)
val tunState = activeTuns[conf.id] ?: return@forEach if (settings.isRestoreOnBootEnabled) {
if (tunState.health() == TunnelState.Health.UNHEALTHY) { tunnelsRepository.resetActiveTunnels()
runCatching { when (val mode = settings.appMode) {
val updated = handleDnsReresolve(conf) AppMode.VPN,
// TODO user messages AppMode.PROXY,
if (updated) { AppMode.LOCK_DOWN -> {
Timber.i("Successfully update the peer endpoint to new address.") if (serviceManager.hasVpnPermission()) {
} else { if (mode == AppMode.LOCK_DOWN)
Timber.i("Current endpoint address is already up to date.") handleLockDownModeInit(settings.isLanOnKillSwitchEnabled)
} } else
} return@withContext localErrorEvents.emit(
.onFailure { null to BackendCoreException.NotAuthorized
Timber.e(it, "Failed to handle dns re-resolution for ${conf.tunName}") )
} }
// TODO backoff AppMode.KERNEL -> Unit
delay(30_000L)
} }
defaultTunnel?.let { startTunnel(it) }
} }
} }
private suspend fun handleTunnelsActiveChange( private suspend fun handleTunnelsActiveChange(
previousActiveTuns: Map<Int, TunnelState>, previousActiveTuns: Map<Int, TunnelState>,
activeTuns: Map<Int, TunnelState>, activeTuns: Map<Int, TunnelState>,
tuns: List<TunnelConf>, tuns: List<TunnelConfig>,
) { ) {
val relevantTunnels = previousActiveTuns.keys + activeTuns.keys val relevantTunnels = previousActiveTuns.keys + activeTuns.keys
@@ -376,9 +409,82 @@ constructor(
} }
} }
private suspend fun handleDynamicDnsMonitoring(
activeTuns: Map<Int, TunnelState>,
configs: List<TunnelConfig>,
settings: GeneralSettings,
) =
ddnsMutex.withLock {
val activeIds =
activeTuns.keys
.filter { id ->
configs.find { it.id == id }?.restartOnPingFailure == true &&
settings.appMode != AppMode.KERNEL
}
.toSet()
val currentJobs = ddnsJobs.keys.toSet()
val obsoleteIds = currentJobs - activeIds
Timber.d(
"DDNS Monitoring: Active IDs: $activeIds, Obsolete IDs: $obsoleteIds, Total jobs before: ${ddnsJobs.size}"
)
obsoleteIds.forEach { id ->
ddnsJobs[id]?.cancel()
ddnsJobs.remove(id)
}
activeIds.forEach { id ->
if (ddnsJobs.containsKey(id)) return@forEach // Skip if already monitored
val conf = configs.find { it.id == id } ?: return@forEach
val tunStateFlow =
activeTunnels.map { it[id] }.stateIn(applicationScope + ioDispatcher)
val newJob =
applicationScope.launch(ioDispatcher) {
var backoff = 30_000L
while (isActive) {
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(conf)
if (updated) {
localMessageEvents.emit(
conf.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 ${conf.name}",
)
}
delay(backoff)
backoff = (backoff * 1.5).toLong().coerceAtMost(MAX_BACKOFF_TIME)
}
}
ddnsJobs[id] = newJob
}
}
private suspend fun handleFullTunnelMonitoring( private suspend fun handleFullTunnelMonitoring(
activeTuns: Map<Int, TunnelState>, activeTuns: Map<Int, TunnelState>,
configs: List<TunnelConf>, configs: List<TunnelConfig>,
settings: GeneralSettings, settings: GeneralSettings,
) = ) =
monitoringMutex.withLock { monitoringMutex.withLock {
@@ -397,7 +503,7 @@ constructor(
activeIds.forEach { id -> activeIds.forEach { id ->
if (monitoringJobs.containsKey(id)) return@forEach // Skip if already monitored if (monitoringJobs.containsKey(id)) return@forEach // Skip if already monitored
val config = configs.find { it.id == id } ?: return@forEach configs.find { it.id == id } ?: return@forEach
val tunStateFlow = val tunStateFlow =
activeTunnels.map { it[id] }.stateIn(applicationScope + ioDispatcher) activeTunnels.map { it[id] }.stateIn(applicationScope + ioDispatcher)
val newJob = val newJob =
@@ -407,7 +513,7 @@ constructor(
withLogs = settings.appMode != AppMode.KERNEL, withLogs = settings.appMode != AppMode.KERNEL,
tunStateFlow = tunStateFlow, tunStateFlow = tunStateFlow,
getStatistics = { tunnelId -> getStatistics(tunnelId) }, getStatistics = { tunnelId -> getStatistics(tunnelId) },
updateTunnelStatus = { tid, status, stats, pings, logHealth -> updateTunnelStatus = { tid, _, stats, pings, logHealth ->
updateTunnelStatus(tid, null, stats, pings, logHealth) updateTunnelStatus(tid, null, stats, pings, logHealth)
}, },
) )
@@ -415,4 +521,9 @@ constructor(
monitoringJobs[id] = newJob monitoringJobs[id] = newJob
} }
} }
companion object {
const val BASE_BACKOFF = 30_000L
const val MAX_BACKOFF_TIME = 300_000L
}
} }
@@ -4,22 +4,21 @@ import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.networkmonitor.NetworkMonitor import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.data.model.AppMode import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.state.FailureReason import com.zaneschepke.wireguardautotunnel.domain.state.*
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.extensions.toMillis
import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils
import dagger.hilt.android.scopes.ServiceScoped import dagger.hilt.android.scopes.ServiceScoped
import inet.ipaddr.AddressValueException
import inet.ipaddr.IPAddress
import inet.ipaddr.IPAddressString
import io.ktor.util.collections.* import io.ktor.util.collections.*
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import org.amnezia.awg.crypto.Key
import timber.log.Timber import timber.log.Timber
@ServiceScoped @ServiceScoped
@@ -28,6 +27,7 @@ class TunnelMonitor
constructor( constructor(
private val settingsRepository: GeneralSettingRepository, private val settingsRepository: GeneralSettingRepository,
private val tunnelsRepository: TunnelRepository, private val tunnelsRepository: TunnelRepository,
private val monitoringSettingsRepository: MonitoringSettingsRepository,
private val networkMonitor: NetworkMonitor, private val networkMonitor: NetworkMonitor,
private val networkUtils: NetworkUtils, private val networkUtils: NetworkUtils,
private val logReader: LogReader, private val logReader: LogReader,
@@ -41,7 +41,7 @@ constructor(
getStatistics: suspend (Int) -> TunnelStatistics?, getStatistics: suspend (Int) -> TunnelStatistics?,
updateTunnelStatus: updateTunnelStatus:
suspend ( suspend (
Int, TunnelStatus?, TunnelStatistics?, Map<Key, PingState>?, LogHealthState?, Int, TunnelStatus?, TunnelStatistics?, Map<String, PingState>?, LogHealthState?,
) -> Unit, ) -> Unit,
): Job = coroutineScope { ): Job = coroutineScope {
launch { launch {
@@ -53,14 +53,14 @@ constructor(
} }
private suspend fun startLogsMonitor( private suspend fun startLogsMonitor(
tunnelConf: TunnelConf, tunnelConfig: TunnelConfig,
updateTunnelStatus: updateTunnelStatus:
suspend ( suspend (
Int, TunnelStatus?, TunnelStatistics?, Map<Key, PingState>?, LogHealthState?, Int, TunnelStatus?, TunnelStatistics?, Map<String, PingState>?, LogHealthState?,
) -> Unit, ) -> Unit,
) { ) {
logReader.liveLogs logReader.liveLogs
.filter { log -> log.tag.contains(tunnelConf.tunName) } .filter { log -> log.tag.contains(tunnelConfig.name) }
.mapNotNull { log -> .mapNotNull { log ->
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
@@ -76,20 +76,20 @@ constructor(
} }
.distinctUntilChangedBy { it.isHealthy } // Only emit when health changes .distinctUntilChangedBy { it.isHealthy } // Only emit when health changes
.collect { logHealthState -> .collect { logHealthState ->
Timber.d("Tunnel log health updated for ${tunnelConf.tunName}: $logHealthState") Timber.d("Tunnel log health updated for ${tunnelConfig.name}: $logHealthState")
updateTunnelStatus(tunnelConf.id, null, null, null, logHealthState) updateTunnelStatus(tunnelConfig.id, null, null, null, logHealthState)
} }
} }
private suspend fun startPingMonitor( private suspend fun startPingMonitor(
tunnelConf: TunnelConf, tunnelConfig: TunnelConfig,
tunStateFlow: StateFlow<TunnelState?>, tunStateFlow: StateFlow<TunnelState?>,
updateTunnelStatus: updateTunnelStatus:
suspend ( suspend (
Int, TunnelStatus?, TunnelStatistics?, Map<Key, PingState>?, LogHealthState?, Int, TunnelStatus?, TunnelStatistics?, Map<String, PingState>?, LogHealthState?,
) -> Unit, ) -> Unit,
) = coroutineScope { ) = coroutineScope {
val pingStatsFlow = MutableStateFlow<Map<Key, PingState>>(emptyMap()) val pingStatsFlow = MutableStateFlow<Map<String, PingState>>(emptyMap())
val connectivityStateFlow = networkMonitor.connectivityStateFlow.stateIn(this) val connectivityStateFlow = networkMonitor.connectivityStateFlow.stateIn(this)
@@ -114,124 +114,151 @@ constructor(
.distinctUntilChanged() .distinctUntilChanged()
.stateIn(this) .stateIn(this)
settingsRepository.flow combine(
.distinctUntilChanged { old, new -> settingsRepository.flow.distinctUntilChangedBy { it.appMode },
old.isPingEnabled == new.isPingEnabled && monitoringSettingsRepository.flow,
old.tunnelPingIntervalSeconds == new.tunnelPingIntervalSeconds && ) { settings, monitorSettings ->
old.tunnelPingAttempts == new.tunnelPingAttempts && Pair(settings.appMode, monitorSettings)
old.tunnelPingTimeoutSeconds == new.tunnelPingTimeoutSeconds &&
old.appMode == new.appMode
} }
.collectLatest { settings -> .collectLatest { (appMode, settings) ->
if (!settings.isPingEnabled) return@collectLatest if (!settings.isPingEnabled) return@collectLatest
// TODO for now until we get monitoring for these modes // TODO for now until we get monitoring for these modes
if (settings.appMode == AppMode.LOCK_DOWN || settings.appMode == AppMode.PROXY) if (appMode == AppMode.LOCK_DOWN || appMode == AppMode.PROXY) return@collectLatest
return@collectLatest
Timber.d("Starting pinger for ${tunnelConf.tunName} with settings") Timber.d("Starting pinger for ${tunnelConfig.name} with settings")
val config = tunnelConf.toAmConfig() val config = tunnelConfig.toAmConfig()
val pingablePeers = config.peers.filter { it.allowedIps.isNotEmpty() } val pingablePeers = config.peers.filter { it.allowedIps.isNotEmpty() }
if (pingablePeers.isEmpty()) return@collectLatest if (pingablePeers.isEmpty()) return@collectLatest
suspend fun performPing() { suspend fun performPing() {
val updates = ConcurrentMap<Key, PingState>() val updates = ConcurrentMap<String, PingState>()
pingablePeers.forEach { peer -> pingablePeers
ensureActive() .map { it.publicKey.toBase64() to it }
val previousState = pingStatsFlow.value[peer.publicKey] ?: PingState() .forEach { (key, peer) ->
ensureActive()
val previousState = pingStatsFlow.value[key] ?: PingState()
val allowedIpStr = peer.allowedIps.firstOrNull()?.toString() val allowedIpStr = peer.allowedIps.firstOrNull()?.toString()
if (allowedIpStr == null) { if (allowedIpStr == null) {
updates[peer.publicKey] = updates[key] =
previousState.copy( previousState.copy(
isReachable = false, isReachable = false,
failureReason = FailureReason.NoResolvedEndpoint, failureReason = FailureReason.NoResolvedEndpoint,
lastPingAttemptMillis = System.currentTimeMillis(), lastPingAttemptMillis = System.currentTimeMillis(),
) )
return@forEach return@forEach
} }
val host = val host =
tunnelConf.pingTarget tunnelConfig.pingTarget
?: { ?: run {
val parts = allowedIpStr.split("/") val parts = allowedIpStr.split("/")
val internalIp = val internalIp =
if (parts.size == 2) parts[0] else allowedIpStr if (parts.size == 2) parts[0] else allowedIpStr
val prefix = val prefix =
if (parts.size == 2) parts[1].toIntOrNull() ?: 32 if (parts.size == 2) parts[1].toIntOrNull() ?: 32
else 32 else 32
val cleanedIp = internalIp.removeSurrounding("[", "]")
val defaultCloudflare =
if (cleanedIp.contains(":")) CLOUDFLARE_IPV6_IP
else CLOUDFLARE_IPV4_IP
if (prefix <= 1) { if (prefix <= 1) {
CLOUDFLARE_IPV4_IP defaultCloudflare
} else { } else {
internalIp.removeSurrounding("[", "]") 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
}
} }
} }
.invoke()
val attemptTime = System.currentTimeMillis() val attemptTime = System.currentTimeMillis()
runCatching { runCatching {
withTimeout( withTimeout(
settings.tunnelPingTimeoutSeconds?.toMillis() ?: 5000L settings.tunnelPingTimeoutSeconds?.toMillis() ?: 5000L
) { ) {
val pingStats = val pingStats =
settings.tunnelPingTimeoutSeconds?.let { settings.tunnelPingTimeoutSeconds?.let {
networkUtils.pingWithStats( networkUtils.pingWithStats(
host, host,
settings.tunnelPingAttempts, settings.tunnelPingAttempts,
it.toMillis(), it.toMillis(),
) )
} }
?: networkUtils.pingWithStats( ?: networkUtils.pingWithStats(
host, host,
settings.tunnelPingAttempts, settings.tunnelPingAttempts,
) )
updates[peer.publicKey] = 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( previousState.copy(
transmitted = pingStats.transmitted, isReachable = false,
received = pingStats.received, failureReason = FailureReason.PingFailed,
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, pingTarget = host,
lastPingAttemptMillis = attemptTime, 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 ${tunnelConf.tunName} to host $host",
)
updates[peer.publicKey] =
previousState.copy(
isReachable = false,
failureReason = FailureReason.PingFailed,
pingTarget = host,
lastPingAttemptMillis = attemptTime,
)
}
}
if (updates.isNotEmpty()) { if (updates.isNotEmpty()) {
ensureActive() ensureActive()
pingStatsFlow.update { updates } pingStatsFlow.update { updates }
updateTunnelStatus(tunnelConf.id, null, null, updates, null) updateTunnelStatus(tunnelConfig.id, null, null, updates, null)
} }
} }
@@ -256,7 +283,7 @@ constructor(
} }
} }
ensureActive() ensureActive()
updateTunnelStatus(tunnelConf.id, null, null, pingStatsFlow.value, null) updateTunnelStatus(tunnelConfig.id, null, null, pingStatsFlow.value, null)
} }
delay(settings.tunnelPingIntervalSeconds.toMillis()) delay(settings.tunnelPingIntervalSeconds.toMillis())
} }
@@ -268,7 +295,7 @@ constructor(
getStatistics: suspend (Int) -> TunnelStatistics?, getStatistics: suspend (Int) -> TunnelStatistics?,
updateTunnelStatus: updateTunnelStatus:
suspend ( suspend (
Int, TunnelStatus?, TunnelStatistics?, Map<Key, PingState>?, LogHealthState?, Int, TunnelStatus?, TunnelStatistics?, Map<String, PingState>?, LogHealthState?,
) -> Unit, ) -> Unit,
) = coroutineScope { ) = coroutineScope {
while (isActive) { while (isActive) {
@@ -295,7 +322,6 @@ constructor(
const val CLOUDFLARE_IPV6_IP = "2606:4700:4700::1111" const val CLOUDFLARE_IPV6_IP = "2606:4700:4700::1111"
const val CLOUDFLARE_IPV4_IP = "1.1.1.1" const val CLOUDFLARE_IPV4_IP = "1.1.1.1"
const val STATS_DELAY = 1_000L const val STATS_DELAY = 1_000L
} }
} }
@@ -4,18 +4,17 @@ import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException
import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.state.LogHealthState import com.zaneschepke.wireguardautotunnel.domain.state.LogHealthState
import com.zaneschepke.wireguardautotunnel.domain.state.PingState import com.zaneschepke.wireguardautotunnel.domain.state.PingState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import org.amnezia.awg.crypto.Key
interface TunnelProvider { interface TunnelProvider {
/** Starts the specified tunnel configuration. */ /** Starts the specified tunnel configuration. */
suspend fun startTunnel(tunnelConf: TunnelConf) suspend fun startTunnel(tunnelConfig: TunnelConfig)
/** /**
* Stops the specified tunnel. * Stops the specified tunnel.
@@ -24,6 +23,8 @@ interface TunnelProvider {
*/ */
suspend fun stopTunnel(tunnelId: Int) suspend fun stopTunnel(tunnelId: Int)
suspend fun forceStopTunnel(tunnelId: Int)
/** Stops all active tunnels. */ /** Stops all active tunnels. */
suspend fun stopActiveTunnels() suspend fun stopActiveTunnels()
@@ -33,21 +34,21 @@ interface TunnelProvider {
suspend fun runningTunnelNames(): Set<String> suspend fun runningTunnelNames(): Set<String>
fun handleDnsReresolve(tunnelConf: TunnelConf): Boolean fun handleDnsReresolve(tunnelConfig: TunnelConfig): Boolean
fun getStatistics(tunnelId: Int): TunnelStatistics? fun getStatistics(tunnelId: Int): TunnelStatistics?
val activeTunnels: StateFlow<Map<Int, TunnelState>> val activeTunnels: StateFlow<Map<Int, TunnelState>>
val errorEvents: SharedFlow<Pair<String, BackendCoreException>> val errorEvents: SharedFlow<Pair<String?, BackendCoreException>>
val messageEvents: SharedFlow<Pair<String, BackendMessage>> val messageEvents: SharedFlow<Pair<String?, BackendMessage>>
suspend fun updateTunnelStatus( suspend fun updateTunnelStatus(
tunnelId: Int, tunnelId: Int,
status: TunnelStatus? = null, status: TunnelStatus? = null,
stats: TunnelStatistics? = null, stats: TunnelStatistics? = null,
pingStates: Map<Key, PingState>? = null, pingStates: Map<String, PingState>? = null,
logHealthState: LogHealthState? = null, logHealthState: LogHealthState? = null,
) )
} }
@@ -2,12 +2,13 @@ package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.data.model.DnsProtocol import com.zaneschepke.wireguardautotunnel.data.model.DnsProtocol
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException
import com.zaneschepke.wireguardautotunnel.domain.model.AppProxySettings import com.zaneschepke.wireguardautotunnel.domain.model.ProxySettings
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository import com.zaneschepke.wireguardautotunnel.domain.repository.DnsSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.ProxySettingsRepository import com.zaneschepke.wireguardautotunnel.domain.repository.ProxySettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.state.AmneziaStatistics import com.zaneschepke.wireguardautotunnel.domain.state.AmneziaStatistics
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
@@ -19,13 +20,13 @@ import java.io.IOException
import java.util.* import java.util.*
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.flow.update
import org.amnezia.awg.backend.Backend import org.amnezia.awg.backend.Backend
import org.amnezia.awg.backend.BackendException import org.amnezia.awg.backend.BackendException
import org.amnezia.awg.backend.ProxyGoBackend import org.amnezia.awg.backend.ProxyGoBackend
@@ -41,73 +42,81 @@ class UserspaceTunnel
@Inject @Inject
constructor( constructor(
@ApplicationScope applicationScope: CoroutineScope, @ApplicationScope applicationScope: CoroutineScope,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
private val proxySettingsRepository: ProxySettingsRepository, private val proxySettingsRepository: ProxySettingsRepository,
private val settingsRepository: GeneralSettingRepository, private val dnsSettingsRepository: DnsSettingsRepository,
private val backend: Backend, private val backend: Backend,
) : BaseTunnel(applicationScope) { ) : BaseTunnel(applicationScope, ioDispatcher) {
private val runtimeTunnels = ConcurrentHashMap<Int, AwgTunnel>() private val runtimeTunnels = ConcurrentHashMap<Int, AwgTunnel>()
override fun tunnelStateFlow(tunnelConf: TunnelConf): Flow<TunnelStatus> = callbackFlow { override fun tunnelStateFlow(tunnelConfig: TunnelConfig): Flow<TunnelStatus> = callbackFlow {
val stateChannel = Channel<AwgTunnel.State>() val stateChannel = Channel<AwgTunnel.State>()
val runtimeTunnel = RuntimeAwgTunnel(tunnelConf, stateChannel) val runtimeTunnel = RuntimeAwgTunnel(tunnelConfig, stateChannel)
runtimeTunnels[tunnelConf.id] = runtimeTunnel runtimeTunnels[tunnelConfig.id] = runtimeTunnel
val consumerJob = launch { val consumerJob = launch {
stateChannel.consumeAsFlow().collect { awgState -> trySend(awgState.asTunnelState()) } stateChannel.consumeAsFlow().collect { awgState -> trySend(awgState.asTunnelState()) }
} }
try { try {
updateTunnelStatus(tunnelConf.id, TunnelStatus.Starting) withTimeout(STARTUP_TIMEOUT_MS) {
updateTunnelStatus(tunnelConfig.id, TunnelStatus.Starting)
val proxies: List<Proxy> = val proxies: List<Proxy> =
when (backend) { when (backend) {
is ProxyGoBackend -> { is ProxyGoBackend -> {
val proxySettings = proxySettingsRepository.get() val proxySettings = proxySettingsRepository.getProxySettings()
Timber.d("Adding proxy configs") Timber.d("Adding proxy configs")
buildList { buildList {
if (proxySettings.socks5ProxyEnabled) { if (proxySettings.socks5ProxyEnabled) {
add( add(
Socks5Proxy( Socks5Proxy(
proxySettings.socks5ProxyBindAddress proxySettings.socks5ProxyBindAddress
?: AppProxySettings.DEFAULT_SOCKS_BIND_ADDRESS, ?: ProxySettings.DEFAULT_SOCKS_BIND_ADDRESS,
proxySettings.proxyUsername, proxySettings.proxyUsername,
proxySettings.proxyPassword, proxySettings.proxyPassword,
)
) )
) }
} if (proxySettings.httpProxyEnabled) {
if (proxySettings.httpProxyEnabled) { add(
add( HttpProxy(
HttpProxy( proxySettings.httpProxyBindAddress
proxySettings.httpProxyBindAddress ?: ProxySettings.DEFAULT_HTTP_BIND_ADDRESS,
?: AppProxySettings.DEFAULT_HTTP_BIND_ADDRESS, proxySettings.proxyUsername,
proxySettings.proxyUsername, proxySettings.proxyPassword,
proxySettings.proxyPassword, )
) )
) }
} }
} }
else -> emptyList()
} }
else -> emptyList() val setting = dnsSettingsRepository.getDnsSettings()
} val config = tunnelConfig.toAmConfig()
val setting = settingsRepository.get() val updatedConfig =
val config = tunnelConf.toAmConfig() Config.Builder()
val updatedConfig = .apply {
Config.Builder() setInterface(config.`interface`)
.apply { addPeers(config.peers)
setInterface(config.`interface`) addProxies(proxies)
addPeers(config.peers) setDnsSettings(
addProxies(proxies) DnsSettings(
setDnsSettings( setting.dnsProtocol == DnsProtocol.DOH,
DnsSettings( Optional.ofNullable(setting.dnsEndpoint),
setting.dnsProtocol == DnsProtocol.DOH, )
Optional.ofNullable(setting.dnsEndpoint),
) )
) }
} .build()
.build() backend.setState(runtimeTunnel, AwgTunnel.State.UP, updatedConfig)
backend.setState(runtimeTunnel, AwgTunnel.State.UP, updatedConfig) }
} catch (e: TimeoutCancellationException) {
Timber.e("Startup timed out for ${tunnelConfig.name} (likely DNS hang)")
errors.emit(tunnelConfig.name to BackendCoreException.DNS)
forceStopTunnel(tunnelConfig.id)
close()
} catch (e: BackendException) { } catch (e: BackendException) {
close(e.toBackendCoreException()) close(e.toBackendCoreException())
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
@@ -121,11 +130,11 @@ constructor(
try { try {
backend.setState(runtimeTunnel, AwgTunnel.State.DOWN, null) backend.setState(runtimeTunnel, AwgTunnel.State.DOWN, null)
} catch (e: BackendException) { } catch (e: BackendException) {
errors.tryEmit(tunnelConf.tunName to e.toBackendCoreException()) errors.tryEmit(tunnelConfig.name to e.toBackendCoreException())
} finally { } finally {
consumerJob.cancel() consumerJob.cancel()
stateChannel.close() stateChannel.close()
runtimeTunnels.remove(tunnelConf.id) runtimeTunnels.remove(tunnelConfig.id)
trySend(TunnelStatus.Down) trySend(TunnelStatus.Down)
close() close()
} }
@@ -148,10 +157,9 @@ constructor(
return backend.backendMode.asBackendMode() return backend.backendMode.asBackendMode()
} }
override fun handleDnsReresolve(tunnelConf: TunnelConf): Boolean { override fun handleDnsReresolve(tunnelConfig: TunnelConfig): Boolean {
val tunnel = val tunnel = runtimeTunnels[tunnelConfig.id] ?: throw BackendCoreException.ServiceNotRunning
runtimeTunnels.get(tunnelConf.id) ?: throw BackendCoreException.ServiceNotRunning return backend.resolveDDNS(tunnelConfig.toAmConfig(), tunnel.isIpv4ResolutionPreferred)
return backend.resolveDDNS(tunnelConf.toAmConfig(), tunnel.isIpv4ResolutionPreferred)
} }
override suspend fun runningTunnelNames(): Set<String> { override suspend fun runningTunnelNames(): Set<String> {
@@ -167,4 +175,19 @@ constructor(
null null
} }
} }
override suspend fun forceStopTunnel(tunnelId: Int) {
val runtimeTunnel = runtimeTunnels[tunnelId] ?: return
try {
backend.setState(runtimeTunnel, AwgTunnel.State.DOWN, null)
} catch (e: BackendException) {
Timber.e(e, "Force stop failed for $tunnelId")
} finally {
tunJobs[tunnelId]?.cancel()
runtimeTunnels.remove(tunnelId)
tunJobs.remove(tunnelId)
activeTuns.update { it - tunnelId }
updateTunnelStatus(tunnelId, TunnelStatus.Down)
}
}
} }
@@ -5,7 +5,7 @@ import androidx.hilt.work.HiltWorker
import androidx.work.* import androidx.work.*
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@@ -20,7 +20,7 @@ constructor(
@Assisted private val context: Context, @Assisted private val context: Context,
@Assisted private val params: WorkerParameters, @Assisted private val params: WorkerParameters,
private val serviceManager: ServiceManager, private val serviceManager: ServiceManager,
private val settingsRepository: GeneralSettingRepository, private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher, @IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : CoroutineWorker(context, params) { ) : CoroutineWorker(context, params) {
@@ -50,11 +50,11 @@ constructor(
override suspend fun doWork(): Result = override suspend fun doWork(): Result =
withContext(ioDispatcher) { withContext(ioDispatcher) {
Timber.i("Service worker started") Timber.i("Service worker started")
with(settingsRepository.get()) { with(autoTunnelSettingsRepository.getAutoTunnelSettings()) {
Timber.i("Checking to see if auto-tunnel has been killed by system") Timber.i("Checking to see if auto-tunnel has been killed by system")
if (isAutoTunnelEnabled && serviceManager.autoTunnelService.value == null) { if (isAutoTunnelEnabled && serviceManager.autoTunnelService.value == null) {
Timber.i("Service has been killed by system, restoring.") Timber.i("Service has been killed by system, restoring.")
settingsRepository.updateAutoTunnelEnabled(true) serviceManager.startAutoTunnelService()
} }
} }
Result.success() Result.success()
@@ -3,16 +3,20 @@ package com.zaneschepke.wireguardautotunnel.data
import androidx.room.* import androidx.room.*
import androidx.room.migration.AutoMigrationSpec import androidx.room.migration.AutoMigrationSpec
import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteDatabase
import com.zaneschepke.wireguardautotunnel.data.dao.ProxySettingsDao import com.zaneschepke.wireguardautotunnel.data.dao.*
import com.zaneschepke.wireguardautotunnel.data.dao.SettingsDao import com.zaneschepke.wireguardautotunnel.data.entity.*
import com.zaneschepke.wireguardautotunnel.data.dao.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.data.entity.ProxySettings
import com.zaneschepke.wireguardautotunnel.data.entity.Settings
import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
@Database( @Database(
entities = [Settings::class, TunnelConfig::class, ProxySettings::class], entities =
version = 22, [
TunnelConfig::class,
ProxySettings::class,
GeneralSettings::class,
AutoTunnelSettings::class,
MonitoringSettings::class,
DnsSettings::class,
],
version = 25,
autoMigrations = autoMigrations =
[ [
AutoMigration(from = 1, to = 2), AutoMigration(from = 1, to = 2),
@@ -36,16 +40,24 @@ import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
AutoMigration(from = 19, to = 20, spec = ProxyMigration::class), AutoMigration(from = 19, to = 20, spec = ProxyMigration::class),
AutoMigration(from = 20, to = 21, spec = FixProxySettingsMigration::class), AutoMigration(from = 20, to = 21, spec = FixProxySettingsMigration::class),
AutoMigration(from = 21, to = 22), AutoMigration(from = 21, to = 22),
AutoMigration(from = 22, to = 23),
AutoMigration(from = 24, to = 25),
], ],
exportSchema = true, exportSchema = true,
) )
@TypeConverters(DatabaseConverters::class) @TypeConverters(DatabaseConverters::class)
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
abstract fun settingDao(): SettingsDao
abstract fun tunnelConfigDoa(): TunnelConfigDao abstract fun tunnelConfigDoa(): TunnelConfigDao
abstract fun proxySettingsDoa(): ProxySettingsDao abstract fun proxySettingsDoa(): ProxySettingsDao
abstract fun generalSettingsDao(): GeneralSettingsDao
abstract fun autoTunnelSettingsDao(): AutoTunnelSettingsDao
abstract fun monitoringSettingsDao(): MonitoringSettingsDao
abstract fun dnsSettingsDao(): DnsSettingsDao
} }
@DeleteColumn(tableName = "Settings", columnName = "default_tunnel") @DeleteColumn(tableName = "Settings", columnName = "default_tunnel")
@@ -4,7 +4,6 @@ import android.content.Context
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore import androidx.datastore.preferences.preferencesDataStore
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import java.io.IOException import java.io.IOException
@@ -20,27 +19,19 @@ class DataStoreManager(
private val context: Context, private val context: Context,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher, @IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) { ) {
private val preferencesKey = "preferences"
val Context.dataStore by preferencesDataStore(name = preferencesKey)
val dataStore = context.dataStore
companion object { companion object {
val locationDisclosureShown = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN") val locationDisclosureShown = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN")
val batteryDisableShown = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN") val batteryDisableShown = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN")
val pinLockEnabled = booleanPreferencesKey("PIN_LOCK_ENABLED")
val expandedTunnelIds = stringPreferencesKey("EXPANDED_TUNNEL_IDS")
val isLocalLogsEnabled = booleanPreferencesKey("LOCAL_LOGS_ENABLED")
val locale = stringPreferencesKey("LOCALE")
val theme = stringPreferencesKey("THEME")
val isRemoteControlEnabled = booleanPreferencesKey("IS_REMOTE_CONTROL_ENABLED")
val remoteKey = stringPreferencesKey("REMOTE_KEY")
val showDetailedPingStats = booleanPreferencesKey("SHOW_DETAILED_PING_STATS")
} }
// preferences
private val preferencesKey = "preferences"
private val Context.dataStore by preferencesDataStore(name = preferencesKey)
suspend fun init() { suspend fun init() {
withContext(ioDispatcher) { withContext(ioDispatcher) {
try { try {
context.dataStore.data.first() dataStore.data.first()
} catch (e: IOException) { } catch (e: IOException) {
Timber.e(e) Timber.e(e)
} }
@@ -50,7 +41,7 @@ class DataStoreManager(
suspend fun <T> saveToDataStore(key: Preferences.Key<T>, value: T) { suspend fun <T> saveToDataStore(key: Preferences.Key<T>, value: T) {
withContext(ioDispatcher) { withContext(ioDispatcher) {
try { try {
context.dataStore.edit { it[key] = value } dataStore.edit { it[key] = value }
} catch (e: IOException) { } catch (e: IOException) {
Timber.e(e) Timber.e(e)
} catch (e: Exception) { } catch (e: Exception) {
@@ -62,7 +53,7 @@ class DataStoreManager(
suspend fun <T> removeFromDataStore(key: Preferences.Key<T>) { suspend fun <T> removeFromDataStore(key: Preferences.Key<T>) {
withContext(ioDispatcher) { withContext(ioDispatcher) {
try { try {
context.dataStore.edit { it.remove(key) } dataStore.edit { it.remove(key) }
} catch (e: IOException) { } catch (e: IOException) {
Timber.e(e) Timber.e(e)
} catch (e: Exception) { } catch (e: Exception) {
@@ -76,7 +67,7 @@ class DataStoreManager(
suspend fun <T> getFromStore(key: Preferences.Key<T>): T? { suspend fun <T> getFromStore(key: Preferences.Key<T>): T? {
return withContext(ioDispatcher) { return withContext(ioDispatcher) {
try { try {
context.dataStore.data.map { it[key] }.first() dataStore.data.map { it[key] }.first()
} catch (e: IOException) { } catch (e: IOException) {
Timber.e(e) Timber.e(e)
null null
@@ -84,5 +75,5 @@ class DataStoreManager(
} }
} }
val preferencesFlow: Flow<Preferences?> = context.dataStore.data.flowOn(ioDispatcher) val preferencesFlow: Flow<Preferences?> = dataStore.data.flowOn(ioDispatcher)
} }
@@ -4,13 +4,17 @@ import androidx.room.RoomDatabase
import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteDatabase
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider import javax.inject.Provider
import timber.log.Timber
class DatabaseCallback @Inject constructor(private val databaseProvider: Provider<AppDatabase>) : class DatabaseCallback @Inject constructor(private val databaseProvider: Provider<AppDatabase>) :
RoomDatabase.Callback() { RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) { override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db) super.onCreate(db)
Timber.d("Database created, inserting default rows")
db.execSQL("INSERT INTO proxy_settings DEFAULT VALUES") db.execSQL("INSERT INTO proxy_settings DEFAULT VALUES")
db.execSQL("INSERT INTO Settings DEFAULT VALUES") db.execSQL("INSERT INTO general_settings DEFAULT VALUES")
db.execSQL("INSERT INTO auto_tunnel_settings DEFAULT VALUES")
db.execSQL("INSERT INTO monitoring_settings DEFAULT VALUES")
db.execSQL("INSERT INTO dns_settings DEFAULT VALUES")
} }
} }
@@ -24,6 +24,24 @@ class DatabaseConverters {
} }
} }
@TypeConverter
fun mapToString(map: Map<String, String>): String {
return Json.encodeToString(map)
}
@TypeConverter
fun stringToMap(json: String): Map<String, String> {
return if (json.isEmpty() || json == "{}") {
emptyMap()
} else {
try {
Json.decodeFromString<Map<String, String>>(json)
} catch (_: Exception) {
emptyMap()
}
}
}
@TypeConverter @TypeConverter
fun setToString(value: Set<String>): String { fun setToString(value: Set<String>): String {
return listToString(value.toList()) return listToString(value.toList())
@@ -0,0 +1,21 @@
package com.zaneschepke.wireguardautotunnel.data.dao
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Upsert
import com.zaneschepke.wireguardautotunnel.data.entity.AutoTunnelSettings
import kotlinx.coroutines.flow.Flow
@Dao
interface AutoTunnelSettingsDao {
@Query("SELECT * FROM auto_tunnel_settings LIMIT 1")
suspend fun getAutoTunnelSettings(): AutoTunnelSettings?
@Upsert suspend fun upsert(autoTunnelSettings: AutoTunnelSettings)
@Query("SELECT * FROM auto_tunnel_settings LIMIT 1")
fun getAutoTunnelSettingsFlow(): Flow<AutoTunnelSettings?>
@Query("UPDATE auto_tunnel_settings SET is_tunnel_enabled = :enabled")
suspend fun updateAutoTunnelEnabled(enabled: Boolean)
}
@@ -0,0 +1,16 @@
package com.zaneschepke.wireguardautotunnel.data.dao
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Upsert
import com.zaneschepke.wireguardautotunnel.data.entity.DnsSettings
import kotlinx.coroutines.flow.Flow
@Dao
interface DnsSettingsDao {
@Query("SELECT * FROM dns_settings LIMIT 1") suspend fun getDnsSettings(): DnsSettings?
@Upsert suspend fun upsert(dnsSettings: DnsSettings)
@Query("SELECT * FROM dns_settings LIMIT 1") fun getDnsSettingsFlow(): Flow<DnsSettings?>
}
@@ -0,0 +1,18 @@
package com.zaneschepke.wireguardautotunnel.data.dao
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Upsert
import com.zaneschepke.wireguardautotunnel.data.entity.GeneralSettings
import kotlinx.coroutines.flow.Flow
@Dao
interface GeneralSettingsDao {
@Query("SELECT * FROM general_settings LIMIT 1")
suspend fun getGeneralSettings(): GeneralSettings?
@Upsert suspend fun upsert(generalSettings: GeneralSettings)
@Query("SELECT * FROM general_settings LIMIT 1")
fun getGeneralSettingsFlow(): Flow<GeneralSettings?>
}
@@ -0,0 +1,18 @@
package com.zaneschepke.wireguardautotunnel.data.dao
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Upsert
import com.zaneschepke.wireguardautotunnel.data.entity.MonitoringSettings
import kotlinx.coroutines.flow.Flow
@Dao
interface MonitoringSettingsDao {
@Query("SELECT * FROM monitoring_settings LIMIT 1")
suspend fun getMonitoringSettings(): MonitoringSettings?
@Upsert suspend fun upsert(monitoringSettings: MonitoringSettings)
@Query("SELECT * FROM monitoring_settings LIMIT 1")
fun getMonitoringSettingsFlow(): Flow<MonitoringSettings?>
}
@@ -1,25 +1,16 @@
package com.zaneschepke.wireguardautotunnel.data.dao package com.zaneschepke.wireguardautotunnel.data.dao
import androidx.room.* import androidx.room.Dao
import androidx.room.Query
import androidx.room.Upsert
import com.zaneschepke.wireguardautotunnel.data.entity.ProxySettings import com.zaneschepke.wireguardautotunnel.data.entity.ProxySettings
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@Dao @Dao
interface ProxySettingsDao { interface ProxySettingsDao {
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun save(t: ProxySettings) @Upsert suspend fun upsert(proxySettings: ProxySettings)
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun saveAll(t: List<ProxySettings>) @Query("SELECT * FROM proxy_settings LIMIT 1") suspend fun getProxySettings(): ProxySettings?
@Query("SELECT * FROM proxy_settings WHERE id=:id") @Query("SELECT * FROM proxy_settings LIMIT 1") fun getProxySettingsFlow(): Flow<ProxySettings?>
suspend fun getById(id: Long): ProxySettings?
@Query("SELECT * FROM proxy_settings") suspend fun getAll(): List<ProxySettings>
@Query("SELECT * FROM proxy_settings LIMIT 1") fun getSettingsFlow(): Flow<ProxySettings>
@Query("SELECT * FROM proxy_settings") fun getAllFlow(): Flow<List<ProxySettings>>
@Delete suspend fun delete(t: ProxySettings)
@Query("SELECT COUNT('id') FROM proxy_settings") suspend fun count(): Long
} }
@@ -1,27 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.dao
import androidx.room.*
import com.zaneschepke.wireguardautotunnel.data.entity.Settings
import kotlinx.coroutines.flow.Flow
@Dao
interface SettingsDao {
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun save(t: Settings)
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun saveAll(t: List<Settings>)
@Query("SELECT * FROM settings WHERE id=:id") suspend fun getById(id: Long): Settings?
@Query("SELECT * FROM settings") suspend fun getAll(): List<Settings>
@Query("SELECT * FROM settings LIMIT 1") fun getSettingsFlow(): Flow<Settings>
@Query("SELECT * FROM settings") fun getAllFlow(): Flow<List<Settings>>
@Delete suspend fun delete(t: Settings)
@Query("SELECT COUNT('id') FROM settings") suspend fun count(): Long
@Query("UPDATE settings SET is_tunnel_enabled = :enabled")
suspend fun updateAutoTunnelEnabled(enabled: Boolean)
}
@@ -2,54 +2,55 @@ package com.zaneschepke.wireguardautotunnel.data.dao
import androidx.room.* import androidx.room.*
import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@Dao @Dao
interface TunnelConfigDao { interface TunnelConfigDao {
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun save(t: TunnelConfig)
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun saveAll(t: TunnelConfigs) @Upsert suspend fun upsert(t: TunnelConfig)
@Query("SELECT * FROM TunnelConfig WHERE id=:id") suspend fun getById(id: Long): TunnelConfig? @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun saveAll(t: List<TunnelConfig>)
@Query("UPDATE TunnelConfig SET is_Active = 0 WHERE is_Active = 1") @Query("SELECT * FROM tunnel_config WHERE id=:id") suspend fun getById(id: Long): TunnelConfig?
@Query("UPDATE tunnel_config SET is_Active = 0 WHERE is_Active = 1")
suspend fun resetActiveTunnels() suspend fun resetActiveTunnels()
@Query("SELECT * FROM TunnelConfig WHERE name=:name") @Query("SELECT * FROM tunnel_config WHERE name=:name")
suspend fun getByName(name: String): TunnelConfig? suspend fun getByName(name: String): TunnelConfig?
@Query("SELECT * FROM TunnelConfig WHERE is_Active=1") suspend fun getActive(): TunnelConfigs @Query("SELECT * FROM tunnel_config WHERE is_Active=1")
suspend fun getActive(): List<TunnelConfig>
@Query("SELECT * FROM TunnelConfig") suspend fun getAll(): TunnelConfigs @Query("SELECT * FROM tunnel_config") suspend fun getAll(): List<TunnelConfig>
@Delete suspend fun delete(t: TunnelConfig) @Delete suspend fun delete(t: TunnelConfig)
@Delete suspend fun delete(t: TunnelConfigs) @Delete suspend fun delete(t: List<TunnelConfig>)
@Query("SELECT COUNT('id') FROM TunnelConfig") suspend fun count(): Long @Query("SELECT COUNT('id') FROM tunnel_config") suspend fun count(): Long
@Query("SELECT * FROM TunnelConfig WHERE tunnel_networks LIKE '%' || :name || '%'") @Query("SELECT * FROM tunnel_config WHERE tunnel_networks LIKE '%' || :name || '%'")
suspend fun findByTunnelNetworkName(name: String): TunnelConfigs suspend fun findByTunnelNetworkName(name: String): List<TunnelConfig>
@Query("UPDATE TunnelConfig SET is_primary_tunnel = 0 WHERE is_primary_tunnel =1") @Query("UPDATE tunnel_config SET is_primary_tunnel = 0 WHERE is_primary_tunnel =1")
suspend fun resetPrimaryTunnel() suspend fun resetPrimaryTunnel()
@Query("UPDATE TunnelConfig SET is_mobile_data_tunnel = 0 WHERE is_mobile_data_tunnel =1") @Query("UPDATE tunnel_config SET is_mobile_data_tunnel = 0 WHERE is_mobile_data_tunnel =1")
suspend fun resetMobileDataTunnel() suspend fun resetMobileDataTunnel()
@Query("UPDATE TunnelConfig SET is_ethernet_tunnel = 0 WHERE is_ethernet_tunnel =1") @Query("UPDATE tunnel_config SET is_ethernet_tunnel = 0 WHERE is_ethernet_tunnel =1")
suspend fun resetEthernetTunnel() suspend fun resetEthernetTunnel()
@Query("SELECT * FROM TUNNELCONFIG WHERE is_primary_tunnel=1") @Query("SELECT * FROM tunnel_config WHERE is_primary_tunnel=1")
suspend fun findByPrimary(): TunnelConfigs suspend fun findByPrimary(): List<TunnelConfig>
@Query("SELECT * FROM TUNNELCONFIG WHERE is_mobile_data_tunnel=1") @Query("SELECT * FROM tunnel_config WHERE is_mobile_data_tunnel=1")
suspend fun findByMobileDataTunnel(): TunnelConfigs suspend fun findByMobileDataTunnel(): List<TunnelConfig>
@Query( @Query(
""" """
SELECT * FROM TunnelConfig SELECT * FROM tunnel_config
ORDER BY ORDER BY
CASE WHEN is_primary_tunnel = 1 THEN 0 ELSE 1 END, CASE WHEN is_primary_tunnel = 1 THEN 0 ELSE 1 END,
position ASC position ASC
@@ -59,7 +60,7 @@ interface TunnelConfigDao {
@Query( @Query(
""" """
SELECT * FROM TunnelConfig SELECT * FROM tunnel_config
ORDER BY ORDER BY
CASE WHEN is_Active = 1 THEN 0 CASE WHEN is_Active = 1 THEN 0
WHEN is_primary_tunnel = 1 THEN 1 WHEN is_primary_tunnel = 1 THEN 1
@@ -69,6 +70,14 @@ interface TunnelConfigDao {
) )
suspend fun getStartTunnel(): TunnelConfig? suspend fun getStartTunnel(): TunnelConfig?
@Query("SELECT * FROM tunnelconfig ORDER BY position") @Query("SELECT * FROM tunnel_config ORDER BY position")
fun getAllFlow(): Flow<List<TunnelConfig>> fun getAllFlow(): Flow<List<TunnelConfig>>
@Query("SELECT * FROM tunnel_config WHERE name != :globalName ORDER BY position")
fun getAllTunnelsExceptGlobal(
globalName: String = TunnelConfig.GLOBAL_CONFIG_NAME
): Flow<List<TunnelConfig>>
@Query("SELECT * FROM tunnel_config WHERE name = :globalName LIMIT 1")
fun getGlobalTunnel(globalName: String = TunnelConfig.GLOBAL_CONFIG_NAME): Flow<TunnelConfig?>
} }
@@ -0,0 +1,6 @@
package com.zaneschepke.wireguardautotunnel.data.entity
data class AppState(
val isLocationDisclosureShown: Boolean = false,
val isBatteryOptimizationDisableShown: Boolean = false,
)
@@ -0,0 +1,32 @@
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
@Entity(tableName = "auto_tunnel_settings")
data class AutoTunnelSettings(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
@ColumnInfo(name = "is_tunnel_enabled", defaultValue = "0")
val isAutoTunnelEnabled: Boolean = false,
@ColumnInfo(name = "is_tunnel_on_mobile_data_enabled", defaultValue = "0")
val isTunnelOnMobileDataEnabled: Boolean = false,
@ColumnInfo(name = "trusted_network_ssids", defaultValue = "")
val trustedNetworkSSIDs: Set<String> = emptySet(),
@ColumnInfo(name = "is_tunnel_on_ethernet_enabled", defaultValue = "0")
val isTunnelOnEthernetEnabled: Boolean = false,
@ColumnInfo(name = "is_tunnel_on_wifi_enabled", defaultValue = "0")
val isTunnelOnWifiEnabled: Boolean = false,
@ColumnInfo(name = "is_wildcards_enabled", defaultValue = "0")
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")
val wifiDetectionMethod: WifiDetectionMethod = WifiDetectionMethod.fromValue(0),
@ColumnInfo(name = "start_on_boot", defaultValue = "0") val startOnBoot: Boolean = false,
)
@@ -0,0 +1,14 @@
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
@Entity(tableName = "dns_settings")
data class DnsSettings(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
@ColumnInfo(name = "dns_protocol", defaultValue = "0")
val dnsProtocol: DnsProtocol = DnsProtocol.fromValue(0),
@ColumnInfo(name = "dns_endpoint") val dnsEndpoint: String? = null,
)
@@ -0,0 +1,33 @@
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
@Entity(tableName = "general_settings")
data class GeneralSettings(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
@ColumnInfo(name = "is_shortcuts_enabled", defaultValue = "0")
val isShortcutsEnabled: Boolean = false,
@ColumnInfo(name = "is_restore_on_boot_enabled", defaultValue = "0")
val isRestoreOnBootEnabled: Boolean = false,
@ColumnInfo(name = "is_multi_tunnel_enabled", defaultValue = "0")
val isMultiTunnelEnabled: Boolean = false,
@ColumnInfo(name = "is_tunnel_globals_enabled", defaultValue = "0")
val isTunnelGlobalsEnabled: Boolean = false,
@ColumnInfo(name = "app_mode", defaultValue = "0") val appMode: AppMode = AppMode.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,
@ColumnInfo(name = "is_remote_control_enabled", defaultValue = "0")
val isRemoteControlEnabled: Boolean = false,
@ColumnInfo(name = "is_pin_lock_enabled", defaultValue = "0")
val isPinLockEnabled: Boolean = false,
@ColumnInfo(name = "is_always_on_vpn_enabled", defaultValue = "0")
val isAlwaysOnVpnEnabled: Boolean = false,
@ColumnInfo(name = "is_lan_on_kill_switch_enabled", defaultValue = "0")
val isLanOnKillSwitchEnabled: Boolean = false,
@ColumnInfo(name = "custom_split_packages", defaultValue = "{}")
val customSplitPackages: Map<String, String> = emptyMap(),
)
@@ -1,26 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.entity
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
data class GeneralState(
val isLocationDisclosureShown: Boolean = LOCATION_DISCLOSURE_SHOWN_DEFAULT,
val isBatteryOptimizationDisableShown: Boolean = BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT,
val isPinLockEnabled: Boolean = PIN_LOCK_ENABLED_DEFAULT,
val expandedTunnelIds: List<Int> = emptyList(),
val isLocalLogsEnabled: Boolean = IS_LOGS_ENABLED_DEFAULT,
val isRemoteControlEnabled: Boolean = IS_REMOTE_CONTROL_ENABLED,
val showDetailedPingStats: Boolean = SHOW_DETAILED_PING_STATS_DEFAULT,
val remoteKey: String? = null,
val locale: String? = null,
val theme: Theme = Theme.AUTOMATIC,
) {
companion object {
const val LOCATION_DISCLOSURE_SHOWN_DEFAULT = false
const val BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT = false
const val PIN_LOCK_ENABLED_DEFAULT = false
const val IS_LOGS_ENABLED_DEFAULT = false
const val IS_REMOTE_CONTROL_ENABLED = false
const val SHOW_DETAILED_PING_STATS_DEFAULT = false
}
}
@@ -0,0 +1,21 @@
package com.zaneschepke.wireguardautotunnel.data.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
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,
)
@@ -1,56 +0,0 @@
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.data.model.DnsProtocol
import com.zaneschepke.wireguardautotunnel.data.model.WifiDetectionMethod
@Entity
data class Settings(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
@ColumnInfo(name = "is_tunnel_enabled", defaultValue = "0")
val isAutoTunnelEnabled: Boolean = false,
@ColumnInfo(name = "is_tunnel_on_mobile_data_enabled", defaultValue = "0")
val isTunnelOnMobileDataEnabled: Boolean = false,
@ColumnInfo(name = "trusted_network_ssids", defaultValue = "")
val trustedNetworkSSIDs: Set<String> = emptySet(),
@ColumnInfo(name = "is_always_on_vpn_enabled", defaultValue = "0")
val isAlwaysOnVpnEnabled: Boolean = false,
@ColumnInfo(name = "is_tunnel_on_ethernet_enabled", defaultValue = "0")
val isTunnelOnEthernetEnabled: Boolean = false,
@ColumnInfo(name = "is_shortcuts_enabled", defaultValue = "0")
val isShortcutsEnabled: Boolean = false,
@ColumnInfo(name = "is_tunnel_on_wifi_enabled", defaultValue = "0")
val isTunnelOnWifiEnabled: Boolean = false,
@ColumnInfo(name = "is_restore_on_boot_enabled", defaultValue = "0")
val isRestoreOnBootEnabled: Boolean = false,
@ColumnInfo(name = "is_multi_tunnel_enabled", defaultValue = "0")
val isMultiTunnelEnabled: Boolean = false,
@ColumnInfo(name = "is_ping_enabled", defaultValue = "0") val isPingEnabled: Boolean = false,
@ColumnInfo(name = "is_wildcards_enabled", defaultValue = "0")
val isWildcardsEnabled: Boolean = false,
@ColumnInfo(name = "is_stop_on_no_internet_enabled", defaultValue = "0")
val isStopOnNoInternetEnabled: Boolean = false,
@ColumnInfo(name = "is_lan_on_kill_switch_enabled", defaultValue = "0")
val isLanOnKillSwitchEnabled: Boolean = false,
@ColumnInfo(name = "debounce_delay_seconds", defaultValue = "3")
val debounceDelaySeconds: Int = 3,
@ColumnInfo(name = "is_disable_kill_switch_on_trusted_enabled", defaultValue = "0")
val isDisableKillSwitchOnTrustedEnabled: Boolean = false,
@ColumnInfo(name = "is_tunnel_on_unsecure_enabled", defaultValue = "0")
val isTunnelOnUnsecureEnabled: Boolean = false,
@ColumnInfo(name = "wifi_detection_method", defaultValue = "0")
val wifiDetectionMethod: WifiDetectionMethod = WifiDetectionMethod.fromValue(0),
@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 = "app_mode", defaultValue = "0") val appMode: AppMode = AppMode.fromValue(0),
@ColumnInfo(name = "dns_protocol", defaultValue = "0")
val dnsProtocol: DnsProtocol = DnsProtocol.fromValue(0),
@ColumnInfo(name = "dns_endpoint") val dnsEndpoint: String? = null,
)
@@ -5,7 +5,7 @@ import androidx.room.Entity
import androidx.room.Index import androidx.room.Index
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
@Entity(indices = [Index(value = ["name"], unique = true)]) @Entity(tableName = "tunnel_config", indices = [Index(value = ["name"], unique = true)])
data class TunnelConfig( data class TunnelConfig(
@PrimaryKey(autoGenerate = true) val id: Int = 0, @PrimaryKey(autoGenerate = true) val id: Int = 0,
@ColumnInfo(name = "name") val name: String, @ColumnInfo(name = "name") val name: String,
@@ -16,7 +16,7 @@ data class TunnelConfig(
val isMobileDataTunnel: Boolean = false, val isMobileDataTunnel: Boolean = false,
@ColumnInfo(name = "is_primary_tunnel", defaultValue = "false") @ColumnInfo(name = "is_primary_tunnel", defaultValue = "false")
val isPrimaryTunnel: Boolean = false, val isPrimaryTunnel: Boolean = false,
@ColumnInfo(name = "am_quick", defaultValue = "") val amQuick: String = AM_QUICK_DEFAULT, @ColumnInfo(name = "am_quick", defaultValue = "") val amQuick: String = "",
@ColumnInfo(name = "is_Active", defaultValue = "false") val isActive: Boolean = false, @ColumnInfo(name = "is_Active", defaultValue = "false") val isActive: Boolean = false,
@ColumnInfo(name = "restart_on_ping_failure", defaultValue = "false") @ColumnInfo(name = "restart_on_ping_failure", defaultValue = "false")
val restartOnPingFailure: Boolean = false, val restartOnPingFailure: Boolean = false,
@@ -27,10 +27,10 @@ data class TunnelConfig(
val isIpv4Preferred: Boolean = true, val isIpv4Preferred: Boolean = true,
@ColumnInfo(name = "position", defaultValue = "0") val position: Int = 0, @ColumnInfo(name = "position", defaultValue = "0") val position: Int = 0,
@ColumnInfo(name = "auto_tunnel_apps", defaultValue = "[]") @ColumnInfo(name = "auto_tunnel_apps", defaultValue = "[]")
val autoTunnelApps: Set<String> = setOf(), val autoTunnelApps: Set<String> = emptySet(),
) { ) {
companion object { companion object {
const val AM_QUICK_DEFAULT = "" const val GLOBAL_CONFIG_NAME = "4675ab06-903a-438b-8485-6ea4187a9512"
} }
} }
@@ -0,0 +1,36 @@
package com.zaneschepke.wireguardautotunnel.data.mapper
import com.zaneschepke.wireguardautotunnel.data.entity.AutoTunnelSettings as Entity
import com.zaneschepke.wireguardautotunnel.domain.model.AutoTunnelSettings as Domain
fun Entity.toDomain(): Domain =
Domain(
id = id,
isAutoTunnelEnabled = isAutoTunnelEnabled,
isTunnelOnMobileDataEnabled = isTunnelOnMobileDataEnabled,
trustedNetworkSSIDs = trustedNetworkSSIDs,
isTunnelOnEthernetEnabled = isTunnelOnEthernetEnabled,
isTunnelOnWifiEnabled = isTunnelOnWifiEnabled,
isWildcardsEnabled = isWildcardsEnabled,
isStopOnNoInternetEnabled = isStopOnNoInternetEnabled,
debounceDelaySeconds = debounceDelaySeconds,
isTunnelOnUnsecureEnabled = isTunnelOnUnsecureEnabled,
wifiDetectionMethod = wifiDetectionMethod,
startOnBoot = startOnBoot,
)
fun Domain.toEntity(): Entity =
Entity(
id = id,
isAutoTunnelEnabled = isAutoTunnelEnabled,
isTunnelOnMobileDataEnabled = isTunnelOnMobileDataEnabled,
trustedNetworkSSIDs = trustedNetworkSSIDs,
isTunnelOnEthernetEnabled = isTunnelOnEthernetEnabled,
isTunnelOnWifiEnabled = isTunnelOnWifiEnabled,
isWildcardsEnabled = isWildcardsEnabled,
isStopOnNoInternetEnabled = isStopOnNoInternetEnabled,
debounceDelaySeconds = debounceDelaySeconds,
isTunnelOnUnsecureEnabled = isTunnelOnUnsecureEnabled,
wifiDetectionMethod = wifiDetectionMethod,
startOnBoot = startOnBoot,
)
@@ -0,0 +1,10 @@
package com.zaneschepke.wireguardautotunnel.data.mapper
import com.zaneschepke.wireguardautotunnel.data.entity.DnsSettings as Entity
import com.zaneschepke.wireguardautotunnel.domain.model.DnsSettings as Domain
fun Entity.toDomain(): Domain =
Domain(id = id, dnsProtocol = dnsProtocol, dnsEndpoint = dnsEndpoint)
fun Domain.toEntity(): Entity =
Entity(id = id, dnsProtocol = dnsProtocol, dnsEndpoint = dnsEndpoint)
@@ -1,39 +1,10 @@
package com.zaneschepke.wireguardautotunnel.data.mapper package com.zaneschepke.wireguardautotunnel.data.mapper
import com.zaneschepke.wireguardautotunnel.data.entity.GeneralState import com.zaneschepke.wireguardautotunnel.data.entity.AppState as Entity
import com.zaneschepke.wireguardautotunnel.domain.model.AppState import com.zaneschepke.wireguardautotunnel.domain.model.AppState as Domain
object GeneralStateMapper { fun Entity.toDomain(): Domain =
fun toAppState(generalState: GeneralState): AppState = Domain(
with(generalState) { isLocationDisclosureShown = isLocationDisclosureShown,
AppState( isBatteryOptimizationDisableShown = isBatteryOptimizationDisableShown,
isLocationDisclosureShown, )
isBatteryOptimizationDisableShown,
isPinLockEnabled,
expandedTunnelIds,
isLocalLogsEnabled,
isRemoteControlEnabled,
showDetailedPingStats,
remoteKey,
locale,
theme,
)
}
fun toGeneralState(appState: AppState): GeneralState {
return with(appState) {
GeneralState(
isLocationDisclosureShown,
isBatteryOptimizationDisableShown,
isPinLockEnabled,
expandedTunnelIds,
isLocalLogsEnabled,
isRemoteControlEnabled,
showDetailedPingStats,
remoteKey,
locale,
theme,
)
}
}
}
@@ -0,0 +1,28 @@
package com.zaneschepke.wireguardautotunnel.data.mapper
import com.zaneschepke.wireguardautotunnel.data.entity.MonitoringSettings as Entity
import com.zaneschepke.wireguardautotunnel.domain.model.MonitoringSettings as Domain
fun Entity.toDomain(): Domain =
Domain(
id = id,
isPingEnabled = isPingEnabled,
isPingMonitoringEnabled = isPingMonitoringEnabled,
tunnelPingIntervalSeconds = tunnelPingIntervalSeconds,
tunnelPingAttempts = tunnelPingAttempts,
tunnelPingTimeoutSeconds = tunnelPingTimeoutSeconds,
showDetailedPingStats = showDetailedPingStats,
isLocalLogsEnabled = isLocalLogsEnabled,
)
fun Domain.toEntity(): Entity =
Entity(
id = id,
isPingEnabled = isPingEnabled,
isPingMonitoringEnabled = isPingMonitoringEnabled,
tunnelPingIntervalSeconds = tunnelPingIntervalSeconds,
tunnelPingAttempts = tunnelPingAttempts,
tunnelPingTimeoutSeconds = tunnelPingTimeoutSeconds,
showDetailedPingStats = showDetailedPingStats,
isLocalLogsEnabled = isLocalLogsEnabled,
)
@@ -1,32 +1,26 @@
package com.zaneschepke.wireguardautotunnel.data.mapper package com.zaneschepke.wireguardautotunnel.data.mapper
import com.zaneschepke.wireguardautotunnel.data.entity.ProxySettings import com.zaneschepke.wireguardautotunnel.data.entity.ProxySettings as Entity
import com.zaneschepke.wireguardautotunnel.domain.model.AppProxySettings import com.zaneschepke.wireguardautotunnel.domain.model.ProxySettings as Domain
object ProxySettingsMapper { fun Entity.toDomain(): Domain =
fun to(proxySettings: ProxySettings): AppProxySettings = Domain(
with(proxySettings) { id = id,
AppProxySettings( socks5ProxyEnabled = socks5ProxyEnabled,
id, socks5ProxyBindAddress = socks5ProxyBindAddress,
socks5ProxyEnabled, httpProxyEnabled = httpProxyEnabled,
socks5ProxyBindAddress, httpProxyBindAddress = httpProxyBindAddress,
httpProxyEnabled, proxyUsername = proxyUsername,
httpProxyBindAddress, proxyPassword = proxyPassword,
proxyUsername, )
proxyPassword,
)
}
fun to(proxySettings: AppProxySettings): ProxySettings = fun Domain.toEntity(): Entity =
with(proxySettings) { Entity(
ProxySettings( id = id,
id, socks5ProxyEnabled = socks5ProxyEnabled,
socks5ProxyEnabled, socks5ProxyBindAddress = socks5ProxyBindAddress,
socks5ProxyBindAddress, httpProxyEnabled = httpProxyEnabled,
httpProxyEnabled, httpProxyBindAddress = httpProxyBindAddress,
httpProxyBindAddress, proxyUsername = proxyUsername,
proxyUsername, proxyPassword = proxyPassword,
proxyPassword, )
)
}
}
@@ -1,77 +1,41 @@
package com.zaneschepke.wireguardautotunnel.data.mapper package com.zaneschepke.wireguardautotunnel.data.mapper
import com.zaneschepke.wireguardautotunnel.data.entity.Settings import com.zaneschepke.wireguardautotunnel.data.entity.GeneralSettings as Entity
import com.zaneschepke.wireguardautotunnel.data.model.DnsProtocol import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings as Domain
import com.zaneschepke.wireguardautotunnel.data.model.DnsSettings import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
import com.zaneschepke.wireguardautotunnel.data.model.WifiDetectionMethod
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings
fun Settings.toAppSettings(): GeneralSettings { fun Entity.toDomain(): Domain =
return GeneralSettings( Domain(
id = id, id = id,
isAutoTunnelEnabled = isAutoTunnelEnabled,
isTunnelOnMobileDataEnabled = isTunnelOnMobileDataEnabled,
trustedNetworkSSIDs = trustedNetworkSSIDs,
isAlwaysOnVpnEnabled = isAlwaysOnVpnEnabled,
isTunnelOnEthernetEnabled = isTunnelOnEthernetEnabled,
isShortcutsEnabled = isShortcutsEnabled, isShortcutsEnabled = isShortcutsEnabled,
isTunnelOnWifiEnabled = isTunnelOnWifiEnabled,
isRestoreOnBootEnabled = isRestoreOnBootEnabled, isRestoreOnBootEnabled = isRestoreOnBootEnabled,
isMultiTunnelEnabled = isMultiTunnelEnabled, isMultiTunnelEnabled = isMultiTunnelEnabled,
isPingEnabled = isPingEnabled, isTunnelGlobalsEnabled = isTunnelGlobalsEnabled,
isWildcardsEnabled = isWildcardsEnabled,
isStopOnNoInternetEnabled = isStopOnNoInternetEnabled,
isLanOnKillSwitchEnabled = isLanOnKillSwitchEnabled,
debounceDelaySeconds = debounceDelaySeconds,
isDisableKillSwitchOnTrustedEnabled = isDisableKillSwitchOnTrustedEnabled,
isTunnelOnUnsecureEnabled = isTunnelOnUnsecureEnabled,
wifiDetectionMethod = WifiDetectionMethod.fromValue(wifiDetectionMethod.value),
tunnelPingIntervalSeconds = tunnelPingIntervalSeconds,
tunnelPingAttempts = tunnelPingAttempts,
tunnelPingTimeoutSeconds = tunnelPingTimeoutSeconds,
appMode = appMode, appMode = appMode,
dnsProtocol = dnsProtocol, theme = Theme.valueOf(theme.uppercase()),
dnsEndpoint = dnsEndpoint, locale = locale,
) remoteKey = remoteKey,
} isRemoteControlEnabled = isRemoteControlEnabled,
isPinLockEnabled = isPinLockEnabled,
fun GeneralSettings.toSettings(): Settings {
return Settings(
id = id,
isAutoTunnelEnabled = isAutoTunnelEnabled,
isTunnelOnMobileDataEnabled = isTunnelOnMobileDataEnabled,
trustedNetworkSSIDs = trustedNetworkSSIDs,
isAlwaysOnVpnEnabled = isAlwaysOnVpnEnabled, isAlwaysOnVpnEnabled = isAlwaysOnVpnEnabled,
isTunnelOnEthernetEnabled = isTunnelOnEthernetEnabled, isLanOnKillSwitchEnabled = isLanOnKillSwitchEnabled,
customSplitPackages = customSplitPackages,
)
fun Domain.toEntity(): Entity =
Entity(
id = id,
isShortcutsEnabled = isShortcutsEnabled, isShortcutsEnabled = isShortcutsEnabled,
isTunnelOnWifiEnabled = isTunnelOnWifiEnabled,
isRestoreOnBootEnabled = isRestoreOnBootEnabled, isRestoreOnBootEnabled = isRestoreOnBootEnabled,
isMultiTunnelEnabled = isMultiTunnelEnabled, isMultiTunnelEnabled = isMultiTunnelEnabled,
isPingEnabled = isPingEnabled, isTunnelGlobalsEnabled = isTunnelGlobalsEnabled,
isWildcardsEnabled = isWildcardsEnabled,
isStopOnNoInternetEnabled = isStopOnNoInternetEnabled,
isLanOnKillSwitchEnabled = isLanOnKillSwitchEnabled,
debounceDelaySeconds = debounceDelaySeconds,
isDisableKillSwitchOnTrustedEnabled = isDisableKillSwitchOnTrustedEnabled,
isTunnelOnUnsecureEnabled = isTunnelOnUnsecureEnabled,
wifiDetectionMethod = WifiDetectionMethod.fromValue(wifiDetectionMethod.value),
tunnelPingIntervalSeconds = tunnelPingIntervalSeconds,
tunnelPingAttempts = tunnelPingAttempts,
tunnelPingTimeoutSeconds = tunnelPingTimeoutSeconds,
appMode = appMode, appMode = appMode,
dnsProtocol = dnsProtocol, theme = theme.name,
dnsEndpoint = dnsEndpoint, locale = locale,
remoteKey = remoteKey,
isRemoteControlEnabled = isRemoteControlEnabled,
isPinLockEnabled = isPinLockEnabled,
isAlwaysOnVpnEnabled = isAlwaysOnVpnEnabled,
isLanOnKillSwitchEnabled = isLanOnKillSwitchEnabled,
customSplitPackages = customSplitPackages,
) )
}
fun GeneralSettings.toDomain(): DnsSettings {
return DnsSettings(
protocol =
DnsProtocol.entries.toTypedArray().getOrElse(dnsProtocol.value) { DnsProtocol.SYSTEM },
endpoint = dnsEndpoint,
)
}
fun DnsSettings.toAppSettings(existing: GeneralSettings): GeneralSettings {
return existing.copy(dnsProtocol = protocol, dnsEndpoint = endpoint)
}
@@ -1,46 +1,40 @@
package com.zaneschepke.wireguardautotunnel.data.mapper package com.zaneschepke.wireguardautotunnel.data.mapper
import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig as Entity
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig as Domain
object TunnelConfigMapper { fun Entity.toDomain(): Domain =
fun toTunnelConf(tunnelConfig: TunnelConfig): TunnelConf { Domain(
return with(tunnelConfig) { id = id,
TunnelConf( name = name,
id, wgQuick = wgQuick,
name, tunnelNetworks = tunnelNetworks,
wgQuick, isMobileDataTunnel = isMobileDataTunnel,
tunnelNetworks, isPrimaryTunnel = isPrimaryTunnel,
isMobileDataTunnel, amQuick = amQuick,
isPrimaryTunnel, isActive = isActive,
amQuick, restartOnPingFailure = restartOnPingFailure,
isActive, pingTarget = pingTarget,
pingTarget, isEthernetTunnel = isEthernetTunnel,
restartOnPingFailure, isIpv4Preferred = isIpv4Preferred,
isEthernetTunnel, position = position,
isIpv4Preferred, autoTunnelApps = autoTunnelApps,
position, )
)
}
}
fun toTunnelConfig(tunnelConf: TunnelConf): TunnelConfig { fun Domain.toEntity(): Entity =
return with(tunnelConf) { Entity(
TunnelConfig( id = id,
id, name = name,
tunName, wgQuick = wgQuick,
wgQuick, tunnelNetworks = tunnelNetworks,
tunnelNetworks, isMobileDataTunnel = isMobileDataTunnel,
isMobileDataTunnel, isPrimaryTunnel = isPrimaryTunnel,
isPrimaryTunnel, amQuick = amQuick,
amQuick, isActive = isActive,
isActive, restartOnPingFailure = restartOnPingFailure,
restartOnPingFailure, pingTarget = pingTarget,
pingTarget, isEthernetTunnel = isEthernetTunnel,
isEthernetTunnel, isIpv4Preferred = isIpv4Preferred,
isIpv4Preferred, position = position,
position, autoTunnelApps = autoTunnelApps,
) )
}
}
}
@@ -0,0 +1,318 @@
package com.zaneschepke.wireguardautotunnel.data.migrations
import android.content.ContentValues
import android.database.sqlite.SQLiteDatabase
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import timber.log.Timber
val MIGRATION_23_24 =
fun(dataStore: DataStore<Preferences>): Migration {
return object : Migration(23, 24) {
override fun migrate(db: SupportSQLiteDatabase) {
Timber.d("Starting migration from 23 to 24")
db.execSQL(
"""
CREATE TABLE IF NOT EXISTS `general_settings` (
`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,
`is_tunnel_globals_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,
`is_lan_on_kill_switch_enabled` INTEGER NOT NULL DEFAULT 0
)
"""
)
db.execSQL(
"""
CREATE TABLE IF NOT EXISTS `auto_tunnel_settings` (
`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,
`debounce_delay_seconds` INTEGER NOT NULL DEFAULT 3,
`is_tunnel_on_unsecure_enabled` INTEGER NOT NULL DEFAULT 0,
`wifi_detection_method` INTEGER NOT NULL DEFAULT 0
)
"""
)
db.execSQL(
"""
CREATE TABLE IF NOT EXISTS `monitoring_settings` (
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
`is_ping_enabled` INTEGER NOT NULL DEFAULT 0,
`is_ping_monitoring_enabled` INTEGER NOT NULL DEFAULT 1,
`tunnel_ping_interval_sec` INTEGER NOT NULL DEFAULT 30,
`tunnel_ping_attempts` INTEGER NOT NULL DEFAULT 3,
`tunnel_ping_timeout_sec` INTEGER,
`show_detailed_ping_stats` INTEGER NOT NULL DEFAULT 0,
`is_local_logs_enabled` INTEGER NOT NULL DEFAULT 0
)
"""
)
db.execSQL(
"""
CREATE TABLE IF NOT EXISTS `dns_settings` (
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
`dns_protocol` INTEGER NOT NULL DEFAULT 0,
`dns_endpoint` TEXT
)
"""
)
db.execSQL(
"""
CREATE TABLE IF NOT EXISTS `tunnel_config` (
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
`name` TEXT NOT NULL,
`wg_quick` TEXT NOT NULL,
`tunnel_networks` TEXT NOT NULL DEFAULT '',
`is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false,
`is_primary_tunnel` INTEGER NOT NULL DEFAULT false,
`am_quick` TEXT NOT NULL DEFAULT '',
`is_Active` INTEGER NOT NULL DEFAULT false,
`restart_on_ping_failure` INTEGER NOT NULL DEFAULT false,
`ping_target` TEXT DEFAULT null,
`is_ethernet_tunnel` INTEGER NOT NULL DEFAULT false,
`is_ipv4_preferred` INTEGER NOT NULL DEFAULT true,
`position` INTEGER NOT NULL DEFAULT 0,
`auto_tunnel_apps` TEXT NOT NULL DEFAULT '[]'
)
"""
)
db.execSQL(
"""
CREATE UNIQUE INDEX `index_tunnel_config_name` ON `tunnel_config` (`name`)
"""
)
try {
db.execSQL(
"""
INSERT INTO `general_settings` (
`id`, `is_shortcuts_enabled`, `is_restore_on_boot_enabled`,
`is_multi_tunnel_enabled`, `is_tunnel_globals_enabled`, `app_mode`,
`is_always_on_vpn_enabled`, `is_lan_on_kill_switch_enabled`
)
SELECT
`id`,
COALESCE(`is_shortcuts_enabled`, 0),
COALESCE(`is_restore_on_boot_enabled`, 0),
COALESCE(`is_multi_tunnel_enabled`, 0),
COALESCE(`is_tunnel_globals_enabled`, 0),
COALESCE(`app_mode`, 0),
COALESCE(`is_always_on_vpn_enabled`, 0),
COALESCE(`is_lan_on_kill_switch_enabled`, 0)
FROM `Settings`
"""
)
Timber.d("Migrated data to general_settings")
} catch (e: Exception) {
Timber.e(e, "Failed to migrate data to general_settings, inserting default row")
db.execSQL("INSERT INTO `general_settings` DEFAULT VALUES")
}
try {
db.execSQL(
"""
INSERT INTO `auto_tunnel_settings` (
`id`, `is_tunnel_enabled`, `is_tunnel_on_mobile_data_enabled`,
`trusted_network_ssids`, `is_tunnel_on_ethernet_enabled`,
`is_tunnel_on_wifi_enabled`, `is_wildcards_enabled`, `is_stop_on_no_internet_enabled`,
`debounce_delay_seconds`, `is_tunnel_on_unsecure_enabled`,
`wifi_detection_method`
)
SELECT
`id`,
COALESCE(`is_tunnel_enabled`, 0),
COALESCE(`is_tunnel_on_mobile_data_enabled`, 0),
COALESCE(`trusted_network_ssids`, ''),
COALESCE(`is_tunnel_on_ethernet_enabled`, 0),
COALESCE(`is_tunnel_on_wifi_enabled`, 0),
COALESCE(`is_wildcards_enabled`, 0),
COALESCE(`is_stop_on_no_internet_enabled`, 0),
COALESCE(`debounce_delay_seconds`, 3),
COALESCE(`is_tunnel_on_unsecure_enabled`, 0),
COALESCE(`wifi_detection_method`, 0)
FROM `Settings`
"""
)
Timber.d("Migrated data to auto_tunnel_settings")
} catch (e: Exception) {
Timber.e(
e,
"Failed to migrate data to auto_tunnel_settings, inserting default row",
)
db.execSQL("INSERT INTO `auto_tunnel_settings` DEFAULT VALUES")
}
try {
db.execSQL(
"""
INSERT INTO `monitoring_settings` (
`id`, `is_ping_enabled`, `is_ping_monitoring_enabled`,
`tunnel_ping_interval_sec`, `tunnel_ping_attempts`, `tunnel_ping_timeout_sec`
)
SELECT
`id`,
COALESCE(`is_ping_enabled`, 0),
COALESCE(`is_ping_monitoring_enabled`, 1),
COALESCE(`tunnel_ping_interval_sec`, 30),
COALESCE(`tunnel_ping_attempts`, 3),
COALESCE(`tunnel_ping_timeout_sec`, NULL)
FROM `Settings`
"""
)
Timber.d("Migrated data to monitoring_settings")
} catch (e: Exception) {
Timber.e(
e,
"Failed to migrate data to monitoring_settings, inserting default row",
)
db.execSQL("INSERT INTO `monitoring_settings` DEFAULT VALUES")
}
try {
db.execSQL(
"""
INSERT INTO `dns_settings` (
`id`, `dns_protocol`, `dns_endpoint`
)
SELECT
`id`,
COALESCE(`dns_protocol`, 0),
COALESCE(`dns_endpoint`, NULL)
FROM `Settings`
"""
)
Timber.d("Migrated data to dns_settings")
} catch (e: Exception) {
Timber.e(e, "Failed to migrate data to dns_settings, inserting default row")
db.execSQL("INSERT INTO `dns_settings` DEFAULT VALUES")
}
try {
db.execSQL(
"""
INSERT INTO `tunnel_config` (
`id`, `name`, `wg_quick`, `tunnel_networks`, `is_mobile_data_tunnel`,
`is_primary_tunnel`, `am_quick`, `is_Active`, `restart_on_ping_failure`,
`ping_target`, `is_ethernet_tunnel`, `is_ipv4_preferred`, `position`,
`auto_tunnel_apps`
)
SELECT
`id`, `name`, `wg_quick`, `tunnel_networks`, `is_mobile_data_tunnel`,
`is_primary_tunnel`, `am_quick`, `is_Active`, `restart_on_ping_failure`,
`ping_target`, `is_ethernet_tunnel`, `is_ipv4_preferred`, `position`,
`auto_tunnel_apps`
FROM `TunnelConfig`
"""
)
} catch (e: Exception) {
Timber.e(e, "Failed to migrate data to tunnel_config")
}
try {
runBlocking {
val preferences = dataStore.data.first()
val pinLockEnabled = booleanPreferencesKey("PIN_LOCK_ENABLED")
val isLocalLogsEnabled = booleanPreferencesKey("LOCAL_LOGS_ENABLED")
val locale = stringPreferencesKey("LOCALE")
val theme = stringPreferencesKey("THEME")
val isRemoteControlEnabled =
booleanPreferencesKey("IS_REMOTE_CONTROL_ENABLED")
val remoteKey = stringPreferencesKey("REMOTE_KEY")
val showDetailedPingStats =
booleanPreferencesKey("SHOW_DETAILED_PING_STATS")
val currentTheme = preferences[theme] ?: "AUTOMATIC"
val currentLocale = preferences[locale]
val currentRemoteKey = preferences[remoteKey]
val isRemoteEnabled = preferences[isRemoteControlEnabled] ?: false
val isPinLockEnabled = preferences[pinLockEnabled] ?: false
val detailedPingStats = preferences[showDetailedPingStats] ?: false
val localLogs = preferences[isLocalLogsEnabled] ?: false
val generalValues =
ContentValues().apply {
put("id", 1)
put("theme", currentTheme)
put("locale", currentLocale)
put("remote_key", currentRemoteKey)
put("is_remote_control_enabled", if (isRemoteEnabled) 1 else 0)
put("is_pin_lock_enabled", if (isPinLockEnabled) 1 else 0)
}
// Try updating first
val rowsAffected =
db.update(
table = "general_settings",
conflictAlgorithm = SQLiteDatabase.CONFLICT_REPLACE,
values = generalValues,
whereClause = "id = ?",
whereArgs = arrayOf("1"),
)
if (rowsAffected == 0) {
db.insert(
"general_settings",
SQLiteDatabase.CONFLICT_REPLACE,
generalValues,
)
}
Timber.d("Updated or inserted DataStore values in general_settings")
val monitoringValues =
ContentValues().apply {
put("id", 1)
put("show_detailed_ping_stats", if (detailedPingStats) 1 else 0)
put("is_local_logs_enabled", if (localLogs) 1 else 0)
}
val monitoringRowsAffected =
db.update(
table = "monitoring_settings",
conflictAlgorithm = SQLiteDatabase.CONFLICT_REPLACE,
values = monitoringValues,
whereClause = "id = ?",
whereArgs = arrayOf("1"),
)
if (monitoringRowsAffected == 0) {
db.insert(
"monitoring_settings",
SQLiteDatabase.CONFLICT_REPLACE,
monitoringValues,
)
}
Timber.d("Updated or inserted DataStore values in monitoring_settings")
}
} catch (e: Exception) {
Timber.e(e, "Failed to migrate datastore data")
}
db.execSQL("DROP TABLE IF EXISTS `Settings`")
db.execSQL("DROP TABLE IF EXISTS `TunnelConfig`")
Timber.d("Migration 23 to 24 completed")
}
}
}
@@ -1,13 +1,12 @@
package com.zaneschepke.wireguardautotunnel.data.repository package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.DataStoreManager import com.zaneschepke.wireguardautotunnel.data.DataStoreManager
import com.zaneschepke.wireguardautotunnel.data.entity.GeneralState import com.zaneschepke.wireguardautotunnel.data.entity.AppState as Entity
import com.zaneschepke.wireguardautotunnel.data.mapper.GeneralStateMapper import com.zaneschepke.wireguardautotunnel.data.mapper.toDomain
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.model.AppState import com.zaneschepke.wireguardautotunnel.domain.model.AppState as Domain
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@@ -23,163 +22,42 @@ class DataStoreAppStateRepository(
@IoDispatcher private val ioDispatcher: CoroutineDispatcher, @IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : AppStateRepository { ) : AppStateRepository {
override suspend fun isLocationDisclosureShown(): Boolean { override suspend fun isLocationDisclosureShown(): Boolean {
return dataStoreManager.getFromStore(DataStoreManager.locationDisclosureShown) return dataStoreManager.getFromStore(DataStoreManager.locationDisclosureShown) ?: false
?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT
} }
override suspend fun setLocationDisclosureShown(shown: Boolean) { override suspend fun setLocationDisclosureShown(shown: Boolean) {
dataStoreManager.saveToDataStore(DataStoreManager.locationDisclosureShown, shown) dataStoreManager.saveToDataStore(DataStoreManager.locationDisclosureShown, shown)
} }
override suspend fun isPinLockEnabled(): Boolean {
return dataStoreManager.getFromStore(DataStoreManager.pinLockEnabled)
?: GeneralState.PIN_LOCK_ENABLED_DEFAULT
}
override suspend fun setPinLockEnabled(enabled: Boolean) {
dataStoreManager.saveToDataStore(DataStoreManager.pinLockEnabled, enabled)
}
override suspend fun isBatteryOptimizationDisableShown(): Boolean { override suspend fun isBatteryOptimizationDisableShown(): Boolean {
return dataStoreManager.getFromStore(DataStoreManager.batteryDisableShown) return dataStoreManager.getFromStore(DataStoreManager.batteryDisableShown) ?: false
?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT
} }
override suspend fun setBatteryOptimizationDisableShown(shown: Boolean) { override suspend fun setBatteryOptimizationDisableShown(shown: Boolean) {
dataStoreManager.saveToDataStore(DataStoreManager.batteryDisableShown, shown) dataStoreManager.saveToDataStore(DataStoreManager.batteryDisableShown, shown)
} }
override suspend fun setTunnelExpanded(id: Int) { override val flow: Flow<Domain> =
val ids =
dataStoreManager
.getFromStore(DataStoreManager.expandedTunnelIds)
?.split(",")
?.mapNotNull { it.toIntOrNull() } ?: emptyList()
if (ids.contains(id)) return
val updatedList = ids.toMutableList().apply { add(id) }
dataStoreManager.saveToDataStore(
DataStoreManager.expandedTunnelIds,
updatedList.joinToString(","),
)
}
override suspend fun removeTunnelExpanded(id: Int) {
val ids =
dataStoreManager
.getFromStore(DataStoreManager.expandedTunnelIds)
?.split(",")
?.mapNotNull { it.toIntOrNull() } ?: emptyList()
if (ids.isEmpty() || !ids.contains(id)) return
val updatedList = ids.toMutableList().apply { remove(id) }
dataStoreManager.saveToDataStore(
DataStoreManager.expandedTunnelIds,
updatedList.joinToString(","),
)
}
override suspend fun setTheme(theme: Theme) {
dataStoreManager.saveToDataStore(DataStoreManager.theme, theme.name)
}
override suspend fun getTheme(): Theme {
return dataStoreManager.getFromStore(DataStoreManager.theme)?.let {
try {
Theme.valueOf(it)
} catch (_: IllegalArgumentException) {
Theme.AUTOMATIC
}
} ?: Theme.AUTOMATIC
}
override suspend fun isLocalLogsEnabled(): Boolean {
return dataStoreManager.getFromStore(DataStoreManager.isLocalLogsEnabled)
?: GeneralState.IS_LOGS_ENABLED_DEFAULT
}
override suspend fun setLocalLogsEnabled(enabled: Boolean) {
dataStoreManager.saveToDataStore(DataStoreManager.isLocalLogsEnabled, enabled)
}
override suspend fun setLocale(localeTag: String) {
dataStoreManager.saveToDataStore(DataStoreManager.locale, localeTag)
}
override suspend fun getLocale(): String? {
return dataStoreManager.getFromStore(DataStoreManager.locale)
}
override suspend fun setIsRemoteControlEnabled(enabled: Boolean) {
dataStoreManager.saveToDataStore(DataStoreManager.isRemoteControlEnabled, enabled)
}
override suspend fun isRemoteControlEnabled(): Boolean {
return dataStoreManager.getFromStore(DataStoreManager.isRemoteControlEnabled)
?: GeneralState.IS_REMOTE_CONTROL_ENABLED
}
override suspend fun setRemoteKey(key: String) {
dataStoreManager.saveToDataStore(DataStoreManager.remoteKey, key)
}
override suspend fun getRemoteKey(): String? {
return dataStoreManager.getFromStore(DataStoreManager.remoteKey)
}
override suspend fun setShowDetailedPingStats(showDetailedPing: Boolean) {
dataStoreManager.saveToDataStore(DataStoreManager.showDetailedPingStats, showDetailedPing)
}
override suspend fun getShowDetailedPing(): Boolean {
return dataStoreManager.getFromStore(DataStoreManager.showDetailedPingStats)
?: GeneralState.SHOW_DETAILED_PING_STATS_DEFAULT
}
override val flow: Flow<AppState> =
dataStoreManager.preferencesFlow dataStoreManager.preferencesFlow
.map { prefs -> .map { prefs ->
prefs?.let { pref -> prefs?.let { pref ->
try { try {
GeneralState( Entity(
isLocationDisclosureShown = isLocationDisclosureShown =
pref[DataStoreManager.locationDisclosureShown] pref[DataStoreManager.locationDisclosureShown] ?: false,
?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT,
isBatteryOptimizationDisableShown = isBatteryOptimizationDisableShown =
pref[DataStoreManager.batteryDisableShown] pref[DataStoreManager.batteryDisableShown] ?: false,
?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT,
isPinLockEnabled =
pref[DataStoreManager.pinLockEnabled]
?: GeneralState.PIN_LOCK_ENABLED_DEFAULT,
expandedTunnelIds =
pref[DataStoreManager.expandedTunnelIds]?.split(",")?.mapNotNull {
it.toIntOrNull()
} ?: emptyList(),
isLocalLogsEnabled =
pref[DataStoreManager.isLocalLogsEnabled]
?: GeneralState.IS_LOGS_ENABLED_DEFAULT,
isRemoteControlEnabled =
pref[DataStoreManager.isRemoteControlEnabled]
?: GeneralState.IS_REMOTE_CONTROL_ENABLED,
showDetailedPingStats =
pref[DataStoreManager.showDetailedPingStats]
?: GeneralState.SHOW_DETAILED_PING_STATS_DEFAULT,
remoteKey = pref[DataStoreManager.remoteKey],
locale = pref[DataStoreManager.locale],
theme = getTheme(),
) )
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
Timber.e(e) Timber.e(e)
GeneralState() Entity()
} }
} ?: GeneralState() } ?: Entity()
} }
.map(GeneralStateMapper::toAppState) .map { it.toDomain() }
.stateIn( .stateIn(
scope = applicationScope + ioDispatcher, scope = applicationScope + ioDispatcher,
started = SharingStarted.Eagerly, started = SharingStarted.Eagerly,
initialValue = AppState(), initialValue = com.zaneschepke.wireguardautotunnel.domain.model.AppState(),
) )
} }
@@ -0,0 +1,37 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.dao.AutoTunnelSettingsDao
import com.zaneschepke.wireguardautotunnel.data.entity.AutoTunnelSettings as Entity
import com.zaneschepke.wireguardautotunnel.data.mapper.toDomain
import com.zaneschepke.wireguardautotunnel.data.mapper.toEntity
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.model.AutoTunnelSettings as Domain
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
class RoomAutoTunnelSettingsRepository(
private val autoTunnelSettingsDao: AutoTunnelSettingsDao,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : AutoTunnelSettingsRepository {
override suspend fun upsert(autoTunnelSettings: Domain) {
autoTunnelSettingsDao.upsert(autoTunnelSettings.toEntity())
}
override val flow: Flow<Domain>
get() =
autoTunnelSettingsDao
.getAutoTunnelSettingsFlow()
.map { (it ?: Entity()).toDomain() }
.flowOn(ioDispatcher)
override suspend fun getAutoTunnelSettings(): Domain {
return (autoTunnelSettingsDao.getAutoTunnelSettings() ?: Entity()).toDomain()
}
override suspend fun updateAutoTunnelEnabled(enabled: Boolean) {
autoTunnelSettingsDao.updateAutoTunnelEnabled(enabled)
}
}
@@ -0,0 +1,33 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.dao.DnsSettingsDao
import com.zaneschepke.wireguardautotunnel.data.entity.DnsSettings as Entity
import com.zaneschepke.wireguardautotunnel.data.mapper.toDomain
import com.zaneschepke.wireguardautotunnel.data.mapper.toEntity
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.model.DnsSettings as Domain
import com.zaneschepke.wireguardautotunnel.domain.repository.DnsSettingsRepository
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
class RoomDnsSettingsRepository(
private val dnsSettingsDao: DnsSettingsDao,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : DnsSettingsRepository {
override suspend fun upsert(dnsSettings: Domain) {
dnsSettingsDao.upsert(dnsSettings.toEntity())
}
override val flow: Flow<Domain>
get() =
dnsSettingsDao
.getDnsSettingsFlow()
.map { (it ?: Entity()).toDomain() }
.flowOn(ioDispatcher)
override suspend fun getDnsSettings(): Domain {
return (dnsSettingsDao.getDnsSettings() ?: Entity()).toDomain()
}
}
@@ -0,0 +1,33 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.dao.MonitoringSettingsDao
import com.zaneschepke.wireguardautotunnel.data.entity.MonitoringSettings as Entity
import com.zaneschepke.wireguardautotunnel.data.mapper.toDomain
import com.zaneschepke.wireguardautotunnel.data.mapper.toEntity
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.model.MonitoringSettings as Domain
import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
class RoomMonitoringSettingsRepository(
private val monitoringSettingsDao: MonitoringSettingsDao,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : MonitoringSettingsRepository {
override suspend fun upsert(monitoringSettings: Domain) {
monitoringSettingsDao.upsert(monitoringSettings.toEntity())
}
override val flow: Flow<Domain>
get() =
monitoringSettingsDao
.getMonitoringSettingsFlow()
.map { (it ?: Entity()).toDomain() }
.flowOn(ioDispatcher)
override suspend fun getMonitoringSettings(): Domain {
return (monitoringSettingsDao.getMonitoringSettings() ?: Entity()).toDomain()
}
}
@@ -1,10 +1,11 @@
package com.zaneschepke.wireguardautotunnel.data.repository package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.dao.ProxySettingsDao import com.zaneschepke.wireguardautotunnel.data.dao.ProxySettingsDao
import com.zaneschepke.wireguardautotunnel.data.entity.ProxySettings import com.zaneschepke.wireguardautotunnel.data.entity.ProxySettings as Entity
import com.zaneschepke.wireguardautotunnel.data.mapper.ProxySettingsMapper import com.zaneschepke.wireguardautotunnel.data.mapper.toDomain
import com.zaneschepke.wireguardautotunnel.data.mapper.toEntity
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.model.AppProxySettings import com.zaneschepke.wireguardautotunnel.domain.model.ProxySettings as Domain
import com.zaneschepke.wireguardautotunnel.domain.repository.ProxySettingsRepository import com.zaneschepke.wireguardautotunnel.domain.repository.ProxySettingsRepository
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
@@ -15,16 +16,20 @@ class RoomProxySettingsRepository(
private val proxySettingsDao: ProxySettingsDao, private val proxySettingsDao: ProxySettingsDao,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher, @IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : ProxySettingsRepository { ) : ProxySettingsRepository {
override suspend fun save(proxySettings: AppProxySettings) { override suspend fun upsert(proxySettings: Domain) {
withContext(ioDispatcher) { proxySettingsDao.save(ProxySettingsMapper.to(proxySettings)) } withContext(ioDispatcher) { proxySettingsDao.upsert(proxySettings.toEntity()) }
} }
override val flow = override val flow =
proxySettingsDao.getSettingsFlow().flowOn(ioDispatcher).map(ProxySettingsMapper::to) proxySettingsDao
.getProxySettingsFlow()
.flowOn(ioDispatcher)
.map { (it ?: Entity()).toDomain() }
.flowOn(ioDispatcher)
override suspend fun get(): AppProxySettings { override suspend fun getProxySettings(): Domain {
return withContext(ioDispatcher) { return withContext(ioDispatcher) {
ProxySettingsMapper.to(proxySettingsDao.getAll().firstOrNull() ?: ProxySettings()) (proxySettingsDao.getProxySettings() ?: Entity()).toDomain()
} }
} }
} }
@@ -1,11 +1,11 @@
package com.zaneschepke.wireguardautotunnel.data.repository package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.dao.SettingsDao import com.zaneschepke.wireguardautotunnel.data.dao.GeneralSettingsDao
import com.zaneschepke.wireguardautotunnel.data.entity.Settings import com.zaneschepke.wireguardautotunnel.data.entity.GeneralSettings as Entity
import com.zaneschepke.wireguardautotunnel.data.mapper.toAppSettings import com.zaneschepke.wireguardautotunnel.data.mapper.toDomain
import com.zaneschepke.wireguardautotunnel.data.mapper.toSettings import com.zaneschepke.wireguardautotunnel.data.mapper.toEntity
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings as Domain
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
@@ -13,24 +13,23 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
class RoomSettingsRepository( class RoomSettingsRepository(
private val settingsDoa: SettingsDao, private val settingsDoa: GeneralSettingsDao,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher, @IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : GeneralSettingRepository { ) : GeneralSettingRepository {
override suspend fun save(generalSettings: GeneralSettings) { override suspend fun upsert(generalSettings: Domain) {
withContext(ioDispatcher) { settingsDoa.save(generalSettings.toSettings()) } withContext(ioDispatcher) { settingsDoa.upsert(generalSettings.toEntity()) }
} }
override val flow = override val flow =
settingsDoa.getSettingsFlow().flowOn(ioDispatcher).map { it.toAppSettings() } settingsDoa
.getGeneralSettingsFlow()
.map { (it ?: Entity()).toDomain() }
.flowOn(ioDispatcher)
override suspend fun get(): GeneralSettings { override suspend fun getGeneralSettings(): Domain {
return withContext(ioDispatcher) { return withContext(ioDispatcher) {
(settingsDoa.getAll().firstOrNull() ?: Settings()).toAppSettings() (settingsDoa.getGeneralSettings() ?: Entity()).toDomain()
} }
} }
override suspend fun updateAutoTunnelEnabled(enabled: Boolean) {
withContext(ioDispatcher) { settingsDoa.updateAutoTunnelEnabled(enabled) }
}
} }
@@ -1,12 +1,13 @@
package com.zaneschepke.wireguardautotunnel.data.repository package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.dao.TunnelConfigDao import com.zaneschepke.wireguardautotunnel.data.dao.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.data.mapper.TunnelConfigMapper import com.zaneschepke.wireguardautotunnel.data.mapper.toDomain
import com.zaneschepke.wireguardautotunnel.data.mapper.toEntity
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig as Domain
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.util.extensions.Tunnels
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -18,31 +19,37 @@ class RoomTunnelRepository(
override val flow = override val flow =
tunnelConfigDao.getAllFlow().flowOn(ioDispatcher).map { tunnelConfigDao.getAllFlow().flowOn(ioDispatcher).map {
it.map(TunnelConfigMapper::toTunnelConf) it.map { tunnelConfig -> tunnelConfig.toDomain() }
} }
override suspend fun getAll(): Tunnels { override val userTunnelsFlow =
return withContext(ioDispatcher) { tunnelConfigDao.getAllTunnelsExceptGlobal().flowOn(ioDispatcher).map {
tunnelConfigDao.getAll().map(TunnelConfigMapper::toTunnelConf) it.map { tunnelConfig -> tunnelConfig.toDomain() }
} }
override val globalTunnelFlow: Flow<Domain?> =
tunnelConfigDao.getGlobalTunnel().flowOn(ioDispatcher).map { it?.toDomain() }
override suspend fun getAll(): List<Domain> {
return withContext(ioDispatcher) { tunnelConfigDao.getAll().map { it.toDomain() } }
} }
override suspend fun save(tunnelConf: TunnelConf) { override suspend fun save(tunnelConfig: Domain) {
withContext(ioDispatcher) { tunnelConfigDao.upsert(tunnelConfig.toEntity()) }
}
override suspend fun saveAll(tunnelConfigList: List<Domain>) {
withContext(ioDispatcher) { withContext(ioDispatcher) {
tunnelConfigDao.save(TunnelConfigMapper.toTunnelConfig(tunnelConf)) tunnelConfigDao.saveAll(
tunnelConfigList.map { tunnelConfig -> tunnelConfig.toEntity() }
)
} }
} }
override suspend fun saveAll(tunnelConfList: List<TunnelConf>) { override suspend fun updatePrimaryTunnel(tunnelConfig: Domain?) {
withContext(ioDispatcher) {
tunnelConfigDao.saveAll(tunnelConfList.map(TunnelConfigMapper::toTunnelConfig))
}
}
override suspend fun updatePrimaryTunnel(tunnelConf: TunnelConf?) {
withContext(ioDispatcher) { withContext(ioDispatcher) {
tunnelConfigDao.resetPrimaryTunnel() tunnelConfigDao.resetPrimaryTunnel()
tunnelConf?.let { save(it.copy(isPrimaryTunnel = true)) } tunnelConfig?.let { save(it.copy(isPrimaryTunnel = true)) }
} }
} }
@@ -50,81 +57,65 @@ class RoomTunnelRepository(
withContext(ioDispatcher) { tunnelConfigDao.resetActiveTunnels() } withContext(ioDispatcher) { tunnelConfigDao.resetActiveTunnels() }
} }
override suspend fun updateMobileDataTunnel(tunnelConf: TunnelConf?) { override suspend fun updateMobileDataTunnel(tunnelConfig: Domain?) {
withContext(ioDispatcher) { withContext(ioDispatcher) {
tunnelConfigDao.resetMobileDataTunnel() tunnelConfigDao.resetMobileDataTunnel()
tunnelConf?.let { save(it.copy(isMobileDataTunnel = true)) } tunnelConfig?.let { save(it.copy(isMobileDataTunnel = true)) }
} }
} }
override suspend fun updateEthernetTunnel(tunnelConf: TunnelConf?) { override suspend fun updateEthernetTunnel(tunnelConfig: Domain?) {
withContext(ioDispatcher) { withContext(ioDispatcher) {
tunnelConfigDao.resetEthernetTunnel() tunnelConfigDao.resetEthernetTunnel()
tunnelConf?.let { save(it.copy(isEthernetTunnel = true)) } tunnelConfig?.let { save(it.copy(isEthernetTunnel = true)) }
} }
} }
override suspend fun delete(tunnelConf: TunnelConf) { override suspend fun delete(tunnelConfig: Domain) {
withContext(ioDispatcher) { withContext(ioDispatcher) { tunnelConfigDao.delete(tunnelConfig.toEntity()) }
tunnelConfigDao.delete(TunnelConfigMapper.toTunnelConfig(tunnelConf))
}
} }
override suspend fun getById(id: Int): TunnelConf? { override suspend fun getById(id: Int): Domain? {
return withContext(ioDispatcher) { return withContext(ioDispatcher) { tunnelConfigDao.getById(id.toLong())?.toDomain() }
tunnelConfigDao.getById(id.toLong())?.let(TunnelConfigMapper::toTunnelConf)
}
} }
override suspend fun getActive(): Tunnels { override suspend fun getActive(): List<Domain> {
return withContext(ioDispatcher) { return withContext(ioDispatcher) { tunnelConfigDao.getActive().map { it.toDomain() } }
tunnelConfigDao.getActive().map(TunnelConfigMapper::toTunnelConf)
}
} }
override suspend fun getDefaultTunnel(): TunnelConf? { override suspend fun getDefaultTunnel(): Domain? {
return withContext(ioDispatcher) { return withContext(ioDispatcher) { tunnelConfigDao.getDefaultTunnel()?.toDomain() }
tunnelConfigDao.getDefaultTunnel()?.let(TunnelConfigMapper::toTunnelConf)
}
} }
override suspend fun getStartTunnel(): TunnelConf? { override suspend fun getStartTunnel(): Domain? {
return withContext(ioDispatcher) { return withContext(ioDispatcher) { tunnelConfigDao.getStartTunnel()?.toDomain() }
tunnelConfigDao.getStartTunnel()?.let(TunnelConfigMapper::toTunnelConf)
}
} }
override suspend fun count(): Int { override suspend fun count(): Int {
return withContext(ioDispatcher) { tunnelConfigDao.count().toInt() } return withContext(ioDispatcher) { tunnelConfigDao.count().toInt() }
} }
override suspend fun findByTunnelName(name: String): TunnelConf? { override suspend fun findByTunnelName(name: String): Domain? {
return withContext(ioDispatcher) { tunnelConfigDao.getByName(name)?.toDomain() }
}
override suspend fun findByTunnelNetworksName(name: String): List<Domain> {
return withContext(ioDispatcher) { return withContext(ioDispatcher) {
tunnelConfigDao.getByName(name)?.let(TunnelConfigMapper::toTunnelConf) tunnelConfigDao.findByTunnelNetworkName(name).map { it.toDomain() }
} }
} }
override suspend fun findByTunnelNetworksName(name: String): Tunnels { override suspend fun findByMobileDataTunnel(): List<Domain> {
return withContext(ioDispatcher) { return withContext(ioDispatcher) {
tunnelConfigDao.findByTunnelNetworkName(name).map(TunnelConfigMapper::toTunnelConf) tunnelConfigDao.findByMobileDataTunnel().map { it.toDomain() }
} }
} }
override suspend fun findByMobileDataTunnel(): Tunnels { override suspend fun findPrimary(): List<Domain> {
return withContext(ioDispatcher) { return withContext(ioDispatcher) { tunnelConfigDao.findByPrimary().map { it.toDomain() } }
tunnelConfigDao.findByMobileDataTunnel().map(TunnelConfigMapper::toTunnelConf)
}
} }
override suspend fun findPrimary(): Tunnels { override suspend fun delete(tunnels: List<Domain>) {
return withContext(ioDispatcher) { withContext(ioDispatcher) { tunnelConfigDao.delete(tunnels.map { it.toEntity() }) }
tunnelConfigDao.findByPrimary().map(TunnelConfigMapper::toTunnelConf)
}
}
override suspend fun delete(tunnels: List<TunnelConf>) {
withContext(ioDispatcher) {
tunnelConfigDao.delete(tunnels.map { TunnelConfigMapper.toTunnelConfig(it) })
}
} }
} }
@@ -6,9 +6,8 @@ import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.AppDatabase import com.zaneschepke.wireguardautotunnel.data.AppDatabase
import com.zaneschepke.wireguardautotunnel.data.DataStoreManager import com.zaneschepke.wireguardautotunnel.data.DataStoreManager
import com.zaneschepke.wireguardautotunnel.data.DatabaseCallback import com.zaneschepke.wireguardautotunnel.data.DatabaseCallback
import com.zaneschepke.wireguardautotunnel.data.dao.ProxySettingsDao import com.zaneschepke.wireguardautotunnel.data.dao.*
import com.zaneschepke.wireguardautotunnel.data.dao.SettingsDao import com.zaneschepke.wireguardautotunnel.data.migrations.MIGRATION_23_24
import com.zaneschepke.wireguardautotunnel.data.dao.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.data.network.GitHubApi import com.zaneschepke.wireguardautotunnel.data.network.GitHubApi
import com.zaneschepke.wireguardautotunnel.data.network.KtorClient import com.zaneschepke.wireguardautotunnel.data.network.KtorClient
import com.zaneschepke.wireguardautotunnel.data.network.KtorGitHubApi import com.zaneschepke.wireguardautotunnel.data.network.KtorGitHubApi
@@ -49,12 +48,14 @@ class RepositoryModule {
fun provideDatabase( fun provideDatabase(
@ApplicationContext context: Context, @ApplicationContext context: Context,
callback: DatabaseCallback, callback: DatabaseCallback,
dataStoreManager: DataStoreManager,
): AppDatabase { ): AppDatabase {
return Room.databaseBuilder( return Room.databaseBuilder(
context, context,
AppDatabase::class.java, AppDatabase::class.java,
context.getString(R.string.db_name), context.getString(R.string.db_name),
) )
.addMigrations(MIGRATION_23_24(dataStoreManager.dataStore))
.fallbackToDestructiveMigration(true) .fallbackToDestructiveMigration(true)
.addCallback(callback) .addCallback(callback)
.build() .build()
@@ -62,8 +63,26 @@ class RepositoryModule {
@Singleton @Singleton
@Provides @Provides
fun provideSettingsDoa(appDatabase: AppDatabase): SettingsDao { fun provideSettingsDoa(appDatabase: AppDatabase): GeneralSettingsDao {
return appDatabase.settingDao() return appDatabase.generalSettingsDao()
}
@Singleton
@Provides
fun provideDnsSettingsDao(appDatabase: AppDatabase): DnsSettingsDao {
return appDatabase.dnsSettingsDao()
}
@Singleton
@Provides
fun provideAutoTunnelDao(appDatabase: AppDatabase): AutoTunnelSettingsDao {
return appDatabase.autoTunnelSettingsDao()
}
@Singleton
@Provides
fun provideMonitoringDao(appDatabase: AppDatabase): MonitoringSettingsDao {
return appDatabase.monitoringSettingsDao()
} }
@Singleton @Singleton
@@ -89,13 +108,40 @@ class RepositoryModule {
@Singleton @Singleton
@Provides @Provides
fun provideSettingsRepository( fun provideGeneralSettingsRepository(
settingsDao: SettingsDao, settingsDao: GeneralSettingsDao,
@IoDispatcher ioDispatcher: CoroutineDispatcher, @IoDispatcher ioDispatcher: CoroutineDispatcher,
): GeneralSettingRepository { ): GeneralSettingRepository {
return RoomSettingsRepository(settingsDao, ioDispatcher) return RoomSettingsRepository(settingsDao, ioDispatcher)
} }
@Singleton
@Provides
fun provideMonitoringSettingsRepository(
monitoringSettingsDao: MonitoringSettingsDao,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): MonitoringSettingsRepository {
return RoomMonitoringSettingsRepository(monitoringSettingsDao, ioDispatcher)
}
@Singleton
@Provides
fun provideDnsSettingsRepository(
dnsSettingsDao: DnsSettingsDao,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): DnsSettingsRepository {
return RoomDnsSettingsRepository(dnsSettingsDao, ioDispatcher)
}
@Singleton
@Provides
fun provideAutoTunnelSettingsRepository(
autoTunnelSettingsDao: AutoTunnelSettingsDao,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): AutoTunnelSettingsRepository {
return RoomAutoTunnelSettingsRepository(autoTunnelSettingsDao, ioDispatcher)
}
@Singleton @Singleton
@Provides @Provides
fun provideProxySettingsRepository( fun provideProxySettingsRepository(
@@ -9,9 +9,7 @@ import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
import com.zaneschepke.networkmonitor.NetworkMonitor import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.* import com.zaneschepke.wireguardautotunnel.core.tunnel.*
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository import com.zaneschepke.wireguardautotunnel.domain.repository.*
import com.zaneschepke.wireguardautotunnel.domain.repository.ProxySettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.util.extensions.to import com.zaneschepke.wireguardautotunnel.util.extensions.to
import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils
import dagger.Module import dagger.Module
@@ -85,9 +83,10 @@ class TunnelModule {
@Kernel @Kernel
fun provideKernelProvider( fun provideKernelProvider(
@ApplicationScope applicationScope: CoroutineScope, @ApplicationScope applicationScope: CoroutineScope,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
backend: com.wireguard.android.backend.Backend, backend: com.wireguard.android.backend.Backend,
): TunnelProvider { ): TunnelProvider {
return KernelTunnel(applicationScope, backend) return KernelTunnel(applicationScope, ioDispatcher, backend)
} }
@Provides @Provides
@@ -96,13 +95,15 @@ class TunnelModule {
fun provideUserspaceProvider( fun provideUserspaceProvider(
@ApplicationScope applicationScope: CoroutineScope, @ApplicationScope applicationScope: CoroutineScope,
proxySettingsRepository: ProxySettingsRepository, proxySettingsRepository: ProxySettingsRepository,
settingsRepository: GeneralSettingRepository, dnsSettingsRepository: DnsSettingsRepository,
@Userspace backend: Backend, @Userspace backend: Backend,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): TunnelProvider { ): TunnelProvider {
return UserspaceTunnel( return UserspaceTunnel(
applicationScope, applicationScope,
ioDispatcher,
proxySettingsRepository, proxySettingsRepository,
settingsRepository, dnsSettingsRepository,
backend, backend,
) )
} }
@@ -112,14 +113,16 @@ class TunnelModule {
@ProxyUserspace @ProxyUserspace
fun provideProxyUserspaceProvider( fun provideProxyUserspaceProvider(
@ApplicationScope applicationScope: CoroutineScope, @ApplicationScope applicationScope: CoroutineScope,
settingsRepository: GeneralSettingRepository, dnsSettingsRepository: DnsSettingsRepository,
proxySettingsRepository: ProxySettingsRepository, proxySettingsRepository: ProxySettingsRepository,
@ProxyUserspace backend: Backend, @ProxyUserspace backend: Backend,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): TunnelProvider { ): TunnelProvider {
return UserspaceTunnel( return UserspaceTunnel(
applicationScope, applicationScope,
ioDispatcher,
proxySettingsRepository, proxySettingsRepository,
settingsRepository, dnsSettingsRepository,
backend, backend,
) )
} }
@@ -133,6 +136,7 @@ class TunnelModule {
serviceManager: ServiceManager, serviceManager: ServiceManager,
tunnelRepository: TunnelRepository, tunnelRepository: TunnelRepository,
settingsRepository: GeneralSettingRepository, settingsRepository: GeneralSettingRepository,
autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
tunnelMonitor: TunnelMonitor, tunnelMonitor: TunnelMonitor,
@IoDispatcher ioDispatcher: CoroutineDispatcher, @IoDispatcher ioDispatcher: CoroutineDispatcher,
@ApplicationScope applicationScope: CoroutineScope, @ApplicationScope applicationScope: CoroutineScope,
@@ -143,6 +147,7 @@ class TunnelModule {
proxyTunnel, proxyTunnel,
serviceManager, serviceManager,
settingsRepository, settingsRepository,
autoTunnelSettingsRepository,
tunnelRepository, tunnelRepository,
tunnelMonitor, tunnelMonitor,
applicationScope, applicationScope,
@@ -154,7 +159,7 @@ class TunnelModule {
@Singleton @Singleton
fun provideNetworkMonitor( fun provideNetworkMonitor(
@ApplicationContext context: Context, @ApplicationContext context: Context,
settingsRepository: GeneralSettingRepository, autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
@ApplicationScope applicationScope: CoroutineScope, @ApplicationScope applicationScope: CoroutineScope,
@AppShell appShell: RootShell, @AppShell appShell: RootShell,
): NetworkMonitor { ): NetworkMonitor {
@@ -163,7 +168,7 @@ class TunnelModule {
object : AndroidNetworkMonitor.ConfigurationListener { object : AndroidNetworkMonitor.ConfigurationListener {
override val detectionMethod: Flow<AndroidNetworkMonitor.WifiDetectionMethod> override val detectionMethod: Flow<AndroidNetworkMonitor.WifiDetectionMethod>
get() = get() =
settingsRepository.flow autoTunnelSettingsRepository.flow
.distinctUntilChangedBy { it.wifiDetectionMethod } .distinctUntilChangedBy { it.wifiDetectionMethod }
.map { it.wifiDetectionMethod.to() } .map { it.wifiDetectionMethod.to() }
@@ -181,14 +186,14 @@ class TunnelModule {
@IoDispatcher ioDispatcher: CoroutineDispatcher, @IoDispatcher ioDispatcher: CoroutineDispatcher,
@MainDispatcher mainCoroutineDispatcher: CoroutineDispatcher, @MainDispatcher mainCoroutineDispatcher: CoroutineDispatcher,
@ApplicationScope applicationScope: CoroutineScope, @ApplicationScope applicationScope: CoroutineScope,
settingsRepository: GeneralSettingRepository, autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
): ServiceManager { ): ServiceManager {
return ServiceManager( return ServiceManager(
context, context,
ioDispatcher, ioDispatcher,
applicationScope, applicationScope,
mainCoroutineDispatcher, mainCoroutineDispatcher,
settingsRepository, autoTunnelSettingsRepository,
) )
} }
@@ -200,10 +205,12 @@ class TunnelModule {
logReader: LogReader, logReader: LogReader,
tunnelsRepository: TunnelRepository, tunnelsRepository: TunnelRepository,
settingsRepository: GeneralSettingRepository, settingsRepository: GeneralSettingRepository,
monitoringSettingsRepository: MonitoringSettingsRepository,
): TunnelMonitor { ): TunnelMonitor {
return TunnelMonitor( return TunnelMonitor(
settingsRepository, settingsRepository,
tunnelsRepository, tunnelsRepository,
monitoringSettingsRepository,
networkMonitor, networkMonitor,
networkUtils, networkUtils,
logReader, logReader,
@@ -1,13 +1,16 @@
package com.zaneschepke.wireguardautotunnel.di package com.zaneschepke.wireguardautotunnel.di
import android.content.Context import android.content.Context
import com.wireguard.android.util.RootShell
import com.zaneschepke.wireguardautotunnel.util.FileUtils import com.zaneschepke.wireguardautotunnel.util.FileUtils
import com.zaneschepke.wireguardautotunnel.util.RootShellUtils
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.components.ViewModelComponent import dagger.hilt.android.components.ViewModelComponent
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.scopes.ViewModelScoped import dagger.hilt.android.scopes.ViewModelScoped
import javax.inject.Provider
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
@Module @Module
@@ -21,4 +24,13 @@ class ViewModelModule {
): FileUtils { ): FileUtils {
return FileUtils(context, ioDispatcher) return FileUtils(context, ioDispatcher)
} }
@ViewModelScoped
@Provides
fun provideRootShellUtils(
@AppShell rootShell: Provider<RootShell>,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): RootShellUtils {
return RootShellUtils(rootShell, ioDispatcher)
}
} }
@@ -0,0 +1,8 @@
package com.zaneschepke.wireguardautotunnel.domain.enums
enum class NetworkType {
WIFI,
ETHERNET,
MOBILE_DATA,
NONE,
}
@@ -1,9 +1,9 @@
package com.zaneschepke.wireguardautotunnel.domain.events package com.zaneschepke.wireguardautotunnel.domain.events
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
sealed class AutoTunnelEvent { sealed class AutoTunnelEvent {
data class Start(val tunnelConf: TunnelConf? = null) : AutoTunnelEvent() data class Start(val tunnelConfig: TunnelConfig? = null) : AutoTunnelEvent()
data object Stop : AutoTunnelEvent() data object Stop : AutoTunnelEvent()
@@ -22,8 +22,6 @@ sealed class BackendCoreException : Exception() {
data object UapiUpdateFailed : BackendCoreException() data object UapiUpdateFailed : BackendCoreException()
data class BounceFailed(val error: BackendCoreException) : BackendCoreException()
fun toStringRes() = fun toStringRes() =
when (this) { when (this) {
Config -> R.string.config_error Config -> R.string.config_error
@@ -34,18 +32,10 @@ sealed class BackendCoreException : Exception() {
ServiceNotRunning -> R.string.service_running_error ServiceNotRunning -> R.string.service_running_error
Unknown -> R.string.unknown_error Unknown -> R.string.unknown_error
TunnelNameTooLong -> R.string.error_tunnel_name TunnelNameTooLong -> R.string.error_tunnel_name
is BounceFailed -> R.string.bounce_failed_template
UapiUpdateFailed -> R.string.active_tunnel_update_failed UapiUpdateFailed -> R.string.active_tunnel_update_failed
} }
fun toStringValue(): StringValue { fun toStringValue(): StringValue {
return when (val backendError = this) { return StringValue.StringResource(toStringRes())
is BounceFailed ->
StringValue.StringResource(
backendError.toStringRes(),
backendError.error.toStringRes(),
)
else -> StringValue.StringResource(backendError.toStringRes())
}
} }
} }
@@ -5,14 +5,11 @@ import com.zaneschepke.wireguardautotunnel.util.StringValue
sealed class BackendMessage { sealed class BackendMessage {
data object BounceSuccess : BackendMessage() data object DynamicDnsSuccess : BackendMessage()
data object BounceRecovery : BackendMessage()
fun toStringRes() = fun toStringRes() =
when (this) { when (this) {
BounceRecovery -> R.string.pinger_bounce_recovery DynamicDnsSuccess -> R.string.ddns_success_message
BounceSuccess -> R.string.pinger_bounce_successful
} }
fun toStringValue() = StringValue.StringResource(this.toStringRes()) fun toStringValue() = StringValue.StringResource(this.toStringRes())
@@ -1,16 +1,6 @@
package com.zaneschepke.wireguardautotunnel.domain.model package com.zaneschepke.wireguardautotunnel.domain.model
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
data class AppState( data class AppState(
val isLocationDisclosureShown: Boolean = false, val isLocationDisclosureShown: Boolean = false,
val isBatteryOptimizationDisableShown: Boolean = false, val isBatteryOptimizationDisableShown: Boolean = false,
val isPinLockEnabled: Boolean = false,
val expandedTunnelIds: List<Int> = emptyList(),
val isLocalLogsEnabled: Boolean = false,
val isRemoteControlEnabled: Boolean = false,
val showDetailedPingStats: Boolean = false,
val remoteKey: String? = null,
val locale: String? = null,
val theme: Theme = Theme.AUTOMATIC,
) )
@@ -0,0 +1,18 @@
package com.zaneschepke.wireguardautotunnel.domain.model
import com.zaneschepke.wireguardautotunnel.data.model.WifiDetectionMethod
data class AutoTunnelSettings(
val id: Int = 0,
val isAutoTunnelEnabled: Boolean = false,
val isTunnelOnMobileDataEnabled: Boolean = false,
val trustedNetworkSSIDs: Set<String> = emptySet(),
val isTunnelOnEthernetEnabled: Boolean = false,
val isTunnelOnWifiEnabled: Boolean = false,
val isWildcardsEnabled: Boolean = false,
val isStopOnNoInternetEnabled: Boolean = false,
val debounceDelaySeconds: Int = 3,
val isTunnelOnUnsecureEnabled: Boolean = false,
val wifiDetectionMethod: WifiDetectionMethod = WifiDetectionMethod.fromValue(0),
val startOnBoot: Boolean = false,
)
@@ -0,0 +1,9 @@
package com.zaneschepke.wireguardautotunnel.domain.model
import com.zaneschepke.wireguardautotunnel.data.model.DnsProtocol
data class DnsSettings(
val id: Int = 0,
val dnsProtocol: DnsProtocol = DnsProtocol.fromValue(0),
val dnsEndpoint: String? = null,
)
@@ -1,51 +1,21 @@
package com.zaneschepke.wireguardautotunnel.domain.model package com.zaneschepke.wireguardautotunnel.domain.model
import com.zaneschepke.wireguardautotunnel.data.model.AppMode import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.data.model.DnsProtocol import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
import com.zaneschepke.wireguardautotunnel.data.model.WifiDetectionMethod
data class GeneralSettings( data class GeneralSettings(
val id: Int = 0, val id: Int = 0,
val isAutoTunnelEnabled: Boolean = false,
val isTunnelOnMobileDataEnabled: Boolean = false,
val trustedNetworkSSIDs: Set<String> = emptySet(),
val isAlwaysOnVpnEnabled: Boolean = false,
val isTunnelOnEthernetEnabled: Boolean = false,
val isShortcutsEnabled: Boolean = false, val isShortcutsEnabled: Boolean = false,
val isTunnelOnWifiEnabled: Boolean = false,
val isRestoreOnBootEnabled: Boolean = false, val isRestoreOnBootEnabled: Boolean = false,
val isMultiTunnelEnabled: Boolean = false, val isMultiTunnelEnabled: Boolean = false,
val isPingEnabled: Boolean = false, val isTunnelGlobalsEnabled: Boolean = false,
val isWildcardsEnabled: Boolean = false, val appMode: AppMode = AppMode.fromValue(0),
val isStopOnNoInternetEnabled: Boolean = false, val theme: Theme = Theme.AUTOMATIC,
val isVpnKillSwitchEnabled: Boolean = false, val locale: String? = null,
val isKernelKillSwitchEnabled: Boolean = false, val remoteKey: String? = null,
val isRemoteControlEnabled: Boolean = false,
val isPinLockEnabled: Boolean = false,
val isAlwaysOnVpnEnabled: Boolean = false,
val isLanOnKillSwitchEnabled: Boolean = false, val isLanOnKillSwitchEnabled: Boolean = false,
val debounceDelaySeconds: Int = 3, val customSplitPackages: Map<String, String> = emptyMap(),
val isDisableKillSwitchOnTrustedEnabled: Boolean = false, )
val isTunnelOnUnsecureEnabled: Boolean = false,
val wifiDetectionMethod: WifiDetectionMethod = WifiDetectionMethod.DEFAULT,
val tunnelPingIntervalSeconds: Int = PING_INTERVAL_DEFAULT,
val tunnelPingAttempts: Int = PING_ATTEMPTS_DEFAULT,
val tunnelPingTimeoutSeconds: Int? = null,
val appMode: AppMode = AppMode.VPN,
val dnsProtocol: DnsProtocol = DnsProtocol.SYSTEM,
val dnsEndpoint: String? = null,
) {
fun toAutoTunnelStateString(): String {
return """
TunnelOnWifi: $isTunnelOnWifiEnabled
TunnelOnMobileData: $isTunnelOnMobileDataEnabled
TunnelOnEthernet: $isTunnelOnEthernetEnabled
Wildcards: $isWildcardsEnabled
StopOnNoInternet: $isStopOnNoInternetEnabled
Trusted Networks: $trustedNetworkSSIDs
"""
.trimIndent()
}
companion object {
const val PING_INTERVAL_DEFAULT = 30
const val PING_ATTEMPTS_DEFAULT = 3
}
}
@@ -0,0 +1,12 @@
package com.zaneschepke.wireguardautotunnel.domain.model
data class MonitoringSettings(
val id: Int = 0,
val isPingEnabled: Boolean = false,
val isPingMonitoringEnabled: Boolean = true,
val tunnelPingIntervalSeconds: Int = 30,
val tunnelPingAttempts: Int = 3,
val tunnelPingTimeoutSeconds: Int? = null,
val showDetailedPingStats: Boolean = false,
val isLocalLogsEnabled: Boolean = false,
)
@@ -1,6 +1,6 @@
package com.zaneschepke.wireguardautotunnel.domain.model package com.zaneschepke.wireguardautotunnel.domain.model
data class AppProxySettings( data class ProxySettings(
val id: Long = 0, val id: Long = 0,
val socks5ProxyEnabled: Boolean = false, val socks5ProxyEnabled: Boolean = false,
val socks5ProxyBindAddress: String? = null, val socks5ProxyBindAddress: String? = null,
@@ -1,137 +0,0 @@
package com.zaneschepke.wireguardautotunnel.domain.model
import com.wireguard.config.Config
import com.zaneschepke.wireguardautotunnel.util.extensions.defaultName
import com.zaneschepke.wireguardautotunnel.util.extensions.isValidIpv4orIpv6Address
import java.io.InputStream
import java.nio.charset.StandardCharsets
data class TunnelConf(
val id: Int = 0,
val tunName: String,
val wgQuick: String,
val tunnelNetworks: Set<String> = emptySet(),
val isMobileDataTunnel: Boolean = false,
val isPrimaryTunnel: Boolean = false,
val amQuick: String,
val isActive: Boolean = false,
val pingTarget: String? = null,
val restartOnPingFailure: Boolean = false,
val isEthernetTunnel: Boolean = false,
val isIpv4Preferred: Boolean = true,
val position: Int = 0,
) {
val isNameKernelCompatible: Boolean = (tunName.length <= 15)
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is TunnelConf) return false
return id == other.id &&
tunName == other.tunName &&
wgQuick == other.wgQuick &&
amQuick == other.amQuick &&
isPrimaryTunnel == other.isPrimaryTunnel &&
isMobileDataTunnel == other.isMobileDataTunnel &&
isEthernetTunnel == other.isEthernetTunnel &&
pingTarget == other.pingTarget &&
restartOnPingFailure == other.restartOnPingFailure &&
tunnelNetworks == other.tunnelNetworks &&
isIpv4Preferred == other.isIpv4Preferred
}
override fun hashCode(): Int {
var result = id
result = 31 * result + tunName.hashCode()
result = 31 * result + wgQuick.hashCode()
result = 31 * result + amQuick.hashCode()
return result
}
fun isStaticallyConfigured(): Boolean {
return toAmConfig().peers.all { it.endpoint.get().host.isValidIpv4orIpv6Address() }
}
fun toAmConfig(): org.amnezia.awg.config.Config {
return configFromAmQuick(amQuick.ifBlank { wgQuick })
}
fun toWgConfig(): Config {
return configFromWgQuick(wgQuick)
}
companion object {
fun configFromWgQuick(wgQuick: String): Config {
val inputStream: InputStream = wgQuick.byteInputStream()
return inputStream.bufferedReader(StandardCharsets.UTF_8).use { Config.parse(it) }
}
fun configFromAmQuick(amQuick: String): org.amnezia.awg.config.Config {
val inputStream: InputStream = amQuick.byteInputStream()
return inputStream.bufferedReader(StandardCharsets.UTF_8).use {
org.amnezia.awg.config.Config.parse(it)
}
}
fun tunnelConfFromQuick(amQuick: String, name: String? = null): TunnelConf {
val config = configFromAmQuick(amQuick)
val wgQuick = config.toWgQuickString(true)
return TunnelConf(
tunName = name ?: config.defaultName(),
wgQuick = wgQuick,
amQuick = amQuick,
)
}
private fun tunnelConfFromAmConfig(
config: org.amnezia.awg.config.Config,
name: String? = null,
): TunnelConf {
val amQuick = config.toAwgQuickString(true, false)
val wgQuick = config.toWgQuickString(true)
return TunnelConf(
tunName = name ?: config.defaultName(),
wgQuick = wgQuick,
amQuick = amQuick,
)
}
private const val IPV6_ALL_NETWORKS = "::/0"
private const val IPV4_ALL_NETWORKS = "0.0.0.0/0"
val ALL_IPS = listOf(IPV4_ALL_NETWORKS, IPV6_ALL_NETWORKS)
val IPV4_PUBLIC_NETWORKS =
setOf(
"0.0.0.0/5",
"8.0.0.0/7",
"11.0.0.0/8",
"12.0.0.0/6",
"16.0.0.0/4",
"32.0.0.0/3",
"64.0.0.0/2",
"128.0.0.0/3",
"160.0.0.0/5",
"168.0.0.0/6",
"172.0.0.0/12",
"172.32.0.0/11",
"172.64.0.0/10",
"172.128.0.0/9",
"173.0.0.0/8",
"174.0.0.0/7",
"176.0.0.0/4",
"192.0.0.0/9",
"192.128.0.0/11",
"192.160.0.0/13",
"192.169.0.0/16",
"192.170.0.0/15",
"192.172.0.0/14",
"192.176.0.0/12",
"192.192.0.0/10",
"193.0.0.0/8",
"194.0.0.0/7",
"196.0.0.0/6",
"200.0.0.0/5",
"208.0.0.0/4",
)
val LAN_BYPASS_ALLOWED_IPS = setOf(IPV6_ALL_NETWORKS) + IPV4_PUBLIC_NETWORKS
}
}
@@ -0,0 +1,295 @@
package com.zaneschepke.wireguardautotunnel.domain.model
import com.wireguard.config.Config
import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig.Companion.GLOBAL_CONFIG_NAME
import com.zaneschepke.wireguardautotunnel.util.extensions.defaultName
import com.zaneschepke.wireguardautotunnel.util.extensions.isValidIpv4orIpv6Address
import java.io.InputStream
import java.nio.charset.StandardCharsets
import org.amnezia.awg.config.InetEndpoint
import org.amnezia.awg.config.InetNetwork
import org.amnezia.awg.config.Interface
import org.amnezia.awg.config.Peer
import org.amnezia.awg.crypto.KeyPair
data class TunnelConfig(
val id: Int = 0,
val name: String,
val wgQuick: String,
val tunnelNetworks: Set<String> = setOf(),
val isMobileDataTunnel: Boolean = false,
val isPrimaryTunnel: Boolean = false,
val amQuick: String = "",
val isActive: Boolean = false,
val restartOnPingFailure: Boolean = false,
var pingTarget: String? = null,
val isEthernetTunnel: Boolean = false,
val isIpv4Preferred: Boolean = true,
val position: Int = 0,
val autoTunnelApps: Set<String> = setOf(),
) {
val isNameKernelCompatible: Boolean = (name.length <= 15)
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is TunnelConfig) return false
return id == other.id &&
name == other.name &&
wgQuick == other.wgQuick &&
amQuick == other.amQuick &&
isPrimaryTunnel == other.isPrimaryTunnel &&
isMobileDataTunnel == other.isMobileDataTunnel &&
isEthernetTunnel == other.isEthernetTunnel &&
pingTarget == other.pingTarget &&
restartOnPingFailure == other.restartOnPingFailure &&
tunnelNetworks == other.tunnelNetworks &&
isIpv4Preferred == other.isIpv4Preferred
}
override fun hashCode(): Int {
var result = id
result = 31 * result + name.hashCode()
result = 31 * result + wgQuick.hashCode()
result = 31 * result + amQuick.hashCode()
return result
}
fun isStaticallyConfigured(): Boolean {
return toAmConfig().peers.all { it.endpoint.get().host.isValidIpv4orIpv6Address() }
}
fun toAmConfig(): org.amnezia.awg.config.Config {
return configFromAmQuick(amQuick.ifBlank { wgQuick })
}
fun toWgConfig(): Config {
return configFromWgQuick(wgQuick)
}
fun copyWithGlobalValues(globalTunnel: TunnelConfig): TunnelConfig {
val existingConfig = toAmConfig()
val globalConfig = globalTunnel.toAmConfig()
val newInterfaceBuilder =
Interface.Builder().apply {
setKeyPair(existingConfig.`interface`.keyPair)
setAddresses(existingConfig.`interface`.addresses)
setDnsServers(existingConfig.`interface`.dnsServers)
setDnsSearchDomains(existingConfig.`interface`.dnsSearchDomains)
setExcludedApplications(existingConfig.`interface`.excludedApplications)
setIncludedApplications(existingConfig.`interface`.includedApplications)
existingConfig.`interface`.listenPort.ifPresent { setListenPort(it) }
existingConfig.`interface`.mtu.ifPresent { setMtu(it) }
existingConfig.`interface`.junkPacketCount.ifPresent { setJunkPacketCount(it) }
existingConfig.`interface`.junkPacketMinSize.ifPresent { setJunkPacketMinSize(it) }
existingConfig.`interface`.junkPacketMaxSize.ifPresent { setJunkPacketMaxSize(it) }
existingConfig.`interface`.initPacketJunkSize.ifPresent {
setInitPacketJunkSize(it)
}
existingConfig.`interface`.responsePacketJunkSize.ifPresent {
setResponsePacketJunkSize(it)
}
existingConfig.`interface`.initPacketMagicHeader.ifPresent {
setInitPacketMagicHeader(it)
}
existingConfig.`interface`.responsePacketMagicHeader.ifPresent {
setResponsePacketMagicHeader(it)
}
existingConfig.`interface`.underloadPacketMagicHeader.ifPresent {
setUnderloadPacketMagicHeader(it)
}
existingConfig.`interface`.transportPacketMagicHeader.ifPresent {
setTransportPacketMagicHeader(it)
}
existingConfig.`interface`.i1.ifPresent { setI1(it) }
existingConfig.`interface`.i2.ifPresent { setI2(it) }
existingConfig.`interface`.i3.ifPresent { setI3(it) }
existingConfig.`interface`.i4.ifPresent { setI4(it) }
existingConfig.`interface`.i5.ifPresent { setI5(it) }
existingConfig.`interface`.j1.ifPresent { setJ1(it) }
existingConfig.`interface`.j2.ifPresent { setJ2(it) }
existingConfig.`interface`.j3.ifPresent { setJ3(it) }
existingConfig.`interface`.itime.ifPresent { setItime(it) }
setPreUp(existingConfig.`interface`.preUp)
setPostUp(existingConfig.`interface`.postUp)
setPreDown(existingConfig.`interface`.preDown)
setPostDown(existingConfig.`interface`.postDown)
globalConfig.`interface`.mtu.ifPresent { setMtu(it) }
if (globalConfig.`interface`.dnsServers.isNotEmpty()) {
setDnsServers(globalConfig.`interface`.dnsServers)
}
if (globalConfig.`interface`.dnsSearchDomains.isNotEmpty()) {
setDnsSearchDomains(globalConfig.`interface`.dnsSearchDomains)
}
if (globalConfig.`interface`.excludedApplications.isNotEmpty()) {
setExcludedApplications(globalConfig.`interface`.excludedApplications)
}
if (!globalConfig.`interface`.includedApplications.isEmpty()) {
setIncludedApplications(globalConfig.`interface`.includedApplications)
}
if (globalConfig.`interface`.preUp.isNotEmpty()) {
setPreUp(globalConfig.`interface`.preUp)
}
if (globalConfig.`interface`.postUp.isNotEmpty()) {
setPostUp(globalConfig.`interface`.postUp)
}
if (globalConfig.`interface`.preDown.isNotEmpty()) {
setPreDown(globalConfig.`interface`.preDown)
}
if (globalConfig.`interface`.postDown.isNotEmpty()) {
setPostDown(globalConfig.`interface`.postDown)
}
globalConfig.`interface`.junkPacketCount.ifPresent { setJunkPacketCount(it) }
globalConfig.`interface`.junkPacketMinSize.ifPresent { setJunkPacketMinSize(it) }
globalConfig.`interface`.junkPacketMaxSize.ifPresent { setJunkPacketMaxSize(it) }
globalConfig.`interface`.initPacketJunkSize.ifPresent { setInitPacketJunkSize(it) }
globalConfig.`interface`.responsePacketJunkSize.ifPresent {
setResponsePacketJunkSize(it)
}
globalConfig.`interface`.initPacketMagicHeader.ifPresent {
setInitPacketMagicHeader(it)
}
globalConfig.`interface`.responsePacketMagicHeader.ifPresent {
setResponsePacketMagicHeader(it)
}
globalConfig.`interface`.underloadPacketMagicHeader.ifPresent {
setUnderloadPacketMagicHeader(it)
}
globalConfig.`interface`.transportPacketMagicHeader.ifPresent {
setTransportPacketMagicHeader(it)
}
globalConfig.`interface`.i1.ifPresent { setI1(it) }
globalConfig.`interface`.i2.ifPresent { setI2(it) }
globalConfig.`interface`.i3.ifPresent { setI3(it) }
globalConfig.`interface`.i4.ifPresent { setI4(it) }
globalConfig.`interface`.i5.ifPresent { setI5(it) }
globalConfig.`interface`.j1.ifPresent { setJ1(it) }
globalConfig.`interface`.j2.ifPresent { setJ2(it) }
globalConfig.`interface`.j3.ifPresent { setJ3(it) }
globalConfig.`interface`.itime.ifPresent { setItime(it) }
}
val newInterface = newInterfaceBuilder.build()
val newConfigBuilder =
org.amnezia.awg.config.Config.Builder().apply {
setInterface(newInterface)
addPeers(existingConfig.peers)
}
val newAmConfig = newConfigBuilder.build()
return copy(
wgQuick = newAmConfig.toWgQuickString(true),
amQuick = newAmConfig.toAwgQuickString(true, false),
)
}
companion object {
fun configFromWgQuick(wgQuick: String): Config {
val inputStream: InputStream = wgQuick.byteInputStream()
return inputStream.bufferedReader(StandardCharsets.UTF_8).use { Config.parse(it) }
}
fun configFromAmQuick(amQuick: String): org.amnezia.awg.config.Config {
val inputStream: InputStream = amQuick.byteInputStream()
return inputStream.bufferedReader(StandardCharsets.UTF_8).use {
org.amnezia.awg.config.Config.parse(it)
}
}
fun tunnelConfFromQuick(amQuick: String, name: String? = null): TunnelConfig {
val config = configFromAmQuick(amQuick)
val wgQuick = config.toWgQuickString(true)
return TunnelConfig(
name = name ?: config.defaultName(),
wgQuick = wgQuick,
amQuick = amQuick,
)
}
private fun tunnelConfFromAmConfig(
config: org.amnezia.awg.config.Config,
name: String? = null,
): TunnelConfig {
val amQuick = config.toAwgQuickString(true, false)
val wgQuick = config.toWgQuickString(true)
return TunnelConfig(
name = name ?: config.defaultName(),
wgQuick = wgQuick,
amQuick = amQuick,
)
}
fun generateDefaultGlobalConfig(): TunnelConfig {
val keyPair = KeyPair()
val config =
org.amnezia.awg.config.Config.Builder()
.apply {
setInterface(
Interface.Builder()
.apply {
setKeyPair(keyPair)
parseAddresses("10.0.0.2/32")
}
.build()
)
addPeer(
Peer.Builder()
.apply {
setPublicKey(keyPair.publicKey)
addAllowedIps(listOf(InetNetwork.parse("0.0.0.0/0")))
setEndpoint(InetEndpoint.parse("server.example.com:51820"))
}
.build()
)
}
.build()
return TunnelConfig(
name = GLOBAL_CONFIG_NAME,
amQuick = config.toAwgQuickString(false, false),
wgQuick = config.toWgQuickString(false),
)
}
private const val IPV6_ALL_NETWORKS = "::/0"
private const val IPV4_ALL_NETWORKS = "0.0.0.0/0"
val ALL_IPS = listOf(IPV4_ALL_NETWORKS, IPV6_ALL_NETWORKS)
val IPV4_PUBLIC_NETWORKS =
setOf(
"0.0.0.0/5",
"8.0.0.0/7",
"11.0.0.0/8",
"12.0.0.0/6",
"16.0.0.0/4",
"32.0.0.0/3",
"64.0.0.0/2",
"128.0.0.0/3",
"160.0.0.0/5",
"168.0.0.0/6",
"172.0.0.0/12",
"172.32.0.0/11",
"172.64.0.0/10",
"172.128.0.0/9",
"173.0.0.0/8",
"174.0.0.0/7",
"176.0.0.0/4",
"192.0.0.0/9",
"192.128.0.0/11",
"192.160.0.0/13",
"192.169.0.0/16",
"192.170.0.0/15",
"192.172.0.0/14",
"192.176.0.0/12",
"192.192.0.0/10",
"193.0.0.0/8",
"194.0.0.0/7",
"196.0.0.0/6",
"200.0.0.0/5",
"208.0.0.0/4",
)
val LAN_BYPASS_ALLOWED_IPS = setOf(IPV6_ALL_NETWORKS) + IPV4_PUBLIC_NETWORKS
}
}
@@ -1,7 +1,6 @@
package com.zaneschepke.wireguardautotunnel.domain.repository package com.zaneschepke.wireguardautotunnel.domain.repository
import com.zaneschepke.wireguardautotunnel.domain.model.AppState import com.zaneschepke.wireguardautotunnel.domain.model.AppState
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface AppStateRepository { interface AppStateRepository {
@@ -9,41 +8,9 @@ interface AppStateRepository {
suspend fun setLocationDisclosureShown(shown: Boolean) suspend fun setLocationDisclosureShown(shown: Boolean)
suspend fun isPinLockEnabled(): Boolean
suspend fun setPinLockEnabled(enabled: Boolean)
suspend fun isBatteryOptimizationDisableShown(): Boolean suspend fun isBatteryOptimizationDisableShown(): Boolean
suspend fun setBatteryOptimizationDisableShown(shown: Boolean) suspend fun setBatteryOptimizationDisableShown(shown: Boolean)
suspend fun setTunnelExpanded(id: Int)
suspend fun removeTunnelExpanded(id: Int)
suspend fun setTheme(theme: Theme)
suspend fun getTheme(): Theme
suspend fun isLocalLogsEnabled(): Boolean
suspend fun setLocalLogsEnabled(enabled: Boolean)
suspend fun setLocale(localeTag: String)
suspend fun getLocale(): String?
suspend fun setIsRemoteControlEnabled(enabled: Boolean)
suspend fun isRemoteControlEnabled(): Boolean
suspend fun setRemoteKey(key: String)
suspend fun getRemoteKey(): String?
suspend fun setShowDetailedPingStats(showDetailedPing: Boolean)
suspend fun getShowDetailedPing(): Boolean
val flow: Flow<AppState> val flow: Flow<AppState>
} }
@@ -0,0 +1,14 @@
package com.zaneschepke.wireguardautotunnel.domain.repository
import com.zaneschepke.wireguardautotunnel.domain.model.AutoTunnelSettings
import kotlinx.coroutines.flow.Flow
interface AutoTunnelSettingsRepository {
suspend fun upsert(autoTunnelSettings: AutoTunnelSettings)
val flow: Flow<AutoTunnelSettings>
suspend fun getAutoTunnelSettings(): AutoTunnelSettings
suspend fun updateAutoTunnelEnabled(enabled: Boolean)
}
@@ -0,0 +1,12 @@
package com.zaneschepke.wireguardautotunnel.domain.repository
import com.zaneschepke.wireguardautotunnel.domain.model.DnsSettings
import kotlinx.coroutines.flow.Flow
interface DnsSettingsRepository {
suspend fun upsert(dnsSettings: DnsSettings)
val flow: Flow<DnsSettings>
suspend fun getDnsSettings(): DnsSettings
}
@@ -4,11 +4,9 @@ import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface GeneralSettingRepository { interface GeneralSettingRepository {
suspend fun save(generalSettings: GeneralSettings) suspend fun upsert(generalSettings: GeneralSettings)
val flow: Flow<GeneralSettings> val flow: Flow<GeneralSettings>
suspend fun get(): GeneralSettings suspend fun getGeneralSettings(): GeneralSettings
suspend fun updateAutoTunnelEnabled(enabled: Boolean)
} }
@@ -0,0 +1,12 @@
package com.zaneschepke.wireguardautotunnel.domain.repository
import com.zaneschepke.wireguardautotunnel.domain.model.MonitoringSettings
import kotlinx.coroutines.flow.Flow
interface MonitoringSettingsRepository {
suspend fun upsert(monitoringSettings: MonitoringSettings)
val flow: Flow<MonitoringSettings>
suspend fun getMonitoringSettings(): MonitoringSettings
}
@@ -1,12 +1,12 @@
package com.zaneschepke.wireguardautotunnel.domain.repository package com.zaneschepke.wireguardautotunnel.domain.repository
import com.zaneschepke.wireguardautotunnel.domain.model.AppProxySettings import com.zaneschepke.wireguardautotunnel.domain.model.ProxySettings
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface ProxySettingsRepository { interface ProxySettingsRepository {
suspend fun save(proxySettings: AppProxySettings) suspend fun upsert(proxySettings: ProxySettings)
val flow: Flow<AppProxySettings> val flow: Flow<ProxySettings>
suspend fun get(): AppProxySettings suspend fun getProxySettings(): ProxySettings
} }
@@ -1,45 +1,48 @@
package com.zaneschepke.wireguardautotunnel.domain.repository package com.zaneschepke.wireguardautotunnel.domain.repository
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.util.extensions.Tunnels
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface TunnelRepository { interface TunnelRepository {
val flow: Flow<List<TunnelConf>> val flow: Flow<List<TunnelConfig>>
suspend fun getAll(): Tunnels val userTunnelsFlow: Flow<List<TunnelConfig>>
suspend fun save(tunnelConf: TunnelConf) val globalTunnelFlow: Flow<TunnelConfig?>
suspend fun saveAll(tunnelConfList: List<TunnelConf>) suspend fun getAll(): List<TunnelConfig>
suspend fun updatePrimaryTunnel(tunnelConf: TunnelConf?) suspend fun save(tunnelConfig: TunnelConfig)
suspend fun saveAll(tunnelConfigList: List<TunnelConfig>)
suspend fun updatePrimaryTunnel(tunnelConfig: TunnelConfig?)
suspend fun resetActiveTunnels() suspend fun resetActiveTunnels()
suspend fun updateMobileDataTunnel(tunnelConf: TunnelConf?) suspend fun updateMobileDataTunnel(tunnelConfig: TunnelConfig?)
suspend fun updateEthernetTunnel(tunnelConf: TunnelConf?) suspend fun updateEthernetTunnel(tunnelConfig: TunnelConfig?)
suspend fun delete(tunnelConf: TunnelConf) suspend fun delete(tunnelConfig: TunnelConfig)
suspend fun getById(id: Int): TunnelConf? suspend fun getById(id: Int): TunnelConfig?
suspend fun getActive(): Tunnels suspend fun getActive(): List<TunnelConfig>
suspend fun getDefaultTunnel(): TunnelConf? suspend fun getDefaultTunnel(): TunnelConfig?
suspend fun getStartTunnel(): TunnelConf? suspend fun getStartTunnel(): TunnelConfig?
suspend fun count(): Int suspend fun count(): Int
suspend fun findByTunnelName(name: String): TunnelConf? suspend fun findByTunnelName(name: String): TunnelConfig?
suspend fun findByTunnelNetworksName(name: String): Tunnels suspend fun findByTunnelNetworksName(name: String): List<TunnelConfig>
suspend fun findByMobileDataTunnel(): Tunnels suspend fun findByMobileDataTunnel(): List<TunnelConfig>
suspend fun findPrimary(): Tunnels suspend fun findPrimary(): List<TunnelConfig>
suspend fun delete(tunnels: List<TunnelConf>) suspend fun delete(tunnels: List<TunnelConfig>)
} }
@@ -1,12 +1,11 @@
package com.zaneschepke.wireguardautotunnel.domain.sideeffect package com.zaneschepke.wireguardautotunnel.domain.sideeffect
import com.zaneschepke.wireguardautotunnel.data.model.AppMode import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.util.StringValue import com.zaneschepke.wireguardautotunnel.util.StringValue
import java.io.File import java.io.File
sealed class GlobalSideEffect { sealed class GlobalSideEffect {
data object RequestBatteryOptimizationDisabled : GlobalSideEffect()
data class Snackbar(val message: StringValue) : GlobalSideEffect() data class Snackbar(val message: StringValue) : GlobalSideEffect()
@@ -18,7 +17,7 @@ sealed class GlobalSideEffect {
data object ConfigChanged : GlobalSideEffect() data object ConfigChanged : GlobalSideEffect()
data class RequestVpnPermission(val requestingMode: AppMode, val config: TunnelConf?) : data class RequestVpnPermission(val requestingMode: AppMode, val config: TunnelConfig?) :
GlobalSideEffect() GlobalSideEffect()
data class InstallApk(val apk: File) : GlobalSideEffect() data class InstallApk(val apk: File) : GlobalSideEffect()
@@ -4,8 +4,8 @@ import org.amnezia.awg.backend.Statistics
import org.amnezia.awg.crypto.Key import org.amnezia.awg.crypto.Key
class AmneziaStatistics(private val statistics: Statistics) : TunnelStatistics() { class AmneziaStatistics(private val statistics: Statistics) : TunnelStatistics() {
override fun peerStats(peer: Key): PeerStats? { override fun peerStats(peerBase64: String): PeerStats? {
val key = Key.fromBase64(peer.toBase64()) val key = Key.fromBase64(peerBase64)
val stats = statistics.peer(key) val stats = statistics.peer(key)
return stats?.let { return stats?.let {
PeerStats( PeerStats(
@@ -21,8 +21,8 @@ class AmneziaStatistics(private val statistics: Statistics) : TunnelStatistics()
return statistics.isStale return statistics.isStale
} }
override fun getPeers(): Array<Key> { override fun getPeers(): Array<String> {
return statistics.peers() return statistics.peers().map { it.toBase64() }.toTypedArray()
} }
override fun rx(): Long { override fun rx(): Long {
@@ -1,26 +1,31 @@
package com.zaneschepke.wireguardautotunnel.domain.state package com.zaneschepke.wireguardautotunnel.domain.state
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.ActiveTunnelsChange
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.NetworkChange
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.SettingsChange
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.StateChange import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.StateChange
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent.DoNothing import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent.DoNothing
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent.Start import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent.Start
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings import com.zaneschepke.wireguardautotunnel.domain.model.AutoTunnelSettings
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.util.extensions.isMatchingToWildcardList import com.zaneschepke.wireguardautotunnel.util.extensions.isMatchingToWildcardList
data class AutoTunnelState( data class AutoTunnelState(
val activeTunnels: Map<Int, TunnelState> = emptyMap(), val activeTunnels: Map<Int, TunnelState> = emptyMap(),
val networkState: NetworkState = NetworkState(), val networkState: NetworkState = NetworkState(),
val settings: GeneralSettings = GeneralSettings(), val settings: AutoTunnelSettings = AutoTunnelSettings(),
val tunnels: List<TunnelConf> = emptyList(), val appMode: AppMode = AppMode.VPN,
val tunnels: List<TunnelConfig> = emptyList(),
) { ) {
fun determineAutoTunnelEvent(stateChange: StateChange): AutoTunnelEvent { fun determineAutoTunnelEvent(stateChange: StateChange): AutoTunnelEvent {
when (stateChange) { when (stateChange) {
is StateChange.NetworkChange, is NetworkChange,
is StateChange.SettingsChange -> { is SettingsChange -> {
// Compute desired tunnel based on network conditions // Compute desired tunnel based on network conditions
var desiredTunnel: TunnelConf? = null var desiredTunnel: TunnelConfig? = null
if (networkState.isEthernetConnected && settings.isTunnelOnEthernetEnabled) { if (networkState.isEthernetConnected && settings.isTunnelOnEthernetEnabled) {
desiredTunnel = preferredEthernetTunnel() desiredTunnel = preferredEthernetTunnel()
} else if (isMobileDataActive() && settings.isTunnelOnMobileDataEnabled) { } else if (isMobileDataActive() && settings.isTunnelOnMobileDataEnabled) {
@@ -42,21 +47,16 @@ data class AutoTunnelState(
// Handle tunnel start/stop/change // Handle tunnel start/stop/change
if (desiredTunnel != null) { if (desiredTunnel != null) {
if (currentTunnel != desiredTunnel.id) { if (currentTunnel != desiredTunnel.id) {
// Start or switch to the desired tunnel (overrides any kill switch)
return Start(desiredTunnel) return Start(desiredTunnel)
} }
// If already active and matching, fall through to kill switch check (though
// unlikely needed)
} else { } else {
if (currentTunnel != null) { if (currentTunnel != null) {
// Stop the active tunnel (then next emission can handle kill switch if
// needed)
return AutoTunnelEvent.Stop return AutoTunnelEvent.Stop
} }
} }
} }
is StateChange.ActiveTunnelsChange -> Unit is ActiveTunnelsChange -> Unit
} }
return DoNothing return DoNothing
} }
@@ -68,19 +68,19 @@ data class AutoTunnelState(
networkState.isMobileDataConnected networkState.isMobileDataConnected
} }
private fun preferredMobileDataTunnel(): TunnelConf? { private fun preferredMobileDataTunnel(): TunnelConfig? {
return tunnels.firstOrNull { it.isMobileDataTunnel } return tunnels.firstOrNull { it.isMobileDataTunnel }
?: tunnels.firstOrNull { it.isPrimaryTunnel } ?: tunnels.firstOrNull { it.isPrimaryTunnel }
?: tunnels.firstOrNull() ?: tunnels.firstOrNull()
} }
private fun preferredEthernetTunnel(): TunnelConf? { private fun preferredEthernetTunnel(): TunnelConfig? {
return tunnels.firstOrNull { it.isEthernetTunnel } return tunnels.firstOrNull { it.isEthernetTunnel }
?: tunnels.firstOrNull { it.isPrimaryTunnel } ?: tunnels.firstOrNull { it.isPrimaryTunnel }
?: tunnels.firstOrNull() ?: tunnels.firstOrNull()
} }
private fun preferredWifiTunnel(): TunnelConf? { private fun preferredWifiTunnel(): TunnelConfig? {
return getTunnelWithMatchingTunnelNetwork() return getTunnelWithMatchingTunnelNetwork()
?: tunnels.firstOrNull { it.isPrimaryTunnel } ?: tunnels.firstOrNull { it.isPrimaryTunnel }
?: tunnels.firstOrNull() ?: tunnels.firstOrNull()
@@ -112,7 +112,7 @@ data class AutoTunnelState(
} }
} }
private fun getTunnelWithMatchingTunnelNetwork(): TunnelConf? { private fun getTunnelWithMatchingTunnelNetwork(): TunnelConfig? {
return networkState.wifiName?.let { wifiName -> return networkState.wifiName?.let { wifiName ->
tunnels.firstOrNull { hasTrustedWifiName(wifiName, it.tunnelNetworks) } tunnels.firstOrNull { hasTrustedWifiName(wifiName, it.tunnelNetworks) }
} }
@@ -2,13 +2,12 @@ package com.zaneschepke.wireguardautotunnel.domain.state
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import org.amnezia.awg.crypto.Key
data class TunnelState( data class TunnelState(
val status: TunnelStatus = TunnelStatus.Down, val status: TunnelStatus = TunnelStatus.Down,
val backendState: BackendMode = BackendMode.Inactive, val backendState: BackendMode = BackendMode.Inactive,
val statistics: TunnelStatistics? = null, val statistics: TunnelStatistics? = null,
val pingStates: Map<Key, PingState>? = null, val pingStates: Map<String, PingState>? = null,
val logHealthState: LogHealthState? = null, val logHealthState: LogHealthState? = null,
) { ) {
@@ -37,7 +36,9 @@ data class TunnelState(
// Stats health if no logs or pings // Stats health if no logs or pings
statistics?.let { stats -> statistics?.let { stats ->
return if (stats.isTunnelStale()) Health.STALE else Health.HEALTHY if (stats.isTunnelStale()) return Health.STALE
if (stats.rx() == 0L) return Health.UNKNOWN
return Health.HEALTHY
} }
return Health.UNKNOWN return Health.UNKNOWN
@@ -1,7 +1,5 @@
package com.zaneschepke.wireguardautotunnel.domain.state package com.zaneschepke.wireguardautotunnel.domain.state
import org.amnezia.awg.crypto.Key
abstract class TunnelStatistics { abstract class TunnelStatistics {
open class PeerStats( open class PeerStats(
val rxBytes: Long, val rxBytes: Long,
@@ -9,7 +7,6 @@ abstract class TunnelStatistics {
val latestHandshakeEpochMillis: Long, val latestHandshakeEpochMillis: Long,
val resolvedEndpoint: String, val resolvedEndpoint: String,
) { ) {
// mimic data class copy
open fun copy( open fun copy(
rxBytes: Long = this.rxBytes, rxBytes: Long = this.rxBytes,
txBytes: Long = this.txBytes, txBytes: Long = this.txBytes,
@@ -17,16 +14,15 @@ abstract class TunnelStatistics {
resolvedEndpoint: String = this.resolvedEndpoint, resolvedEndpoint: String = this.resolvedEndpoint,
): PeerStats = PeerStats(rxBytes, txBytes, latestHandshakeEpochMillis, resolvedEndpoint) ): PeerStats = PeerStats(rxBytes, txBytes, latestHandshakeEpochMillis, resolvedEndpoint)
// Manual toString: Format like data class
override fun toString(): String = override fun toString(): String =
"PeerStats(rxBytes=$rxBytes, txBytes=$txBytes, latestHandshakeEpochMillis=$latestHandshakeEpochMillis, resolvedEndpoint=$resolvedEndpoint)" "PeerStats(rxBytes=$rxBytes, txBytes=$txBytes, latestHandshakeEpochMillis=$latestHandshakeEpochMillis, resolvedEndpoint=$resolvedEndpoint)"
} }
abstract fun peerStats(peer: Key): PeerStats? abstract fun peerStats(peerBase64: String): PeerStats?
abstract fun isTunnelStale(): Boolean abstract fun isTunnelStale(): Boolean
abstract fun getPeers(): Array<Key> abstract fun getPeers(): Array<String>
abstract fun rx(): Long abstract fun rx(): Long
@@ -1,18 +1,18 @@
package com.zaneschepke.wireguardautotunnel.domain.state package com.zaneschepke.wireguardautotunnel.domain.state
import com.wireguard.android.backend.Statistics import com.wireguard.android.backend.Statistics
import org.amnezia.awg.crypto.Key import com.wireguard.crypto.Key
class WireGuardStatistics(private val statistics: Statistics) : TunnelStatistics() { class WireGuardStatistics(private val statistics: Statistics) : TunnelStatistics() {
override fun peerStats(peer: Key): PeerStats? { override fun peerStats(peerBase64: String): PeerStats? {
val key = com.wireguard.crypto.Key.fromBase64(peer.toBase64()) val key = Key.fromBase64(peerBase64)
val peerStats = statistics.peer(key) val peerStats = statistics.peer(key)
return peerStats?.let { return peerStats?.let {
PeerStats( PeerStats(
txBytes = peerStats.txBytes, rxBytes = it.rxBytes,
rxBytes = peerStats.rxBytes, txBytes = it.txBytes,
latestHandshakeEpochMillis = peerStats.latestHandshakeEpochMillis, latestHandshakeEpochMillis = it.latestHandshakeEpochMillis,
resolvedEndpoint = peerStats.resolvedEndpoint, resolvedEndpoint = it.resolvedEndpoint,
) )
} }
} }
@@ -21,8 +21,8 @@ class WireGuardStatistics(private val statistics: Statistics) : TunnelStatistics
return statistics.isStale return statistics.isStale
} }
override fun getPeers(): Array<Key> { override fun getPeers(): Array<String> {
return statistics.peers().map { Key.fromBase64(it.toBase64()) }.toTypedArray() return statistics.peers().map { it.toBase64() }.toTypedArray()
} }
override fun rx(): Long { override fun rx(): Long {
@@ -1,13 +1,16 @@
package com.zaneschepke.wireguardautotunnel.ui package com.zaneschepke.wireguardautotunnel.ui
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.runtime.staticCompositionLocalOf
import androidx.navigation.NavHostController import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavKey
import com.zaneschepke.wireguardautotunnel.ui.navigation.NavController
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
val LocalNavController =
compositionLocalOf<NavHostController> { error("NavController was not provided") }
val LocalIsAndroidTV = staticCompositionLocalOf { false } val LocalIsAndroidTV = staticCompositionLocalOf { false }
val LocalSharedVm = staticCompositionLocalOf<SharedAppViewModel> { error("No SharedVm") } val LocalSharedVm =
staticCompositionLocalOf<SharedAppViewModel> { error("No shared viewmodel provided") }
val LocalNavController = staticCompositionLocalOf<NavController> { error("No backstack provided") }
typealias BackStack = NavBackStack<NavKey>
@@ -4,13 +4,11 @@ import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -22,29 +20,28 @@ fun ExpandingRowListItem(
text: String, text: String,
trailing: @Composable () -> Unit, trailing: @Composable () -> Unit,
isSelected: Boolean, isSelected: Boolean,
expanded: (@Composable () -> Unit), expanded: @Composable () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Box( Box(
modifier = modifier =
modifier modifier
.animateContentSize() .animateContentSize()
.clip(RoundedCornerShape(8.dp))
.background( .background(
if (isSelected) MaterialTheme.colorScheme.primary.copy(alpha = 0.1f) if (isSelected) MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
else Color.Transparent else Color.Transparent
) )
) { ) {
Column { Column(Modifier.padding(horizontal = 16.dp)) {
Row( Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp).height(48.dp), modifier = Modifier.fillMaxWidth().height(54.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.spacedBy(16.dp),
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.fillMaxWidth(13 / 20f), modifier = Modifier.weight(1f),
) { ) {
leading() leading()
Text( Text(
@@ -53,6 +50,7 @@ fun ExpandingRowListItem(
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.labelLarge, style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onBackground, color = MaterialTheme.colorScheme.onBackground,
modifier = Modifier.weight(1f),
) )
} }
trailing() trailing()
@@ -1,16 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.common
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun SectionDivider() {
HorizontalDivider(
color = MaterialTheme.colorScheme.outline.copy(0.30f),
modifier = Modifier.padding(horizontal = 12.dp),
)
}

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