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 - name: Get release apk path
id: 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 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 - name: Upload All APK Artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v5
with: with:
name: android_artifacts_${{ inputs.flavor }} name: android_artifacts_${{ inputs.flavor }}
path: >- path: >-
app/build/outputs/apk/${{ inputs.flavor }}/${{ inputs.build_type }}/${{ app/build/outputs/apk/${{ inputs.flavor }}/${{ inputs.build_type }}/*.apk
inputs.flavor == 'fdroid' && inputs.build_type == 'release'
&& 'wgtunnel-fdroid-release-*.apk'
|| format('wgtunnel-{0}-v*.apk', inputs.flavor)
}}
retention-days: 1 retention-days: 1
if-no-files-found: warn if-no-files-found: warn
+5 -3
View File
@@ -26,6 +26,9 @@ jobs:
echo "new_commits=$NEW_COMMITS" >> $GITHUB_OUTPUT echo "new_commits=$NEW_COMMITS" >> $GITHUB_OUTPUT
build-standalone-nightly: build-standalone-nightly:
needs:
- check_commits
if: ${{ needs.check_commits.outputs.has_new_commits > 0 && inputs.release_type != 'none' }}
uses: ./.github/workflows/build.yml uses: ./.github/workflows/build.yml
secrets: inherit secrets: inherit
with: with:
@@ -34,7 +37,6 @@ jobs:
publish: publish:
needs: needs:
- check_commits
- build-standalone-nightly - build-standalone-nightly
if: ${{ needs.check_commits.outputs.has_new_commits > 0 && inputs.release_type != 'none' }} if: ${{ needs.check_commits.outputs.has_new_commits > 0 && inputs.release_type != 'none' }}
name: publish-nightly name: publish-nightly
@@ -69,7 +71,7 @@ jobs:
run: mkdir ${{ github.workspace }}/temp run: mkdir ${{ github.workspace }}/temp
- name: Download artifacts - name: Download artifacts
uses: actions/download-artifact@v5 uses: actions/download-artifact@v6
with: with:
pattern: android_artifacts_* pattern: android_artifacts_*
path: ${{ github.workspace }}/temp path: ${{ github.workspace }}/temp
@@ -124,4 +126,4 @@ jobs:
files: | files: |
${{ github.workspace }}/temp/**/*.apk ${{ github.workspace }}/temp/**/*.apk
env: env:
GITHUB_TOKEN: ${{ github.token }} GITHUB_TOKEN: ${{ secrets.PAT }}
+25 -6
View File
@@ -1,10 +1,13 @@
name: notifications name: notifications
permissions:
contents: write
packages: write
on: on:
issues: issues:
types: [opened, closed] types: [opened, closed]
release: release:
types: [published] types: [published, prereleased]
jobs: jobs:
notify: notify:
@@ -43,15 +46,23 @@ jobs:
--data-urlencode "text=$TEXT" --data-urlencode "text=$TEXT"
- name: Send to Telegram - New Release - 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: env:
NAME: ${{ github.event.release.name }} NAME: ${{ github.event.release.name }}
TAG: ${{ github.event.release.tag_name }} TAG: ${{ github.event.release.tag_name }}
BODY: ${{ github.event.release.body || 'No notes provided' }} BODY: ${{ github.event.release.body || 'No notes provided' }}
URL: ${{ github.event.release.html_url }} URL: ${{ github.event.release.html_url }}
ACTION: ${{ github.event.action }}
run: | run: |
BODY_TRUNC="${BODY:0:200}" # Truncate to avoid spam 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" \ curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN }}/sendMessage" \
-d chat_id="${{ vars.TELEGRAM_CHAT_ID }}" \ -d chat_id="${{ vars.TELEGRAM_CHAT_ID }}" \
${{ vars.TELEGRAM_THREAD_ID && format('-d message_thread_id="{0}"', vars.TELEGRAM_THREAD_ID) || '' }} \ ${{ vars.TELEGRAM_THREAD_ID && format('-d message_thread_id="{0}"', vars.TELEGRAM_THREAD_ID) || '' }} \
@@ -106,15 +117,23 @@ jobs:
-d "$PAYLOAD" -d "$PAYLOAD"
- name: Send to Matrix - New Release - 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: env:
NAME: ${{ github.event.release.name }} NAME: ${{ github.event.release.name }}
TAG: ${{ github.event.release.tag_name }} TAG: ${{ github.event.release.tag_name }}
BODY: ${{ github.event.release.body || 'No notes provided' }} BODY: ${{ github.event.release.body || 'No notes provided' }}
URL: ${{ github.event.release.html_url }} URL: ${{ github.event.release.html_url }}
ACTION: ${{ github.event.action }}
run: | run: |
PLAIN_MESSAGE=$(echo -e "🚀 New Release $NAME ($TAG)\n\n$BODY\n\nView Release: $URL") if [ "$ACTION" == "prereleased" ]; then
HTML_MESSAGE=$(echo -e "<p>🚀 New Release <strong>$NAME</strong> ($TAG)</p><p>$BODY</p><p><a href=\"$URL\">View Release</a></p>") 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}" PLAIN_MESSAGE="${PLAIN_MESSAGE:0:220}"
PAYLOAD=$(jq -n --arg body "$PLAIN_MESSAGE" --arg formatted "$HTML_MESSAGE" '{ PAYLOAD=$(jq -n --arg body "$PLAIN_MESSAGE" --arg formatted "$HTML_MESSAGE" '{
"msgtype": "m.text", "msgtype": "m.text",
+4 -4
View File
@@ -109,7 +109,7 @@ jobs:
run: mkdir ${{ github.workspace }}/temp run: mkdir ${{ github.workspace }}/temp
- name: Download artifacts - name: Download artifacts
uses: actions/download-artifact@v5 uses: actions/download-artifact@v6
with: with:
pattern: android_artifacts_* pattern: android_artifacts_*
path: ${{ github.workspace }}/temp path: ${{ github.workspace }}/temp
@@ -118,8 +118,8 @@ jobs:
- name: Set version release notes - name: Set version release notes
if: ${{ github.event_name == 'push' || inputs.release_type == 'release' }} if: ${{ github.event_name == 'push' || inputs.release_type == 'release' }}
run: | run: |
VERSION_NAME=$(grep "const val VERSION_NAME" buildSrc/src/main/kotlin/Constants.kt | awk -F'"' '{print $2}') 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_NAME}.txt || echo "No changelog found for ${VERSION_NAME}")" 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<<EOF" >> $GITHUB_ENV
echo "$RELEASE_NOTES" >> $GITHUB_ENV echo "$RELEASE_NOTES" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV echo "EOF" >> $GITHUB_ENV
@@ -162,7 +162,7 @@ jobs:
files: | files: |
${{ github.workspace }}/temp/**/*.apk ${{ github.workspace }}/temp/**/*.apk
env: env:
GITHUB_TOKEN: ${{ github.token }} GITHUB_TOKEN: ${{ secrets.PAT }}
publish-fdroid-public: publish-fdroid-public:
runs-on: ubuntu-latest runs-on: ubuntu-latest
+5 -8
View File
@@ -21,8 +21,7 @@ and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/)
<div align="center"> <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) [![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/) [![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)
[![Personal](https://img.shields.io/static/v1?style=for-the-badge&message=Personal&color=1976D2&logo=F-Droid&logoColor=FFFFFF&label=)](https://github.com/zaneschepke/fdroid)
[![Obtainium](https://img.shields.io/badge/Obtainium-414141?style=for-the-badge&logo=Obtainium&logoColor=white)](https://apps.obtainium.imranr.dev/redirect?r=obtainium://app/%7B%22id%22%3A%22com.zaneschepke.wireguardautotunnel%22%2C%22url%22%3A%22https%3A%2F%2Fgithub.com%2Fzaneschepke%2Fwgtunnel%22%2C%22author%22%3A%22zaneschepke%22%2C%22name%22%3A%22WG%20Tunnel%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Atrue%2C%5C%22sortMethodChoice%5C%22%3A%5C%22date%5C%22%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Atrue%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22WG%20Tunnel%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22Zane%20Schepke%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%7D%22%2C%22overrideSource%22%3Anull%7D) [![Obtainium](https://img.shields.io/badge/Obtainium-414141?style=for-the-badge&logo=Obtainium&logoColor=white)](https://apps.obtainium.imranr.dev/redirect?r=obtainium://app/%7B%22id%22%3A%22com.zaneschepke.wireguardautotunnel%22%2C%22url%22%3A%22https%3A%2F%2Fgithub.com%2Fzaneschepke%2Fwgtunnel%22%2C%22author%22%3A%22zaneschepke%22%2C%22name%22%3A%22WG%20Tunnel%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Atrue%2C%5C%22sortMethodChoice%5C%22%3A%5C%22date%5C%22%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Atrue%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22WG%20Tunnel%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22Zane%20Schepke%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%7D%22%2C%22overrideSource%22%3Anull%7D)
</div> </div>
@@ -60,14 +59,12 @@ WG Tunnel is an alternative Android client for WireGuard and AmneziaWG, inspired
</div> </div>
<div style="display: flex; flex-wrap: wrap; justify-content: left; gap: 10px;"> <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="Main" src="fastlane/metadata/android/en-US/images/phoneScreenshots/main_screen.png" width="200" alt="Main"/>
<img label="Settings" src="fastlane/metadata/android/en-US/images/phoneScreenshots/settings_screen.png" width="200" /> <img label="Config" src="fastlane/metadata/android/en-US/images/phoneScreenshots/config_screen.png" width="200" alt="Config"/>
<img label="Auto" src="fastlane/metadata/android/en-US/images/phoneScreenshots/auto_screen.png" width="200" /> <img label="Settings" src="fastlane/metadata/android/en-US/images/phoneScreenshots/settings_screen.png" width="200" alt="Settings"/>
<img label="Config" src="fastlane/metadata/android/en-US/images/phoneScreenshots/config_screen.png" width="200" /> <img label="Auto-tunnel" src="fastlane/metadata/android/en-US/images/phoneScreenshots/auto_screen.png" width="200" alt="Auto-tunnel"/>
</div> </div>
<div style="text-align: left;">
## Features ## Features
- **Tunnel Import Methods**: Easily add tunnels using .conf files, ZIP archives, manual entry, or QR code scanning. - **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 import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins { plugins {
@@ -27,6 +28,15 @@ android {
// fix okhttp proguard issue // fix okhttp proguard issue
packaging { resources { pickFirsts.add("okhttp3/internal/publicsuffix/publicsuffixes.gz") } } packaging { resources { pickFirsts.add("okhttp3/internal/publicsuffix/publicsuffixes.gz") } }
splits {
abi {
isEnable = true
reset()
include("armeabi-v7a", "arm64-v8a")
isUniversalApk = true
}
}
defaultConfig { defaultConfig {
applicationId = Constants.APP_ID applicationId = Constants.APP_ID
minSdk = Constants.MIN_SDK minSdk = Constants.MIN_SDK
@@ -128,19 +138,33 @@ android {
allowedLicenseUrls().forEach { allowUrl(it) } allowedLicenseUrls().forEach { allowUrl(it) }
} }
applicationVariants.all { android.applicationVariants.all {
val variant = this val variant = this
variant.outputs
.map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl } val abiNameMap =
.forEach { output -> mapOf(
val outputFileName = "armeabi-v7a" to "armv7",
if (variant.flavorName == "fdroid" && variant.buildType.name == "release") { "arm64-v8a" to "arm64",
"${Constants.APP_NAME}-fdroid-release-${variant.versionName}.apk" "x86" to "x86",
} else { "x86_64" to "x64",
"${Constants.APP_NAME}-${variant.flavorName}-v${variant.versionName}.apk" )
}
output.outputFileName = outputFileName 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.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <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--> <!--foreground service special use for non VPN service tunnels, android 14-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<!--foreground service special use for VPN service tunnels, android 14--> <!--foreground service special use for VPN service tunnels, android 14-->
@@ -14,6 +17,7 @@
<!--foreground service permissions--> <!--foreground service permissions-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<!--start service on boot permission--> <!--start service on boot permission-->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
@@ -42,17 +46,6 @@
<uses-feature android:name="android.hardware.wifi" <uses-feature android:name="android.hardware.wifi"
android:required="false"/> 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 <application
android:name=".WireGuardAutoTunnel" android:name=".WireGuardAutoTunnel"
android:allowBackup="false" android:allowBackup="false"
@@ -18,9 +18,16 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.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.unit.dp
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen 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.rememberNavBackStack
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
import androidx.navigation3.ui.NavDisplay 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.AppDatabase
import com.zaneschepke.wireguardautotunnel.data.DataStoreManager.Companion.shouldShowDonationSnackbar
import com.zaneschepke.wireguardautotunnel.data.model.AppMode import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController 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.banner.AppAlertBanner
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.VpnDeniedDialog import com.zaneschepke.wireguardautotunnel.ui.common.dialog.VpnDeniedDialog
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.CustomSnackBar import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.CustomSnackBar
import com.zaneschepke.wireguardautotunnel.ui.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.Route
import com.zaneschepke.wireguardautotunnel.ui.navigation.Tab import com.zaneschepke.wireguardautotunnel.ui.navigation.Tab
import com.zaneschepke.wireguardautotunnel.ui.navigation.components.BottomNavbar 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.display.DisplayScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.language.LanguageScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.language.LanguageScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.dns.DnsSettingsScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.dns.DnsSettingsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.globals.TunnelGlobalsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.integrations.AndroidIntegrationsScreen 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.TunnelMonitoringScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.logs.LogsScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.logs.LogsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.ping.PingTargetScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.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.support.license.LicenseScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.TunnelsScreen import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.TunnelsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.config.ConfigScreen 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.sort.SortScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.splittunnel.SplitTunnelScreen 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.AlertRed
import com.zaneschepke.wireguardautotunnel.ui.theme.OffWhite import com.zaneschepke.wireguardautotunnel.ui.theme.OffWhite
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
@@ -99,8 +109,7 @@ import xyz.teamgravity.pin_lock_compose.PinManager
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
@Inject lateinit var appStateRepository: AppStateRepository @Inject lateinit var appStateRepository: AppStateRepository
@Inject lateinit var tunnelManager: TunnelManager @Inject lateinit var tunnelRepository: TunnelRepository
@Inject lateinit var networkMonitor: NetworkMonitor
@Inject lateinit var appDatabase: AppDatabase @Inject lateinit var appDatabase: AppDatabase
private lateinit var roomBackup: RoomBackup private lateinit var roomBackup: RoomBackup
@@ -127,16 +136,16 @@ class MainActivity : AppCompatActivity() {
setContent { setContent {
val context = LocalContext.current val context = LocalContext.current
val isTv = isRunningOnTv() val isTv = isRunningOnTv()
val appState by viewModel.container.stateFlow.collectAsStateWithLifecycle() val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
LaunchedEffect(appState.isAppLoaded) { LaunchedEffect(uiState.isAppLoaded) {
if (appState.isAppLoaded) { if (uiState.isAppLoaded) {
appState.locale.let { LocaleUtil.changeLocale(it) } uiState.locale.let { LocaleUtil.changeLocale(it) }
} }
} }
val snackbar = remember { SnackbarHostState() } val snackbarState = rememberCustomSnackbarState()
var showVpnPermissionDialog by remember { mutableStateOf(false) } var showVpnPermissionDialog by remember { mutableStateOf(false) }
var vpnPermissionDenied by remember { mutableStateOf(false) } var vpnPermissionDenied by remember { mutableStateOf(false) }
var requestingAppMode by remember { var requestingAppMode by remember {
@@ -146,14 +155,14 @@ class MainActivity : AppCompatActivity() {
val startingStack = buildList { val startingStack = buildList {
add(Route.Tunnels) add(Route.Tunnels)
if (intent?.action == Intent.ACTION_APPLICATION_PREFERENCES) add(Route.Settings) 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()) val backStack = rememberNavBackStack(*startingStack.toTypedArray())
var previousRoute by remember { mutableStateOf<Route?>(null) } var previousRoute by remember { mutableStateOf<Route?>(null) }
val navController = val navController =
rememberNavController<NavKey>(backStack, appState.isLocationDisclosureShown) { rememberNavController<NavKey>(backStack, uiState.isLocationDisclosureShown) {
previousKey -> previousKey ->
previousRoute = previousKey as? Route previousRoute = previousKey as? Route
} }
@@ -189,10 +198,20 @@ class MainActivity : AppCompatActivity() {
vpnActivity.launch(VpnService.prepare(this@MainActivity)) vpnActivity.launch(VpnService.prepare(this@MainActivity))
} }
is GlobalSideEffect.Snackbar -> is GlobalSideEffect.Snackbar -> {
scope.launch { scope.launch {
snackbar.showSnackbar(sideEffect.message.asString(context)) snackbarState.showSnackbar(
SnackbarInfo(
message =
buildAnnotatedString {
append(sideEffect.message.asString(context))
},
type = sideEffect.type ?: SnackbarType.INFO,
durationMs = sideEffect.durationMs ?: 4000L,
)
)
} }
}
is GlobalSideEffect.Toast -> is GlobalSideEffect.Toast ->
scope.launch { context.showToast(sideEffect.message.asString(context)) } 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 { 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( CompositionLocalProvider(
LocalIsAndroidTV provides isTv, LocalIsAndroidTV provides isTv,
LocalSharedVm provides viewModel, LocalSharedVm provides viewModel,
LocalNavController provides navController, LocalNavController provides navController,
) { ) {
WireguardAutoTunnelTheme(theme = appState.theme) { WireguardAutoTunnelTheme(theme = uiState.theme) {
VpnDeniedDialog( VpnDeniedDialog(
showVpnPermissionDialog, showVpnPermissionDialog,
onDismiss = { 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) { if (showLock) {
PinManager.initialize(context = this@MainActivity) PinManager.initialize(context = this@MainActivity)
PinLockScreen() PinLockScreen()
@@ -236,14 +304,14 @@ class MainActivity : AppCompatActivity() {
} }
val navState by val navState by
currentRouteAsNavbarState( currentRouteAsNavbarState(
appState, uiState,
viewModel, viewModel,
currentRoute, currentRoute,
navController, navController,
) )
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
if (appState.settings.appMode == AppMode.LOCK_DOWN) { if (uiState.settings.appMode == AppMode.LOCK_DOWN) {
AppAlertBanner( AppAlertBanner(
stringResource(R.string.locked_down) stringResource(R.string.locked_down)
.uppercase(Locale.getDefault()), .uppercase(Locale.getDefault()),
@@ -254,14 +322,25 @@ class MainActivity : AppCompatActivity() {
} }
Scaffold( Scaffold(
snackbarHost = { snackbarHost = {
SnackbarHost(snackbar) { snackbarData -> snackbarState.SnackbarHost(
modifier =
Modifier.align(Alignment.BottomCenter)
.padding(
bottom =
if (LocalIsAndroidTV.current) 120.dp
else 80.dp
)
) { info ->
CustomSnackBar( CustomSnackBar(
snackbarData.visuals.message, message = info.message,
isRtl = false, type = info.type,
onDismiss = { snackbarState.dismissCurrent() },
containerColor = containerColor =
MaterialTheme.colorScheme.surfaceColorAtElevation( MaterialTheme.colorScheme.surfaceColorAtElevation(
2.dp 2.dp
), ),
modifier =
Modifier.wrapContentHeight(align = Alignment.Top),
) )
} }
}, },
@@ -269,7 +348,7 @@ class MainActivity : AppCompatActivity() {
bottomBar = { bottomBar = {
if (navState.showBottomItems) { if (navState.showBottomItems) {
BottomNavbar( BottomNavbar(
appState.isAutoTunnelActive, uiState.isAutoTunnelActive,
currentTab, currentTab,
onTabSelected = { tab -> onTabSelected = { tab ->
navController.popUpTo(tab.startRoute) navController.popUpTo(tab.startRoute)
@@ -331,7 +410,7 @@ class MainActivity : AppCompatActivity() {
} }
entry<Route.Tunnels> { TunnelsScreen() } entry<Route.Tunnels> { TunnelsScreen() }
entry<Route.Sort> { SortScreen() } entry<Route.Sort> { SortScreen() }
entry<Route.TunnelOptions> { key -> entry<Route.TunnelSettings> { key ->
val viewModel = val viewModel =
hiltViewModel< hiltViewModel<
TunnelViewModel, TunnelViewModel,
@@ -341,7 +420,7 @@ class MainActivity : AppCompatActivity() {
factory.create(key.id) factory.create(key.id)
} }
) )
TunnelOptionsScreen(viewModel) TunnelSettingsScreen(viewModel)
} }
entry<Route.SplitTunnel> { key -> entry<Route.SplitTunnel> { key ->
val viewModel = val viewModel =
@@ -388,9 +467,6 @@ class MainActivity : AppCompatActivity() {
AndroidIntegrationsScreen() AndroidIntegrationsScreen()
} }
entry<Route.Dns> { DnsSettingsScreen() } entry<Route.Dns> { DnsSettingsScreen() }
entry<Route.TunnelGlobals> { key ->
TunnelGlobalsScreen(key.id)
}
entry<Route.ConfigGlobal> { key -> entry<Route.ConfigGlobal> { key ->
val viewModel = val viewModel =
hiltViewModel< hiltViewModel<
@@ -415,6 +491,9 @@ class MainActivity : AppCompatActivity() {
) )
SplitTunnelScreen(viewModel) SplitTunnelScreen(viewModel)
} }
entry<Route.LockdownSettings> {
LockdownSettingsScreen()
}
entry<Route.ProxySettings> { ProxySettingsScreen() } entry<Route.ProxySettings> { ProxySettingsScreen() }
entry<Route.Appearance> { AppearanceScreen() } entry<Route.Appearance> { AppearanceScreen() }
entry<Route.Language> { LanguageScreen() } entry<Route.Language> { LanguageScreen() }
@@ -442,7 +521,6 @@ class MainActivity : AppCompatActivity() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
WireGuardAutoTunnel.setUiActive(true) WireGuardAutoTunnel.setUiActive(true)
networkMonitor.checkPermissionsAndUpdateState()
} }
override fun onPause() { override fun onPause() {
@@ -452,6 +530,9 @@ class MainActivity : AppCompatActivity() {
fun performBackup() = fun performBackup() =
lifecycleScope.launch { lifecycleScope.launch {
// reset active tuns before backup to prevent trying to start them without permission on
// restore
tunnelRepository.resetActiveTunnels()
roomBackup roomBackup
.database(appDatabase) .database(appDatabase)
.backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG) .backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG)
@@ -7,18 +7,15 @@ import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration import androidx.work.Configuration
import com.zaneschepke.logcatter.LogReader import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationMonitor 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.core.worker.ServiceWorker
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
@@ -43,8 +40,6 @@ class WireGuardAutoTunnel : Application(), Configuration.Provider {
@Inject lateinit var notificationMonitor: NotificationMonitor @Inject lateinit var notificationMonitor: NotificationMonitor
@Inject lateinit var tunnelManager: TunnelManager
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
instance = this instance = this
@@ -73,12 +68,6 @@ class WireGuardAutoTunnel : Application(), Configuration.Provider {
ServiceWorker.start(this) ServiceWorker.start(this)
} }
override fun onTerminate() {
applicationScope.cancel()
tunnelManager.setBackendMode(BackendMode.Inactive)
super.onTerminate()
}
companion object { companion object {
private val _uiActive = MutableStateFlow(false) private val _uiActive = MutableStateFlow(false)
@@ -6,6 +6,7 @@ import android.content.Intent
import com.zaneschepke.logcatter.LogReader import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -19,6 +20,8 @@ class RestartReceiver : BroadcastReceiver() {
@Inject lateinit var tunnelManager: TunnelManager @Inject lateinit var tunnelManager: TunnelManager
@Inject lateinit var appStateRepository: AppStateRepository
@Inject lateinit var logReader: LogReader @Inject lateinit var logReader: LogReader
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
@@ -33,6 +36,7 @@ class RestartReceiver : BroadcastReceiver() {
Intent.ACTION_MY_PACKAGE_REPLACED -> { Intent.ACTION_MY_PACKAGE_REPLACED -> {
tunnelManager.handleRestore() tunnelManager.handleRestore()
logReader.deleteAndClearLogs() logReader.deleteAndClearLogs()
appStateRepository.setShouldShowDonationSnackbar(true)
} }
} }
} }
@@ -32,7 +32,7 @@ constructor(
description = description =
StringValue.StringResource( StringValue.StringResource(
R.string.tunnel_error_template, R.string.tunnel_error_template,
error.toStringRes(), error.toStringValue(),
), ),
groupKey = NotificationManager.VPN_GROUP_KEY, groupKey = NotificationManager.VPN_GROUP_KEY,
) )
@@ -93,6 +93,7 @@ class AutoTunnelService : LifecycleService() {
override fun onDestroy() { override fun onDestroy() {
serviceManager.handleAutoTunnelServiceDestroy() serviceManager.handleAutoTunnelServiceDestroy()
networkMonitor.destroy()
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
super.onDestroy() super.onDestroy()
} }
@@ -106,9 +106,6 @@ abstract class BaseTunnel(
) { ) {
return Timber.w("Tunnel is already running: ${tunnelConfig.name}") return Timber.w("Tunnel is already running: ${tunnelConfig.name}")
} }
updateTunnelStatus(tunnelConfig.id, TunnelStatus.Starting)
val job = val job =
applicationScope.launch(ioDispatcher) { applicationScope.launch(ioDispatcher) {
try { try {
@@ -3,18 +3,23 @@ package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.wireguard.android.backend.Backend import com.wireguard.android.backend.Backend
import com.wireguard.android.backend.BackendException import com.wireguard.android.backend.BackendException
import com.wireguard.android.backend.Tunnel as WgTunnel import com.wireguard.android.backend.Tunnel as WgTunnel
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.di.Kernel import com.zaneschepke.wireguardautotunnel.di.Kernel
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException import com.zaneschepke.wireguardautotunnel.domain.events.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.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.domain.state.WireGuardStatistics import com.zaneschepke.wireguardautotunnel.domain.state.WireGuardStatistics
import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendCoreException import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendCoreException
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.regex.Pattern
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
@@ -30,14 +35,27 @@ class KernelTunnel
constructor( constructor(
@ApplicationScope applicationScope: CoroutineScope, @ApplicationScope applicationScope: CoroutineScope,
@IoDispatcher ioDispatcher: CoroutineDispatcher, @IoDispatcher ioDispatcher: CoroutineDispatcher,
private val runConfigHelper: RunConfigHelper,
@Kernel private val backend: Backend, @Kernel private val backend: Backend,
) : BaseTunnel(applicationScope, ioDispatcher) { ) : BaseTunnel(applicationScope, ioDispatcher) {
private val runtimeTunnels = ConcurrentHashMap<Int, WgTunnel>() 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 { 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>() val stateChannel = Channel<WgTunnel.State>()
@@ -51,21 +69,22 @@ constructor(
try { try {
withTimeout(STARTUP_TIMEOUT_MS) { withTimeout(STARTUP_TIMEOUT_MS) {
updateTunnelStatus(tunnelConfig.id, TunnelStatus.Starting) 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) { } catch (e: TimeoutCancellationException) {
Timber.e("Startup timed out for ${tunnelConfig.name}") Timber.e("Startup timed out for ${tunnelConfig.name}")
errors.emit(tunnelConfig.name to BackendCoreException.DNS) errors.emit(tunnelConfig.name to DnsFailure())
forceStopTunnel(tunnelConfig.id) forceStopTunnel(tunnelConfig.id)
close() close()
} catch (e: BackendException) { } catch (e: BackendException) {
close(e.toBackendCoreException()) close(e.toBackendCoreException())
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
Timber.e(e, "Invalid backend arguments") Timber.e(e, "Invalid backend arguments")
close(BackendCoreException.Config) close(InvalidConfig())
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "Error while setting tunnel state") Timber.e(e, "Error while setting tunnel state")
close(BackendCoreException.Unknown) close(UnknownError())
} }
awaitClose { 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 isIpv4ResolutionPreferred() = tunnelConfig.isIpv4Preferred
override fun isMetered() = tunnelConfig.isMetered
} }
@@ -1,18 +1,19 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig as Entity
import com.zaneschepke.wireguardautotunnel.data.model.AppMode import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.di.* import com.zaneschepke.wireguardautotunnel.di.*
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException
import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage
import com.zaneschepke.wireguardautotunnel.domain.events.NotAuthorized
import com.zaneschepke.wireguardautotunnel.domain.model.AutoTunnelSettings import com.zaneschepke.wireguardautotunnel.domain.model.AutoTunnelSettings
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.LockdownSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.state.LogHealthState import com.zaneschepke.wireguardautotunnel.domain.state.LogHealthState
import com.zaneschepke.wireguardautotunnel.domain.state.PingState import com.zaneschepke.wireguardautotunnel.domain.state.PingState
@@ -39,6 +40,7 @@ constructor(
private val serviceManager: ServiceManager, private val serviceManager: ServiceManager,
private val settingsRepository: GeneralSettingRepository, private val settingsRepository: GeneralSettingRepository,
private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository, private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
private val lockdownSettingsRepository: LockdownSettingsRepository,
private val tunnelsRepository: TunnelRepository, private val tunnelsRepository: TunnelRepository,
private val tunnelMonitor: TunnelMonitor, private val tunnelMonitor: TunnelMonitor,
@ApplicationScope private val applicationScope: CoroutineScope, @ApplicationScope private val applicationScope: CoroutineScope,
@@ -69,12 +71,6 @@ constructor(
val condition: (SideEffectState) -> Boolean, val condition: (SideEffectState) -> Boolean,
) )
private suspend fun getSettings(): GeneralSettings =
settingsRepository.flow.filterNotNull().first { it != GeneralSettings() }
private suspend fun getTunnels(): List<TunnelConfig> =
tunnelsRepository.flow.first { it.isNotEmpty() }
private val tunnelProviderFlow: StateFlow<TunnelProvider> = run { private val tunnelProviderFlow: StateFlow<TunnelProvider> = run {
val currentBackend = AtomicReference(userspaceTunnel) val currentBackend = AtomicReference(userspaceTunnel)
val currentSettings = AtomicReference(GeneralSettings()) val currentSettings = AtomicReference(GeneralSettings())
@@ -84,10 +80,7 @@ constructor(
.filterNotNull() .filterNotNull()
// ignore default state // ignore default state
.filterNot { it == GeneralSettings() } .filterNot { it == GeneralSettings() }
.distinctUntilChanged { old, new -> .distinctUntilChangedBy { it.appMode }
old.appMode == new.appMode &&
old.isLanOnKillSwitchEnabled == new.isLanOnKillSwitchEnabled
}
.map { settings -> .map { settings ->
Timber.d("App mode changes with ${settings.appMode}") Timber.d("App mode changes with ${settings.appMode}")
val backend = val backend =
@@ -108,7 +101,7 @@ constructor(
handleModeChangeCleanup(previousBackend, previousSettings.appMode) handleModeChangeCleanup(previousBackend, previousSettings.appMode)
} }
if (settings.appMode == AppMode.LOCK_DOWN) { if (settings.appMode == AppMode.LOCK_DOWN) {
handleLockDownModeInit(settings.isLanOnKillSwitchEnabled) handleLockDownModeInit()
} }
} }
.map { (_, backend) -> backend } .map { (_, backend) -> backend }
@@ -235,17 +228,7 @@ constructor(
activeTunnels.first { it.isEmpty() } activeTunnels.first { it.isEmpty() }
} ?: run { activeTunnels.value.keys.forEach { id -> provider.forceStopTunnel(id) } } } ?: run { activeTunnels.value.keys.forEach { id -> provider.forceStopTunnel(id) } }
} }
val runConfig = tunnelProviderFlow.value.startTunnel(tunnelConfig)
tunnelConfig.run {
if (getSettings().isTunnelGlobalsEnabled) {
val globalTunnel =
getTunnels().firstOrNull { it.name == Entity.GLOBAL_CONFIG_NAME }
?: return@run this
return@run copyWithGlobalValues(globalTunnel)
}
this
}
tunnelProviderFlow.value.startTunnel(runConfig)
} }
override suspend fun stopTunnel(tunnelId: Int) { override suspend fun stopTunnel(tunnelId: Int) {
@@ -302,13 +285,23 @@ constructor(
serviceManager.updateTunnelTile() serviceManager.updateTunnelTile()
} }
private fun handleLockDownModeInit(withLanBypass: Boolean) { // TODO this can crash if we haven't started foreground service yet, especially for
val allowedIps = if (withLanBypass) TunnelConfig.IPV4_PUBLIC_NETWORKS else emptySet() // workerManager
private suspend fun handleLockDownModeInit() {
val lockdownSettings = lockdownSettingsRepository.getLockdownSettings()
val allowedIps =
if (lockdownSettings.bypassLan) TunnelConfig.IPV4_PUBLIC_NETWORKS else emptySet()
try { try {
if (serviceManager.hasVpnPermission()) { if (serviceManager.hasVpnPermission()) {
proxyUserspaceTunnel.setBackendMode(BackendMode.KillSwitch(allowedIps)) proxyUserspaceTunnel.setBackendMode(
BackendMode.KillSwitch(
allowedIps,
lockdownSettings.metered,
lockdownSettings.dualStack,
)
)
} else { } else {
throw BackendCoreException.NotAuthorized throw NotAuthorized()
} }
} catch (e: BackendCoreException) { } catch (e: BackendCoreException) {
localErrorEvents.tryEmit(null to e) localErrorEvents.tryEmit(null to e)
@@ -325,18 +318,6 @@ constructor(
proxyUserspaceTunnel.setBackendMode(BackendMode.Inactive) 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() = suspend fun handleRestore() =
withContext(ioDispatcher) { withContext(ioDispatcher) {
val settings = settingsRepository.getGeneralSettings() val settings = settingsRepository.getGeneralSettings()
@@ -344,20 +325,19 @@ constructor(
val tunnels = tunnelsRepository.getAll() val tunnels = tunnelsRepository.getAll()
if (autoTunnelSettings.isAutoTunnelEnabled) if (autoTunnelSettings.isAutoTunnelEnabled)
return@withContext restoreAutoTunnel(autoTunnelSettings) return@withContext restoreAutoTunnel(autoTunnelSettings)
if (isVpnAuthorized(settings.appMode)) { if (settings.appMode == AppMode.LOCK_DOWN) handleLockDownModeInit()
when (val mode = settings.appMode) { 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.VPN,
AppMode.PROXY, AppMode.PROXY,
AppMode.LOCK_DOWN -> { AppMode.LOCK_DOWN -> {
if (mode == AppMode.LOCK_DOWN)
handleLockDownModeInit(settings.isLanOnKillSwitchEnabled)
tunnels.firstOrNull { it.isActive }?.let { startTunnel(it) } tunnels.firstOrNull { it.isActive }?.let { startTunnel(it) }
} }
AppMode.KERNEL -> AppMode.KERNEL ->
tunnels.filter { it.isActive }.forEach { conf -> startTunnel(conf) } tunnels.filter { it.isActive }.forEach { conf -> startTunnel(conf) }
} }
} else {
localErrorEvents.emit(null to BackendCoreException.NotAuthorized)
} }
} }
@@ -375,18 +355,15 @@ constructor(
return@withContext restoreAutoTunnel(autoTunnelSettings) return@withContext restoreAutoTunnel(autoTunnelSettings)
if (settings.isRestoreOnBootEnabled) { if (settings.isRestoreOnBootEnabled) {
tunnelsRepository.resetActiveTunnels() tunnelsRepository.resetActiveTunnels()
if (isVpnAuthorized(settings.appMode)) { when (settings.appMode) {
when (val mode = settings.appMode) { AppMode.LOCK_DOWN -> handleLockDownModeInit()
AppMode.LOCK_DOWN -> AppMode.VPN ->
handleLockDownModeInit(settings.isLanOnKillSwitchEnabled) if (!serviceManager.hasVpnPermission())
AppMode.KERNEL, return@withContext localErrorEvents.emit(null to NotAuthorized())
AppMode.VPN, AppMode.KERNEL,
AppMode.PROXY -> Unit AppMode.PROXY -> Unit
}
defaultTunnel?.let { startTunnel(it) }
} else {
localErrorEvents.emit(null to BackendCoreException.NotAuthorized)
} }
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( private suspend fun handleDynamicDnsMonitoring(
activeTuns: Map<Int, TunnelState>, activeTuns: Map<Int, TunnelState>,
configs: List<TunnelConfig>, configs: List<TunnelConfig>,
@@ -536,5 +553,6 @@ constructor(
companion object { companion object {
const val BASE_BACKOFF = 30_000L const val BASE_BACKOFF = 30_000L
const val MAX_BACKOFF_TIME = 300_000L const val MAX_BACKOFF_TIME = 300_000L
const val RESTART_TUNNEL_DELAY = 300L
} }
} }
@@ -1,5 +1,6 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel package com.zaneschepke.wireguardautotunnel.core.tunnel
import android.os.PowerManager
import com.zaneschepke.logcatter.LogReader import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.networkmonitor.NetworkMonitor import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.data.model.AppMode import com.zaneschepke.wireguardautotunnel.data.model.AppMode
@@ -11,17 +12,17 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.state.* import com.zaneschepke.wireguardautotunnel.domain.state.*
import com.zaneschepke.wireguardautotunnel.util.extensions.toMillis import com.zaneschepke.wireguardautotunnel.util.extensions.toMillis
import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils
import dagger.hilt.android.scopes.ServiceScoped
import inet.ipaddr.AddressValueException import inet.ipaddr.AddressValueException
import inet.ipaddr.IPAddress import inet.ipaddr.IPAddress
import inet.ipaddr.IPAddressString import inet.ipaddr.IPAddressString
import io.ktor.util.collections.* import io.ktor.util.collections.*
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import timber.log.Timber import timber.log.Timber
@ServiceScoped @Singleton
class TunnelMonitor class TunnelMonitor
@Inject @Inject
constructor( constructor(
@@ -31,6 +32,7 @@ constructor(
private val networkMonitor: NetworkMonitor, private val networkMonitor: NetworkMonitor,
private val networkUtils: NetworkUtils, private val networkUtils: NetworkUtils,
private val logReader: LogReader, private val logReader: LogReader,
private val powerManager: PowerManager,
) { ) {
@OptIn(FlowPreview::class) @OptIn(FlowPreview::class)
@@ -74,7 +76,7 @@ constructor(
else -> null else -> null
} }
} }
.distinctUntilChangedBy { it.isHealthy } // Only emit when health changes .distinctUntilChangedBy { it.isHealthy }
.collect { logHealthState -> .collect { logHealthState ->
Timber.d("Tunnel log health updated for ${tunnelConfig.name}: $logHealthState") Timber.d("Tunnel log health updated for ${tunnelConfig.name}: $logHealthState")
updateTunnelStatus(tunnelConfig.id, null, null, null, logHealthState) updateTunnelStatus(tunnelConfig.id, null, null, null, logHealthState)
@@ -199,6 +201,7 @@ constructor(
} }
val attemptTime = System.currentTimeMillis() val attemptTime = System.currentTimeMillis()
val timeout = settings.tunnelPingTimeoutSeconds?.toMillis() ?: 5000L
runCatching { runCatching {
withTimeout( withTimeout(
settings.tunnelPingTimeoutSeconds?.toMillis() ?: 5000L settings.tunnelPingTimeoutSeconds?.toMillis() ?: 5000L
@@ -270,20 +273,28 @@ constructor(
while (isActive) { while (isActive) {
ensureActive() ensureActive()
if (isNetworkConnected.value) { if (!powerManager.isDeviceIdleMode) {
performPing() if (isNetworkConnected.value) {
} else { performPing()
pingStatsFlow.update { current -> } else {
current.mapValues { entry -> pingStatsFlow.update { current ->
entry.value.copy( current.mapValues { entry ->
isReachable = false, entry.value.copy(
failureReason = FailureReason.NoConnectivity, isReachable = false,
lastPingAttemptMillis = System.currentTimeMillis(), 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()) delay(settings.tunnelPingIntervalSeconds.toMillis())
} }
@@ -300,9 +311,11 @@ constructor(
) = coroutineScope { ) = coroutineScope {
while (isActive) { while (isActive) {
ensureActive() ensureActive()
val stats = getStatistics(tunnelId) if (!powerManager.isDeviceIdleMode) {
ensureActive() val stats = getStatistics(tunnelId)
updateTunnelStatus(tunnelId, null, stats, null, null) ensureActive()
updateTunnelStatus(tunnelId, null, stats, null, null)
}
delay(STATS_DELAY) delay(STATS_DELAY)
} }
} }
@@ -1,15 +1,11 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.data.model.DnsProtocol
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException import com.zaneschepke.wireguardautotunnel.domain.events.*
import com.zaneschepke.wireguardautotunnel.domain.model.ProxySettings
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig 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.AmneziaStatistics
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.util.extensions.asAmBackendMode 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.asTunnelState
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendCoreException import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendCoreException
import java.io.IOException import java.io.IOException
import java.util.*
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.* import kotlinx.coroutines.*
@@ -29,13 +24,7 @@ import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import org.amnezia.awg.backend.Backend import org.amnezia.awg.backend.Backend
import org.amnezia.awg.backend.BackendException import org.amnezia.awg.backend.BackendException
import org.amnezia.awg.backend.ProxyGoBackend
import org.amnezia.awg.backend.Tunnel as AwgTunnel 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 import timber.log.Timber
class UserspaceTunnel class UserspaceTunnel
@@ -43,9 +32,8 @@ class UserspaceTunnel
constructor( constructor(
@ApplicationScope applicationScope: CoroutineScope, @ApplicationScope applicationScope: CoroutineScope,
@IoDispatcher ioDispatcher: CoroutineDispatcher, @IoDispatcher ioDispatcher: CoroutineDispatcher,
private val proxySettingsRepository: ProxySettingsRepository,
private val dnsSettingsRepository: DnsSettingsRepository,
private val backend: Backend, private val backend: Backend,
private val runConfigHelper: RunConfigHelper,
) : BaseTunnel(applicationScope, ioDispatcher) { ) : BaseTunnel(applicationScope, ioDispatcher) {
private val runtimeTunnels = ConcurrentHashMap<Int, AwgTunnel>() private val runtimeTunnels = ConcurrentHashMap<Int, AwgTunnel>()
@@ -63,67 +51,21 @@ constructor(
try { try {
withTimeout(STARTUP_TIMEOUT_MS) { withTimeout(STARTUP_TIMEOUT_MS) {
updateTunnelStatus(tunnelConfig.id, TunnelStatus.Starting) updateTunnelStatus(tunnelConfig.id, TunnelStatus.Starting)
val runConfig = runConfigHelper.buildAmRunConfig(tunnelConfig)
val proxies: List<Proxy> = backend.setState(runtimeTunnel, AwgTunnel.State.UP, runConfig)
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)
} }
} catch (e: TimeoutCancellationException) { } catch (_: TimeoutCancellationException) {
Timber.e("Startup timed out for ${tunnelConfig.name} (likely DNS hang)") 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) forceStopTunnel(tunnelConfig.id)
close() close()
} catch (e: BackendException) { } catch (e: BackendException) {
close(e.toBackendCoreException()) close(e.toBackendCoreException())
} catch (e: IllegalArgumentException) { } catch (_: IllegalArgumentException) {
close(BackendCoreException.Config) close(InvalidConfig())
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "Error while setting tunnel state") Timber.e(e, "Error while setting tunnel state")
close(BackendCoreException.Unknown) close(UnknownError())
} }
awaitClose { awaitClose {
@@ -149,7 +91,7 @@ constructor(
throw e.toBackendCoreException() throw e.toBackendCoreException()
// TODO this should be mapped to BackendException in the lib // TODO this should be mapped to BackendException in the lib
} catch (e: IOException) { } catch (e: IOException) {
throw BackendCoreException.NotAuthorized throw VpnUnauthorized()
} }
} }
@@ -158,7 +100,7 @@ constructor(
} }
override fun handleDnsReresolve(tunnelConfig: TunnelConfig): Boolean { 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) return backend.resolveDDNS(tunnelConfig.toAmConfig(), tunnel.isIpv4ResolutionPreferred)
} }
@@ -15,8 +15,9 @@ import com.zaneschepke.wireguardautotunnel.data.entity.*
AutoTunnelSettings::class, AutoTunnelSettings::class,
MonitoringSettings::class, MonitoringSettings::class,
DnsSettings::class, DnsSettings::class,
LockdownSettings::class,
], ],
version = 25, version = 28,
autoMigrations = autoMigrations =
[ [
AutoMigration(from = 1, to = 2), AutoMigration(from = 1, to = 2),
@@ -42,6 +43,8 @@ import com.zaneschepke.wireguardautotunnel.data.entity.*
AutoMigration(from = 21, to = 22), AutoMigration(from = 21, to = 22),
AutoMigration(from = 22, to = 23), AutoMigration(from = 22, to = 23),
AutoMigration(from = 24, to = 25), AutoMigration(from = 24, to = 25),
AutoMigration(from = 26, to = 27, spec = GlobalsMigration::class),
AutoMigration(from = 27, to = 28, spec = DonationMigration::class),
], ],
exportSchema = true, exportSchema = true,
) )
@@ -57,6 +60,8 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun monitoringSettingsDao(): MonitoringSettingsDao abstract fun monitoringSettingsDao(): MonitoringSettingsDao
abstract fun lockdownSettingsDao(): LockdownSettingsDao
abstract fun dnsSettingsDao(): DnsSettingsDao 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 { companion object {
val locationDisclosureShown = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN") val locationDisclosureShown = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN")
val batteryDisableShown = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN") val batteryDisableShown = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN")
val shouldShowDonationSnackbar = booleanPreferencesKey("SHOW_DONATION_SNACK")
} }
suspend fun init() { 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( data class AppState(
val isLocationDisclosureShown: Boolean = false, val isLocationDisclosureShown: Boolean = false,
val isBatteryOptimizationDisableShown: Boolean = false, val isBatteryOptimizationDisableShown: Boolean = false,
val shouldShowDonationSnackbar: Boolean = false,
) )
@@ -11,4 +11,6 @@ data class DnsSettings(
@ColumnInfo(name = "dns_protocol", defaultValue = "0") @ColumnInfo(name = "dns_protocol", defaultValue = "0")
val dnsProtocol: DnsProtocol = DnsProtocol.fromValue(0), val dnsProtocol: DnsProtocol = DnsProtocol.fromValue(0),
@ColumnInfo(name = "dns_endpoint") val dnsEndpoint: String? = null, @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, val isRestoreOnBootEnabled: Boolean = false,
@ColumnInfo(name = "is_multi_tunnel_enabled", defaultValue = "0") @ColumnInfo(name = "is_multi_tunnel_enabled", defaultValue = "0")
val isMultiTunnelEnabled: Boolean = false, val isMultiTunnelEnabled: Boolean = false,
@ColumnInfo(name = "is_tunnel_globals_enabled", defaultValue = "0") @ColumnInfo(name = "global_split_tunnel_enabled", defaultValue = "0")
val isTunnelGlobalsEnabled: Boolean = false, val isGlobalSplitTunnelEnabled: Boolean = false,
@ColumnInfo(name = "app_mode", defaultValue = "0") val appMode: AppMode = AppMode.fromValue(0), @ColumnInfo(name = "app_mode", defaultValue = "0") val appMode: AppMode = AppMode.fromValue(0),
@ColumnInfo(name = "theme", defaultValue = "AUTOMATIC") val theme: String = "AUTOMATIC", @ColumnInfo(name = "theme", defaultValue = "AUTOMATIC") val theme: String = "AUTOMATIC",
@ColumnInfo(name = "locale") val locale: String? = null, @ColumnInfo(name = "locale") val locale: String? = null,
@@ -26,8 +26,5 @@ data class GeneralSettings(
val isPinLockEnabled: Boolean = false, val isPinLockEnabled: Boolean = false,
@ColumnInfo(name = "is_always_on_vpn_enabled", defaultValue = "0") @ColumnInfo(name = "is_always_on_vpn_enabled", defaultValue = "0")
val isAlwaysOnVpnEnabled: Boolean = false, val isAlwaysOnVpnEnabled: Boolean = false,
@ColumnInfo(name = "is_lan_on_kill_switch_enabled", defaultValue = "0") @ColumnInfo(name = "already_donated", defaultValue = "0") val alreadyDonated: Boolean = false,
val isLanOnKillSwitchEnabled: Boolean = false,
@ColumnInfo(name = "custom_split_packages", defaultValue = "{}")
val customSplitPackages: Map<String, String> = emptyMap(),
) )
@@ -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 = "position", defaultValue = "0") val position: Int = 0,
@ColumnInfo(name = "auto_tunnel_apps", defaultValue = "[]") @ColumnInfo(name = "auto_tunnel_apps", defaultValue = "[]")
val autoTunnelApps: Set<String> = emptySet(), val autoTunnelApps: Set<String> = emptySet(),
@ColumnInfo(name = "is_metered", defaultValue = "true") val isMetered: Boolean = true,
) { ) {
companion object { companion object {
const val GLOBAL_CONFIG_NAME = "4675ab06-903a-438b-8485-6ea4187a9512" 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 import com.zaneschepke.wireguardautotunnel.domain.model.DnsSettings as Domain
fun Entity.toDomain(): 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 = 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( Domain(
isLocationDisclosureShown = isLocationDisclosureShown, isLocationDisclosureShown = isLocationDisclosureShown,
isBatteryOptimizationDisableShown = isBatteryOptimizationDisableShown, 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, isShortcutsEnabled = isShortcutsEnabled,
isRestoreOnBootEnabled = isRestoreOnBootEnabled, isRestoreOnBootEnabled = isRestoreOnBootEnabled,
isMultiTunnelEnabled = isMultiTunnelEnabled, isMultiTunnelEnabled = isMultiTunnelEnabled,
isTunnelGlobalsEnabled = isTunnelGlobalsEnabled, isGlobalSplitTunnelEnabled = isGlobalSplitTunnelEnabled,
appMode = appMode, appMode = appMode,
theme = Theme.valueOf(theme.uppercase()), theme = Theme.valueOf(theme.uppercase()),
locale = locale, locale = locale,
@@ -18,8 +18,7 @@ fun Entity.toDomain(): Domain =
isRemoteControlEnabled = isRemoteControlEnabled, isRemoteControlEnabled = isRemoteControlEnabled,
isPinLockEnabled = isPinLockEnabled, isPinLockEnabled = isPinLockEnabled,
isAlwaysOnVpnEnabled = isAlwaysOnVpnEnabled, isAlwaysOnVpnEnabled = isAlwaysOnVpnEnabled,
isLanOnKillSwitchEnabled = isLanOnKillSwitchEnabled, alreadyDonated = alreadyDonated,
customSplitPackages = customSplitPackages,
) )
fun Domain.toEntity(): Entity = fun Domain.toEntity(): Entity =
@@ -28,7 +27,7 @@ fun Domain.toEntity(): Entity =
isShortcutsEnabled = isShortcutsEnabled, isShortcutsEnabled = isShortcutsEnabled,
isRestoreOnBootEnabled = isRestoreOnBootEnabled, isRestoreOnBootEnabled = isRestoreOnBootEnabled,
isMultiTunnelEnabled = isMultiTunnelEnabled, isMultiTunnelEnabled = isMultiTunnelEnabled,
isTunnelGlobalsEnabled = isTunnelGlobalsEnabled, isGlobalSplitTunnelEnabled = isGlobalSplitTunnelEnabled,
appMode = appMode, appMode = appMode,
theme = theme.name, theme = theme.name,
locale = locale, locale = locale,
@@ -36,6 +35,5 @@ fun Domain.toEntity(): Entity =
isRemoteControlEnabled = isRemoteControlEnabled, isRemoteControlEnabled = isRemoteControlEnabled,
isPinLockEnabled = isPinLockEnabled, isPinLockEnabled = isPinLockEnabled,
isAlwaysOnVpnEnabled = isAlwaysOnVpnEnabled, isAlwaysOnVpnEnabled = isAlwaysOnVpnEnabled,
isLanOnKillSwitchEnabled = isLanOnKillSwitchEnabled, alreadyDonated = alreadyDonated,
customSplitPackages = customSplitPackages,
) )
@@ -19,6 +19,7 @@ fun Entity.toDomain(): Domain =
isIpv4Preferred = isIpv4Preferred, isIpv4Preferred = isIpv4Preferred,
position = position, position = position,
autoTunnelApps = autoTunnelApps, autoTunnelApps = autoTunnelApps,
isMetered = isMetered,
) )
fun Domain.toEntity(): Entity = fun Domain.toEntity(): Entity =
@@ -37,4 +38,5 @@ fun Domain.toEntity(): Entity =
isIpv4Preferred = isIpv4Preferred, isIpv4Preferred = isIpv4Preferred,
position = position, position = position,
autoTunnelApps = autoTunnelApps, 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( data class DnsSettings(val protocol: DnsProtocol = DnsProtocol.SYSTEM, val endpoint: String? = null)
val protocol: DnsProtocol = DnsProtocol.SYSTEM,
val endpoint: String? = null,
)
enum class DnsProvider(private val systemAddress: String, private val dohAddress: String) { enum class DnsProvider(private val systemAddress: String, private val dohAddress: String) {
CLOUDFLARE("1.1.1.1", "https://1.1.1.1/dns-query"), CLOUDFLARE("1.1.1.1", "https://1.1.1.1/dns-query"),
@@ -37,6 +37,14 @@ class DataStoreAppStateRepository(
dataStoreManager.saveToDataStore(DataStoreManager.batteryDisableShown, shown) 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> = override val flow: Flow<Domain> =
dataStoreManager.preferencesFlow dataStoreManager.preferencesFlow
.map { prefs -> .map { prefs ->
@@ -38,27 +38,40 @@ class GitHubUpdateRepository(
gitHubApi.getLatestRelease(githubOwner, githubRepo).onFailure(Timber::e) gitHubApi.getLatestRelease(githubOwner, githubRepo).onFailure(Timber::e)
} }
release.map { release -> release.map { release ->
val standaloneApkAsset = val universalApkAsset =
release.assets.find { asset -> release.assets.find { asset ->
asset.name.startsWith("wgtunnel-${Constants.STANDALONE_FLAVOR}-v") && val prefix = "wgtunnel-${Constants.STANDALONE_FLAVOR}-v"
asset.name.endsWith(".apk") val apkSuffix = ".apk"
asset.name.startsWith(prefix) &&
asset.name.endsWith(apkSuffix) &&
!asset.name.endsWith("-arm64$apkSuffix") &&
!asset.name.endsWith("-armv7$apkSuffix")
} }
val newVersion = val newVersion =
standaloneApkAsset universalApkAsset
?.name ?.name
?.removePrefix("wgtunnel-${Constants.STANDALONE_FLAVOR}-v") ?.removePrefix("wgtunnel-${Constants.STANDALONE_FLAVOR}-v")
?.removeSuffix(".apk") ?: return@map null ?.removeSuffix(".apk") ?: return@map null
Timber.i("Latest version: $newVersion, current version: $currentVersion") Timber.i("Latest version: $newVersion, current version: $currentVersion")
if (isNightly && newVersion != currentVersion) if (isNightly) {
return@map GitHubReleaseMapper.toAppUpdate(release, newVersion) if (newVersion != currentVersion) {
if (NumberUtils.compareVersions(newVersion, currentVersion) > 0) { GitHubReleaseMapper.toAppUpdate(
GitHubReleaseMapper.toAppUpdate( release.copy(assets = listOf(universalApkAsset)),
release.copy(assets = listOf(standaloneApkAsset)), newVersion,
newVersion, )
) } else {
null
}
} else { } 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 package com.zaneschepke.wireguardautotunnel.data.repository
import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager import android.content.pm.PackageManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.model.InstalledPackage import com.zaneschepke.wireguardautotunnel.domain.model.InstalledPackage
import com.zaneschepke.wireguardautotunnel.domain.repository.InstalledPackageRepository import com.zaneschepke.wireguardautotunnel.domain.repository.InstalledPackageRepository
import com.zaneschepke.wireguardautotunnel.util.extensions.getAllInternetCapablePackages
import com.zaneschepke.wireguardautotunnel.util.extensions.getFriendlyAppName import com.zaneschepke.wireguardautotunnel.util.extensions.getFriendlyAppName
import javax.inject.Singleton import javax.inject.Singleton
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
@@ -27,32 +22,6 @@ class InstalledAndroidPackageRepository(
private var cachedPackages: List<InstalledPackage>? = null 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> = override suspend fun getInstalledPackages(): List<InstalledPackage> =
withContext(ioDispatcher) { withContext(ioDispatcher) {
cachedPackages?.let { cachedPackages?.let {
@@ -63,7 +32,7 @@ class InstalledAndroidPackageRepository(
override suspend fun refreshInstalledPackages(): List<InstalledPackage> = override suspend fun refreshInstalledPackages(): List<InstalledPackage> =
withContext(ioDispatcher) { withContext(ioDispatcher) {
val packages = context.getAllInternetCapablePackages() val packages = context.packageManager.getInstalledPackages(0)
val installedPackages = val installedPackages =
packages.mapNotNull { packageInfo -> 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 = override val flow =
proxySettingsDao proxySettingsDao
.getProxySettingsFlow() .getProxySettingsFlow()
.flowOn(ioDispatcher)
.map { (it ?: Entity()).toDomain() } .map { (it ?: Entity()).toDomain() }
.flowOn(ioDispatcher) .flowOn(ioDispatcher)
@@ -1,6 +1,7 @@
package com.zaneschepke.wireguardautotunnel.di package com.zaneschepke.wireguardautotunnel.di
import android.content.Context import android.content.Context
import android.os.PowerManager
import com.zaneschepke.logcatter.LogReader import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.logcatter.LogcatReader import com.zaneschepke.logcatter.LogcatReader
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
@@ -66,4 +67,9 @@ class AppModule {
): NotificationMonitor { ): NotificationMonitor {
return NotificationMonitor(tunnelManager, notificationManager) 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.DatabaseCallback
import com.zaneschepke.wireguardautotunnel.data.dao.* import com.zaneschepke.wireguardautotunnel.data.dao.*
import com.zaneschepke.wireguardautotunnel.data.migrations.MIGRATION_23_24 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.GitHubApi
import com.zaneschepke.wireguardautotunnel.data.network.KtorClient import com.zaneschepke.wireguardautotunnel.data.network.KtorClient
import com.zaneschepke.wireguardautotunnel.data.network.KtorGitHubApi import com.zaneschepke.wireguardautotunnel.data.network.KtorGitHubApi
@@ -56,6 +57,7 @@ class RepositoryModule {
context.getString(R.string.db_name), context.getString(R.string.db_name),
) )
.addMigrations(MIGRATION_23_24(dataStoreManager.dataStore)) .addMigrations(MIGRATION_23_24(dataStoreManager.dataStore))
.addMigrations(MIGRATION_25_26)
.fallbackToDestructiveMigration(true) .fallbackToDestructiveMigration(true)
.addCallback(callback) .addCallback(callback)
.build() .build()
@@ -67,6 +69,12 @@ class RepositoryModule {
return appDatabase.generalSettingsDao() return appDatabase.generalSettingsDao()
} }
@Singleton
@Provides
fun provideLockdownDoa(appDatabase: AppDatabase): LockdownSettingsDao {
return appDatabase.lockdownSettingsDao()
}
@Singleton @Singleton
@Provides @Provides
fun provideDnsSettingsDao(appDatabase: AppDatabase): DnsSettingsDao { fun provideDnsSettingsDao(appDatabase: AppDatabase): DnsSettingsDao {
@@ -106,6 +114,15 @@ class RepositoryModule {
return RoomTunnelRepository(tunnelConfigDao, ioDispatcher) return RoomTunnelRepository(tunnelConfigDao, ioDispatcher)
} }
@Singleton
@Provides
fun provideLockdownSettingsRepository(
lockdownSettingsDao: LockdownSettingsDao,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): LockdownSettingsRepository {
return RoomLockdownSettingsRepository(lockdownSettingsDao, ioDispatcher)
}
@Singleton @Singleton
@Provides @Provides
fun provideGeneralSettingsRepository( fun provideGeneralSettingsRepository(
@@ -1,6 +1,7 @@
package com.zaneschepke.wireguardautotunnel.di package com.zaneschepke.wireguardautotunnel.di
import android.content.Context import android.content.Context
import android.os.PowerManager
import com.wireguard.android.backend.WgQuickBackend import com.wireguard.android.backend.WgQuickBackend
import com.wireguard.android.util.RootShell import com.wireguard.android.util.RootShell
import com.wireguard.android.util.ToolsInstaller import com.wireguard.android.util.ToolsInstaller
@@ -85,8 +86,9 @@ class TunnelModule {
@ApplicationScope applicationScope: CoroutineScope, @ApplicationScope applicationScope: CoroutineScope,
@IoDispatcher ioDispatcher: CoroutineDispatcher, @IoDispatcher ioDispatcher: CoroutineDispatcher,
backend: com.wireguard.android.backend.Backend, backend: com.wireguard.android.backend.Backend,
runConfigHelper: RunConfigHelper,
): TunnelProvider { ): TunnelProvider {
return KernelTunnel(applicationScope, ioDispatcher, backend) return KernelTunnel(applicationScope, ioDispatcher, runConfigHelper, backend)
} }
@Provides @Provides
@@ -94,18 +96,11 @@ class TunnelModule {
@Userspace @Userspace
fun provideUserspaceProvider( fun provideUserspaceProvider(
@ApplicationScope applicationScope: CoroutineScope, @ApplicationScope applicationScope: CoroutineScope,
proxySettingsRepository: ProxySettingsRepository, runConfigHelper: RunConfigHelper,
dnsSettingsRepository: DnsSettingsRepository,
@Userspace backend: Backend, @Userspace backend: Backend,
@IoDispatcher ioDispatcher: CoroutineDispatcher, @IoDispatcher ioDispatcher: CoroutineDispatcher,
): TunnelProvider { ): TunnelProvider {
return UserspaceTunnel( return UserspaceTunnel(applicationScope, ioDispatcher, backend, runConfigHelper)
applicationScope,
ioDispatcher,
proxySettingsRepository,
dnsSettingsRepository,
backend,
)
} }
@Provides @Provides
@@ -113,18 +108,11 @@ class TunnelModule {
@ProxyUserspace @ProxyUserspace
fun provideProxyUserspaceProvider( fun provideProxyUserspaceProvider(
@ApplicationScope applicationScope: CoroutineScope, @ApplicationScope applicationScope: CoroutineScope,
dnsSettingsRepository: DnsSettingsRepository, runConfigHelper: RunConfigHelper,
proxySettingsRepository: ProxySettingsRepository,
@ProxyUserspace backend: Backend, @ProxyUserspace backend: Backend,
@IoDispatcher ioDispatcher: CoroutineDispatcher, @IoDispatcher ioDispatcher: CoroutineDispatcher,
): TunnelProvider { ): TunnelProvider {
return UserspaceTunnel( return UserspaceTunnel(applicationScope, ioDispatcher, backend, runConfigHelper)
applicationScope,
ioDispatcher,
proxySettingsRepository,
dnsSettingsRepository,
backend,
)
} }
@Provides @Provides
@@ -135,6 +123,7 @@ class TunnelModule {
@ProxyUserspace proxyTunnel: TunnelProvider, @ProxyUserspace proxyTunnel: TunnelProvider,
serviceManager: ServiceManager, serviceManager: ServiceManager,
tunnelRepository: TunnelRepository, tunnelRepository: TunnelRepository,
lockdownSettingsRepository: LockdownSettingsRepository,
settingsRepository: GeneralSettingRepository, settingsRepository: GeneralSettingRepository,
autoTunnelSettingsRepository: AutoTunnelSettingsRepository, autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
tunnelMonitor: TunnelMonitor, tunnelMonitor: TunnelMonitor,
@@ -148,6 +137,7 @@ class TunnelModule {
serviceManager, serviceManager,
settingsRepository, settingsRepository,
autoTunnelSettingsRepository, autoTunnelSettingsRepository,
lockdownSettingsRepository,
tunnelRepository, tunnelRepository,
tunnelMonitor, tunnelMonitor,
applicationScope, 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 @Provides
@Singleton @Singleton
fun provideNetworkMonitor( fun provideNetworkMonitor(
@@ -178,42 +224,4 @@ class TunnelModule {
applicationScope, 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 { sealed class BackendMode {
data object Inactive : 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 package com.zaneschepke.wireguardautotunnel.domain.events
import androidx.annotation.Keep
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
sealed class AutoTunnelEvent { 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 import com.zaneschepke.wireguardautotunnel.util.StringValue
sealed class BackendCoreException : Exception() { sealed class BackendCoreException : Exception() {
data object DNS : BackendCoreException() abstract val stringRes: Int
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
}
fun toStringValue(): StringValue { 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( data class AppState(
val isLocationDisclosureShown: Boolean = false, val isLocationDisclosureShown: Boolean = false,
val isBatteryOptimizationDisableShown: Boolean = false, val isBatteryOptimizationDisableShown: Boolean = false,
val shouldShowDonationSnackbar: Boolean = false,
) )
@@ -6,4 +6,5 @@ data class DnsSettings(
val id: Int = 0, val id: Int = 0,
val dnsProtocol: DnsProtocol = DnsProtocol.fromValue(0), val dnsProtocol: DnsProtocol = DnsProtocol.fromValue(0),
val dnsEndpoint: String? = null, val dnsEndpoint: String? = null,
val isGlobalTunnelDnsEnabled: Boolean = false,
) )
@@ -8,7 +8,7 @@ data class GeneralSettings(
val isShortcutsEnabled: Boolean = false, val isShortcutsEnabled: Boolean = false,
val isRestoreOnBootEnabled: Boolean = false, val isRestoreOnBootEnabled: Boolean = false,
val isMultiTunnelEnabled: Boolean = false, val isMultiTunnelEnabled: Boolean = false,
val isTunnelGlobalsEnabled: Boolean = false, val isGlobalSplitTunnelEnabled: Boolean = false,
val appMode: AppMode = AppMode.fromValue(0), val appMode: AppMode = AppMode.fromValue(0),
val theme: Theme = Theme.AUTOMATIC, val theme: Theme = Theme.AUTOMATIC,
val locale: String? = null, val locale: String? = null,
@@ -16,6 +16,6 @@ data class GeneralSettings(
val isRemoteControlEnabled: Boolean = false, val isRemoteControlEnabled: Boolean = false,
val isPinLockEnabled: Boolean = false, val isPinLockEnabled: Boolean = false,
val isAlwaysOnVpnEnabled: Boolean = false, val isAlwaysOnVpnEnabled: Boolean = false,
val isLanOnKillSwitchEnabled: Boolean = false, val isKillSwitchMetered: Boolean = true,
val customSplitPackages: Map<String, String> = emptyMap(), 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 isIpv4Preferred: Boolean = true,
val position: Int = 0, val position: Int = 0,
val autoTunnelApps: Set<String> = setOf(), val autoTunnelApps: Set<String> = setOf(),
val isMetered: Boolean = true,
) { ) {
val isNameKernelCompatible: Boolean = (name.length <= 15)
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
@@ -43,7 +43,8 @@ data class TunnelConfig(
pingTarget == other.pingTarget && pingTarget == other.pingTarget &&
restartOnPingFailure == other.restartOnPingFailure && restartOnPingFailure == other.restartOnPingFailure &&
tunnelNetworks == other.tunnelNetworks && tunnelNetworks == other.tunnelNetworks &&
isIpv4Preferred == other.isIpv4Preferred isIpv4Preferred == other.isIpv4Preferred &&
isMetered == other.isMetered
} }
override fun hashCode(): Int { override fun hashCode(): Int {
@@ -66,7 +67,11 @@ data class TunnelConfig(
return configFromWgQuick(wgQuick) return configFromWgQuick(wgQuick)
} }
fun copyWithGlobalValues(globalTunnel: TunnelConfig): TunnelConfig { fun copyWithGlobalValues(
globalTunnel: TunnelConfig,
includeDns: Boolean,
includeSpitTunneling: Boolean,
): TunnelConfig {
val existingConfig = toAmConfig() val existingConfig = toAmConfig()
val globalConfig = globalTunnel.toAmConfig() val globalConfig = globalTunnel.toAmConfig()
@@ -115,62 +120,14 @@ data class TunnelConfig(
setPreDown(existingConfig.`interface`.preDown) setPreDown(existingConfig.`interface`.preDown)
setPostDown(existingConfig.`interface`.postDown) setPostDown(existingConfig.`interface`.postDown)
globalConfig.`interface`.mtu.ifPresent { setMtu(it) } if (includeDns) {
if (globalConfig.`interface`.dnsServers.isNotEmpty()) {
setDnsServers(globalConfig.`interface`.dnsServers) setDnsServers(globalConfig.`interface`.dnsServers)
}
if (globalConfig.`interface`.dnsSearchDomains.isNotEmpty()) {
setDnsSearchDomains(globalConfig.`interface`.dnsSearchDomains) setDnsSearchDomains(globalConfig.`interface`.dnsSearchDomains)
} }
if (includeSpitTunneling) {
if (globalConfig.`interface`.excludedApplications.isNotEmpty()) {
setExcludedApplications(globalConfig.`interface`.excludedApplications) setExcludedApplications(globalConfig.`interface`.excludedApplications)
}
if (!globalConfig.`interface`.includedApplications.isEmpty()) {
setIncludedApplications(globalConfig.`interface`.includedApplications) setIncludedApplications(globalConfig.`interface`.includedApplications)
} }
if (globalConfig.`interface`.preUp.isNotEmpty()) {
setPreUp(globalConfig.`interface`.preUp)
}
if (globalConfig.`interface`.postUp.isNotEmpty()) {
setPostUp(globalConfig.`interface`.postUp)
}
if (globalConfig.`interface`.preDown.isNotEmpty()) {
setPreDown(globalConfig.`interface`.preDown)
}
if (globalConfig.`interface`.postDown.isNotEmpty()) {
setPostDown(globalConfig.`interface`.postDown)
}
globalConfig.`interface`.junkPacketCount.ifPresent { setJunkPacketCount(it) }
globalConfig.`interface`.junkPacketMinSize.ifPresent { setJunkPacketMinSize(it) }
globalConfig.`interface`.junkPacketMaxSize.ifPresent { setJunkPacketMaxSize(it) }
globalConfig.`interface`.initPacketJunkSize.ifPresent { setInitPacketJunkSize(it) }
globalConfig.`interface`.responsePacketJunkSize.ifPresent {
setResponsePacketJunkSize(it)
}
globalConfig.`interface`.initPacketMagicHeader.ifPresent {
setInitPacketMagicHeader(it)
}
globalConfig.`interface`.responsePacketMagicHeader.ifPresent {
setResponsePacketMagicHeader(it)
}
globalConfig.`interface`.underloadPacketMagicHeader.ifPresent {
setUnderloadPacketMagicHeader(it)
}
globalConfig.`interface`.transportPacketMagicHeader.ifPresent {
setTransportPacketMagicHeader(it)
}
globalConfig.`interface`.i1.ifPresent { setI1(it) }
globalConfig.`interface`.i2.ifPresent { setI2(it) }
globalConfig.`interface`.i3.ifPresent { setI3(it) }
globalConfig.`interface`.i4.ifPresent { setI4(it) }
globalConfig.`interface`.i5.ifPresent { setI5(it) }
globalConfig.`interface`.j1.ifPresent { setJ1(it) }
globalConfig.`interface`.j2.ifPresent { setJ2(it) }
globalConfig.`interface`.j3.ifPresent { setJ3(it) }
globalConfig.`interface`.itime.ifPresent { setItime(it) }
} }
val newInterface = newInterfaceBuilder.build() val newInterface = newInterfaceBuilder.build()
@@ -12,5 +12,9 @@ interface AppStateRepository {
suspend fun setBatteryOptimizationDisableShown(shown: Boolean) suspend fun setBatteryOptimizationDisableShown(shown: Boolean)
suspend fun setShouldShowDonationSnackbar(show: Boolean)
suspend fun shouldShowDonationSnackbar(): Boolean
val flow: Flow<AppState> 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.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarType
import com.zaneschepke.wireguardautotunnel.util.StringValue import com.zaneschepke.wireguardautotunnel.util.StringValue
import java.io.File import java.io.File
sealed class GlobalSideEffect { sealed class GlobalSideEffect {
data 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() 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 package com.zaneschepke.wireguardautotunnel.ui.common.button
import android.view.KeyEvent
import androidx.compose.animation.animateContentSize import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable 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.FocusRequester
import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.focus.focusRequester 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.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
import com.zaneschepke.wireguardautotunnel.ui.theme.Disabled
@Composable @Composable
fun SurfaceRow( fun SurfaceRow(
@@ -28,13 +34,14 @@ fun SurfaceRow(
onClick: (() -> Unit)? = null, onClick: (() -> Unit)? = null,
description: @Composable (() -> Unit)? = null, description: @Composable (() -> Unit)? = null,
expandedContent: @Composable (() -> Unit)? = null, expandedContent: @Composable (() -> Unit)? = null,
onLongClick: () -> Unit = {}, onLongClick: (() -> Unit)? = null,
enabled: Boolean = true, enabled: Boolean = true,
selected: Boolean = false, selected: Boolean = false,
leading: @Composable (() -> Unit)? = null, leading: @Composable (() -> Unit)? = null,
trailing: @Composable ((Modifier) -> Unit)? = null, trailing: @Composable ((Modifier) -> Unit)? = null,
) { ) {
val density = LocalDensity.current val density = LocalDensity.current
val isTv = LocalIsAndroidTV.current
var leadingPadding by remember { mutableStateOf(0.dp) } var leadingPadding by remember { mutableStateOf(0.dp) }
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
val mainFocusRequester = remember { FocusRequester() } val mainFocusRequester = remember { FocusRequester() }
@@ -44,7 +51,6 @@ fun SurfaceRow(
modifier = modifier =
modifier modifier
.fillMaxWidth() .fillMaxWidth()
// .focusGroup()
.indication(interactionSource, ripple()) .indication(interactionSource, ripple())
.background( .background(
if (!selected) MaterialTheme.colorScheme.surface if (!selected) MaterialTheme.colorScheme.surface
@@ -62,7 +68,20 @@ fun SurfaceRow(
) { ) {
Row( Row(
modifier = 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 { .focusProperties {
if (onClick != null) { if (onClick != null) {
right = trailingFocusRequester right = trailingFocusRequester
@@ -109,9 +128,7 @@ fun SurfaceRow(
Text( Text(
text = title, text = title,
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
color = color = if (enabled) MaterialTheme.colorScheme.onSurface else Disabled,
if (enabled) MaterialTheme.colorScheme.onSurface
else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f),
) )
if (description != null) { if (description != null) {
description() description()
@@ -27,7 +27,7 @@ fun SwitchWithDivider(
color = MaterialTheme.colorScheme.outline, color = MaterialTheme.colorScheme.outline,
) )
Box(modifier = Modifier.pointerInput(Unit) { detectTapGestures {} }) { Box(modifier = Modifier.pointerInput(Unit) { detectTapGestures {} }) {
ScaledSwitch( ThemedSwitch(
checked = checked, checked = checked,
onClick = onClick, onClick = onClick,
enabled = enabled, enabled = enabled,
@@ -5,9 +5,10 @@ import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults import androidx.compose.material3.SwitchDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import com.zaneschepke.wireguardautotunnel.ui.theme.Disabled
@Composable @Composable
fun ScaledSwitch( fun ThemedSwitch(
checked: Boolean, checked: Boolean,
onClick: (checked: Boolean) -> Unit, onClick: (checked: Boolean) -> Unit,
enabled: Boolean = true, enabled: Boolean = true,
@@ -21,12 +22,15 @@ fun ScaledSwitch(
colors = colors =
SwitchDefaults.colors() SwitchDefaults.colors()
.copy( .copy(
checkedThumbColor = MaterialTheme.colorScheme.background, checkedThumbColor = MaterialTheme.colorScheme.surface,
checkedIconColor = MaterialTheme.colorScheme.background, checkedIconColor = MaterialTheme.colorScheme.surface,
uncheckedTrackColor = MaterialTheme.colorScheme.surface, uncheckedTrackColor = MaterialTheme.colorScheme.surface,
uncheckedBorderColor = MaterialTheme.colorScheme.outline, uncheckedBorderColor = MaterialTheme.colorScheme.outline,
uncheckedThumbColor = MaterialTheme.colorScheme.outline, uncheckedThumbColor = MaterialTheme.colorScheme.outline,
uncheckedIconColor = 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.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
@@ -11,22 +12,26 @@ import com.zaneschepke.wireguardautotunnel.R
fun InfoDialog( fun InfoDialog(
onAttest: () -> Unit, onAttest: () -> Unit,
onDismiss: () -> Unit, onDismiss: () -> Unit,
title: @Composable () -> Unit, title: String,
body: @Composable () -> Unit, body: @Composable (() -> Unit),
confirmText: @Composable () -> Unit, confirmText: String,
modifier: Modifier = Modifier,
) { ) {
MaterialTheme(colorScheme = MaterialTheme.colorScheme.copy()) { MaterialTheme(colorScheme = MaterialTheme.colorScheme.copy()) {
Surface(color = MaterialTheme.colorScheme.surface, tonalElevation = 0.dp) { Surface(color = MaterialTheme.colorScheme.surface, tonalElevation = 0.dp) {
AlertDialog( AlertDialog(
modifier = modifier,
onDismissRequest = { onDismiss() }, onDismissRequest = { onDismiss() },
confirmButton = { TextButton(onClick = { onAttest() }) { confirmText() } }, confirmButton = {
TextButton(onClick = { onAttest() }) { Text(text = confirmText) }
},
dismissButton = { dismissButton = {
TextButton(onClick = { onDismiss() }) { TextButton(onClick = { onDismiss() }) {
Text(text = stringResource(R.string.cancel)) Text(text = stringResource(R.string.cancel))
} }
}, },
containerColor = MaterialTheme.colorScheme.surface, containerColor = MaterialTheme.colorScheme.surface,
title = { title() }, title = { Text(text = title) },
text = { body() }, text = { body() },
properties = DialogProperties(usePlatformDefaultWidth = true), properties = DialogProperties(usePlatformDefaultWidth = true),
) )
@@ -34,7 +34,7 @@ fun VpnDeniedDialog(show: Boolean, onDismiss: () -> Unit) {
InfoDialog( InfoDialog(
onDismiss = { onDismiss() }, onDismiss = { onDismiss() },
onAttest = { onDismiss() }, onAttest = { onDismiss() },
title = { Text(text = stringResource(R.string.vpn_denied_dialog_title)) }, title = stringResource(R.string.vpn_denied_dialog_title),
body = { body = {
Text( Text(
text = alwaysOnDescription, 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.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
@Composable @Composable
fun SheetOption( fun SheetOption(
@@ -50,9 +51,12 @@ fun SheetOption(
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun CustomBottomSheet(options: List<SheetOption>, onDismiss: () -> Unit) { fun CustomBottomSheet(options: List<SheetOption>, onDismiss: () -> Unit) {
val isTv = LocalIsAndroidTV.current
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = isTv)
ModalBottomSheet( ModalBottomSheet(
containerColor = MaterialTheme.colorScheme.surface, containerColor = MaterialTheme.colorScheme.surface,
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
sheetState = sheetState,
) { ) {
options.forEachIndexed { index, option -> options.forEachIndexed { index, option ->
SheetOption(option.label, option.leadingIcon, option.onClick, option.selected) SheetOption(option.label, option.leadingIcon, option.onClick, option.selected)
@@ -1,56 +1,93 @@
package com.zaneschepke.wireguardautotunnel.ui.common.snackbar package com.zaneschepke.wireguardautotunnel.ui.common.snackbar
import android.R.attr.padding
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons 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.material.icons.rounded.Info
import androidx.compose.material3.Icon import androidx.compose.material.icons.rounded.Warning
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.*
import androidx.compose.material3.Snackbar
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
@Composable @Composable
fun CustomSnackBar( fun CustomSnackBar(
message: String, message: AnnotatedString,
isRtl: Boolean = true, onDismiss: () -> Unit,
modifier: Modifier = Modifier,
type: SnackbarType = SnackbarType.INFO,
containerColor: Color = MaterialTheme.colorScheme.surface, containerColor: Color = MaterialTheme.colorScheme.surface,
) { ) {
val isTv = LocalIsAndroidTV.current 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( Snackbar(
containerColor = containerColor, 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), shape = RoundedCornerShape(16.dp),
) { ) {
CompositionLocalProvider( Row(
LocalLayoutDirection provides if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr modifier =
Modifier.fillMaxWidth()
.height(IntrinsicSize.Min)
.width(IntrinsicSize.Min)
.padding(vertical = 16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) { ) {
Row( Row(
modifier = Modifier.width(IntrinsicSize.Max).height(IntrinsicSize.Min), modifier = Modifier.fillMaxWidth().weight(1f),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start, horizontalArrangement = Arrangement.Start,
) { ) {
val icon = Icons.Rounded.Info
Icon( Icon(
icon, icon,
contentDescription = icon.name, contentDescription = iconDescription,
tint = MaterialTheme.colorScheme.onSurface, tint = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(end = 10.dp),
) )
Spacer(modifier = Modifier.width(16.dp))
Text( Text(
message, text = message,
color = MaterialTheme.colorScheme.onSurface, 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.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import com.zaneschepke.wireguardautotunnel.ui.theme.Disabled
@Composable @Composable
fun DescriptionText(text: String, modifier: Modifier = Modifier) { fun DescriptionText(text: String, modifier: Modifier = Modifier, disabled: Boolean = false) {
Text( Text(
text = 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, modifier = modifier,
) )
} }
@Composable @Composable
fun DescriptionText(text: AnnotatedString, modifier: Modifier = Modifier) { fun DescriptionText(
text: AnnotatedString,
modifier: Modifier = Modifier,
disabled: Boolean = false,
) {
Text( Text(
text = 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, modifier = modifier,
) )
} }
@@ -2,7 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.common.textbox
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.fillMaxWidth 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.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -39,7 +39,7 @@ fun ConfigurationTextBox(
isError = isError, isError = isError,
textStyle = textStyle =
MaterialTheme.typography.bodySmall.copy(color = MaterialTheme.colorScheme.onSurface), MaterialTheme.typography.bodySmall.copy(color = MaterialTheme.colorScheme.onSurface),
modifier = modifier.fillMaxWidth().height(48.dp), modifier = modifier.fillMaxWidth().heightIn(48.dp),
value = value, value = value,
visualTransformation = visualTransformation, visualTransformation = visualTransformation,
singleLine = singleLine, singleLine = singleLine,
@@ -52,10 +52,18 @@ fun CustomTextField(
val editable = enabled && !readOnly val editable = enabled && !readOnly
val mainFocusRequester = remember { FocusRequester() } val mainFocusRequester = remember { FocusRequester() }
val trailingFocusRequester = 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( BasicTextField(
value = value, value = value,
textStyle = textStyle, textStyle = effectiveTextStyle,
onValueChange = { onValueChange(it) }, onValueChange = { onValueChange(it) },
keyboardActions = keyboardActions, keyboardActions = keyboardActions,
keyboardOptions = keyboardOptions, keyboardOptions = keyboardOptions,
@@ -105,7 +113,18 @@ fun CustomTextField(
colors = colors =
TextFieldDefaults.colors() TextFieldDefaults.colors()
.copy( .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, disabledContainerColor = containerColor,
focusedLabelColor = MaterialTheme.colorScheme.onSurface, focusedLabelColor = MaterialTheme.colorScheme.onSurface,
focusedContainerColor = containerColor, focusedContainerColor = containerColor,
@@ -127,8 +146,13 @@ fun CustomTextField(
TextFieldDefaults.colors() TextFieldDefaults.colors()
.copy( .copy(
errorContainerColor = containerColor, errorContainerColor = containerColor,
disabledLabelColor = MaterialTheme.colorScheme.onSurface, disabledLabelColor =
MaterialTheme.colorScheme.onSurface.copy(alpha = disabledAlpha),
disabledContainerColor = containerColor, disabledContainerColor = containerColor,
disabledIndicatorColor =
MaterialTheme.colorScheme.onSurface.copy(
alpha = disabledBorderAlpha
),
focusedIndicatorColor = MaterialTheme.colorScheme.primary, focusedIndicatorColor = MaterialTheme.colorScheme.primary,
unfocusedIndicatorColor = MaterialTheme.colorScheme.outline, unfocusedIndicatorColor = MaterialTheme.colorScheme.outline,
focusedLabelColor = MaterialTheme.colorScheme.onSurface, focusedLabelColor = MaterialTheme.colorScheme.onSurface,
@@ -34,7 +34,7 @@ sealed class Route : NavKey {
@Keep @Serializable data object Tunnels : Route() @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() @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 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 class SplitTunnelGlobal(val id: Int) : Route()
@Keep @Serializable data object Sort : 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 ProxySettings : Route()
@Keep @Serializable data object LockdownSettings : Route()
@Keep @Serializable data object AutoTunnel : Route() @Keep @Serializable data object AutoTunnel : Route()
@Keep @Serializable data object AdvancedAutoTunnel : Route() @Keep @Serializable data object AdvancedAutoTunnel : Route()
@@ -107,7 +107,7 @@ enum class Tab(
when (route) { when (route) {
is Route.Tunnels, is Route.Tunnels,
Route.Sort, Route.Sort,
is Route.TunnelOptions, is Route.TunnelSettings,
is Route.Config, is Route.Config,
is Route.Lock, is Route.Lock,
is Route.SplitTunnel -> TUNNELS is Route.SplitTunnel -> TUNNELS
@@ -121,14 +121,14 @@ enum class Tab(
Route.TunnelMonitoring, Route.TunnelMonitoring,
Route.AndroidIntegrations, Route.AndroidIntegrations,
Route.Dns, Route.Dns,
is Route.TunnelGlobals,
is Route.ConfigGlobal,
is Route.SplitTunnelGlobal, is Route.SplitTunnelGlobal,
Route.ProxySettings, Route.ProxySettings,
Route.LockdownSettings,
Route.Appearance, Route.Appearance,
Route.Language, Route.Language,
Route.Display, Route.Display,
Route.PingTarget, Route.PingTarget,
is Route.ConfigGlobal,
Route.Logs -> SETTINGS Route.Logs -> SETTINGS
is Route.Support, is Route.Support,
Route.License, Route.License,
@@ -113,6 +113,29 @@ fun currentRouteAsNavbarState(
showBottomItems = true, showBottomItems = true,
topTitle = context.getString(R.string.language), 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 -> License ->
NavbarState( NavbarState(
topLeading = { topLeading = {
@@ -211,8 +234,11 @@ fun currentRouteAsNavbarState(
} }
}, },
) )
is Config -> { is Config,
val tunnelName = sharedState.tunnels.find { it.id == route.id }?.name 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( NavbarState(
topLeading = { topLeading = {
IconButton(onClick = { navController.pop() }) { IconButton(onClick = { navController.pop() }) {
@@ -236,8 +262,12 @@ fun currentRouteAsNavbarState(
}, },
) )
} }
is SplitTunnel -> { is SplitTunnel,
val tunnelName = sharedState.tunnels.find { it.id == route.id }?.name 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( NavbarState(
topLeading = { topLeading = {
IconButton(onClick = { navController.pop() }) { IconButton(onClick = { navController.pop() }) {
@@ -260,52 +290,6 @@ fun currentRouteAsNavbarState(
showBottomItems = true, 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 -> Support ->
NavbarState( NavbarState(
topTitle = context.getString(R.string.support), topTitle = context.getString(R.string.support),
@@ -337,7 +321,7 @@ fun currentRouteAsNavbarState(
topTitle = context.getString(R.string.ping_monitor), topTitle = context.getString(R.string.ping_monitor),
showBottomItems = true, showBottomItems = true,
) )
is TunnelOptions -> { is TunnelSettings -> {
val tunnelName = sharedState.tunnels.find { it.id == route.id }?.name val tunnelName = sharedState.tunnels.find { it.id == route.id }?.name
NavbarState( NavbarState(
topLeading = { topLeading = {
@@ -497,20 +481,6 @@ fun currentRouteAsNavbarState(
showBottomItems = true, 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 -> { is WifiPreferences -> {
NavbarState( NavbarState(
topLeading = { topLeading = {
@@ -40,9 +40,9 @@ import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.domain.enums.NetworkType import com.zaneschepke.wireguardautotunnel.domain.enums.NetworkType
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm 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.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.common.button.SwitchWithDivider 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.functions.rememberClipboardHelper
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.common.text.DescriptionText 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), title = stringResource(R.string.stop_on_no_internet),
description = { DescriptionText(stringResource(R.string.stop_on_internet_loss)) }, description = { DescriptionText(stringResource(R.string.stop_on_internet_loss)) },
trailing = { trailing = {
ScaledSwitch( ThemedSwitch(
checked = autoTunnelState.autoTunnelSettings.isStopOnNoInternetEnabled, checked = autoTunnelState.autoTunnelSettings.isStopOnNoInternetEnabled,
onClick = { viewModel.setStopOnNoInternetEnabled(it) }, onClick = { viewModel.setStopOnNoInternetEnabled(it) },
) )
@@ -315,7 +315,7 @@ fun AutoTunnelScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
leading = { Icon(Icons.Outlined.RestartAlt, contentDescription = null) }, leading = { Icon(Icons.Outlined.RestartAlt, contentDescription = null) },
title = stringResource(R.string.restart_at_boot), title = stringResource(R.string.restart_at_boot),
trailing = { trailing = {
ScaledSwitch( ThemedSwitch(
checked = autoTunnelState.autoTunnelSettings.startOnBoot, checked = autoTunnelState.autoTunnelSettings.startOnBoot,
onClick = { viewModel.setStartAtBoot(it) }, onClick = { viewModel.setStartAtBoot(it) },
) )
@@ -26,8 +26,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.banner.WarningBanner 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.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.common.button.ThemedSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.common.text.DescriptionText import com.zaneschepke.wireguardautotunnel.ui.common.text.DescriptionText
@@ -88,9 +88,9 @@ fun WifiSettingsScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
showLocationDialog = false showLocationDialog = false
}, },
onDismiss = { 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)) }, 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 = { trailing = {
ScaledSwitch( ThemedSwitch(
checked = autoTunnelState.autoTunnelSettings.isWildcardsEnabled, checked = autoTunnelState.autoTunnelSettings.isWildcardsEnabled,
onClick = { viewModel.setWildcardsEnabled(it) }, onClick = { viewModel.setWildcardsEnabled(it) },
) )
@@ -7,16 +7,16 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons 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.automirrored.outlined.ViewQuilt
import androidx.compose.material.icons.outlined.* import androidx.compose.material.icons.outlined.*
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource 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.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm 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.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.common.button.SwitchWithDivider 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.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.common.text.DescriptionText import com.zaneschepke.wireguardautotunnel.ui.common.text.DescriptionText
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.BackupBottomSheet import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.BackupBottomSheet
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.proxy.compoents.AppModeBottomSheet 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.StringValue
import com.zaneschepke.wireguardautotunnel.util.extensions.asString import com.zaneschepke.wireguardautotunnel.util.extensions.asString
import com.zaneschepke.wireguardautotunnel.util.extensions.asTitleString import com.zaneschepke.wireguardautotunnel.util.extensions.asTitleString
@@ -53,29 +55,24 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
val locale = remember { Locale.getDefault() } val locale = remember { Locale.getDefault() }
val sharedState by sharedViewModel.container.stateFlow.collectAsStateWithLifecycle() val sharedUiState by sharedViewModel.container.stateFlow.collectAsStateWithLifecycle()
val settingsState by viewModel.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 showBackupSheet by rememberSaveable { mutableStateOf(false) }
var showAppModeSheet 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 dnsEnabled by rememberSaveable(appMode) { mutableStateOf(appMode != AppMode.KERNEL) }
val showProxySettings by val showModeDivider by
remember(appMode) { remember(appMode) {
derivedStateOf { derivedStateOf { appMode == AppMode.PROXY || appMode == AppMode.LOCK_DOWN }
when (appMode) {
AppMode.PROXY -> true
else -> false
}
}
} }
fun performBackupRestore(action: () -> Unit) { fun performBackupRestore(action: () -> Unit) {
if (sharedState.activeTunnels.isNotEmpty() || sharedState.isAutoTunnelActive) if (sharedUiState.activeTunnels.isNotEmpty() || sharedUiState.isAutoTunnelActive)
return context.showToast(R.string.all_services_disabled) return context.showToast(R.string.all_services_disabled)
showBackupSheet = false showBackupSheet = false
action() action()
@@ -89,22 +86,10 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
showBackupSheet = false showBackupSheet = false
} }
if (showAppModeSheet) if (showAppModeSheet)
AppModeBottomSheet(sharedViewModel::setAppMode, settingsState.settings.appMode) { AppModeBottomSheet(sharedViewModel::setAppMode, uiState.settings.appMode) {
showAppModeSheet = false 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( Column(
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Top), verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Top),
@@ -119,11 +104,8 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
leading = { leading = {
Icon(ImageVector.vectorResource(R.drawable.sdk), contentDescription = null) Icon(ImageVector.vectorResource(R.drawable.sdk), contentDescription = null)
}, },
trailing = { trailing = { modifier ->
Icon( SheetButtonWithDivider(showModeDivider, modifier) { showAppModeSheet = true }
Icons.Outlined.ExpandMore,
contentDescription = stringResource(R.string.select),
)
}, },
title = stringResource(R.string.backend_mode), title = stringResource(R.string.backend_mode),
description = { description = {
@@ -131,40 +113,23 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
stringResource(R.string.current_template, appMode.asTitleString(context)) 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( SurfaceRow(
leading = { leading = {
Icon( Icon(
Icons.Outlined.Dns, Icons.Outlined.Dns,
null, 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), title = stringResource(R.string.dns_settings),
@@ -182,29 +147,39 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
) )
SurfaceRow( SurfaceRow(
leading = { leading = {
Icon(ImageVector.vectorResource(R.drawable.globe), contentDescription = null) Icon(
}, Icons.AutoMirrored.Outlined.CallSplit,
title = stringResource(R.string.global_overrides), contentDescription = null,
trailing = { modifier -> tint =
SwitchWithDivider( if (sharedUiState.proxyEnabled) Disabled
checked = settingsState.settings.isTunnelGlobalsEnabled, else MaterialTheme.colorScheme.onSurface,
onClick = { viewModel.setTunnelGlobals(it) },
modifier = modifier,
) )
}, },
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 = { onClick = {
settingsState.globalTunnelConfig?.let { uiState.globalTunnelConfig?.let {
navController.push(Route.TunnelGlobals(it.id)) 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( SurfaceRow(
leading = { Icon(Icons.Outlined.Android, null) }, leading = { Icon(Icons.Outlined.Android, null) },
title = stringResource(R.string.android_integrations), title = stringResource(R.string.android_integrations),
@@ -222,17 +197,27 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
Icons.Outlined.NetworkPing, Icons.Outlined.NetworkPing,
contentDescription = null, contentDescription = null,
tint = tint =
if (isPingMonitoringAvailable) MaterialTheme.colorScheme.onSurface if (!sharedUiState.proxyEnabled) MaterialTheme.colorScheme.onSurface
else Color.Gray, else Disabled,
) )
}, },
title = stringResource(R.string.ping_monitor), title = stringResource(R.string.ping_monitor),
enabled = isPingMonitoringAvailable, enabled = !sharedUiState.proxyEnabled,
trailing = { description =
if (sharedUiState.proxyEnabled) {
{
DescriptionText(
stringResource(R.string.unavailable_in_mode),
disabled = true,
)
}
} else null,
trailing = { modifier ->
SwitchWithDivider( SwitchWithDivider(
checked = settingsState.monitoring.isPingEnabled, checked = uiState.monitoring.isPingEnabled,
onClick = { viewModel.setPingEnabled(it) }, onClick = { viewModel.setPingEnabled(it) },
enabled = isPingMonitoringAvailable, enabled = !sharedUiState.proxyEnabled,
modifier = modifier,
) )
}, },
onClick = { navController.push(Route.TunnelMonitoring) }, onClick = { navController.push(Route.TunnelMonitoring) },
@@ -242,7 +227,7 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
title = stringResource(R.string.local_logging), title = stringResource(R.string.local_logging),
trailing = { modifier -> trailing = { modifier ->
SwitchWithDivider( SwitchWithDivider(
checked = settingsState.monitoring.isLocalLogsEnabled, checked = uiState.monitoring.isLocalLogsEnabled,
onClick = { viewModel.setLocalLogging(it) }, onClick = { viewModel.setLocalLogging(it) },
modifier = modifier, modifier = modifier,
) )
@@ -266,8 +251,8 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
leading = { Icon(Icons.Outlined.Pin, contentDescription = null) }, leading = { Icon(Icons.Outlined.Pin, contentDescription = null) },
title = stringResource(R.string.enable_app_lock), title = stringResource(R.string.enable_app_lock),
trailing = { trailing = {
ScaledSwitch( ThemedSwitch(
checked = settingsState.isPinLockEnabled, checked = uiState.isPinLockEnabled,
onClick = { onClick = {
if (it) { if (it) {
navController.push(Route.Lock) navController.push(Route.Lock)
@@ -278,7 +263,7 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
) )
}, },
onClick = { onClick = {
if (!settingsState.isPinLockEnabled) { if (!uiState.isPinLockEnabled) {
navController.push(Route.Lock) navController.push(Route.Lock)
} else { } else {
sharedViewModel.setPinLockEnabled(false) sharedViewModel.setPinLockEnabled(false)
@@ -289,11 +274,13 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
leading = { Icon(Icons.Outlined.SettingsBackupRestore, contentDescription = null) }, leading = { Icon(Icons.Outlined.SettingsBackupRestore, contentDescription = null) },
title = stringResource(R.string.backup_and_restore), title = stringResource(R.string.backup_and_restore),
onClick = { showBackupSheet = true }, onClick = { showBackupSheet = true },
trailing = { trailing = { modifier ->
Icon( IconButton(modifier = modifier, onClick = { showBackupSheet = true }) {
Icons.Outlined.ExpandMore, Icon(
contentDescription = stringResource(R.string.select), 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.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons 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.material3.Icon
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.model.DnsProtocol import com.zaneschepke.wireguardautotunnel.data.model.DnsProtocol
import com.zaneschepke.wireguardautotunnel.data.model.DnsProvider 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.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 com.zaneschepke.wireguardautotunnel.viewmodel.DnsViewModel
import java.util.*
@Composable @Composable
fun DnsSettingsScreen(viewModel: DnsViewModel = hiltViewModel()) { fun DnsSettingsScreen(viewModel: DnsViewModel = hiltViewModel()) {
val context = LocalContext.current val context = LocalContext.current
val navController = LocalNavController.current
val dnsUiState by viewModel.container.stateFlow.collectAsStateWithLifecycle() val dnsUiState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
if (dnsUiState.isLoading) return if (dnsUiState.isLoading) return
val locale = remember { Locale.getDefault() }
Column( Column(
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.Top), verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Top),
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()), modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()),
) { ) {
Column { Column {
GroupLabel(stringResource(R.string.endpoint), Modifier.padding(horizontal = 16.dp))
LabelledDropdown( LabelledDropdown(
title = stringResource(R.string.dns_protocol), title = stringResource(R.string.dns_protocol),
leading = { Icon(Icons.Outlined.Dns, contentDescription = null) }, 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.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
@@ -25,8 +24,8 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV 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.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.common.button.ThemedSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberClipboardHelper import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberClipboardHelper
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.common.security.SecureScreenFromRecording import com.zaneschepke.wireguardautotunnel.ui.common.security.SecureScreenFromRecording
@@ -72,7 +71,7 @@ fun AndroidIntegrationsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
SurfaceRow( SurfaceRow(
leading = { Icon(Icons.Outlined.VpnLock, contentDescription = null) }, leading = { Icon(Icons.Outlined.VpnLock, contentDescription = null) },
trailing = { trailing = {
ScaledSwitch( ThemedSwitch(
checked = isAlwaysOnEnabled, checked = isAlwaysOnEnabled,
onClick = { viewModel.setAlwaysOnVpnEnabled(it) }, onClick = { viewModel.setAlwaysOnVpnEnabled(it) },
) )
@@ -94,12 +93,12 @@ fun AndroidIntegrationsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
Icons.Outlined.Restore, Icons.Outlined.Restore,
contentDescription = null, contentDescription = null,
tint = tint =
if (isAlwaysOnEnabled) Color.Gray if (isAlwaysOnEnabled) MaterialTheme.colorScheme.outline
else MaterialTheme.colorScheme.onSurface, else MaterialTheme.colorScheme.onSurface,
) )
}, },
trailing = { trailing = {
ScaledSwitch( ThemedSwitch(
checked = settingsState.settings.isRestoreOnBootEnabled, checked = settingsState.settings.isRestoreOnBootEnabled,
onClick = { viewModel.setRestoreOnBootEnabled(it) }, onClick = { viewModel.setRestoreOnBootEnabled(it) },
enabled = !isAlwaysOnEnabled, enabled = !isAlwaysOnEnabled,
@@ -117,7 +116,7 @@ fun AndroidIntegrationsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
SurfaceRow( SurfaceRow(
leading = { Icon(Icons.Filled.AppShortcut, contentDescription = null) }, leading = { Icon(Icons.Filled.AppShortcut, contentDescription = null) },
trailing = { trailing = {
ScaledSwitch( ThemedSwitch(
checked = settingsState.settings.isShortcutsEnabled, checked = settingsState.settings.isShortcutsEnabled,
onClick = { viewModel.setShortcutsEnabled(it) }, onClick = { viewModel.setShortcutsEnabled(it) },
) )
@@ -130,7 +129,7 @@ fun AndroidIntegrationsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
SurfaceRow( SurfaceRow(
leading = { Icon(Icons.Filled.SmartToy, contentDescription = null) }, leading = { Icon(Icons.Filled.SmartToy, contentDescription = null) },
trailing = { trailing = {
ScaledSwitch( ThemedSwitch(
checked = settingsState.isRemoteEnabled, checked = settingsState.isRemoteEnabled,
onClick = { viewModel.setRemoteEnabled(it) }, 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 androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController 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.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.common.button.ThemedSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.dropdown.LabelledDropdown import com.zaneschepke.wireguardautotunnel.ui.common.dropdown.LabelledDropdown
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
@@ -86,7 +86,7 @@ fun TunnelMonitoringScreen(viewModel: MonitoringViewModel = hiltViewModel()) {
leading = { Icon(Icons.Outlined.QueryStats, contentDescription = null) }, leading = { Icon(Icons.Outlined.QueryStats, contentDescription = null) },
title = stringResource(R.string.display_detailed_ping_stats), title = stringResource(R.string.display_detailed_ping_stats),
trailing = { trailing = {
ScaledSwitch( ThemedSwitch(
checked = monitoringUiState.monitoringSettings.showDetailedPingStats, checked = monitoringUiState.monitoringSettings.showDetailedPingStats,
onClick = { viewModel.setDetailedPingStats(it) }, 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.material.icons.outlined.RemoveRedEye
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -24,73 +25,84 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.domain.model.ProxySettings import com.zaneschepke.wireguardautotunnel.domain.model.ProxySettings
import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm 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.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.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.common.security.SecureScreenFromRecording import com.zaneschepke.wireguardautotunnel.ui.common.security.SecureScreenFromRecording
import com.zaneschepke.wireguardautotunnel.ui.common.textbox.ConfigurationTextBox import com.zaneschepke.wireguardautotunnel.ui.common.textbox.ConfigurationTextBox
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
import com.zaneschepke.wireguardautotunnel.viewmodel.ProxySettingsViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.ProxySettingsViewModel
import java.util.Locale
import org.orbitmvi.orbit.compose.collectSideEffect import org.orbitmvi.orbit.compose.collectSideEffect
@Composable @Composable
fun ProxySettingsScreen(viewModel: ProxySettingsViewModel = hiltViewModel()) { fun ProxySettingsScreen(viewModel: ProxySettingsViewModel = hiltViewModel()) {
val sharedViewModel = LocalSharedVm.current 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 if (uiState.isLoading) return
remember(proxySettingsState) { mutableStateOf(proxySettingsState.proxySettings) }
val locale = remember { Locale.getDefault() }
val proxySettings by remember(uiState) { mutableStateOf(uiState.proxySettings) }
var socks5Enabled by var socks5Enabled by
remember(proxySettings) { remember(proxySettings) { mutableStateOf(uiState.proxySettings.socks5ProxyEnabled) }
mutableStateOf(proxySettingsState.proxySettings.socks5ProxyEnabled)
}
var httpEnabled by var httpEnabled by
remember(proxySettings) { remember(proxySettings) { mutableStateOf(uiState.proxySettings.httpProxyEnabled) }
mutableStateOf(proxySettingsState.proxySettings.httpProxyEnabled)
}
var socksBindAddress by var socksBindAddress by
remember(proxySettings) { remember(proxySettings) {
mutableStateOf(proxySettingsState.proxySettings.socks5ProxyBindAddress ?: "") mutableStateOf(uiState.proxySettings.socks5ProxyBindAddress ?: "")
} }
var httpBindAddress by var httpBindAddress by
remember(proxySettings) { remember(proxySettings) { mutableStateOf(uiState.proxySettings.httpProxyBindAddress ?: "") }
mutableStateOf(proxySettingsState.proxySettings.httpProxyBindAddress ?: "")
}
var proxyUsername by var proxyUsername by
remember(proxySettings) { remember(proxySettings) { mutableStateOf(uiState.proxySettings.proxyUsername ?: "") }
mutableStateOf(proxySettingsState.proxySettings.proxyUsername ?: "")
}
var proxyPassword by var proxyPassword by
remember(proxySettings) { remember(proxySettings) { mutableStateOf(uiState.proxySettings.proxyPassword ?: "") }
mutableStateOf(proxySettingsState.proxySettings.proxyPassword ?: "") var passwordVisible by remember(proxySettings) { mutableStateOf(uiState.passwordVisible) }
}
var passwordVisible by
remember(proxySettings) { mutableStateOf(proxySettingsState.passwordVisible) }
val keyboardController = LocalSoftwareKeyboardController.current val keyboardController = LocalSoftwareKeyboardController.current
val keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() }) val keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() })
val keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done) 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 -> sharedViewModel.collectSideEffect { sideEffect ->
when (sideEffect) { if (sideEffect is LocalSideEffect.SaveChanges) {
LocalSideEffect.SaveChanges -> { if (uiState.activeTuns.isNotEmpty()) viewModel.setShowSaveModal(true) else saveChanges()
viewModel.save( }
ProxySettings( }
socks5ProxyEnabled = socks5Enabled,
socks5ProxyBindAddress = socksBindAddress, if (uiState.showSaveModal) {
httpProxyEnabled = httpEnabled, InfoDialog(
httpProxyBindAddress = httpBindAddress, onDismiss = { viewModel.setShowSaveModal(false) },
proxyUsername = proxyUsername, onAttest = { saveChanges() },
proxyPassword = proxyPassword, 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() SecureScreenFromRecording()
@@ -105,7 +117,7 @@ fun ProxySettingsScreen(viewModel: ProxySettingsViewModel = hiltViewModel()) {
leading = { Icon(Icons.Outlined.Forward5, contentDescription = null) }, leading = { Icon(Icons.Outlined.Forward5, contentDescription = null) },
title = stringResource(R.string.socks_5_proxy), title = stringResource(R.string.socks_5_proxy),
trailing = { trailing = {
ScaledSwitch(checked = socks5Enabled, onClick = { socks5Enabled = it }) ThemedSwitch(checked = socks5Enabled, onClick = { socks5Enabled = it })
}, },
onClick = { socks5Enabled = !socks5Enabled }, onClick = { socks5Enabled = !socks5Enabled },
) )
@@ -119,10 +131,9 @@ fun ProxySettingsScreen(viewModel: ProxySettingsViewModel = hiltViewModel()) {
), ),
label = stringResource(R.string.socks_5_bind_address), label = stringResource(R.string.socks_5_bind_address),
value = socksBindAddress, value = socksBindAddress,
isError = proxySettingsState.isSocks5BindAddressError, isError = uiState.isSocks5BindAddressError,
onValueChange = { onValueChange = {
if (proxySettingsState.isSocks5BindAddressError) if (uiState.isSocks5BindAddressError) viewModel.clearSocks5BindError()
viewModel.clearSocks5BindError()
socksBindAddress = it socksBindAddress = it
}, },
) )
@@ -132,7 +143,7 @@ fun ProxySettingsScreen(viewModel: ProxySettingsViewModel = hiltViewModel()) {
SurfaceRow( SurfaceRow(
leading = { Icon(Icons.Outlined.Http, contentDescription = null) }, leading = { Icon(Icons.Outlined.Http, contentDescription = null) },
title = stringResource(R.string.http_proxy), title = stringResource(R.string.http_proxy),
trailing = { ScaledSwitch(checked = httpEnabled, onClick = { httpEnabled = it }) }, trailing = { ThemedSwitch(checked = httpEnabled, onClick = { httpEnabled = it }) },
onClick = { httpEnabled = !httpEnabled }, onClick = { httpEnabled = !httpEnabled },
) )
if (httpEnabled) { if (httpEnabled) {
@@ -144,10 +155,9 @@ fun ProxySettingsScreen(viewModel: ProxySettingsViewModel = hiltViewModel()) {
), ),
label = stringResource(R.string.http_bind_address), label = stringResource(R.string.http_bind_address),
value = httpBindAddress, value = httpBindAddress,
isError = proxySettingsState.isHttpBindAddressError, isError = uiState.isHttpBindAddressError,
onValueChange = { onValueChange = {
if (proxySettingsState.isSocks5BindAddressError) if (uiState.isSocks5BindAddressError) viewModel.clearHttpBindError()
viewModel.clearHttpBindError()
httpBindAddress = it httpBindAddress = it
}, },
modifier = Modifier.padding(horizontal = 12.dp), modifier = Modifier.padding(horizontal = 12.dp),
@@ -169,11 +179,11 @@ fun ProxySettingsScreen(viewModel: ProxySettingsViewModel = hiltViewModel()) {
ConfigurationTextBox( ConfigurationTextBox(
value = proxyUsername, value = proxyUsername,
onValueChange = { onValueChange = {
if (proxySettingsState.isUserNameError) viewModel.clearUsernameError() if (uiState.isUserNameError) viewModel.clearUsernameError()
proxyUsername = it proxyUsername = it
}, },
label = stringResource(R.string.username), label = stringResource(R.string.username),
isError = proxySettingsState.isUserNameError, isError = uiState.isUserNameError,
hint = "", hint = "",
keyboardActions = keyboardActions, keyboardActions = keyboardActions,
keyboardOptions = keyboardOptions, keyboardOptions = keyboardOptions,
@@ -181,11 +191,11 @@ fun ProxySettingsScreen(viewModel: ProxySettingsViewModel = hiltViewModel()) {
ConfigurationTextBox( ConfigurationTextBox(
value = proxyPassword, value = proxyPassword,
onValueChange = { onValueChange = {
if (proxySettingsState.isUserNameError) viewModel.clearPasswordError() if (uiState.isUserNameError) viewModel.clearPasswordError()
proxyPassword = it proxyPassword = it
}, },
label = stringResource(R.string.password), label = stringResource(R.string.password),
isError = proxySettingsState.isPasswordError, isError = uiState.isPasswordError,
hint = "", hint = "",
keyboardActions = keyboardActions, keyboardActions = keyboardActions,
keyboardOptions = keyboardOptions, keyboardOptions = keyboardOptions,
@@ -3,6 +3,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings.proxy.compoents
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import com.zaneschepke.wireguardautotunnel.data.model.AppMode 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.CustomBottomSheet
import com.zaneschepke.wireguardautotunnel.ui.common.sheet.SheetOption import com.zaneschepke.wireguardautotunnel.ui.common.sheet.SheetOption
import com.zaneschepke.wireguardautotunnel.util.extensions.asIcon import com.zaneschepke.wireguardautotunnel.util.extensions.asIcon
@@ -15,19 +16,23 @@ fun AppModeBottomSheet(
onDismiss: () -> Unit, onDismiss: () -> Unit,
) { ) {
val context = LocalContext.current val context = LocalContext.current
val isTv = LocalIsAndroidTV.current
CustomBottomSheet( CustomBottomSheet(
enumValues<AppMode>().map { enumValues<AppMode>()
val icon = it.asIcon() .filterNot { isTv && it == AppMode.KERNEL }
SheetOption( .map {
icon, val icon = it.asIcon()
label = it.asTitleString(context), SheetOption(
onClick = { icon,
onDismiss() label = it.asTitleString(context),
onAppModeChange(it) onClick = {
}, onDismiss()
selected = appMode == it, onAppModeChange(it)
) },
} selected = appMode == it,
)
}
) { ) {
onDismiss() onDismiss()
} }
@@ -73,6 +73,11 @@ fun SupportScreen(viewModel: SupportViewModel = hiltViewModel()) {
) )
Column { Column {
GroupLabel(stringResource(R.string.resources), Modifier.padding(horizontal = 16.dp)) 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( SurfaceRow(
stringResource(R.string.docs_description), stringResource(R.string.docs_description),
onClick = { context.openWebUrl(context.getString(R.string.docs_url)) }, 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) }, trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
) )
SurfaceRow( SurfaceRow(
leading = { Icon(Icons.Outlined.Favorite, contentDescription = null) }, stringResource(R.string.translation),
title = stringResource(R.string.donate), onClick = { context.openWebUrl(context.getString(R.string.translation_url)) },
onClick = { navController.push(Route.Donate) }, description = { DescriptionText(stringResource(R.string.help_translate)) },
leading = { Icon(Icons.Outlined.Translate, contentDescription = null) },
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
) )
SurfaceRow( SurfaceRow(
leading = { Icon(Icons.Outlined.Balance, contentDescription = null) }, leading = { Icon(Icons.Outlined.Balance, contentDescription = null) },
@@ -16,8 +16,8 @@ fun PermissionDialog(context: Context, onDismiss: () -> Unit) {
context.requestInstallPackagesPermission() context.requestInstallPackagesPermission()
onDismiss() onDismiss()
}, },
title = { Text(stringResource(R.string.permission_required)) }, title = stringResource(R.string.permission_required),
body = { Text(stringResource(R.string.install_updated_permission)) }, 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() onPermissionNeeded()
} }
}, },
title = { Text(stringResource(R.string.update_available)) }, title = stringResource(R.string.update_available),
body = { body = {
Column( Column(
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.Start,
@@ -77,7 +77,7 @@ fun UpdateDialog(viewModel: SupportViewModel, context: Context, onPermissionNeed
Text(text = annotatedString) Text(text = annotatedString)
if (supportState.isLoading) { if (supportState.isLoading) {
val stroke = Stroke(cap = StrokeCap.Round) val stroke = Stroke(cap = StrokeCap.Round, width = 4.0f)
LinearWavyProgressIndicator( LinearWavyProgressIndicator(
progress = { supportState.downloadProgress }, progress = { supportState.downloadProgress },
modifier = Modifier.fillMaxWidth().padding(top = 16.dp), modifier = Modifier.fillMaxWidth().padding(top = 16.dp),
@@ -89,12 +89,8 @@ fun UpdateDialog(viewModel: SupportViewModel, context: Context, onPermissionNeed
} }
} }
}, },
confirmText = { confirmText =
Text( if (BuildConfig.FLAVOR != Constants.STANDALONE_FLAVOR) stringResource(R.string.download)
if (BuildConfig.FLAVOR != Constants.STANDALONE_FLAVOR) else stringResource(R.string.download_and_install),
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.Icons
import androidx.compose.material.icons.automirrored.outlined.Launch import androidx.compose.material.icons.automirrored.outlined.Launch
import androidx.compose.material.icons.outlined.CurrencyBitcoin import androidx.compose.material.icons.outlined.CurrencyBitcoin
import androidx.compose.material.icons.outlined.Done
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.stringResource
import androidx.compose.ui.res.vectorResource import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp 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.BuildConfig
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow 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.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.common.text.DescriptionText
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route 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.DonationHeroSection
import com.zaneschepke.wireguardautotunnel.ui.screens.support.donate.components.GoogleDonationMessage import com.zaneschepke.wireguardautotunnel.ui.screens.support.donate.components.GoogleDonationMessage
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
import com.zaneschepke.wireguardautotunnel.viewmodel.SettingsViewModel
@Composable @Composable
fun DonateScreen() { fun DonateScreen(viewModel: SettingsViewModel = hiltViewModel()) {
val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
if (uiState.isLoading) return
val context = LocalContext.current val context = LocalContext.current
val navController = LocalNavController.current val navController = LocalNavController.current
val isGoogleFlavor = remember { BuildConfig.FLAVOR == Constants.GOOGLE_PLAY_FLAVOR } val isGoogleFlavor = remember { BuildConfig.FLAVOR == Constants.GOOGLE_PLAY_FLAVOR }
@@ -91,6 +100,26 @@ fun DonateScreen() {
} else { } else {
GoogleDonationMessage() 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.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.journeyapps.barcodescanner.ScanContract import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanOptions import com.journeyapps.barcodescanner.ScanOptions
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
@@ -94,9 +95,9 @@ fun TunnelsScreen() {
viewModel.deleteSelectedTunnels() viewModel.deleteSelectedTunnels()
showDeleteModal = false 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)) }, 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()) { if (sharedState.selectedTunnels.isNotEmpty()) {
viewModel.toggleSelectedTunnel(tunnel.id) viewModel.toggleSelectedTunnel(tunnel.id)
} else { } else {
navController.push(Route.TunnelOptions(tunnel.id)) navController.push(Route.TunnelSettings(tunnel.id))
viewModel.clearSelectedTunnels() viewModel.clearSelectedTunnels()
} }
}, },
@@ -5,14 +5,18 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm 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.common.security.SecureScreenFromRecording
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.config.components.AddPeerButton import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.config.components.AddPeerButton
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.config.components.InterfaceSection 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.ConfigProxy
import com.zaneschepke.wireguardautotunnel.ui.state.PeerProxy import com.zaneschepke.wireguardautotunnel.ui.state.PeerProxy
import com.zaneschepke.wireguardautotunnel.viewmodel.ConfigViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.ConfigViewModel
import java.util.Locale
import org.orbitmvi.orbit.compose.collectSideEffect import org.orbitmvi.orbit.compose.collectSideEffect
@Composable @Composable
fun ConfigScreen(viewModel: ConfigViewModel) { fun ConfigScreen(viewModel: ConfigViewModel) {
val sharedViewModel = LocalSharedVm.current 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 { var configProxy by remember {
mutableStateOf( mutableStateOf(uiState.tunnel?.let { ConfigProxy.from(it.toAmConfig()) } ?: ConfigProxy())
configUiState.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 isGlobalConfig = rememberSaveable { tunnelName == TunnelConfig.GLOBAL_CONFIG_NAME }
val isTunnelNameTaken by val isTunnelNameTaken by
remember(tunnelName) { remember(tunnelName) { derivedStateOf { uiState.unavailableNames.contains(tunnelName) } }
derivedStateOf { configUiState.unavailableNames.contains(tunnelName) }
}
sharedViewModel.collectSideEffect { sideEffect -> sharedViewModel.collectSideEffect { sideEffect ->
if (sideEffect is LocalSideEffect.SaveChanges) 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() SecureScreenFromRecording()
@@ -60,6 +81,7 @@ fun ConfigScreen(viewModel: ConfigViewModel) {
InterfaceSection( InterfaceSection(
isGlobalConfig, isGlobalConfig,
configProxy = configProxy, configProxy = configProxy,
uiState.isRunning,
tunnelName, tunnelName,
isTunnelNameTaken, isTunnelNameTaken,
onInterfaceChange = { configProxy = configProxy.copy(`interface` = it) }, onInterfaceChange = { configProxy = configProxy.copy(`interface` = it) },
@@ -165,14 +165,15 @@ fun InterfaceFields(
.lowercase(locale), .lowercase(locale),
modifier = Modifier.weight(3f), modifier = Modifier.weight(3f),
) )
ConfigurationTextBox( if (!isGlobalConfig)
value = interfaceState.mtu, ConfigurationTextBox(
onValueChange = { onInterfaceChange(interfaceState.copy(mtu = it)) }, value = interfaceState.mtu,
label = stringResource(R.string.mtu), onValueChange = { onInterfaceChange(interfaceState.copy(mtu = it)) },
hint = stringResource(R.string.auto).lowercase(locale), label = stringResource(R.string.mtu),
modifier = Modifier.weight(2f), hint = stringResource(R.string.auto).lowercase(locale),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), modifier = Modifier.weight(2f),
) keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
)
} }
if (showScripts) { if (showScripts) {
ConfigurationTextBox( ConfigurationTextBox(
@@ -18,6 +18,7 @@ import com.wireguard.crypto.KeyPair
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel 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.common.textbox.ConfigurationTextBox
import com.zaneschepke.wireguardautotunnel.ui.state.ConfigProxy import com.zaneschepke.wireguardautotunnel.ui.state.ConfigProxy
import com.zaneschepke.wireguardautotunnel.ui.state.InterfaceProxy import com.zaneschepke.wireguardautotunnel.ui.state.InterfaceProxy
@@ -27,6 +28,7 @@ import java.util.*
fun InterfaceSection( fun InterfaceSection(
isGlobalConfig: Boolean, isGlobalConfig: Boolean,
configProxy: ConfigProxy, configProxy: ConfigProxy,
isRunning: Boolean,
tunnelName: String, tunnelName: String,
isTunnelNameTaken: Boolean, isTunnelNameTaken: Boolean,
onInterfaceChange: (InterfaceProxy) -> Unit, onInterfaceChange: (InterfaceProxy) -> Unit,
@@ -64,60 +66,62 @@ fun InterfaceSection(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) { ) {
GroupLabel( if (!isGlobalConfig)
stringResource(R.string.interface_), GroupLabel(
modifier = Modifier.padding(horizontal = 16.dp), 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)
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( Column(
verticalArrangement = Arrangement.spacedBy(12.dp), verticalArrangement = Arrangement.spacedBy(12.dp),
@@ -126,9 +130,18 @@ fun InterfaceSection(
if (!isGlobalConfig) if (!isGlobalConfig)
ConfigurationTextBox( ConfigurationTextBox(
value = tunnelName, value = tunnelName,
enabled = !isRunning,
onValueChange = onTunnelNameChange, onValueChange = onTunnelNameChange,
label = stringResource(R.string.name), label = stringResource(R.string.name),
isError = isTunnelNameTaken, isError = isTunnelNameTaken,
supportingText =
if (isRunning) {
{
DescriptionText(
stringResource(R.string.tunnel_running_name_message)
)
}
} else null,
hint = hint =
stringResource( stringResource(
R.string.hint_template, 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.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -8,6 +8,7 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.CallSplit 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.Dns
import androidx.compose.material.icons.outlined.Star import androidx.compose.material.icons.outlined.Star
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@@ -28,21 +29,24 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm 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.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.common.button.ThemedSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.common.text.DescriptionText import com.zaneschepke.wireguardautotunnel.ui.common.text.DescriptionText
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route 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.sideeffect.LocalSideEffect
import com.zaneschepke.wireguardautotunnel.ui.theme.Disabled
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelViewModel
import org.orbitmvi.orbit.compose.collectSideEffect import org.orbitmvi.orbit.compose.collectSideEffect
@Composable @Composable
fun TunnelOptionsScreen(viewModel: TunnelViewModel) { fun TunnelSettingsScreen(viewModel: TunnelViewModel) {
val navController = LocalNavController.current val navController = LocalNavController.current
val sharedViewModel = LocalSharedVm.current val sharedViewModel = LocalSharedVm.current
val sharedUiState by sharedViewModel.container.stateFlow.collectAsStateWithLifecycle()
val tunnelState by viewModel.container.stateFlow.collectAsStateWithLifecycle() val tunnelState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
if (tunnelState.isLoading) return if (tunnelState.isLoading) return
@@ -81,7 +85,7 @@ fun TunnelOptionsScreen(viewModel: TunnelViewModel) {
) )
}, },
trailing = { trailing = {
ScaledSwitch( ThemedSwitch(
checked = tunnel.isPrimaryTunnel, checked = tunnel.isPrimaryTunnel,
onClick = { viewModel.togglePrimaryTunnel() }, onClick = { viewModel.togglePrimaryTunnel() },
) )
@@ -90,9 +94,25 @@ fun TunnelOptionsScreen(viewModel: TunnelViewModel) {
) )
SurfaceRow( SurfaceRow(
leading = { 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), 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)) }, onClick = { navController.push(Route.SplitTunnel(id = tunnel.id)) },
) )
} }
@@ -108,7 +128,7 @@ fun TunnelOptionsScreen(viewModel: TunnelViewModel) {
DescriptionText(stringResource(R.string.ddns_auto_update_description)) DescriptionText(stringResource(R.string.ddns_auto_update_description))
}, },
trailing = { trailing = {
ScaledSwitch( ThemedSwitch(
checked = tunnel.restartOnPingFailure, checked = tunnel.restartOnPingFailure,
onClick = { viewModel.setRestartOnPing(it) }, onClick = { viewModel.setRestartOnPing(it) },
) )
@@ -121,12 +141,42 @@ fun TunnelOptionsScreen(viewModel: TunnelViewModel) {
}, },
title = stringResource(R.string.prefer_ipv6_resolution), title = stringResource(R.string.prefer_ipv6_resolution),
trailing = { trailing = {
ScaledSwitch( ThemedSwitch(
checked = !tunnel.isIpv4Preferred, 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.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
@@ -154,8 +154,10 @@ private fun ConfigTypeSelector(
) )
val activeContainerColor = Color.White val activeContainerColor = Color.White
val inactiveContainerColor = Color.White val inactiveContainerColor = Color.White
val activeContentColor = if (isEnabled) Color.Black else Color.Gray val activeContentColor =
val inactiveContentColor = if (isEnabled) Color.Black else Color.Gray if (isEnabled) Color.Black else MaterialTheme.colorScheme.outline
val inactiveContentColor =
if (isEnabled) Color.Black else MaterialTheme.colorScheme.outline
SegmentedButton( SegmentedButton(
shape = shape =
SegmentedButtonDefaults.itemShape( SegmentedButtonDefaults.itemShape(
@@ -172,7 +174,7 @@ private fun ConfigTypeSelector(
contentDescription = stringResource(R.string.select), contentDescription = stringResource(R.string.select),
tint = tint =
if (isEnabled) MaterialTheme.colorScheme.primary if (isEnabled) MaterialTheme.colorScheme.primary
else Color.Gray, else MaterialTheme.colorScheme.outline,
modifier = Modifier.size(SegmentedButtonDefaults.IconSize), modifier = Modifier.size(SegmentedButtonDefaults.IconSize),
) )
}, },
@@ -180,7 +182,8 @@ private fun ConfigTypeSelector(
Icon( Icon(
imageVector = Icons.Outlined.VpnKey, imageVector = Icons.Outlined.VpnKey,
contentDescription = typeName, contentDescription = typeName,
tint = if (isEnabled) Color.Black else Color.Gray, tint =
if (isEnabled) Color.Black else MaterialTheme.colorScheme.outline,
modifier = Modifier.size(SegmentedButtonDefaults.IconSize), modifier = Modifier.size(SegmentedButtonDefaults.IconSize),
) )
} }
@@ -197,7 +200,7 @@ private fun ConfigTypeSelector(
) { ) {
Text( Text(
text = typeName, text = typeName,
color = if (isEnabled) Color.Black else Color.Gray, color = if (isEnabled) Color.Black else MaterialTheme.colorScheme.outline,
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
) )
} }
@@ -6,4 +6,6 @@ data class ConfigUiState(
val unavailableNames: List<String> = emptyList(), val unavailableNames: List<String> = emptyList(),
val isLoading: Boolean = true, val isLoading: Boolean = true,
val tunnel: TunnelConfig? = null, val tunnel: TunnelConfig? = null,
val isRunning: Boolean = false,
val showSaveModal: Boolean = false,
) )
@@ -1,5 +1,10 @@
package com.zaneschepke.wireguardautotunnel.ui.state package com.zaneschepke.wireguardautotunnel.ui.state
import com.zaneschepke.wireguardautotunnel.domain.model.DnsSettings 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) parsePublicKey(publicKey)
if (preSharedKey.isNotBlank()) parsePreSharedKey(preSharedKey) if (preSharedKey.isNotBlank()) parsePreSharedKey(preSharedKey)
if (persistentKeepalive.isNotBlank()) parsePersistentKeepalive(persistentKeepalive) if (persistentKeepalive.isNotBlank()) parsePersistentKeepalive(persistentKeepalive)
parseEndpoint(endpoint) if (endpoint.isNotBlank()) parseEndpoint(endpoint)
parseAllowedIPs(allowedIps) parseAllowedIPs(allowedIps)
} }
.build() .build()
@@ -29,7 +29,7 @@ data class PeerProxy(
parsePublicKey(publicKey) parsePublicKey(publicKey)
if (preSharedKey.isNotBlank()) parsePreSharedKey(preSharedKey) if (preSharedKey.isNotBlank()) parsePreSharedKey(preSharedKey)
if (persistentKeepalive.isNotBlank()) parsePersistentKeepalive(persistentKeepalive) if (persistentKeepalive.isNotBlank()) parsePersistentKeepalive(persistentKeepalive)
parseEndpoint(endpoint) if (endpoint.isNotBlank()) parseEndpoint(endpoint)
parseAllowedIPs(allowedIps) parseAllowedIPs(allowedIps)
} }
.build() .build()
@@ -1,13 +1,16 @@
package com.zaneschepke.wireguardautotunnel.ui.state package com.zaneschepke.wireguardautotunnel.ui.state
import com.zaneschepke.wireguardautotunnel.domain.model.ProxySettings import com.zaneschepke.wireguardautotunnel.domain.model.ProxySettings
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
data class ProxySettingsUiState( data class ProxySettingsUiState(
val proxySettings: ProxySettings = ProxySettings(), val proxySettings: ProxySettings = ProxySettings(),
val activeTuns: Map<Int, TunnelState> = emptyMap(),
val isSocks5BindAddressError: Boolean = false, val isSocks5BindAddressError: Boolean = false,
val isHttpBindAddressError: Boolean = false, val isHttpBindAddressError: Boolean = false,
val isUserNameError: Boolean = false, val isUserNameError: Boolean = false,
val isPasswordError: Boolean = false, val isPasswordError: Boolean = false,
val passwordVisible: Boolean = false, val passwordVisible: Boolean = false,
val isLoading: Boolean = true, val isLoading: Boolean = true,
val showSaveModal: Boolean = false,
) )
@@ -11,6 +11,7 @@ data class SharedAppUiState(
val theme: Theme = Theme.AUTOMATIC, val theme: Theme = Theme.AUTOMATIC,
val locale: String = LocaleUtil.OPTION_PHONE_LANGUAGE, val locale: String = LocaleUtil.OPTION_PHONE_LANGUAGE,
val pinLockEnabled: Boolean = false, val pinLockEnabled: Boolean = false,
val shouldShowDonationSnackbar: Boolean = false,
val tunnels: List<TunnelConfig> = emptyList(), val tunnels: List<TunnelConfig> = emptyList(),
val selectedTunnels: List<TunnelConfig> = emptyList(), val selectedTunnels: List<TunnelConfig> = emptyList(),
val activeTunnels: Map<Int, TunnelState> = emptyMap(), val activeTunnels: Map<Int, TunnelState> = emptyMap(),
@@ -20,5 +21,6 @@ data class SharedAppUiState(
val isAutoTunnelActive: Boolean = false, val isAutoTunnelActive: Boolean = false,
val isLocationDisclosureShown: Boolean = false, val isLocationDisclosureShown: Boolean = false,
val isBatteryOptimizationShown: Boolean = false, val isBatteryOptimizationShown: Boolean = false,
val proxyEnabled: Boolean = false,
val settings: GeneralSettings = GeneralSettings(), val settings: GeneralSettings = GeneralSettings(),
) )
@@ -19,6 +19,8 @@ val AlertRed = Color(0xFFCF6679)
val Straw = Color(0xFFD4C483) val Straw = Color(0xFFD4C483)
val Disabled = CoolGray.copy(alpha = 0.4f)
sealed class ThemeColors( sealed class ThemeColors(
val background: Color, val background: Color,
val surface: Color, val surface: Color,

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