Compare commits

..

30 Commits

Author SHA1 Message Date
Zane Schepke 880d30fdfa fix: snackbar and restarts bug 2025-11-02 02:54:03 -05:00
Zane Schepke 765785ff41 chore: release v4.1.2 2025-11-02 02:12:09 -05:00
Zane Schepke 9991e06c44 fix: updater after abi split change 2025-11-02 00:48:13 -04:00
Weblate (bot) 433d383b0b feat(lang): update localization from Hosted Weblate (#1029)
Co-authored-by: CyanWolf <hydemr@pm.me>
Co-authored-by: MouaisTe44 <r.craft.212121@gmail.com>
Co-authored-by: teemue <eemil.koivula@gmail.com>
Co-authored-by: Hamed Ap <hamed.ap1366@gmail.com>
Co-authored-by: Priit Jõerüüt <jrthwlate@users.noreply.hosted.weblate.org>
Co-authored-by: Qotsa1984 <carlominzi@inwind.it>
Co-authored-by: kometchtech <kometch@gmail.com>
Co-authored-by: Matthaiks <kitynska@gmail.com>
Co-authored-by: Jan-Pascal van Best <janpascal@vanbest.org>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: Patrik <patrik1305@binternet.eu>
Co-authored-by: mak7im01 <mak7im02@gmail.com>
Co-authored-by: தமிழ்நேரம் <tamilneram247@gmail.com>
Co-authored-by: 翻譯得真好下次別翻了 <x86_64-pc-linux-gnu@proton.me>
Co-authored-by: Prefill add-on <noreply-addon-prefill@weblate.org>
2025-11-02 00:29:50 -04:00
Zane Schepke 041f12dc77 chore: remove orphaned strings 2025-11-01 23:35:20 -04:00
Zane Schepke afdc49629c refactor: snackbar, support prompt on update 2025-11-01 22:16:57 -04:00
Zane Schepke f846d54d78 chore: update screenshots 2025-11-01 02:45:17 -04:00
Zane Schepke ffd9c4192a feat: add translations link 2025-11-01 02:21:40 -04:00
Weblate (bot) d6138b80eb Translations update from Hosted Weblate (#950)
Co-authored-by: Matthaiks <kitynska@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Co-authored-by: Kachelkaiser <kachelkaiser@htpst.de>
Co-authored-by: Qotsa1984 <carlominzi@inwind.it>
Co-authored-by: Priit Jõerüüt <jrthwlate@users.noreply.hosted.weblate.org>
Co-authored-by: solokot <solokot@gmail.com>
Co-authored-by: catelixor <catelixor+weblate@proton.me>
Co-authored-by: EESF-2 <eesf-2@users.noreply.hosted.weblate.org>
Co-authored-by: Faisal Gull <mail.faisalrehman.345@gmail.com>
Co-authored-by: angrybb <lijadolija@gmail.com>
Co-authored-by: Noureddine <noureddinex@protonmail.com>
Co-authored-by: Patrik <patrik1305@binternet.eu>
Co-authored-by: weblate4ljj9 <weblate.4ljj9@aleeas.com>
Co-authored-by: Zane Schepke <zanecschepke@gmail.com>
2025-11-01 01:58:56 -04:00
Zane Schepke 0874db8bbf refactor: restart modal on config changes for proxy and tuns 2025-11-01 01:24:19 -04:00
Zane Schepke 6d483459a6 refactor: add restart to lockdown on config change 2025-11-01 00:06:40 -04:00
Zane Schepke 7e4f055833 fix: app start kills other vpns bug
closes #1020
2025-10-31 20:57:38 -04:00
Zane Schepke 8ea432b4f6 build: split abis 2025-10-31 02:20:05 -04:00
Zane Schepke 6637539d1f fix: android tv hide kernel mode, ping toggle, import sheet
closes #1008

#1008
2025-10-31 00:34:01 -04:00
Zane Schepke bfe3533030 fix: android tv long press behavior
#1008
2025-10-30 00:53:56 -04:00
Zane Schepke b61d49469f chore: bump ksp 2025-10-29 15:30:03 -04:00
Zane Schepke 349b56b2e2 fix: prevent backup restore issues by resetting active tuns storage 2025-10-29 15:20:08 -04:00
dependabot[bot] 42aa378938 chore(deps): bump actions/download-artifact from 5 to 6 (#1022)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-29 03:45:19 -04:00
dependabot[bot] 062f59aa33 chore(deps): bump actions/upload-artifact from 4 to 5 (#1021)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-29 03:45:07 -04:00
Zane Schepke 883b9f7dae refactor: wifi detection, bump deps
#1005
2025-10-29 03:43:50 -04:00
Zane Schepke 1f8d24c704 refactor: show disabled feature by mode 2025-10-29 01:44:25 -04:00
Zane Schepke 68ab3fdc52 fix: split tunnel missing packages
closes #750
2025-10-28 22:36:12 -04:00
Zane Schepke b4b96a7e77 feat!: dual-stack kill switch support, metered tunnels
Adds dual-stack option for kill switch.

Add metered option for kill switch and individual tunnels.

closes #966
closes #962
2025-10-28 21:43:35 -04:00
Zane Schepke 59a70e53ff fix: mtu parser errors
closes #890
2025-10-24 03:20:01 -04:00
Zane Schepke 170b12ab79 fix: endpoints should be optional
closes #808
2025-10-24 02:00:52 -04:00
Zane Schepke 0f365e2ef8 chore: update description, fdroid link 2025-10-23 21:32:39 -04:00
Zane Schepke 950d75b57f ci: fix duplicate notifications 2025-10-23 00:58:31 -04:00
Zane Schepke ea90896061 ci: optimize nightly ci 2025-10-23 00:33:47 -04:00
Zane Schepke c62b328187 fix: kernel tunnel symbols error handling
closes #1017
2025-10-23 00:31:33 -04:00
Zane Schepke 46a962a730 ci: switch to pat, improve notifications to include nightly 2025-10-21 21:09:48 -04:00
423 changed files with 5378 additions and 1446 deletions
-22
View File
@@ -1,22 +0,0 @@
# Contributor Code of Conduct
## Pledge
We as individuals involved in this project, pledge to participate in this
community in a respectful, constructive, and civil manner as we work towards a common goal
of delivering free, open source, and value adding software for all.
## Standard
The standard for this community is the Golden Rule.
> “Do unto others as you would have them do unto you.”
## Scope
This Code of Conduct applies to all spaces related to WG Tunnel.
## Incidents or Concerns
For any incidents or concerns, reach out to Zane at
<support@zaneschepke.com>.
+3 -7
View File
@@ -114,15 +114,11 @@ jobs:
- name: Get release apk path
id: apk-path
run: echo "path=$(find . -regex '^.*/build/outputs/apk/${{ inputs.flavor }}/${{ inputs.build_type }}/.*\.apk$' -type f | head -1 | tail -c+2)" >> $GITHUB_OUTPUT
- name: Upload APK
uses: actions/upload-artifact@v4
- name: Upload All APK Artifacts
uses: actions/upload-artifact@v5
with:
name: android_artifacts_${{ inputs.flavor }}
path: >-
app/build/outputs/apk/${{ inputs.flavor }}/${{ inputs.build_type }}/${{
inputs.flavor == 'fdroid' && inputs.build_type == 'release'
&& 'wgtunnel-fdroid-release-*.apk'
|| format('wgtunnel-{0}-v*.apk', inputs.flavor)
}}
app/build/outputs/apk/${{ inputs.flavor }}/${{ inputs.build_type }}/*.apk
retention-days: 1
if-no-files-found: warn
+5 -3
View File
@@ -26,6 +26,9 @@ jobs:
echo "new_commits=$NEW_COMMITS" >> $GITHUB_OUTPUT
build-standalone-nightly:
needs:
- check_commits
if: ${{ needs.check_commits.outputs.has_new_commits > 0 && inputs.release_type != 'none' }}
uses: ./.github/workflows/build.yml
secrets: inherit
with:
@@ -34,7 +37,6 @@ jobs:
publish:
needs:
- check_commits
- build-standalone-nightly
if: ${{ needs.check_commits.outputs.has_new_commits > 0 && inputs.release_type != 'none' }}
name: publish-nightly
@@ -69,7 +71,7 @@ jobs:
run: mkdir ${{ github.workspace }}/temp
- name: Download artifacts
uses: actions/download-artifact@v5
uses: actions/download-artifact@v6
with:
pattern: android_artifacts_*
path: ${{ github.workspace }}/temp
@@ -124,4 +126,4 @@ jobs:
files: |
${{ github.workspace }}/temp/**/*.apk
env:
GITHUB_TOKEN: ${{ github.token }}
GITHUB_TOKEN: ${{ secrets.PAT }}
+25 -6
View File
@@ -1,10 +1,13 @@
name: notifications
permissions:
contents: write
packages: write
on:
issues:
types: [opened, closed]
release:
types: [published]
types: [published, prereleased]
jobs:
notify:
@@ -43,15 +46,23 @@ jobs:
--data-urlencode "text=$TEXT"
- name: Send to Telegram - New Release
if: github.event_name == 'release' && github.event.action == 'published'
if: github.event_name == 'release' && ((github.event.action == 'published' && !github.event.release.prerelease) || (github.event.action == 'prereleased' && github.event.release.prerelease && github.event.release.name == 'nightly'))
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 }}
ACTION: ${{ github.event.action }}
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)")
if [ "$ACTION" == "prereleased" ]; then
ICON="🌙"
PREFIX="New Nightly Release"
else
ICON="🚀"
PREFIX="New Release"
fi
TEXT=$(echo -e "$ICON $PREFIX *$NAME* ($TAG)\n\n$BODY_TRUNC\n\n[View Release]($URL)")
curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN }}/sendMessage" \
-d chat_id="${{ vars.TELEGRAM_CHAT_ID }}" \
${{ vars.TELEGRAM_THREAD_ID && format('-d message_thread_id="{0}"', vars.TELEGRAM_THREAD_ID) || '' }} \
@@ -106,15 +117,23 @@ jobs:
-d "$PAYLOAD"
- name: Send to Matrix - New Release
if: github.event_name == 'release' && github.event.action == 'published'
if: github.event_name == 'release' && ((github.event.action == 'published' && !github.event.release.prerelease) || (github.event.action == 'prereleased' && github.event.release.prerelease && github.event.release.name == 'nightly'))
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 }}
ACTION: ${{ github.event.action }}
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>")
if [ "$ACTION" == "prereleased" ]; then
ICON="🌙"
PREFIX="New Nightly Release"
else
ICON="🚀"
PREFIX="New Release"
fi
PLAIN_MESSAGE=$(echo -e "$ICON $PREFIX $NAME ($TAG)\n\n$BODY\n\nView Release: $URL")
HTML_MESSAGE=$(echo -e "<p>$ICON $PREFIX <strong>$NAME</strong> ($TAG)</p><p>$BODY</p><p><a href=\"$URL\">View Release</a></p>")
PLAIN_MESSAGE="${PLAIN_MESSAGE:0:220}"
PAYLOAD=$(jq -n --arg body "$PLAIN_MESSAGE" --arg formatted "$HTML_MESSAGE" '{
"msgtype": "m.text",
+4 -4
View File
@@ -109,7 +109,7 @@ jobs:
run: mkdir ${{ github.workspace }}/temp
- name: Download artifacts
uses: actions/download-artifact@v5
uses: actions/download-artifact@v6
with:
pattern: android_artifacts_*
path: ${{ github.workspace }}/temp
@@ -118,8 +118,8 @@ jobs:
- name: Set version release notes
if: ${{ github.event_name == 'push' || inputs.release_type == 'release' }}
run: |
VERSION_NAME=$(grep "const val VERSION_NAME" buildSrc/src/main/kotlin/Constants.kt | awk -F'"' '{print $2}')
RELEASE_NOTES="$(cat ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/${VERSION_NAME}.txt || echo "No changelog found for ${VERSION_NAME}")"
VERSION_CODE=$(grep "const val VERSION_CODE" buildSrc/src/main/kotlin/Constants.kt | awk -F'"' '{print $2}')
RELEASE_NOTES="$(cat ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/${VERSION_CODE}.txt || echo "No changelog found for ${VERSION_CODE}")"
echo "RELEASE_NOTES<<EOF" >> $GITHUB_ENV
echo "$RELEASE_NOTES" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
@@ -162,7 +162,7 @@ jobs:
files: |
${{ github.workspace }}/temp/**/*.apk
env:
GITHUB_TOKEN: ${{ github.token }}
GITHUB_TOKEN: ${{ secrets.PAT }}
publish-fdroid-public:
runs-on: ubuntu-latest
+5 -8
View File
@@ -21,8 +21,7 @@ and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/)
<div align="center">
[![Google Play](https://img.shields.io/badge/Google_Play-414141?style=for-the-badge&logo=google-play&logoColor=white)](https://play.google.com/store/apps/details?id=com.zaneschepke.wireguardautotunnel)
[![F-Droid](https://img.shields.io/static/v1?style=for-the-badge&message=F-Droid&color=1976D2&logo=F-Droid&logoColor=FFFFFF&label=)](https://f-droid.org/packages/com.zaneschepke.wireguardautotunnel/)
[![Personal](https://img.shields.io/static/v1?style=for-the-badge&message=Personal&color=1976D2&logo=F-Droid&logoColor=FFFFFF&label=)](https://github.com/zaneschepke/fdroid)
[![F-Droid](https://img.shields.io/static/v1?style=for-the-badge&message=F-Droid&color=1976D2&logo=F-Droid&logoColor=FFFFFF&label=)](https://github.com/zaneschepke/fdroid)
[![Obtainium](https://img.shields.io/badge/Obtainium-414141?style=for-the-badge&logo=Obtainium&logoColor=white)](https://apps.obtainium.imranr.dev/redirect?r=obtainium://app/%7B%22id%22%3A%22com.zaneschepke.wireguardautotunnel%22%2C%22url%22%3A%22https%3A%2F%2Fgithub.com%2Fzaneschepke%2Fwgtunnel%22%2C%22author%22%3A%22zaneschepke%22%2C%22name%22%3A%22WG%20Tunnel%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Atrue%2C%5C%22sortMethodChoice%5C%22%3A%5C%22date%5C%22%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Atrue%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22WG%20Tunnel%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22Zane%20Schepke%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%7D%22%2C%22overrideSource%22%3Anull%7D)
</div>
@@ -60,14 +59,12 @@ WG Tunnel is an alternative Android client for WireGuard and AmneziaWG, inspired
</div>
<div style="display: flex; flex-wrap: wrap; justify-content: left; gap: 10px;">
<img label="Main" src="fastlane/metadata/android/en-US/images/phoneScreenshots/main_screen.png" width="200" />
<img label="Settings" src="fastlane/metadata/android/en-US/images/phoneScreenshots/settings_screen.png" width="200" />
<img label="Auto" src="fastlane/metadata/android/en-US/images/phoneScreenshots/auto_screen.png" width="200" />
<img label="Config" src="fastlane/metadata/android/en-US/images/phoneScreenshots/config_screen.png" width="200" />
<img label="Main" src="fastlane/metadata/android/en-US/images/phoneScreenshots/main_screen.png" width="200" alt="Main"/>
<img label="Config" src="fastlane/metadata/android/en-US/images/phoneScreenshots/config_screen.png" width="200" alt="Config"/>
<img label="Settings" src="fastlane/metadata/android/en-US/images/phoneScreenshots/settings_screen.png" width="200" alt="Settings"/>
<img label="Auto-tunnel" src="fastlane/metadata/android/en-US/images/phoneScreenshots/auto_screen.png" width="200" alt="Auto-tunnel"/>
</div>
<div style="text-align: left;">
## Features
- **Tunnel Import Methods**: Easily add tunnels using .conf files, ZIP archives, manual entry, or QR code scanning.
+36 -12
View File
@@ -1,3 +1,4 @@
import com.android.build.gradle.internal.api.BaseVariantOutputImpl
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
@@ -27,6 +28,15 @@ android {
// fix okhttp proguard issue
packaging { resources { pickFirsts.add("okhttp3/internal/publicsuffix/publicsuffixes.gz") } }
splits {
abi {
isEnable = true
reset()
include("armeabi-v7a", "arm64-v8a")
isUniversalApk = true
}
}
defaultConfig {
applicationId = Constants.APP_ID
minSdk = Constants.MIN_SDK
@@ -128,19 +138,33 @@ android {
allowedLicenseUrls().forEach { allowUrl(it) }
}
applicationVariants.all {
android.applicationVariants.all {
val variant = this
variant.outputs
.map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl }
.forEach { output ->
val outputFileName =
if (variant.flavorName == "fdroid" && variant.buildType.name == "release") {
"${Constants.APP_NAME}-fdroid-release-${variant.versionName}.apk"
} else {
"${Constants.APP_NAME}-${variant.flavorName}-v${variant.versionName}.apk"
}
output.outputFileName = outputFileName
}
val abiNameMap =
mapOf(
"armeabi-v7a" to "armv7",
"arm64-v8a" to "arm64",
"x86" to "x86",
"x86_64" to "x64",
)
variant.outputs.all {
val output = this as BaseVariantOutputImpl
val abi = output.getFilter("ABI")
val baseFileName = "${Constants.APP_NAME}-${variant.flavorName}-v${variant.versionName}"
val outputFileName =
if (!abi.isNullOrEmpty()) {
val shortAbiName = abiNameMap.getOrDefault(abi, abi)
"${baseFileName}-${shortAbiName}.apk"
} else {
"${baseFileName}.apk"
}
output.outputFileName = outputFileName
}
}
}
@@ -0,0 +1,509 @@
{
"formatVersion": 1,
"database": {
"version": 26,
"identityHash": "a420594a08fff58ecda3e0424fb43e47",
"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, `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": "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"
]
}
},
{
"tableName": "lockdown_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `bypass_lan` INTEGER NOT NULL DEFAULT 0, `metered` INTEGER NOT NULL DEFAULT 0, `dual_stack` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "bypassLan",
"columnName": "bypass_lan",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "metered",
"columnName": "metered",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "dualStack",
"columnName": "dual_stack",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a420594a08fff58ecda3e0424fb43e47')"
]
}
}
@@ -0,0 +1,523 @@
{
"formatVersion": 1,
"database": {
"version": 27,
"identityHash": "98452d8160a1ae66c852ec8cd739e675",
"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 '[]', `is_metered` INTEGER NOT NULL DEFAULT true)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "wgQuick",
"columnName": "wg_quick",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tunnelNetworks",
"columnName": "tunnel_networks",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isMobileDataTunnel",
"columnName": "is_mobile_data_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPrimaryTunnel",
"columnName": "is_primary_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "amQuick",
"columnName": "am_quick",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isActive",
"columnName": "is_Active",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "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": "'[]'"
},
{
"fieldPath": "isMetered",
"columnName": "is_metered",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "true"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_tunnel_config_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_tunnel_config_name` ON `${TABLE_NAME}` (`name`)"
}
]
},
{
"tableName": "proxy_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `socks5_proxy_enabled` INTEGER NOT NULL DEFAULT 0, `socks5_proxy_bind_address` TEXT, `http_proxy_enable` INTEGER NOT NULL DEFAULT 0, `http_proxy_bind_address` TEXT, `proxy_username` TEXT, `proxy_password` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "socks5ProxyEnabled",
"columnName": "socks5_proxy_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "socks5ProxyBindAddress",
"columnName": "socks5_proxy_bind_address",
"affinity": "TEXT"
},
{
"fieldPath": "httpProxyEnabled",
"columnName": "http_proxy_enable",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "httpProxyBindAddress",
"columnName": "http_proxy_bind_address",
"affinity": "TEXT"
},
{
"fieldPath": "proxyUsername",
"columnName": "proxy_username",
"affinity": "TEXT"
},
{
"fieldPath": "proxyPassword",
"columnName": "proxy_password",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "general_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT 0, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT 0, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `global_split_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `app_mode` INTEGER NOT NULL DEFAULT 0, `theme` TEXT NOT NULL DEFAULT 'AUTOMATIC', `locale` TEXT, `remote_key` TEXT, `is_remote_control_enabled` INTEGER NOT NULL DEFAULT 0, `is_pin_lock_enabled` INTEGER NOT NULL DEFAULT 0, `is_always_on_vpn_enabled` INTEGER NOT NULL DEFAULT 0, `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": "isGlobalSplitTunnelEnabled",
"columnName": "global_split_tunnel_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": "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, `global_tunnel_dns_enabled` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dnsProtocol",
"columnName": "dns_protocol",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "dnsEndpoint",
"columnName": "dns_endpoint",
"affinity": "TEXT"
},
{
"fieldPath": "isGlobalTunnelDnsEnabled",
"columnName": "global_tunnel_dns_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "lockdown_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `bypass_lan` INTEGER NOT NULL DEFAULT 0, `metered` INTEGER NOT NULL DEFAULT 0, `dual_stack` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "bypassLan",
"columnName": "bypass_lan",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "metered",
"columnName": "metered",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "dualStack",
"columnName": "dual_stack",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '98452d8160a1ae66c852ec8cd739e675')"
]
}
}
@@ -0,0 +1,523 @@
{
"formatVersion": 1,
"database": {
"version": 28,
"identityHash": "4792d0cc61a527c69962b5e58463e6da",
"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 '[]', `is_metered` INTEGER NOT NULL DEFAULT true)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "wgQuick",
"columnName": "wg_quick",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tunnelNetworks",
"columnName": "tunnel_networks",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isMobileDataTunnel",
"columnName": "is_mobile_data_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPrimaryTunnel",
"columnName": "is_primary_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "amQuick",
"columnName": "am_quick",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isActive",
"columnName": "is_Active",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "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": "'[]'"
},
{
"fieldPath": "isMetered",
"columnName": "is_metered",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "true"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_tunnel_config_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_tunnel_config_name` ON `${TABLE_NAME}` (`name`)"
}
]
},
{
"tableName": "proxy_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `socks5_proxy_enabled` INTEGER NOT NULL DEFAULT 0, `socks5_proxy_bind_address` TEXT, `http_proxy_enable` INTEGER NOT NULL DEFAULT 0, `http_proxy_bind_address` TEXT, `proxy_username` TEXT, `proxy_password` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "socks5ProxyEnabled",
"columnName": "socks5_proxy_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "socks5ProxyBindAddress",
"columnName": "socks5_proxy_bind_address",
"affinity": "TEXT"
},
{
"fieldPath": "httpProxyEnabled",
"columnName": "http_proxy_enable",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "httpProxyBindAddress",
"columnName": "http_proxy_bind_address",
"affinity": "TEXT"
},
{
"fieldPath": "proxyUsername",
"columnName": "proxy_username",
"affinity": "TEXT"
},
{
"fieldPath": "proxyPassword",
"columnName": "proxy_password",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "general_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT 0, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT 0, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `global_split_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `app_mode` INTEGER NOT NULL DEFAULT 0, `theme` TEXT NOT NULL DEFAULT 'AUTOMATIC', `locale` TEXT, `remote_key` TEXT, `is_remote_control_enabled` INTEGER NOT NULL DEFAULT 0, `is_pin_lock_enabled` INTEGER NOT NULL DEFAULT 0, `is_always_on_vpn_enabled` INTEGER NOT NULL DEFAULT 0, `already_donated` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isShortcutsEnabled",
"columnName": "is_shortcuts_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isRestoreOnBootEnabled",
"columnName": "is_restore_on_boot_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isMultiTunnelEnabled",
"columnName": "is_multi_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isGlobalSplitTunnelEnabled",
"columnName": "global_split_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "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": "alreadyDonated",
"columnName": "already_donated",
"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, `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, `global_tunnel_dns_enabled` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dnsProtocol",
"columnName": "dns_protocol",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "dnsEndpoint",
"columnName": "dns_endpoint",
"affinity": "TEXT"
},
{
"fieldPath": "isGlobalTunnelDnsEnabled",
"columnName": "global_tunnel_dns_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "lockdown_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `bypass_lan` INTEGER NOT NULL DEFAULT 0, `metered` INTEGER NOT NULL DEFAULT 0, `dual_stack` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "bypassLan",
"columnName": "bypass_lan",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "metered",
"columnName": "metered",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "dualStack",
"columnName": "dual_stack",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4792d0cc61a527c69962b5e58463e6da')"
]
}
}
+4 -11
View File
@@ -5,6 +5,9 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!--for split tunneling-->
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
<!--foreground service special use for non VPN service tunnels, android 14-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<!--foreground service special use for VPN service tunnels, android 14-->
@@ -14,6 +17,7 @@
<!--foreground service permissions-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<!--start service on boot permission-->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
@@ -42,17 +46,6 @@
<uses-feature android:name="android.hardware.wifi"
android:required="false"/>
<queries>
<intent>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent>
<intent>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent>
</queries>
<application
android:name=".WireGuardAutoTunnel"
android:allowBackup="false"
@@ -18,9 +18,16 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextLinkStyles
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withLink
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
@@ -33,12 +40,12 @@ import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
import androidx.navigation3.ui.NavDisplay
import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
import com.zaneschepke.wireguardautotunnel.data.DataStoreManager.Companion.shouldShowDonationSnackbar
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
@@ -46,6 +53,9 @@ import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm
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.common.snackbar.SnackbarInfo
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarType
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.rememberCustomSnackbarState
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
import com.zaneschepke.wireguardautotunnel.ui.navigation.Tab
import com.zaneschepke.wireguardautotunnel.ui.navigation.components.BottomNavbar
@@ -64,8 +74,8 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.Appear
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.display.DisplayScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.language.LanguageScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.dns.DnsSettingsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.globals.TunnelGlobalsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.integrations.AndroidIntegrationsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.lockdown.LockdownSettingsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.TunnelMonitoringScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.logs.LogsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.ping.PingTargetScreen
@@ -75,9 +85,9 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.support.donate.crypto.Addr
import com.zaneschepke.wireguardautotunnel.ui.screens.support.license.LicenseScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.TunnelsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.config.ConfigScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.settings.TunnelSettingsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.sort.SortScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.splittunnel.SplitTunnelScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.tunneloptions.TunnelOptionsScreen
import com.zaneschepke.wireguardautotunnel.ui.theme.AlertRed
import com.zaneschepke.wireguardautotunnel.ui.theme.OffWhite
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
@@ -99,8 +109,7 @@ import xyz.teamgravity.pin_lock_compose.PinManager
class MainActivity : AppCompatActivity() {
@Inject lateinit var appStateRepository: AppStateRepository
@Inject lateinit var tunnelManager: TunnelManager
@Inject lateinit var networkMonitor: NetworkMonitor
@Inject lateinit var tunnelRepository: TunnelRepository
@Inject lateinit var appDatabase: AppDatabase
private lateinit var roomBackup: RoomBackup
@@ -127,16 +136,16 @@ class MainActivity : AppCompatActivity() {
setContent {
val context = LocalContext.current
val isTv = isRunningOnTv()
val appState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
val scope = rememberCoroutineScope()
LaunchedEffect(appState.isAppLoaded) {
if (appState.isAppLoaded) {
appState.locale.let { LocaleUtil.changeLocale(it) }
LaunchedEffect(uiState.isAppLoaded) {
if (uiState.isAppLoaded) {
uiState.locale.let { LocaleUtil.changeLocale(it) }
}
}
val snackbar = remember { SnackbarHostState() }
val snackbarState = rememberCustomSnackbarState()
var showVpnPermissionDialog by remember { mutableStateOf(false) }
var vpnPermissionDenied by remember { mutableStateOf(false) }
var requestingAppMode by remember {
@@ -146,14 +155,14 @@ class MainActivity : AppCompatActivity() {
val startingStack = buildList {
add(Route.Tunnels)
if (intent?.action == Intent.ACTION_APPLICATION_PREFERENCES) add(Route.Settings)
if (appState.pinLockEnabled) add(Route.Lock)
if (uiState.pinLockEnabled) add(Route.Lock)
}
val backStack = rememberNavBackStack(*startingStack.toTypedArray())
var previousRoute by remember { mutableStateOf<Route?>(null) }
val navController =
rememberNavController<NavKey>(backStack, appState.isLocationDisclosureShown) {
rememberNavController<NavKey>(backStack, uiState.isLocationDisclosureShown) {
previousKey ->
previousRoute = previousKey as? Route
}
@@ -189,10 +198,20 @@ class MainActivity : AppCompatActivity() {
vpnActivity.launch(VpnService.prepare(this@MainActivity))
}
is GlobalSideEffect.Snackbar ->
is GlobalSideEffect.Snackbar -> {
scope.launch {
snackbar.showSnackbar(sideEffect.message.asString(context))
snackbarState.showSnackbar(
SnackbarInfo(
message =
buildAnnotatedString {
append(sideEffect.message.asString(context))
},
type = sideEffect.type ?: SnackbarType.INFO,
durationMs = sideEffect.durationMs ?: 4000L,
)
)
}
}
is GlobalSideEffect.Toast ->
scope.launch { context.showToast(sideEffect.message.asString(context)) }
@@ -203,19 +222,19 @@ class MainActivity : AppCompatActivity() {
}
}
if (!appState.isAppLoaded) return@setContent
if (!uiState.isAppLoaded) return@setContent
var showLock by remember {
mutableStateOf(appState.pinLockEnabled && !appState.isPinVerified)
mutableStateOf(uiState.pinLockEnabled && !uiState.isPinVerified)
}
LaunchedEffect(appState.isPinVerified) { if (appState.isPinVerified) showLock = false }
LaunchedEffect(uiState.isPinVerified) { if (uiState.isPinVerified) showLock = false }
CompositionLocalProvider(
LocalIsAndroidTV provides isTv,
LocalSharedVm provides viewModel,
LocalNavController provides navController,
) {
WireguardAutoTunnelTheme(theme = appState.theme) {
WireguardAutoTunnelTheme(theme = uiState.theme) {
VpnDeniedDialog(
showVpnPermissionDialog,
onDismiss = {
@@ -224,6 +243,55 @@ class MainActivity : AppCompatActivity() {
},
)
val annotatedMessage = buildAnnotatedString {
append(context.getString(R.string.donation_prompt_prefix))
append(" ")
withLink(
LinkAnnotation.Clickable(
tag = context.getString(R.string.support),
styles =
TextLinkStyles(
style =
SpanStyle(
textDecoration = TextDecoration.Underline,
color = MaterialTheme.colorScheme.primary,
),
focusedStyle =
SpanStyle(
textDecoration = TextDecoration.Underline,
color = MaterialTheme.colorScheme.primary,
background =
MaterialTheme.colorScheme.primary.copy(
alpha = 0.2f
),
),
),
) {
snackbarState.dismissCurrent()
navController.push(Route.Donate)
}
) {
append(context.getString(R.string.donation_prompt_link))
}
append(" ")
append(context.getString(R.string.donation_prompt_suffix))
}
LaunchedEffect(uiState.shouldShowDonationSnackbar) {
if (
uiState.shouldShowDonationSnackbar && !uiState.settings.alreadyDonated
) {
viewModel.setShouldShowDonationSnackbar(false)
snackbarState.showSnackbar(
SnackbarInfo(
message = annotatedMessage,
type = SnackbarType.THANK_YOU,
durationMs = 30_000L,
)
)
}
}
if (showLock) {
PinManager.initialize(context = this@MainActivity)
PinLockScreen()
@@ -236,14 +304,14 @@ class MainActivity : AppCompatActivity() {
}
val navState by
currentRouteAsNavbarState(
appState,
uiState,
viewModel,
currentRoute,
navController,
)
Box(modifier = Modifier.fillMaxSize()) {
if (appState.settings.appMode == AppMode.LOCK_DOWN) {
if (uiState.settings.appMode == AppMode.LOCK_DOWN) {
AppAlertBanner(
stringResource(R.string.locked_down)
.uppercase(Locale.getDefault()),
@@ -254,14 +322,25 @@ class MainActivity : AppCompatActivity() {
}
Scaffold(
snackbarHost = {
SnackbarHost(snackbar) { snackbarData ->
snackbarState.SnackbarHost(
modifier =
Modifier.align(Alignment.BottomCenter)
.padding(
bottom =
if (LocalIsAndroidTV.current) 120.dp
else 80.dp
)
) { info ->
CustomSnackBar(
snackbarData.visuals.message,
isRtl = false,
message = info.message,
type = info.type,
onDismiss = { snackbarState.dismissCurrent() },
containerColor =
MaterialTheme.colorScheme.surfaceColorAtElevation(
2.dp
),
modifier =
Modifier.wrapContentHeight(align = Alignment.Top),
)
}
},
@@ -269,7 +348,7 @@ class MainActivity : AppCompatActivity() {
bottomBar = {
if (navState.showBottomItems) {
BottomNavbar(
appState.isAutoTunnelActive,
uiState.isAutoTunnelActive,
currentTab,
onTabSelected = { tab ->
navController.popUpTo(tab.startRoute)
@@ -331,7 +410,7 @@ class MainActivity : AppCompatActivity() {
}
entry<Route.Tunnels> { TunnelsScreen() }
entry<Route.Sort> { SortScreen() }
entry<Route.TunnelOptions> { key ->
entry<Route.TunnelSettings> { key ->
val viewModel =
hiltViewModel<
TunnelViewModel,
@@ -341,7 +420,7 @@ class MainActivity : AppCompatActivity() {
factory.create(key.id)
}
)
TunnelOptionsScreen(viewModel)
TunnelSettingsScreen(viewModel)
}
entry<Route.SplitTunnel> { key ->
val viewModel =
@@ -388,9 +467,6 @@ class MainActivity : AppCompatActivity() {
AndroidIntegrationsScreen()
}
entry<Route.Dns> { DnsSettingsScreen() }
entry<Route.TunnelGlobals> { key ->
TunnelGlobalsScreen(key.id)
}
entry<Route.ConfigGlobal> { key ->
val viewModel =
hiltViewModel<
@@ -415,6 +491,9 @@ class MainActivity : AppCompatActivity() {
)
SplitTunnelScreen(viewModel)
}
entry<Route.LockdownSettings> {
LockdownSettingsScreen()
}
entry<Route.ProxySettings> { ProxySettingsScreen() }
entry<Route.Appearance> { AppearanceScreen() }
entry<Route.Language> { LanguageScreen() }
@@ -442,7 +521,6 @@ class MainActivity : AppCompatActivity() {
override fun onResume() {
super.onResume()
WireGuardAutoTunnel.setUiActive(true)
networkMonitor.checkPermissionsAndUpdateState()
}
override fun onPause() {
@@ -452,6 +530,9 @@ class MainActivity : AppCompatActivity() {
fun performBackup() =
lifecycleScope.launch {
// reset active tuns before backup to prevent trying to start them without permission on
// restore
tunnelRepository.resetActiveTunnels()
roomBackup
.database(appDatabase)
.backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG)
@@ -7,18 +7,15 @@ import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration
import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationMonitor
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.domain.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
import dagger.hilt.android.HiltAndroidApp
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
@@ -43,8 +40,6 @@ class WireGuardAutoTunnel : Application(), Configuration.Provider {
@Inject lateinit var notificationMonitor: NotificationMonitor
@Inject lateinit var tunnelManager: TunnelManager
override fun onCreate() {
super.onCreate()
instance = this
@@ -73,12 +68,6 @@ class WireGuardAutoTunnel : Application(), Configuration.Provider {
ServiceWorker.start(this)
}
override fun onTerminate() {
applicationScope.cancel()
tunnelManager.setBackendMode(BackendMode.Inactive)
super.onTerminate()
}
companion object {
private val _uiActive = MutableStateFlow(false)
@@ -6,6 +6,7 @@ import android.content.Intent
import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
@@ -19,6 +20,8 @@ class RestartReceiver : BroadcastReceiver() {
@Inject lateinit var tunnelManager: TunnelManager
@Inject lateinit var appStateRepository: AppStateRepository
@Inject lateinit var logReader: LogReader
override fun onReceive(context: Context, intent: Intent) {
@@ -33,6 +36,7 @@ class RestartReceiver : BroadcastReceiver() {
Intent.ACTION_MY_PACKAGE_REPLACED -> {
tunnelManager.handleRestore()
logReader.deleteAndClearLogs()
appStateRepository.setShouldShowDonationSnackbar(true)
}
}
}
@@ -32,7 +32,7 @@ constructor(
description =
StringValue.StringResource(
R.string.tunnel_error_template,
error.toStringRes(),
error.toStringValue(),
),
groupKey = NotificationManager.VPN_GROUP_KEY,
)
@@ -93,6 +93,7 @@ class AutoTunnelService : LifecycleService() {
override fun onDestroy() {
serviceManager.handleAutoTunnelServiceDestroy()
networkMonitor.destroy()
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
super.onDestroy()
}
@@ -106,9 +106,6 @@ abstract class BaseTunnel(
) {
return Timber.w("Tunnel is already running: ${tunnelConfig.name}")
}
updateTunnelStatus(tunnelConfig.id, TunnelStatus.Starting)
val job =
applicationScope.launch(ioDispatcher) {
try {
@@ -3,18 +3,23 @@ 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.R
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.events.DnsFailure
import com.zaneschepke.wireguardautotunnel.domain.events.InvalidConfig
import com.zaneschepke.wireguardautotunnel.domain.events.KernelTunnelName
import com.zaneschepke.wireguardautotunnel.domain.events.UnknownError
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.domain.state.WireGuardStatistics
import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendCoreException
import java.util.concurrent.ConcurrentHashMap
import java.util.regex.Pattern
import javax.inject.Inject
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
@@ -30,14 +35,27 @@ class KernelTunnel
constructor(
@ApplicationScope applicationScope: CoroutineScope,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
private val runConfigHelper: RunConfigHelper,
@Kernel private val backend: Backend,
) : BaseTunnel(applicationScope, ioDispatcher) {
private val runtimeTunnels = ConcurrentHashMap<Int, WgTunnel>()
// TODO Add DNS settings
private fun validateWireGuardInterfaceName(name: String): Result<Unit> {
if (name.isEmpty() || name.length > 15)
return Result.failure(KernelTunnelName(R.string.kernel_name_error))
if (name == "." || name == "..") {
return Result.failure(KernelTunnelName(R.string.kernel_name_dots))
}
val pattern = Pattern.compile("^[a-zA-Z0-9_=+.-]{1,15}$")
if (!pattern.matcher(name).matches()) {
return Result.failure(KernelTunnelName(R.string.kernel_name_special_characters))
}
return Result.success(Unit)
}
override fun tunnelStateFlow(tunnelConfig: TunnelConfig): Flow<TunnelStatus> = callbackFlow {
if (!tunnelConfig.isNameKernelCompatible) close(BackendCoreException.TunnelNameTooLong)
validateWireGuardInterfaceName(tunnelConfig.name).onFailure { close(it) }
val stateChannel = Channel<WgTunnel.State>()
@@ -51,21 +69,22 @@ constructor(
try {
withTimeout(STARTUP_TIMEOUT_MS) {
updateTunnelStatus(tunnelConfig.id, TunnelStatus.Starting)
backend.setState(runtimeTunnel, WgTunnel.State.UP, tunnelConfig.toWgConfig())
val runConfig = runConfigHelper.buildWgRunConfig(tunnelConfig)
backend.setState(runtimeTunnel, WgTunnel.State.UP, runConfig)
}
} catch (e: TimeoutCancellationException) {
Timber.e("Startup timed out for ${tunnelConfig.name}")
errors.emit(tunnelConfig.name to BackendCoreException.DNS)
errors.emit(tunnelConfig.name to DnsFailure())
forceStopTunnel(tunnelConfig.id)
close()
} catch (e: BackendException) {
close(e.toBackendCoreException())
} catch (e: IllegalArgumentException) {
Timber.e(e, "Invalid backend arguments")
close(BackendCoreException.Config)
close(InvalidConfig())
} catch (e: Exception) {
Timber.e(e, "Error while setting tunnel state")
close(BackendCoreException.Unknown)
close(UnknownError())
}
awaitClose {
@@ -0,0 +1,104 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.data.model.DnsProtocol
import com.zaneschepke.wireguardautotunnel.domain.events.InvalidConfig
import com.zaneschepke.wireguardautotunnel.domain.model.DnsSettings
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings
import com.zaneschepke.wireguardautotunnel.domain.model.ProxySettings
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.repository.DnsSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.ProxySettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import java.util.*
import javax.inject.Inject
import kotlinx.coroutines.flow.firstOrNull
import org.amnezia.awg.config.Config
import org.amnezia.awg.config.proxy.HttpProxy
import org.amnezia.awg.config.proxy.Socks5Proxy
class RunConfigHelper
@Inject
constructor(
private val settingsRepository: GeneralSettingRepository,
private val proxySettingsRepository: ProxySettingsRepository,
private val dnsSettingsRepository: DnsSettingsRepository,
private val tunnelsRepository: TunnelRepository,
) {
private data class PrepResult(
val effectiveConfig: TunnelConfig,
val generalSettings: GeneralSettings,
val dnsSettings: DnsSettings,
)
private suspend fun prepare(tunnelConfig: TunnelConfig): PrepResult {
val generalSettings = settingsRepository.getGeneralSettings()
val dnsSettings = dnsSettingsRepository.getDnsSettings()
val effectiveConfig =
if (
generalSettings.isGlobalSplitTunnelEnabled || dnsSettings.isGlobalTunnelDnsEnabled
) {
val globalConfig =
tunnelsRepository.globalTunnelFlow.firstOrNull() ?: throw InvalidConfig()
tunnelConfig.copyWithGlobalValues(
globalConfig,
dnsSettings.isGlobalTunnelDnsEnabled,
generalSettings.isGlobalSplitTunnelEnabled,
)
} else {
tunnelConfig
}
return PrepResult(effectiveConfig, generalSettings, dnsSettings)
}
suspend fun buildAmRunConfig(tunnelConfig: TunnelConfig): Config {
val prep = prepare(tunnelConfig)
val proxies =
if (prep.generalSettings.appMode == AppMode.PROXY) {
val proxySettings = proxySettingsRepository.getProxySettings()
buildList {
if (proxySettings.socks5ProxyEnabled) {
add(
Socks5Proxy(
proxySettings.socks5ProxyBindAddress
?: ProxySettings.DEFAULT_SOCKS_BIND_ADDRESS,
proxySettings.proxyUsername,
proxySettings.proxyPassword,
)
)
}
if (proxySettings.httpProxyEnabled) {
add(
HttpProxy(
proxySettings.httpProxyBindAddress
?: ProxySettings.DEFAULT_HTTP_BIND_ADDRESS,
proxySettings.proxyUsername,
proxySettings.proxyPassword,
)
)
}
}
} else {
emptyList()
}
val amConfig = prep.effectiveConfig.toAmConfig()
return Config.Builder()
.setInterface(amConfig.`interface`)
.addPeers(amConfig.peers)
.addProxies(proxies)
.setDnsSettings(
org.amnezia.awg.config.DnsSettings(
prep.dnsSettings.dnsProtocol == DnsProtocol.DOH,
Optional.ofNullable(prep.dnsSettings.dnsEndpoint),
)
)
.build()
}
suspend fun buildWgRunConfig(tunnelConfig: TunnelConfig): com.wireguard.config.Config {
val prep = prepare(tunnelConfig)
return prep.effectiveConfig.toWgConfig()
}
}
@@ -16,4 +16,6 @@ class RuntimeAwgTunnel(
}
override fun isIpv4ResolutionPreferred() = tunnelConfig.isIpv4Preferred
override fun isMetered() = tunnelConfig.isMetered
}
@@ -1,18 +1,19 @@
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.events.NotAuthorized
import com.zaneschepke.wireguardautotunnel.domain.model.AutoTunnelSettings
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.LockdownSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.state.LogHealthState
import com.zaneschepke.wireguardautotunnel.domain.state.PingState
@@ -39,6 +40,7 @@ constructor(
private val serviceManager: ServiceManager,
private val settingsRepository: GeneralSettingRepository,
private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
private val lockdownSettingsRepository: LockdownSettingsRepository,
private val tunnelsRepository: TunnelRepository,
private val tunnelMonitor: TunnelMonitor,
@ApplicationScope private val applicationScope: CoroutineScope,
@@ -69,12 +71,6 @@ 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 tunnelProviderFlow: StateFlow<TunnelProvider> = run {
val currentBackend = AtomicReference(userspaceTunnel)
val currentSettings = AtomicReference(GeneralSettings())
@@ -84,10 +80,7 @@ constructor(
.filterNotNull()
// ignore default state
.filterNot { it == GeneralSettings() }
.distinctUntilChanged { old, new ->
old.appMode == new.appMode &&
old.isLanOnKillSwitchEnabled == new.isLanOnKillSwitchEnabled
}
.distinctUntilChangedBy { it.appMode }
.map { settings ->
Timber.d("App mode changes with ${settings.appMode}")
val backend =
@@ -108,7 +101,7 @@ constructor(
handleModeChangeCleanup(previousBackend, previousSettings.appMode)
}
if (settings.appMode == AppMode.LOCK_DOWN) {
handleLockDownModeInit(settings.isLanOnKillSwitchEnabled)
handleLockDownModeInit()
}
}
.map { (_, backend) -> backend }
@@ -235,17 +228,7 @@ constructor(
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(tunnelConfig)
}
override suspend fun stopTunnel(tunnelId: Int) {
@@ -302,13 +285,23 @@ constructor(
serviceManager.updateTunnelTile()
}
private fun handleLockDownModeInit(withLanBypass: Boolean) {
val allowedIps = if (withLanBypass) TunnelConfig.IPV4_PUBLIC_NETWORKS else emptySet()
// TODO this can crash if we haven't started foreground service yet, especially for
// workerManager
private suspend fun handleLockDownModeInit() {
val lockdownSettings = lockdownSettingsRepository.getLockdownSettings()
val allowedIps =
if (lockdownSettings.bypassLan) TunnelConfig.IPV4_PUBLIC_NETWORKS else emptySet()
try {
if (serviceManager.hasVpnPermission()) {
proxyUserspaceTunnel.setBackendMode(BackendMode.KillSwitch(allowedIps))
proxyUserspaceTunnel.setBackendMode(
BackendMode.KillSwitch(
allowedIps,
lockdownSettings.metered,
lockdownSettings.dualStack,
)
)
} else {
throw BackendCoreException.NotAuthorized
throw NotAuthorized()
}
} catch (e: BackendCoreException) {
localErrorEvents.tryEmit(null to e)
@@ -325,18 +318,6 @@ 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
}
}
suspend fun handleRestore() =
withContext(ioDispatcher) {
val settings = settingsRepository.getGeneralSettings()
@@ -344,20 +325,19 @@ constructor(
val tunnels = tunnelsRepository.getAll()
if (autoTunnelSettings.isAutoTunnelEnabled)
return@withContext restoreAutoTunnel(autoTunnelSettings)
if (isVpnAuthorized(settings.appMode)) {
when (val mode = settings.appMode) {
if (settings.appMode == AppMode.LOCK_DOWN) handleLockDownModeInit()
if (tunnels.any { it.isActive }) {
if (settings.appMode == AppMode.VPN && !serviceManager.hasVpnPermission())
return@withContext localErrorEvents.emit(null to NotAuthorized())
when (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) }
}
} else {
localErrorEvents.emit(null to BackendCoreException.NotAuthorized)
}
}
@@ -375,18 +355,15 @@ constructor(
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)
when (settings.appMode) {
AppMode.LOCK_DOWN -> handleLockDownModeInit()
AppMode.VPN ->
if (!serviceManager.hasVpnPermission())
return@withContext localErrorEvents.emit(null to NotAuthorized())
AppMode.KERNEL,
AppMode.PROXY -> Unit
}
defaultTunnel?.let { startTunnel(it) }
}
}
@@ -420,6 +397,46 @@ constructor(
}
}
suspend fun restartActiveTunnel(id: Int) =
withContext(ioDispatcher) {
val activeIds = activeTunnels.value.keys.toList()
if (activeIds.isEmpty()) return@withContext
if (!activeIds.contains(id)) return@withContext
val tunnel = tunnelsRepository.getById(id) ?: return@withContext
restartTunnel(tunnel)
}
suspend fun restartActiveTunnels() =
withContext(ioDispatcher) {
val activeIds = activeTunnels.value.keys.toList()
if (activeIds.isEmpty()) return@withContext
val tunnels = tunnelsRepository.getAll()
if (tunnels.isEmpty()) return@withContext
supervisorScope {
activeIds.forEach { id ->
val tunnel =
tunnels.find { it.id == id }
?: run {
Timber.w("Tunnel config $id not found; skipping restart")
return@forEach
}
restartTunnel(tunnel)
}
}
}
private suspend fun restartTunnel(tunnel: TunnelConfig) {
runCatching { stopTunnel(tunnel.id) }
.onFailure { e -> Timber.e(e, "Failed to stop tunnel ${tunnel.id} during restart") }
delay(RESTART_TUNNEL_DELAY)
runCatching { startTunnel(tunnel) }
.onFailure { e -> Timber.e(e, "Failed to restart tunnel ${tunnel.id}") }
}
private suspend fun handleDynamicDnsMonitoring(
activeTuns: Map<Int, TunnelState>,
configs: List<TunnelConfig>,
@@ -536,5 +553,6 @@ constructor(
companion object {
const val BASE_BACKOFF = 30_000L
const val MAX_BACKOFF_TIME = 300_000L
const val RESTART_TUNNEL_DELAY = 300L
}
}
@@ -1,5 +1,6 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import android.os.PowerManager
import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
@@ -11,17 +12,17 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.state.*
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 javax.inject.Singleton
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import timber.log.Timber
@ServiceScoped
@Singleton
class TunnelMonitor
@Inject
constructor(
@@ -31,6 +32,7 @@ constructor(
private val networkMonitor: NetworkMonitor,
private val networkUtils: NetworkUtils,
private val logReader: LogReader,
private val powerManager: PowerManager,
) {
@OptIn(FlowPreview::class)
@@ -74,7 +76,7 @@ constructor(
else -> null
}
}
.distinctUntilChangedBy { it.isHealthy } // Only emit when health changes
.distinctUntilChangedBy { it.isHealthy }
.collect { logHealthState ->
Timber.d("Tunnel log health updated for ${tunnelConfig.name}: $logHealthState")
updateTunnelStatus(tunnelConfig.id, null, null, null, logHealthState)
@@ -199,6 +201,7 @@ constructor(
}
val attemptTime = System.currentTimeMillis()
val timeout = settings.tunnelPingTimeoutSeconds?.toMillis() ?: 5000L
runCatching {
withTimeout(
settings.tunnelPingTimeoutSeconds?.toMillis() ?: 5000L
@@ -270,20 +273,28 @@ constructor(
while (isActive) {
ensureActive()
if (isNetworkConnected.value) {
performPing()
} else {
pingStatsFlow.update { current ->
current.mapValues { entry ->
entry.value.copy(
isReachable = false,
failureReason = FailureReason.NoConnectivity,
lastPingAttemptMillis = System.currentTimeMillis(),
)
if (!powerManager.isDeviceIdleMode) {
if (isNetworkConnected.value) {
performPing()
} else {
pingStatsFlow.update { current ->
current.mapValues { entry ->
entry.value.copy(
isReachable = false,
failureReason = FailureReason.NoConnectivity,
lastPingAttemptMillis = System.currentTimeMillis(),
)
}
}
ensureActive()
updateTunnelStatus(
tunnelConfig.id,
null,
null,
pingStatsFlow.value,
null,
)
}
ensureActive()
updateTunnelStatus(tunnelConfig.id, null, null, pingStatsFlow.value, null)
}
delay(settings.tunnelPingIntervalSeconds.toMillis())
}
@@ -300,9 +311,11 @@ constructor(
) = coroutineScope {
while (isActive) {
ensureActive()
val stats = getStatistics(tunnelId)
ensureActive()
updateTunnelStatus(tunnelId, null, stats, null, null)
if (!powerManager.isDeviceIdleMode) {
val stats = getStatistics(tunnelId)
ensureActive()
updateTunnelStatus(tunnelId, null, stats, null, null)
}
delay(STATS_DELAY)
}
}
@@ -1,15 +1,11 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
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.events.*
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.state.AmneziaStatistics
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.util.extensions.asAmBackendMode
@@ -17,7 +13,6 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.asBackendMode
import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendCoreException
import java.io.IOException
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
import kotlinx.coroutines.*
@@ -29,13 +24,7 @@ import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.update
import org.amnezia.awg.backend.Backend
import org.amnezia.awg.backend.BackendException
import org.amnezia.awg.backend.ProxyGoBackend
import org.amnezia.awg.backend.Tunnel as AwgTunnel
import org.amnezia.awg.config.Config
import org.amnezia.awg.config.DnsSettings
import org.amnezia.awg.config.proxy.HttpProxy
import org.amnezia.awg.config.proxy.Proxy
import org.amnezia.awg.config.proxy.Socks5Proxy
import timber.log.Timber
class UserspaceTunnel
@@ -43,9 +32,8 @@ class UserspaceTunnel
constructor(
@ApplicationScope applicationScope: CoroutineScope,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
private val proxySettingsRepository: ProxySettingsRepository,
private val dnsSettingsRepository: DnsSettingsRepository,
private val backend: Backend,
private val runConfigHelper: RunConfigHelper,
) : BaseTunnel(applicationScope, ioDispatcher) {
private val runtimeTunnels = ConcurrentHashMap<Int, AwgTunnel>()
@@ -63,67 +51,21 @@ constructor(
try {
withTimeout(STARTUP_TIMEOUT_MS) {
updateTunnelStatus(tunnelConfig.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,
)
)
}
if (proxySettings.httpProxyEnabled) {
add(
HttpProxy(
proxySettings.httpProxyBindAddress
?: ProxySettings.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),
)
)
}
.build()
backend.setState(runtimeTunnel, AwgTunnel.State.UP, updatedConfig)
val runConfig = runConfigHelper.buildAmRunConfig(tunnelConfig)
backend.setState(runtimeTunnel, AwgTunnel.State.UP, runConfig)
}
} catch (e: TimeoutCancellationException) {
} catch (_: TimeoutCancellationException) {
Timber.e("Startup timed out for ${tunnelConfig.name} (likely DNS hang)")
errors.emit(tunnelConfig.name to BackendCoreException.DNS)
errors.emit(tunnelConfig.name to DnsFailure())
forceStopTunnel(tunnelConfig.id)
close()
} catch (e: BackendException) {
close(e.toBackendCoreException())
} catch (e: IllegalArgumentException) {
close(BackendCoreException.Config)
} catch (_: IllegalArgumentException) {
close(InvalidConfig())
} catch (e: Exception) {
Timber.e(e, "Error while setting tunnel state")
close(BackendCoreException.Unknown)
close(UnknownError())
}
awaitClose {
@@ -149,7 +91,7 @@ constructor(
throw e.toBackendCoreException()
// TODO this should be mapped to BackendException in the lib
} catch (e: IOException) {
throw BackendCoreException.NotAuthorized
throw VpnUnauthorized()
}
}
@@ -158,7 +100,7 @@ constructor(
}
override fun handleDnsReresolve(tunnelConfig: TunnelConfig): Boolean {
val tunnel = runtimeTunnels[tunnelConfig.id] ?: throw BackendCoreException.ServiceNotRunning
val tunnel = runtimeTunnels[tunnelConfig.id] ?: throw ServiceNotRunning()
return backend.resolveDDNS(tunnelConfig.toAmConfig(), tunnel.isIpv4ResolutionPreferred)
}
@@ -15,8 +15,9 @@ import com.zaneschepke.wireguardautotunnel.data.entity.*
AutoTunnelSettings::class,
MonitoringSettings::class,
DnsSettings::class,
LockdownSettings::class,
],
version = 25,
version = 28,
autoMigrations =
[
AutoMigration(from = 1, to = 2),
@@ -42,6 +43,8 @@ import com.zaneschepke.wireguardautotunnel.data.entity.*
AutoMigration(from = 21, to = 22),
AutoMigration(from = 22, to = 23),
AutoMigration(from = 24, to = 25),
AutoMigration(from = 26, to = 27, spec = GlobalsMigration::class),
AutoMigration(from = 27, to = 28, spec = DonationMigration::class),
],
exportSchema = true,
)
@@ -57,6 +60,8 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun monitoringSettingsDao(): MonitoringSettingsDao
abstract fun lockdownSettingsDao(): LockdownSettingsDao
abstract fun dnsSettingsDao(): DnsSettingsDao
}
@@ -112,3 +117,15 @@ class FixProxySettingsMigration : AutoMigrationSpec {
}
}
}
@RenameColumn.Entries(
RenameColumn(
tableName = "general_settings",
fromColumnName = "is_tunnel_globals_enabled",
toColumnName = "global_split_tunnel_enabled",
)
)
class GlobalsMigration : AutoMigrationSpec
@DeleteColumn(tableName = "general_settings", columnName = "custom_split_packages")
class DonationMigration : AutoMigrationSpec
@@ -26,6 +26,7 @@ class DataStoreManager(
companion object {
val locationDisclosureShown = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN")
val batteryDisableShown = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN")
val shouldShowDonationSnackbar = booleanPreferencesKey("SHOW_DONATION_SNACK")
}
suspend fun init() {
@@ -0,0 +1,18 @@
package com.zaneschepke.wireguardautotunnel.data.dao
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Upsert
import com.zaneschepke.wireguardautotunnel.data.entity.LockdownSettings
import kotlinx.coroutines.flow.Flow
@Dao
interface LockdownSettingsDao {
@Query("SELECT * FROM lockdown_settings LIMIT 1")
suspend fun getLockdownSettings(): LockdownSettings?
@Upsert suspend fun upsert(lockdownSettings: LockdownSettings)
@Query("SELECT * FROM lockdown_settings LIMIT 1")
fun getLockdownSettingsFlow(): Flow<LockdownSettings?>
}
@@ -3,4 +3,5 @@ package com.zaneschepke.wireguardautotunnel.data.entity
data class AppState(
val isLocationDisclosureShown: Boolean = false,
val isBatteryOptimizationDisableShown: Boolean = false,
val shouldShowDonationSnackbar: Boolean = false,
)
@@ -11,4 +11,6 @@ data class DnsSettings(
@ColumnInfo(name = "dns_protocol", defaultValue = "0")
val dnsProtocol: DnsProtocol = DnsProtocol.fromValue(0),
@ColumnInfo(name = "dns_endpoint") val dnsEndpoint: String? = null,
@ColumnInfo(name = "global_tunnel_dns_enabled", defaultValue = "0")
val isGlobalTunnelDnsEnabled: Boolean = false,
)
@@ -14,8 +14,8 @@ data class GeneralSettings(
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 = "global_split_tunnel_enabled", defaultValue = "0")
val isGlobalSplitTunnelEnabled: 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,
@@ -26,8 +26,5 @@ data class GeneralSettings(
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(),
@ColumnInfo(name = "already_donated", defaultValue = "0") val alreadyDonated: Boolean = false,
)
@@ -0,0 +1,13 @@
package com.zaneschepke.wireguardautotunnel.data.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "lockdown_settings")
data class LockdownSettings(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
@ColumnInfo(name = "bypass_lan", defaultValue = "0") val bypassLan: Boolean = false,
@ColumnInfo(name = "metered", defaultValue = "0") val metered: Boolean = false,
@ColumnInfo(name = "dual_stack", defaultValue = "0") val dualStack: Boolean = false,
)
@@ -28,8 +28,8 @@ data class TunnelConfig(
@ColumnInfo(name = "position", defaultValue = "0") val position: Int = 0,
@ColumnInfo(name = "auto_tunnel_apps", defaultValue = "[]")
val autoTunnelApps: Set<String> = emptySet(),
@ColumnInfo(name = "is_metered", defaultValue = "true") val isMetered: Boolean = true,
) {
companion object {
const val GLOBAL_CONFIG_NAME = "4675ab06-903a-438b-8485-6ea4187a9512"
}
@@ -4,7 +4,17 @@ 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)
Domain(
id = id,
dnsProtocol = dnsProtocol,
dnsEndpoint = dnsEndpoint,
isGlobalTunnelDnsEnabled = isGlobalTunnelDnsEnabled,
)
fun Domain.toEntity(): Entity =
Entity(id = id, dnsProtocol = dnsProtocol, dnsEndpoint = dnsEndpoint)
Entity(
id = id,
dnsProtocol = dnsProtocol,
dnsEndpoint = dnsEndpoint,
isGlobalTunnelDnsEnabled = isGlobalTunnelDnsEnabled,
)
@@ -7,4 +7,5 @@ fun Entity.toDomain(): Domain =
Domain(
isLocationDisclosureShown = isLocationDisclosureShown,
isBatteryOptimizationDisableShown = isBatteryOptimizationDisableShown,
shouldShowDonationSnackbar = shouldShowDonationSnackbar,
)
@@ -0,0 +1,10 @@
package com.zaneschepke.wireguardautotunnel.data.mapper
import com.zaneschepke.wireguardautotunnel.data.entity.LockdownSettings as Entity
import com.zaneschepke.wireguardautotunnel.domain.model.LockdownSettings as Domain
fun Entity.toDomain(): Domain =
Domain(id = id, bypassLan = bypassLan, metered = metered, dualStack = dualStack)
fun Domain.toEntity(): Entity =
Entity(id = id, bypassLan = bypassLan, metered = metered, dualStack = dualStack)
@@ -10,7 +10,7 @@ fun Entity.toDomain(): Domain =
isShortcutsEnabled = isShortcutsEnabled,
isRestoreOnBootEnabled = isRestoreOnBootEnabled,
isMultiTunnelEnabled = isMultiTunnelEnabled,
isTunnelGlobalsEnabled = isTunnelGlobalsEnabled,
isGlobalSplitTunnelEnabled = isGlobalSplitTunnelEnabled,
appMode = appMode,
theme = Theme.valueOf(theme.uppercase()),
locale = locale,
@@ -18,8 +18,7 @@ fun Entity.toDomain(): Domain =
isRemoteControlEnabled = isRemoteControlEnabled,
isPinLockEnabled = isPinLockEnabled,
isAlwaysOnVpnEnabled = isAlwaysOnVpnEnabled,
isLanOnKillSwitchEnabled = isLanOnKillSwitchEnabled,
customSplitPackages = customSplitPackages,
alreadyDonated = alreadyDonated,
)
fun Domain.toEntity(): Entity =
@@ -28,7 +27,7 @@ fun Domain.toEntity(): Entity =
isShortcutsEnabled = isShortcutsEnabled,
isRestoreOnBootEnabled = isRestoreOnBootEnabled,
isMultiTunnelEnabled = isMultiTunnelEnabled,
isTunnelGlobalsEnabled = isTunnelGlobalsEnabled,
isGlobalSplitTunnelEnabled = isGlobalSplitTunnelEnabled,
appMode = appMode,
theme = theme.name,
locale = locale,
@@ -36,6 +35,5 @@ fun Domain.toEntity(): Entity =
isRemoteControlEnabled = isRemoteControlEnabled,
isPinLockEnabled = isPinLockEnabled,
isAlwaysOnVpnEnabled = isAlwaysOnVpnEnabled,
isLanOnKillSwitchEnabled = isLanOnKillSwitchEnabled,
customSplitPackages = customSplitPackages,
alreadyDonated = alreadyDonated,
)
@@ -19,6 +19,7 @@ fun Entity.toDomain(): Domain =
isIpv4Preferred = isIpv4Preferred,
position = position,
autoTunnelApps = autoTunnelApps,
isMetered = isMetered,
)
fun Domain.toEntity(): Entity =
@@ -37,4 +38,5 @@ fun Domain.toEntity(): Entity =
isIpv4Preferred = isIpv4Preferred,
position = position,
autoTunnelApps = autoTunnelApps,
isMetered = isMetered,
)
@@ -316,3 +316,98 @@ val MIGRATION_23_24 =
}
}
}
val MIGRATION_25_26 =
object : Migration(25, 26) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL(
"""
CREATE TABLE IF NOT EXISTS `lockdown_settings` (
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
`bypass_lan` INTEGER NOT NULL DEFAULT 0,
`metered` INTEGER NOT NULL DEFAULT 0,
`dual_stack` INTEGER NOT NULL DEFAULT 0
)
"""
.trimIndent()
)
val cursor =
db.query("SELECT `is_lan_on_kill_switch_enabled` FROM `general_settings` LIMIT 1")
var bypassLan = 0
if (cursor.moveToFirst()) {
bypassLan = if (cursor.getInt(0) != 0) 1 else 0
}
cursor.close()
db.execSQL(
"""
INSERT INTO `lockdown_settings` (`bypass_lan`, `metered`, `dual_stack`)
VALUES (?, 0, 0)
"""
.trimIndent(),
arrayOf(bypassLan),
)
db.execSQL(
"""
CREATE TABLE IF NOT EXISTS `general_settings_new` (
`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,
`custom_split_packages` TEXT NOT NULL DEFAULT '{}'
)
"""
.trimIndent()
)
db.execSQL(
"""
INSERT INTO `general_settings_new` (
`id`,
`is_shortcuts_enabled`,
`is_restore_on_boot_enabled`,
`is_multi_tunnel_enabled`,
`is_tunnel_globals_enabled`,
`app_mode`,
`theme`,
`locale`,
`remote_key`,
`is_remote_control_enabled`,
`is_pin_lock_enabled`,
`is_always_on_vpn_enabled`,
`custom_split_packages`
)
SELECT
`id`,
`is_shortcuts_enabled`,
`is_restore_on_boot_enabled`,
`is_multi_tunnel_enabled`,
`is_tunnel_globals_enabled`,
`app_mode`,
`theme`,
`locale`,
`remote_key`,
`is_remote_control_enabled`,
`is_pin_lock_enabled`,
`is_always_on_vpn_enabled`,
`custom_split_packages`
FROM `general_settings`
"""
.trimIndent()
)
db.execSQL("DROP TABLE `general_settings`")
db.execSQL("ALTER TABLE `general_settings_new` RENAME TO `general_settings`")
}
}
@@ -20,10 +20,7 @@ enum class DnsProtocol(val value: Int) {
}
}
data class DnsSettings(
val protocol: DnsProtocol = DnsProtocol.SYSTEM,
val endpoint: String? = null,
)
data class DnsSettings(val protocol: DnsProtocol = DnsProtocol.SYSTEM, val endpoint: String? = null)
enum class DnsProvider(private val systemAddress: String, private val dohAddress: String) {
CLOUDFLARE("1.1.1.1", "https://1.1.1.1/dns-query"),
@@ -37,6 +37,14 @@ class DataStoreAppStateRepository(
dataStoreManager.saveToDataStore(DataStoreManager.batteryDisableShown, shown)
}
override suspend fun setShouldShowDonationSnackbar(show: Boolean) {
dataStoreManager.saveToDataStore(DataStoreManager.shouldShowDonationSnackbar, show)
}
override suspend fun shouldShowDonationSnackbar(): Boolean {
return dataStoreManager.getFromStore(DataStoreManager.shouldShowDonationSnackbar) ?: false
}
override val flow: Flow<Domain> =
dataStoreManager.preferencesFlow
.map { prefs ->
@@ -38,27 +38,40 @@ class GitHubUpdateRepository(
gitHubApi.getLatestRelease(githubOwner, githubRepo).onFailure(Timber::e)
}
release.map { release ->
val standaloneApkAsset =
val universalApkAsset =
release.assets.find { asset ->
asset.name.startsWith("wgtunnel-${Constants.STANDALONE_FLAVOR}-v") &&
asset.name.endsWith(".apk")
val prefix = "wgtunnel-${Constants.STANDALONE_FLAVOR}-v"
val apkSuffix = ".apk"
asset.name.startsWith(prefix) &&
asset.name.endsWith(apkSuffix) &&
!asset.name.endsWith("-arm64$apkSuffix") &&
!asset.name.endsWith("-armv7$apkSuffix")
}
val newVersion =
standaloneApkAsset
universalApkAsset
?.name
?.removePrefix("wgtunnel-${Constants.STANDALONE_FLAVOR}-v")
?.removeSuffix(".apk") ?: return@map null
Timber.i("Latest version: $newVersion, current version: $currentVersion")
if (isNightly && newVersion != currentVersion)
return@map GitHubReleaseMapper.toAppUpdate(release, newVersion)
if (NumberUtils.compareVersions(newVersion, currentVersion) > 0) {
GitHubReleaseMapper.toAppUpdate(
release.copy(assets = listOf(standaloneApkAsset)),
newVersion,
)
if (isNightly) {
if (newVersion != currentVersion) {
GitHubReleaseMapper.toAppUpdate(
release.copy(assets = listOf(universalApkAsset)),
newVersion,
)
} else {
null
}
} else {
null
if (NumberUtils.compareVersions(newVersion, currentVersion) > 0) {
GitHubReleaseMapper.toAppUpdate(
release.copy(assets = listOf(universalApkAsset)),
newVersion,
)
} else {
null
}
}
}
}
@@ -1,20 +1,15 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.model.InstalledPackage
import com.zaneschepke.wireguardautotunnel.domain.repository.InstalledPackageRepository
import com.zaneschepke.wireguardautotunnel.util.extensions.getAllInternetCapablePackages
import com.zaneschepke.wireguardautotunnel.util.extensions.getFriendlyAppName
import javax.inject.Singleton
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
@@ -27,32 +22,6 @@ class InstalledAndroidPackageRepository(
private var cachedPackages: List<InstalledPackage>? = null
init {
val receiver =
object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
Intent.ACTION_PACKAGE_ADDED,
Intent.ACTION_PACKAGE_REMOVED,
Intent.ACTION_PACKAGE_CHANGED -> {
// don't update if we have nothing cached
if (cachedPackages == null) return
Timber.d("Updating installed packages cache")
applicationScope.launch { refreshInstalledPackages() }
}
}
}
}
val filter =
IntentFilter().apply {
addAction(Intent.ACTION_PACKAGE_ADDED)
addAction(Intent.ACTION_PACKAGE_REMOVED)
addAction(Intent.ACTION_PACKAGE_CHANGED)
addDataScheme("package")
}
context.registerReceiver(receiver, filter)
}
override suspend fun getInstalledPackages(): List<InstalledPackage> =
withContext(ioDispatcher) {
cachedPackages?.let {
@@ -63,7 +32,7 @@ class InstalledAndroidPackageRepository(
override suspend fun refreshInstalledPackages(): List<InstalledPackage> =
withContext(ioDispatcher) {
val packages = context.getAllInternetCapablePackages()
val packages = context.packageManager.getInstalledPackages(0)
val installedPackages =
packages.mapNotNull { packageInfo ->
@@ -0,0 +1,34 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.dao.LockdownSettingsDao
import com.zaneschepke.wireguardautotunnel.data.entity.LockdownSettings 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.LockdownSettings as Domain
import com.zaneschepke.wireguardautotunnel.domain.repository.LockdownSettingsRepository
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
class RoomLockdownSettingsRepository(
private val lockdownSettingsDao: LockdownSettingsDao,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : LockdownSettingsRepository {
override suspend fun upsert(lockdownSettings: Domain) {
withContext(ioDispatcher) { lockdownSettingsDao.upsert(lockdownSettings.toEntity()) }
}
override val flow =
lockdownSettingsDao
.getLockdownSettingsFlow()
.map { (it ?: Entity()).toDomain() }
.flowOn(ioDispatcher)
override suspend fun getLockdownSettings(): Domain {
return withContext(ioDispatcher) {
(lockdownSettingsDao.getLockdownSettings() ?: Entity()).toDomain()
}
}
}
@@ -23,7 +23,6 @@ class RoomProxySettingsRepository(
override val flow =
proxySettingsDao
.getProxySettingsFlow()
.flowOn(ioDispatcher)
.map { (it ?: Entity()).toDomain() }
.flowOn(ioDispatcher)
@@ -1,6 +1,7 @@
package com.zaneschepke.wireguardautotunnel.di
import android.content.Context
import android.os.PowerManager
import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.logcatter.LogcatReader
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
@@ -66,4 +67,9 @@ class AppModule {
): NotificationMonitor {
return NotificationMonitor(tunnelManager, notificationManager)
}
@Provides
fun providePowerManager(@ApplicationContext context: Context): PowerManager {
return context.getSystemService(Context.POWER_SERVICE) as PowerManager
}
}
@@ -8,6 +8,7 @@ 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.migrations.MIGRATION_25_26
import com.zaneschepke.wireguardautotunnel.data.network.GitHubApi
import com.zaneschepke.wireguardautotunnel.data.network.KtorClient
import com.zaneschepke.wireguardautotunnel.data.network.KtorGitHubApi
@@ -56,6 +57,7 @@ class RepositoryModule {
context.getString(R.string.db_name),
)
.addMigrations(MIGRATION_23_24(dataStoreManager.dataStore))
.addMigrations(MIGRATION_25_26)
.fallbackToDestructiveMigration(true)
.addCallback(callback)
.build()
@@ -67,6 +69,12 @@ class RepositoryModule {
return appDatabase.generalSettingsDao()
}
@Singleton
@Provides
fun provideLockdownDoa(appDatabase: AppDatabase): LockdownSettingsDao {
return appDatabase.lockdownSettingsDao()
}
@Singleton
@Provides
fun provideDnsSettingsDao(appDatabase: AppDatabase): DnsSettingsDao {
@@ -106,6 +114,15 @@ class RepositoryModule {
return RoomTunnelRepository(tunnelConfigDao, ioDispatcher)
}
@Singleton
@Provides
fun provideLockdownSettingsRepository(
lockdownSettingsDao: LockdownSettingsDao,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): LockdownSettingsRepository {
return RoomLockdownSettingsRepository(lockdownSettingsDao, ioDispatcher)
}
@Singleton
@Provides
fun provideGeneralSettingsRepository(
@@ -1,6 +1,7 @@
package com.zaneschepke.wireguardautotunnel.di
import android.content.Context
import android.os.PowerManager
import com.wireguard.android.backend.WgQuickBackend
import com.wireguard.android.util.RootShell
import com.wireguard.android.util.ToolsInstaller
@@ -85,8 +86,9 @@ class TunnelModule {
@ApplicationScope applicationScope: CoroutineScope,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
backend: com.wireguard.android.backend.Backend,
runConfigHelper: RunConfigHelper,
): TunnelProvider {
return KernelTunnel(applicationScope, ioDispatcher, backend)
return KernelTunnel(applicationScope, ioDispatcher, runConfigHelper, backend)
}
@Provides
@@ -94,18 +96,11 @@ class TunnelModule {
@Userspace
fun provideUserspaceProvider(
@ApplicationScope applicationScope: CoroutineScope,
proxySettingsRepository: ProxySettingsRepository,
dnsSettingsRepository: DnsSettingsRepository,
runConfigHelper: RunConfigHelper,
@Userspace backend: Backend,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): TunnelProvider {
return UserspaceTunnel(
applicationScope,
ioDispatcher,
proxySettingsRepository,
dnsSettingsRepository,
backend,
)
return UserspaceTunnel(applicationScope, ioDispatcher, backend, runConfigHelper)
}
@Provides
@@ -113,18 +108,11 @@ class TunnelModule {
@ProxyUserspace
fun provideProxyUserspaceProvider(
@ApplicationScope applicationScope: CoroutineScope,
dnsSettingsRepository: DnsSettingsRepository,
proxySettingsRepository: ProxySettingsRepository,
runConfigHelper: RunConfigHelper,
@ProxyUserspace backend: Backend,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): TunnelProvider {
return UserspaceTunnel(
applicationScope,
ioDispatcher,
proxySettingsRepository,
dnsSettingsRepository,
backend,
)
return UserspaceTunnel(applicationScope, ioDispatcher, backend, runConfigHelper)
}
@Provides
@@ -135,6 +123,7 @@ class TunnelModule {
@ProxyUserspace proxyTunnel: TunnelProvider,
serviceManager: ServiceManager,
tunnelRepository: TunnelRepository,
lockdownSettingsRepository: LockdownSettingsRepository,
settingsRepository: GeneralSettingRepository,
autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
tunnelMonitor: TunnelMonitor,
@@ -148,6 +137,7 @@ class TunnelModule {
serviceManager,
settingsRepository,
autoTunnelSettingsRepository,
lockdownSettingsRepository,
tunnelRepository,
tunnelMonitor,
applicationScope,
@@ -155,6 +145,62 @@ class TunnelModule {
)
}
@Provides
@Singleton
fun provideTunnelConfigHelper(
settingsRepository: GeneralSettingRepository,
proxySettingsRepository: ProxySettingsRepository,
dnsSettingsRepository: DnsSettingsRepository,
tunnelRepository: TunnelRepository,
): RunConfigHelper {
return RunConfigHelper(
settingsRepository,
proxySettingsRepository,
dnsSettingsRepository,
tunnelRepository,
)
}
@Singleton
@Provides
fun provideServiceManager(
@ApplicationContext context: Context,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
@MainDispatcher mainCoroutineDispatcher: CoroutineDispatcher,
@ApplicationScope applicationScope: CoroutineScope,
autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
): ServiceManager {
return ServiceManager(
context,
ioDispatcher,
applicationScope,
mainCoroutineDispatcher,
autoTunnelSettingsRepository,
)
}
@Singleton
@Provides
fun provideTunnelMonitor(
powerManager: PowerManager,
networkMonitor: NetworkMonitor,
networkUtils: NetworkUtils,
logReader: LogReader,
tunnelsRepository: TunnelRepository,
settingsRepository: GeneralSettingRepository,
monitoringSettingsRepository: MonitoringSettingsRepository,
): TunnelMonitor {
return TunnelMonitor(
settingsRepository,
tunnelsRepository,
monitoringSettingsRepository,
networkMonitor,
networkUtils,
logReader,
powerManager,
)
}
@Provides
@Singleton
fun provideNetworkMonitor(
@@ -178,42 +224,4 @@ class TunnelModule {
applicationScope,
)
}
@Singleton
@Provides
fun provideServiceManager(
@ApplicationContext context: Context,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
@MainDispatcher mainCoroutineDispatcher: CoroutineDispatcher,
@ApplicationScope applicationScope: CoroutineScope,
autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
): ServiceManager {
return ServiceManager(
context,
ioDispatcher,
applicationScope,
mainCoroutineDispatcher,
autoTunnelSettingsRepository,
)
}
@Singleton
@Provides
fun provideTunnelMonitor(
networkMonitor: NetworkMonitor,
networkUtils: NetworkUtils,
logReader: LogReader,
tunnelsRepository: TunnelRepository,
settingsRepository: GeneralSettingRepository,
monitoringSettingsRepository: MonitoringSettingsRepository,
): TunnelMonitor {
return TunnelMonitor(
settingsRepository,
tunnelsRepository,
monitoringSettingsRepository,
networkMonitor,
networkUtils,
logReader,
)
}
}
@@ -3,5 +3,9 @@ package com.zaneschepke.wireguardautotunnel.domain.enums
sealed class BackendMode {
data object Inactive : BackendMode()
data class KillSwitch(val allowedIps: Set<String>) : BackendMode()
data class KillSwitch(
val allowedIps: Set<String>,
val isMetered: Boolean,
val dualStack: Boolean,
) : BackendMode()
}
@@ -1,11 +1,12 @@
package com.zaneschepke.wireguardautotunnel.domain.events
import androidx.annotation.Keep
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
sealed class AutoTunnelEvent {
data class Start(val tunnelConfig: TunnelConfig? = null) : AutoTunnelEvent()
@Keep data class Start(val tunnelConfig: TunnelConfig? = null) : AutoTunnelEvent()
data object Stop : AutoTunnelEvent()
@Keep data object Stop : AutoTunnelEvent()
data object DoNothing : AutoTunnelEvent()
@Keep data object DoNothing : AutoTunnelEvent()
}
@@ -4,38 +4,39 @@ import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.util.StringValue
sealed class BackendCoreException : Exception() {
data object DNS : BackendCoreException()
data object Unauthorized : BackendCoreException()
data object Config : BackendCoreException()
data object KernelModuleName : BackendCoreException()
data object NotAuthorized : BackendCoreException()
data object ServiceNotRunning : BackendCoreException()
data object Unknown : BackendCoreException()
data object TunnelNameTooLong : BackendCoreException()
data object UapiUpdateFailed : BackendCoreException()
fun toStringRes() =
when (this) {
Config -> R.string.config_error
DNS -> R.string.dns_resolve_error
KernelModuleName -> R.string.kernel_name_error
NotAuthorized,
Unauthorized -> R.string.auth_error
ServiceNotRunning -> R.string.service_running_error
Unknown -> R.string.unknown_error
TunnelNameTooLong -> R.string.error_tunnel_name
UapiUpdateFailed -> R.string.active_tunnel_update_failed
}
abstract val stringRes: Int
fun toStringValue(): StringValue {
return StringValue.StringResource(toStringRes())
return StringValue.StringResource(stringRes)
}
}
class DnsFailure : BackendCoreException() {
override val stringRes = R.string.dns_resolve_error
}
class VpnUnauthorized : BackendCoreException() {
override val stringRes = R.string.auth_error
}
class InvalidConfig : BackendCoreException() {
override val stringRes = R.string.config_error
}
class KernelTunnelName(override val stringRes: Int) : BackendCoreException() {}
class NotAuthorized : BackendCoreException() {
override val stringRes = R.string.auth_error
}
class ServiceNotRunning : BackendCoreException() {
override val stringRes = R.string.service_running_error
}
class UnknownError : BackendCoreException() {
override val stringRes = R.string.unknown_error
}
class UapiUpdateFailed : BackendCoreException() {
override val stringRes = R.string.active_tunnel_update_failed
}
@@ -3,4 +3,5 @@ package com.zaneschepke.wireguardautotunnel.domain.model
data class AppState(
val isLocationDisclosureShown: Boolean = false,
val isBatteryOptimizationDisableShown: Boolean = false,
val shouldShowDonationSnackbar: Boolean = false,
)
@@ -6,4 +6,5 @@ data class DnsSettings(
val id: Int = 0,
val dnsProtocol: DnsProtocol = DnsProtocol.fromValue(0),
val dnsEndpoint: String? = null,
val isGlobalTunnelDnsEnabled: Boolean = false,
)
@@ -8,7 +8,7 @@ data class GeneralSettings(
val isShortcutsEnabled: Boolean = false,
val isRestoreOnBootEnabled: Boolean = false,
val isMultiTunnelEnabled: Boolean = false,
val isTunnelGlobalsEnabled: Boolean = false,
val isGlobalSplitTunnelEnabled: Boolean = false,
val appMode: AppMode = AppMode.fromValue(0),
val theme: Theme = Theme.AUTOMATIC,
val locale: String? = null,
@@ -16,6 +16,6 @@ data class GeneralSettings(
val isRemoteControlEnabled: Boolean = false,
val isPinLockEnabled: Boolean = false,
val isAlwaysOnVpnEnabled: Boolean = false,
val isLanOnKillSwitchEnabled: Boolean = false,
val customSplitPackages: Map<String, String> = emptyMap(),
val isKillSwitchMetered: Boolean = true,
val alreadyDonated: Boolean = false,
)
@@ -0,0 +1,8 @@
package com.zaneschepke.wireguardautotunnel.domain.model
data class LockdownSettings(
val id: Long = 0L,
val bypassLan: Boolean = false,
val metered: Boolean = false,
val dualStack: Boolean = false,
)
@@ -27,8 +27,8 @@ data class TunnelConfig(
val isIpv4Preferred: Boolean = true,
val position: Int = 0,
val autoTunnelApps: Set<String> = setOf(),
val isMetered: Boolean = true,
) {
val isNameKernelCompatible: Boolean = (name.length <= 15)
override fun equals(other: Any?): Boolean {
if (this === other) return true
@@ -43,7 +43,8 @@ data class TunnelConfig(
pingTarget == other.pingTarget &&
restartOnPingFailure == other.restartOnPingFailure &&
tunnelNetworks == other.tunnelNetworks &&
isIpv4Preferred == other.isIpv4Preferred
isIpv4Preferred == other.isIpv4Preferred &&
isMetered == other.isMetered
}
override fun hashCode(): Int {
@@ -66,7 +67,11 @@ data class TunnelConfig(
return configFromWgQuick(wgQuick)
}
fun copyWithGlobalValues(globalTunnel: TunnelConfig): TunnelConfig {
fun copyWithGlobalValues(
globalTunnel: TunnelConfig,
includeDns: Boolean,
includeSpitTunneling: Boolean,
): TunnelConfig {
val existingConfig = toAmConfig()
val globalConfig = globalTunnel.toAmConfig()
@@ -115,62 +120,14 @@ data class TunnelConfig(
setPreDown(existingConfig.`interface`.preDown)
setPostDown(existingConfig.`interface`.postDown)
globalConfig.`interface`.mtu.ifPresent { setMtu(it) }
if (globalConfig.`interface`.dnsServers.isNotEmpty()) {
if (includeDns) {
setDnsServers(globalConfig.`interface`.dnsServers)
}
if (globalConfig.`interface`.dnsSearchDomains.isNotEmpty()) {
setDnsSearchDomains(globalConfig.`interface`.dnsSearchDomains)
}
if (globalConfig.`interface`.excludedApplications.isNotEmpty()) {
if (includeSpitTunneling) {
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()
@@ -12,5 +12,9 @@ interface AppStateRepository {
suspend fun setBatteryOptimizationDisableShown(shown: Boolean)
suspend fun setShouldShowDonationSnackbar(show: Boolean)
suspend fun shouldShowDonationSnackbar(): Boolean
val flow: Flow<AppState>
}
@@ -0,0 +1,12 @@
package com.zaneschepke.wireguardautotunnel.domain.repository
import com.zaneschepke.wireguardautotunnel.domain.model.LockdownSettings
import kotlinx.coroutines.flow.Flow
interface LockdownSettingsRepository {
suspend fun upsert(lockdownSettings: LockdownSettings)
val flow: Flow<LockdownSettings>
suspend fun getLockdownSettings(): LockdownSettings
}
@@ -2,12 +2,19 @@ package com.zaneschepke.wireguardautotunnel.domain.sideeffect
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarType
import com.zaneschepke.wireguardautotunnel.util.StringValue
import java.io.File
sealed class GlobalSideEffect {
data class Snackbar(val message: StringValue) : GlobalSideEffect()
data class Snackbar(
val message: StringValue,
val type: SnackbarType? = null,
val actionLabel: String? = null,
val onAction: (() -> Unit)? = null,
val durationMs: Long? = null,
) : GlobalSideEffect()
data class Toast(val message: StringValue) : GlobalSideEffect()
@@ -0,0 +1,46 @@
package com.zaneschepke.wireguardautotunnel.ui.common.button
import android.R.attr.onClick
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ExpandMore
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.VerticalDivider
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
@Composable
fun SheetButtonWithDivider(
showDivider: Boolean = true,
modifier: Modifier = Modifier,
onClick: () -> Unit,
) {
Row(
modifier = Modifier.height(IntrinsicSize.Min),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
if (showDivider) {
VerticalDivider(
modifier = Modifier.fillMaxHeight().padding(horizontal = 8.dp, vertical = 8.dp),
color = MaterialTheme.colorScheme.outline,
)
}
Box(modifier = Modifier.pointerInput(Unit) { detectTapGestures {} }) {
IconButton(onClick = onClick, modifier) {
Icon(
Icons.Outlined.ExpandMore,
contentDescription = stringResource(R.string.select),
)
}
}
}
}
@@ -1,5 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.common.button
import android.view.KeyEvent
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
@@ -16,10 +17,15 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
import com.zaneschepke.wireguardautotunnel.ui.theme.Disabled
@Composable
fun SurfaceRow(
@@ -28,13 +34,14 @@ fun SurfaceRow(
onClick: (() -> Unit)? = null,
description: @Composable (() -> Unit)? = null,
expandedContent: @Composable (() -> Unit)? = null,
onLongClick: () -> Unit = {},
onLongClick: (() -> Unit)? = null,
enabled: Boolean = true,
selected: Boolean = false,
leading: @Composable (() -> Unit)? = null,
trailing: @Composable ((Modifier) -> Unit)? = null,
) {
val density = LocalDensity.current
val isTv = LocalIsAndroidTV.current
var leadingPadding by remember { mutableStateOf(0.dp) }
val interactionSource = remember { MutableInteractionSource() }
val mainFocusRequester = remember { FocusRequester() }
@@ -44,7 +51,6 @@ fun SurfaceRow(
modifier =
modifier
.fillMaxWidth()
// .focusGroup()
.indication(interactionSource, ripple())
.background(
if (!selected) MaterialTheme.colorScheme.surface
@@ -62,7 +68,20 @@ fun SurfaceRow(
) {
Row(
modifier =
Modifier.focusRequester(mainFocusRequester)
Modifier.onKeyEvent { event ->
if (onLongClick == null || isTv) {
if (
event.key == Key.DirectionCenter &&
event.nativeKeyEvent.action == KeyEvent.ACTION_DOWN
) {
// Consume the down event to prevent the default long press
// behavior
return@onKeyEvent true
}
}
false
}
.focusRequester(mainFocusRequester)
.focusProperties {
if (onClick != null) {
right = trailingFocusRequester
@@ -109,9 +128,7 @@ fun SurfaceRow(
Text(
text = title,
style = MaterialTheme.typography.bodyLarge,
color =
if (enabled) MaterialTheme.colorScheme.onSurface
else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f),
color = if (enabled) MaterialTheme.colorScheme.onSurface else Disabled,
)
if (description != null) {
description()
@@ -27,7 +27,7 @@ fun SwitchWithDivider(
color = MaterialTheme.colorScheme.outline,
)
Box(modifier = Modifier.pointerInput(Unit) { detectTapGestures {} }) {
ScaledSwitch(
ThemedSwitch(
checked = checked,
onClick = onClick,
enabled = enabled,
@@ -5,9 +5,10 @@ import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.zaneschepke.wireguardautotunnel.ui.theme.Disabled
@Composable
fun ScaledSwitch(
fun ThemedSwitch(
checked: Boolean,
onClick: (checked: Boolean) -> Unit,
enabled: Boolean = true,
@@ -21,12 +22,15 @@ fun ScaledSwitch(
colors =
SwitchDefaults.colors()
.copy(
checkedThumbColor = MaterialTheme.colorScheme.background,
checkedIconColor = MaterialTheme.colorScheme.background,
checkedThumbColor = MaterialTheme.colorScheme.surface,
checkedIconColor = MaterialTheme.colorScheme.surface,
uncheckedTrackColor = MaterialTheme.colorScheme.surface,
uncheckedBorderColor = MaterialTheme.colorScheme.outline,
uncheckedThumbColor = MaterialTheme.colorScheme.outline,
uncheckedIconColor = MaterialTheme.colorScheme.outline,
disabledUncheckedBorderColor = Disabled,
disabledUncheckedThumbColor = Disabled,
disabledUncheckedIconColor = Disabled,
),
)
}
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.common.dialog
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
@@ -11,22 +12,26 @@ import com.zaneschepke.wireguardautotunnel.R
fun InfoDialog(
onAttest: () -> Unit,
onDismiss: () -> Unit,
title: @Composable () -> Unit,
body: @Composable () -> Unit,
confirmText: @Composable () -> Unit,
title: String,
body: @Composable (() -> Unit),
confirmText: String,
modifier: Modifier = Modifier,
) {
MaterialTheme(colorScheme = MaterialTheme.colorScheme.copy()) {
Surface(color = MaterialTheme.colorScheme.surface, tonalElevation = 0.dp) {
AlertDialog(
modifier = modifier,
onDismissRequest = { onDismiss() },
confirmButton = { TextButton(onClick = { onAttest() }) { confirmText() } },
confirmButton = {
TextButton(onClick = { onAttest() }) { Text(text = confirmText) }
},
dismissButton = {
TextButton(onClick = { onDismiss() }) {
Text(text = stringResource(R.string.cancel))
}
},
containerColor = MaterialTheme.colorScheme.surface,
title = { title() },
title = { Text(text = title) },
text = { body() },
properties = DialogProperties(usePlatformDefaultWidth = true),
)
@@ -34,7 +34,7 @@ fun VpnDeniedDialog(show: Boolean, onDismiss: () -> Unit) {
InfoDialog(
onDismiss = { onDismiss() },
onAttest = { onDismiss() },
title = { Text(text = stringResource(R.string.vpn_denied_dialog_title)) },
title = stringResource(R.string.vpn_denied_dialog_title),
body = {
Text(
text = alwaysOnDescription,
@@ -44,7 +44,7 @@ fun VpnDeniedDialog(show: Boolean, onDismiss: () -> Unit) {
),
)
},
confirmText = { Text(text = stringResource(R.string.okay)) },
confirmText = stringResource(R.string.okay),
)
}
}
@@ -14,6 +14,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
@Composable
fun SheetOption(
@@ -50,9 +51,12 @@ fun SheetOption(
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CustomBottomSheet(options: List<SheetOption>, onDismiss: () -> Unit) {
val isTv = LocalIsAndroidTV.current
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = isTv)
ModalBottomSheet(
containerColor = MaterialTheme.colorScheme.surface,
onDismissRequest = onDismiss,
sheetState = sheetState,
) {
options.forEachIndexed { index, option ->
SheetOption(option.label, option.leadingIcon, option.onClick, option.selected)
@@ -1,56 +1,93 @@
package com.zaneschepke.wireguardautotunnel.ui.common.snackbar
import android.R.attr.padding
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Favorite
import androidx.compose.material.icons.rounded.Close
import androidx.compose.material.icons.rounded.Info
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Snackbar
import androidx.compose.material3.Text
import androidx.compose.material.icons.rounded.Warning
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
@Composable
fun CustomSnackBar(
message: String,
isRtl: Boolean = true,
message: AnnotatedString,
onDismiss: () -> Unit,
modifier: Modifier = Modifier,
type: SnackbarType = SnackbarType.INFO,
containerColor: Color = MaterialTheme.colorScheme.surface,
) {
val isTv = LocalIsAndroidTV.current
val icon =
when (type) {
SnackbarType.INFO -> Icons.Rounded.Info
SnackbarType.WARNING -> Icons.Rounded.Warning
SnackbarType.THANK_YOU -> Icons.Outlined.Favorite
}
val iconDescription =
when (type) {
SnackbarType.INFO -> stringResource(R.string.info)
SnackbarType.WARNING -> stringResource(R.string.warning)
SnackbarType.THANK_YOU -> stringResource(R.string.thank_you)
}
Snackbar(
containerColor = containerColor,
modifier = Modifier.fillMaxWidth(if (isTv) 1 / 3f else 2 / 3f).padding(bottom = 100.dp),
modifier =
modifier
.wrapContentHeight(align = Alignment.Top)
.padding(horizontal = if (isTv) 48.dp else 16.dp),
shape = RoundedCornerShape(16.dp),
) {
CompositionLocalProvider(
LocalLayoutDirection provides if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr
Row(
modifier =
Modifier.fillMaxWidth()
.height(IntrinsicSize.Min)
.width(IntrinsicSize.Min)
.padding(vertical = 16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Row(
modifier = Modifier.width(IntrinsicSize.Max).height(IntrinsicSize.Min),
modifier = Modifier.fillMaxWidth().weight(1f),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start,
) {
val icon = Icons.Rounded.Info
Icon(
icon,
contentDescription = icon.name,
contentDescription = iconDescription,
tint = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(end = 10.dp),
)
Spacer(modifier = Modifier.width(16.dp))
Text(
message,
text = message,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(end = 5.dp),
maxLines = 8,
overflow = TextOverflow.Ellipsis,
)
}
Row {
IconButton(onClick = onDismiss, modifier = Modifier.size(24.dp)) {
Icon(
Icons.Rounded.Close,
contentDescription = stringResource(R.string.stop),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
}
}
}
@@ -0,0 +1,70 @@
package com.zaneschepke.wireguardautotunnel.ui.common.snackbar
import androidx.compose.foundation.layout.Box
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@Composable
fun rememberCustomSnackbarState(): CustomSnackbarState {
return remember { CustomSnackbarState() }
}
class CustomSnackbarState {
private val _snackbars = Channel<SnackbarInfo>(Channel.BUFFERED)
val snackbars: Channel<SnackbarInfo> = _snackbars
private var currentSnackbar by mutableStateOf<SnackbarInfo?>(null)
private var isShowing by mutableStateOf(false)
fun showSnackbar(info: SnackbarInfo) {
_snackbars.trySend(info)
}
fun dismissCurrent() {
currentSnackbar = null
isShowing = false
}
@Composable
fun SnackbarHost(
modifier: Modifier = Modifier,
snackbar: @Composable (SnackbarInfo) -> Unit = { info ->
CustomSnackBar(
message = info.message,
type = info.type,
onDismiss = { dismissCurrent() },
modifier = Modifier,
containerColor = MaterialTheme.colorScheme.surface.copy(.1f),
)
},
) {
val scope = rememberCoroutineScope()
LaunchedEffect(Unit) {
for (info in snackbars) {
currentSnackbar = info
isShowing = true
scope.launch {
delay(info.durationMs)
if (currentSnackbar?.id == info.id) {
dismissCurrent()
}
}
while (isShowing && currentSnackbar?.id == info.id) {
delay(100)
}
}
}
currentSnackbar?.let { info ->
if (isShowing) {
Box(modifier = modifier) { snackbar(info) }
}
}
}
}
@@ -0,0 +1,16 @@
package com.zaneschepke.wireguardautotunnel.ui.common.snackbar
import androidx.compose.ui.text.AnnotatedString
enum class SnackbarType {
INFO,
WARNING,
THANK_YOU,
}
data class SnackbarInfo(
val message: AnnotatedString,
val type: SnackbarType = SnackbarType.INFO,
val durationMs: Long = 4000L,
val id: String = System.currentTimeMillis().toString(),
)
@@ -5,21 +5,32 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.AnnotatedString
import com.zaneschepke.wireguardautotunnel.ui.theme.Disabled
@Composable
fun DescriptionText(text: String, modifier: Modifier = Modifier) {
fun DescriptionText(text: String, modifier: Modifier = Modifier, disabled: Boolean = false) {
Text(
text = text,
style = MaterialTheme.typography.bodySmall.copy(color = MaterialTheme.colorScheme.outline),
style =
MaterialTheme.typography.bodySmall.copy(
color = if (disabled) Disabled else MaterialTheme.colorScheme.outline
),
modifier = modifier,
)
}
@Composable
fun DescriptionText(text: AnnotatedString, modifier: Modifier = Modifier) {
fun DescriptionText(
text: AnnotatedString,
modifier: Modifier = Modifier,
disabled: Boolean = false,
) {
Text(
text = text,
style = MaterialTheme.typography.bodySmall.copy(color = MaterialTheme.colorScheme.outline),
style =
MaterialTheme.typography.bodySmall.copy(
color = if (disabled) Disabled else MaterialTheme.colorScheme.outline
),
modifier = modifier,
)
}
@@ -2,7 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.common.textbox
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.MaterialTheme
@@ -39,7 +39,7 @@ fun ConfigurationTextBox(
isError = isError,
textStyle =
MaterialTheme.typography.bodySmall.copy(color = MaterialTheme.colorScheme.onSurface),
modifier = modifier.fillMaxWidth().height(48.dp),
modifier = modifier.fillMaxWidth().heightIn(48.dp),
value = value,
visualTransformation = visualTransformation,
singleLine = singleLine,
@@ -52,10 +52,18 @@ fun CustomTextField(
val editable = enabled && !readOnly
val mainFocusRequester = remember { FocusRequester() }
val trailingFocusRequester = remember { FocusRequester() }
val disabledAlpha = 0.38f
val disabledBorderAlpha = 0.12f
val effectiveTextStyle =
if (enabled) {
textStyle
} else {
textStyle.copy(color = textStyle.color.copy(alpha = disabledAlpha))
}
BasicTextField(
value = value,
textStyle = textStyle,
textStyle = effectiveTextStyle,
onValueChange = { onValueChange(it) },
keyboardActions = keyboardActions,
keyboardOptions = keyboardOptions,
@@ -105,7 +113,18 @@ fun CustomTextField(
colors =
TextFieldDefaults.colors()
.copy(
disabledLabelColor = MaterialTheme.colorScheme.onSurface,
disabledTextColor =
MaterialTheme.colorScheme.onSurface.copy(alpha = disabledAlpha),
disabledLabelColor =
MaterialTheme.colorScheme.onSurface.copy(alpha = disabledAlpha),
disabledPlaceholderColor =
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = disabledAlpha),
disabledLeadingIconColor =
MaterialTheme.colorScheme.onSurface.copy(alpha = disabledAlpha),
disabledTrailingIconColor =
MaterialTheme.colorScheme.onSurface.copy(alpha = disabledAlpha),
disabledSupportingTextColor =
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = disabledAlpha),
disabledContainerColor = containerColor,
focusedLabelColor = MaterialTheme.colorScheme.onSurface,
focusedContainerColor = containerColor,
@@ -127,8 +146,13 @@ fun CustomTextField(
TextFieldDefaults.colors()
.copy(
errorContainerColor = containerColor,
disabledLabelColor = MaterialTheme.colorScheme.onSurface,
disabledLabelColor =
MaterialTheme.colorScheme.onSurface.copy(alpha = disabledAlpha),
disabledContainerColor = containerColor,
disabledIndicatorColor =
MaterialTheme.colorScheme.onSurface.copy(
alpha = disabledBorderAlpha
),
focusedIndicatorColor = MaterialTheme.colorScheme.primary,
unfocusedIndicatorColor = MaterialTheme.colorScheme.outline,
focusedLabelColor = MaterialTheme.colorScheme.onSurface,
@@ -34,7 +34,7 @@ sealed class Route : NavKey {
@Keep @Serializable data object Tunnels : Route()
@Keep @Serializable data class TunnelOptions(val id: Int) : Route()
@Keep @Serializable data class TunnelSettings(val id: Int) : Route()
@Keep @Serializable data class Config(val id: Int?) : Route()
@@ -42,8 +42,6 @@ sealed class Route : NavKey {
@Keep @Serializable data class ConfigGlobal(val id: Int?) : Route()
@Keep @Serializable data class TunnelGlobals(val id: Int) : Route()
@Keep @Serializable data class SplitTunnelGlobal(val id: Int) : Route()
@Keep @Serializable data object Sort : Route()
@@ -58,6 +56,8 @@ sealed class Route : NavKey {
@Keep @Serializable data object ProxySettings : Route()
@Keep @Serializable data object LockdownSettings : Route()
@Keep @Serializable data object AutoTunnel : Route()
@Keep @Serializable data object AdvancedAutoTunnel : Route()
@@ -107,7 +107,7 @@ enum class Tab(
when (route) {
is Route.Tunnels,
Route.Sort,
is Route.TunnelOptions,
is Route.TunnelSettings,
is Route.Config,
is Route.Lock,
is Route.SplitTunnel -> TUNNELS
@@ -121,14 +121,14 @@ enum class Tab(
Route.TunnelMonitoring,
Route.AndroidIntegrations,
Route.Dns,
is Route.TunnelGlobals,
is Route.ConfigGlobal,
is Route.SplitTunnelGlobal,
Route.ProxySettings,
Route.LockdownSettings,
Route.Appearance,
Route.Language,
Route.Display,
Route.PingTarget,
is Route.ConfigGlobal,
Route.Logs -> SETTINGS
is Route.Support,
Route.License,
@@ -113,6 +113,29 @@ fun currentRouteAsNavbarState(
showBottomItems = true,
topTitle = context.getString(R.string.language),
)
LockdownSettings ->
NavbarState(
topLeading = {
IconButton(onClick = { navController.pop() }) {
Icon(
Icons.AutoMirrored.Rounded.ArrowBack,
stringResource(R.string.back),
)
}
},
showBottomItems = true,
topTitle = context.getString(R.string.lockdown_settings),
topTrailing = {
IconButton(
onClick = {
keyboardController?.hide()
sharedViewModel.postSideEffect(LocalSideEffect.SaveChanges)
}
) {
Icon(Icons.Rounded.Save, stringResource(R.string.save))
}
},
)
License ->
NavbarState(
topLeading = {
@@ -211,8 +234,11 @@ fun currentRouteAsNavbarState(
}
},
)
is Config -> {
val tunnelName = sharedState.tunnels.find { it.id == route.id }?.name
is Config,
is ConfigGlobal -> {
val tunnelName =
if (route is Config) sharedState.tunnels.find { it.id == route.id }?.name
else context.getString(R.string.global_dns_servers)
NavbarState(
topLeading = {
IconButton(onClick = { navController.pop() }) {
@@ -236,8 +262,12 @@ fun currentRouteAsNavbarState(
},
)
}
is SplitTunnel -> {
val tunnelName = sharedState.tunnels.find { it.id == route.id }?.name
is SplitTunnel,
is SplitTunnelGlobal -> {
val tunnelName =
if (route is SplitTunnel)
sharedState.tunnels.find { it.id == route.id }?.name
else context.getString(R.string.global_split_tunneling)
NavbarState(
topLeading = {
IconButton(onClick = { navController.pop() }) {
@@ -260,52 +290,6 @@ fun currentRouteAsNavbarState(
showBottomItems = true,
)
}
is SplitTunnelGlobal -> {
NavbarState(
topLeading = {
IconButton(onClick = { navController.pop() }) {
Icon(
Icons.AutoMirrored.Rounded.ArrowBack,
stringResource(R.string.back),
)
}
},
topTitle = context.getString(R.string.splt_tunneling),
topTrailing = {
IconButton(
onClick = {
sharedViewModel.postSideEffect(LocalSideEffect.SaveChanges)
}
) {
Icon(Icons.Rounded.Save, stringResource(R.string.save))
}
},
showBottomItems = true,
)
}
is ConfigGlobal -> {
NavbarState(
topLeading = {
IconButton(onClick = { navController.pop() }) {
Icon(
Icons.AutoMirrored.Rounded.ArrowBack,
stringResource(R.string.back),
)
}
},
showBottomItems = true,
topTitle = context.getString(R.string.configuration),
topTrailing = {
IconButton(
onClick = {
sharedViewModel.postSideEffect(LocalSideEffect.SaveChanges)
}
) {
Icon(Icons.Rounded.Save, stringResource(R.string.save))
}
},
)
}
Support ->
NavbarState(
topTitle = context.getString(R.string.support),
@@ -337,7 +321,7 @@ fun currentRouteAsNavbarState(
topTitle = context.getString(R.string.ping_monitor),
showBottomItems = true,
)
is TunnelOptions -> {
is TunnelSettings -> {
val tunnelName = sharedState.tunnels.find { it.id == route.id }?.name
NavbarState(
topLeading = {
@@ -497,20 +481,6 @@ fun currentRouteAsNavbarState(
showBottomItems = true,
)
}
is TunnelGlobals -> {
NavbarState(
topLeading = {
IconButton(onClick = { navController.pop() }) {
Icon(
Icons.AutoMirrored.Rounded.ArrowBack,
stringResource(R.string.back),
)
}
},
topTitle = context.getString(R.string.global_overrides),
showBottomItems = true,
)
}
is WifiPreferences -> {
NavbarState(
topLeading = {
@@ -40,9 +40,9 @@ import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.domain.enums.NetworkType
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.common.button.SwitchWithDivider
import com.zaneschepke.wireguardautotunnel.ui.common.button.ThemedSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberClipboardHelper
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.common.text.DescriptionText
@@ -294,7 +294,7 @@ fun AutoTunnelScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
title = stringResource(R.string.stop_on_no_internet),
description = { DescriptionText(stringResource(R.string.stop_on_internet_loss)) },
trailing = {
ScaledSwitch(
ThemedSwitch(
checked = autoTunnelState.autoTunnelSettings.isStopOnNoInternetEnabled,
onClick = { viewModel.setStopOnNoInternetEnabled(it) },
)
@@ -315,7 +315,7 @@ fun AutoTunnelScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
leading = { Icon(Icons.Outlined.RestartAlt, contentDescription = null) },
title = stringResource(R.string.restart_at_boot),
trailing = {
ScaledSwitch(
ThemedSwitch(
checked = autoTunnelState.autoTunnelSettings.startOnBoot,
onClick = { viewModel.setStartAtBoot(it) },
)
@@ -26,8 +26,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.banner.WarningBanner
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.common.button.ThemedSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.common.text.DescriptionText
@@ -88,9 +88,9 @@ fun WifiSettingsScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
showLocationDialog = false
},
onDismiss = { showLocationDialog = false },
title = { Text(stringResource(R.string.location_permissions)) },
title = stringResource(R.string.location_permissions),
body = { Text(stringResource(R.string.location_justification)) },
confirmText = { Text(stringResource(R.string.open_settings)) },
confirmText = stringResource(R.string.open_settings),
)
}
@@ -156,7 +156,7 @@ fun WifiSettingsScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
)
},
trailing = {
ScaledSwitch(
ThemedSwitch(
checked = autoTunnelState.autoTunnelSettings.isWildcardsEnabled,
onClick = { viewModel.setWildcardsEnabled(it) },
)
@@ -7,16 +7,16 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.CallSplit
import androidx.compose.material.icons.automirrored.outlined.ViewQuilt
import androidx.compose.material.icons.outlined.*
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
@@ -29,14 +29,16 @@ import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.SheetButtonWithDivider
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.common.button.SwitchWithDivider
import com.zaneschepke.wireguardautotunnel.ui.common.button.ThemedSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.common.text.DescriptionText
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.BackupBottomSheet
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.proxy.compoents.AppModeBottomSheet
import com.zaneschepke.wireguardautotunnel.ui.theme.Disabled
import com.zaneschepke.wireguardautotunnel.util.StringValue
import com.zaneschepke.wireguardautotunnel.util.extensions.asString
import com.zaneschepke.wireguardautotunnel.util.extensions.asTitleString
@@ -53,29 +55,24 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
val locale = remember { Locale.getDefault() }
val sharedState by sharedViewModel.container.stateFlow.collectAsStateWithLifecycle()
val settingsState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
val sharedUiState by sharedViewModel.container.stateFlow.collectAsStateWithLifecycle()
val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
if (settingsState.isLoading) return
if (uiState.isLoading) return
var showBackupSheet by rememberSaveable { mutableStateOf(false) }
var showAppModeSheet by rememberSaveable { mutableStateOf(false) }
val appMode = settingsState.settings.appMode
val appMode = uiState.settings.appMode
val dnsEnabled by rememberSaveable(appMode) { mutableStateOf(appMode != AppMode.KERNEL) }
val showProxySettings by
val showModeDivider by
remember(appMode) {
derivedStateOf {
when (appMode) {
AppMode.PROXY -> true
else -> false
}
}
derivedStateOf { appMode == AppMode.PROXY || appMode == AppMode.LOCK_DOWN }
}
fun performBackupRestore(action: () -> Unit) {
if (sharedState.activeTunnels.isNotEmpty() || sharedState.isAutoTunnelActive)
if (sharedUiState.activeTunnels.isNotEmpty() || sharedUiState.isAutoTunnelActive)
return context.showToast(R.string.all_services_disabled)
showBackupSheet = false
action()
@@ -89,22 +86,10 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
showBackupSheet = false
}
if (showAppModeSheet)
AppModeBottomSheet(sharedViewModel::setAppMode, settingsState.settings.appMode) {
AppModeBottomSheet(sharedViewModel::setAppMode, uiState.settings.appMode) {
showAppModeSheet = false
}
val isPingMonitoringAvailable by
remember(settingsState.settings.appMode) {
derivedStateOf {
settingsState.settings.appMode != AppMode.PROXY &&
settingsState.settings.appMode != AppMode.LOCK_DOWN
}
}
LaunchedEffect(isPingMonitoringAvailable) {
if (!isPingMonitoringAvailable) viewModel.setPingEnabled(false)
}
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Top),
@@ -119,11 +104,8 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
leading = {
Icon(ImageVector.vectorResource(R.drawable.sdk), contentDescription = null)
},
trailing = {
Icon(
Icons.Outlined.ExpandMore,
contentDescription = stringResource(R.string.select),
)
trailing = { modifier ->
SheetButtonWithDivider(showModeDivider, modifier) { showAppModeSheet = true }
},
title = stringResource(R.string.backend_mode),
description = {
@@ -131,40 +113,23 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
stringResource(R.string.current_template, appMode.asTitleString(context))
)
},
onClick = { showAppModeSheet = true },
onClick = {
when (appMode) {
AppMode.PROXY -> navController.push(Route.ProxySettings)
AppMode.LOCK_DOWN -> navController.push(Route.LockdownSettings)
AppMode.KERNEL,
AppMode.VPN -> showAppModeSheet = true
}
},
)
if (appMode == AppMode.LOCK_DOWN) {
SurfaceRow(
leading = { Icon(Icons.Outlined.Lan, contentDescription = null) },
title = stringResource(R.string.allow_lan_traffic),
description = {
Text(
text = stringResource(R.string.bypass_lan_for_kill_switch),
style =
MaterialTheme.typography.bodySmall.copy(
MaterialTheme.colorScheme.outline
),
)
},
trailing = {
ScaledSwitch(
checked = settingsState.settings.isLanOnKillSwitchEnabled,
onClick = { viewModel.setLanKillSwitchEnabled(it) },
)
},
onClick = {
viewModel.setLanKillSwitchEnabled(
!settingsState.settings.isLanOnKillSwitchEnabled
)
},
)
}
SurfaceRow(
leading = {
Icon(
Icons.Outlined.Dns,
null,
tint = if (dnsEnabled) MaterialTheme.colorScheme.onSurface else Color.Gray,
tint =
if (dnsEnabled) MaterialTheme.colorScheme.onSurface
else MaterialTheme.colorScheme.outline,
)
},
title = stringResource(R.string.dns_settings),
@@ -182,29 +147,39 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
)
SurfaceRow(
leading = {
Icon(ImageVector.vectorResource(R.drawable.globe), contentDescription = null)
},
title = stringResource(R.string.global_overrides),
trailing = { modifier ->
SwitchWithDivider(
checked = settingsState.settings.isTunnelGlobalsEnabled,
onClick = { viewModel.setTunnelGlobals(it) },
modifier = modifier,
Icon(
Icons.AutoMirrored.Outlined.CallSplit,
contentDescription = null,
tint =
if (sharedUiState.proxyEnabled) Disabled
else MaterialTheme.colorScheme.onSurface,
)
},
enabled = !sharedUiState.proxyEnabled,
title = stringResource(R.string.global_split_tunneling),
trailing = { modifier ->
SwitchWithDivider(
checked = uiState.settings.isGlobalSplitTunnelEnabled,
onClick = { viewModel.setGlobalSplitTunneling(it) },
modifier = modifier,
enabled = !sharedUiState.proxyEnabled,
)
},
description =
if (sharedUiState.proxyEnabled) {
{
DescriptionText(
stringResource(R.string.unavailable_in_mode),
disabled = true,
)
}
} else null,
onClick = {
settingsState.globalTunnelConfig?.let {
navController.push(Route.TunnelGlobals(it.id))
uiState.globalTunnelConfig?.let {
navController.push(Route.SplitTunnelGlobal(id = it.id))
}
},
)
if (showProxySettings) {
SurfaceRow(
leading = { Icon(ImageVector.vectorResource(R.drawable.proxy), null) },
title = stringResource(R.string.proxy_settings),
onClick = { navController.push(Route.ProxySettings) },
)
}
SurfaceRow(
leading = { Icon(Icons.Outlined.Android, null) },
title = stringResource(R.string.android_integrations),
@@ -222,17 +197,27 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
Icons.Outlined.NetworkPing,
contentDescription = null,
tint =
if (isPingMonitoringAvailable) MaterialTheme.colorScheme.onSurface
else Color.Gray,
if (!sharedUiState.proxyEnabled) MaterialTheme.colorScheme.onSurface
else Disabled,
)
},
title = stringResource(R.string.ping_monitor),
enabled = isPingMonitoringAvailable,
trailing = {
enabled = !sharedUiState.proxyEnabled,
description =
if (sharedUiState.proxyEnabled) {
{
DescriptionText(
stringResource(R.string.unavailable_in_mode),
disabled = true,
)
}
} else null,
trailing = { modifier ->
SwitchWithDivider(
checked = settingsState.monitoring.isPingEnabled,
checked = uiState.monitoring.isPingEnabled,
onClick = { viewModel.setPingEnabled(it) },
enabled = isPingMonitoringAvailable,
enabled = !sharedUiState.proxyEnabled,
modifier = modifier,
)
},
onClick = { navController.push(Route.TunnelMonitoring) },
@@ -242,7 +227,7 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
title = stringResource(R.string.local_logging),
trailing = { modifier ->
SwitchWithDivider(
checked = settingsState.monitoring.isLocalLogsEnabled,
checked = uiState.monitoring.isLocalLogsEnabled,
onClick = { viewModel.setLocalLogging(it) },
modifier = modifier,
)
@@ -266,8 +251,8 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
leading = { Icon(Icons.Outlined.Pin, contentDescription = null) },
title = stringResource(R.string.enable_app_lock),
trailing = {
ScaledSwitch(
checked = settingsState.isPinLockEnabled,
ThemedSwitch(
checked = uiState.isPinLockEnabled,
onClick = {
if (it) {
navController.push(Route.Lock)
@@ -278,7 +263,7 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
)
},
onClick = {
if (!settingsState.isPinLockEnabled) {
if (!uiState.isPinLockEnabled) {
navController.push(Route.Lock)
} else {
sharedViewModel.setPinLockEnabled(false)
@@ -289,11 +274,13 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
leading = { Icon(Icons.Outlined.SettingsBackupRestore, contentDescription = null) },
title = stringResource(R.string.backup_and_restore),
onClick = { showBackupSheet = true },
trailing = {
Icon(
Icons.Outlined.ExpandMore,
contentDescription = stringResource(R.string.select),
)
trailing = { modifier ->
IconButton(modifier = modifier, onClick = { showBackupSheet = true }) {
Icon(
Icons.Outlined.ExpandMore,
contentDescription = stringResource(R.string.select),
)
}
},
)
}
@@ -4,6 +4,7 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
@@ -12,32 +13,45 @@ import androidx.compose.material.icons.outlined.Dns
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.model.DnsProtocol
import com.zaneschepke.wireguardautotunnel.data.model.DnsProvider
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.common.button.SwitchWithDivider
import com.zaneschepke.wireguardautotunnel.ui.common.dropdown.LabelledDropdown
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
import com.zaneschepke.wireguardautotunnel.util.extensions.capitalize
import com.zaneschepke.wireguardautotunnel.viewmodel.DnsViewModel
import java.util.*
@Composable
fun DnsSettingsScreen(viewModel: DnsViewModel = hiltViewModel()) {
val context = LocalContext.current
val navController = LocalNavController.current
val dnsUiState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
if (dnsUiState.isLoading) return
val locale = remember { Locale.getDefault() }
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.Top),
verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Top),
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()),
) {
Column {
GroupLabel(stringResource(R.string.endpoint), Modifier.padding(horizontal = 16.dp))
LabelledDropdown(
title = stringResource(R.string.dns_protocol),
leading = { Icon(Icons.Outlined.Dns, contentDescription = null) },
@@ -59,5 +73,27 @@ fun DnsSettingsScreen(viewModel: DnsViewModel = hiltViewModel()) {
)
}
}
Column {
GroupLabel(
stringResource(R.string.tunnel).capitalize(locale),
Modifier.padding(horizontal = 16.dp),
)
SurfaceRow(
leading = {
Icon(ImageVector.vectorResource(R.drawable.host), contentDescription = null)
},
title = stringResource(R.string.global_dns_servers),
trailing = { modifier ->
SwitchWithDivider(
checked = dnsUiState.dnsSettings.isGlobalTunnelDnsEnabled,
onClick = { viewModel.setGlobalTunnelDnsEnabled(it) },
modifier = modifier,
)
},
onClick = {
dnsUiState.globalConfig?.let { navController.push(Route.ConfigGlobal(it.id)) }
},
)
}
}
}
@@ -1,46 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.globals
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.CallSplit
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
@Composable
fun TunnelGlobalsScreen(globalTunnelId: Int) {
val navController = LocalNavController.current
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Top),
modifier = Modifier.verticalScroll(rememberScrollState()).fillMaxSize(),
) {
Column {
SurfaceRow(
leading = { Icon(Icons.Outlined.Settings, contentDescription = null) },
title = stringResource(R.string.configuration),
onClick = { navController.push(Route.ConfigGlobal(globalTunnelId)) },
)
SurfaceRow(
leading = {
Icon(Icons.AutoMirrored.Outlined.CallSplit, contentDescription = null)
},
title = stringResource(R.string.splt_tunneling),
onClick = { navController.push(Route.SplitTunnelGlobal(id = globalTunnelId)) },
)
}
}
}
@@ -16,7 +16,6 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
@@ -25,8 +24,8 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.common.button.ThemedSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberClipboardHelper
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.common.security.SecureScreenFromRecording
@@ -72,7 +71,7 @@ fun AndroidIntegrationsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
SurfaceRow(
leading = { Icon(Icons.Outlined.VpnLock, contentDescription = null) },
trailing = {
ScaledSwitch(
ThemedSwitch(
checked = isAlwaysOnEnabled,
onClick = { viewModel.setAlwaysOnVpnEnabled(it) },
)
@@ -94,12 +93,12 @@ fun AndroidIntegrationsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
Icons.Outlined.Restore,
contentDescription = null,
tint =
if (isAlwaysOnEnabled) Color.Gray
if (isAlwaysOnEnabled) MaterialTheme.colorScheme.outline
else MaterialTheme.colorScheme.onSurface,
)
},
trailing = {
ScaledSwitch(
ThemedSwitch(
checked = settingsState.settings.isRestoreOnBootEnabled,
onClick = { viewModel.setRestoreOnBootEnabled(it) },
enabled = !isAlwaysOnEnabled,
@@ -117,7 +116,7 @@ fun AndroidIntegrationsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
SurfaceRow(
leading = { Icon(Icons.Filled.AppShortcut, contentDescription = null) },
trailing = {
ScaledSwitch(
ThemedSwitch(
checked = settingsState.settings.isShortcutsEnabled,
onClick = { viewModel.setShortcutsEnabled(it) },
)
@@ -130,7 +129,7 @@ fun AndroidIntegrationsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
SurfaceRow(
leading = { Icon(Icons.Filled.SmartToy, contentDescription = null) },
trailing = {
ScaledSwitch(
ThemedSwitch(
checked = settingsState.isRemoteEnabled,
onClick = { viewModel.setRemoteEnabled(it) },
)
@@ -0,0 +1,123 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.lockdown
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.DataUsage
import androidx.compose.material.icons.outlined.Lan
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.common.button.ThemedSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.common.text.DescriptionText
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
import com.zaneschepke.wireguardautotunnel.viewmodel.LockdownViewModel
import org.orbitmvi.orbit.compose.collectSideEffect
@Composable
fun LockdownSettingsScreen(viewModel: LockdownViewModel = hiltViewModel()) {
val sharedViewModel = LocalSharedVm.current
val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
if (uiState.isLoading) return
var metered by remember { mutableStateOf(uiState.lockdownSettings.metered) }
var dualStack by remember { mutableStateOf(uiState.lockdownSettings.dualStack) }
var bypassLan by remember { mutableStateOf(uiState.lockdownSettings.bypassLan) }
sharedViewModel.collectSideEffect {
if (it is LocalSideEffect.SaveChanges) viewModel.setShowSaveModal(true)
}
if (uiState.showSaveModal) {
InfoDialog(
onDismiss = { viewModel.setShowSaveModal(false) },
onAttest = {
viewModel.setLockdownSettings(
uiState.lockdownSettings.copy(
metered = metered,
dualStack = dualStack,
bypassLan = bypassLan,
)
)
},
title = stringResource(R.string.save_changes),
body = {
Text(
stringResource(
R.string.restart_message_template,
stringResource(R.string.kill_switch),
)
)
},
confirmText = stringResource(R.string._continue),
)
}
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.Top),
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()),
) {
Column {
GroupLabel(
stringResource(R.string.configuration),
modifier = Modifier.padding(horizontal = 16.dp),
)
SurfaceRow(
leading = { Icon(Icons.Outlined.Lan, contentDescription = null) },
title = stringResource(R.string.allow_lan_traffic),
description = {
Text(
text = stringResource(R.string.bypass_lan_for_kill_switch),
style =
MaterialTheme.typography.bodySmall.copy(
MaterialTheme.colorScheme.outline
),
)
},
trailing = { ThemedSwitch(checked = bypassLan, onClick = { bypassLan = it }) },
onClick = { bypassLan = !bypassLan },
)
SurfaceRow(
leading = { Icon(Icons.Outlined.DataUsage, contentDescription = null) },
title = stringResource(R.string.metered_tunnel),
trailing = { ThemedSwitch(checked = metered, onClick = { metered = it }) },
onClick = { metered = !metered },
)
SurfaceRow(
leading = {
Icon(ImageVector.vectorResource(R.drawable.host), contentDescription = null)
},
title = stringResource(R.string.dual_stack),
description = { DescriptionText(stringResource(R.string.dual_stack_description)) },
trailing = { ThemedSwitch(checked = dualStack, onClick = { dualStack = it }) },
onClick = { dualStack = !dualStack },
)
}
}
}
@@ -21,8 +21,8 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.common.button.ThemedSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.dropdown.LabelledDropdown
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
@@ -86,7 +86,7 @@ fun TunnelMonitoringScreen(viewModel: MonitoringViewModel = hiltViewModel()) {
leading = { Icon(Icons.Outlined.QueryStats, contentDescription = null) },
title = stringResource(R.string.display_detailed_ping_stats),
trailing = {
ScaledSwitch(
ThemedSwitch(
checked = monitoringUiState.monitoringSettings.showDetailedPingStats,
onClick = { viewModel.setDetailedPingStats(it) },
)
@@ -10,6 +10,7 @@ import androidx.compose.material.icons.outlined.Http
import androidx.compose.material.icons.outlined.RemoveRedEye
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -24,73 +25,84 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.domain.model.ProxySettings
import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.common.button.ThemedSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.common.security.SecureScreenFromRecording
import com.zaneschepke.wireguardautotunnel.ui.common.textbox.ConfigurationTextBox
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
import com.zaneschepke.wireguardautotunnel.viewmodel.ProxySettingsViewModel
import java.util.Locale
import org.orbitmvi.orbit.compose.collectSideEffect
@Composable
fun ProxySettingsScreen(viewModel: ProxySettingsViewModel = hiltViewModel()) {
val sharedViewModel = LocalSharedVm.current
val proxySettingsState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
if (proxySettingsState.isLoading) return
val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
val proxySettings by
remember(proxySettingsState) { mutableStateOf(proxySettingsState.proxySettings) }
if (uiState.isLoading) return
val locale = remember { Locale.getDefault() }
val proxySettings by remember(uiState) { mutableStateOf(uiState.proxySettings) }
var socks5Enabled by
remember(proxySettings) {
mutableStateOf(proxySettingsState.proxySettings.socks5ProxyEnabled)
}
remember(proxySettings) { mutableStateOf(uiState.proxySettings.socks5ProxyEnabled) }
var httpEnabled by
remember(proxySettings) {
mutableStateOf(proxySettingsState.proxySettings.httpProxyEnabled)
}
remember(proxySettings) { mutableStateOf(uiState.proxySettings.httpProxyEnabled) }
var socksBindAddress by
remember(proxySettings) {
mutableStateOf(proxySettingsState.proxySettings.socks5ProxyBindAddress ?: "")
mutableStateOf(uiState.proxySettings.socks5ProxyBindAddress ?: "")
}
var httpBindAddress by
remember(proxySettings) {
mutableStateOf(proxySettingsState.proxySettings.httpProxyBindAddress ?: "")
}
remember(proxySettings) { mutableStateOf(uiState.proxySettings.httpProxyBindAddress ?: "") }
var proxyUsername by
remember(proxySettings) {
mutableStateOf(proxySettingsState.proxySettings.proxyUsername ?: "")
}
remember(proxySettings) { mutableStateOf(uiState.proxySettings.proxyUsername ?: "") }
var proxyPassword by
remember(proxySettings) {
mutableStateOf(proxySettingsState.proxySettings.proxyPassword ?: "")
}
var passwordVisible by
remember(proxySettings) { mutableStateOf(proxySettingsState.passwordVisible) }
remember(proxySettings) { mutableStateOf(uiState.proxySettings.proxyPassword ?: "") }
var passwordVisible by remember(proxySettings) { mutableStateOf(uiState.passwordVisible) }
val keyboardController = LocalSoftwareKeyboardController.current
val keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() })
val keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done)
fun saveChanges() {
viewModel.save(
ProxySettings(
socks5ProxyEnabled = socks5Enabled,
socks5ProxyBindAddress = socksBindAddress,
httpProxyEnabled = httpEnabled,
httpProxyBindAddress = httpBindAddress,
proxyUsername = proxyUsername,
proxyPassword = proxyPassword,
)
)
}
sharedViewModel.collectSideEffect { sideEffect ->
when (sideEffect) {
LocalSideEffect.SaveChanges -> {
viewModel.save(
ProxySettings(
socks5ProxyEnabled = socks5Enabled,
socks5ProxyBindAddress = socksBindAddress,
httpProxyEnabled = httpEnabled,
httpProxyBindAddress = httpBindAddress,
proxyUsername = proxyUsername,
proxyPassword = proxyPassword,
if (sideEffect is LocalSideEffect.SaveChanges) {
if (uiState.activeTuns.isNotEmpty()) viewModel.setShowSaveModal(true) else saveChanges()
}
}
if (uiState.showSaveModal) {
InfoDialog(
onDismiss = { viewModel.setShowSaveModal(false) },
onAttest = { saveChanges() },
title = stringResource(R.string.save_changes),
body = {
Text(
stringResource(
R.string.restart_message_template,
stringResource(R.string.tunnels).lowercase(locale),
)
)
}
else -> Unit
}
},
confirmText = stringResource(R.string._continue),
)
}
SecureScreenFromRecording()
@@ -105,7 +117,7 @@ fun ProxySettingsScreen(viewModel: ProxySettingsViewModel = hiltViewModel()) {
leading = { Icon(Icons.Outlined.Forward5, contentDescription = null) },
title = stringResource(R.string.socks_5_proxy),
trailing = {
ScaledSwitch(checked = socks5Enabled, onClick = { socks5Enabled = it })
ThemedSwitch(checked = socks5Enabled, onClick = { socks5Enabled = it })
},
onClick = { socks5Enabled = !socks5Enabled },
)
@@ -119,10 +131,9 @@ fun ProxySettingsScreen(viewModel: ProxySettingsViewModel = hiltViewModel()) {
),
label = stringResource(R.string.socks_5_bind_address),
value = socksBindAddress,
isError = proxySettingsState.isSocks5BindAddressError,
isError = uiState.isSocks5BindAddressError,
onValueChange = {
if (proxySettingsState.isSocks5BindAddressError)
viewModel.clearSocks5BindError()
if (uiState.isSocks5BindAddressError) viewModel.clearSocks5BindError()
socksBindAddress = it
},
)
@@ -132,7 +143,7 @@ fun ProxySettingsScreen(viewModel: ProxySettingsViewModel = hiltViewModel()) {
SurfaceRow(
leading = { Icon(Icons.Outlined.Http, contentDescription = null) },
title = stringResource(R.string.http_proxy),
trailing = { ScaledSwitch(checked = httpEnabled, onClick = { httpEnabled = it }) },
trailing = { ThemedSwitch(checked = httpEnabled, onClick = { httpEnabled = it }) },
onClick = { httpEnabled = !httpEnabled },
)
if (httpEnabled) {
@@ -144,10 +155,9 @@ fun ProxySettingsScreen(viewModel: ProxySettingsViewModel = hiltViewModel()) {
),
label = stringResource(R.string.http_bind_address),
value = httpBindAddress,
isError = proxySettingsState.isHttpBindAddressError,
isError = uiState.isHttpBindAddressError,
onValueChange = {
if (proxySettingsState.isSocks5BindAddressError)
viewModel.clearHttpBindError()
if (uiState.isSocks5BindAddressError) viewModel.clearHttpBindError()
httpBindAddress = it
},
modifier = Modifier.padding(horizontal = 12.dp),
@@ -169,11 +179,11 @@ fun ProxySettingsScreen(viewModel: ProxySettingsViewModel = hiltViewModel()) {
ConfigurationTextBox(
value = proxyUsername,
onValueChange = {
if (proxySettingsState.isUserNameError) viewModel.clearUsernameError()
if (uiState.isUserNameError) viewModel.clearUsernameError()
proxyUsername = it
},
label = stringResource(R.string.username),
isError = proxySettingsState.isUserNameError,
isError = uiState.isUserNameError,
hint = "",
keyboardActions = keyboardActions,
keyboardOptions = keyboardOptions,
@@ -181,11 +191,11 @@ fun ProxySettingsScreen(viewModel: ProxySettingsViewModel = hiltViewModel()) {
ConfigurationTextBox(
value = proxyPassword,
onValueChange = {
if (proxySettingsState.isUserNameError) viewModel.clearPasswordError()
if (uiState.isUserNameError) viewModel.clearPasswordError()
proxyPassword = it
},
label = stringResource(R.string.password),
isError = proxySettingsState.isPasswordError,
isError = uiState.isPasswordError,
hint = "",
keyboardActions = keyboardActions,
keyboardOptions = keyboardOptions,
@@ -3,6 +3,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings.proxy.compoents
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
import com.zaneschepke.wireguardautotunnel.ui.common.sheet.CustomBottomSheet
import com.zaneschepke.wireguardautotunnel.ui.common.sheet.SheetOption
import com.zaneschepke.wireguardautotunnel.util.extensions.asIcon
@@ -15,19 +16,23 @@ fun AppModeBottomSheet(
onDismiss: () -> Unit,
) {
val context = LocalContext.current
val isTv = LocalIsAndroidTV.current
CustomBottomSheet(
enumValues<AppMode>().map {
val icon = it.asIcon()
SheetOption(
icon,
label = it.asTitleString(context),
onClick = {
onDismiss()
onAppModeChange(it)
},
selected = appMode == it,
)
}
enumValues<AppMode>()
.filterNot { isTv && it == AppMode.KERNEL }
.map {
val icon = it.asIcon()
SheetOption(
icon,
label = it.asTitleString(context),
onClick = {
onDismiss()
onAppModeChange(it)
},
selected = appMode == it,
)
}
) {
onDismiss()
}
@@ -73,6 +73,11 @@ fun SupportScreen(viewModel: SupportViewModel = hiltViewModel()) {
)
Column {
GroupLabel(stringResource(R.string.resources), Modifier.padding(horizontal = 16.dp))
SurfaceRow(
leading = { Icon(Icons.Outlined.Favorite, contentDescription = null) },
title = stringResource(R.string.donate),
onClick = { navController.push(Route.Donate) },
)
SurfaceRow(
stringResource(R.string.docs_description),
onClick = { context.openWebUrl(context.getString(R.string.docs_url)) },
@@ -86,9 +91,11 @@ fun SupportScreen(viewModel: SupportViewModel = hiltViewModel()) {
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
)
SurfaceRow(
leading = { Icon(Icons.Outlined.Favorite, contentDescription = null) },
title = stringResource(R.string.donate),
onClick = { navController.push(Route.Donate) },
stringResource(R.string.translation),
onClick = { context.openWebUrl(context.getString(R.string.translation_url)) },
description = { DescriptionText(stringResource(R.string.help_translate)) },
leading = { Icon(Icons.Outlined.Translate, contentDescription = null) },
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
)
SurfaceRow(
leading = { Icon(Icons.Outlined.Balance, contentDescription = null) },
@@ -16,8 +16,8 @@ fun PermissionDialog(context: Context, onDismiss: () -> Unit) {
context.requestInstallPackagesPermission()
onDismiss()
},
title = { Text(stringResource(R.string.permission_required)) },
title = stringResource(R.string.permission_required),
body = { Text(stringResource(R.string.install_updated_permission)) },
confirmText = { Text(stringResource(R.string.allow)) },
confirmText = stringResource(R.string.allow),
)
}
@@ -46,7 +46,7 @@ fun UpdateDialog(viewModel: SupportViewModel, context: Context, onPermissionNeed
onPermissionNeeded()
}
},
title = { Text(stringResource(R.string.update_available)) },
title = stringResource(R.string.update_available),
body = {
Column(
horizontalAlignment = Alignment.Start,
@@ -77,7 +77,7 @@ fun UpdateDialog(viewModel: SupportViewModel, context: Context, onPermissionNeed
Text(text = annotatedString)
if (supportState.isLoading) {
val stroke = Stroke(cap = StrokeCap.Round)
val stroke = Stroke(cap = StrokeCap.Round, width = 4.0f)
LinearWavyProgressIndicator(
progress = { supportState.downloadProgress },
modifier = Modifier.fillMaxWidth().padding(top = 16.dp),
@@ -89,12 +89,8 @@ fun UpdateDialog(viewModel: SupportViewModel, context: Context, onPermissionNeed
}
}
},
confirmText = {
Text(
if (BuildConfig.FLAVOR != Constants.STANDALONE_FLAVOR)
stringResource(R.string.download)
else stringResource(R.string.download_and_install)
)
},
confirmText =
if (BuildConfig.FLAVOR != Constants.STANDALONE_FLAVOR) stringResource(R.string.download)
else stringResource(R.string.download_and_install),
)
}
@@ -6,8 +6,10 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.Launch
import androidx.compose.material.icons.outlined.CurrencyBitcoin
import androidx.compose.material.icons.outlined.Done
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -16,19 +18,26 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.BuildConfig
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.common.button.ThemedSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.common.text.DescriptionText
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
import com.zaneschepke.wireguardautotunnel.ui.screens.support.donate.components.DonationHeroSection
import com.zaneschepke.wireguardautotunnel.ui.screens.support.donate.components.GoogleDonationMessage
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
import com.zaneschepke.wireguardautotunnel.viewmodel.SettingsViewModel
@Composable
fun DonateScreen() {
fun DonateScreen(viewModel: SettingsViewModel = hiltViewModel()) {
val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
if (uiState.isLoading) return
val context = LocalContext.current
val navController = LocalNavController.current
val isGoogleFlavor = remember { BuildConfig.FLAVOR == Constants.GOOGLE_PLAY_FLAVOR }
@@ -91,6 +100,26 @@ fun DonateScreen() {
} else {
GoogleDonationMessage()
}
SurfaceRow(
leading = {
Icon(
Icons.Outlined.Done,
contentDescription = null,
modifier = Modifier.size(24.dp),
)
},
title = stringResource(R.string.already_donated),
description = {
DescriptionText(stringResource(R.string.already_donated_description))
},
trailing = {
ThemedSwitch(
checked = uiState.settings.alreadyDonated,
onClick = { viewModel.setAlreadyDonated(it) },
)
},
onClick = { viewModel.setAlreadyDonated(!uiState.settings.alreadyDonated) },
)
}
}
}
@@ -12,6 +12,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanOptions
import com.zaneschepke.wireguardautotunnel.R
@@ -94,9 +95,9 @@ fun TunnelsScreen() {
viewModel.deleteSelectedTunnels()
showDeleteModal = false
},
title = { Text(text = stringResource(R.string.delete_tunnel)) },
title = stringResource(R.string.delete_tunnel),
body = { Text(text = stringResource(R.string.delete_tunnel_message)) },
confirmText = { Text(text = stringResource(R.string.yes)) },
confirmText = stringResource(R.string.yes),
)
}
@@ -94,7 +94,7 @@ fun TunnelList(
if (sharedState.selectedTunnels.isNotEmpty()) {
viewModel.toggleSelectedTunnel(tunnel.id)
} else {
navController.push(Route.TunnelOptions(tunnel.id))
navController.push(Route.TunnelSettings(tunnel.id))
viewModel.clearSelectedTunnels()
}
},
@@ -5,14 +5,18 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
import com.zaneschepke.wireguardautotunnel.ui.common.security.SecureScreenFromRecording
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.config.components.AddPeerButton
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.config.components.InterfaceSection
@@ -21,33 +25,50 @@ import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
import com.zaneschepke.wireguardautotunnel.ui.state.ConfigProxy
import com.zaneschepke.wireguardautotunnel.ui.state.PeerProxy
import com.zaneschepke.wireguardautotunnel.viewmodel.ConfigViewModel
import java.util.Locale
import org.orbitmvi.orbit.compose.collectSideEffect
@Composable
fun ConfigScreen(viewModel: ConfigViewModel) {
val sharedViewModel = LocalSharedVm.current
val configUiState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
if (configUiState.isLoading) return
if (uiState.isLoading) return
val locale = remember { Locale.getDefault() }
var configProxy by remember {
mutableStateOf(
configUiState.tunnel?.let { ConfigProxy.from(it.toAmConfig()) } ?: ConfigProxy()
)
mutableStateOf(uiState.tunnel?.let { ConfigProxy.from(it.toAmConfig()) } ?: ConfigProxy())
}
var tunnelName by remember { mutableStateOf(configUiState.tunnel?.name ?: "") }
var tunnelName by remember { mutableStateOf(uiState.tunnel?.name ?: "") }
val isGlobalConfig = rememberSaveable { tunnelName == TunnelConfig.GLOBAL_CONFIG_NAME }
val isTunnelNameTaken by
remember(tunnelName) {
derivedStateOf { configUiState.unavailableNames.contains(tunnelName) }
}
remember(tunnelName) { derivedStateOf { uiState.unavailableNames.contains(tunnelName) } }
sharedViewModel.collectSideEffect { sideEffect ->
if (sideEffect is LocalSideEffect.SaveChanges)
viewModel.saveConfigProxy(configProxy, tunnelName)
if (uiState.isRunning) viewModel.setShowSaveModal(true)
else viewModel.saveConfigProxy(configProxy, tunnelName)
}
if (uiState.showSaveModal) {
InfoDialog(
onDismiss = { viewModel.setShowSaveModal(false) },
onAttest = { viewModel.saveConfigProxy(configProxy, tunnelName) },
title = stringResource(R.string.save_changes),
body = {
Text(
stringResource(
R.string.restart_message_template,
stringResource(R.string.tunnels).lowercase(locale),
)
)
},
confirmText = stringResource(R.string._continue),
)
}
SecureScreenFromRecording()
@@ -60,6 +81,7 @@ fun ConfigScreen(viewModel: ConfigViewModel) {
InterfaceSection(
isGlobalConfig,
configProxy = configProxy,
uiState.isRunning,
tunnelName,
isTunnelNameTaken,
onInterfaceChange = { configProxy = configProxy.copy(`interface` = it) },
@@ -165,14 +165,15 @@ fun InterfaceFields(
.lowercase(locale),
modifier = Modifier.weight(3f),
)
ConfigurationTextBox(
value = interfaceState.mtu,
onValueChange = { onInterfaceChange(interfaceState.copy(mtu = it)) },
label = stringResource(R.string.mtu),
hint = stringResource(R.string.auto).lowercase(locale),
modifier = Modifier.weight(2f),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
)
if (!isGlobalConfig)
ConfigurationTextBox(
value = interfaceState.mtu,
onValueChange = { onInterfaceChange(interfaceState.copy(mtu = it)) },
label = stringResource(R.string.mtu),
hint = stringResource(R.string.auto).lowercase(locale),
modifier = Modifier.weight(2f),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
)
}
if (showScripts) {
ConfigurationTextBox(
@@ -18,6 +18,7 @@ import com.wireguard.crypto.KeyPair
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.common.text.DescriptionText
import com.zaneschepke.wireguardautotunnel.ui.common.textbox.ConfigurationTextBox
import com.zaneschepke.wireguardautotunnel.ui.state.ConfigProxy
import com.zaneschepke.wireguardautotunnel.ui.state.InterfaceProxy
@@ -27,6 +28,7 @@ import java.util.*
fun InterfaceSection(
isGlobalConfig: Boolean,
configProxy: ConfigProxy,
isRunning: Boolean,
tunnelName: String,
isTunnelNameTaken: Boolean,
onInterfaceChange: (InterfaceProxy) -> Unit,
@@ -64,60 +66,62 @@ fun InterfaceSection(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth(),
) {
GroupLabel(
stringResource(R.string.interface_),
modifier = Modifier.padding(horizontal = 16.dp),
)
Row {
if (isTv && !isGlobalConfig) {
IconButton(onClick = { showPrivateKey = !showPrivateKey }) {
Icon(
Icons.Outlined.RemoveRedEye,
stringResource(R.string.show_password),
)
}
IconButton(
enabled = true,
onClick = {
val keypair = KeyPair()
onInterfaceChange(
configProxy.`interface`.copy(
privateKey = keypair.privateKey.toBase64(),
publicKey = keypair.publicKey.toBase64(),
)
)
},
) {
Icon(
Icons.Rounded.Refresh,
stringResource(R.string.rotate_keys),
tint = MaterialTheme.colorScheme.onSurface,
)
}
}
InterfaceDropdown(
expanded = isDropDownExpanded,
onExpandedChange = { isDropDownExpanded = it },
showScripts = showScripts,
showAmneziaValues = showAmneziaValues,
isAmneziaCompatibilitySet = isAmneziaCompatibilitySet,
onToggleScripts = { showScripts = !showScripts },
onToggleAmneziaValues = { showAmneziaValues = !showAmneziaValues },
onToggleAmneziaCompatibility = { toggleAmneziaCompat() },
onMimicQuic = {
showAmneziaValues = true
onMimicQuic()
},
onMimicDns = {
showAmneziaValues = true
onMimicDns()
},
onMimicSip = {
showAmneziaValues = true
onMimicSip()
},
if (!isGlobalConfig)
GroupLabel(
stringResource(R.string.interface_),
modifier = Modifier.padding(horizontal = 16.dp),
)
}
if (!isGlobalConfig)
Row {
if (isTv) {
IconButton(onClick = { showPrivateKey = !showPrivateKey }) {
Icon(
Icons.Outlined.RemoveRedEye,
stringResource(R.string.show_password),
)
}
IconButton(
enabled = true,
onClick = {
val keypair = KeyPair()
onInterfaceChange(
configProxy.`interface`.copy(
privateKey = keypair.privateKey.toBase64(),
publicKey = keypair.publicKey.toBase64(),
)
)
},
) {
Icon(
Icons.Rounded.Refresh,
stringResource(R.string.rotate_keys),
tint = MaterialTheme.colorScheme.onSurface,
)
}
}
InterfaceDropdown(
expanded = isDropDownExpanded,
onExpandedChange = { isDropDownExpanded = it },
showScripts = showScripts,
showAmneziaValues = showAmneziaValues,
isAmneziaCompatibilitySet = isAmneziaCompatibilitySet,
onToggleScripts = { showScripts = !showScripts },
onToggleAmneziaValues = { showAmneziaValues = !showAmneziaValues },
onToggleAmneziaCompatibility = { toggleAmneziaCompat() },
onMimicQuic = {
showAmneziaValues = true
onMimicQuic()
},
onMimicDns = {
showAmneziaValues = true
onMimicDns()
},
onMimicSip = {
showAmneziaValues = true
onMimicSip()
},
)
}
}
Column(
verticalArrangement = Arrangement.spacedBy(12.dp),
@@ -126,9 +130,18 @@ fun InterfaceSection(
if (!isGlobalConfig)
ConfigurationTextBox(
value = tunnelName,
enabled = !isRunning,
onValueChange = onTunnelNameChange,
label = stringResource(R.string.name),
isError = isTunnelNameTaken,
supportingText =
if (isRunning) {
{
DescriptionText(
stringResource(R.string.tunnel_running_name_message)
)
}
} else null,
hint =
stringResource(
R.string.hint_template,
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.tunneloptions
package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.settings
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -8,6 +8,7 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.CallSplit
import androidx.compose.material.icons.outlined.DataUsage
import androidx.compose.material.icons.outlined.Dns
import androidx.compose.material.icons.outlined.Star
import androidx.compose.material3.Icon
@@ -28,21 +29,24 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.common.button.ThemedSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.common.text.DescriptionText
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.tunneloptions.components.QrCodeDialog
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.settings.components.QrCodeDialog
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
import com.zaneschepke.wireguardautotunnel.ui.theme.Disabled
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelViewModel
import org.orbitmvi.orbit.compose.collectSideEffect
@Composable
fun TunnelOptionsScreen(viewModel: TunnelViewModel) {
fun TunnelSettingsScreen(viewModel: TunnelViewModel) {
val navController = LocalNavController.current
val sharedViewModel = LocalSharedVm.current
val sharedUiState by sharedViewModel.container.stateFlow.collectAsStateWithLifecycle()
val tunnelState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
if (tunnelState.isLoading) return
@@ -81,7 +85,7 @@ fun TunnelOptionsScreen(viewModel: TunnelViewModel) {
)
},
trailing = {
ScaledSwitch(
ThemedSwitch(
checked = tunnel.isPrimaryTunnel,
onClick = { viewModel.togglePrimaryTunnel() },
)
@@ -90,9 +94,25 @@ fun TunnelOptionsScreen(viewModel: TunnelViewModel) {
)
SurfaceRow(
leading = {
Icon(Icons.AutoMirrored.Outlined.CallSplit, contentDescription = null)
Icon(
Icons.AutoMirrored.Outlined.CallSplit,
contentDescription = null,
tint =
if (sharedUiState.proxyEnabled) Disabled
else MaterialTheme.colorScheme.onSurface,
)
},
enabled = !sharedUiState.proxyEnabled,
title = stringResource(R.string.splt_tunneling),
description =
if (sharedUiState.proxyEnabled) {
{
DescriptionText(
stringResource(R.string.unavailable_in_mode),
disabled = true,
)
}
} else null,
onClick = { navController.push(Route.SplitTunnel(id = tunnel.id)) },
)
}
@@ -108,7 +128,7 @@ fun TunnelOptionsScreen(viewModel: TunnelViewModel) {
DescriptionText(stringResource(R.string.ddns_auto_update_description))
},
trailing = {
ScaledSwitch(
ThemedSwitch(
checked = tunnel.restartOnPingFailure,
onClick = { viewModel.setRestartOnPing(it) },
)
@@ -121,12 +141,42 @@ fun TunnelOptionsScreen(viewModel: TunnelViewModel) {
},
title = stringResource(R.string.prefer_ipv6_resolution),
trailing = {
ScaledSwitch(
ThemedSwitch(
checked = !tunnel.isIpv4Preferred,
onClick = { viewModel.toggleIpv4Preferred() },
onClick = { viewModel.setIpv4Preferred(!it) },
)
},
onClick = { viewModel.toggleIpv4Preferred() },
onClick = { viewModel.setIpv4Preferred(!tunnel.isIpv4Preferred) },
)
SurfaceRow(
leading = {
Icon(
Icons.Outlined.DataUsage,
contentDescription = null,
tint =
if (sharedUiState.proxyEnabled) Disabled
else MaterialTheme.colorScheme.onSurface,
)
},
title = stringResource(R.string.metered_tunnel),
enabled = !sharedUiState.proxyEnabled,
description =
if (sharedUiState.proxyEnabled) {
{
DescriptionText(
stringResource(R.string.unavailable_in_mode),
disabled = true,
)
}
} else null,
trailing = {
ThemedSwitch(
checked = tunnel.isMetered,
onClick = { viewModel.setMetered(it) },
enabled = !sharedUiState.proxyEnabled,
)
},
onClick = { viewModel.setMetered(!tunnel.isMetered) },
)
}
}
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.tunneloptions.components
package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.settings.components
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
@@ -154,8 +154,10 @@ private fun ConfigTypeSelector(
)
val activeContainerColor = Color.White
val inactiveContainerColor = Color.White
val activeContentColor = if (isEnabled) Color.Black else Color.Gray
val inactiveContentColor = if (isEnabled) Color.Black else Color.Gray
val activeContentColor =
if (isEnabled) Color.Black else MaterialTheme.colorScheme.outline
val inactiveContentColor =
if (isEnabled) Color.Black else MaterialTheme.colorScheme.outline
SegmentedButton(
shape =
SegmentedButtonDefaults.itemShape(
@@ -172,7 +174,7 @@ private fun ConfigTypeSelector(
contentDescription = stringResource(R.string.select),
tint =
if (isEnabled) MaterialTheme.colorScheme.primary
else Color.Gray,
else MaterialTheme.colorScheme.outline,
modifier = Modifier.size(SegmentedButtonDefaults.IconSize),
)
},
@@ -180,7 +182,8 @@ private fun ConfigTypeSelector(
Icon(
imageVector = Icons.Outlined.VpnKey,
contentDescription = typeName,
tint = if (isEnabled) Color.Black else Color.Gray,
tint =
if (isEnabled) Color.Black else MaterialTheme.colorScheme.outline,
modifier = Modifier.size(SegmentedButtonDefaults.IconSize),
)
}
@@ -197,7 +200,7 @@ private fun ConfigTypeSelector(
) {
Text(
text = typeName,
color = if (isEnabled) Color.Black else Color.Gray,
color = if (isEnabled) Color.Black else MaterialTheme.colorScheme.outline,
style = MaterialTheme.typography.labelMedium,
)
}
@@ -6,4 +6,6 @@ data class ConfigUiState(
val unavailableNames: List<String> = emptyList(),
val isLoading: Boolean = true,
val tunnel: TunnelConfig? = null,
val isRunning: Boolean = false,
val showSaveModal: Boolean = false,
)
@@ -1,5 +1,10 @@
package com.zaneschepke.wireguardautotunnel.ui.state
import com.zaneschepke.wireguardautotunnel.domain.model.DnsSettings
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
data class DnsUiState(val isLoading: Boolean = true, val dnsSettings: DnsSettings = DnsSettings())
data class DnsUiState(
val isLoading: Boolean = true,
val dnsSettings: DnsSettings = DnsSettings(),
val globalConfig: TunnelConfig? = null,
)
@@ -0,0 +1,9 @@
package com.zaneschepke.wireguardautotunnel.ui.state
import com.zaneschepke.wireguardautotunnel.domain.model.LockdownSettings
data class LockdownSettingsUiState(
val lockdownSettings: LockdownSettings = LockdownSettings(),
val isLoading: Boolean = true,
val showSaveModal: Boolean = false,
)
@@ -17,7 +17,7 @@ data class PeerProxy(
parsePublicKey(publicKey)
if (preSharedKey.isNotBlank()) parsePreSharedKey(preSharedKey)
if (persistentKeepalive.isNotBlank()) parsePersistentKeepalive(persistentKeepalive)
parseEndpoint(endpoint)
if (endpoint.isNotBlank()) parseEndpoint(endpoint)
parseAllowedIPs(allowedIps)
}
.build()
@@ -29,7 +29,7 @@ data class PeerProxy(
parsePublicKey(publicKey)
if (preSharedKey.isNotBlank()) parsePreSharedKey(preSharedKey)
if (persistentKeepalive.isNotBlank()) parsePersistentKeepalive(persistentKeepalive)
parseEndpoint(endpoint)
if (endpoint.isNotBlank()) parseEndpoint(endpoint)
parseAllowedIPs(allowedIps)
}
.build()
@@ -1,13 +1,16 @@
package com.zaneschepke.wireguardautotunnel.ui.state
import com.zaneschepke.wireguardautotunnel.domain.model.ProxySettings
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
data class ProxySettingsUiState(
val proxySettings: ProxySettings = ProxySettings(),
val activeTuns: Map<Int, TunnelState> = emptyMap(),
val isSocks5BindAddressError: Boolean = false,
val isHttpBindAddressError: Boolean = false,
val isUserNameError: Boolean = false,
val isPasswordError: Boolean = false,
val passwordVisible: Boolean = false,
val isLoading: Boolean = true,
val showSaveModal: Boolean = false,
)
@@ -11,6 +11,7 @@ data class SharedAppUiState(
val theme: Theme = Theme.AUTOMATIC,
val locale: String = LocaleUtil.OPTION_PHONE_LANGUAGE,
val pinLockEnabled: Boolean = false,
val shouldShowDonationSnackbar: Boolean = false,
val tunnels: List<TunnelConfig> = emptyList(),
val selectedTunnels: List<TunnelConfig> = emptyList(),
val activeTunnels: Map<Int, TunnelState> = emptyMap(),
@@ -20,5 +21,6 @@ data class SharedAppUiState(
val isAutoTunnelActive: Boolean = false,
val isLocationDisclosureShown: Boolean = false,
val isBatteryOptimizationShown: Boolean = false,
val proxyEnabled: Boolean = false,
val settings: GeneralSettings = GeneralSettings(),
)
@@ -19,6 +19,8 @@ val AlertRed = Color(0xFFCF6679)
val Straw = Color(0xFFD4C483)
val Disabled = CoolGray.copy(alpha = 0.4f)
sealed class ThemeColors(
val background: Color,
val surface: Color,

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