Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot] 747dba6c1e chore(deps): bump lifecycle-runtime-compose from 2.9.2 to 2.9.3
Bumps `lifecycle-runtime-compose` from 2.9.2 to 2.9.3.

Updates `androidx.lifecycle:lifecycle-process` from 2.9.2 to 2.9.3

Updates `androidx.lifecycle:lifecycle-service` from 2.9.2 to 2.9.3

Updates `androidx.lifecycle:lifecycle-runtime-ktx` from 2.9.2 to 2.9.3

Updates `androidx.lifecycle:lifecycle-runtime-compose` from 2.9.2 to 2.9.3

---
updated-dependencies:
- dependency-name: androidx.lifecycle:lifecycle-process
  dependency-version: 2.9.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: androidx.lifecycle:lifecycle-service
  dependency-version: 2.9.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: androidx.lifecycle:lifecycle-runtime-ktx
  dependency-version: 2.9.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: androidx.lifecycle:lifecycle-runtime-compose
  dependency-version: 2.9.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-10 13:13:22 +00:00
417 changed files with 10070 additions and 11353 deletions
+1 -1
View File
@@ -15,7 +15,7 @@ A clear and concise description of what the bug is.
- Device: [e.g. Pixel 4a]
- Android Version: [e.g. Android 13]
- App Version [e.g. 3.3.3]
- App mode: [e.g. Kernel, VPN, Proxy, Lockdown]
- Backend: [e.g. Kernel, Userspace]
**To Reproduce**
Steps to reproduce the behavior:
+2 -2
View File
@@ -72,11 +72,11 @@ jobs:
outputs:
UPLOAD_DIR_ANDROID: ${{ env.UPLOAD_DIR_ANDROID }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up JDK 17
uses: actions/setup-java@v5
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
+7 -7
View File
@@ -16,11 +16,11 @@ jobs:
has_new_commits: ${{ steps.check.outputs.new_commits }}
steps:
- name: Checkout Repository
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Check for new commits
id: check
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
NEW_COMMITS=$(git rev-list --count --after="$(date -Iseconds -d '23 hours ago')" ${{ github.sha }})
echo "new_commits=$NEW_COMMITS" >> $GITHUB_OUTPUT
@@ -41,7 +41,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Install system dependencies
run: |
@@ -53,14 +53,14 @@ jobs:
tag: "latest"
message: "Automated tag for HEAD commit"
force_push_tag: true
github_token: ${{ github.token }}
github_token: ${{ secrets.GITHUB_TOKEN }}
tag_exists_error: false
- name: Generate Changelog
id: changelog
uses: requarks/changelog-action@v1
with:
token: ${{ github.token }}
token: ${{ secrets.GITHUB_TOKEN }}
toTag: "nightly"
fromTag: "latest"
writeToFile: false
@@ -84,7 +84,7 @@ jobs:
tag_name: "nightly"
delete_release: true
env:
GITHUB_TOKEN: ${{ github.token }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Get checksum
id: checksum
@@ -124,4 +124,4 @@ jobs:
files: |
${{ github.workspace }}/temp/**/*.apk
env:
GITHUB_TOKEN: ${{ github.token }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-129
View File
@@ -1,129 +0,0 @@
name: notifications
on:
issues:
types: [opened, closed]
release:
types: [published]
jobs:
notify:
runs-on: ubuntu-latest
steps:
- name: Send to Telegram - New Issue
if: github.event_name == 'issues' && github.event.action == 'opened'
env:
TITLE: ${{ github.event.issue.title }}
NUMBER: ${{ github.event.issue.number }}
USER: ${{ github.event.issue.user.login }}
BODY: ${{ github.event.issue.body || 'No body provided' }}
URL: ${{ github.event.issue.html_url }}
run: |
BODY_TRUNC="${BODY:0:200}" # Truncate to avoid spam
TEXT=$(echo -e "🆕 New Issue #$NUMBER: *$TITLE* by $USER\n\n$BODY_TRUNC\n\n[View Issue]($URL)")
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 Telegram - Closed Issue
if: github.event_name == 'issues' && github.event.action == 'closed'
env:
TITLE: ${{ github.event.issue.title }}
NUMBER: ${{ github.event.issue.number }}
USER: ${{ github.event.issue.user.login }}
URL: ${{ github.event.issue.html_url }}
run: |
TEXT=$(echo -e "✅ Issue Closed #$NUMBER: *$TITLE* by $USER\n\n[View Issue]($URL)")
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 Telegram - New Release
if: github.event_name == 'release' && github.event.action == 'published'
env:
NAME: ${{ github.event.release.name }}
TAG: ${{ github.event.release.tag_name }}
BODY: ${{ github.event.release.body || 'No notes provided' }}
URL: ${{ github.event.release.html_url }}
run: |
BODY_TRUNC="${BODY:0:200}" # Truncate to avoid spam
TEXT=$(echo -e "🚀 New Release *$NAME* ($TAG)\n\n$BODY_TRUNC\n\n[View Release]($URL)")
curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN }}/sendMessage" \
-d chat_id="${{ vars.TELEGRAM_CHAT_ID }}" \
${{ vars.TELEGRAM_THREAD_ID && format('-d message_thread_id="{0}"', vars.TELEGRAM_THREAD_ID) || '' }} \
-d parse_mode="Markdown" \
--data-urlencode "text=$TEXT"
- name: Send to Matrix - New Issue
if: github.event_name == 'issues' && github.event.action == 'opened'
env:
NUMBER: ${{ github.event.issue.number }}
TITLE: ${{ github.event.issue.title }}
USER: ${{ github.event.issue.user.login }}
BODY: ${{ github.event.issue.body || 'No body provided' }}
URL: ${{ github.event.issue.html_url }}
run: |
PLAIN_MESSAGE=$(echo -e "🆕 New Issue #$NUMBER: $TITLE by $USER\n\n$BODY\n\nView Issue: $URL")
HTML_MESSAGE=$(echo -e "<p>🆕 New Issue #$NUMBER: <strong>$TITLE</strong> by $USER</p><p>$BODY</p><p><a href=\"$URL\">View Issue</a></p>")
PLAIN_MESSAGE="${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"
- name: Send to Matrix - Closed Issue
if: github.event_name == 'issues' && github.event.action == 'closed'
env:
NUMBER: ${{ github.event.issue.number }}
TITLE: ${{ github.event.issue.title }}
USER: ${{ github.event.issue.user.login }}
URL: ${{ github.event.issue.html_url }}
run: |
PLAIN_MESSAGE=$(echo -e "✅ Issue Closed #$NUMBER: $TITLE by $USER\n\nView Issue: $URL")
HTML_MESSAGE=$(echo -e "<p>✅ Issue Closed #$NUMBER: <strong>$TITLE</strong> by $USER</p><p><a href=\"$URL\">View Issue</a></p>")
PLAIN_MESSAGE="${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"
- name: Send to Matrix - New Release
if: github.event_name == 'release' && github.event.action == 'published'
env:
NAME: ${{ github.event.release.name }}
TAG: ${{ github.event.release.tag_name }}
BODY: ${{ github.event.release.body || 'No notes provided' }}
URL: ${{ github.event.release.html_url }}
run: |
PLAIN_MESSAGE=$(echo -e "🚀 New Release $NAME ($TAG)\n\n$BODY\n\nView Release: $URL")
HTML_MESSAGE=$(echo -e "<p>🚀 New Release <strong>$NAME</strong> ($TAG)</p><p>$BODY</p><p><a href=\"$URL\">View Release</a></p>")
PLAIN_MESSAGE="${PLAIN_MESSAGE:0:220}"
PAYLOAD=$(jq -n --arg body "$PLAIN_MESSAGE" --arg formatted "$HTML_MESSAGE" '{
"msgtype": "m.text",
"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"
+2 -2
View File
@@ -10,9 +10,9 @@ jobs:
format_check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v5
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
+10 -11
View File
@@ -72,9 +72,9 @@ jobs:
name: publish-github
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'push' && github.ref || 'master' }}
ref: ${{ github.event_name == 'push' && github.ref || 'main' }}
- name: Install system dependencies
run: |
sudo apt update && sudo apt install -y gh apksigner
@@ -86,21 +86,20 @@ jobs:
tag: "latest"
message: "Automated tag for HEAD commit"
force_push_tag: true
github_token: ${{ github.token }}
github_token: ${{ secrets.GITHUB_TOKEN }}
tag_exists_error: false
- name: Get latest release
id: latest_release
uses: kaliber5/action-get-release@v1
with:
token: ${{ github.token }}
token: ${{ secrets.GITHUB_TOKEN }}
latest: true
- name: Generate Changelog
id: changelog
uses: requarks/changelog-action@v1
with:
token: ${{ github.token }}
token: ${{ secrets.GITHUB_TOKEN }}
toTag: ${{ steps.latest_release.outputs.tag_name }}
fromTag: "latest"
writeToFile: false
@@ -162,7 +161,7 @@ jobs:
files: |
${{ github.workspace }}/temp/**/*.apk
env:
GITHUB_TOKEN: ${{ github.token }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
publish-fdroid-public:
runs-on: ubuntu-latest
@@ -171,9 +170,9 @@ jobs:
if: ${{ github.event_name == 'push' || inputs.release_type == 'release' }}
steps:
- name: Dispatch update for fdroid repo
uses: peter-evans/repository-dispatch@v4
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.PAT }}
token: ${{ secrets.GITHUB_TOKEN }}
repository: wgtunnel/fdroid
event-type: fdroid-update
@@ -190,9 +189,9 @@ jobs:
KEY_STORE_LOCATION: ${{ github.workspace }}/app/keystore/
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v5
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
+29 -29
View File
@@ -4,7 +4,7 @@ WG Tunnel
<div align="center">
An alternative FOSS Android client for [WireGuard](https://www.wireguard.com/)
An alternative Android client app for [WireGuard](https://www.wireguard.com/)
and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/)
<br />
<br />
@@ -37,11 +37,11 @@ and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/)
<summary>Table of Contents</summary>
- [About](#about)
- [Acknowledgements](#acknowledgements)
- [Screenshots](#screenshots)
- [Features](#features)
- [Building](#building)
- [Translation](#translation)
- [Acknowledgements](#acknowledgements)
- [Contributing](#contributing)
</details>
@@ -49,13 +49,22 @@ and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/)
<div style="text-align: left;">
## About
WG Tunnel is an alternative Android client for WireGuard and AmneziaWG, inspired by the official WireGuard Android app. It fills gaps in the official client by adding advanced features like auto-tunneling (on-demand VPN activation), while seamlessly supporting both protocols across app modes—including Kernel (for direct WireGuard kernel integration; AmneziaWG not supported), VPN (standard system-level tunneling), Lockdown (a custom kill switch for leak prevention), and Proxy (built-in HTTP/SOCKS5 forwarding)—for enhanced privacy, censorship resistance, and flexibility.
Inspired by the official [wireguard-android](https://github.com/WireGuard/wireguard-android) app, WG Tunnel was created to address features and support missing from the official app. This app combines support for both [WireGuard](https://www.wireguard.com/)
and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/), with its primary feature of auto-tunneling (on-demand tunneling).
</div>
<div style="text-align: left;">
## Acknowledgements
Thank you to the following:
- All of the users that have helped contribute to the project with ideas, translations, feedback, bug reports, testing, and donations.
- [WireGuard](https://www.wireguard.com/) - Jason A. Donenfeld (https://github.com/WireGuard/wireguard-android)
- [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) - Amnezia Team (https://github.com/amnezia-vpn/amneziawg-android)
## Screenshots
</div>
@@ -70,26 +79,26 @@ WG Tunnel is an alternative Android client for WireGuard and AmneziaWG, inspired
## Features
- **Tunnel Import Methods**: Easily add tunnels using .conf files, ZIP archives, manual entry, or QR code scanning.
- **Auto-Tunneling**: Automatically activate tunnels based on Wi-Fi SSID, Ethernet connections, or mobile data networks.
- **Split Tunneling**: Flexible support for routing specific apps or traffic through the VPN.
- **WireGuard Modes**: Full compatibility with WireGuard in both kernel and userspace implementations.
- **AmneziaWG Integration**: Userspace mode for AmneziaWG, providing robust censorship evasion.
- **Always-On VPN**: Ensures continuous protection with Android's Always-On VPN feature.
- **Quick Controls**: Quick Settings tile and home screen shortcuts for easy VPN toggling.
- **Automation Support**: Intent-based automation for controlling tunnels.
- **Auto-Restore**: Seamlessly restores auto-tunneling and active tunnels after device restarts or app updates.
- **Proxying Options**: Built-in HTTP and SOCKS5 proxy support within tunnels.
- **Lockdown Mode**: Custom kill switch for maximum leak prevention and security.
- **Dynamic DNS Handling**: Detects and updates DNS changes without tunnel restarts.
- **Monitoring Tools**: Advanced tunnel monitoring features for tunnel performance monitoring.
- **Android TV Support**: Android TV support for secure streaming and browsing.
- **Advanced DNS**: DNS over HTTPS support for tunnel endpoint resolutions.
* Add tunnels via .conf file, zip, manual entry, clipboard, or QR code
* Auto-tunnel based on Wi-Fi SSID, ethernet, or mobile data
* Split tunneling by application with search
* Support for kernel and userspace modes
* Amnezia support for userspace mode for DPI/censorship protection
* Pre/Post Up/Down scripts support for all modes on a rooted device
* Always-On VPN support
* Export tunnels to zip
* Quick tile support for tunnel toggling, auto-tunneling
* Shortcuts support for tunnel toggling, auto-tunneling
* Intent automation support for all tunnels
* In app VPN kill switch with LAN bypass
* Automatic auto-tunneling service and/or tunnel restart after reboot or app update
* Battery preservation measures
* Restart tunnel on ping failure
## Building
```sh
git clone https://github.com/wgtunnel/wgtunnel
git clone https://github.com/zaneschepke/wgtunnel
cd wgtunnel
```
@@ -105,15 +114,6 @@ Help translate WG Tunnel into your language
at [Hosted Weblate](https://hosted.weblate.org/engage/wg-tunnel/).\
[![Translation status](https://hosted.weblate.org/widgets/wg-tunnel/-/multi-auto.svg)](https://hosted.weblate.org/engage/wg-tunnel/)
## Acknowledgements
Thank you to the following:
- All of the users that have helped contribute to the project with ideas, translations, feedback, bug reports, testing, and donations.
- [WireGuard](https://www.wireguard.com/) - Jason A. Donenfeld (https://github.com/WireGuard/wireguard-android)
- [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) - Amnezia Team (https://github.com/amnezia-vpn/amneziawg-android)
- [JetBrains](https://jetbrains.com) - For supporting open-source developers with free software licenses.
## Contributing
Any contributions in the form of feedback, issues, code, or translations are welcome and much
+74 -62
View File
@@ -9,6 +9,7 @@ plugins {
alias(libs.plugins.compose.compiler)
alias(libs.plugins.grgit)
alias(libs.plugins.licensee)
id("kotlin-parcelize")
}
android {
@@ -24,9 +25,6 @@ android {
ksp { arg("room.schemaLocation", "$projectDir/schemas") }
// fix okhttp proguard issue
packaging { resources { pickFirsts.add("okhttp3/internal/publicsuffix/publicsuffixes.gz") } }
defaultConfig {
applicationId = Constants.APP_ID
minSdk = Constants.MIN_SDK
@@ -148,66 +146,19 @@ dependencies {
implementation(project(":logcatter"))
implementation(project(":networkmonitor"))
// Core foundations
implementation(libs.bundles.androidx.core.full)
implementation(libs.bundles.androidx.lifecycle.core)
implementation(libs.bundles.androidx.appcompat)
implementation(libs.bundles.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.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.service)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
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)
// 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.androidx.junit)
androidTestImplementation(libs.androidx.junit)
@@ -218,10 +169,71 @@ dependencies {
debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.manifest)
// Room database backup
implementation(libs.tunnel)
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) {
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") {
-1
View File
@@ -1 +0,0 @@
-dontwarn javax.lang.model.**
@@ -1,371 +0,0 @@
{
"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')"
]
}
}
@@ -1,463 +0,0 @@
{
"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')"
]
}
}
@@ -1,477 +0,0 @@
{
"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')"
]
}
}
+1 -7
View File
@@ -73,16 +73,10 @@
<activity
android:name=".MainActivity"
android:exported="true"
android:windowSoftInputMode="adjustResize"
android:windowSoftInputMode="adjustNothing"
android:theme="@style/Theme.WireguardAutoTunnel"
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>
<action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.SHOW_APP_INFO" />
@@ -1,43 +1,46 @@
package com.zaneschepke.wireguardautotunnel
import ProxySettingsScreen
import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.Color
import android.net.VpnService
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import androidx.activity.SystemBarStyle
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.core.net.toUri
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
import androidx.navigation3.ui.NavDisplay
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navigation
import androidx.navigation.toRoute
import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
@@ -47,33 +50,28 @@ import com.zaneschepke.wireguardautotunnel.ui.common.banner.AppAlertBanner
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.VpnDeniedDialog
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.CustomSnackBar
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.DynamicTopAppBar
import com.zaneschepke.wireguardautotunnel.ui.navigation.components.currentRouteAsNavbarState
import com.zaneschepke.wireguardautotunnel.ui.navigation.functions.rememberNavController
import com.zaneschepke.wireguardautotunnel.ui.navigation.components.currentBackStackEntryAsNavbarState
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.AutoTunnelScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.advanced.AutoTunnelAdvancedScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.detection.WifiDetectionMethodScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.disclosure.LocationDisclosureScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.preferred.PreferredTunnelScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.wifi.WifiSettingsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.pin.PinLockScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.AppearanceScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.display.DisplayScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.language.LanguageScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.dns.DnsSettingsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.globals.TunnelGlobalsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.integrations.AndroidIntegrationsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.logs.LogsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.TunnelMonitoringScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.logs.LogsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.ping.PingTargetScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.system.SystemFeaturesScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.donate.DonateScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.donate.crypto.AddressesScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.license.LicenseScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.TunnelsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.autotunnel.TunnelAutoTunnelScreen
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.splittunnel.SplitTunnelScreen
@@ -83,16 +81,13 @@ import com.zaneschepke.wireguardautotunnel.ui.theme.OffWhite
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
import com.zaneschepke.wireguardautotunnel.util.extensions.*
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 com.zaneschepke.wireguardautotunnel.viewmodel.*
import dagger.hilt.android.AndroidEntryPoint
import de.raphaelebner.roomdatabasebackup.core.RoomBackup
import java.util.*
import javax.inject.Inject
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import timber.log.Timber
import xyz.teamgravity.pin_lock_compose.PinManager
@AndroidEntryPoint
@@ -105,11 +100,11 @@ class MainActivity : AppCompatActivity() {
private lateinit var roomBackup: RoomBackup
@OptIn(ExperimentalMaterial3Api::class)
@SuppressLint("BatteryLife")
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge(
statusBarStyle = SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT),
navigationBarStyle = SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT),
statusBarStyle = SystemBarStyle.Companion.auto(Color.TRANSPARENT, Color.TRANSPARENT),
navigationBarStyle = SystemBarStyle.Companion.auto(Color.TRANSPARENT, Color.TRANSPARENT),
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
window.isNavigationBarContrastEnforced = false
@@ -128,35 +123,31 @@ class MainActivity : AppCompatActivity() {
val context = LocalContext.current
val isTv = isRunningOnTv()
val appState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
val navController = rememberNavController()
val scope = rememberCoroutineScope()
var pinManagerInitialized by remember { mutableStateOf(false) }
LaunchedEffect(appState.isAppLoaded) {
if (appState.isAppLoaded) {
if (appState.pinLockEnabled && !pinManagerInitialized) {
PinManager.initialize(this@MainActivity)
pinManagerInitialized = true
}
appState.locale.let { LocaleUtil.changeLocale(it) }
}
}
val navState by
navController.currentBackStackEntryAsNavbarState(viewModel, navController)
val snackbar = remember { SnackbarHostState() }
var showVpnPermissionDialog by remember { mutableStateOf(false) }
var vpnPermissionDenied by remember { mutableStateOf(false) }
var requestingAppMode by remember {
mutableStateOf<Pair<AppMode?, TunnelConfig?>>(Pair(null, null))
mutableStateOf<Pair<AppMode?, TunnelConf?>>(Pair(null, null))
}
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
}
LaunchedEffect(navState) { Timber.d("New navbar state $navState") }
val vpnActivity =
rememberLauncherForActivityResult(
@@ -179,24 +170,40 @@ 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) {
viewModel.globalSideEffect.collectLatest { sideEffect ->
viewModel.globalSideEffect.collect { sideEffect ->
when (sideEffect) {
GlobalSideEffect.ConfigChanged -> restartApp()
GlobalSideEffect.PopBackStack -> navController.pop()
GlobalSideEffect.PopBackStack -> navController.popBackStack()
GlobalSideEffect.RequestBatteryOptimizationDisabled ->
requestDisableBatteryOptimizations()
is GlobalSideEffect.RequestVpnPermission -> {
requestingAppMode = Pair(sideEffect.requestingMode, sideEffect.config)
vpnActivity.launch(VpnService.prepare(this@MainActivity))
}
is GlobalSideEffect.ShareFile -> context.launchShareFile(sideEffect.file)
is GlobalSideEffect.Snackbar ->
scope.launch {
snackbar.showSnackbar(sideEffect.message.asString(context))
}
is GlobalSideEffect.Toast ->
scope.launch { context.showToast(sideEffect.message.asString(context)) }
is GlobalSideEffect.LaunchUrl -> context.openWebUrl(sideEffect.url)
is GlobalSideEffect.InstallApk -> context.installApk(sideEffect.apk)
}
@@ -205,11 +212,6 @@ class MainActivity : AppCompatActivity() {
if (!appState.isAppLoaded) return@setContent
var showLock by remember {
mutableStateOf(appState.pinLockEnabled && !appState.isPinVerified)
}
LaunchedEffect(appState.isPinVerified) { if (appState.isPinVerified) showLock = false }
CompositionLocalProvider(
LocalIsAndroidTV provides isTv,
LocalSharedVm provides viewModel,
@@ -224,213 +226,174 @@ class MainActivity : AppCompatActivity() {
},
)
if (showLock) {
PinManager.initialize(context = this@MainActivity)
PinLockScreen()
} else {
val currentRoute by remember {
derivedStateOf { backStack.lastOrNull() as? Route }
}
val currentTab by remember {
derivedStateOf { Tab.fromRoute(currentRoute ?: Route.Tunnels) }
}
val navState by
currentRouteAsNavbarState(
appState,
viewModel,
currentRoute,
navController,
Box(modifier = Modifier.fillMaxSize()) {
if (appState.settings.appMode == AppMode.LOCK_DOWN) {
AppAlertBanner(
stringResource(R.string.locked_down).uppercase(Locale.getDefault()),
OffWhite,
AlertRed,
modifier = Modifier.fillMaxWidth().zIndex(2f),
)
}
Box(modifier = Modifier.fillMaxSize()) {
if (appState.settings.appMode == AppMode.LOCK_DOWN) {
AppAlertBanner(
stringResource(R.string.locked_down)
.uppercase(Locale.getDefault()),
OffWhite,
AlertRed,
modifier = Modifier.fillMaxWidth().zIndex(2f),
)
}
Scaffold(
snackbarHost = {
SnackbarHost(snackbar) { snackbarData ->
CustomSnackBar(
snackbarData.visuals.message,
isRtl = false,
containerColor =
MaterialTheme.colorScheme.surfaceColorAtElevation(
2.dp
),
)
}
},
topBar = { DynamicTopAppBar(navState) },
bottomBar = {
if (navState.showBottomItems) {
BottomNavbar(
appState.isAutoTunnelActive,
currentTab,
onTabSelected = { tab ->
navController.popUpTo(tab.startRoute)
},
)
}
},
) { padding ->
Column(
modifier =
Modifier.fillMaxSize()
.background(MaterialTheme.colorScheme.surface)
.padding(
top = padding.calculateTopPadding().plus(8.dp),
bottom = padding.calculateBottomPadding(),
)
.consumeWindowInsets(padding)
.imePadding()
) {
NavDisplay(
backStack = backStack,
modifier = Modifier.fillMaxSize(),
onBack = { navController.pop() },
transitionSpec = {
val initialIndex =
previousRoute?.let(Tab::fromRoute)?.index ?: 0
val targetIndex =
currentRoute?.let(Tab::fromRoute)?.index ?: 0
if (initialIndex != targetIndex) {
val dir = if (targetIndex > initialIndex) 1 else -1
(slideInHorizontally { dir * it } +
fadeIn()) togetherWith
(slideOutHorizontally { dir * -it } + fadeOut())
} else {
(slideInHorizontally { it } + fadeIn()) togetherWith
(slideOutHorizontally { -it } + fadeOut())
}
},
popTransitionSpec = {
(slideInHorizontally { -it } + fadeIn()) togetherWith
(slideOutHorizontally { it } + fadeOut())
},
predictivePopTransitionSpec = {
(slideInHorizontally { -it } + fadeIn()) togetherWith
(slideOutHorizontally { it } + fadeOut())
},
entryDecorators =
listOf(
rememberSaveableStateHolderNavEntryDecorator(),
rememberViewModelStoreNavEntryDecorator(),
),
entryProvider =
entryProvider {
entry<Route.Lock> {
PinManager.initialize(
context = this@MainActivity
)
PinLockScreen()
}
entry<Route.Tunnels> { TunnelsScreen() }
entry<Route.Sort> { SortScreen() }
entry<Route.TunnelOptions> { key ->
val viewModel =
hiltViewModel<
TunnelViewModel,
TunnelViewModel.Factory,
>(
creationCallback = { factory ->
factory.create(key.id)
}
)
TunnelOptionsScreen(viewModel)
}
entry<Route.SplitTunnel> { key ->
val viewModel =
hiltViewModel<
SplitTunnelViewModel,
SplitTunnelViewModel.Factory,
>(
creationCallback = { factory ->
factory.create(key.id)
}
)
SplitTunnelScreen(viewModel)
}
entry<Route.Config> { key ->
val viewModel =
hiltViewModel<
ConfigViewModel,
ConfigViewModel.Factory,
>(
creationCallback = { factory ->
factory.create(key.id)
}
)
ConfigScreen(viewModel)
}
entry<Route.LocationDisclosure> {
LocationDisclosureScreen()
}
entry<Route.AutoTunnel> { AutoTunnelScreen() }
entry<Route.WifiPreferences> {
WifiSettingsScreen()
}
entry<Route.AdvancedAutoTunnel> {
AutoTunnelAdvancedScreen()
}
entry<Route.WifiDetectionMethod> {
WifiDetectionMethodScreen()
}
entry<Route.Settings> { SettingsScreen() }
entry<Route.TunnelMonitoring> {
TunnelMonitoringScreen()
}
entry<Route.AndroidIntegrations> {
AndroidIntegrationsScreen()
}
entry<Route.Dns> { DnsSettingsScreen() }
entry<Route.TunnelGlobals> { key ->
TunnelGlobalsScreen(key.id)
}
entry<Route.ConfigGlobal> { key ->
val viewModel =
hiltViewModel<
ConfigViewModel,
ConfigViewModel.Factory,
>(
creationCallback = { factory ->
factory.create(key.id)
}
)
ConfigScreen(viewModel)
}
entry<Route.SplitTunnelGlobal> { key ->
val viewModel =
hiltViewModel<
SplitTunnelViewModel,
SplitTunnelViewModel.Factory,
>(
creationCallback = { factory ->
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() }
},
Scaffold(
snackbarHost = {
SnackbarHost(snackbar) { snackbarData ->
CustomSnackBar(
snackbarData.visuals.message,
isRtl = false,
containerColor =
MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp),
)
}
},
topBar = { DynamicTopAppBar(navState) },
bottomBar = {
BottomNavbar(appState.isAutoTunnelActive, navState, navController)
},
modifier =
Modifier.pointerInput(Unit) {
detectTapGestures { viewModel.clearSelectedTunnels() }
},
) { padding ->
Box(
modifier =
Modifier.fillMaxSize()
.background(MaterialTheme.colorScheme.surface)
.padding(padding)
.consumeWindowInsets(padding)
.imePadding()
) {
NavHost(
navController = navController,
startDestination =
if (appState.pinLockEnabled && !appState.isAuthorized)
Route.Lock
else Route.TunnelsGraph,
) {
composable<Route.Lock> { PinLockScreen() }
navigation<Route.TunnelsGraph>(
startDestination = Route.Tunnels
) {
composable<Route.Tunnels> {
val viewModel =
it.sharedViewModel<TunnelsViewModel>(navController)
TunnelsScreen(viewModel)
}
composable<Route.Sort> {
val viewModel =
it.sharedViewModel<TunnelsViewModel>(navController)
SortScreen(viewModel)
}
composable<Route.TunnelOptions> { backStackEntry ->
val args = backStackEntry.toRoute<Route.TunnelOptions>()
val viewModel =
backStackEntry.sharedViewModel<TunnelsViewModel>(
navController
)
TunnelOptionsScreen(args.id, viewModel)
}
composable<Route.SplitTunnel> { backStackEntry ->
val args = backStackEntry.toRoute<Route.SplitTunnel>()
SplitTunnelScreen(args.id)
}
composable<Route.TunnelAutoTunnel> { backStackEntry ->
val args =
backStackEntry.toRoute<Route.TunnelAutoTunnel>()
val viewModel =
backStackEntry.sharedViewModel<TunnelsViewModel>(
navController
)
TunnelAutoTunnelScreen(args.id, viewModel)
}
composable<Route.Config> { backStackEntry ->
val args = backStackEntry.toRoute<Route.Config>()
val viewModel =
backStackEntry.sharedViewModel<TunnelsViewModel>(
navController
)
ConfigScreen(args.id, viewModel)
}
}
navigation<Route.AutoTunnelGraph>(
startDestination =
if (appState.isLocationDisclosureShown) Route.AutoTunnel
else Route.LocationDisclosure
) {
composable<Route.LocationDisclosure> {
val viewModel =
it.sharedViewModel<AutoTunnelViewModel>(
navController
)
LocationDisclosureScreen(viewModel)
}
composable<Route.AutoTunnel> {
val viewModel =
it.sharedViewModel<AutoTunnelViewModel>(
navController
)
AutoTunnelScreen(viewModel)
}
composable<Route.AdvancedAutoTunnel> {
val viewModel =
it.sharedViewModel<AutoTunnelViewModel>(
navController
)
AutoTunnelAdvancedScreen(viewModel)
}
composable<Route.WifiDetectionMethod> {
val viewModel =
it.sharedViewModel<AutoTunnelViewModel>(
navController
)
WifiDetectionMethodScreen(viewModel)
}
}
navigation<Route.SettingsGraph>(
startDestination = Route.Settings
) {
composable<Route.Settings> {
val viewModel =
it.sharedViewModel<SettingsViewModel>(navController)
SettingsScreen(viewModel)
}
composable<Route.TunnelMonitoring> {
val viewModel =
it.sharedViewModel<SettingsViewModel>(navController)
TunnelMonitoringScreen(viewModel)
}
composable<Route.SystemFeatures> {
val viewModel =
it.sharedViewModel<SettingsViewModel>(navController)
SystemFeaturesScreen(viewModel)
}
composable<Route.Dns> {
val viewModel =
it.sharedViewModel<SettingsViewModel>(navController)
DnsSettingsScreen(viewModel)
}
composable<Route.ProxySettings> { ProxySettingsScreen() }
composable<Route.Appearance> { AppearanceScreen() }
composable<Route.Language> { LanguageScreen() }
composable<Route.Display> { DisplayScreen() }
composable<Route.Logs> { LogsScreen() }
}
navigation<Route.SupportGraph>(
startDestination = Route.Support
) {
composable<Route.Support> {
val viewModel =
it.sharedViewModel<SupportViewModel>(navController)
SupportScreen(viewModel)
}
composable<Route.License> { LicenseScreen() }
composable<Route.Donate> { DonateScreen(navController) }
composable<Route.Addresses> { AddressesScreen() }
}
}
}
}
}
@@ -11,8 +11,9 @@ import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.core.worker.ServiceWorker
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.di.MainDispatcher
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
import dagger.hilt.android.HiltAndroidApp
import javax.inject.Inject
@@ -23,6 +24,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.amnezia.awg.backend.GoBackend
import timber.log.Timber
@HiltAndroidApp
@@ -37,9 +39,11 @@ class WireGuardAutoTunnel : Application(), Configuration.Provider {
@Inject lateinit var logReader: LogReader
@Inject lateinit var appDataRepository: AppDataRepository
@Inject @IoDispatcher lateinit var ioDispatcher: CoroutineDispatcher
@Inject lateinit var monitoringRepository: MonitoringSettingsRepository
@Inject @MainDispatcher lateinit var mainDispatcher: CoroutineDispatcher
@Inject lateinit var notificationMonitor: NotificationMonitor
@@ -63,13 +67,22 @@ class WireGuardAutoTunnel : Application(), Configuration.Provider {
}
applicationScope.launch(ioDispatcher) {
launch {
val monitoringSettings = monitoringRepository.getMonitoringSettings()
if (monitoringSettings.isLocalLogsEnabled) logReader.start()
}
launch { if (appDataRepository.appState.isLocalLogsEnabled()) logReader.start() }
launch { notificationMonitor.handleApplicationNotifications() }
}
GoBackend.setAlwaysOnCallback {
applicationScope.launch {
val settings = appDataRepository.settings.get()
if (settings.isAlwaysOnVpnEnabled) {
val tunnel = appDataRepository.getPrimaryOrFirstTunnel()
tunnel?.let { tunnelManager.startTunnel(it) }
} else {
Timber.w("Always-on VPN is not enabled in app settings")
}
}
}
ServiceWorker.start(this)
}
@@ -3,6 +3,7 @@ package com.zaneschepke.wireguardautotunnel.core.broadcast
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
@@ -18,6 +19,8 @@ class KernelReceiver : BroadcastReceiver() {
@Inject lateinit var tunnelRepository: TunnelRepository
@Inject lateinit var serviceManager: ServiceManager
@Inject lateinit var tunnelManager: TunnelManager
override fun onReceive(context: Context, intent: Intent) {
@@ -28,6 +31,7 @@ class KernelReceiver : BroadcastReceiver() {
val tunnel = tunnelRepository.findByTunnelName(name)
tunnel?.let { tunnelRepository.save(it.copy(isActive = true)) }
}
serviceManager.updateTunnelTile()
}
}
}
@@ -4,10 +4,10 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@@ -17,24 +17,23 @@ import kotlinx.coroutines.launch
@AndroidEntryPoint
class NotificationActionReceiver : BroadcastReceiver() {
@Inject lateinit var serviceManager: ServiceManager
@Inject lateinit var tunnelManager: TunnelManager
@Inject lateinit var tunnelRepository: TunnelRepository
@Inject lateinit var autoTunnelRepository: AutoTunnelSettingsRepository
@Inject @ApplicationScope lateinit var applicationScope: CoroutineScope
override fun onReceive(context: Context, intent: Intent) {
applicationScope.launch {
when (intent.action) {
NotificationAction.AUTO_TUNNEL_OFF.name ->
autoTunnelRepository.updateAutoTunnelEnabled(false)
NotificationAction.AUTO_TUNNEL_OFF.name -> serviceManager.stopAutoTunnel()
NotificationAction.TUNNEL_OFF.name -> {
val tunnelId = intent.getIntExtra(NotificationManager.EXTRA_ID, 0)
if (tunnelId == STOP_ALL_TUNNELS_ID)
return@launch tunnelManager.stopActiveTunnels()
tunnelManager.stopTunnel(tunnelId)
tunnelRepository.getById(tunnelId)?.let { tunnelManager.stopTunnel(it.id) }
}
}
}
@@ -3,11 +3,10 @@ package com.zaneschepke.wireguardautotunnel.core.broadcast
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
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.TunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.util.Constants
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@@ -20,9 +19,9 @@ class RemoteControlReceiver : BroadcastReceiver() {
@Inject @ApplicationScope lateinit var applicationScope: CoroutineScope
@Inject lateinit var settingsRepository: GeneralSettingRepository
@Inject lateinit var tunnelsRepository: TunnelRepository
@Inject lateinit var autoTunnelSettingsRepository: AutoTunnelSettingsRepository
@Inject lateinit var appDataRepository: AppDataRepository
@Inject lateinit var serviceManager: ServiceManager
@Inject lateinit var tunnelManager: TunnelManager
@@ -53,9 +52,11 @@ class RemoteControlReceiver : BroadcastReceiver() {
val action = intent.action ?: return
val appAction = Action.fromAction(action) ?: return Timber.w("Unknown action $action")
applicationScope.launch {
val settings = settingsRepository.getGeneralSettings()
if (!settings.isRemoteControlEnabled) return@launch Timber.w("Remote control disabled")
val key = settings.remoteKey ?: return@launch Timber.w("Remote control key missing")
if (!appDataRepository.appState.isRemoteControlEnabled())
return@launch Timber.w("Remote control disabled")
val key =
appDataRepository.appState.getRemoteKey()
?: return@launch Timber.w("Remote control key missing")
if (key != intent.getStringExtra(EXTRA_KEY)?.trim())
return@launch Timber.w("Invalid remote control key")
when (appAction) {
@@ -63,7 +64,7 @@ class RemoteControlReceiver : BroadcastReceiver() {
val tunnelName =
intent.getStringExtra(EXTRA_TUN_NAME) ?: return@launch startDefaultTunnel()
val tunnel =
tunnelsRepository.findByTunnelName(tunnelName)
appDataRepository.tunnels.findByTunnelName(tunnelName)
?: return@launch startDefaultTunnel()
tunnelManager.startTunnel(tunnel)
}
@@ -72,20 +73,20 @@ class RemoteControlReceiver : BroadcastReceiver() {
intent.getStringExtra(EXTRA_TUN_NAME)
?: return@launch tunnelManager.stopActiveTunnels()
val tunnel =
tunnelsRepository.findByTunnelName(tunnelName)
appDataRepository.tunnels.findByTunnelName(tunnelName)
?: return@launch tunnelManager.stopActiveTunnels()
tunnelManager.stopTunnel(tunnel.id)
}
Action.START_AUTO_TUNNEL ->
autoTunnelSettingsRepository.updateAutoTunnelEnabled(true)
Action.STOP_AUTO_TUNNEL ->
autoTunnelSettingsRepository.updateAutoTunnelEnabled(false)
Action.START_AUTO_TUNNEL -> serviceManager.startAutoTunnel()
Action.STOP_AUTO_TUNNEL -> serviceManager.stopAutoTunnel()
}
}
}
private suspend fun startDefaultTunnel() {
tunnelsRepository.getDefaultTunnel()?.let { tunnel -> tunnelManager.startTunnel(tunnel) }
appDataRepository.getPrimaryOrFirstTunnel()?.let { tunnel ->
tunnelManager.startTunnel(tunnel)
}
}
companion object {
@@ -4,37 +4,38 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
@AndroidEntryPoint
class RestartReceiver : BroadcastReceiver() {
@Inject lateinit var appDataRepository: AppDataRepository
@Inject @ApplicationScope lateinit var applicationScope: CoroutineScope
@Inject lateinit var serviceManager: ServiceManager
// injecting this should let tunnelManger handle clean startup
@Inject lateinit var tunnelManager: TunnelManager
@Inject lateinit var logReader: LogReader
@Inject @IoDispatcher lateinit var ioDispatcher: CoroutineDispatcher
override fun onReceive(context: Context, intent: Intent) {
Timber.d("RestartReceiver triggered with action: ${intent.action}")
applicationScope.launch {
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()
}
}
}
serviceManager.updateTunnelTile()
serviceManager.updateAutoTunnelTile()
if (intent.action == Intent.ACTION_MY_PACKAGE_REPLACED)
applicationScope.launch(ioDispatcher) { logReader.deleteAndClearLogs() }
}
}
@@ -17,11 +17,9 @@ interface NotificationManager {
actions: Collection<NotificationCompat.Action> = emptyList(),
description: String = "",
showTimestamp: Boolean = true,
importance: Int = NotificationManager.IMPORTANCE_LOW,
importance: Int = NotificationManager.IMPORTANCE_HIGH,
onGoing: Boolean = false,
onlyAlertOnce: Boolean = true,
groupKey: String? = null,
isGroupSummary: Boolean = false,
): Notification
fun createNotification(
@@ -30,11 +28,9 @@ interface NotificationManager {
actions: Collection<NotificationCompat.Action> = emptyList(),
description: StringValue,
showTimestamp: Boolean = true,
importance: Int = NotificationManager.IMPORTANCE_LOW,
importance: Int = NotificationManager.IMPORTANCE_HIGH,
onGoing: Boolean = false,
onlyAlertOnce: Boolean = true,
groupKey: String? = null,
isGroupSummary: Boolean = false,
): Notification
fun createNotificationAction(
@@ -47,8 +43,6 @@ interface NotificationManager {
fun show(notificationId: Int, notification: Notification)
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_SERVICES_ID = 124
// For auto tunnel foreground notification
@@ -3,6 +3,7 @@ package com.zaneschepke.wireguardautotunnel.core.notification
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException
import com.zaneschepke.wireguardautotunnel.util.StringValue
import jakarta.inject.Inject
import kotlinx.coroutines.coroutineScope
@@ -26,15 +27,16 @@ constructor(
val notification =
notificationManager.createNotification(
WireGuardNotification.NotificationChannels.VPN,
title =
tunName?.let { StringValue.DynamicString(it) }
?: StringValue.StringResource(R.string.tunnel),
title = StringValue.DynamicString(tunName),
description =
StringValue.StringResource(
R.string.tunnel_error_template,
error.toStringRes(),
),
groupKey = NotificationManager.VPN_GROUP_KEY,
when (error) {
is BackendCoreException.BounceFailed -> error.toStringValue()
else ->
StringValue.StringResource(
R.string.tunnel_error_template,
error.toStringRes(),
)
},
)
notificationManager.show(
NotificationManager.TUNNEL_ERROR_NOTIFICATION_ID,
@@ -49,11 +51,8 @@ constructor(
val notification =
notificationManager.createNotification(
WireGuardNotification.NotificationChannels.VPN,
title =
tunName?.let { StringValue.DynamicString(it) }
?: StringValue.StringResource(R.string.tunnel),
title = StringValue.DynamicString(tunName),
description = message.toStringValue(),
groupKey = NotificationManager.VPN_GROUP_KEY,
)
notificationManager.show(
NotificationManager.TUNNEL_MESSAGES_NOTIFICATION_ID,
@@ -3,10 +3,12 @@ package com.zaneschepke.wireguardautotunnel.core.notification
import android.Manifest
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Color
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
@@ -20,7 +22,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
class WireGuardNotification @Inject constructor(@ApplicationContext override val context: Context) :
NotificationManager {
com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager {
enum class NotificationChannels {
VPN,
@@ -38,10 +40,8 @@ class WireGuardNotification @Inject constructor(@ApplicationContext override val
importance: Int,
onGoing: Boolean,
onlyAlertOnce: Boolean,
groupKey: String?,
isGroupSummary: Boolean,
): Notification {
notificationManager.createNotificationChannel(channel.asChannel(importance))
notificationManager.createNotificationChannel(channel.asChannel())
return channel
.asBuilder()
.apply {
@@ -51,23 +51,16 @@ class WireGuardNotification @Inject constructor(@ApplicationContext override val
PendingIntent.getActivity(
context,
0,
Intent(context, MainActivity::class.java)
.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP),
Intent(context, MainActivity::class.java),
PendingIntent.FLAG_IMMUTABLE,
)
)
setContentText(description)
setOnlyAlertOnce(onlyAlertOnce)
setOngoing(onGoing)
setPriority(NotificationCompat.PRIORITY_LOW)
setPriority(NotificationCompat.PRIORITY_HIGH)
setShowWhen(showTimestamp)
setSmallIcon(R.drawable.ic_notification)
if (groupKey != null) {
setGroup(groupKey)
if (isGroupSummary) {
setGroupSummary(true)
}
}
}
.build()
}
@@ -81,8 +74,6 @@ class WireGuardNotification @Inject constructor(@ApplicationContext override val
importance: Int,
onGoing: Boolean,
onlyAlertOnce: Boolean,
groupKey: String?,
isGroupSummary: Boolean,
): Notification {
return createNotification(
channel,
@@ -103,12 +94,12 @@ class WireGuardNotification @Inject constructor(@ApplicationContext override val
val pendingIntent =
PendingIntent.getBroadcast(
context,
extraId ?: 0,
0,
Intent(context, NotificationActionReceiver::class.java).apply {
action = notificationAction.name
if (extraId != null) putExtra(EXTRA_ID, extraId)
},
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
PendingIntent.FLAG_IMMUTABLE,
)
return NotificationCompat.Action.Builder(
R.drawable.ic_notification,
@@ -150,24 +141,34 @@ class WireGuardNotification @Inject constructor(@ApplicationContext override val
}
}
private fun NotificationChannels.asChannel(importance: Int): NotificationChannel {
private fun NotificationChannels.asChannel(): NotificationChannel {
return when (this) {
NotificationChannels.VPN -> {
NotificationChannel(
context.getString(R.string.vpn_channel_id),
context.getString(R.string.vpn_channel_name),
importance,
NotificationManager.IMPORTANCE_HIGH,
)
.apply { description = context.getString(R.string.vpn_channel_description) }
.apply {
description = context.getString(R.string.vpn_channel_description)
enableLights(true)
lightColor = Color.WHITE
enableVibration(false)
vibrationPattern = longArrayOf(100, 200, 300)
}
}
NotificationChannels.AUTO_TUNNEL -> {
NotificationChannel(
context.getString(R.string.auto_tunnel_channel_id),
context.getString(R.string.auto_tunnel_channel_name),
importance,
NotificationManager.IMPORTANCE_HIGH,
)
.apply {
description = context.getString(R.string.auto_tunnel_channel_description)
enableLights(true)
lightColor = Color.WHITE
enableVibration(false)
vibrationPattern = longArrayOf(100, 200, 300)
}
}
}
@@ -13,13 +13,14 @@ import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelMonitor
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.util.extensions.distinctByKeys
import dagger.hilt.android.AndroidEntryPoint
import io.ktor.util.collections.*
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import timber.log.Timber
@@ -36,9 +37,9 @@ abstract class BaseTunnelForegroundService : LifecycleService(), TunnelService {
@Inject @IoDispatcher lateinit var ioDispatcher: CoroutineDispatcher
@Inject lateinit var settingsRepository: GeneralSettingRepository
@Inject lateinit var appDataRepository: AppDataRepository
@Inject lateinit var tunnelsRepository: TunnelRepository
private val tunnelJobs = ConcurrentMap<Int, Job>()
protected abstract val fgsType: Int
@@ -65,24 +66,7 @@ abstract class BaseTunnelForegroundService : LifecycleService(), TunnelService {
onCreateNotification(),
fgsType,
)
if (
intent == null ||
intent.component == null ||
(intent.component?.packageName != this.packageName)
) {
Timber.d("Service started by Always-on VPN feature")
lifecycleScope.launch {
val settings = settingsRepository.getGeneralSettings()
if (settings.isAlwaysOnVpnEnabled) {
val tunnel = tunnelsRepository.getDefaultTunnel()
tunnel?.let { tunnelManager.startTunnel(it) }
} else {
Timber.w("Always-on VPN is not enabled in app settings")
}
}
} else {
start()
}
start()
return START_STICKY
}
@@ -90,7 +74,13 @@ abstract class BaseTunnelForegroundService : LifecycleService(), TunnelService {
lifecycleScope.launch(ioDispatcher) {
tunnelManager.activeTunnels.distinctByKeys().collect { activeTunnels ->
val activeTunConfigs = activeTunnels.keys
val tunnels = tunnelsRepository.getAll()
val obsoleteJobs = tunnelJobs.keys - activeTunConfigs
obsoleteJobs.forEach { tunId -> tunnelJobs[tunId]?.cancel() }
activeTunConfigs.forEach { tunId ->
if (tunnelJobs.contains(tunId)) return@forEach
tunnelJobs[tunId] = launch { tunnelMonitor.startMonitoring(tunId, true) }
}
val tunnels = appDataRepository.tunnels.getAll()
val activeConfigs = tunnels.filter { activeTunConfigs.contains(it.id) }
updateServiceNotification(activeConfigs)
}
@@ -98,7 +88,7 @@ abstract class BaseTunnelForegroundService : LifecycleService(), TunnelService {
}
// TODO Would be cool to have this include kill switch
private fun updateServiceNotification(activeConfigs: List<TunnelConfig>) {
private fun updateServiceNotification(activeConfigs: List<TunnelConf>) {
val notification =
when (activeConfigs.size) {
0 -> onCreateNotification()
@@ -115,31 +105,31 @@ abstract class BaseTunnelForegroundService : LifecycleService(), TunnelService {
override fun stop() {
Timber.d("Stop called")
tunnelJobs.forEach { it.value.cancel() }
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
stopSelf()
}
override fun onDestroy() {
tunnelJobs.forEach { it.value.cancel() }
serviceManager.handleTunnelServiceDestroy()
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
Timber.d("onDestroy")
super.onDestroy()
}
private fun createTunnelNotification(tunnelConfig: TunnelConfig): Notification {
private fun createTunnelNotification(tunnelConf: TunnelConf): Notification {
return notificationManager.createNotification(
WireGuardNotification.NotificationChannels.VPN,
title = "${getString(R.string.tunnel_running)} - ${tunnelConfig.name}",
title = "${getString(R.string.tunnel_running)} - ${tunnelConf.tunName}",
actions =
listOf(
notificationManager.createNotificationAction(
NotificationAction.TUNNEL_OFF,
tunnelConfig.id,
tunnelConf.id,
)
),
onGoing = true,
groupKey = NotificationManager.VPN_GROUP_KEY,
isGroupSummary = true,
)
}
@@ -151,8 +141,6 @@ abstract class BaseTunnelForegroundService : LifecycleService(), TunnelService {
listOf(
notificationManager.createNotificationAction(NotificationAction.TUNNEL_OFF, 0)
),
groupKey = NotificationManager.VPN_GROUP_KEY,
isGroupSummary = true,
)
}
@@ -160,8 +148,6 @@ abstract class BaseTunnelForegroundService : LifecycleService(), TunnelService {
return notificationManager.createNotification(
WireGuardNotification.NotificationChannels.VPN,
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.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
import jakarta.inject.Inject
@@ -30,7 +30,7 @@ constructor(
@IoDispatcher ioDispatcher: CoroutineDispatcher,
@ApplicationScope applicationScope: CoroutineScope,
private val mainDispatcher: CoroutineDispatcher,
private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
private val appDataRepository: AppDataRepository,
) {
private val autoTunnelMutex = Mutex()
@@ -41,39 +41,11 @@ constructor(
val autoTunnelService = _autoTunnelService.asStateFlow()
val tunnelService = _tunnelService.asStateFlow()
init {
applicationScope.launch(ioDispatcher) {
_autoTunnelService
.onEach { _ -> withContext(mainDispatcher) { updateAutoTunnelTile() } }
.launchIn(this)
}
applicationScope.launch(ioDispatcher) {
combine(
autoTunnelSettingsRepository.flow
.map { it.isAutoTunnelEnabled }
.distinctUntilChanged(),
_autoTunnelService,
) { enabled, service ->
enabled to (service != null)
}
.collect { (enabled, isRunning) ->
when {
enabled && !isRunning -> {
autoTunnelMutex.withLock { startServiceInternal() }
}
!enabled && isRunning -> {
autoTunnelMutex.withLock { stopServiceInternal() }
}
}
}
}
}
private val tunnelServiceConnection =
object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
val binder = service as? LocalBinder
_tunnelService.update { binder?.service }
_tunnelService.value = binder?.service
val serviceClass =
when {
name.className.contains("VpnForegroundService") -> "VpnForegroundService"
@@ -85,7 +57,7 @@ constructor(
}
override fun onServiceDisconnected(name: ComponentName) {
_tunnelService.update { null }
_tunnelService.value = null
val serviceClass =
when {
name.className.contains("VpnForegroundService") -> "VpnForegroundService"
@@ -101,39 +73,57 @@ constructor(
object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
val binder = service as? AutoTunnelService.LocalBinder
_autoTunnelService.update { binder?.service }
_autoTunnelService.value = binder?.service
Timber.d("AutoTunnelService connected")
}
override fun onServiceDisconnected(name: ComponentName) {
_autoTunnelService.update { null }
_autoTunnelService.value = null
Timber.d("AutoTunnelService disconnected")
}
}
init {
// Observe changes to the AutoTunnelService and trigger side effects
applicationScope.launch(ioDispatcher) {
_autoTunnelService
.onEach { service ->
withContext(mainDispatcher) { updateAutoTunnelTile() }
if (service == null) {
// The service is disconnected, update the DB state
val settings = appDataRepository.settings.get()
appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = false))
}
}
.launchIn(this)
}
}
fun hasVpnPermission(): Boolean {
return VpnService.prepare(context) == null
}
private fun startServiceInternal() {
if (autoTunnelService.value == null) {
suspend fun startAutoTunnel() {
autoTunnelMutex.withLock {
if (_autoTunnelService.value != null) return
val intent = Intent(context, AutoTunnelService::class.java)
context.startForegroundService(intent)
context.bindService(intent, autoTunnelServiceConnection, Context.BIND_AUTO_CREATE)
}
}
suspend fun startAutoTunnelService() = autoTunnelMutex.withLock { startServiceInternal() }
private fun stopServiceInternal() {
_autoTunnelService.value?.stop()
try {
context.unbindService(autoTunnelServiceConnection)
} catch (e: Exception) {
Timber.e(e, "Failed to unbind AutoTunnelService")
suspend fun stopAutoTunnel() =
autoTunnelMutex.withLock {
if (_autoTunnelService.value == null) return@withLock
_autoTunnelService.value?.let { service ->
service.stop()
try {
context.unbindService(autoTunnelServiceConnection)
} catch (e: Exception) {
Timber.e(e, "Failed to unbind AutoTunnelService")
}
}
}
_autoTunnelService.update { null }
}
suspend fun startTunnelService(appMode: AppMode) =
tunnelMutex.withLock {
@@ -14,18 +14,16 @@ import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelMonitor
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
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.TunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.state.AutoTunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.NetworkState
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.Tunnels
import com.zaneschepke.wireguardautotunnel.util.extensions.to
import com.zaneschepke.wireguardautotunnel.util.extensions.toMillis
import dagger.hilt.android.AndroidEntryPoint
@@ -42,6 +40,8 @@ class AutoTunnelService : LifecycleService() {
@Inject lateinit var networkMonitor: NetworkMonitor
@Inject lateinit var appDataRepository: Provider<AppDataRepository>
@Inject lateinit var notificationManager: NotificationManager
@Inject @IoDispatcher lateinit var ioDispatcher: CoroutineDispatcher
@@ -50,9 +50,7 @@ class AutoTunnelService : LifecycleService() {
@Inject lateinit var tunnelManager: TunnelManager
@Inject lateinit var autoTunnelRepository: Provider<AutoTunnelSettingsRepository>
@Inject lateinit var settingsRepository: GeneralSettingRepository
@Inject lateinit var tunnelsRepository: TunnelRepository
@Inject lateinit var tunnelMonitor: TunnelMonitor
private val defaultState = AutoTunnelState()
@@ -112,14 +110,12 @@ class AutoTunnelService : LifecycleService() {
)
),
onGoing = true,
groupKey = NotificationManager.AUTO_TUNNEL_GROUP_KEY,
isGroupSummary = true,
)
ServiceCompat.startForeground(
this,
NotificationManager.AUTO_TUNNEL_NOTIFICATION_ID,
notification,
Constants.SPECIAL_USE_SERVICE_TYPE_ID,
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
)
}
@@ -129,15 +125,14 @@ class AutoTunnelService : LifecycleService() {
debouncedConnectivityStateFlow
.flowOn(ioDispatcher)
.map(NetworkState::from)
.map(::NetworkChange)
.map { StateChange.NetworkChange(it) }
.distinctUntilChanged()
val settingsFlow =
combineSettings().map { (appMode, settings, tunnels) ->
SettingsChange(appMode, settings, tunnels)
}
combineSettings().map { StateChange.SettingsChange(it.first, it.second) }
val tunnelsFlow = tunnelManager.activeTunnels.map(::ActiveTunnelsChange)
val tunnelsFlow =
tunnelManager.activeTunnels.map { StateChange.ActiveTunnelsChange(it) }
var reevaluationJob: Job? = null
@@ -154,46 +149,36 @@ class AutoTunnelService : LifecycleService() {
}
.first()
val initialState = autoTunnelStateFlow.value
if (initialState != defaultState) {
handleAutoTunnelEvent(
initialState.determineAutoTunnelEvent(NetworkChange(initialState.networkState))
)
}
// use merge to limit the noise of a combine and also increase the scalability of auto
// tunnel handling new states
merge(networkFlow, settingsFlow, tunnelsFlow).collect { change ->
if (change !is ActiveTunnelsChange) {
if (change !is StateChange.ActiveTunnelsChange) {
Timber.d("New state changed to ${change.javaClass.simpleName}")
}
val previousState = autoTunnelStateFlow.value
when (change) {
is NetworkChange -> {
Timber.d("Network change: ${change.networkState}")
is StateChange.NetworkChange -> {
reevaluationJob?.cancel()
val previousState = autoTunnelStateFlow.value
autoTunnelStateFlow.update { it.copy(networkState = change.networkState) }
if (previousState.networkState == change.networkState) {
Timber.d("Duplicate network state change detected, ignoring")
// Android late mobile data state change, we can ignore handling this
if (
isAndroidLateCellularActiveChange(
previousState.networkState,
change.networkState,
)
) {
Timber.d("Android late cellular active state change")
return@collect
}
}
is SettingsChange -> {
is StateChange.SettingsChange -> {
reevaluationJob?.cancel()
autoTunnelStateFlow.update {
it.copy(settings = change.settings, tunnels = change.tunnels)
}
if (
previousState.settings == change.settings &&
previousState.tunnels == change.tunnels
) {
Timber.d("Duplicate settings change detected, ignoring")
return@collect
}
}
is ActiveTunnelsChange -> {
is StateChange.ActiveTunnelsChange -> {
autoTunnelStateFlow.update { it.copy(activeTunnels = change.activeTunnels) }
return@collect
}
@@ -202,34 +187,54 @@ class AutoTunnelService : LifecycleService() {
handleAutoTunnelEvent(autoTunnelStateFlow.value.determineAutoTunnelEvent(change))
reevaluationJob = launch {
val snapshotNetwork = autoTunnelStateFlow.value.networkState
delay(REEVALUATE_CHECK_DELAY)
val currentState = autoTunnelStateFlow.value
if (
currentState != defaultState && currentState.networkState != snapshotNetwork
) {
Timber.d(
"Re-evaluating auto-tunnel state.. (network changed since snapshot)"
)
if (currentState != defaultState) {
Timber.d("Re-evaluating auto-tunnel state..")
handleAutoTunnelEvent(currentState.determineAutoTunnelEvent(change))
} else {
Timber.d("Skipping re-eval: network unchanged or default state")
}
}
}
}
private fun combineSettings(): Flow<Triple<AppMode, AutoTunnelSettings, List<TunnelConfig>>> {
private fun isAndroidLateCellularActiveChange(
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)
}
private fun combineSettings(): Flow<Pair<GeneralSettings, Tunnels>> {
return combine(
settingsRepository.flow.map { it.appMode }.distinctUntilChanged(),
autoTunnelRepository.get().flow,
tunnelsRepository.flow.map { tunnels ->
appDataRepository
.get()
.settings
.flow
.distinctUntilChanged(::areAutoTunnelSettingsTheSame),
appDataRepository.get().tunnels.flow.map { tunnels ->
// isActive is ignored for equality checks so user can manually toggle off
// tunnel with auto-tunnel
tunnels.map { it.copy(isActive = false) }
},
) { appMode, autoTunnel, tunnels ->
Triple(appMode, autoTunnel, tunnels)
) { settings, tunnels ->
Pair(settings, tunnels)
}
.distinctUntilChanged()
}
@@ -338,7 +343,7 @@ class AutoTunnelService : LifecycleService() {
}
) {
is AutoTunnelEvent.Start ->
(event.tunnelConfig ?: tunnelsRepository.getDefaultTunnel())?.let {
(event.tunnelConf ?: appDataRepository.get().getPrimaryOrFirstTunnel())?.let {
tunnelManager.startTunnel(it)
}
is AutoTunnelEvent.Stop -> tunnelManager.stopActiveTunnels()
@@ -349,8 +354,9 @@ class AutoTunnelService : LifecycleService() {
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
private val debouncedConnectivityStateFlow: Flow<ConnectivityState> by lazy {
autoTunnelRepository
appDataRepository
.get()
.settings
.flow
.map { it.debounceDelaySeconds.toMillis() }
.distinctUntilChanged()
@@ -360,6 +366,7 @@ class AutoTunnelService : LifecycleService() {
}
companion object {
const val REEVALUATE_CHECK_DELAY = 3_000L
// try to keep this window short as it will interrupt manual overrides
const val REEVALUATE_CHECK_DELAY = 2_000L
}
}
@@ -1,19 +1,14 @@
package com.zaneschepke.wireguardautotunnel.core.service.autotunnel
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.domain.model.AutoTunnelSettings
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings
import com.zaneschepke.wireguardautotunnel.domain.state.NetworkState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.util.extensions.Tunnels
sealed interface StateChange
sealed class StateChange {
data class NetworkChange(val networkState: NetworkState) : StateChange()
data class NetworkChange(val networkState: NetworkState) : StateChange
data class SettingsChange(val settings: GeneralSettings, val tunnels: Tunnels) : StateChange()
data class SettingsChange(
val appMode: AppMode,
val settings: AutoTunnelSettings,
val tunnels: List<TunnelConfig>,
) : StateChange
data class ActiveTunnelsChange(val activeTunnels: Map<Int, TunnelState>) : StateChange
data class ActiveTunnelsChange(val activeTunnels: Map<Int, TunnelState>) : StateChange()
}
@@ -9,8 +9,7 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.lifecycleScope
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.launch
@@ -18,9 +17,7 @@ import timber.log.Timber
@AndroidEntryPoint
class AutoTunnelControlTile : TileService(), LifecycleOwner {
@Inject lateinit var autoTunnelSettingsRepository: AutoTunnelSettingsRepository
@Inject lateinit var tunnelsRepository: TunnelRepository
@Inject lateinit var appDataRepository: AppDataRepository
@Inject lateinit var serviceManager: ServiceManager
@@ -47,7 +44,7 @@ class AutoTunnelControlTile : TileService(), LifecycleOwner {
}
}
lifecycleScope.launch {
tunnelsRepository.flow.collect {
appDataRepository.tunnels.flow.collect {
if (it.isEmpty()) {
setUnavailable()
}
@@ -60,10 +57,10 @@ class AutoTunnelControlTile : TileService(), LifecycleOwner {
unlockAndRun {
lifecycleScope.launch {
if (serviceManager.autoTunnelService.value != null) {
autoTunnelSettingsRepository.updateAutoTunnelEnabled(false)
serviceManager.stopAutoTunnel()
setInactive()
} else {
autoTunnelSettingsRepository.updateAutoTunnelEnabled(true)
serviceManager.startAutoTunnel()
setActive()
}
}
@@ -84,6 +81,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 TunnelControlTile")
}
return ret
}
private fun setUnavailable() {
runCatching {
qsTile.state = Tile.STATE_UNAVAILABLE
@@ -91,17 +99,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 AutoTunnelControlTile")
}
return ret
}
override val lifecycle: Lifecycle
get() = lifecycleRegistry
}
@@ -13,7 +13,7 @@ import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.launch
@@ -21,8 +21,7 @@ import timber.log.Timber
@AndroidEntryPoint
class TunnelControlTile : TileService(), LifecycleOwner {
@Inject lateinit var tunnelsRepository: TunnelRepository
@Inject lateinit var appDataRepository: AppDataRepository
@Inject lateinit var serviceManager: ServiceManager
@@ -53,7 +52,7 @@ class TunnelControlTile : TileService(), LifecycleOwner {
private suspend fun updateTileState() {
try {
val tunnels = tunnelsRepository.getAll()
val tunnels = appDataRepository.tunnels.getAll()
if (tunnels.isEmpty()) {
setUnavailable()
return
@@ -69,8 +68,9 @@ class TunnelControlTile : TileService(), LifecycleOwner {
// multiple tunnels
// this would be better managed elsewhere
WireGuardAutoTunnel.setLastActiveTunnels(activeIds)
val tunnels = appDataRepository.tunnels.getAll()
val activeTunNames =
tunnels.filter { activeTunnels.keys.contains(it.id) }.map { it.name }
tunnels.filter { activeTunnels.keys.contains(it.id) }.map { it.tunName }
updateTileForActiveTunnels(activeTunNames)
}
else -> updateTileForLastActiveTunnels()
@@ -93,14 +93,15 @@ class TunnelControlTile : TileService(), LifecycleOwner {
val lastActiveIds = WireGuardAutoTunnel.getLastActiveTunnels()
when {
lastActiveIds.isEmpty() -> {
tunnelsRepository.getStartTunnel()?.let { config -> updateTile(config.name, false) }
?: setUnavailable()
appDataRepository.getStartTunnelConfig()?.let { config ->
updateTile(config.tunName, false)
} ?: setUnavailable()
}
lastActiveIds.size > 1 -> updateTile(getString(R.string.multiple), false)
else -> {
val tunnelId = lastActiveIds.first()
tunnelsRepository.getById(tunnelId)?.let { tunnel ->
updateTile(tunnel.name, false)
appDataRepository.tunnels.getById(tunnelId)?.let { tunnel ->
updateTile(tunnel.tunName, false)
} ?: setUnavailable()
}
}
@@ -114,10 +115,10 @@ class TunnelControlTile : TileService(), LifecycleOwner {
return@launch tunnelManager.stopActiveTunnels()
val lastActive = WireGuardAutoTunnel.getLastActiveTunnels()
if (lastActive.isEmpty()) {
tunnelsRepository.getStartTunnel()?.let { tunnelManager.startTunnel(it) }
appDataRepository.getStartTunnelConfig()?.let { tunnelManager.startTunnel(it) }
} else {
lastActive.forEach { id ->
tunnelsRepository.getById(id)?.let { tunnelManager.startTunnel(it) }
appDataRepository.tunnels.getById(id)?.let { tunnelManager.startTunnel(it) }
}
}
}
@@ -159,15 +160,6 @@ 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. */
override fun onBind(intent: Intent): IBinder? {
var ret: IBinder? = null
@@ -179,6 +171,15 @@ class TunnelControlTile : TileService(), LifecycleOwner {
return ret
}
private fun updateTile(name: String, active: Boolean) {
runCatching {
setTileDescription(name)
if (active) return setActive()
setInactive()
}
.onFailure { Timber.e(it) }
}
override val lifecycle: Lifecycle
get() = lifecycleRegistry
}
@@ -2,13 +2,12 @@ package com.zaneschepke.wireguardautotunnel.core.shortcut
import android.os.Bundle
import androidx.activity.ComponentActivity
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelService
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
@@ -17,10 +16,9 @@ import timber.log.Timber
@AndroidEntryPoint
class ShortcutsActivity : ComponentActivity() {
@Inject lateinit var appDataRepository: AppDataRepository
@Inject lateinit var settingsRepository: GeneralSettingRepository
@Inject lateinit var autoTunnelSettingsRepository: AutoTunnelSettingsRepository
@Inject lateinit var tunnelsRepository: TunnelRepository
@Inject lateinit var serviceManager: ServiceManager
@Inject lateinit var tunnelManager: TunnelManager
@@ -29,7 +27,7 @@ class ShortcutsActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
applicationScope.launch {
val settings = settingsRepository.getGeneralSettings()
val settings = appDataRepository.settings.get()
if (settings.isShortcutsEnabled) {
when (intent.getStringExtra(CLASS_NAME_EXTRA_KEY)) {
LEGACY_TUNNEL_SERVICE_NAME,
@@ -37,9 +35,12 @@ class ShortcutsActivity : ComponentActivity() {
val tunnelName = intent.getStringExtra(TUNNEL_NAME_EXTRA_KEY)
Timber.d("Tunnel name extra: $tunnelName")
val tunnelConfig =
tunnelName?.let { tunnelsRepository.findByTunnelName(it) }
?: tunnelsRepository.getDefaultTunnel()
Timber.d("Shortcut action on name: ${tunnelConfig?.name}")
tunnelName?.let {
appDataRepository.tunnels.getAll().firstOrNull {
it.tunName == tunnelName
}
} ?: appDataRepository.getStartTunnelConfig()
Timber.d("Shortcut action on name: ${tunnelConfig?.tunName}")
tunnelConfig?.let {
when (intent.action) {
Action.START.name -> tunnelManager.startTunnel(it)
@@ -51,10 +52,8 @@ class ShortcutsActivity : ComponentActivity() {
AutoTunnelService::class.java.simpleName,
LEGACY_AUTO_TUNNEL_SERVICE_NAME -> {
when (intent.action) {
Action.START.name ->
autoTunnelSettingsRepository.updateAutoTunnelEnabled(true)
Action.STOP.name ->
autoTunnelSettingsRepository.updateAutoTunnelEnabled(false)
Action.START.name -> serviceManager.startAutoTunnel()
Action.STOP.name -> serviceManager.stopAutoTunnel()
}
}
}
@@ -1,27 +1,32 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException
import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.state.LogHealthState
import com.zaneschepke.wireguardautotunnel.domain.state.PingState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import java.util.concurrent.ConcurrentHashMap
import kotlin.coroutines.cancellation.CancellationException
import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.amnezia.awg.crypto.Key
import timber.log.Timber
abstract class BaseTunnel(
@ApplicationScope protected val applicationScope: CoroutineScope,
@IoDispatcher protected val ioDispatcher: CoroutineDispatcher,
protected val appDataRepository: AppDataRepository,
protected val serviceManager: ServiceManager,
) : TunnelProvider {
protected val errors = MutableSharedFlow<Pair<String, BackendCoreException>>()
@@ -33,35 +38,33 @@ abstract class BaseTunnel(
protected val activeTuns = MutableStateFlow<Map<Int, TunnelState>>(emptyMap())
override val activeTunnels = activeTuns.asStateFlow()
protected val tunJobs = ConcurrentHashMap<Int, Job>()
private val tunJobs = ConcurrentHashMap<Int, Job>()
private val tunMutex = Mutex()
private val tunStatusMutex = Mutex()
abstract fun tunnelStateFlow(tunnelConfig: TunnelConfig): Flow<TunnelStatus>
abstract fun tunnelStateFlow(tunnelConf: TunnelConf): Flow<TunnelStatus>
abstract override fun setBackendMode(backendMode: BackendMode)
abstract override fun getBackendMode(): BackendMode
abstract override suspend fun forceStopTunnel(tunnelId: Int)
abstract override fun handleDnsReresolve(tunnelConfig: TunnelConfig): Boolean
abstract override fun handleDnsReresolve(tunnelConf: TunnelConf): Boolean
abstract override fun getStatistics(tunnelId: Int): TunnelStatistics?
override fun hasVpnPermission(): Boolean {
return serviceManager.hasVpnPermission()
}
override suspend fun updateTunnelStatus(
tunnelId: Int,
status: TunnelStatus?,
stats: TunnelStatistics?,
pingStates: Map<String, PingState>?,
pingStates: Map<Key, PingState>?,
logHealthState: LogHealthState?,
) {
tunStatusMutex.withLock {
activeTuns.update { currentTuns ->
if (!currentTuns.containsKey(tunnelId) && status != TunnelStatus.Starting) {
Timber.d("Ignoring update for inactive tunnel $tunnelId")
return@update currentTuns
}
val existingState = currentTuns[tunnelId] ?: TunnelState()
val newStatus = status ?: existingState.status
if (newStatus == TunnelStatus.Down) {
@@ -98,51 +101,38 @@ abstract class BaseTunnel(
}
}
override suspend fun startTunnel(tunnelConfig: TunnelConfig) {
override suspend fun startTunnel(tunnelConf: TunnelConf) {
tunMutex.withLock {
if (
activeTuns.value.containsKey(tunnelConfig.id) ||
tunJobs.containsKey(tunnelConfig.id)
) {
return Timber.w("Tunnel is already running: ${tunnelConfig.name}")
if (activeTuns.value.containsKey(tunnelConf.id) || tunJobs.containsKey(tunnelConf.id)) {
return Timber.w("Tunnel is already running: ${tunnelConf.tunName}")
}
updateTunnelStatus(tunnelConfig.id, TunnelStatus.Starting)
updateTunnelStatus(tunnelConf.id, TunnelStatus.Starting)
val job =
applicationScope.launch(ioDispatcher) {
applicationScope.launch {
try {
tunnelStateFlow(tunnelConfig).collect { status ->
updateTunnelStatus(tunnelConfig.id, status)
tunnelStateFlow(tunnelConf).collect { status ->
updateTunnelStatus(tunnelConf.id, status)
serviceManager.updateTunnelTile()
}
} catch (e: BackendCoreException) {
errors.emit(tunnelConfig.name to e)
updateTunnelStatus(tunnelConfig.id, TunnelStatus.Down)
errors.emit(tunnelConf.tunName to e)
updateTunnelStatus(tunnelConf.id, TunnelStatus.Down)
} catch (_: CancellationException) {}
}
tunJobs[tunnelConfig.id] = job
tunJobs[tunnelConf.id] = job
job.invokeOnCompletion {
tunJobs.remove(tunnelConfig.id)
activeTuns.update { it - tunnelConfig.id }
tunJobs.remove(tunnelConf.id)
activeTuns.update { it - tunnelConf.id }
}
}
}
override suspend fun stopTunnel(tunnelId: Int) {
tunMutex.withLock {
val currentState = activeTuns.value[tunnelId]?.status ?: return@withLock
updateTunnelStatus(tunnelId, TunnelStatus.Stopping)
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)
}
tunJobs[tunnelId]?.cancel() // Triggers awaitClose to stop backend
}
}
@@ -150,9 +140,4 @@ abstract class BaseTunnel(
Timber.d("Removing job for $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
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import kotlinx.coroutines.flow.MutableStateFlow
fun Map<TunnelConfig, TunnelState>.allDown(): Boolean {
fun Map<TunnelConf, TunnelState>.allDown(): Boolean {
return this.all { it.value.status.isDown() }
}
fun Map<TunnelConfig, TunnelState>.hasActive(): Boolean {
fun Map<TunnelConf, TunnelState>.hasActive(): Boolean {
return this.any { it.value.status.isUp() }
}
fun Map<TunnelConfig, TunnelState>.getValueById(id: Int): TunnelState? {
fun Map<TunnelConf, TunnelState>.getValueById(id: Int): TunnelState? {
val key = this.keys.find { it.id == id }
return key?.let { this@getValueById[it] }
}
fun Map<TunnelConfig, TunnelState>.getKeyById(id: Int): TunnelConfig? {
fun Map<TunnelConf, TunnelState>.getKeyById(id: Int): TunnelConf? {
return this.keys.find { it.id == id }
}
fun Map<TunnelConfig, TunnelState>.isUp(tunnelConfig: TunnelConfig): Boolean {
return this.getValueById(tunnelConfig.id)?.status?.isUp() ?: false
fun Map<TunnelConf, TunnelState>.isUp(tunnelConf: TunnelConf): Boolean {
return this.getValueById(tunnelConf.id)?.status?.isUp() ?: false
}
fun MutableStateFlow<Map<TunnelConfig, TunnelState>>.exists(id: Int): Boolean {
fun MutableStateFlow<Map<TunnelConf, TunnelState>>.exists(id: Int): Boolean {
return this.value.any { it.key.id == id }
}
fun MutableStateFlow<Map<TunnelConfig, TunnelState>>.isUp(id: Int): Boolean {
fun MutableStateFlow<Map<TunnelConf, TunnelState>>.isUp(id: Int): Boolean {
return this.value.any { it.key.id == id && it.value.status == TunnelStatus.Up }
}
fun MutableStateFlow<Map<TunnelConfig, TunnelState>>.isStarting(id: Int): Boolean {
fun MutableStateFlow<Map<TunnelConf, TunnelState>>.isStarting(id: Int): Boolean {
return this.value.any { it.key.id == id && it.value.status == TunnelStatus.Starting }
}
fun MutableStateFlow<Map<TunnelConfig, TunnelState>>.findTunnel(id: Int): TunnelConfig? {
fun MutableStateFlow<Map<TunnelConf, TunnelState>>.findTunnel(id: Int): TunnelConf? {
return this.value.keys.find { it.id == id }
}
@@ -3,61 +3,56 @@ package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.wireguard.android.backend.Backend
import com.wireguard.android.backend.BackendException
import com.wireguard.android.backend.Tunnel as WgTunnel
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.di.Kernel
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.domain.state.WireGuardStatistics
import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendCoreException
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
class KernelTunnel
@Inject
constructor(
@ApplicationScope applicationScope: CoroutineScope,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
serviceManager: ServiceManager,
appDataRepository: AppDataRepository,
@Kernel private val backend: Backend,
) : BaseTunnel(applicationScope, ioDispatcher) {
) : BaseTunnel(applicationScope, appDataRepository, serviceManager) {
private val runtimeTunnels = ConcurrentHashMap<Int, WgTunnel>()
// TODO Add DNS settings
override fun tunnelStateFlow(tunnelConfig: TunnelConfig): Flow<TunnelStatus> = callbackFlow {
if (!tunnelConfig.isNameKernelCompatible) close(BackendCoreException.TunnelNameTooLong)
override fun tunnelStateFlow(tunnelConf: TunnelConf): Flow<TunnelStatus> = callbackFlow {
if (!tunnelConf.isNameKernelCompatible) close(BackendCoreException.TunnelNameTooLong)
val stateChannel = Channel<WgTunnel.State>()
val runtimeTunnel = RuntimeWgTunnel(tunnelConfig, stateChannel)
runtimeTunnels[tunnelConfig.id] = runtimeTunnel
val runtimeTunnel = RuntimeWgTunnel(tunnelConf, stateChannel)
runtimeTunnels[tunnelConf.id] = runtimeTunnel
val consumerJob = launch {
stateChannel.consumeAsFlow().collect { state -> trySend(state.asTunnelState()) }
}
try {
withTimeout(STARTUP_TIMEOUT_MS) {
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()
updateTunnelStatus(tunnelConf.id, TunnelStatus.Starting)
backend.setState(runtimeTunnel, WgTunnel.State.UP, tunnelConf.toWgConfig())
} catch (e: BackendException) {
close(e.toBackendCoreException())
} catch (e: IllegalArgumentException) {
@@ -72,11 +67,11 @@ constructor(
try {
backend.setState(runtimeTunnel, WgTunnel.State.DOWN, null)
} catch (e: BackendException) {
errors.tryEmit(tunnelConfig.name to e.toBackendCoreException())
errors.tryEmit(tunnelConf.tunName to e.toBackendCoreException())
} finally {
consumerJob.cancel()
stateChannel.close()
runtimeTunnels.remove(tunnelConfig.id)
runtimeTunnels.remove(tunnelConf.id)
trySend(TunnelStatus.Down)
close()
}
@@ -101,26 +96,11 @@ constructor(
return BackendMode.Inactive
}
override fun handleDnsReresolve(tunnelConfig: TunnelConfig): Boolean {
override fun handleDnsReresolve(tunnelConf: TunnelConf): Boolean {
throw NotImplementedError()
}
override suspend fun runningTunnelNames(): Set<String> {
return backend.runningTunnelNames
}
override suspend fun forceStopTunnel(tunnelId: Int) {
val runtimeTunnel = runtimeTunnels[tunnelId] ?: return
try {
backend.setState(runtimeTunnel, 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
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import kotlinx.coroutines.channels.Channel
import org.amnezia.awg.backend.Tunnel
class RuntimeAwgTunnel(
private val tunnelConfig: TunnelConfig,
private val tunnelConf: TunnelConf,
private val stateChannel: Channel<Tunnel.State>,
) : Tunnel {
override fun getName() = tunnelConfig.name
override fun getName() = tunnelConf.tunName
override fun onStateChange(newState: Tunnel.State) {
stateChannel.trySend(newState)
}
override fun isIpv4ResolutionPreferred() = tunnelConfig.isIpv4Preferred
override fun isIpv4ResolutionPreferred() = tunnelConf.isIpv4Preferred
}
@@ -1,15 +1,15 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import kotlinx.coroutines.channels.Channel
class RuntimeWgTunnel(
private val config: TunnelConfig,
private val config: TunnelConf,
private val stateChannel: Channel<Tunnel.State>,
) : Tunnel {
override fun getName() = config.name
override fun getName() = config.tunName
override fun onStateChange(newState: Tunnel.State) {
stateChannel.trySend(newState)
@@ -1,32 +1,27 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
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.di.*
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException
import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage
import com.zaneschepke.wireguardautotunnel.domain.model.AutoTunnelSettings
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.state.LogHealthState
import com.zaneschepke.wireguardautotunnel.domain.state.PingState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
import kotlin.concurrent.atomics.AtomicBoolean
import kotlin.concurrent.atomics.AtomicReference
import kotlin.concurrent.atomics.ExperimentalAtomicApi
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.amnezia.awg.crypto.Key
import timber.log.Timber
@OptIn(ExperimentalCoroutinesApi::class, ExperimentalAtomicApi::class)
@@ -37,29 +32,14 @@ constructor(
@Userspace private val userspaceTunnel: TunnelProvider,
@ProxyUserspace private val proxyUserspaceTunnel: TunnelProvider,
private val serviceManager: ServiceManager,
private val settingsRepository: GeneralSettingRepository,
private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
private val tunnelsRepository: TunnelRepository,
private val tunnelMonitor: TunnelMonitor,
@ApplicationScope private val applicationScope: CoroutineScope,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
private val appDataRepository: AppDataRepository,
@ApplicationScope applicationScope: CoroutineScope,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
) : 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 monitoringJobs = ConcurrentHashMap<Int, Job>()
private val ddnsMutex = Mutex()
private val ddnsJobs = ConcurrentHashMap<Int, Job>()
private data class SideEffectState(
val activeTuns: Map<Int, TunnelState>,
val tuns: List<TunnelConfig>,
val tuns: List<TunnelConf>,
val settings: GeneralSettings,
val previouslyActive: Map<Int, TunnelState>,
)
@@ -69,18 +49,15 @@ constructor(
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 sideEffectChannelFlow =
MutableStateFlow<Channel<SideEffectState>>(Channel(Channel.CONFLATED))
private val tunnelProviderFlow: StateFlow<TunnelProvider> = run {
val currentBackend = AtomicReference(userspaceTunnel)
val currentSettings = AtomicReference(GeneralSettings())
val initialEmit = AtomicBoolean(true)
settingsRepository.flow
appDataRepository.settings.flow
.filterNotNull()
// ignore default state
.filterNot { it == GeneralSettings() }
@@ -122,77 +99,69 @@ constructor(
override val activeTunnels: StateFlow<Map<Int, TunnelState>> = run {
val activeTunsReference: AtomicReference<Map<Int, TunnelState>> =
AtomicReference(emptyMap())
tunnelProviderFlow
.flatMapLatest { backend ->
// Create a new channel for each backend to reset side-effect processing
val newChannel = Channel<SideEffectState>(Channel.CONFLATED)
sideEffectChannelFlow.value = newChannel
val sideEffects =
listOf(
SideEffectWithCondition(
effect = { s ->
handleTunnelServiceChange(s.settings.appMode, s.activeTuns)
},
condition = { s -> s.activeTuns.size != s.previouslyActive.size },
),
SideEffectWithCondition(
effect = { s ->
handleActiveTunnelsChange(s.previouslyActive, s.activeTuns, s.tuns)
},
condition = { s -> s.activeTuns.size != s.previouslyActive.size },
),
// TODO Not for kernel mode for now
SideEffectWithCondition(
effect = { s -> handleTunnelMonitoringChanges(s.activeTuns, s.tuns) },
condition = { s ->
s.tuns.any {
it.restartOnPingFailure && s.activeTuns.keys.contains(it.id)
} && s.settings.appMode != AppMode.KERNEL
},
),
)
applicationScope.launch(ioDispatcher) {
for (state in newChannel) {
supervisorScope {
sideEffects
.filter { it.condition(state) }
.forEach { sideEffect ->
launch {
try {
sideEffect.effect(state)
} catch (e: Exception) {
Timber.e(e, "Side effect failed")
}
}
}
}
}
}
combine(
backend.activeTunnels,
tunnelsRepository.flow,
settingsRepository.flow.filterNotNull(),
appDataRepository.tunnels.flow,
appDataRepository.settings.flow.filterNotNull(),
) { activeTuns, tuns, settings ->
Triple(activeTuns, tuns, settings)
}
}
.onStart { handleRestore() }
.onStart { handleStateRestore() }
.onEach { (activeTuns, tuns, settings) ->
val previouslyActive = activeTunsReference.exchange(activeTuns)
val state = SideEffectState(activeTuns, tuns, settings, previouslyActive)
applicationScope.launch(ioDispatcher) {
supervisorScope {
val sideEffects =
listOf(
SideEffectWithCondition(
effect = { s ->
handleTunnelServiceChange(s.settings.appMode, s.activeTuns)
},
condition = { s ->
s.activeTuns.size != s.previouslyActive.size
},
),
SideEffectWithCondition(
effect = { s ->
handleTunnelsActiveChange(
s.previouslyActive,
s.activeTuns,
s.tuns,
)
},
condition = { s ->
s.activeTuns.size != s.previouslyActive.size
},
),
SideEffectWithCondition(
effect = { s ->
handleDynamicDnsMonitoring(s.activeTuns, s.tuns, s.settings)
},
condition = { s ->
s.activeTuns.keys != s.previouslyActive.keys
},
),
SideEffectWithCondition(
effect = { s ->
handleFullTunnelMonitoring(s.activeTuns, s.tuns, s.settings)
},
condition = { s ->
s.activeTuns.keys != s.previouslyActive.keys
},
),
)
sideEffects
.filter { it.condition(state) }
.forEach { sideEffect ->
launch {
try {
sideEffect.effect(state)
} catch (e: Exception) {
Timber.e(e, "Side effect failed")
}
}
}
}
}
sideEffectChannelFlow.value.trySend(
SideEffectState(activeTuns, tuns, settings, previouslyActive)
)
}
.map { (activeTuns, _, _) -> activeTuns }
.stateIn(
@@ -203,59 +172,44 @@ constructor(
}
@OptIn(ExperimentalCoroutinesApi::class)
override val errorEvents: SharedFlow<Pair<String?, BackendCoreException>> =
merge(localErrorEvents, tunnelProviderFlow.flatMapLatest { it.errorEvents })
.shareIn(
scope = applicationScope + ioDispatcher,
started = SharingStarted.Eagerly,
replay = 0,
)
@OptIn(ExperimentalCoroutinesApi::class)
override val messageEvents: SharedFlow<Pair<String?, BackendMessage>> =
merge(localMessageEvents, tunnelProviderFlow.flatMapLatest { it.messageEvents })
override val errorEvents: SharedFlow<Pair<String, BackendCoreException>> =
tunnelProviderFlow
.flatMapLatest { it.errorEvents }
.shareIn(
scope = applicationScope.plus(ioDispatcher),
started = SharingStarted.Eagerly,
replay = 0,
)
@OptIn(ExperimentalCoroutinesApi::class)
override val messageEvents: SharedFlow<Pair<String, BackendMessage>> =
tunnelProviderFlow
.flatMapLatest { it.messageEvents }
.shareIn(
scope = applicationScope.plus(ioDispatcher),
started = SharingStarted.Eagerly,
replay = 0,
)
override fun hasVpnPermission(): Boolean {
return userspaceTunnel.hasVpnPermission()
}
override fun getStatistics(tunnelId: Int): TunnelStatistics? {
return tunnelProviderFlow.value.getStatistics(tunnelId)
}
override suspend fun startTunnel(tunnelConfig: TunnelConfig) {
if (activeTunnels.value.containsKey(tunnelConfig.id)) return
val provider = tunnelProviderFlow.value
val isKernel = provider is KernelTunnel
if (!isKernel && activeTunnels.value.isNotEmpty()) {
override suspend fun startTunnel(tunnelConf: TunnelConf) {
// for VPN Mode, we need to stop active tunnels as we can only have one active at a time
if (activeTunnels.value.isNotEmpty() && tunnelProviderFlow.value == userspaceTunnel)
stopActiveTunnels()
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)
tunnelProviderFlow.value.startTunnel(tunnelConf)
}
override suspend fun stopTunnel(tunnelId: Int) {
tunnelProviderFlow.value.stopTunnel(tunnelId)
}
override suspend fun forceStopTunnel(tunnelId: Int) {
tunnelProviderFlow.value.forceStopTunnel(tunnelId)
}
override suspend fun stopActiveTunnels() {
tunnelProviderFlow.value.stopActiveTunnels()
}
@@ -272,15 +226,15 @@ constructor(
return tunnelProviderFlow.value.runningTunnelNames()
}
override fun handleDnsReresolve(tunnelConfig: TunnelConfig): Boolean {
return tunnelProviderFlow.value.handleDnsReresolve(tunnelConfig)
override fun handleDnsReresolve(tunnelConf: TunnelConf): Boolean {
return tunnelProviderFlow.value.handleDnsReresolve(tunnelConf)
}
override suspend fun updateTunnelStatus(
tunnelId: Int,
status: TunnelStatus?,
stats: TunnelStatistics?,
pingStates: Map<String, PingState>?,
pingStates: Map<Key, PingState>?,
logHealthState: LogHealthState?,
) {
tunnelProviderFlow.value.updateTunnelStatus(
@@ -299,19 +253,18 @@ constructor(
if (activeTuns.isEmpty()) serviceManager.stopTunnelService()
if (activeTuns.isNotEmpty() && serviceManager.tunnelService.value == null)
serviceManager.startTunnelService(appMode)
serviceManager.updateTunnelTile()
}
private fun handleLockDownModeInit(withLanBypass: Boolean) {
val allowedIps = if (withLanBypass) TunnelConfig.IPV4_PUBLIC_NETWORKS else emptySet()
val allowedIps = if (withLanBypass) TunnelConf.IPV4_PUBLIC_NETWORKS else emptySet()
try {
if (serviceManager.hasVpnPermission()) {
// TODO handle situation where they don't have vpn permission, request it
if (hasVpnPermission()) {
proxyUserspaceTunnel.setBackendMode(BackendMode.KillSwitch(allowedIps))
} else {
throw BackendCoreException.NotAuthorized
}
} catch (e: BackendCoreException) {
localErrorEvents.tryEmit(null to e)
// TODO expose this error to user
Timber.e(e)
}
}
@@ -325,75 +278,66 @@ constructor(
proxyUserspaceTunnel.setBackendMode(BackendMode.Inactive)
}
private fun isVpnAuthorized(
mode: AppMode,
hasVpnPermission: () -> Boolean = { serviceManager.hasVpnPermission() },
): Boolean {
return when (mode) {
AppMode.VPN,
AppMode.LOCK_DOWN -> hasVpnPermission()
AppMode.KERNEL,
AppMode.PROXY -> true
private suspend fun handleStateRestore() {
val settings = appDataRepository.settings.flow.first()
if (settings.isRestoreOnBootEnabled) {
// if auto tun enabled, reset active and restore auto tun, letting it start appropriate
// tuns
if (settings.isAutoTunnelEnabled) {
appDataRepository.tunnels.resetActiveTunnels()
return serviceManager.startAutoTunnel()
}
val tunnels = appDataRepository.tunnels.flow.first()
when (settings.appMode) {
// TODO eventually, lockdown/proxy can support multi
AppMode.VPN,
AppMode.LOCK_DOWN,
AppMode.PROXY ->
tunnels
.firstOrNull { it.isActive }
?.let {
// clear any duplicates
appDataRepository.tunnels.resetActiveTunnels()
startTunnel(it)
}
// kernel supports multi
AppMode.KERNEL ->
tunnels.filter { it.isActive }.forEach { conf -> startTunnel(conf) }
}
}
}
suspend fun handleRestore() =
withContext(ioDispatcher) {
val settings = settingsRepository.getGeneralSettings()
val autoTunnelSettings = autoTunnelSettingsRepository.getAutoTunnelSettings()
val tunnels = tunnelsRepository.getAll()
if (autoTunnelSettings.isAutoTunnelEnabled)
return@withContext restoreAutoTunnel(autoTunnelSettings)
if (isVpnAuthorized(settings.appMode)) {
when (val mode = settings.appMode) {
AppMode.VPN,
AppMode.PROXY,
AppMode.LOCK_DOWN -> {
if (mode == AppMode.LOCK_DOWN)
handleLockDownModeInit(settings.isLanOnKillSwitchEnabled)
tunnels.firstOrNull { it.isActive }?.let { startTunnel(it) }
}
AppMode.KERNEL ->
tunnels.filter { it.isActive }.forEach { conf -> startTunnel(conf) }
private suspend fun handleTunnelMonitoringChanges(
activeTuns: Map<Int, TunnelState>,
configs: List<TunnelConf>,
) {
configs
.filter { it.restartOnPingFailure && activeTuns.keys.contains(it.id) }
.forEach { conf ->
val tunState = activeTuns[conf.id] ?: return@forEach
if (tunState.health() == TunnelState.Health.UNHEALTHY) {
runCatching {
val updated = handleDnsReresolve(conf)
// TODO user messages
if (updated) {
Timber.i("Successfully update the peer endpoint to new address.")
} else {
Timber.i("Current endpoint address is already up to date.")
}
}
.onFailure {
Timber.e(it, "Failed to handle dns re-resolution for ${conf.tunName}")
}
// TODO backoff
delay(30_000L)
}
} else {
localErrorEvents.emit(null to BackendCoreException.NotAuthorized)
}
}
private suspend fun restoreAutoTunnel(autoTunnelSettings: AutoTunnelSettings) {
autoTunnelSettingsRepository.upsert(autoTunnelSettings.copy(isAutoTunnelEnabled = true))
serviceManager.startAutoTunnelService()
}
suspend fun handleReboot() =
withContext(ioDispatcher) {
val settings = settingsRepository.getGeneralSettings()
val autoTunnelSettings = autoTunnelSettingsRepository.getAutoTunnelSettings()
val defaultTunnel = tunnelsRepository.getStartTunnel()
if (autoTunnelSettings.startOnBoot)
return@withContext restoreAutoTunnel(autoTunnelSettings)
if (settings.isRestoreOnBootEnabled) {
tunnelsRepository.resetActiveTunnels()
if (isVpnAuthorized(settings.appMode)) {
when (val mode = settings.appMode) {
AppMode.LOCK_DOWN ->
handleLockDownModeInit(settings.isLanOnKillSwitchEnabled)
AppMode.KERNEL,
AppMode.VPN,
AppMode.PROXY -> Unit
}
defaultTunnel?.let { startTunnel(it) }
} else {
localErrorEvents.emit(null to BackendCoreException.NotAuthorized)
}
}
}
private suspend fun handleTunnelsActiveChange(
private suspend fun handleActiveTunnelsChange(
previousActiveTuns: Map<Int, TunnelState>,
activeTuns: Map<Int, TunnelState>,
tuns: List<TunnelConfig>,
tuns: List<TunnelConf>,
) {
val relevantTunnels = previousActiveTuns.keys + activeTuns.keys
@@ -406,135 +350,17 @@ constructor(
tuns
.find { it.id == tunnelId }
?.let { dbTunnelConf ->
tunnelsRepository.save(dbTunnelConf.copy(isActive = true))
appDataRepository.tunnels.save(dbTunnelConf.copy(isActive = true))
}
}
wasActive && !isActiveNow -> {
tuns
.find { it.id == tunnelId }
?.let { dbTunnelConf ->
tunnelsRepository.save(dbTunnelConf.copy(isActive = false))
appDataRepository.tunnels.save(dbTunnelConf.copy(isActive = false))
}
}
}
}
}
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(
activeTuns: Map<Int, TunnelState>,
configs: List<TunnelConfig>,
settings: GeneralSettings,
) =
monitoringMutex.withLock {
val activeIds = activeTuns.keys.toSet()
val currentJobs = monitoringJobs.keys.toSet()
val obsoleteIds = currentJobs - activeIds
Timber.d(
"Monitoring: Active IDs: $activeIds, Obsolete IDs: $obsoleteIds, Total jobs before: ${monitoringJobs.size}"
)
obsoleteIds.forEach { id ->
monitoringJobs[id]?.cancel()
monitoringJobs.remove(id)
}
activeIds.forEach { id ->
if (monitoringJobs.containsKey(id)) return@forEach // Skip if already monitored
configs.find { it.id == id } ?: return@forEach
val tunStateFlow =
activeTunnels.map { it[id] }.stateIn(applicationScope + ioDispatcher)
val newJob =
applicationScope.launch(ioDispatcher) {
tunnelMonitor.startMonitoring(
id,
withLogs = settings.appMode != AppMode.KERNEL,
tunStateFlow = tunStateFlow,
getStatistics = { tunnelId -> getStatistics(tunnelId) },
updateTunnelStatus = { tid, _, stats, pings, logHealth ->
updateTunnelStatus(tid, null, stats, pings, logHealth)
},
)
}
monitoringJobs[id] = newJob
}
}
companion object {
const val BASE_BACKOFF = 30_000L
const val MAX_BACKOFF_TIME = 300_000L
}
}
@@ -4,63 +4,45 @@ import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.state.*
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.state.FailureReason
import com.zaneschepke.wireguardautotunnel.domain.state.LogHealthState
import com.zaneschepke.wireguardautotunnel.domain.state.PingState
import com.zaneschepke.wireguardautotunnel.util.extensions.toMillis
import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils
import dagger.hilt.android.scopes.ServiceScoped
import inet.ipaddr.AddressValueException
import inet.ipaddr.IPAddress
import inet.ipaddr.IPAddressString
import io.ktor.util.collections.*
import javax.inject.Inject
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import org.amnezia.awg.crypto.Key
import timber.log.Timber
@ServiceScoped
class TunnelMonitor
@Inject
constructor(
private val settingsRepository: GeneralSettingRepository,
private val tunnelsRepository: TunnelRepository,
private val monitoringSettingsRepository: MonitoringSettingsRepository,
private val appDataRepository: AppDataRepository,
private val tunnelManager: TunnelManager,
private val networkMonitor: NetworkMonitor,
private val networkUtils: NetworkUtils,
private val logReader: LogReader,
) {
@OptIn(FlowPreview::class)
suspend fun startMonitoring(
tunnelId: Int,
withLogs: Boolean,
tunStateFlow: StateFlow<TunnelState?>,
getStatistics: suspend (Int) -> TunnelStatistics?,
updateTunnelStatus:
suspend (
Int, TunnelStatus?, TunnelStatistics?, Map<String, PingState>?, LogHealthState?,
) -> Unit,
): Job = coroutineScope {
suspend fun startMonitoring(tunnelId: Int, withLogs: Boolean): Job = coroutineScope {
launch {
val config = tunnelsRepository.getById(tunnelId) ?: return@launch
launch { startPingMonitor(config, tunStateFlow, updateTunnelStatus) }
launch { startWgStatsPoll(tunnelId, getStatistics, updateTunnelStatus) }
if (withLogs) launch { startLogsMonitor(config, updateTunnelStatus) }
val config = appDataRepository.tunnels.getById(tunnelId) ?: return@launch
launch { startPingMonitor(config) }
launch { startWgStatsPoll(config.id) }
if (withLogs) launch { startLogsMonitor(config) }
}
}
private suspend fun startLogsMonitor(
tunnelConfig: TunnelConfig,
updateTunnelStatus:
suspend (
Int, TunnelStatus?, TunnelStatistics?, Map<String, PingState>?, LogHealthState?,
) -> Unit,
) {
private suspend fun startLogsMonitor(tunnelConf: TunnelConf) {
logReader.liveLogs
.filter { log -> log.tag.contains(tunnelConfig.name) }
.filter { log -> log.tag.contains(tunnelConf.tunName) }
.mapNotNull { log ->
val now = System.currentTimeMillis()
@@ -76,20 +58,16 @@ constructor(
}
.distinctUntilChangedBy { it.isHealthy } // Only emit when health changes
.collect { logHealthState ->
Timber.d("Tunnel log health updated for ${tunnelConfig.name}: $logHealthState")
updateTunnelStatus(tunnelConfig.id, null, null, null, logHealthState)
Timber.d("Tunnel log health updated for ${tunnelConf.tunName}: $logHealthState")
tunnelManager.updateTunnelStatus(tunnelConf.id, logHealthState = logHealthState)
}
}
private suspend fun startPingMonitor(
tunnelConfig: TunnelConfig,
tunStateFlow: StateFlow<TunnelState?>,
updateTunnelStatus:
suspend (
Int, TunnelStatus?, TunnelStatistics?, Map<String, PingState>?, LogHealthState?,
) -> Unit,
) = coroutineScope {
val pingStatsFlow = MutableStateFlow<Map<String, PingState>>(emptyMap())
private suspend fun startPingMonitor(tunnelConf: TunnelConf) = coroutineScope {
val pingStatsFlow = MutableStateFlow<Map<Key, PingState>>(emptyMap())
val tunStateFlow =
tunnelManager.activeTunnels.mapNotNull { it[tunnelConf.id] }.stateIn(this)
val connectivityStateFlow = networkMonitor.connectivityStateFlow.stateIn(this)
@@ -114,162 +92,128 @@ constructor(
.distinctUntilChanged()
.stateIn(this)
combine(
settingsRepository.flow.distinctUntilChangedBy { it.appMode },
monitoringSettingsRepository.flow,
) { settings, monitorSettings ->
Pair(settings.appMode, monitorSettings)
appDataRepository.settings.flow
.distinctUntilChanged { old, new ->
old.isPingEnabled == new.isPingEnabled &&
old.tunnelPingIntervalSeconds == new.tunnelPingIntervalSeconds &&
old.tunnelPingAttempts == new.tunnelPingAttempts &&
old.tunnelPingTimeoutSeconds == new.tunnelPingTimeoutSeconds
old.appMode == new.appMode
}
.collectLatest { (appMode, settings) ->
.collectLatest { settings ->
if (!settings.isPingEnabled) return@collectLatest
// TODO for now until we get monitoring for these modes
if (appMode == AppMode.LOCK_DOWN || appMode == AppMode.PROXY) return@collectLatest
if (settings.appMode == AppMode.LOCK_DOWN || settings.appMode == AppMode.PROXY)
return@collectLatest
Timber.d("Starting pinger for ${tunnelConfig.name} with settings")
Timber.d("Starting pinger for ${tunnelConf.tunName} with settings")
val config = tunnelConfig.toAmConfig()
val config = tunnelConf.toAmConfig()
val pingablePeers = config.peers.filter { it.allowedIps.isNotEmpty() }
if (pingablePeers.isEmpty()) return@collectLatest
suspend fun performPing() {
val updates = ConcurrentMap<String, PingState>()
val updates = ConcurrentMap<Key, PingState>()
pingablePeers
.map { it.publicKey.toBase64() to it }
.forEach { (key, peer) ->
ensureActive()
val previousState = pingStatsFlow.value[key] ?: PingState()
pingablePeers.forEach { peer ->
val previousState = pingStatsFlow.value[peer.publicKey] ?: PingState()
val allowedIpStr = peer.allowedIps.firstOrNull()?.toString()
if (allowedIpStr == null) {
updates[key] =
previousState.copy(
isReachable = false,
failureReason = FailureReason.NoResolvedEndpoint,
lastPingAttemptMillis = System.currentTimeMillis(),
)
return@forEach
}
val allowedIpStr = peer.allowedIps.firstOrNull()?.toString()
if (allowedIpStr == null) {
updates[peer.publicKey] =
previousState.copy(
isReachable = false,
failureReason = FailureReason.NoResolvedEndpoint,
lastPingAttemptMillis = System.currentTimeMillis(),
)
return@forEach
}
val host =
tunnelConfig.pingTarget
?: run {
val host =
tunnelConf.pingTarget
?: {
val parts = allowedIpStr.split("/")
val internalIp =
if (parts.size == 2) parts[0] else allowedIpStr
val prefix =
if (parts.size == 2) parts[1].toIntOrNull() ?: 32
else 32
val cleanedIp = internalIp.removeSurrounding("[", "]")
val defaultCloudflare =
if (cleanedIp.contains(":")) CLOUDFLARE_IPV6_IP
else CLOUDFLARE_IPV4_IP
if (prefix <= 1) {
defaultCloudflare
CLOUDFLARE_IPV4_IP
} else {
try {
val addrStr = IPAddressString(cleanedIp)
val addr: IPAddress =
addrStr.address
?: throw AddressValueException(
"Invalid IP: $cleanedIp"
)
val isIpv6 = addr.isIPv6
val cloudflareIp =
if (isIpv6) CLOUDFLARE_IPV6_IP
else CLOUDFLARE_IPV4_IP
val max = if (isIpv6) 128 else 32
if (prefix == max) {
addr.toCanonicalString()
} else {
val nextAddr: IPAddress? = addr.increment(1)
nextAddr?.toCanonicalString() ?: cloudflareIp
}
} catch (e: AddressValueException) {
Timber.e(
e,
"Failed to parse or increment IP: $cleanedIp",
)
defaultCloudflare
}
internalIp.removeSurrounding("[", "]")
}
}
.invoke()
val attemptTime = System.currentTimeMillis()
runCatching {
withTimeout(
settings.tunnelPingTimeoutSeconds?.toMillis() ?: 5000L
) {
val pingStats =
settings.tunnelPingTimeoutSeconds?.let {
networkUtils.pingWithStats(
host,
settings.tunnelPingAttempts,
it.toMillis(),
)
}
?: networkUtils.pingWithStats(
host,
settings.tunnelPingAttempts,
)
updates[key] =
previousState.copy(
transmitted = pingStats.transmitted,
received = pingStats.received,
packetLoss = pingStats.packetLoss,
rttMin = pingStats.rttMin,
rttMax = pingStats.rttMax,
rttAvg = pingStats.rttAvg,
rttStddev = pingStats.rttStddev,
isReachable = pingStats.isReachable,
failureReason =
if (pingStats.isReachable) null
else FailureReason.PingFailed,
lastSuccessfulPingMillis =
pingStats.lastSuccessfulPingMillis
?: previousState.lastSuccessfulPingMillis,
pingTarget = host,
lastPingAttemptMillis = attemptTime,
)
Timber.d(
"Ping completed for peer ${peer.publicKey.toBase64().substring(0, 5)}.. to host $host with stats: $pingStats"
val attemptTime = System.currentTimeMillis()
runCatching {
val pingStats =
settings.tunnelPingTimeoutSeconds?.let {
networkUtils.pingWithStats(
host,
settings.tunnelPingAttempts,
it.toMillis(),
)
}
}
.onFailure {
Timber.e(
it,
"Ping failed for peer ${peer.publicKey} in ${tunnelConfig.name} to host $host",
)
updates[key] =
previousState.copy(
isReachable = false,
failureReason = FailureReason.PingFailed,
pingTarget = host,
lastPingAttemptMillis = attemptTime,
?: networkUtils.pingWithStats(
host,
settings.tunnelPingAttempts,
)
}
}
updates[peer.publicKey] =
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 ${tunnelConf.tunName} to host $host",
)
updates[peer.publicKey] =
previousState.copy(
isReachable = false,
failureReason = FailureReason.PingFailed,
pingTarget = host,
lastPingAttemptMillis = attemptTime,
)
}
}
if (updates.isNotEmpty()) {
ensureActive()
pingStatsFlow.update { updates }
updateTunnelStatus(tunnelConfig.id, null, null, updates, null)
tunnelManager.updateTunnelStatus(tunnelConf.id, null, null, updates)
}
}
// Wait for the tunnel to be fully active
tunStateFlow.filter { state -> state?.status == TunnelStatus.Up }.first()
tunStateFlow.filter { state -> state.status == TunnelStatus.Up }.first()
// small delay to make sure tunnel is fully up before we actively monitor
delay(3_000L)
while (isActive) {
ensureActive()
if (isNetworkConnected.value) {
performPing()
} else {
@@ -282,27 +226,22 @@ constructor(
)
}
}
ensureActive()
updateTunnelStatus(tunnelConfig.id, null, null, pingStatsFlow.value, null)
tunnelManager.updateTunnelStatus(
tunnelConf.id,
null,
null,
pingStatsFlow.value,
)
}
delay(settings.tunnelPingIntervalSeconds.toMillis())
}
}
}
private suspend fun startWgStatsPoll(
tunnelId: Int,
getStatistics: suspend (Int) -> TunnelStatistics?,
updateTunnelStatus:
suspend (
Int, TunnelStatus?, TunnelStatistics?, Map<String, PingState>?, LogHealthState?,
) -> Unit,
) = coroutineScope {
private suspend fun startWgStatsPoll(tunnelId: Int) = coroutineScope {
while (isActive) {
ensureActive()
val stats = getStatistics(tunnelId)
ensureActive()
updateTunnelStatus(tunnelId, null, stats, null, null)
val stats = tunnelManager.getStatistics(tunnelId)
tunnelManager.updateTunnelStatus(tunnelId, null, stats, null)
delay(STATS_DELAY)
}
}
@@ -322,6 +261,14 @@ constructor(
const val CLOUDFLARE_IPV6_IP = "2606:4700:4700::1111"
const val CLOUDFLARE_IPV4_IP = "1.1.1.1"
const val STATS_DELAY = 1_000L
const val KEEPALIVE_RESPONSE_TEXT = "Receiving keepalive packet"
const val HANDSHAKE_RESPONSE_TEXT = "Received handshake response"
const val HANDSHAKE_INIT_FAILED_TEXT = "Failed to send handshake initiation: write udp"
const val DATA_PACKET_FAILED_TEXT = "Failed to send data packets"
const val HANDSHAKE_NOT_COMPLETED_TEXT =
"Handshake did not complete after 5 seconds, retrying"
}
}
@@ -4,17 +4,18 @@ import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException
import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.state.LogHealthState
import com.zaneschepke.wireguardautotunnel.domain.state.PingState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import org.amnezia.awg.crypto.Key
interface TunnelProvider {
/** Starts the specified tunnel configuration. */
suspend fun startTunnel(tunnelConfig: TunnelConfig)
suspend fun startTunnel(tunnelConf: TunnelConf)
/**
* Stops the specified tunnel.
@@ -23,8 +24,6 @@ interface TunnelProvider {
*/
suspend fun stopTunnel(tunnelId: Int)
suspend fun forceStopTunnel(tunnelId: Int)
/** Stops all active tunnels. */
suspend fun stopActiveTunnels()
@@ -34,21 +33,23 @@ interface TunnelProvider {
suspend fun runningTunnelNames(): Set<String>
fun handleDnsReresolve(tunnelConfig: TunnelConfig): Boolean
fun handleDnsReresolve(tunnelConf: TunnelConf): Boolean
fun getStatistics(tunnelId: Int): TunnelStatistics?
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>>
fun hasVpnPermission(): Boolean
suspend fun updateTunnelStatus(
tunnelId: Int,
status: TunnelStatus? = null,
stats: TunnelStatistics? = null,
pingStates: Map<String, PingState>? = null,
pingStates: Map<Key, PingState>? = null,
logHealthState: LogHealthState? = null,
)
}
@@ -1,15 +1,14 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.data.model.DnsProtocol
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.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException
import com.zaneschepke.wireguardautotunnel.domain.model.ProxySettings
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.repository.DnsSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.ProxySettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.model.AppProxySettings
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.state.AmneziaStatistics
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.util.extensions.asAmBackendMode
@@ -20,13 +19,13 @@ import java.io.IOException
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.amnezia.awg.backend.Backend
import org.amnezia.awg.backend.BackendException
import org.amnezia.awg.backend.ProxyGoBackend
@@ -42,81 +41,73 @@ class UserspaceTunnel
@Inject
constructor(
@ApplicationScope applicationScope: CoroutineScope,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
private val proxySettingsRepository: ProxySettingsRepository,
private val dnsSettingsRepository: DnsSettingsRepository,
serviceManager: ServiceManager,
appDataRepository: AppDataRepository,
private val backend: Backend,
) : BaseTunnel(applicationScope, ioDispatcher) {
) : BaseTunnel(applicationScope, appDataRepository, serviceManager) {
private val runtimeTunnels = ConcurrentHashMap<Int, AwgTunnel>()
override fun tunnelStateFlow(tunnelConfig: TunnelConfig): Flow<TunnelStatus> = callbackFlow {
override fun tunnelStateFlow(tunnelConf: TunnelConf): Flow<TunnelStatus> = callbackFlow {
val stateChannel = Channel<AwgTunnel.State>()
val runtimeTunnel = RuntimeAwgTunnel(tunnelConfig, stateChannel)
runtimeTunnels[tunnelConfig.id] = runtimeTunnel
val runtimeTunnel = RuntimeAwgTunnel(tunnelConf, stateChannel)
runtimeTunnels[tunnelConf.id] = runtimeTunnel
val consumerJob = launch {
stateChannel.consumeAsFlow().collect { awgState -> trySend(awgState.asTunnelState()) }
}
try {
withTimeout(STARTUP_TIMEOUT_MS) {
updateTunnelStatus(tunnelConfig.id, TunnelStatus.Starting)
updateTunnelStatus(tunnelConf.id, TunnelStatus.Starting)
val proxies: List<Proxy> =
when (backend) {
is ProxyGoBackend -> {
val proxySettings = proxySettingsRepository.getProxySettings()
Timber.d("Adding proxy configs")
buildList {
if (proxySettings.socks5ProxyEnabled) {
add(
Socks5Proxy(
proxySettings.socks5ProxyBindAddress
?: ProxySettings.DEFAULT_SOCKS_BIND_ADDRESS,
proxySettings.proxyUsername,
proxySettings.proxyPassword,
)
val proxies: List<Proxy> =
when (backend) {
is ProxyGoBackend -> {
val proxySettings = appDataRepository.proxySettings.get()
Timber.d("Adding proxy configs")
buildList {
if (proxySettings.socks5ProxyEnabled) {
add(
Socks5Proxy(
proxySettings.socks5ProxyBindAddress
?: AppProxySettings.DEFAULT_SOCKS_BIND_ADDRESS,
proxySettings.proxyUsername,
proxySettings.proxyPassword,
)
}
if (proxySettings.httpProxyEnabled) {
add(
HttpProxy(
proxySettings.httpProxyBindAddress
?: ProxySettings.DEFAULT_HTTP_BIND_ADDRESS,
proxySettings.proxyUsername,
proxySettings.proxyPassword,
)
)
}
if (proxySettings.httpProxyEnabled) {
add(
HttpProxy(
proxySettings.httpProxyBindAddress
?: AppProxySettings.DEFAULT_HTTP_BIND_ADDRESS,
proxySettings.proxyUsername,
proxySettings.proxyPassword,
)
}
)
}
}
else -> emptyList()
}
val setting = dnsSettingsRepository.getDnsSettings()
val config = tunnelConfig.toAmConfig()
val updatedConfig =
Config.Builder()
.apply {
setInterface(config.`interface`)
addPeers(config.peers)
addProxies(proxies)
setDnsSettings(
DnsSettings(
setting.dnsProtocol == DnsProtocol.DOH,
Optional.ofNullable(setting.dnsEndpoint),
)
else -> emptyList()
}
val setting = appDataRepository.settings.get()
val config = tunnelConf.toAmConfig()
val updatedConfig =
Config.Builder()
.apply {
setInterface(config.`interface`)
addPeers(config.peers)
addProxies(proxies)
setDnsSettings(
DnsSettings(
setting.dnsProtocol == DnsProtocol.DOH,
Optional.ofNullable(setting.dnsEndpoint),
)
}
.build()
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()
)
}
.build()
backend.setState(runtimeTunnel, AwgTunnel.State.UP, updatedConfig)
} catch (e: BackendException) {
close(e.toBackendCoreException())
} catch (e: IllegalArgumentException) {
@@ -130,11 +121,11 @@ constructor(
try {
backend.setState(runtimeTunnel, AwgTunnel.State.DOWN, null)
} catch (e: BackendException) {
errors.tryEmit(tunnelConfig.name to e.toBackendCoreException())
errors.tryEmit(tunnelConf.tunName to e.toBackendCoreException())
} finally {
consumerJob.cancel()
stateChannel.close()
runtimeTunnels.remove(tunnelConfig.id)
runtimeTunnels.remove(tunnelConf.id)
trySend(TunnelStatus.Down)
close()
}
@@ -157,9 +148,10 @@ constructor(
return backend.backendMode.asBackendMode()
}
override fun handleDnsReresolve(tunnelConfig: TunnelConfig): Boolean {
val tunnel = runtimeTunnels[tunnelConfig.id] ?: throw BackendCoreException.ServiceNotRunning
return backend.resolveDDNS(tunnelConfig.toAmConfig(), tunnel.isIpv4ResolutionPreferred)
override fun handleDnsReresolve(tunnelConf: TunnelConf): Boolean {
val tunnel =
runtimeTunnels.get(tunnelConf.id) ?: throw BackendCoreException.ServiceNotRunning
return backend.resolveDDNS(tunnelConf.toAmConfig(), tunnel.isIpv4ResolutionPreferred)
}
override suspend fun runningTunnelNames(): Set<String> {
@@ -175,19 +167,4 @@ constructor(
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 com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import java.util.concurrent.TimeUnit
@@ -20,7 +20,7 @@ constructor(
@Assisted private val context: Context,
@Assisted private val params: WorkerParameters,
private val serviceManager: ServiceManager,
private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
private val appDataRepository: AppDataRepository,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : CoroutineWorker(context, params) {
@@ -50,11 +50,11 @@ constructor(
override suspend fun doWork(): Result =
withContext(ioDispatcher) {
Timber.i("Service worker started")
with(autoTunnelSettingsRepository.getAutoTunnelSettings()) {
with(appDataRepository.settings.get()) {
Timber.i("Checking to see if auto-tunnel has been killed by system")
if (isAutoTunnelEnabled && serviceManager.autoTunnelService.value == null) {
Timber.i("Service has been killed by system, restoring.")
serviceManager.startAutoTunnelService()
serviceManager.startAutoTunnel()
}
}
Result.success()
@@ -3,20 +3,16 @@ package com.zaneschepke.wireguardautotunnel.data
import androidx.room.*
import androidx.room.migration.AutoMigrationSpec
import androidx.sqlite.db.SupportSQLiteDatabase
import com.zaneschepke.wireguardautotunnel.data.dao.*
import com.zaneschepke.wireguardautotunnel.data.entity.*
import com.zaneschepke.wireguardautotunnel.data.dao.ProxySettingsDao
import com.zaneschepke.wireguardautotunnel.data.dao.SettingsDao
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(
entities =
[
TunnelConfig::class,
ProxySettings::class,
GeneralSettings::class,
AutoTunnelSettings::class,
MonitoringSettings::class,
DnsSettings::class,
],
version = 25,
entities = [Settings::class, TunnelConfig::class, ProxySettings::class],
version = 22,
autoMigrations =
[
AutoMigration(from = 1, to = 2),
@@ -40,24 +36,16 @@ import com.zaneschepke.wireguardautotunnel.data.entity.*
AutoMigration(from = 19, to = 20, spec = ProxyMigration::class),
AutoMigration(from = 20, to = 21, spec = FixProxySettingsMigration::class),
AutoMigration(from = 21, to = 22),
AutoMigration(from = 22, to = 23),
AutoMigration(from = 24, to = 25),
],
exportSchema = true,
)
@TypeConverters(DatabaseConverters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun settingDao(): SettingsDao
abstract fun tunnelConfigDoa(): TunnelConfigDao
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")
@@ -4,6 +4,7 @@ import android.content.Context
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import java.io.IOException
@@ -19,21 +20,29 @@ class DataStoreManager(
private val context: Context,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) {
private val preferencesKey = "preferences"
val Context.dataStore by preferencesDataStore(name = preferencesKey)
val dataStore = context.dataStore
companion object {
val locationDisclosureShown = booleanPreferencesKey("LOCATION_DISCLOSURE_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() {
withContext(ioDispatcher) {
try {
dataStore.data.first()
context.dataStore.data.first()
} catch (e: IOException) {
Timber.e(e)
Timber.Forest.e(e)
}
}
}
@@ -41,11 +50,11 @@ class DataStoreManager(
suspend fun <T> saveToDataStore(key: Preferences.Key<T>, value: T) {
withContext(ioDispatcher) {
try {
dataStore.edit { it[key] = value }
context.dataStore.edit { it[key] = value }
} catch (e: IOException) {
Timber.e(e)
Timber.Forest.e(e)
} catch (e: Exception) {
Timber.e(e)
Timber.Forest.e(e)
}
}
}
@@ -53,11 +62,11 @@ class DataStoreManager(
suspend fun <T> removeFromDataStore(key: Preferences.Key<T>) {
withContext(ioDispatcher) {
try {
dataStore.edit { it.remove(key) }
context.dataStore.edit { it.remove(key) }
} catch (e: IOException) {
Timber.e(e)
Timber.Forest.e(e)
} catch (e: Exception) {
Timber.e(e)
Timber.Forest.e(e)
}
}
}
@@ -67,13 +76,13 @@ class DataStoreManager(
suspend fun <T> getFromStore(key: Preferences.Key<T>): T? {
return withContext(ioDispatcher) {
try {
dataStore.data.map { it[key] }.first()
context.dataStore.data.map { it[key] }.first()
} catch (e: IOException) {
Timber.e(e)
Timber.Forest.e(e)
null
}
}
}
val preferencesFlow: Flow<Preferences?> = dataStore.data.flowOn(ioDispatcher)
val preferencesFlow: Flow<Preferences?> = context.dataStore.data.flowOn(ioDispatcher)
}
@@ -4,17 +4,13 @@ import androidx.room.RoomDatabase
import androidx.sqlite.db.SupportSQLiteDatabase
import javax.inject.Inject
import javax.inject.Provider
import timber.log.Timber
class DatabaseCallback @Inject constructor(private val databaseProvider: Provider<AppDatabase>) :
RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
Timber.d("Database created, inserting default rows")
db.execSQL("INSERT INTO proxy_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")
db.execSQL("INSERT INTO Settings DEFAULT VALUES")
}
}
@@ -24,24 +24,6 @@ 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
fun setToString(value: Set<String>): String {
return listToString(value.toList())
@@ -1,21 +0,0 @@
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)
}
@@ -1,16 +0,0 @@
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?>
}
@@ -1,18 +0,0 @@
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?>
}
@@ -1,18 +0,0 @@
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,16 +1,25 @@
package com.zaneschepke.wireguardautotunnel.data.dao
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Upsert
import androidx.room.*
import com.zaneschepke.wireguardautotunnel.data.entity.ProxySettings
import kotlinx.coroutines.flow.Flow
@Dao
interface ProxySettingsDao {
@Upsert suspend fun upsert(proxySettings: ProxySettings)
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun save(t: ProxySettings)
@Query("SELECT * FROM proxy_settings LIMIT 1") suspend fun getProxySettings(): ProxySettings?
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun saveAll(t: List<ProxySettings>)
@Query("SELECT * FROM proxy_settings LIMIT 1") fun getProxySettingsFlow(): Flow<ProxySettings?>
@Query("SELECT * FROM proxy_settings WHERE id=:id")
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
}
@@ -0,0 +1,24 @@
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
}
@@ -2,82 +2,51 @@ package com.zaneschepke.wireguardautotunnel.data.dao
import androidx.room.*
import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
import kotlinx.coroutines.flow.Flow
@Dao
interface TunnelConfigDao {
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun save(t: TunnelConfig)
@Upsert suspend fun upsert(t: TunnelConfig)
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun saveAll(t: TunnelConfigs)
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun saveAll(t: List<TunnelConfig>)
@Query("SELECT * FROM TunnelConfig WHERE id=:id") suspend fun getById(id: Long): TunnelConfig?
@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")
@Query("UPDATE TunnelConfig SET is_Active = 0 WHERE is_Active = 1")
suspend fun resetActiveTunnels()
@Query("SELECT * FROM tunnel_config WHERE name=:name")
@Query("SELECT * FROM TunnelConfig WHERE name=:name")
suspend fun getByName(name: String): TunnelConfig?
@Query("SELECT * FROM tunnel_config WHERE is_Active=1")
suspend fun getActive(): List<TunnelConfig>
@Query("SELECT * FROM TunnelConfig WHERE is_Active=1") suspend fun getActive(): TunnelConfigs
@Query("SELECT * FROM tunnel_config") suspend fun getAll(): List<TunnelConfig>
@Query("SELECT * FROM TunnelConfig") suspend fun getAll(): TunnelConfigs
@Delete suspend fun delete(t: TunnelConfig)
@Delete suspend fun delete(t: List<TunnelConfig>)
@Delete suspend fun delete(t: TunnelConfigs)
@Query("SELECT COUNT('id') FROM tunnel_config") suspend fun count(): Long
@Query("SELECT COUNT('id') FROM TunnelConfig") suspend fun count(): Long
@Query("SELECT * FROM tunnel_config WHERE tunnel_networks LIKE '%' || :name || '%'")
suspend fun findByTunnelNetworkName(name: String): List<TunnelConfig>
@Query("SELECT * FROM TunnelConfig WHERE tunnel_networks LIKE '%' || :name || '%'")
suspend fun findByTunnelNetworkName(name: String): TunnelConfigs
@Query("UPDATE tunnel_config SET is_primary_tunnel = 0 WHERE is_primary_tunnel =1")
@Query("UPDATE TunnelConfig SET is_primary_tunnel = 0 WHERE is_primary_tunnel =1")
suspend fun resetPrimaryTunnel()
@Query("UPDATE tunnel_config SET is_mobile_data_tunnel = 0 WHERE is_mobile_data_tunnel =1")
@Query("UPDATE TunnelConfig SET is_mobile_data_tunnel = 0 WHERE is_mobile_data_tunnel =1")
suspend fun resetMobileDataTunnel()
@Query("UPDATE tunnel_config SET is_ethernet_tunnel = 0 WHERE is_ethernet_tunnel =1")
@Query("UPDATE TunnelConfig SET is_ethernet_tunnel = 0 WHERE is_ethernet_tunnel =1")
suspend fun resetEthernetTunnel()
@Query("SELECT * FROM tunnel_config WHERE is_primary_tunnel=1")
suspend fun findByPrimary(): List<TunnelConfig>
@Query("SELECT * FROM TUNNELCONFIG WHERE is_primary_tunnel=1")
suspend fun findByPrimary(): TunnelConfigs
@Query("SELECT * FROM tunnel_config WHERE is_mobile_data_tunnel=1")
suspend fun findByMobileDataTunnel(): List<TunnelConfig>
@Query("SELECT * FROM TUNNELCONFIG WHERE is_mobile_data_tunnel=1")
suspend fun findByMobileDataTunnel(): TunnelConfigs
@Query(
"""
SELECT * FROM tunnel_config
ORDER BY
CASE WHEN is_primary_tunnel = 1 THEN 0 ELSE 1 END,
position ASC
LIMIT 1"""
)
suspend fun getDefaultTunnel(): TunnelConfig?
@Query(
"""
SELECT * FROM tunnel_config
ORDER BY
CASE WHEN is_Active = 1 THEN 0
WHEN is_primary_tunnel = 1 THEN 1
ELSE 2 END,
position ASC
LIMIT 1"""
)
suspend fun getStartTunnel(): TunnelConfig?
@Query("SELECT * FROM tunnel_config ORDER BY position")
@Query("SELECT * FROM tunnelconfig ORDER BY position")
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?>
}
@@ -1,6 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.entity
data class AppState(
val isLocationDisclosureShown: Boolean = false,
val isBatteryOptimizationDisableShown: Boolean = false,
)
@@ -1,32 +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.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,
)
@@ -1,14 +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.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,
)
@@ -1,33 +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
@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(),
)
@@ -0,0 +1,26 @@
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
}
}
@@ -1,21 +0,0 @@
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,
)
@@ -0,0 +1,56 @@
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.PrimaryKey
@Entity(tableName = "tunnel_config", indices = [Index(value = ["name"], unique = true)])
@Entity(indices = [Index(value = ["name"], unique = true)])
data class TunnelConfig(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
@ColumnInfo(name = "name") val name: String,
@@ -16,7 +16,7 @@ data class TunnelConfig(
val isMobileDataTunnel: Boolean = false,
@ColumnInfo(name = "is_primary_tunnel", defaultValue = "false")
val isPrimaryTunnel: Boolean = false,
@ColumnInfo(name = "am_quick", defaultValue = "") val amQuick: String = "",
@ColumnInfo(name = "am_quick", defaultValue = "") val amQuick: String = AM_QUICK_DEFAULT,
@ColumnInfo(name = "is_Active", defaultValue = "false") val isActive: Boolean = false,
@ColumnInfo(name = "restart_on_ping_failure", defaultValue = "false")
val restartOnPingFailure: Boolean = false,
@@ -27,10 +27,10 @@ data class TunnelConfig(
val isIpv4Preferred: Boolean = true,
@ColumnInfo(name = "position", defaultValue = "0") val position: Int = 0,
@ColumnInfo(name = "auto_tunnel_apps", defaultValue = "[]")
val autoTunnelApps: Set<String> = emptySet(),
val autoTunnelApps: Set<String> = setOf(),
) {
companion object {
const val GLOBAL_CONFIG_NAME = "4675ab06-903a-438b-8485-6ea4187a9512"
const val AM_QUICK_DEFAULT = ""
}
}
@@ -1,36 +0,0 @@
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,
)
@@ -1,10 +0,0 @@
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,10 +1,39 @@
package com.zaneschepke.wireguardautotunnel.data.mapper
import com.zaneschepke.wireguardautotunnel.data.entity.AppState as Entity
import com.zaneschepke.wireguardautotunnel.domain.model.AppState as Domain
import com.zaneschepke.wireguardautotunnel.data.entity.GeneralState
import com.zaneschepke.wireguardautotunnel.domain.model.AppState
fun Entity.toDomain(): Domain =
Domain(
isLocationDisclosureShown = isLocationDisclosureShown,
isBatteryOptimizationDisableShown = isBatteryOptimizationDisableShown,
)
object GeneralStateMapper {
fun toAppState(generalState: GeneralState): AppState =
with(generalState) {
AppState(
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,
)
}
}
}
@@ -1,28 +0,0 @@
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,26 +1,32 @@
package com.zaneschepke.wireguardautotunnel.data.mapper
import com.zaneschepke.wireguardautotunnel.data.entity.ProxySettings as Entity
import com.zaneschepke.wireguardautotunnel.domain.model.ProxySettings as Domain
import com.zaneschepke.wireguardautotunnel.data.entity.ProxySettings
import com.zaneschepke.wireguardautotunnel.domain.model.AppProxySettings
fun Entity.toDomain(): Domain =
Domain(
id = id,
socks5ProxyEnabled = socks5ProxyEnabled,
socks5ProxyBindAddress = socks5ProxyBindAddress,
httpProxyEnabled = httpProxyEnabled,
httpProxyBindAddress = httpProxyBindAddress,
proxyUsername = proxyUsername,
proxyPassword = proxyPassword,
)
object ProxySettingsMapper {
fun to(proxySettings: ProxySettings): AppProxySettings =
with(proxySettings) {
AppProxySettings(
id,
socks5ProxyEnabled,
socks5ProxyBindAddress,
httpProxyEnabled,
httpProxyBindAddress,
proxyUsername,
proxyPassword,
)
}
fun Domain.toEntity(): Entity =
Entity(
id = id,
socks5ProxyEnabled = socks5ProxyEnabled,
socks5ProxyBindAddress = socks5ProxyBindAddress,
httpProxyEnabled = httpProxyEnabled,
httpProxyBindAddress = httpProxyBindAddress,
proxyUsername = proxyUsername,
proxyPassword = proxyPassword,
)
fun to(proxySettings: AppProxySettings): ProxySettings =
with(proxySettings) {
ProxySettings(
id,
socks5ProxyEnabled,
socks5ProxyBindAddress,
httpProxyEnabled,
httpProxyBindAddress,
proxyUsername,
proxyPassword,
)
}
}
@@ -1,41 +1,77 @@
package com.zaneschepke.wireguardautotunnel.data.mapper
import com.zaneschepke.wireguardautotunnel.data.entity.GeneralSettings as Entity
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings as Domain
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
import com.zaneschepke.wireguardautotunnel.data.entity.Settings
import com.zaneschepke.wireguardautotunnel.data.model.DnsProtocol
import com.zaneschepke.wireguardautotunnel.data.model.DnsSettings
import com.zaneschepke.wireguardautotunnel.data.model.WifiDetectionMethod
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings
fun Entity.toDomain(): Domain =
Domain(
fun Settings.toAppSettings(): GeneralSettings {
return GeneralSettings(
id = id,
isAutoTunnelEnabled = isAutoTunnelEnabled,
isTunnelOnMobileDataEnabled = isTunnelOnMobileDataEnabled,
trustedNetworkSSIDs = trustedNetworkSSIDs,
isAlwaysOnVpnEnabled = isAlwaysOnVpnEnabled,
isTunnelOnEthernetEnabled = isTunnelOnEthernetEnabled,
isShortcutsEnabled = isShortcutsEnabled,
isTunnelOnWifiEnabled = isTunnelOnWifiEnabled,
isRestoreOnBootEnabled = isRestoreOnBootEnabled,
isMultiTunnelEnabled = isMultiTunnelEnabled,
isTunnelGlobalsEnabled = isTunnelGlobalsEnabled,
appMode = appMode,
theme = Theme.valueOf(theme.uppercase()),
locale = locale,
remoteKey = remoteKey,
isRemoteControlEnabled = isRemoteControlEnabled,
isPinLockEnabled = isPinLockEnabled,
isAlwaysOnVpnEnabled = isAlwaysOnVpnEnabled,
isPingEnabled = isPingEnabled,
isWildcardsEnabled = isWildcardsEnabled,
isStopOnNoInternetEnabled = isStopOnNoInternetEnabled,
isLanOnKillSwitchEnabled = isLanOnKillSwitchEnabled,
customSplitPackages = customSplitPackages,
debounceDelaySeconds = debounceDelaySeconds,
isDisableKillSwitchOnTrustedEnabled = isDisableKillSwitchOnTrustedEnabled,
isTunnelOnUnsecureEnabled = isTunnelOnUnsecureEnabled,
wifiDetectionMethod = WifiDetectionMethod.fromValue(wifiDetectionMethod.value),
tunnelPingIntervalSeconds = tunnelPingIntervalSeconds,
tunnelPingAttempts = tunnelPingAttempts,
tunnelPingTimeoutSeconds = tunnelPingTimeoutSeconds,
appMode = appMode,
dnsProtocol = dnsProtocol,
dnsEndpoint = dnsEndpoint,
)
}
fun Domain.toEntity(): Entity =
Entity(
fun GeneralSettings.toSettings(): Settings {
return Settings(
id = id,
isAutoTunnelEnabled = isAutoTunnelEnabled,
isTunnelOnMobileDataEnabled = isTunnelOnMobileDataEnabled,
trustedNetworkSSIDs = trustedNetworkSSIDs,
isAlwaysOnVpnEnabled = isAlwaysOnVpnEnabled,
isTunnelOnEthernetEnabled = isTunnelOnEthernetEnabled,
isShortcutsEnabled = isShortcutsEnabled,
isTunnelOnWifiEnabled = isTunnelOnWifiEnabled,
isRestoreOnBootEnabled = isRestoreOnBootEnabled,
isMultiTunnelEnabled = isMultiTunnelEnabled,
isTunnelGlobalsEnabled = isTunnelGlobalsEnabled,
appMode = appMode,
theme = theme.name,
locale = locale,
remoteKey = remoteKey,
isRemoteControlEnabled = isRemoteControlEnabled,
isPinLockEnabled = isPinLockEnabled,
isAlwaysOnVpnEnabled = isAlwaysOnVpnEnabled,
isPingEnabled = isPingEnabled,
isWildcardsEnabled = isWildcardsEnabled,
isStopOnNoInternetEnabled = isStopOnNoInternetEnabled,
isLanOnKillSwitchEnabled = isLanOnKillSwitchEnabled,
customSplitPackages = customSplitPackages,
debounceDelaySeconds = debounceDelaySeconds,
isDisableKillSwitchOnTrustedEnabled = isDisableKillSwitchOnTrustedEnabled,
isTunnelOnUnsecureEnabled = isTunnelOnUnsecureEnabled,
wifiDetectionMethod = WifiDetectionMethod.fromValue(wifiDetectionMethod.value),
tunnelPingIntervalSeconds = tunnelPingIntervalSeconds,
tunnelPingAttempts = tunnelPingAttempts,
tunnelPingTimeoutSeconds = tunnelPingTimeoutSeconds,
appMode = appMode,
dnsProtocol = dnsProtocol,
dnsEndpoint = dnsEndpoint,
)
}
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,40 +1,46 @@
package com.zaneschepke.wireguardautotunnel.data.mapper
import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig as Entity
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig as Domain
import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
fun Entity.toDomain(): Domain =
Domain(
id = id,
name = name,
wgQuick = wgQuick,
tunnelNetworks = tunnelNetworks,
isMobileDataTunnel = isMobileDataTunnel,
isPrimaryTunnel = isPrimaryTunnel,
amQuick = amQuick,
isActive = isActive,
restartOnPingFailure = restartOnPingFailure,
pingTarget = pingTarget,
isEthernetTunnel = isEthernetTunnel,
isIpv4Preferred = isIpv4Preferred,
position = position,
autoTunnelApps = autoTunnelApps,
)
object TunnelConfigMapper {
fun toTunnelConf(tunnelConfig: TunnelConfig): TunnelConf {
return with(tunnelConfig) {
TunnelConf(
id,
name,
wgQuick,
tunnelNetworks,
isMobileDataTunnel,
isPrimaryTunnel,
amQuick,
isActive,
pingTarget,
restartOnPingFailure,
isEthernetTunnel,
isIpv4Preferred,
position,
)
}
}
fun Domain.toEntity(): Entity =
Entity(
id = id,
name = name,
wgQuick = wgQuick,
tunnelNetworks = tunnelNetworks,
isMobileDataTunnel = isMobileDataTunnel,
isPrimaryTunnel = isPrimaryTunnel,
amQuick = amQuick,
isActive = isActive,
restartOnPingFailure = restartOnPingFailure,
pingTarget = pingTarget,
isEthernetTunnel = isEthernetTunnel,
isIpv4Preferred = isIpv4Preferred,
position = position,
autoTunnelApps = autoTunnelApps,
)
fun toTunnelConfig(tunnelConf: TunnelConf): TunnelConfig {
return with(tunnelConf) {
TunnelConfig(
id,
tunName,
wgQuick,
tunnelNetworks,
isMobileDataTunnel,
isPrimaryTunnel,
amQuick,
isActive,
restartOnPingFailure,
pingTarget,
isEthernetTunnel,
isIpv4Preferred,
position,
)
}
}
}
@@ -1,318 +0,0 @@
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")
}
}
}
@@ -0,0 +1,26 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.repository.*
import javax.inject.Inject
class AppDataRoomRepository
@Inject
constructor(
override val settings: GeneralSettingRepository,
override val tunnels: TunnelRepository,
override val appState: AppStateRepository,
override val proxySettings: ProxySettingsRepository,
) : AppDataRepository {
override suspend fun getPrimaryOrFirstTunnel(): TunnelConf? {
return tunnels.findPrimary().firstOrNull() ?: tunnels.getAll().firstOrNull()
}
override suspend fun getStartTunnelConfig(): TunnelConf? {
tunnels.getActive().let {
if (it.isNotEmpty()) return it.first()
return getPrimaryOrFirstTunnel()
}
}
}
@@ -1,12 +1,13 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.DataStoreManager
import com.zaneschepke.wireguardautotunnel.data.entity.AppState as Entity
import com.zaneschepke.wireguardautotunnel.data.mapper.toDomain
import com.zaneschepke.wireguardautotunnel.data.entity.GeneralState
import com.zaneschepke.wireguardautotunnel.data.mapper.GeneralStateMapper
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.model.AppState as Domain
import com.zaneschepke.wireguardautotunnel.domain.model.AppState
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
@@ -22,42 +23,163 @@ class DataStoreAppStateRepository(
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : AppStateRepository {
override suspend fun isLocationDisclosureShown(): Boolean {
return dataStoreManager.getFromStore(DataStoreManager.locationDisclosureShown) ?: false
return dataStoreManager.getFromStore(DataStoreManager.locationDisclosureShown)
?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT
}
override suspend fun setLocationDisclosureShown(shown: Boolean) {
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 {
return dataStoreManager.getFromStore(DataStoreManager.batteryDisableShown) ?: false
return dataStoreManager.getFromStore(DataStoreManager.batteryDisableShown)
?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT
}
override suspend fun setBatteryOptimizationDisableShown(shown: Boolean) {
dataStoreManager.saveToDataStore(DataStoreManager.batteryDisableShown, shown)
}
override val flow: Flow<Domain> =
override suspend fun setTunnelExpanded(id: Int) {
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
.map { prefs ->
prefs?.let { pref ->
try {
Entity(
GeneralState(
isLocationDisclosureShown =
pref[DataStoreManager.locationDisclosureShown] ?: false,
pref[DataStoreManager.locationDisclosureShown]
?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT,
isBatteryOptimizationDisableShown =
pref[DataStoreManager.batteryDisableShown] ?: false,
pref[DataStoreManager.batteryDisableShown]
?: 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) {
Timber.e(e)
Entity()
GeneralState()
}
} ?: Entity()
} ?: GeneralState()
}
.map { it.toDomain() }
.map(GeneralStateMapper::toAppState)
.stateIn(
scope = applicationScope + ioDispatcher,
started = SharingStarted.Eagerly,
initialValue = com.zaneschepke.wireguardautotunnel.domain.model.AppState(),
initialValue = AppState(),
)
}
@@ -1,37 +0,0 @@
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)
}
}
@@ -1,33 +0,0 @@
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()
}
}
@@ -1,33 +0,0 @@
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,11 +1,10 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.dao.ProxySettingsDao
import com.zaneschepke.wireguardautotunnel.data.entity.ProxySettings as Entity
import com.zaneschepke.wireguardautotunnel.data.mapper.toDomain
import com.zaneschepke.wireguardautotunnel.data.mapper.toEntity
import com.zaneschepke.wireguardautotunnel.data.entity.ProxySettings
import com.zaneschepke.wireguardautotunnel.data.mapper.ProxySettingsMapper
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.model.ProxySettings as Domain
import com.zaneschepke.wireguardautotunnel.domain.model.AppProxySettings
import com.zaneschepke.wireguardautotunnel.domain.repository.ProxySettingsRepository
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.flowOn
@@ -16,20 +15,16 @@ class RoomProxySettingsRepository(
private val proxySettingsDao: ProxySettingsDao,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : ProxySettingsRepository {
override suspend fun upsert(proxySettings: Domain) {
withContext(ioDispatcher) { proxySettingsDao.upsert(proxySettings.toEntity()) }
override suspend fun save(proxySettings: AppProxySettings) {
withContext(ioDispatcher) { proxySettingsDao.save(ProxySettingsMapper.to(proxySettings)) }
}
override val flow =
proxySettingsDao
.getProxySettingsFlow()
.flowOn(ioDispatcher)
.map { (it ?: Entity()).toDomain() }
.flowOn(ioDispatcher)
proxySettingsDao.getSettingsFlow().flowOn(ioDispatcher).map(ProxySettingsMapper::to)
override suspend fun getProxySettings(): Domain {
override suspend fun get(): AppProxySettings {
return withContext(ioDispatcher) {
(proxySettingsDao.getProxySettings() ?: Entity()).toDomain()
ProxySettingsMapper.to(proxySettingsDao.getAll().firstOrNull() ?: ProxySettings())
}
}
}
@@ -1,11 +1,11 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.dao.GeneralSettingsDao
import com.zaneschepke.wireguardautotunnel.data.entity.GeneralSettings as Entity
import com.zaneschepke.wireguardautotunnel.data.mapper.toDomain
import com.zaneschepke.wireguardautotunnel.data.mapper.toEntity
import com.zaneschepke.wireguardautotunnel.data.dao.SettingsDao
import com.zaneschepke.wireguardautotunnel.data.entity.Settings
import com.zaneschepke.wireguardautotunnel.data.mapper.toAppSettings
import com.zaneschepke.wireguardautotunnel.data.mapper.toSettings
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings as Domain
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.flowOn
@@ -13,23 +13,20 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
class RoomSettingsRepository(
private val settingsDoa: GeneralSettingsDao,
private val settingsDoa: SettingsDao,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : GeneralSettingRepository {
override suspend fun upsert(generalSettings: Domain) {
withContext(ioDispatcher) { settingsDoa.upsert(generalSettings.toEntity()) }
override suspend fun save(generalSettings: GeneralSettings) {
withContext(ioDispatcher) { settingsDoa.save(generalSettings.toSettings()) }
}
override val flow =
settingsDoa
.getGeneralSettingsFlow()
.map { (it ?: Entity()).toDomain() }
.flowOn(ioDispatcher)
settingsDoa.getSettingsFlow().flowOn(ioDispatcher).map { it.toAppSettings() }
override suspend fun getGeneralSettings(): Domain {
override suspend fun get(): GeneralSettings {
return withContext(ioDispatcher) {
(settingsDoa.getGeneralSettings() ?: Entity()).toDomain()
(settingsDoa.getAll().firstOrNull() ?: Settings()).toAppSettings()
}
}
}
@@ -1,13 +1,12 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.dao.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.data.mapper.toDomain
import com.zaneschepke.wireguardautotunnel.data.mapper.toEntity
import com.zaneschepke.wireguardautotunnel.data.mapper.TunnelConfigMapper
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig as Domain
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.util.extensions.Tunnels
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
@@ -19,37 +18,31 @@ class RoomTunnelRepository(
override val flow =
tunnelConfigDao.getAllFlow().flowOn(ioDispatcher).map {
it.map { tunnelConfig -> tunnelConfig.toDomain() }
it.map(TunnelConfigMapper::toTunnelConf)
}
override val userTunnelsFlow =
tunnelConfigDao.getAllTunnelsExceptGlobal().flowOn(ioDispatcher).map {
it.map { tunnelConfig -> tunnelConfig.toDomain() }
override suspend fun getAll(): Tunnels {
return withContext(ioDispatcher) {
tunnelConfigDao.getAll().map(TunnelConfigMapper::toTunnelConf)
}
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(tunnelConfig: Domain) {
withContext(ioDispatcher) { tunnelConfigDao.upsert(tunnelConfig.toEntity()) }
}
override suspend fun saveAll(tunnelConfigList: List<Domain>) {
override suspend fun save(tunnelConf: TunnelConf) {
withContext(ioDispatcher) {
tunnelConfigDao.saveAll(
tunnelConfigList.map { tunnelConfig -> tunnelConfig.toEntity() }
)
tunnelConfigDao.save(TunnelConfigMapper.toTunnelConfig(tunnelConf))
}
}
override suspend fun updatePrimaryTunnel(tunnelConfig: Domain?) {
override suspend fun saveAll(tunnelConfList: List<TunnelConf>) {
withContext(ioDispatcher) {
tunnelConfigDao.saveAll(tunnelConfList.map(TunnelConfigMapper::toTunnelConfig))
}
}
override suspend fun updatePrimaryTunnel(tunnelConf: TunnelConf?) {
withContext(ioDispatcher) {
tunnelConfigDao.resetPrimaryTunnel()
tunnelConfig?.let { save(it.copy(isPrimaryTunnel = true)) }
tunnelConf?.let { save(it.copy(isPrimaryTunnel = true)) }
}
}
@@ -57,65 +50,69 @@ class RoomTunnelRepository(
withContext(ioDispatcher) { tunnelConfigDao.resetActiveTunnels() }
}
override suspend fun updateMobileDataTunnel(tunnelConfig: Domain?) {
override suspend fun updateMobileDataTunnel(tunnelConf: TunnelConf?) {
withContext(ioDispatcher) {
tunnelConfigDao.resetMobileDataTunnel()
tunnelConfig?.let { save(it.copy(isMobileDataTunnel = true)) }
tunnelConf?.let { save(it.copy(isMobileDataTunnel = true)) }
}
}
override suspend fun updateEthernetTunnel(tunnelConfig: Domain?) {
override suspend fun updateEthernetTunnel(tunnelConf: TunnelConf?) {
withContext(ioDispatcher) {
tunnelConfigDao.resetEthernetTunnel()
tunnelConfig?.let { save(it.copy(isEthernetTunnel = true)) }
tunnelConf?.let { save(it.copy(isEthernetTunnel = true)) }
}
}
override suspend fun delete(tunnelConfig: Domain) {
withContext(ioDispatcher) { tunnelConfigDao.delete(tunnelConfig.toEntity()) }
override suspend fun delete(tunnelConf: TunnelConf) {
withContext(ioDispatcher) {
tunnelConfigDao.delete(TunnelConfigMapper.toTunnelConfig(tunnelConf))
}
}
override suspend fun getById(id: Int): Domain? {
return withContext(ioDispatcher) { tunnelConfigDao.getById(id.toLong())?.toDomain() }
override suspend fun getById(id: Int): TunnelConf? {
return withContext(ioDispatcher) {
tunnelConfigDao.getById(id.toLong())?.let(TunnelConfigMapper::toTunnelConf)
}
}
override suspend fun getActive(): List<Domain> {
return withContext(ioDispatcher) { tunnelConfigDao.getActive().map { it.toDomain() } }
}
override suspend fun getDefaultTunnel(): Domain? {
return withContext(ioDispatcher) { tunnelConfigDao.getDefaultTunnel()?.toDomain() }
}
override suspend fun getStartTunnel(): Domain? {
return withContext(ioDispatcher) { tunnelConfigDao.getStartTunnel()?.toDomain() }
override suspend fun getActive(): Tunnels {
return withContext(ioDispatcher) {
tunnelConfigDao.getActive().map(TunnelConfigMapper::toTunnelConf)
}
}
override suspend fun count(): Int {
return withContext(ioDispatcher) { tunnelConfigDao.count().toInt() }
}
override suspend fun findByTunnelName(name: String): Domain? {
return withContext(ioDispatcher) { tunnelConfigDao.getByName(name)?.toDomain() }
}
override suspend fun findByTunnelNetworksName(name: String): List<Domain> {
override suspend fun findByTunnelName(name: String): TunnelConf? {
return withContext(ioDispatcher) {
tunnelConfigDao.findByTunnelNetworkName(name).map { it.toDomain() }
tunnelConfigDao.getByName(name)?.let(TunnelConfigMapper::toTunnelConf)
}
}
override suspend fun findByMobileDataTunnel(): List<Domain> {
override suspend fun findByTunnelNetworksName(name: String): Tunnels {
return withContext(ioDispatcher) {
tunnelConfigDao.findByMobileDataTunnel().map { it.toDomain() }
tunnelConfigDao.findByTunnelNetworkName(name).map(TunnelConfigMapper::toTunnelConf)
}
}
override suspend fun findPrimary(): List<Domain> {
return withContext(ioDispatcher) { tunnelConfigDao.findByPrimary().map { it.toDomain() } }
override suspend fun findByMobileDataTunnel(): Tunnels {
return withContext(ioDispatcher) {
tunnelConfigDao.findByMobileDataTunnel().map(TunnelConfigMapper::toTunnelConf)
}
}
override suspend fun delete(tunnels: List<Domain>) {
withContext(ioDispatcher) { tunnelConfigDao.delete(tunnels.map { it.toEntity() }) }
override suspend fun findPrimary(): Tunnels {
return withContext(ioDispatcher) {
tunnelConfigDao.findByPrimary().map(TunnelConfigMapper::toTunnelConf)
}
}
override suspend fun delete(tunnels: List<TunnelConf>) {
withContext(ioDispatcher) {
tunnelConfigDao.delete(tunnels.map { TunnelConfigMapper.toTunnelConfig(it) })
}
}
}
@@ -6,8 +6,9 @@ import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
import com.zaneschepke.wireguardautotunnel.data.DataStoreManager
import com.zaneschepke.wireguardautotunnel.data.DatabaseCallback
import com.zaneschepke.wireguardautotunnel.data.dao.*
import com.zaneschepke.wireguardautotunnel.data.migrations.MIGRATION_23_24
import com.zaneschepke.wireguardautotunnel.data.dao.ProxySettingsDao
import com.zaneschepke.wireguardautotunnel.data.dao.SettingsDao
import com.zaneschepke.wireguardautotunnel.data.dao.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.data.network.GitHubApi
import com.zaneschepke.wireguardautotunnel.data.network.KtorClient
import com.zaneschepke.wireguardautotunnel.data.network.KtorGitHubApi
@@ -48,14 +49,12 @@ class RepositoryModule {
fun provideDatabase(
@ApplicationContext context: Context,
callback: DatabaseCallback,
dataStoreManager: DataStoreManager,
): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
context.getString(R.string.db_name),
)
.addMigrations(MIGRATION_23_24(dataStoreManager.dataStore))
.fallbackToDestructiveMigration(true)
.addCallback(callback)
.build()
@@ -63,26 +62,8 @@ class RepositoryModule {
@Singleton
@Provides
fun provideSettingsDoa(appDatabase: AppDatabase): GeneralSettingsDao {
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()
fun provideSettingsDoa(appDatabase: AppDatabase): SettingsDao {
return appDatabase.settingDao()
}
@Singleton
@@ -108,40 +89,13 @@ class RepositoryModule {
@Singleton
@Provides
fun provideGeneralSettingsRepository(
settingsDao: GeneralSettingsDao,
fun provideSettingsRepository(
settingsDao: SettingsDao,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): GeneralSettingRepository {
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
@Provides
fun provideProxySettingsRepository(
@@ -170,6 +124,22 @@ class RepositoryModule {
return DataStoreAppStateRepository(dataStoreManager, applicationScope, ioDispatcher)
}
@Provides
@Singleton
fun provideAppDataRepository(
settingsRepository: GeneralSettingRepository,
tunnelRepository: TunnelRepository,
appStateRepository: AppStateRepository,
proxySettingsRepository: ProxySettingsRepository,
): AppDataRepository {
return AppDataRoomRepository(
settingsRepository,
tunnelRepository,
appStateRepository,
proxySettingsRepository,
)
}
@Provides
@Singleton
fun provideHttpClient(): HttpClient {
@@ -9,7 +9,8 @@ import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.*
import com.zaneschepke.wireguardautotunnel.domain.repository.*
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.util.extensions.to
import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils
import dagger.Module
@@ -83,10 +84,11 @@ class TunnelModule {
@Kernel
fun provideKernelProvider(
@ApplicationScope applicationScope: CoroutineScope,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
serviceManager: ServiceManager,
appDataRepository: AppDataRepository,
backend: com.wireguard.android.backend.Backend,
): TunnelProvider {
return KernelTunnel(applicationScope, ioDispatcher, backend)
return KernelTunnel(applicationScope, serviceManager, appDataRepository, backend)
}
@Provides
@@ -94,18 +96,11 @@ class TunnelModule {
@Userspace
fun provideUserspaceProvider(
@ApplicationScope applicationScope: CoroutineScope,
proxySettingsRepository: ProxySettingsRepository,
dnsSettingsRepository: DnsSettingsRepository,
serviceManager: ServiceManager,
appDataRepository: AppDataRepository,
@Userspace backend: Backend,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): TunnelProvider {
return UserspaceTunnel(
applicationScope,
ioDispatcher,
proxySettingsRepository,
dnsSettingsRepository,
backend,
)
return UserspaceTunnel(applicationScope, serviceManager, appDataRepository, backend)
}
@Provides
@@ -113,18 +108,11 @@ class TunnelModule {
@ProxyUserspace
fun provideProxyUserspaceProvider(
@ApplicationScope applicationScope: CoroutineScope,
dnsSettingsRepository: DnsSettingsRepository,
proxySettingsRepository: ProxySettingsRepository,
serviceManager: ServiceManager,
appDataRepository: AppDataRepository,
@ProxyUserspace backend: Backend,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): TunnelProvider {
return UserspaceTunnel(
applicationScope,
ioDispatcher,
proxySettingsRepository,
dnsSettingsRepository,
backend,
)
return UserspaceTunnel(applicationScope, serviceManager, appDataRepository, backend)
}
@Provides
@@ -134,10 +122,7 @@ class TunnelModule {
@Userspace userspaceTunnel: TunnelProvider,
@ProxyUserspace proxyTunnel: TunnelProvider,
serviceManager: ServiceManager,
tunnelRepository: TunnelRepository,
settingsRepository: GeneralSettingRepository,
autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
tunnelMonitor: TunnelMonitor,
appDataRepository: AppDataRepository,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
@ApplicationScope applicationScope: CoroutineScope,
): TunnelManager {
@@ -146,10 +131,7 @@ class TunnelModule {
userspaceTunnel,
proxyTunnel,
serviceManager,
settingsRepository,
autoTunnelSettingsRepository,
tunnelRepository,
tunnelMonitor,
appDataRepository,
applicationScope,
ioDispatcher,
)
@@ -159,7 +141,7 @@ class TunnelModule {
@Singleton
fun provideNetworkMonitor(
@ApplicationContext context: Context,
autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
settingsRepository: GeneralSettingRepository,
@ApplicationScope applicationScope: CoroutineScope,
@AppShell appShell: RootShell,
): NetworkMonitor {
@@ -168,7 +150,7 @@ class TunnelModule {
object : AndroidNetworkMonitor.ConfigurationListener {
override val detectionMethod: Flow<AndroidNetworkMonitor.WifiDetectionMethod>
get() =
autoTunnelSettingsRepository.flow
settingsRepository.flow
.distinctUntilChangedBy { it.wifiDetectionMethod }
.map { it.wifiDetectionMethod.to() }
@@ -186,31 +168,30 @@ class TunnelModule {
@IoDispatcher ioDispatcher: CoroutineDispatcher,
@MainDispatcher mainCoroutineDispatcher: CoroutineDispatcher,
@ApplicationScope applicationScope: CoroutineScope,
autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
appDataRepository: AppDataRepository,
): ServiceManager {
return ServiceManager(
context,
ioDispatcher,
applicationScope,
mainCoroutineDispatcher,
autoTunnelSettingsRepository,
appDataRepository,
)
}
@Singleton
@Provides
fun provideTunnelMonitor(
@ApplicationContext context: Context,
tunnelManager: TunnelManager,
networkMonitor: NetworkMonitor,
networkUtils: NetworkUtils,
logReader: LogReader,
tunnelsRepository: TunnelRepository,
settingsRepository: GeneralSettingRepository,
monitoringSettingsRepository: MonitoringSettingsRepository,
appDataRepository: AppDataRepository,
): TunnelMonitor {
return TunnelMonitor(
settingsRepository,
tunnelsRepository,
monitoringSettingsRepository,
appDataRepository,
tunnelManager,
networkMonitor,
networkUtils,
logReader,
@@ -1,16 +1,13 @@
package com.zaneschepke.wireguardautotunnel.di
import android.content.Context
import com.wireguard.android.util.RootShell
import com.zaneschepke.wireguardautotunnel.util.FileUtils
import com.zaneschepke.wireguardautotunnel.util.RootShellUtils
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ViewModelComponent
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.scopes.ViewModelScoped
import javax.inject.Provider
import kotlinx.coroutines.CoroutineDispatcher
@Module
@@ -24,13 +21,4 @@ class ViewModelModule {
): FileUtils {
return FileUtils(context, ioDispatcher)
}
@ViewModelScoped
@Provides
fun provideRootShellUtils(
@AppShell rootShell: Provider<RootShell>,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): RootShellUtils {
return RootShellUtils(rootShell, ioDispatcher)
}
}
@@ -1,8 +0,0 @@
package com.zaneschepke.wireguardautotunnel.domain.enums
enum class NetworkType {
WIFI,
ETHERNET,
MOBILE_DATA,
NONE,
}
@@ -1,9 +1,9 @@
package com.zaneschepke.wireguardautotunnel.domain.events
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
sealed class AutoTunnelEvent {
data class Start(val tunnelConfig: TunnelConfig? = null) : AutoTunnelEvent()
data class Start(val tunnelConf: TunnelConf? = null) : AutoTunnelEvent()
data object Stop : AutoTunnelEvent()
@@ -22,6 +22,8 @@ sealed class BackendCoreException : Exception() {
data object UapiUpdateFailed : BackendCoreException()
data class BounceFailed(val error: BackendCoreException) : BackendCoreException()
fun toStringRes() =
when (this) {
Config -> R.string.config_error
@@ -32,10 +34,18 @@ sealed class BackendCoreException : Exception() {
ServiceNotRunning -> R.string.service_running_error
Unknown -> R.string.unknown_error
TunnelNameTooLong -> R.string.error_tunnel_name
is BounceFailed -> R.string.bounce_failed_template
UapiUpdateFailed -> R.string.active_tunnel_update_failed
}
fun toStringValue(): StringValue {
return StringValue.StringResource(toStringRes())
return when (val backendError = this) {
is BounceFailed ->
StringValue.StringResource(
backendError.toStringRes(),
backendError.error.toStringRes(),
)
else -> StringValue.StringResource(backendError.toStringRes())
}
}
}
@@ -5,11 +5,14 @@ import com.zaneschepke.wireguardautotunnel.util.StringValue
sealed class BackendMessage {
data object DynamicDnsSuccess : BackendMessage()
data object BounceSuccess : BackendMessage()
data object BounceRecovery : BackendMessage()
fun toStringRes() =
when (this) {
DynamicDnsSuccess -> R.string.ddns_success_message
BounceRecovery -> R.string.pinger_bounce_recovery
BounceSuccess -> R.string.pinger_bounce_successful
}
fun toStringValue() = StringValue.StringResource(this.toStringRes())
@@ -1,6 +1,6 @@
package com.zaneschepke.wireguardautotunnel.domain.model
data class ProxySettings(
data class AppProxySettings(
val id: Long = 0,
val socks5ProxyEnabled: Boolean = false,
val socks5ProxyBindAddress: String? = null,
@@ -1,6 +1,16 @@
package com.zaneschepke.wireguardautotunnel.domain.model
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
data class AppState(
val isLocationDisclosureShown: 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,
)
@@ -1,18 +0,0 @@
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,
)
@@ -1,9 +0,0 @@
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,21 +1,51 @@
package com.zaneschepke.wireguardautotunnel.domain.model
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
import com.zaneschepke.wireguardautotunnel.data.model.DnsProtocol
import com.zaneschepke.wireguardautotunnel.data.model.WifiDetectionMethod
data class GeneralSettings(
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 isTunnelOnWifiEnabled: Boolean = false,
val isRestoreOnBootEnabled: Boolean = false,
val isMultiTunnelEnabled: Boolean = false,
val isTunnelGlobalsEnabled: Boolean = false,
val appMode: AppMode = AppMode.fromValue(0),
val theme: Theme = Theme.AUTOMATIC,
val locale: String? = null,
val remoteKey: String? = null,
val isRemoteControlEnabled: Boolean = false,
val isPinLockEnabled: Boolean = false,
val isAlwaysOnVpnEnabled: Boolean = false,
val isPingEnabled: Boolean = false,
val isWildcardsEnabled: Boolean = false,
val isStopOnNoInternetEnabled: Boolean = false,
val isVpnKillSwitchEnabled: Boolean = false,
val isKernelKillSwitchEnabled: Boolean = false,
val isLanOnKillSwitchEnabled: Boolean = false,
val customSplitPackages: Map<String, String> = emptyMap(),
)
val debounceDelaySeconds: Int = 3,
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
}
}
@@ -1,12 +0,0 @@
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,
)
@@ -0,0 +1,138 @@
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 com.zaneschepke.wireguardautotunnel.util.extensions.toWgQuickString
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()
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()
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
}
}
@@ -1,295 +0,0 @@
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
}
}
@@ -0,0 +1,15 @@
package com.zaneschepke.wireguardautotunnel.domain.repository
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
interface AppDataRepository {
suspend fun getPrimaryOrFirstTunnel(): TunnelConf?
suspend fun getStartTunnelConfig(): TunnelConf?
val settings: GeneralSettingRepository
val tunnels: TunnelRepository
val appState: AppStateRepository
val proxySettings: ProxySettingsRepository
}
@@ -1,6 +1,7 @@
package com.zaneschepke.wireguardautotunnel.domain.repository
import com.zaneschepke.wireguardautotunnel.domain.model.AppState
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
import kotlinx.coroutines.flow.Flow
interface AppStateRepository {
@@ -8,9 +9,41 @@ interface AppStateRepository {
suspend fun setLocationDisclosureShown(shown: Boolean)
suspend fun isPinLockEnabled(): Boolean
suspend fun setPinLockEnabled(enabled: Boolean)
suspend fun isBatteryOptimizationDisableShown(): 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>
}
@@ -1,14 +0,0 @@
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)
}
@@ -1,12 +0,0 @@
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,9 +4,9 @@ import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings
import kotlinx.coroutines.flow.Flow
interface GeneralSettingRepository {
suspend fun upsert(generalSettings: GeneralSettings)
suspend fun save(generalSettings: GeneralSettings)
val flow: Flow<GeneralSettings>
suspend fun getGeneralSettings(): GeneralSettings
suspend fun get(): GeneralSettings
}
@@ -1,12 +0,0 @@
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
import com.zaneschepke.wireguardautotunnel.domain.model.ProxySettings
import com.zaneschepke.wireguardautotunnel.domain.model.AppProxySettings
import kotlinx.coroutines.flow.Flow
interface ProxySettingsRepository {
suspend fun upsert(proxySettings: ProxySettings)
suspend fun save(proxySettings: AppProxySettings)
val flow: Flow<ProxySettings>
val flow: Flow<AppProxySettings>
suspend fun getProxySettings(): ProxySettings
suspend fun get(): AppProxySettings
}
@@ -1,48 +1,41 @@
package com.zaneschepke.wireguardautotunnel.domain.repository
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.util.extensions.Tunnels
import kotlinx.coroutines.flow.Flow
interface TunnelRepository {
val flow: Flow<List<TunnelConfig>>
val flow: Flow<List<TunnelConf>>
val userTunnelsFlow: Flow<List<TunnelConfig>>
suspend fun getAll(): Tunnels
val globalTunnelFlow: Flow<TunnelConfig?>
suspend fun save(tunnelConf: TunnelConf)
suspend fun getAll(): List<TunnelConfig>
suspend fun saveAll(tunnelConfList: List<TunnelConf>)
suspend fun save(tunnelConfig: TunnelConfig)
suspend fun saveAll(tunnelConfigList: List<TunnelConfig>)
suspend fun updatePrimaryTunnel(tunnelConfig: TunnelConfig?)
suspend fun updatePrimaryTunnel(tunnelConf: TunnelConf?)
suspend fun resetActiveTunnels()
suspend fun updateMobileDataTunnel(tunnelConfig: TunnelConfig?)
suspend fun updateMobileDataTunnel(tunnelConf: TunnelConf?)
suspend fun updateEthernetTunnel(tunnelConfig: TunnelConfig?)
suspend fun updateEthernetTunnel(tunnelConf: TunnelConf?)
suspend fun delete(tunnelConfig: TunnelConfig)
suspend fun delete(tunnelConf: TunnelConf)
suspend fun getById(id: Int): TunnelConfig?
suspend fun getById(id: Int): TunnelConf?
suspend fun getActive(): List<TunnelConfig>
suspend fun getDefaultTunnel(): TunnelConfig?
suspend fun getStartTunnel(): TunnelConfig?
suspend fun getActive(): Tunnels
suspend fun count(): Int
suspend fun findByTunnelName(name: String): TunnelConfig?
suspend fun findByTunnelName(name: String): TunnelConf?
suspend fun findByTunnelNetworksName(name: String): List<TunnelConfig>
suspend fun findByTunnelNetworksName(name: String): Tunnels
suspend fun findByMobileDataTunnel(): List<TunnelConfig>
suspend fun findByMobileDataTunnel(): Tunnels
suspend fun findPrimary(): List<TunnelConfig>
suspend fun findPrimary(): Tunnels
suspend fun delete(tunnels: List<TunnelConfig>)
suspend fun delete(tunnels: List<TunnelConf>)
}
@@ -1,11 +1,12 @@
package com.zaneschepke.wireguardautotunnel.domain.sideeffect
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.util.StringValue
import java.io.File
sealed class GlobalSideEffect {
data object RequestBatteryOptimizationDisabled : GlobalSideEffect()
data class Snackbar(val message: StringValue) : GlobalSideEffect()
@@ -13,11 +14,13 @@ sealed class GlobalSideEffect {
data object PopBackStack : GlobalSideEffect()
data class ShareFile(val file: File) : GlobalSideEffect()
data class LaunchUrl(val url: String) : GlobalSideEffect()
data object ConfigChanged : GlobalSideEffect()
data class RequestVpnPermission(val requestingMode: AppMode, val config: TunnelConfig?) :
data class RequestVpnPermission(val requestingMode: AppMode, val config: TunnelConf?) :
GlobalSideEffect()
data class InstallApk(val apk: File) : GlobalSideEffect()
@@ -4,8 +4,8 @@ import org.amnezia.awg.backend.Statistics
import org.amnezia.awg.crypto.Key
class AmneziaStatistics(private val statistics: Statistics) : TunnelStatistics() {
override fun peerStats(peerBase64: String): PeerStats? {
val key = Key.fromBase64(peerBase64)
override fun peerStats(peer: Key): PeerStats? {
val key = Key.fromBase64(peer.toBase64())
val stats = statistics.peer(key)
return stats?.let {
PeerStats(
@@ -21,8 +21,8 @@ class AmneziaStatistics(private val statistics: Statistics) : TunnelStatistics()
return statistics.isStale
}
override fun getPeers(): Array<String> {
return statistics.peers().map { it.toBase64() }.toTypedArray()
override fun getPeers(): Array<Key> {
return statistics.peers()
}
override fun rx(): Long {

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