Compare commits

...

90 Commits

Author SHA1 Message Date
Zane Schepke 16d0642a51 chore: release 4.2.1 2025-12-19 11:35:43 -05:00
Zane Schepke eac674c996 fix: auto-tunnel screen not loading without wifi
Fixes auto tunnel screen failing to load if you haven't connected to wifi once.

Fixes import via url.

Closes #1108
Closes #1105
2025-12-19 11:30:39 -05:00
Zane Schepke 394188b55f chore: release 4.2.0 2025-12-19 01:10:11 -05:00
Weblate (bot) d61a4213cf Translations update from Hosted Weblate (#1030)
Co-authored-by: Fill read-only add-on <noreply-addon-fill@weblate.org>
Co-authored-by: solokot <solokot@gmail.com>
Co-authored-by: Matthaiks <kitynska@gmail.com>
Co-authored-by: Qotsa1984 <carlominzi@inwind.it>
Co-authored-by: jaime-grj <weblate.4ljj9@aleeas.com>
Co-authored-by: catelixor <catelixor+weblate@proton.me>
Co-authored-by: Priit Jõerüüt <jrthwlate@users.noreply.hosted.weblate.org>
Co-authored-by: Prefill add-on <noreply-addon-prefill@weblate.org>
Co-authored-by: Kachelkaiser <kachelkaiser@htpst.de>
Co-authored-by: Henrik Sozzi <henrik_sozzi@hotmail.com>
Co-authored-by: EESF-2 <eesf-2@users.noreply.hosted.weblate.org>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: Aleksandre Ghvineria <Ghvinerias@gmail.com>
Co-authored-by: lateweb <weblate@techkoala.net>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Co-authored-by: Alvar Kusma <kaabuta@gmail.com>
Co-authored-by: Denny Schwender <denny.schwender@gmail.com>
Co-authored-by: Salizan <sohrab.sy1@gmail.com>
Co-authored-by: Jacob <jacob.venborg@gmail.com>
Co-authored-by: CyanWolf <hydemr@pm.me>
2025-12-18 22:05:31 -05:00
Zane Schepke 90a693abb4 feat: copy split config from existing 2025-12-18 16:28:44 -05:00
Patryk 05b950fbb3 fix: add check if kernel supports WG (#993) 2025-12-18 10:25:31 -05:00
Zane Schepke 5310cf17e6 chore: bump deps 2025-12-17 23:19:55 -05:00
dependabot[bot] 3ffc8c5e39 chore(deps): bump actions/upload-artifact from 5 to 6 (#1096)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-16 22:50:36 -05:00
dependabot[bot] e538ec80d0 chore(deps): bump actions/download-artifact from 6 to 7 (#1097)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-16 22:50:12 -05:00
Zane Schepke 6b24cca551 feat: improve amnezia label names
closes #1089
2025-12-15 12:36:50 -05:00
Zane Schepke f0c885708b fix: amnezia 2.0 header ranges 2025-12-14 22:30:54 -05:00
Zane Schepke c028b9b5b6 fix: logger start and clear 2025-12-09 10:45:34 -05:00
Zane Schepke de8b39d848 fix: amnezia 2.0 backwards compat, bump deps 2025-12-08 11:02:08 -05:00
Zane Schepke a573208db6 feat: amnezia 2.0 2025-12-03 00:53:35 -05:00
dependabot[bot] 0e9a63c850 chore(deps): bump actions/checkout from 5 to 6 (#1065)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-25 00:38:39 -05:00
Zane Schepke 341a806f29 chore: bump deps 2025-11-25 00:37:32 -05:00
Zane Schepke 8d257a604c fix: improve quickly state sync, nightly icon
closes #1056
closes #1055
2025-11-24 23:57:13 -05:00
Zane Schepke 0b674684ba fix: redundant location queries in legacy mode
closes #1062
2025-11-18 23:37:02 -05:00
Zane Schepke 85a27f48a2 chore: release v4.1.8 2025-11-14 14:11:22 -05:00
Zane Schepke 1f978cdf96 fix: rapid network changes race in network monitoring 2025-11-14 14:06:59 -05:00
Zane Schepke 4f816fa175 chore: release v4.1.7 2025-11-12 15:12:53 -05:00
Zane Schepke ee4ac4e968 fix: improve qr device support and scanner
#844
closes #1040
2025-11-12 14:13:35 -05:00
Zane Schepke ff53454966 fix: underlying network detection race
#1052
2025-11-12 11:59:20 -05:00
Zane Schepke 22c17ef66b fix: tile update crash when triggerd from non-user profile 2025-11-11 17:43:30 -05:00
Zane Schepke 7a60b90d2b fix: qr scanning scanning can cause crash 2025-11-11 17:29:05 -05:00
Zane Schepke 5fd3f89a59 feat: show tunnel uptime, improve duration display
closes #820
2025-11-11 16:20:08 -05:00
Zane Schepke 9510f43252 fix: global overrides regression, support prompt bug 2025-11-10 20:51:41 -05:00
Zane Schepke 064aa6aa74 fix: error notification bug 2025-11-10 00:56:56 -05:00
Zane Schepke 0c09add0e4 chore: add custom funding link 2025-11-09 12:42:21 -05:00
Zane Schepke fd0fd33f71 chore: release v4.1.6 2025-11-08 20:23:09 -05:00
Zane Schepke aaeb251bbf chore: shorten ur short description 2025-11-08 20:11:48 -05:00
Zane Schepke e563608e49 chore: bump deps 2025-11-08 20:06:09 -05:00
Zane Schepke 584f0386b6 fix: network monitor ignoring valid states for underlying networks 2025-11-08 14:00:14 -05:00
Zane Schepke cf49c34bff ci: simplify publish 2025-11-08 00:43:47 -05:00
Zane Schepke a0f89d40f5 chore: DE short description length too long 2025-11-08 00:17:29 -05:00
Zane Schepke 4da05e23f1 chore: release v4.1.5 2025-11-07 23:58:45 -05:00
Zane Schepke 6749719e21 chore: bump deps, update app description 2025-11-07 23:50:07 -05:00
Zane Schepke 1c160ff5f9 fix: network monitor should ignore default network VPN events
#1038
2025-11-07 21:54:16 -05:00
Zane Schepke 861440b7db fix: disable metered option for Android 9 and lower
closes #1044

#1031
2025-11-07 20:49:32 -05:00
Zane Schepke bdb0d27b53 ci: add aab build workflow 2025-11-05 00:47:46 -05:00
Zane Schepke 9b3283a2b1 chore: release 4.1.4 2025-11-04 20:20:41 -05:00
Zane Schepke 78def29980 fix: keep network monitor for full app lifecyle 2025-11-04 20:16:23 -05:00
Zane Schepke e83bbdf23a fix: tunnel service bind race 2025-11-04 19:59:30 -05:00
Zane Schepke 4beeb4e01e fix: network monitoring bug 2025-11-04 17:48:40 -05:00
Zane Schepke 4bcd810b38 chore: release 4.1.3 2025-11-04 03:57:24 -05:00
Zane Schepke e71174995b fix: tab back navigation bug 2025-11-04 03:39:23 -05:00
Zane Schepke f256a32bda fix: restore proper metered tunnel default
closes #1035
2025-11-04 03:03:24 -05:00
Zane Schepke c49666303a fix: network monitor changes for Android 10 2025-11-04 02:00:58 -05:00
Zane Schepke 3a9b435e50 fix: default wifi method needs flag 2025-11-03 11:52:34 -05:00
Zane Schepke 0993f60977 fix: auto tunnel service binder 2025-11-03 10:55:57 -05:00
Zane Schepke 3d88feb97c fix: r8 ip parsing bug
closes #1031
2025-11-03 09:45:56 -05:00
Zane Schepke f61e6d6c6e fix: network detection bug
closes #1032
2025-11-03 08:20:35 -05:00
Zane Schepke df864ade95 fix: binder leak 2025-11-03 02:24:19 -05:00
Zane Schepke 0abe3f67ef chore: fix fastlane deploy 2025-11-02 03:30:16 -05:00
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
Zane Schepke 6631ebcf49 ci: fix notifications 2025-10-21 15:13:39 -04:00
Zane Schepke c28e157616 fix: proxy mode vpn permission 2025-10-21 00:31:34 -04:00
Zane Schepke b2bd8574ec chore: release 4.1.1 2025-10-19 02:45:22 -04:00
Zane Schepke e8146f0b97 fix: peer stats ui bug
closes #1007
2025-10-19 02:41:33 -04:00
Zane Schepke 706513a5b0 fix: auto tunnel start show battery optimization 2025-10-19 02:15:00 -04:00
Zane Schepke a77aa4d92f ci: fix token 2025-10-17 18:35:14 -04:00
749 changed files with 16571 additions and 2705 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>.
+1
View File
@@ -1,3 +1,4 @@
ko_fi: zaneschepke
liberapay: zaneschepke
github: zaneschepke
custom: ["https://wgtunnel.com/donate/"]
+130
View File
@@ -0,0 +1,130 @@
name: build-aab
permissions:
contents: read
on:
workflow_dispatch:
inputs:
build_type:
type: choice
description: "Build type"
required: true
default: release
options:
- release
flavor:
type: choice
description: "Product flavor"
required: true
default: google
options:
- google
secrets:
SIGNING_KEY_ALIAS:
required: false
SIGNING_KEY_PASSWORD:
required: false
SIGNING_STORE_PASSWORD:
required: false
SERVICE_ACCOUNT_JSON:
required: false
KEYSTORE:
required: false
workflow_call:
inputs:
build_type:
type: string
description: "Build type"
required: true
default: release
flavor:
type: string
description: "Product flavor"
required: false
default: google
secrets:
SIGNING_KEY_ALIAS:
required: false
SIGNING_KEY_PASSWORD:
required: false
SIGNING_STORE_PASSWORD:
required: false
SERVICE_ACCOUNT_JSON:
required: false
KEYSTORE:
required: false
env:
UPLOAD_DIR_ANDROID: android_artifacts
jobs:
build:
runs-on: ubuntu-latest
env:
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
KEY_STORE_FILE: 'android_keystore.jks'
KEY_STORE_LOCATION: ${{ github.workspace }}/app/keystore/
outputs:
UPLOAD_DIR_ANDROID: ${{ env.UPLOAD_DIR_ANDROID }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up JDK 17
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: '17'
cache: gradle
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Decode Keystore
id: decode_keystore
uses: timheuer/base64-to-file@v1.2
with:
fileName: ${{ env.KEY_STORE_FILE }}
fileDir: ${{ env.KEY_STORE_LOCATION }}
encodedString: ${{ secrets.KEYSTORE }}
- name: Create keystore path env var
if: ${{ inputs.build_type != 'debug' }}
run: |
store_path=${{ env.KEY_STORE_LOCATION }}${{ env.KEY_STORE_FILE }}
echo "KEY_STORE_PATH=$store_path" >> $GITHUB_ENV
- name: Build AAB (noSplits=true)
run: |
flavor=${{ inputs.flavor }}
build_type=${{ inputs.build_type }}
case $build_type in
"release")
./gradlew :app:bundle${flavor^}Release \
-PnoSplits=true \
--info
;;
esac
- name: Get release AAB path
id: aab-path
run: |
AAB_PATH=$(find app/build/outputs/bundle -iname "*google*release*.aab" -type f | head -1)
if [ -z "$AAB_PATH" ]; then
echo "Error: AAB not found!" >&2
exit 1
fi
echo "Found AAB: $AAB_PATH"
echo "path=$AAB_PATH" >> $GITHUB_OUTPUT
- name: Upload AAB Artifact
uses: actions/upload-artifact@v6
with:
name: google-play-aab
path: ${{ steps.aab-path.outputs.path }}
retention-days: 7
if-no-files-found: error
+4 -8
View File
@@ -72,7 +72,7 @@ jobs:
outputs:
UPLOAD_DIR_ANDROID: ${{ env.UPLOAD_DIR_ANDROID }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up JDK 17
@@ -114,15 +114,11 @@ jobs:
- name: Get release apk path
id: apk-path
run: echo "path=$(find . -regex '^.*/build/outputs/apk/${{ inputs.flavor }}/${{ inputs.build_type }}/.*\.apk$' -type f | head -1 | tail -c+2)" >> $GITHUB_OUTPUT
- name: Upload APK
uses: actions/upload-artifact@v4
- name: Upload All APK Artifacts
uses: actions/upload-artifact@v6
with:
name: android_artifacts_${{ inputs.flavor }}
path: >-
app/build/outputs/apk/${{ inputs.flavor }}/${{ inputs.build_type }}/${{
inputs.flavor == 'fdroid' && inputs.build_type == 'release'
&& 'wgtunnel-fdroid-release-*.apk'
|| format('wgtunnel-{0}-v*.apk', inputs.flavor)
}}
app/build/outputs/apk/${{ inputs.flavor }}/${{ inputs.build_type }}/*.apk
retention-days: 1
if-no-files-found: warn
+7 -5
View File
@@ -16,7 +16,7 @@ jobs:
has_new_commits: ${{ steps.check.outputs.new_commits }}
steps:
- name: Checkout Repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Check for new commits
id: check
env:
@@ -26,6 +26,9 @@ jobs:
echo "new_commits=$NEW_COMMITS" >> $GITHUB_OUTPUT
build-standalone-nightly:
needs:
- check_commits
if: ${{ needs.check_commits.outputs.has_new_commits > 0 && inputs.release_type != 'none' }}
uses: ./.github/workflows/build.yml
secrets: inherit
with:
@@ -34,14 +37,13 @@ jobs:
publish:
needs:
- check_commits
- build-standalone-nightly
if: ${{ needs.check_commits.outputs.has_new_commits > 0 && inputs.release_type != 'none' }}
name: publish-nightly
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Install system dependencies
run: |
@@ -69,7 +71,7 @@ jobs:
run: mkdir ${{ github.workspace }}/temp
- name: Download artifacts
uses: actions/download-artifact@v5
uses: actions/download-artifact@v7
with:
pattern: android_artifacts_*
path: ${{ github.workspace }}/temp
@@ -124,4 +126,4 @@ jobs:
files: |
${{ github.workspace }}/temp/**/*.apk
env:
GITHUB_TOKEN: ${{ github.token }}
GITHUB_TOKEN: ${{ secrets.PAT }}
+115 -40
View File
@@ -1,73 +1,148 @@
name: notifications
permissions:
contents: write
packages: write
on:
issues:
types: [opened, closed]
release:
types: [published]
types: [published, prereleased]
jobs:
notify:
runs-on: ubuntu-latest
steps:
- name: Send to Telegram
- name: Send to Telegram - New Issue
if: github.event_name == 'issues' && github.event.action == 'opened'
env:
TITLE: ${{ github.event.issue.title }}
NUMBER: ${{ github.event.issue.number }}
USER: ${{ github.event.issue.user.login }}
BODY: ${{ github.event.issue.body || 'No body provided' }}
URL: ${{ github.event.issue.html_url }}
run: |
if [[ "${{ github.event_name }}" == "issues" && "${{ github.event.action }}" == "opened" ]]; then
TITLE="${{ github.event.issue.title }}"
NUMBER=${{ github.event.issue.number }}
USER="${{ github.event.issue.user.login }}"
BODY="${{ github.event.issue.body || 'No body provided' }}"
BODY_TRUNC="${BODY:0:200}" # Truncate to avoid spam
URL="${{ github.event.issue.html_url }}"
TEXT=$(echo -e "🆕 New Issue #$NUMBER: *$TITLE* by $USER\n\n$BODY_TRUNC\n\n[View Issue]($URL)")
elif [[ "${{ github.event_name }}" == "issues" && "${{ github.event.action }}" == "closed" ]]; then
TITLE="${{ github.event.issue.title }}"
NUMBER=${{ github.event.issue.number }}
USER="${{ github.event.issue.user.login }}"
URL="${{ github.event.issue.html_url }}"
TEXT=$(echo -e "✅ Issue Closed #$NUMBER: *$TITLE* by $USER\n\n[View Issue]($URL)")
elif [[ "${{ github.event_name }}" == "release" && "${{ github.event.action }}" == "published" ]]; then
NAME="${{ github.event.release.name }}"
TAG="${{ github.event.release.tag_name }}"
BODY="${{ github.event.release.body || 'No notes provided' }}"
BODY_TRUNC="${BODY:0:200}" # Truncate to avoid spam
URL="${{ github.event.release.html_url }}"
TEXT=$(echo -e "🚀 New Release *$NAME* ($TAG)\n\n$BODY_TRUNC\n\n[View Release]($URL)")
fi
BODY_TRUNC="${BODY:0:200}" # Truncate to avoid spam
TEXT=$(echo -e "🆕 New Issue #$NUMBER: *$TITLE* by $USER\n\n$BODY_TRUNC\n\n[View Issue]($URL)")
curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN }}/sendMessage" \
-d chat_id="${{ vars.TELEGRAM_CHAT_ID }}" \
${{ vars.TELEGRAM_THREAD_ID && format('-d message_thread_id="{0}"', vars.TELEGRAM_THREAD_ID) || '' }} \
-d parse_mode="Markdown" \
--data-urlencode "text=$TEXT"
- name: Send to Matrix
- name: Send to Telegram - Closed Issue
if: github.event_name == 'issues' && github.event.action == 'closed'
env:
TITLE: ${{ github.event.issue.title }}
NUMBER: ${{ github.event.issue.number }}
USER: ${{ github.event.issue.user.login }}
URL: ${{ github.event.issue.html_url }}
run: |
PLAIN_MESSAGE=""
HTML_MESSAGE=""
if [[ "${{ github.event_name }}" == "issues" && "${{ github.event.action }}" == "opened" ]]; then
PLAIN_MESSAGE=$(echo -e "🆕 New Issue #${{ github.event.issue.number }}: ${{ github.event.issue.title }} by ${{ github.event.issue.user.login }}\n\n${{ github.event.issue.body || 'No body provided' }}\n\nView Issue: ${{ github.event.issue.html_url }}")
HTML_MESSAGE=$(echo -e "<p>🆕 New Issue #${{ github.event.issue.number }}: <strong>${{ github.event.issue.title }}</strong> by ${{ github.event.issue.user.login }}</p><p>${{ github.event.issue.body || 'No body provided' }}</p><p><a href=\"${{ github.event.issue.html_url }}\">View Issue</a></p>")
elif [[ "${{ github.event_name }}" == "issues" && "${{ github.event.action }}" == "closed" ]]; then
PLAIN_MESSAGE=$(echo -e "✅ Issue Closed #${{ github.event.issue.number }}: ${{ github.event.issue.title }} by ${{ github.event.issue.user.login }}\n\nView Issue: ${{ github.event.issue.html_url }}")
HTML_MESSAGE=$(echo -e "<p>✅ Issue Closed #${{ github.event.issue.number }}: <strong>${{ github.event.issue.title }}</strong> by ${{ github.event.issue.user.login }}</p><p><a href=\"${{ github.event.issue.html_url }}\">View Issue</a></p>")
elif [[ "${{ github.event_name }}" == "release" && "${{ github.event.action }}" == "published" ]]; then
PLAIN_MESSAGE=$(echo -e "🚀 New Release ${{ github.event.release.name }} (${{ github.event.release.tag_name }})\n\n${{ github.event.release.body || 'No notes provided' }}\n\nView Release: ${{ github.event.release.html_url }}")
HTML_MESSAGE=$(echo -e "<p>🚀 New Release <strong>${{ github.event.release.name }}</strong> (${{ github.event.release.tag_name }})</p><p>${{ github.event.release.body || 'No notes provided' }}</p><p><a href=\"${{ github.event.release.html_url }}\">View Release</a></p>")
TEXT=$(echo -e "✅ Issue Closed #$NUMBER: *$TITLE* by $USER\n\n[View Issue]($URL)")
curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN }}/sendMessage" \
-d chat_id="${{ vars.TELEGRAM_CHAT_ID }}" \
${{ vars.TELEGRAM_THREAD_ID && format('-d message_thread_id="{0}"', vars.TELEGRAM_THREAD_ID) || '' }} \
-d parse_mode="Markdown" \
--data-urlencode "text=$TEXT"
- name: Send to Telegram - New Release
if: github.event_name == 'release' && ((github.event.action == 'published' && !github.event.release.prerelease) || (github.event.action == 'prereleased' && github.event.release.prerelease && github.event.release.name == 'nightly'))
env:
NAME: ${{ github.event.release.name }}
TAG: ${{ github.event.release.tag_name }}
BODY: ${{ github.event.release.body || 'No notes provided' }}
URL: ${{ github.event.release.html_url }}
ACTION: ${{ github.event.action }}
run: |
BODY_TRUNC="${BODY:0:200}" # Truncate to avoid spam
if [ "$ACTION" == "prereleased" ]; then
ICON="🌙"
PREFIX="New Nightly Release"
else
ICON="🚀"
PREFIX="New Release"
fi
TEXT=$(echo -e "$ICON $PREFIX *$NAME* ($TAG)\n\n$BODY_TRUNC\n\n[View Release]($URL)")
curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN }}/sendMessage" \
-d chat_id="${{ vars.TELEGRAM_CHAT_ID }}" \
${{ vars.TELEGRAM_THREAD_ID && format('-d message_thread_id="{0}"', vars.TELEGRAM_THREAD_ID) || '' }} \
-d parse_mode="Markdown" \
--data-urlencode "text=$TEXT"
- name: Send to Matrix - New Issue
if: github.event_name == 'issues' && github.event.action == 'opened'
env:
NUMBER: ${{ github.event.issue.number }}
TITLE: ${{ github.event.issue.title }}
USER: ${{ github.event.issue.user.login }}
BODY: ${{ github.event.issue.body || 'No body provided' }}
URL: ${{ github.event.issue.html_url }}
run: |
PLAIN_MESSAGE=$(echo -e "🆕 New Issue #$NUMBER: $TITLE by $USER\n\n$BODY\n\nView Issue: $URL")
HTML_MESSAGE=$(echo -e "<p>🆕 New Issue #$NUMBER: <strong>$TITLE</strong> by $USER</p><p>$BODY</p><p><a href=\"$URL\">View Issue</a></p>")
PLAIN_MESSAGE="${PLAIN_MESSAGE:0:220}"
PAYLOAD=$(jq -n --arg body "$PLAIN_MESSAGE" --arg formatted "$HTML_MESSAGE" '{
"msgtype": "m.text",
"body": $body,
"format": "org.matrix.custom.html",
"formatted_body": $formatted
}')
TXN_ID="${{ github.run_id }}-${{ github.run_attempt }}"
curl -s -X PUT "https://${{ vars.MATRIX_HOMESERVER }}/_matrix/client/v3/rooms/${{ vars.MATRIX_ROOM_ID }}/send/m.room.message/$TXN_ID" \
-H "Authorization: Bearer ${{ secrets.MATRIX_ACCESS_TOKEN }}" \
-H "Content-Type: application/json" \
-d "$PAYLOAD"
- name: Send to Matrix - Closed Issue
if: github.event_name == 'issues' && github.event.action == 'closed'
env:
NUMBER: ${{ github.event.issue.number }}
TITLE: ${{ github.event.issue.title }}
USER: ${{ github.event.issue.user.login }}
URL: ${{ github.event.issue.html_url }}
run: |
PLAIN_MESSAGE=$(echo -e "✅ Issue Closed #$NUMBER: $TITLE by $USER\n\nView Issue: $URL")
HTML_MESSAGE=$(echo -e "<p>✅ Issue Closed #$NUMBER: <strong>$TITLE</strong> by $USER</p><p><a href=\"$URL\">View Issue</a></p>")
PLAIN_MESSAGE="${PLAIN_MESSAGE:0:220}"
PAYLOAD=$(jq -n --arg body "$PLAIN_MESSAGE" --arg formatted "$HTML_MESSAGE" '{
"msgtype": "m.text",
"body": $body,
"format": "org.matrix.custom.html",
"formatted_body": $formatted
}')
TXN_ID="${{ github.run_id }}-${{ github.run_attempt }}"
curl -s -X PUT "https://${{ vars.MATRIX_HOMESERVER }}/_matrix/client/v3/rooms/${{ vars.MATRIX_ROOM_ID }}/send/m.room.message/$TXN_ID" \
-H "Authorization: Bearer ${{ secrets.MATRIX_ACCESS_TOKEN }}" \
-H "Content-Type: application/json" \
-d "$PAYLOAD"
- name: Send to Matrix - New Release
if: github.event_name == 'release' && ((github.event.action == 'published' && !github.event.release.prerelease) || (github.event.action == 'prereleased' && github.event.release.prerelease && github.event.release.name == 'nightly'))
env:
NAME: ${{ github.event.release.name }}
TAG: ${{ github.event.release.tag_name }}
BODY: ${{ github.event.release.body || 'No notes provided' }}
URL: ${{ github.event.release.html_url }}
ACTION: ${{ github.event.action }}
run: |
if [ "$ACTION" == "prereleased" ]; then
ICON="🌙"
PREFIX="New Nightly Release"
else
ICON="🚀"
PREFIX="New Release"
fi
PLAIN_MESSAGE=$(echo -e "$ICON $PREFIX $NAME ($TAG)\n\n$BODY\n\nView Release: $URL")
HTML_MESSAGE=$(echo -e "<p>$ICON $PREFIX <strong>$NAME</strong> ($TAG)</p><p>$BODY</p><p><a href=\"$URL\">View Release</a></p>")
PLAIN_MESSAGE="${PLAIN_MESSAGE:0:220}"
PAYLOAD=$(jq -n --arg body "$PLAIN_MESSAGE" --arg formatted "$HTML_MESSAGE" '{
"msgtype": "m.text",
"body": $body,
"format": "org.matrix.custom.html",
"formatted_body": $formatted
}')
TXN_ID="${{ github.run_id }}-${{ github.run_attempt }}"
curl -s -X PUT "https://${{ vars.MATRIX_HOMESERVER }}/_matrix/client/v3/rooms/${{ vars.MATRIX_ROOM_ID }}/send/m.room.message/$TXN_ID" \
-H "Authorization: Bearer ${{ secrets.MATRIX_ACCESS_TOKEN }}" \
-H "Content-Type: application/json" \
-d "$PAYLOAD"
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
format_check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Set up JDK 17
uses: actions/setup-java@v5
with:
+31 -21
View File
@@ -32,14 +32,6 @@ on:
description: "Tag name for release"
required: false
default: 1.1.1
flavor:
type: choice
description: "Product flavor"
required: true
default: standalone
options:
- fdroid
- standalone
workflow_call:
inputs:
flavor:
@@ -51,7 +43,11 @@ on:
jobs:
build-fdroid:
if: ${{ github.event_name == 'push' || inputs.release_type == 'release' || inputs.flavor == 'fdroid' }}
if: >-
${{
github.event_name == 'push' ||
inputs.release_type != 'none'
}}
uses: ./.github/workflows/build.yml
secrets: inherit
with:
@@ -59,20 +55,30 @@ jobs:
flavor: fdroid
build-standalone:
if: ${{ github.event_name == 'push' || inputs.release_type == 'release' || inputs.release_type == 'debug' || inputs.flavor == 'standalone' }}
if: >-
${{
github.event_name == 'push' ||
inputs.release_type != 'none'
}}
uses: ./.github/workflows/build.yml
secrets: inherit
with:
build_type: ${{ github.event_name == 'push' && 'release' || inputs.release_type }}
flavor: standalone
publish:
publish-github:
if: >-
${{
github.event_name == 'push' ||
inputs.release_type != 'none'
}}
needs:
- build-standalone
- build-fdroid
- build-standalone
name: publish-github
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
ref: ${{ github.event_name == 'push' && github.ref || 'master' }}
- name: Install system dependencies
@@ -93,7 +99,7 @@ jobs:
id: latest_release
uses: kaliber5/action-get-release@v1
with:
token: ${{ secrets.PAT }}
token: ${{ github.token }}
latest: true
- name: Generate Changelog
@@ -109,7 +115,7 @@ jobs:
run: mkdir ${{ github.workspace }}/temp
- name: Download artifacts
uses: actions/download-artifact@v5
uses: actions/download-artifact@v7
with:
pattern: android_artifacts_*
path: ${{ github.workspace }}/temp
@@ -118,8 +124,8 @@ jobs:
- name: Set version release notes
if: ${{ github.event_name == 'push' || inputs.release_type == 'release' }}
run: |
VERSION_NAME=$(grep "const val VERSION_NAME" buildSrc/src/main/kotlin/Constants.kt | awk -F'"' '{print $2}')
RELEASE_NOTES="$(cat ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/${VERSION_NAME}.txt || echo "No changelog found for ${VERSION_NAME}")"
VERSION_CODE=$(sed -nE 's/.*const val VERSION_CODE[[:space:]]*=[[:space:]]*([0-9]+).*/\1/p' buildSrc/src/main/kotlin/Constants.kt)
RELEASE_NOTES="$(cat ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/${VERSION_CODE}.txt || echo "No changelog found for ${VERSION_CODE}")"
echo "RELEASE_NOTES<<EOF" >> $GITHUB_ENV
echo "$RELEASE_NOTES" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
@@ -162,13 +168,17 @@ jobs:
files: |
${{ github.workspace }}/temp/**/*.apk
env:
GITHUB_TOKEN: ${{ github.token }}
GITHUB_TOKEN: ${{ secrets.PAT }}
publish-fdroid-public:
runs-on: ubuntu-latest
if: >-
${{
github.event_name == 'push' ||
inputs.release_type != 'none'
}}
needs:
- build-fdroid
if: ${{ github.event_name == 'push' || inputs.release_type == 'release' }}
- publish-github
steps:
- name: Dispatch update for fdroid repo
uses: peter-evans/repository-dispatch@v4
@@ -190,7 +200,7 @@ jobs:
KEY_STORE_LOCATION: ${{ github.workspace }}/app/keystore/
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Set up JDK 17
uses: actions/setup-java@v5
with:
+5 -8
View File
@@ -21,8 +21,7 @@ and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/)
<div align="center">
[![Google Play](https://img.shields.io/badge/Google_Play-414141?style=for-the-badge&logo=google-play&logoColor=white)](https://play.google.com/store/apps/details?id=com.zaneschepke.wireguardautotunnel)
[![F-Droid](https://img.shields.io/static/v1?style=for-the-badge&message=F-Droid&color=1976D2&logo=F-Droid&logoColor=FFFFFF&label=)](https://f-droid.org/packages/com.zaneschepke.wireguardautotunnel/)
[![Personal](https://img.shields.io/static/v1?style=for-the-badge&message=Personal&color=1976D2&logo=F-Droid&logoColor=FFFFFF&label=)](https://github.com/zaneschepke/fdroid)
[![F-Droid](https://img.shields.io/static/v1?style=for-the-badge&message=F-Droid&color=1976D2&logo=F-Droid&logoColor=FFFFFF&label=)](https://github.com/zaneschepke/fdroid)
[![Obtainium](https://img.shields.io/badge/Obtainium-414141?style=for-the-badge&logo=Obtainium&logoColor=white)](https://apps.obtainium.imranr.dev/redirect?r=obtainium://app/%7B%22id%22%3A%22com.zaneschepke.wireguardautotunnel%22%2C%22url%22%3A%22https%3A%2F%2Fgithub.com%2Fzaneschepke%2Fwgtunnel%22%2C%22author%22%3A%22zaneschepke%22%2C%22name%22%3A%22WG%20Tunnel%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Atrue%2C%5C%22sortMethodChoice%5C%22%3A%5C%22date%5C%22%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Atrue%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22WG%20Tunnel%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22Zane%20Schepke%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%7D%22%2C%22overrideSource%22%3Anull%7D)
</div>
@@ -60,14 +59,12 @@ WG Tunnel is an alternative Android client for WireGuard and AmneziaWG, inspired
</div>
<div style="display: flex; flex-wrap: wrap; justify-content: left; gap: 10px;">
<img label="Main" src="fastlane/metadata/android/en-US/images/phoneScreenshots/main_screen.png" width="200" />
<img label="Settings" src="fastlane/metadata/android/en-US/images/phoneScreenshots/settings_screen.png" width="200" />
<img label="Auto" src="fastlane/metadata/android/en-US/images/phoneScreenshots/auto_screen.png" width="200" />
<img label="Config" src="fastlane/metadata/android/en-US/images/phoneScreenshots/config_screen.png" width="200" />
<img label="Main" src="fastlane/metadata/android/en-US/images/phoneScreenshots/main_screen.png" width="200" alt="Main"/>
<img label="Config" src="fastlane/metadata/android/en-US/images/phoneScreenshots/config_screen.png" width="200" alt="Config"/>
<img label="Settings" src="fastlane/metadata/android/en-US/images/phoneScreenshots/settings_screen.png" width="200" alt="Settings"/>
<img label="Auto-tunnel" src="fastlane/metadata/android/en-US/images/phoneScreenshots/auto_screen.png" width="200" alt="Auto-tunnel"/>
</div>
<div style="text-align: left;">
## Features
- **Tunnel Import Methods**: Easily add tunnels using .conf files, ZIP archives, manual entry, or QR code scanning.
+40 -12
View File
@@ -1,3 +1,4 @@
import com.android.build.gradle.internal.api.BaseVariantOutputImpl
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
@@ -27,6 +28,15 @@ android {
// fix okhttp proguard issue
packaging { resources { pickFirsts.add("okhttp3/internal/publicsuffix/publicsuffixes.gz") } }
splits {
abi {
isEnable = !project.hasProperty("noSplits")
reset()
include("armeabi-v7a", "arm64-v8a")
isUniversalApk = !project.hasProperty("noSplits")
}
}
defaultConfig {
applicationId = Constants.APP_ID
minSdk = Constants.MIN_SDK
@@ -126,21 +136,37 @@ android {
licensee {
allowedLicenses().forEach { allow(it) }
allowedLicenseUrls().forEach { allowUrl(it) }
// foss, but missing license
ignoreDependencies("com.github.T8RIN.QuickieExtended")
}
applicationVariants.all {
android.applicationVariants.all {
val variant = this
variant.outputs
.map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl }
.forEach { output ->
val outputFileName =
if (variant.flavorName == "fdroid" && variant.buildType.name == "release") {
"${Constants.APP_NAME}-fdroid-release-${variant.versionName}.apk"
} else {
"${Constants.APP_NAME}-${variant.flavorName}-v${variant.versionName}.apk"
}
output.outputFileName = outputFileName
}
val abiNameMap =
mapOf(
"armeabi-v7a" to "armv7",
"arm64-v8a" to "arm64",
"x86" to "x86",
"x86_64" to "x64",
)
variant.outputs.all {
val output = this as BaseVariantOutputImpl
val abi = output.getFilter("ABI")
val baseFileName = "${Constants.APP_NAME}-${variant.flavorName}-v${variant.versionName}"
val outputFileName =
if (!abi.isNullOrEmpty()) {
val shortAbiName = abiNameMap.getOrDefault(abi, abi)
"${baseFileName}-${shortAbiName}.apk"
} else {
"${baseFileName}.apk"
}
output.outputFileName = outputFileName
}
}
}
@@ -218,6 +244,8 @@ dependencies {
debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.manifest)
debugImplementation(libs.leakcanary.android)
// Room database backup
implementation(libs.roomdatabasebackup) {
exclude(group = "org.reactivestreams", module = "reactive-streams")
@@ -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')"
]
}
}
@@ -0,0 +1,523 @@
{
"formatVersion": 1,
"database": {
"version": 29,
"identityHash": "345471c118dee1b7688afa81d835e62c",
"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 false)",
"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": "false"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_tunnel_config_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_tunnel_config_name` ON `${TABLE_NAME}` (`name`)"
}
]
},
{
"tableName": "proxy_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `socks5_proxy_enabled` INTEGER NOT NULL DEFAULT 0, `socks5_proxy_bind_address` TEXT, `http_proxy_enable` INTEGER NOT NULL DEFAULT 0, `http_proxy_bind_address` TEXT, `proxy_username` TEXT, `proxy_password` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "socks5ProxyEnabled",
"columnName": "socks5_proxy_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "socks5ProxyBindAddress",
"columnName": "socks5_proxy_bind_address",
"affinity": "TEXT"
},
{
"fieldPath": "httpProxyEnabled",
"columnName": "http_proxy_enable",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "httpProxyBindAddress",
"columnName": "http_proxy_bind_address",
"affinity": "TEXT"
},
{
"fieldPath": "proxyUsername",
"columnName": "proxy_username",
"affinity": "TEXT"
},
{
"fieldPath": "proxyPassword",
"columnName": "proxy_password",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "general_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT 0, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT 0, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `global_split_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `app_mode` INTEGER NOT NULL DEFAULT 0, `theme` TEXT NOT NULL DEFAULT 'AUTOMATIC', `locale` TEXT, `remote_key` TEXT, `is_remote_control_enabled` INTEGER NOT NULL DEFAULT 0, `is_pin_lock_enabled` INTEGER NOT NULL DEFAULT 0, `is_always_on_vpn_enabled` INTEGER NOT NULL DEFAULT 0, `already_donated` INTEGER NOT NULL DEFAULT 0)",
"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, '345471c118dee1b7688afa81d835e62c')"
]
}
}
@@ -4,10 +4,10 @@ import androidx.room.testing.MigrationTestHelper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
import java.io.IOException
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import java.io.IOException
@RunWith(AndroidJUnit4::class)
class MigrationTest {
+42 -16
View File
@@ -5,6 +5,9 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!--for split tunneling-->
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
<!--foreground service special use for non VPN service tunnels, android 14-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<!--foreground service special use for VPN service tunnels, android 14-->
@@ -14,6 +17,7 @@
<!--foreground service permissions-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<!--start service on boot permission-->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
@@ -42,17 +46,6 @@
<uses-feature android:name="android.hardware.wifi"
android:required="false"/>
<queries>
<intent>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent>
<intent>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent>
</queries>
<application
android:name=".WireGuardAutoTunnel"
android:allowBackup="false"
@@ -66,10 +59,7 @@
android:supportsRtl="true"
android:theme="@style/Theme.App.Start"
tools:targetApi="tiramisu">
<activity
android:name="com.journeyapps.barcodescanner.CaptureActivity"
android:screenOrientation="portrait"
tools:replace="screenOrientation" />
<activity
android:name=".MainActivity"
android:exported="true"
@@ -119,7 +109,40 @@
android:multiprocess="true"
tools:node="remove">
</provider>
<service
android:name=".core.service.tile.TunnelControlTile"
android:exported="true"
android:icon="@drawable/ic_notification"
android:label="@string/tunnel_control"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<meta-data
android:name="android.service.quicksettings.ACTIVE_TILE"
android:value="true" />
<meta-data
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
android:value="true" />
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
<service
android:name=".core.service.tile.AutoTunnelControlTile"
android:exported="true"
android:icon="@drawable/ic_notification"
android:label="@string/auto_tunnel"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<meta-data
android:name="android.service.quicksettings.ACTIVE_TILE"
android:value="true" />
<meta-data
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
android:value="true" />
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
<service
android:name=".core.service.tile.TunnelControlTile"
android:exported="true"
@@ -205,7 +228,10 @@
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
</receiver>
<receiver
@@ -18,9 +18,17 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextLinkStyles
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withLink
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
@@ -34,11 +42,12 @@ import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
import androidx.navigation3.ui.NavDisplay
import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
import com.zaneschepke.wireguardautotunnel.data.DataStoreManager.Companion.shouldShowDonationSnackbar
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
@@ -46,6 +55,9 @@ import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm
import com.zaneschepke.wireguardautotunnel.ui.common.banner.AppAlertBanner
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.VpnDeniedDialog
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.CustomSnackBar
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarInfo
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarType
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.rememberCustomSnackbarState
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
import com.zaneschepke.wireguardautotunnel.ui.navigation.Tab
import com.zaneschepke.wireguardautotunnel.ui.navigation.components.BottomNavbar
@@ -64,8 +76,8 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.Appear
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.display.DisplayScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.language.LanguageScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.dns.DnsSettingsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.globals.TunnelGlobalsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.integrations.AndroidIntegrationsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.lockdown.LockdownSettingsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.TunnelMonitoringScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.logs.LogsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.ping.PingTargetScreen
@@ -75,9 +87,9 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.support.donate.crypto.Addr
import com.zaneschepke.wireguardautotunnel.ui.screens.support.license.LicenseScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.TunnelsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.config.ConfigScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.settings.TunnelSettingsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.sort.SortScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.splittunnel.SplitTunnelScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.tunneloptions.TunnelOptionsScreen
import com.zaneschepke.wireguardautotunnel.ui.theme.AlertRed
import com.zaneschepke.wireguardautotunnel.ui.theme.OffWhite
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
@@ -89,7 +101,6 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.SplitTunnelViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelViewModel
import dagger.hilt.android.AndroidEntryPoint
import de.raphaelebner.roomdatabasebackup.core.RoomBackup
import java.util.*
import javax.inject.Inject
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
@@ -99,9 +110,9 @@ import xyz.teamgravity.pin_lock_compose.PinManager
class MainActivity : AppCompatActivity() {
@Inject lateinit var appStateRepository: AppStateRepository
@Inject lateinit var tunnelManager: TunnelManager
@Inject lateinit var networkMonitor: NetworkMonitor
@Inject lateinit var tunnelRepository: TunnelRepository
@Inject lateinit var appDatabase: AppDatabase
@Inject lateinit var networkMonitor: NetworkMonitor
private lateinit var roomBackup: RoomBackup
@@ -127,16 +138,16 @@ class MainActivity : AppCompatActivity() {
setContent {
val context = LocalContext.current
val isTv = isRunningOnTv()
val appState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
val scope = rememberCoroutineScope()
LaunchedEffect(appState.isAppLoaded) {
if (appState.isAppLoaded) {
appState.locale.let { LocaleUtil.changeLocale(it) }
LaunchedEffect(uiState.isAppLoaded) {
if (uiState.isAppLoaded) {
uiState.locale.let { LocaleUtil.changeLocale(it) }
}
}
val snackbar = remember { SnackbarHostState() }
val snackbarState = rememberCustomSnackbarState()
var showVpnPermissionDialog by remember { mutableStateOf(false) }
var vpnPermissionDenied by remember { mutableStateOf(false) }
var requestingAppMode by remember {
@@ -146,14 +157,14 @@ class MainActivity : AppCompatActivity() {
val startingStack = buildList {
add(Route.Tunnels)
if (intent?.action == Intent.ACTION_APPLICATION_PREFERENCES) add(Route.Settings)
if (appState.pinLockEnabled) add(Route.Lock)
if (uiState.pinLockEnabled) add(Route.Lock)
}
val backStack = rememberNavBackStack(*startingStack.toTypedArray())
var previousRoute by remember { mutableStateOf<Route?>(null) }
val navController =
rememberNavController<NavKey>(backStack, appState.isLocationDisclosureShown) {
rememberNavController<NavKey>(backStack, uiState.isLocationDisclosureShown) {
previousKey ->
previousRoute = previousKey as? Route
}
@@ -189,10 +200,20 @@ class MainActivity : AppCompatActivity() {
vpnActivity.launch(VpnService.prepare(this@MainActivity))
}
is GlobalSideEffect.Snackbar ->
is GlobalSideEffect.Snackbar -> {
scope.launch {
snackbar.showSnackbar(sideEffect.message.asString(context))
snackbarState.showSnackbar(
SnackbarInfo(
message =
buildAnnotatedString {
append(sideEffect.message.asString(context))
},
type = sideEffect.type ?: SnackbarType.INFO,
durationMs = sideEffect.durationMs ?: 4000L,
)
)
}
}
is GlobalSideEffect.Toast ->
scope.launch { context.showToast(sideEffect.message.asString(context)) }
@@ -203,19 +224,19 @@ class MainActivity : AppCompatActivity() {
}
}
if (!appState.isAppLoaded) return@setContent
if (!uiState.isAppLoaded) return@setContent
var showLock by remember {
mutableStateOf(appState.pinLockEnabled && !appState.isPinVerified)
mutableStateOf(uiState.pinLockEnabled && !uiState.isPinVerified)
}
LaunchedEffect(appState.isPinVerified) { if (appState.isPinVerified) showLock = false }
LaunchedEffect(uiState.isPinVerified) { if (uiState.isPinVerified) showLock = false }
CompositionLocalProvider(
LocalIsAndroidTV provides isTv,
LocalSharedVm provides viewModel,
LocalNavController provides navController,
) {
WireguardAutoTunnelTheme(theme = appState.theme) {
WireguardAutoTunnelTheme(theme = uiState.theme) {
VpnDeniedDialog(
showVpnPermissionDialog,
onDismiss = {
@@ -224,6 +245,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(Unit) {
if (
uiState.shouldShowDonationSnackbar && !uiState.settings.alreadyDonated
) {
viewModel.setShouldShowDonationSnackbar(false)
snackbarState.showSnackbar(
SnackbarInfo(
message = annotatedMessage,
type = SnackbarType.THANK_YOU,
durationMs = 30_000L,
)
)
}
}
if (showLock) {
PinManager.initialize(context = this@MainActivity)
PinLockScreen()
@@ -236,17 +306,17 @@ class MainActivity : AppCompatActivity() {
}
val navState by
currentRouteAsNavbarState(
appState,
uiState,
viewModel,
currentRoute,
navController,
)
Box(modifier = Modifier.fillMaxSize()) {
if (appState.settings.appMode == AppMode.LOCK_DOWN) {
if (uiState.settings.appMode == AppMode.LOCK_DOWN) {
AppAlertBanner(
stringResource(R.string.locked_down)
.uppercase(Locale.getDefault()),
.uppercase(Locale.current.platformLocale),
OffWhite,
AlertRed,
modifier = Modifier.fillMaxWidth().zIndex(2f),
@@ -254,14 +324,25 @@ class MainActivity : AppCompatActivity() {
}
Scaffold(
snackbarHost = {
SnackbarHost(snackbar) { snackbarData ->
snackbarState.SnackbarHost(
modifier =
Modifier.align(Alignment.BottomCenter)
.padding(
bottom =
if (LocalIsAndroidTV.current) 120.dp
else 80.dp
)
) { info ->
CustomSnackBar(
snackbarData.visuals.message,
isRtl = false,
message = info.message,
type = info.type,
onDismiss = { snackbarState.dismissCurrent() },
containerColor =
MaterialTheme.colorScheme.surfaceColorAtElevation(
2.dp
),
modifier =
Modifier.wrapContentHeight(align = Alignment.Top),
)
}
},
@@ -269,7 +350,7 @@ class MainActivity : AppCompatActivity() {
bottomBar = {
if (navState.showBottomItems) {
BottomNavbar(
appState.isAutoTunnelActive,
uiState.isAutoTunnelActive,
currentTab,
onTabSelected = { tab ->
navController.popUpTo(tab.startRoute)
@@ -331,7 +412,7 @@ class MainActivity : AppCompatActivity() {
}
entry<Route.Tunnels> { TunnelsScreen() }
entry<Route.Sort> { SortScreen() }
entry<Route.TunnelOptions> { key ->
entry<Route.TunnelSettings> { key ->
val viewModel =
hiltViewModel<
TunnelViewModel,
@@ -341,7 +422,7 @@ class MainActivity : AppCompatActivity() {
factory.create(key.id)
}
)
TunnelOptionsScreen(viewModel)
TunnelSettingsScreen(viewModel)
}
entry<Route.SplitTunnel> { key ->
val viewModel =
@@ -388,9 +469,6 @@ class MainActivity : AppCompatActivity() {
AndroidIntegrationsScreen()
}
entry<Route.Dns> { DnsSettingsScreen() }
entry<Route.TunnelGlobals> { key ->
TunnelGlobalsScreen(key.id)
}
entry<Route.ConfigGlobal> { key ->
val viewModel =
hiltViewModel<
@@ -415,6 +493,9 @@ class MainActivity : AppCompatActivity() {
)
SplitTunnelScreen(viewModel)
}
entry<Route.LockdownSettings> {
LockdownSettingsScreen()
}
entry<Route.ProxySettings> { ProxySettingsScreen() }
entry<Route.Appearance> { AppearanceScreen() }
entry<Route.Language> { LanguageScreen() }
@@ -441,8 +522,8 @@ class MainActivity : AppCompatActivity() {
override fun onResume() {
super.onResume()
WireGuardAutoTunnel.setUiActive(true)
networkMonitor.checkPermissionsAndUpdateState()
WireGuardAutoTunnel.setUiActive(true)
}
override fun onPause() {
@@ -452,6 +533,9 @@ class MainActivity : AppCompatActivity() {
fun performBackup() =
lifecycleScope.launch {
// reset active tuns before backup to prevent trying to start them without permission on
// restore
tunnelRepository.resetActiveTunnels()
roomBackup
.database(appDatabase)
.backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG)
@@ -7,20 +7,18 @@ import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration
import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationMonitor
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.core.worker.ServiceWorker
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
import dagger.hilt.android.HiltAndroidApp
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
@@ -43,8 +41,6 @@ class WireGuardAutoTunnel : Application(), Configuration.Provider {
@Inject lateinit var notificationMonitor: NotificationMonitor
@Inject lateinit var tunnelManager: TunnelManager
override fun onCreate() {
super.onCreate()
instance = this
@@ -64,8 +60,15 @@ class WireGuardAutoTunnel : Application(), Configuration.Provider {
applicationScope.launch(ioDispatcher) {
launch {
val monitoringSettings = monitoringRepository.getMonitoringSettings()
if (monitoringSettings.isLocalLogsEnabled) logReader.start()
monitoringRepository.flow
.distinctUntilChangedBy { it.isLocalLogsEnabled }
.collect { settings ->
if (settings.isLocalLogsEnabled) {
logReader.start()
} else {
logReader.stop()
}
}
}
launch { notificationMonitor.handleApplicationNotifications() }
}
@@ -73,12 +76,6 @@ class WireGuardAutoTunnel : Application(), Configuration.Provider {
ServiceWorker.start(this)
}
override fun onTerminate() {
applicationScope.cancel()
tunnelManager.setBackendMode(BackendMode.Inactive)
super.onTerminate()
}
companion object {
private val _uiActive = MutableStateFlow(false)
@@ -6,6 +6,7 @@ import android.content.Intent
import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
@@ -19,6 +20,8 @@ class RestartReceiver : BroadcastReceiver() {
@Inject lateinit var tunnelManager: TunnelManager
@Inject lateinit var appStateRepository: AppStateRepository
@Inject lateinit var logReader: LogReader
override fun onReceive(context: Context, intent: Intent) {
@@ -31,8 +34,10 @@ class RestartReceiver : BroadcastReceiver() {
tunnelManager.handleReboot()
}
Intent.ACTION_MY_PACKAGE_REPLACED -> {
Timber.i("Restoring state on package upgrade")
tunnelManager.handleRestore()
logReader.deleteAndClearLogs()
appStateRepository.setShouldShowDonationSnackbar(true)
}
}
}
@@ -32,7 +32,7 @@ constructor(
description =
StringValue.StringResource(
R.string.tunnel_error_template,
error.toStringRes(),
error.stringRes,
),
groupKey = NotificationManager.VPN_GROUP_KEY,
)
@@ -1,5 +1,11 @@
package com.zaneschepke.wireguardautotunnel.core.service
import android.os.Binder
import java.lang.ref.WeakReference
class LocalBinder(val service: TunnelService) : Binder()
class LocalBinder(service: TunnelService) : Binder() {
private val serviceRef = WeakReference(service)
val service: TunnelService?
get() = serviceRef.get()
}
@@ -21,6 +21,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import timber.log.Timber
class ServiceManager
@@ -137,17 +138,25 @@ constructor(
suspend fun startTunnelService(appMode: AppMode) =
tunnelMutex.withLock {
if (_tunnelService.value != null) return@withLock
val serviceClass =
when (appMode) {
AppMode.VPN,
AppMode.LOCK_DOWN -> VpnForegroundService::class.java
AppMode.KERNEL,
AppMode.PROXY -> TunnelForegroundService::class.java
}
val intent = Intent(context, serviceClass)
context.startForegroundService(intent)
context.bindService(intent, tunnelServiceConnection, Context.BIND_AUTO_CREATE)
if (_tunnelService.value != null) {
Timber.d("Service already exists, waiting for disconnect")
withTimeoutOrNull(2000L) { _tunnelService.first { it == null } }
?: Timber.w("Timeout waiting for existing service to disconnect")
}
if (_tunnelService.value == null) {
val serviceClass =
when (appMode) {
AppMode.VPN,
AppMode.LOCK_DOWN -> VpnForegroundService::class.java
AppMode.KERNEL,
AppMode.PROXY -> TunnelForegroundService::class.java
}
val intent = Intent(context, serviceClass)
context.startForegroundService(intent)
context.bindService(intent, tunnelServiceConnection, Context.BIND_AUTO_CREATE)
} else {
Timber.e("Service still not null after timeout")
}
}
suspend fun stopTunnelService() =
@@ -157,7 +166,7 @@ constructor(
try {
context.unbindService(tunnelServiceConnection)
} catch (e: Exception) {
Timber.e(e, "Failed to stop Tunnel Service")
Timber.e(e, "Failed to unbind Tunnel Service")
}
}
}
@@ -24,11 +24,12 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsR
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.state.AutoTunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.NetworkState
import com.zaneschepke.wireguardautotunnel.domain.state.toDomain
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.to
import com.zaneschepke.wireguardautotunnel.util.extensions.toMillis
import dagger.hilt.android.AndroidEntryPoint
import java.lang.ref.WeakReference
import javax.inject.Inject
import javax.inject.Provider
import kotlinx.coroutines.*
@@ -60,7 +61,16 @@ class AutoTunnelService : LifecycleService() {
private val autoTunnelStateFlow = MutableStateFlow(defaultState)
class LocalBinder(val service: AutoTunnelService) : Binder()
private var autoTunnelJob: Job? = null
private var permissionsJob: Job? = null
private var autoTunnelFailoverJob: Job? = null
class LocalBinder(service: AutoTunnelService) : Binder() {
private val serviceRef = WeakReference(service)
val service: AutoTunnelService?
get() = serviceRef.get()
}
private val binder = LocalBinder(this)
@@ -83,8 +93,10 @@ class AutoTunnelService : LifecycleService() {
fun start() {
launchWatcherNotification()
startAutoTunnelStateJob()
startLocationPermissionsNotificationJob()
autoTunnelJob?.cancel()
autoTunnelJob = startAutoTunnelStateJob()
permissionsJob?.cancel()
permissionsJob = startLocationPermissionsNotificationJob()
}
fun stop() {
@@ -123,12 +135,12 @@ class AutoTunnelService : LifecycleService() {
)
}
private fun startAutoTunnelStateJob() =
private fun startAutoTunnelStateJob(): Job =
lifecycleScope.launch(ioDispatcher) {
val networkFlow =
debouncedConnectivityStateFlow
.flowOn(ioDispatcher)
.map(NetworkState::from)
.map { it.toDomain() }
.map(::NetworkChange)
.distinctUntilChanged()
@@ -201,6 +213,7 @@ class AutoTunnelService : LifecycleService() {
handleAutoTunnelEvent(autoTunnelStateFlow.value.determineAutoTunnelEvent(change))
// re-evaluate network state after a short duration to prevent missed state changes
reevaluationJob = launch {
val snapshotNetwork = autoTunnelStateFlow.value.networkState
delay(REEVALUATE_CHECK_DELAY)
@@ -223,7 +236,7 @@ class AutoTunnelService : LifecycleService() {
return combine(
settingsRepository.flow.map { it.appMode }.distinctUntilChanged(),
autoTunnelRepository.get().flow,
tunnelsRepository.flow.map { tunnels ->
tunnelsRepository.userTunnelsFlow.map { tunnels ->
// isActive is ignored for equality checks so user can manually toggle off
// tunnel with auto-tunnel
tunnels.map { it.copy(isActive = false) }
@@ -265,8 +278,8 @@ class AutoTunnelService : LifecycleService() {
.map {
NetworkPermissionState(
it.settings.wifiDetectionMethod.to(),
it.networkState.locationServicesEnabled == true,
it.networkState.locationPermissionGranted == true,
it.networkState.locationServicesEnabled,
it.networkState.locationPermissionGranted,
(it.tunnels.any { tunnel -> tunnel.tunnelNetworks.isNotEmpty() } ||
it.settings.trustedNetworkSSIDs.isNotEmpty()),
)
@@ -347,6 +360,7 @@ class AutoTunnelService : LifecycleService() {
}
}
// restart network flow on debounce changes
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
private val debouncedConnectivityStateFlow: Flow<ConnectivityState> by lazy {
autoTunnelRepository
@@ -4,15 +4,13 @@ import android.content.Intent
import android.os.IBinder
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.*
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlin.concurrent.atomics.AtomicBoolean
import kotlin.concurrent.atomics.ExperimentalAtomicApi
import kotlinx.coroutines.launch
import timber.log.Timber
@@ -20,10 +18,11 @@ import timber.log.Timber
class AutoTunnelControlTile : TileService(), LifecycleOwner {
@Inject lateinit var autoTunnelSettingsRepository: AutoTunnelSettingsRepository
@Inject lateinit var tunnelsRepository: TunnelRepository
@Inject lateinit var serviceManager: ServiceManager
@OptIn(ExperimentalAtomicApi::class) val isCollecting = AtomicBoolean(false)
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
override fun onCreate() {
@@ -36,25 +35,37 @@ class AutoTunnelControlTile : TileService(), LifecycleOwner {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
}
override fun onStartListening() {
super.onStartListening()
override fun onTileAdded() {
super.onTileAdded()
initTileState()
}
override fun onStopListening() {
super.onStopListening()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
}
@OptIn(ExperimentalAtomicApi::class)
private fun initTileState() {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
Timber.d("Start listening called for auto tunnel tile")
lifecycleScope.launch {
serviceManager.autoTunnelService.collect {
if (it != null) return@collect setActive()
setInactive()
}
}
lifecycleScope.launch {
tunnelsRepository.flow.collect {
if (it.isEmpty()) {
setUnavailable()
if (isCollecting.compareAndSet(expectedValue = false, newValue = true)) {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
serviceManager.autoTunnelService.collect {
if (it != null) return@collect setActive()
setInactive()
}
}
}
}
}
override fun onStartListening() {
super.onStartListening()
initTileState()
}
override fun onClick() {
super.onClick()
unlockAndRun {
@@ -71,23 +82,16 @@ class AutoTunnelControlTile : TileService(), LifecycleOwner {
}
private fun setActive() {
runCatching {
qsTile.state = Tile.STATE_ACTIVE
qsTile.updateTile()
qsTile?.let {
it.state = Tile.STATE_ACTIVE
it.updateTile()
}
}
private fun setInactive() {
runCatching {
qsTile.state = Tile.STATE_INACTIVE
qsTile.updateTile()
}
}
private fun setUnavailable() {
runCatching {
qsTile.state = Tile.STATE_UNAVAILABLE
qsTile.updateTile()
qsTile?.let {
it.state = Tile.STATE_INACTIVE
it.updateTile()
}
}
@@ -5,10 +5,7 @@ import android.os.Build
import android.os.IBinder
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.*
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
@@ -16,7 +13,12 @@ import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlin.concurrent.atomics.AtomicBoolean
import kotlin.concurrent.atomics.ExperimentalAtomicApi
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import timber.log.Timber
@AndroidEntryPoint
@@ -28,9 +30,11 @@ class TunnelControlTile : TileService(), LifecycleOwner {
@Inject lateinit var tunnelManager: TunnelManager
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
@OptIn(ExperimentalAtomicApi::class) val isCollecting = AtomicBoolean(false)
private var isCollecting = false
private val startLock = Mutex()
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
override fun onCreate() {
super.onCreate()
@@ -42,13 +46,34 @@ class TunnelControlTile : TileService(), LifecycleOwner {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
}
override fun onStartListening() {
super.onStartListening()
override fun onTileAdded() {
super.onTileAdded()
initTileState()
}
@OptIn(ExperimentalAtomicApi::class)
private fun initTileState() {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
Timber.d("Start listening called for tunnel tile")
if (isCollecting) return
isCollecting = true
lifecycleScope.launch { tunnelManager.activeTunnels.collect { updateTileState() } }
if (isCollecting.compareAndSet(expectedValue = false, newValue = true)) {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
tunnelManager.activeTunnels
.distinctUntilChangedBy { it.size }
.collect { updateTileState() }
}
}
}
}
override fun onStartListening() {
super.onStartListening()
initTileState()
}
override fun onStopListening() {
super.onStopListening()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
}
private suspend fun updateTileState() {
@@ -76,6 +101,7 @@ class TunnelControlTile : TileService(), LifecycleOwner {
else -> updateTileForLastActiveTunnels()
}
} catch (e: Exception) {
Timber.e(e, "Failed to update tunnel state")
setUnavailable()
}
}
@@ -110,14 +136,16 @@ class TunnelControlTile : TileService(), LifecycleOwner {
super.onClick()
unlockAndRun {
lifecycleScope.launch {
if (tunnelManager.activeTunnels.value.isNotEmpty())
return@launch tunnelManager.stopActiveTunnels()
val lastActive = WireGuardAutoTunnel.getLastActiveTunnels()
if (lastActive.isEmpty()) {
tunnelsRepository.getStartTunnel()?.let { tunnelManager.startTunnel(it) }
} else {
lastActive.forEach { id ->
tunnelsRepository.getById(id)?.let { tunnelManager.startTunnel(it) }
startLock.withLock {
if (tunnelManager.activeTunnels.value.isNotEmpty())
return@launch tunnelManager.stopActiveTunnels()
val lastActive = WireGuardAutoTunnel.getLastActiveTunnels()
if (lastActive.isEmpty()) {
tunnelsRepository.getStartTunnel()?.let { tunnelManager.startTunnel(it) }
} else {
lastActive.forEach { id ->
tunnelsRepository.getById(id)?.let { tunnelManager.startTunnel(it) }
}
}
}
}
@@ -125,37 +153,36 @@ class TunnelControlTile : TileService(), LifecycleOwner {
}
private fun setActive() {
runCatching {
qsTile.state = Tile.STATE_ACTIVE
qsTile.updateTile()
qsTile?.let {
it.state = Tile.STATE_ACTIVE
it.updateTile()
}
}
private fun setInactive() {
runCatching {
qsTile.state = Tile.STATE_INACTIVE
qsTile.updateTile()
qsTile?.let {
it.state = Tile.STATE_INACTIVE
it.updateTile()
}
}
private fun setUnavailable() {
runCatching {
qsTile.state = Tile.STATE_UNAVAILABLE
qsTile?.let {
it.state = Tile.STATE_UNAVAILABLE
setTileDescription("")
qsTile.updateTile()
it.updateTile()
}
}
private fun setTileDescription(description: String) {
runCatching {
if (qsTile == null) return@runCatching
qsTile?.let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
qsTile.subtitle = description
qsTile.stateDescription = description
it.subtitle = description
it.stateDescription = description
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
qsTile.subtitle = description
it.subtitle = description
}
qsTile.updateTile()
it.updateTile()
}
}
@@ -106,9 +106,6 @@ abstract class BaseTunnel(
) {
return Timber.w("Tunnel is already running: ${tunnelConfig.name}")
}
updateTunnelStatus(tunnelConfig.id, TunnelStatus.Starting)
val job =
applicationScope.launch(ioDispatcher) {
try {
@@ -31,7 +31,7 @@ fun MutableStateFlow<Map<TunnelConfig, TunnelState>>.exists(id: Int): Boolean {
}
fun MutableStateFlow<Map<TunnelConfig, TunnelState>>.isUp(id: Int): Boolean {
return this.value.any { it.key.id == id && it.value.status == TunnelStatus.Up }
return this.value.any { it.key.id == id && it.value.status is TunnelStatus.Up }
}
fun MutableStateFlow<Map<TunnelConfig, TunnelState>>.isStarting(id: Int): Boolean {
@@ -3,18 +3,25 @@ package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.wireguard.android.backend.Backend
import com.wireguard.android.backend.BackendException
import com.wireguard.android.backend.Tunnel as WgTunnel
import com.wireguard.android.backend.WgQuickBackend
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.di.Kernel
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException
import com.zaneschepke.wireguardautotunnel.domain.events.DnsFailure
import com.zaneschepke.wireguardautotunnel.domain.events.InvalidConfig
import com.zaneschepke.wireguardautotunnel.domain.events.KernelTunnelName
import com.zaneschepke.wireguardautotunnel.domain.events.KernelWireguardNotSupported
import com.zaneschepke.wireguardautotunnel.domain.events.UnknownError
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.domain.state.WireGuardStatistics
import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendCoreException
import java.util.concurrent.ConcurrentHashMap
import java.util.regex.Pattern
import javax.inject.Inject
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
@@ -30,14 +37,28 @@ class KernelTunnel
constructor(
@ApplicationScope applicationScope: CoroutineScope,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
private val runConfigHelper: RunConfigHelper,
@Kernel private val backend: Backend,
) : BaseTunnel(applicationScope, ioDispatcher) {
private val runtimeTunnels = ConcurrentHashMap<Int, WgTunnel>()
// TODO Add DNS settings
private fun validateWireGuardInterfaceName(name: String): Result<Unit> {
if (name.isEmpty() || name.length > 15)
return Result.failure(KernelTunnelName(R.string.kernel_name_error))
if (name == "." || name == "..") {
return Result.failure(KernelTunnelName(R.string.kernel_name_dots))
}
val pattern = Pattern.compile("^[a-zA-Z0-9_=+.-]{1,15}$")
if (!pattern.matcher(name).matches()) {
return Result.failure(KernelTunnelName(R.string.kernel_name_special_characters))
}
return Result.success(Unit)
}
override fun tunnelStateFlow(tunnelConfig: TunnelConfig): Flow<TunnelStatus> = callbackFlow {
if (!tunnelConfig.isNameKernelCompatible) close(BackendCoreException.TunnelNameTooLong)
if (!WgQuickBackend.hasKernelSupport()) close(KernelWireguardNotSupported())
validateWireGuardInterfaceName(tunnelConfig.name).onFailure { close(it) }
val stateChannel = Channel<WgTunnel.State>()
@@ -51,21 +72,22 @@ constructor(
try {
withTimeout(STARTUP_TIMEOUT_MS) {
updateTunnelStatus(tunnelConfig.id, TunnelStatus.Starting)
backend.setState(runtimeTunnel, WgTunnel.State.UP, tunnelConfig.toWgConfig())
val runConfig = runConfigHelper.buildWgRunConfig(tunnelConfig)
backend.setState(runtimeTunnel, WgTunnel.State.UP, runConfig)
}
} catch (e: TimeoutCancellationException) {
Timber.e("Startup timed out for ${tunnelConfig.name}")
errors.emit(tunnelConfig.name to BackendCoreException.DNS)
errors.emit(tunnelConfig.name to DnsFailure())
forceStopTunnel(tunnelConfig.id)
close()
} catch (e: BackendException) {
close(e.toBackendCoreException())
} catch (e: IllegalArgumentException) {
Timber.e(e, "Invalid backend arguments")
close(BackendCoreException.Config)
close(InvalidConfig())
} catch (e: Exception) {
Timber.e(e, "Error while setting tunnel state")
close(BackendCoreException.Unknown)
close(UnknownError())
}
awaitClose {
@@ -0,0 +1,104 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.data.model.DnsProtocol
import com.zaneschepke.wireguardautotunnel.domain.events.InvalidConfig
import com.zaneschepke.wireguardautotunnel.domain.model.DnsSettings
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings
import com.zaneschepke.wireguardautotunnel.domain.model.ProxySettings
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.repository.DnsSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.ProxySettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import java.util.*
import javax.inject.Inject
import kotlinx.coroutines.flow.firstOrNull
import org.amnezia.awg.config.Config
import org.amnezia.awg.config.proxy.HttpProxy
import org.amnezia.awg.config.proxy.Socks5Proxy
class RunConfigHelper
@Inject
constructor(
private val settingsRepository: GeneralSettingRepository,
private val proxySettingsRepository: ProxySettingsRepository,
private val dnsSettingsRepository: DnsSettingsRepository,
private val tunnelsRepository: TunnelRepository,
) {
private data class PrepResult(
val effectiveConfig: TunnelConfig,
val generalSettings: GeneralSettings,
val dnsSettings: DnsSettings,
)
private suspend fun prepare(tunnelConfig: TunnelConfig): PrepResult {
val generalSettings = settingsRepository.getGeneralSettings()
val dnsSettings = dnsSettingsRepository.getDnsSettings()
val effectiveConfig =
if (
generalSettings.isGlobalSplitTunnelEnabled || dnsSettings.isGlobalTunnelDnsEnabled
) {
val globalConfig =
tunnelsRepository.globalTunnelFlow.firstOrNull() ?: throw InvalidConfig()
tunnelConfig.copyWithGlobalValues(
globalConfig,
dnsSettings.isGlobalTunnelDnsEnabled,
generalSettings.isGlobalSplitTunnelEnabled,
)
} else {
tunnelConfig
}
return PrepResult(effectiveConfig, generalSettings, dnsSettings)
}
suspend fun buildAmRunConfig(tunnelConfig: TunnelConfig): Config {
val prep = prepare(tunnelConfig)
val proxies =
if (prep.generalSettings.appMode == AppMode.PROXY) {
val proxySettings = proxySettingsRepository.getProxySettings()
buildList {
if (proxySettings.socks5ProxyEnabled) {
add(
Socks5Proxy(
proxySettings.socks5ProxyBindAddress
?: ProxySettings.DEFAULT_SOCKS_BIND_ADDRESS,
proxySettings.proxyUsername,
proxySettings.proxyPassword,
)
)
}
if (proxySettings.httpProxyEnabled) {
add(
HttpProxy(
proxySettings.httpProxyBindAddress
?: ProxySettings.DEFAULT_HTTP_BIND_ADDRESS,
proxySettings.proxyUsername,
proxySettings.proxyPassword,
)
)
}
}
} else {
emptyList()
}
val amConfig = prep.effectiveConfig.toAmConfig()
return Config.Builder()
.setInterface(amConfig.`interface`)
.addPeers(amConfig.peers)
.addProxies(proxies)
.setDnsSettings(
org.amnezia.awg.config.DnsSettings(
prep.dnsSettings.dnsProtocol == DnsProtocol.DOH,
Optional.ofNullable(prep.dnsSettings.dnsEndpoint),
)
)
.build()
}
suspend fun buildWgRunConfig(tunnelConfig: TunnelConfig): com.wireguard.config.Config {
val prep = prepare(tunnelConfig)
return prep.effectiveConfig.toWgConfig()
}
}
@@ -16,4 +16,6 @@ class RuntimeAwgTunnel(
}
override fun isIpv4ResolutionPreferred() = tunnelConfig.isIpv4Preferred
override fun isMetered() = tunnelConfig.isMetered
}
@@ -1,18 +1,19 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig as Entity
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.di.*
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException
import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage
import com.zaneschepke.wireguardautotunnel.domain.events.NotAuthorized
import com.zaneschepke.wireguardautotunnel.domain.model.AutoTunnelSettings
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.LockdownSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.state.LogHealthState
import com.zaneschepke.wireguardautotunnel.domain.state.PingState
@@ -39,6 +40,7 @@ constructor(
private val serviceManager: ServiceManager,
private val settingsRepository: GeneralSettingRepository,
private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
private val lockdownSettingsRepository: LockdownSettingsRepository,
private val tunnelsRepository: TunnelRepository,
private val tunnelMonitor: TunnelMonitor,
@ApplicationScope private val applicationScope: CoroutineScope,
@@ -69,12 +71,6 @@ constructor(
val condition: (SideEffectState) -> Boolean,
)
private suspend fun getSettings(): GeneralSettings =
settingsRepository.flow.filterNotNull().first { it != GeneralSettings() }
private suspend fun getTunnels(): List<TunnelConfig> =
tunnelsRepository.flow.first { it.isNotEmpty() }
private val tunnelProviderFlow: StateFlow<TunnelProvider> = run {
val currentBackend = AtomicReference(userspaceTunnel)
val currentSettings = AtomicReference(GeneralSettings())
@@ -84,10 +80,7 @@ constructor(
.filterNotNull()
// ignore default state
.filterNot { it == GeneralSettings() }
.distinctUntilChanged { old, new ->
old.appMode == new.appMode &&
old.isLanOnKillSwitchEnabled == new.isLanOnKillSwitchEnabled
}
.distinctUntilChangedBy { it.appMode }
.map { settings ->
Timber.d("App mode changes with ${settings.appMode}")
val backend =
@@ -108,7 +101,7 @@ constructor(
handleModeChangeCleanup(previousBackend, previousSettings.appMode)
}
if (settings.appMode == AppMode.LOCK_DOWN) {
handleLockDownModeInit(settings.isLanOnKillSwitchEnabled)
handleLockDownModeInit()
}
}
.map { (_, backend) -> backend }
@@ -235,17 +228,7 @@ constructor(
activeTunnels.first { it.isEmpty() }
} ?: run { activeTunnels.value.keys.forEach { id -> provider.forceStopTunnel(id) } }
}
val runConfig =
tunnelConfig.run {
if (getSettings().isTunnelGlobalsEnabled) {
val globalTunnel =
getTunnels().firstOrNull { it.name == Entity.GLOBAL_CONFIG_NAME }
?: return@run this
return@run copyWithGlobalValues(globalTunnel)
}
this
}
tunnelProviderFlow.value.startTunnel(runConfig)
tunnelProviderFlow.value.startTunnel(tunnelConfig)
}
override suspend fun stopTunnel(tunnelId: Int) {
@@ -302,13 +285,23 @@ constructor(
serviceManager.updateTunnelTile()
}
private fun handleLockDownModeInit(withLanBypass: Boolean) {
val allowedIps = if (withLanBypass) TunnelConfig.IPV4_PUBLIC_NETWORKS else emptySet()
// TODO this can crash if we haven't started foreground service yet, especially for
// workerManager
private suspend fun handleLockDownModeInit() {
val lockdownSettings = lockdownSettingsRepository.getLockdownSettings()
val allowedIps =
if (lockdownSettings.bypassLan) TunnelConfig.IPV4_PUBLIC_NETWORKS else emptySet()
try {
if (serviceManager.hasVpnPermission()) {
proxyUserspaceTunnel.setBackendMode(BackendMode.KillSwitch(allowedIps))
proxyUserspaceTunnel.setBackendMode(
BackendMode.KillSwitch(
allowedIps,
lockdownSettings.metered,
lockdownSettings.dualStack,
)
)
} else {
throw BackendCoreException.NotAuthorized
throw NotAuthorized()
}
} catch (e: BackendCoreException) {
localErrorEvents.tryEmit(null to e)
@@ -329,21 +322,22 @@ constructor(
withContext(ioDispatcher) {
val settings = settingsRepository.getGeneralSettings()
val autoTunnelSettings = autoTunnelSettingsRepository.getAutoTunnelSettings()
val tunnels = tunnelsRepository.getAll()
val tunnels = tunnelsRepository.userTunnelsFlow.firstOrNull()
if (autoTunnelSettings.isAutoTunnelEnabled)
return@withContext restoreAutoTunnel(autoTunnelSettings)
when (val mode = settings.appMode) {
AppMode.VPN,
AppMode.PROXY,
AppMode.LOCK_DOWN -> {
if (serviceManager.hasVpnPermission()) {
if (mode == AppMode.LOCK_DOWN)
handleLockDownModeInit(settings.isLanOnKillSwitchEnabled)
if (settings.appMode == AppMode.LOCK_DOWN) handleLockDownModeInit()
if (tunnels?.any { it.isActive } == true) {
if (settings.appMode == AppMode.VPN && !serviceManager.hasVpnPermission())
return@withContext localErrorEvents.emit(null to NotAuthorized())
when (settings.appMode) {
AppMode.VPN,
AppMode.PROXY,
AppMode.LOCK_DOWN -> {
tunnels.firstOrNull { it.isActive }?.let { startTunnel(it) }
} else localErrorEvents.emit(null to BackendCoreException.NotAuthorized)
}
AppMode.KERNEL ->
tunnels.filter { it.isActive }.forEach { conf -> startTunnel(conf) }
}
AppMode.KERNEL ->
tunnels.filter { it.isActive }.forEach { conf -> startTunnel(conf) }
}
}
@@ -356,24 +350,18 @@ constructor(
withContext(ioDispatcher) {
val settings = settingsRepository.getGeneralSettings()
val autoTunnelSettings = autoTunnelSettingsRepository.getAutoTunnelSettings()
val defaultTunnel = tunnelsRepository.getStartTunnel()
val defaultTunnel = tunnelsRepository.getDefaultTunnel()
if (autoTunnelSettings.startOnBoot)
return@withContext restoreAutoTunnel(autoTunnelSettings)
if (settings.isRestoreOnBootEnabled) {
tunnelsRepository.resetActiveTunnels()
when (val mode = settings.appMode) {
AppMode.VPN,
AppMode.PROXY,
AppMode.LOCK_DOWN -> {
if (serviceManager.hasVpnPermission()) {
if (mode == AppMode.LOCK_DOWN)
handleLockDownModeInit(settings.isLanOnKillSwitchEnabled)
} else
return@withContext localErrorEvents.emit(
null to BackendCoreException.NotAuthorized
)
}
AppMode.KERNEL -> Unit
when (settings.appMode) {
AppMode.LOCK_DOWN -> handleLockDownModeInit()
AppMode.VPN ->
if (!serviceManager.hasVpnPermission())
return@withContext localErrorEvents.emit(null to NotAuthorized())
AppMode.KERNEL,
AppMode.PROXY -> Unit
}
defaultTunnel?.let { startTunnel(it) }
}
@@ -409,6 +397,46 @@ constructor(
}
}
suspend fun restartActiveTunnel(id: Int) =
withContext(ioDispatcher) {
val activeIds = activeTunnels.value.keys.toList()
if (activeIds.isEmpty()) return@withContext
if (!activeIds.contains(id)) return@withContext
val tunnel = tunnelsRepository.getById(id) ?: return@withContext
restartTunnel(tunnel)
}
suspend fun restartActiveTunnels() =
withContext(ioDispatcher) {
val activeIds = activeTunnels.value.keys.toList()
if (activeIds.isEmpty()) return@withContext
val tunnels = tunnelsRepository.getAll()
if (tunnels.isEmpty()) return@withContext
supervisorScope {
activeIds.forEach { id ->
val tunnel =
tunnels.find { it.id == id }
?: run {
Timber.w("Tunnel config $id not found; skipping restart")
return@forEach
}
restartTunnel(tunnel)
}
}
}
private suspend fun restartTunnel(tunnel: TunnelConfig) {
runCatching { stopTunnel(tunnel.id) }
.onFailure { e -> Timber.e(e, "Failed to stop tunnel ${tunnel.id} during restart") }
delay(RESTART_TUNNEL_DELAY)
runCatching { startTunnel(tunnel) }
.onFailure { e -> Timber.e(e, "Failed to restart tunnel ${tunnel.id}") }
}
private suspend fun handleDynamicDnsMonitoring(
activeTuns: Map<Int, TunnelState>,
configs: List<TunnelConfig>,
@@ -525,5 +553,6 @@ constructor(
companion object {
const val BASE_BACKOFF = 30_000L
const val MAX_BACKOFF_TIME = 300_000L
const val RESTART_TUNNEL_DELAY = 300L
}
}
@@ -1,5 +1,6 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import android.os.PowerManager
import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
@@ -11,17 +12,17 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.state.*
import com.zaneschepke.wireguardautotunnel.util.extensions.toMillis
import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils
import dagger.hilt.android.scopes.ServiceScoped
import inet.ipaddr.AddressValueException
import inet.ipaddr.IPAddress
import inet.ipaddr.IPAddressString
import io.ktor.util.collections.*
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import timber.log.Timber
@ServiceScoped
@Singleton
class TunnelMonitor
@Inject
constructor(
@@ -31,6 +32,7 @@ constructor(
private val networkMonitor: NetworkMonitor,
private val networkUtils: NetworkUtils,
private val logReader: LogReader,
private val powerManager: PowerManager,
) {
@OptIn(FlowPreview::class)
@@ -74,7 +76,7 @@ constructor(
else -> null
}
}
.distinctUntilChangedBy { it.isHealthy } // Only emit when health changes
.distinctUntilChangedBy { it.isHealthy }
.collect { logHealthState ->
Timber.d("Tunnel log health updated for ${tunnelConfig.name}: $logHealthState")
updateTunnelStatus(tunnelConfig.id, null, null, null, logHealthState)
@@ -93,26 +95,7 @@ constructor(
val connectivityStateFlow = networkMonitor.connectivityStateFlow.stateIn(this)
val isNetworkConnected = connectivityStateFlow.map { it.hasConnectivity() }.stateIn(this)
data class NetworkChangeKey(
val ethernetConnected: Boolean,
val wifiConnected: Boolean,
val cellularConnected: Boolean,
val wifiSsid: String?,
)
connectivityStateFlow
.map {
NetworkChangeKey(
ethernetConnected = it.ethernetConnected,
wifiConnected = it.wifiState.connected,
cellularConnected = it.cellularConnected,
wifiSsid = if (it.wifiState.connected) it.wifiState.ssid else null,
)
}
.distinctUntilChanged()
.stateIn(this)
val isNetworkConnected = connectivityStateFlow.map { it.hasInternet() }.stateIn(this)
combine(
settingsRepository.flow.distinctUntilChangedBy { it.appMode },
@@ -199,6 +182,7 @@ constructor(
}
val attemptTime = System.currentTimeMillis()
val timeout = settings.tunnelPingTimeoutSeconds?.toMillis() ?: 5000L
runCatching {
withTimeout(
settings.tunnelPingTimeoutSeconds?.toMillis() ?: 5000L
@@ -263,27 +247,35 @@ constructor(
}
// Wait for the tunnel to be fully active
tunStateFlow.filter { state -> state?.status == TunnelStatus.Up }.first()
tunStateFlow.filter { state -> state?.status is TunnelStatus.Up }.first()
// small delay to make sure tunnel is fully up before we actively monitor
delay(3_000L)
while (isActive) {
ensureActive()
if (isNetworkConnected.value) {
performPing()
} else {
pingStatsFlow.update { current ->
current.mapValues { entry ->
entry.value.copy(
isReachable = false,
failureReason = FailureReason.NoConnectivity,
lastPingAttemptMillis = System.currentTimeMillis(),
)
if (!powerManager.isDeviceIdleMode) {
if (isNetworkConnected.value) {
performPing()
} else {
pingStatsFlow.update { current ->
current.mapValues { entry ->
entry.value.copy(
isReachable = false,
failureReason = FailureReason.NoConnectivity,
lastPingAttemptMillis = System.currentTimeMillis(),
)
}
}
ensureActive()
updateTunnelStatus(
tunnelConfig.id,
null,
null,
pingStatsFlow.value,
null,
)
}
ensureActive()
updateTunnelStatus(tunnelConfig.id, null, null, pingStatsFlow.value, null)
}
delay(settings.tunnelPingIntervalSeconds.toMillis())
}
@@ -300,9 +292,11 @@ constructor(
) = coroutineScope {
while (isActive) {
ensureActive()
val stats = getStatistics(tunnelId)
ensureActive()
updateTunnelStatus(tunnelId, null, stats, null, null)
if (!powerManager.isDeviceIdleMode) {
val stats = getStatistics(tunnelId)
ensureActive()
updateTunnelStatus(tunnelId, null, stats, null, null)
}
delay(STATS_DELAY)
}
}
@@ -1,15 +1,11 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.data.model.DnsProtocol
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException
import com.zaneschepke.wireguardautotunnel.domain.model.ProxySettings
import com.zaneschepke.wireguardautotunnel.domain.events.*
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.repository.DnsSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.ProxySettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.state.AmneziaStatistics
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.util.extensions.asAmBackendMode
@@ -17,7 +13,6 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.asBackendMode
import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendCoreException
import java.io.IOException
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
import kotlinx.coroutines.*
@@ -29,13 +24,7 @@ import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.update
import org.amnezia.awg.backend.Backend
import org.amnezia.awg.backend.BackendException
import org.amnezia.awg.backend.ProxyGoBackend
import org.amnezia.awg.backend.Tunnel as AwgTunnel
import org.amnezia.awg.config.Config
import org.amnezia.awg.config.DnsSettings
import org.amnezia.awg.config.proxy.HttpProxy
import org.amnezia.awg.config.proxy.Proxy
import org.amnezia.awg.config.proxy.Socks5Proxy
import timber.log.Timber
class UserspaceTunnel
@@ -43,9 +32,8 @@ class UserspaceTunnel
constructor(
@ApplicationScope applicationScope: CoroutineScope,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
private val proxySettingsRepository: ProxySettingsRepository,
private val dnsSettingsRepository: DnsSettingsRepository,
private val backend: Backend,
private val runConfigHelper: RunConfigHelper,
) : BaseTunnel(applicationScope, ioDispatcher) {
private val runtimeTunnels = ConcurrentHashMap<Int, AwgTunnel>()
@@ -63,67 +51,21 @@ constructor(
try {
withTimeout(STARTUP_TIMEOUT_MS) {
updateTunnelStatus(tunnelConfig.id, TunnelStatus.Starting)
val proxies: List<Proxy> =
when (backend) {
is ProxyGoBackend -> {
val proxySettings = proxySettingsRepository.getProxySettings()
Timber.d("Adding proxy configs")
buildList {
if (proxySettings.socks5ProxyEnabled) {
add(
Socks5Proxy(
proxySettings.socks5ProxyBindAddress
?: ProxySettings.DEFAULT_SOCKS_BIND_ADDRESS,
proxySettings.proxyUsername,
proxySettings.proxyPassword,
)
)
}
if (proxySettings.httpProxyEnabled) {
add(
HttpProxy(
proxySettings.httpProxyBindAddress
?: ProxySettings.DEFAULT_HTTP_BIND_ADDRESS,
proxySettings.proxyUsername,
proxySettings.proxyPassword,
)
)
}
}
}
else -> emptyList()
}
val setting = dnsSettingsRepository.getDnsSettings()
val config = tunnelConfig.toAmConfig()
val updatedConfig =
Config.Builder()
.apply {
setInterface(config.`interface`)
addPeers(config.peers)
addProxies(proxies)
setDnsSettings(
DnsSettings(
setting.dnsProtocol == DnsProtocol.DOH,
Optional.ofNullable(setting.dnsEndpoint),
)
)
}
.build()
backend.setState(runtimeTunnel, AwgTunnel.State.UP, updatedConfig)
val runConfig = runConfigHelper.buildAmRunConfig(tunnelConfig)
backend.setState(runtimeTunnel, AwgTunnel.State.UP, runConfig)
}
} catch (e: TimeoutCancellationException) {
} catch (_: TimeoutCancellationException) {
Timber.e("Startup timed out for ${tunnelConfig.name} (likely DNS hang)")
errors.emit(tunnelConfig.name to BackendCoreException.DNS)
errors.emit(tunnelConfig.name to DnsFailure())
forceStopTunnel(tunnelConfig.id)
close()
} catch (e: BackendException) {
close(e.toBackendCoreException())
} catch (e: IllegalArgumentException) {
close(BackendCoreException.Config)
} catch (_: IllegalArgumentException) {
close(InvalidConfig())
} catch (e: Exception) {
Timber.e(e, "Error while setting tunnel state")
close(BackendCoreException.Unknown)
close(UnknownError())
}
awaitClose {
@@ -148,8 +90,8 @@ constructor(
} catch (e: BackendException) {
throw e.toBackendCoreException()
// TODO this should be mapped to BackendException in the lib
} catch (e: IOException) {
throw BackendCoreException.NotAuthorized
} catch (_: IOException) {
throw VpnUnauthorized()
}
}
@@ -158,7 +100,7 @@ constructor(
}
override fun handleDnsReresolve(tunnelConfig: TunnelConfig): Boolean {
val tunnel = runtimeTunnels[tunnelConfig.id] ?: throw BackendCoreException.ServiceNotRunning
val tunnel = runtimeTunnels[tunnelConfig.id] ?: throw ServiceNotRunning()
return backend.resolveDDNS(tunnelConfig.toAmConfig(), tunnel.isIpv4ResolutionPreferred)
}
@@ -15,8 +15,9 @@ import com.zaneschepke.wireguardautotunnel.data.entity.*
AutoTunnelSettings::class,
MonitoringSettings::class,
DnsSettings::class,
LockdownSettings::class,
],
version = 25,
version = 29,
autoMigrations =
[
AutoMigration(from = 1, to = 2),
@@ -42,6 +43,8 @@ import com.zaneschepke.wireguardautotunnel.data.entity.*
AutoMigration(from = 21, to = 22),
AutoMigration(from = 22, to = 23),
AutoMigration(from = 24, to = 25),
AutoMigration(from = 26, to = 27, spec = GlobalsMigration::class),
AutoMigration(from = 27, to = 28, spec = DonationMigration::class),
],
exportSchema = true,
)
@@ -57,6 +60,8 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun monitoringSettingsDao(): MonitoringSettingsDao
abstract fun lockdownSettingsDao(): LockdownSettingsDao
abstract fun dnsSettingsDao(): DnsSettingsDao
}
@@ -112,3 +117,15 @@ class FixProxySettingsMigration : AutoMigrationSpec {
}
}
}
@RenameColumn.Entries(
RenameColumn(
tableName = "general_settings",
fromColumnName = "is_tunnel_globals_enabled",
toColumnName = "global_split_tunnel_enabled",
)
)
class GlobalsMigration : AutoMigrationSpec
@DeleteColumn(tableName = "general_settings", columnName = "custom_split_packages")
class DonationMigration : AutoMigrationSpec
@@ -26,6 +26,7 @@ class DataStoreManager(
companion object {
val locationDisclosureShown = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN")
val batteryDisableShown = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN")
val shouldShowDonationSnackbar = booleanPreferencesKey("SHOW_DONATION_SNACK")
}
suspend fun init() {
@@ -0,0 +1,18 @@
package com.zaneschepke.wireguardautotunnel.data.dao
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Upsert
import com.zaneschepke.wireguardautotunnel.data.entity.LockdownSettings
import kotlinx.coroutines.flow.Flow
@Dao
interface LockdownSettingsDao {
@Query("SELECT * FROM lockdown_settings LIMIT 1")
suspend fun getLockdownSettings(): LockdownSettings?
@Upsert suspend fun upsert(lockdownSettings: LockdownSettings)
@Query("SELECT * FROM lockdown_settings LIMIT 1")
fun getLockdownSettingsFlow(): Flow<LockdownSettings?>
}
@@ -50,23 +50,27 @@ interface TunnelConfigDao {
@Query(
"""
SELECT * FROM tunnel_config
ORDER BY
CASE WHEN is_primary_tunnel = 1 THEN 0 ELSE 1 END,
position ASC
LIMIT 1"""
SELECT * FROM tunnel_config
WHERE name != '${TunnelConfig.GLOBAL_CONFIG_NAME}'
ORDER BY
CASE WHEN is_primary_tunnel = 1 THEN 0 ELSE 1 END,
position ASC
LIMIT 1
"""
)
suspend fun getDefaultTunnel(): TunnelConfig?
@Query(
"""
SELECT * FROM tunnel_config
ORDER BY
CASE WHEN is_Active = 1 THEN 0
WHEN is_primary_tunnel = 1 THEN 1
ELSE 2 END,
position ASC
LIMIT 1"""
SELECT * FROM tunnel_config
WHERE name != '${TunnelConfig.GLOBAL_CONFIG_NAME}'
ORDER BY
CASE WHEN is_Active = 1 THEN 0
WHEN is_primary_tunnel = 1 THEN 1
ELSE 2 END,
position ASC
LIMIT 1
"""
)
suspend fun getStartTunnel(): TunnelConfig?
@@ -3,4 +3,5 @@ package com.zaneschepke.wireguardautotunnel.data.entity
data class AppState(
val isLocationDisclosureShown: Boolean = false,
val isBatteryOptimizationDisableShown: Boolean = false,
val shouldShowDonationSnackbar: Boolean = false,
)
@@ -11,4 +11,6 @@ data class DnsSettings(
@ColumnInfo(name = "dns_protocol", defaultValue = "0")
val dnsProtocol: DnsProtocol = DnsProtocol.fromValue(0),
@ColumnInfo(name = "dns_endpoint") val dnsEndpoint: String? = null,
@ColumnInfo(name = "global_tunnel_dns_enabled", defaultValue = "0")
val isGlobalTunnelDnsEnabled: Boolean = false,
)
@@ -14,8 +14,8 @@ data class GeneralSettings(
val isRestoreOnBootEnabled: Boolean = false,
@ColumnInfo(name = "is_multi_tunnel_enabled", defaultValue = "0")
val isMultiTunnelEnabled: Boolean = false,
@ColumnInfo(name = "is_tunnel_globals_enabled", defaultValue = "0")
val isTunnelGlobalsEnabled: Boolean = false,
@ColumnInfo(name = "global_split_tunnel_enabled", defaultValue = "0")
val isGlobalSplitTunnelEnabled: Boolean = false,
@ColumnInfo(name = "app_mode", defaultValue = "0") val appMode: AppMode = AppMode.fromValue(0),
@ColumnInfo(name = "theme", defaultValue = "AUTOMATIC") val theme: String = "AUTOMATIC",
@ColumnInfo(name = "locale") val locale: String? = null,
@@ -26,8 +26,5 @@ data class GeneralSettings(
val isPinLockEnabled: Boolean = false,
@ColumnInfo(name = "is_always_on_vpn_enabled", defaultValue = "0")
val isAlwaysOnVpnEnabled: Boolean = false,
@ColumnInfo(name = "is_lan_on_kill_switch_enabled", defaultValue = "0")
val isLanOnKillSwitchEnabled: Boolean = false,
@ColumnInfo(name = "custom_split_packages", defaultValue = "{}")
val customSplitPackages: Map<String, String> = emptyMap(),
@ColumnInfo(name = "already_donated", defaultValue = "0") val alreadyDonated: Boolean = false,
)
@@ -0,0 +1,13 @@
package com.zaneschepke.wireguardautotunnel.data.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "lockdown_settings")
data class LockdownSettings(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
@ColumnInfo(name = "bypass_lan", defaultValue = "0") val bypassLan: Boolean = false,
@ColumnInfo(name = "metered", defaultValue = "0") val metered: Boolean = false,
@ColumnInfo(name = "dual_stack", defaultValue = "0") val dualStack: Boolean = false,
)
@@ -28,8 +28,8 @@ data class TunnelConfig(
@ColumnInfo(name = "position", defaultValue = "0") val position: Int = 0,
@ColumnInfo(name = "auto_tunnel_apps", defaultValue = "[]")
val autoTunnelApps: Set<String> = emptySet(),
@ColumnInfo(name = "is_metered", defaultValue = "false") val isMetered: Boolean = false,
) {
companion object {
const val GLOBAL_CONFIG_NAME = "4675ab06-903a-438b-8485-6ea4187a9512"
}
@@ -4,7 +4,17 @@ import com.zaneschepke.wireguardautotunnel.data.entity.DnsSettings as Entity
import com.zaneschepke.wireguardautotunnel.domain.model.DnsSettings as Domain
fun Entity.toDomain(): Domain =
Domain(id = id, dnsProtocol = dnsProtocol, dnsEndpoint = dnsEndpoint)
Domain(
id = id,
dnsProtocol = dnsProtocol,
dnsEndpoint = dnsEndpoint,
isGlobalTunnelDnsEnabled = isGlobalTunnelDnsEnabled,
)
fun Domain.toEntity(): Entity =
Entity(id = id, dnsProtocol = dnsProtocol, dnsEndpoint = dnsEndpoint)
Entity(
id = id,
dnsProtocol = dnsProtocol,
dnsEndpoint = dnsEndpoint,
isGlobalTunnelDnsEnabled = isGlobalTunnelDnsEnabled,
)
@@ -7,4 +7,5 @@ fun Entity.toDomain(): Domain =
Domain(
isLocationDisclosureShown = isLocationDisclosureShown,
isBatteryOptimizationDisableShown = isBatteryOptimizationDisableShown,
shouldShowDonationSnackbar = shouldShowDonationSnackbar,
)
@@ -0,0 +1,10 @@
package com.zaneschepke.wireguardautotunnel.data.mapper
import com.zaneschepke.wireguardautotunnel.data.entity.LockdownSettings as Entity
import com.zaneschepke.wireguardautotunnel.domain.model.LockdownSettings as Domain
fun Entity.toDomain(): Domain =
Domain(id = id, bypassLan = bypassLan, metered = metered, dualStack = dualStack)
fun Domain.toEntity(): Entity =
Entity(id = id, bypassLan = bypassLan, metered = metered, dualStack = dualStack)
@@ -10,7 +10,7 @@ fun Entity.toDomain(): Domain =
isShortcutsEnabled = isShortcutsEnabled,
isRestoreOnBootEnabled = isRestoreOnBootEnabled,
isMultiTunnelEnabled = isMultiTunnelEnabled,
isTunnelGlobalsEnabled = isTunnelGlobalsEnabled,
isGlobalSplitTunnelEnabled = isGlobalSplitTunnelEnabled,
appMode = appMode,
theme = Theme.valueOf(theme.uppercase()),
locale = locale,
@@ -18,8 +18,7 @@ fun Entity.toDomain(): Domain =
isRemoteControlEnabled = isRemoteControlEnabled,
isPinLockEnabled = isPinLockEnabled,
isAlwaysOnVpnEnabled = isAlwaysOnVpnEnabled,
isLanOnKillSwitchEnabled = isLanOnKillSwitchEnabled,
customSplitPackages = customSplitPackages,
alreadyDonated = alreadyDonated,
)
fun Domain.toEntity(): Entity =
@@ -28,7 +27,7 @@ fun Domain.toEntity(): Entity =
isShortcutsEnabled = isShortcutsEnabled,
isRestoreOnBootEnabled = isRestoreOnBootEnabled,
isMultiTunnelEnabled = isMultiTunnelEnabled,
isTunnelGlobalsEnabled = isTunnelGlobalsEnabled,
isGlobalSplitTunnelEnabled = isGlobalSplitTunnelEnabled,
appMode = appMode,
theme = theme.name,
locale = locale,
@@ -36,6 +35,5 @@ fun Domain.toEntity(): Entity =
isRemoteControlEnabled = isRemoteControlEnabled,
isPinLockEnabled = isPinLockEnabled,
isAlwaysOnVpnEnabled = isAlwaysOnVpnEnabled,
isLanOnKillSwitchEnabled = isLanOnKillSwitchEnabled,
customSplitPackages = customSplitPackages,
alreadyDonated = alreadyDonated,
)
@@ -19,6 +19,7 @@ fun Entity.toDomain(): Domain =
isIpv4Preferred = isIpv4Preferred,
position = position,
autoTunnelApps = autoTunnelApps,
isMetered = isMetered,
)
fun Domain.toEntity(): Entity =
@@ -37,4 +38,5 @@ fun Domain.toEntity(): Entity =
isIpv4Preferred = isIpv4Preferred,
position = position,
autoTunnelApps = autoTunnelApps,
isMetered = isMetered,
)
@@ -316,3 +316,151 @@ 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`")
}
}
val MIGRATION_28_29 =
object : Migration(28, 29) {
override fun migrate(database: SupportSQLiteDatabase) {
// Migrate tunnel_config table
database.execSQL(
"""
CREATE TABLE IF NOT EXISTS `tunnel_config_new` (
`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 false
)
"""
.trimIndent()
)
database.execSQL(
"""
INSERT INTO `tunnel_config_new` (
`id`, `name`, `wg_quick`, `tunnel_networks`, `is_mobile_data_tunnel`,
`is_primary_tunnel`, `am_quick`, `is_Active`, `restart_on_ping_failure`,
`ping_target`, `is_ethernet_tunnel`, `is_ipv4_preferred`, `position`,
`auto_tunnel_apps`, `is_metered`
)
SELECT
`id`, `name`, `wg_quick`, `tunnel_networks`, `is_mobile_data_tunnel`,
`is_primary_tunnel`, `am_quick`, `is_Active`, `restart_on_ping_failure`,
`ping_target`, `is_ethernet_tunnel`, `is_ipv4_preferred`, `position`,
`auto_tunnel_apps`, 0 AS `is_metered`
FROM `tunnel_config`
"""
.trimIndent()
)
database.execSQL("DROP TABLE `tunnel_config`")
database.execSQL("ALTER TABLE `tunnel_config_new` RENAME TO `tunnel_config`")
database.execSQL(
"CREATE UNIQUE INDEX IF NOT EXISTS `index_tunnel_config_name` ON `tunnel_config` (`name`)"
)
}
}
@@ -20,10 +20,7 @@ enum class DnsProtocol(val value: Int) {
}
}
data class DnsSettings(
val protocol: DnsProtocol = DnsProtocol.SYSTEM,
val endpoint: String? = null,
)
data class DnsSettings(val protocol: DnsProtocol = DnsProtocol.SYSTEM, val endpoint: String? = null)
enum class DnsProvider(private val systemAddress: String, private val dohAddress: String) {
CLOUDFLARE("1.1.1.1", "https://1.1.1.1/dns-query"),
@@ -37,6 +37,14 @@ class DataStoreAppStateRepository(
dataStoreManager.saveToDataStore(DataStoreManager.batteryDisableShown, shown)
}
override suspend fun setShouldShowDonationSnackbar(show: Boolean) {
dataStoreManager.saveToDataStore(DataStoreManager.shouldShowDonationSnackbar, show)
}
override suspend fun shouldShowDonationSnackbar(): Boolean {
return dataStoreManager.getFromStore(DataStoreManager.shouldShowDonationSnackbar) ?: false
}
override val flow: Flow<Domain> =
dataStoreManager.preferencesFlow
.map { prefs ->
@@ -47,6 +55,8 @@ class DataStoreAppStateRepository(
pref[DataStoreManager.locationDisclosureShown] ?: false,
isBatteryOptimizationDisableShown =
pref[DataStoreManager.batteryDisableShown] ?: false,
shouldShowDonationSnackbar =
pref[DataStoreManager.shouldShowDonationSnackbar] ?: false,
)
} catch (e: IllegalArgumentException) {
Timber.e(e)
@@ -38,27 +38,40 @@ class GitHubUpdateRepository(
gitHubApi.getLatestRelease(githubOwner, githubRepo).onFailure(Timber::e)
}
release.map { release ->
val standaloneApkAsset =
val universalApkAsset =
release.assets.find { asset ->
asset.name.startsWith("wgtunnel-${Constants.STANDALONE_FLAVOR}-v") &&
asset.name.endsWith(".apk")
val prefix = "wgtunnel-${Constants.STANDALONE_FLAVOR}-v"
val apkSuffix = ".apk"
asset.name.startsWith(prefix) &&
asset.name.endsWith(apkSuffix) &&
!asset.name.endsWith("-arm64$apkSuffix") &&
!asset.name.endsWith("-armv7$apkSuffix")
}
val newVersion =
standaloneApkAsset
universalApkAsset
?.name
?.removePrefix("wgtunnel-${Constants.STANDALONE_FLAVOR}-v")
?.removeSuffix(".apk") ?: return@map null
Timber.i("Latest version: $newVersion, current version: $currentVersion")
if (isNightly && newVersion != currentVersion)
return@map GitHubReleaseMapper.toAppUpdate(release, newVersion)
if (NumberUtils.compareVersions(newVersion, currentVersion) > 0) {
GitHubReleaseMapper.toAppUpdate(
release.copy(assets = listOf(standaloneApkAsset)),
newVersion,
)
if (isNightly) {
if (newVersion != currentVersion) {
GitHubReleaseMapper.toAppUpdate(
release.copy(assets = listOf(universalApkAsset)),
newVersion,
)
} else {
null
}
} else {
null
if (NumberUtils.compareVersions(newVersion, currentVersion) > 0) {
GitHubReleaseMapper.toAppUpdate(
release.copy(assets = listOf(universalApkAsset)),
newVersion,
)
} else {
null
}
}
}
}
@@ -1,20 +1,15 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.model.InstalledPackage
import com.zaneschepke.wireguardautotunnel.domain.repository.InstalledPackageRepository
import com.zaneschepke.wireguardautotunnel.util.extensions.getAllInternetCapablePackages
import com.zaneschepke.wireguardautotunnel.util.extensions.getFriendlyAppName
import javax.inject.Singleton
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
@@ -27,32 +22,6 @@ class InstalledAndroidPackageRepository(
private var cachedPackages: List<InstalledPackage>? = null
init {
val receiver =
object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
Intent.ACTION_PACKAGE_ADDED,
Intent.ACTION_PACKAGE_REMOVED,
Intent.ACTION_PACKAGE_CHANGED -> {
// don't update if we have nothing cached
if (cachedPackages == null) return
Timber.d("Updating installed packages cache")
applicationScope.launch { refreshInstalledPackages() }
}
}
}
}
val filter =
IntentFilter().apply {
addAction(Intent.ACTION_PACKAGE_ADDED)
addAction(Intent.ACTION_PACKAGE_REMOVED)
addAction(Intent.ACTION_PACKAGE_CHANGED)
addDataScheme("package")
}
context.registerReceiver(receiver, filter)
}
override suspend fun getInstalledPackages(): List<InstalledPackage> =
withContext(ioDispatcher) {
cachedPackages?.let {
@@ -63,7 +32,7 @@ class InstalledAndroidPackageRepository(
override suspend fun refreshInstalledPackages(): List<InstalledPackage> =
withContext(ioDispatcher) {
val packages = context.getAllInternetCapablePackages()
val packages = context.packageManager.getInstalledPackages(0)
val installedPackages =
packages.mapNotNull { packageInfo ->
@@ -0,0 +1,34 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.dao.LockdownSettingsDao
import com.zaneschepke.wireguardautotunnel.data.entity.LockdownSettings as Entity
import com.zaneschepke.wireguardautotunnel.data.mapper.toDomain
import com.zaneschepke.wireguardautotunnel.data.mapper.toEntity
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.model.LockdownSettings as Domain
import com.zaneschepke.wireguardautotunnel.domain.repository.LockdownSettingsRepository
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
class RoomLockdownSettingsRepository(
private val lockdownSettingsDao: LockdownSettingsDao,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : LockdownSettingsRepository {
override suspend fun upsert(lockdownSettings: Domain) {
withContext(ioDispatcher) { lockdownSettingsDao.upsert(lockdownSettings.toEntity()) }
}
override val flow =
lockdownSettingsDao
.getLockdownSettingsFlow()
.map { (it ?: Entity()).toDomain() }
.flowOn(ioDispatcher)
override suspend fun getLockdownSettings(): Domain {
return withContext(ioDispatcher) {
(lockdownSettingsDao.getLockdownSettings() ?: Entity()).toDomain()
}
}
}
@@ -23,7 +23,6 @@ class RoomProxySettingsRepository(
override val flow =
proxySettingsDao
.getProxySettingsFlow()
.flowOn(ioDispatcher)
.map { (it ?: Entity()).toDomain() }
.flowOn(ioDispatcher)
@@ -1,6 +1,7 @@
package com.zaneschepke.wireguardautotunnel.di
import android.content.Context
import android.os.PowerManager
import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.logcatter.LogcatReader
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
@@ -66,4 +67,9 @@ class AppModule {
): NotificationMonitor {
return NotificationMonitor(tunnelManager, notificationManager)
}
@Provides
fun providePowerManager(@ApplicationContext context: Context): PowerManager {
return context.getSystemService(Context.POWER_SERVICE) as PowerManager
}
}
@@ -8,6 +8,8 @@ import com.zaneschepke.wireguardautotunnel.data.DataStoreManager
import com.zaneschepke.wireguardautotunnel.data.DatabaseCallback
import com.zaneschepke.wireguardautotunnel.data.dao.*
import com.zaneschepke.wireguardautotunnel.data.migrations.MIGRATION_23_24
import com.zaneschepke.wireguardautotunnel.data.migrations.MIGRATION_25_26
import com.zaneschepke.wireguardautotunnel.data.migrations.MIGRATION_28_29
import com.zaneschepke.wireguardautotunnel.data.network.GitHubApi
import com.zaneschepke.wireguardautotunnel.data.network.KtorClient
import com.zaneschepke.wireguardautotunnel.data.network.KtorGitHubApi
@@ -55,7 +57,11 @@ class RepositoryModule {
AppDatabase::class.java,
context.getString(R.string.db_name),
)
.addMigrations(MIGRATION_23_24(dataStoreManager.dataStore))
.addMigrations(
MIGRATION_23_24(dataStoreManager.dataStore),
MIGRATION_25_26,
MIGRATION_28_29,
)
.fallbackToDestructiveMigration(true)
.addCallback(callback)
.build()
@@ -67,6 +73,12 @@ class RepositoryModule {
return appDatabase.generalSettingsDao()
}
@Singleton
@Provides
fun provideLockdownDoa(appDatabase: AppDatabase): LockdownSettingsDao {
return appDatabase.lockdownSettingsDao()
}
@Singleton
@Provides
fun provideDnsSettingsDao(appDatabase: AppDatabase): DnsSettingsDao {
@@ -106,6 +118,15 @@ class RepositoryModule {
return RoomTunnelRepository(tunnelConfigDao, ioDispatcher)
}
@Singleton
@Provides
fun provideLockdownSettingsRepository(
lockdownSettingsDao: LockdownSettingsDao,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): LockdownSettingsRepository {
return RoomLockdownSettingsRepository(lockdownSettingsDao, ioDispatcher)
}
@Singleton
@Provides
fun provideGeneralSettingsRepository(
@@ -1,6 +1,7 @@
package com.zaneschepke.wireguardautotunnel.di
import android.content.Context
import android.os.PowerManager
import com.wireguard.android.backend.WgQuickBackend
import com.wireguard.android.util.RootShell
import com.wireguard.android.util.ToolsInstaller
@@ -85,8 +86,9 @@ class TunnelModule {
@ApplicationScope applicationScope: CoroutineScope,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
backend: com.wireguard.android.backend.Backend,
runConfigHelper: RunConfigHelper,
): TunnelProvider {
return KernelTunnel(applicationScope, ioDispatcher, backend)
return KernelTunnel(applicationScope, ioDispatcher, runConfigHelper, backend)
}
@Provides
@@ -94,18 +96,11 @@ class TunnelModule {
@Userspace
fun provideUserspaceProvider(
@ApplicationScope applicationScope: CoroutineScope,
proxySettingsRepository: ProxySettingsRepository,
dnsSettingsRepository: DnsSettingsRepository,
runConfigHelper: RunConfigHelper,
@Userspace backend: Backend,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): TunnelProvider {
return UserspaceTunnel(
applicationScope,
ioDispatcher,
proxySettingsRepository,
dnsSettingsRepository,
backend,
)
return UserspaceTunnel(applicationScope, ioDispatcher, backend, runConfigHelper)
}
@Provides
@@ -113,18 +108,11 @@ class TunnelModule {
@ProxyUserspace
fun provideProxyUserspaceProvider(
@ApplicationScope applicationScope: CoroutineScope,
dnsSettingsRepository: DnsSettingsRepository,
proxySettingsRepository: ProxySettingsRepository,
runConfigHelper: RunConfigHelper,
@ProxyUserspace backend: Backend,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): TunnelProvider {
return UserspaceTunnel(
applicationScope,
ioDispatcher,
proxySettingsRepository,
dnsSettingsRepository,
backend,
)
return UserspaceTunnel(applicationScope, ioDispatcher, backend, runConfigHelper)
}
@Provides
@@ -135,6 +123,7 @@ class TunnelModule {
@ProxyUserspace proxyTunnel: TunnelProvider,
serviceManager: ServiceManager,
tunnelRepository: TunnelRepository,
lockdownSettingsRepository: LockdownSettingsRepository,
settingsRepository: GeneralSettingRepository,
autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
tunnelMonitor: TunnelMonitor,
@@ -148,6 +137,7 @@ class TunnelModule {
serviceManager,
settingsRepository,
autoTunnelSettingsRepository,
lockdownSettingsRepository,
tunnelRepository,
tunnelMonitor,
applicationScope,
@@ -155,6 +145,62 @@ class TunnelModule {
)
}
@Provides
@Singleton
fun provideTunnelConfigHelper(
settingsRepository: GeneralSettingRepository,
proxySettingsRepository: ProxySettingsRepository,
dnsSettingsRepository: DnsSettingsRepository,
tunnelRepository: TunnelRepository,
): RunConfigHelper {
return RunConfigHelper(
settingsRepository,
proxySettingsRepository,
dnsSettingsRepository,
tunnelRepository,
)
}
@Singleton
@Provides
fun provideServiceManager(
@ApplicationContext context: Context,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
@MainDispatcher mainCoroutineDispatcher: CoroutineDispatcher,
@ApplicationScope applicationScope: CoroutineScope,
autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
): ServiceManager {
return ServiceManager(
context,
ioDispatcher,
applicationScope,
mainCoroutineDispatcher,
autoTunnelSettingsRepository,
)
}
@Singleton
@Provides
fun provideTunnelMonitor(
powerManager: PowerManager,
networkMonitor: NetworkMonitor,
networkUtils: NetworkUtils,
logReader: LogReader,
tunnelsRepository: TunnelRepository,
settingsRepository: GeneralSettingRepository,
monitoringSettingsRepository: MonitoringSettingsRepository,
): TunnelMonitor {
return TunnelMonitor(
settingsRepository,
tunnelsRepository,
monitoringSettingsRepository,
networkMonitor,
networkUtils,
logReader,
powerManager,
)
}
@Provides
@Singleton
fun provideNetworkMonitor(
@@ -178,42 +224,4 @@ class TunnelModule {
applicationScope,
)
}
@Singleton
@Provides
fun provideServiceManager(
@ApplicationContext context: Context,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
@MainDispatcher mainCoroutineDispatcher: CoroutineDispatcher,
@ApplicationScope applicationScope: CoroutineScope,
autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
): ServiceManager {
return ServiceManager(
context,
ioDispatcher,
applicationScope,
mainCoroutineDispatcher,
autoTunnelSettingsRepository,
)
}
@Singleton
@Provides
fun provideTunnelMonitor(
networkMonitor: NetworkMonitor,
networkUtils: NetworkUtils,
logReader: LogReader,
tunnelsRepository: TunnelRepository,
settingsRepository: GeneralSettingRepository,
monitoringSettingsRepository: MonitoringSettingsRepository,
): TunnelMonitor {
return TunnelMonitor(
settingsRepository,
tunnelsRepository,
monitoringSettingsRepository,
networkMonitor,
networkUtils,
logReader,
)
}
}
@@ -3,5 +3,9 @@ package com.zaneschepke.wireguardautotunnel.domain.enums
sealed class BackendMode {
data object Inactive : BackendMode()
data class KillSwitch(val allowedIps: Set<String>) : BackendMode()
data class KillSwitch(
val allowedIps: Set<String>,
val isMetered: Boolean,
val dualStack: Boolean,
) : BackendMode()
}
@@ -1,8 +0,0 @@
package com.zaneschepke.wireguardautotunnel.domain.enums
enum class NetworkType {
WIFI,
ETHERNET,
MOBILE_DATA,
NONE,
}
@@ -2,7 +2,7 @@ package com.zaneschepke.wireguardautotunnel.domain.enums
sealed class TunnelStatus {
data object Up : TunnelStatus()
data class Up(val startTime: Long) : TunnelStatus()
data object Down : TunnelStatus()
@@ -15,11 +15,11 @@ sealed class TunnelStatus {
}
fun isUp(): Boolean {
return this == Up
return this is Up
}
fun isUpOrStarting(): Boolean {
return this == Up || this == Starting
return this is Up || this == Starting
}
fun isDownOrStopping(): Boolean {
@@ -1,11 +1,12 @@
package com.zaneschepke.wireguardautotunnel.domain.events
import androidx.annotation.Keep
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
sealed class AutoTunnelEvent {
data class Start(val tunnelConfig: TunnelConfig? = null) : AutoTunnelEvent()
@Keep data class Start(val tunnelConfig: TunnelConfig? = null) : AutoTunnelEvent()
data object Stop : AutoTunnelEvent()
@Keep data object Stop : AutoTunnelEvent()
data object DoNothing : AutoTunnelEvent()
@Keep data object DoNothing : AutoTunnelEvent()
}
@@ -4,38 +4,43 @@ import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.util.StringValue
sealed class BackendCoreException : Exception() {
data object DNS : BackendCoreException()
data object Unauthorized : BackendCoreException()
data object Config : BackendCoreException()
data object KernelModuleName : BackendCoreException()
data object NotAuthorized : BackendCoreException()
data object ServiceNotRunning : BackendCoreException()
data object Unknown : BackendCoreException()
data object TunnelNameTooLong : BackendCoreException()
data object UapiUpdateFailed : BackendCoreException()
fun toStringRes() =
when (this) {
Config -> R.string.config_error
DNS -> R.string.dns_resolve_error
KernelModuleName -> R.string.kernel_name_error
NotAuthorized,
Unauthorized -> R.string.auth_error
ServiceNotRunning -> R.string.service_running_error
Unknown -> R.string.unknown_error
TunnelNameTooLong -> R.string.error_tunnel_name
UapiUpdateFailed -> R.string.active_tunnel_update_failed
}
abstract val stringRes: Int
fun toStringValue(): StringValue {
return StringValue.StringResource(toStringRes())
return StringValue.StringResource(stringRes)
}
}
class DnsFailure : BackendCoreException() {
override val stringRes = R.string.dns_resolve_error
}
class VpnUnauthorized : BackendCoreException() {
override val stringRes = R.string.auth_error
}
class InvalidConfig : BackendCoreException() {
override val stringRes = R.string.config_error
}
class KernelTunnelName(override val stringRes: Int) : BackendCoreException() {}
class NotAuthorized : BackendCoreException() {
override val stringRes = R.string.auth_error
}
class ServiceNotRunning : BackendCoreException() {
override val stringRes = R.string.service_running_error
}
class UnknownError : BackendCoreException() {
override val stringRes = R.string.unknown_error
}
class UapiUpdateFailed : BackendCoreException() {
override val stringRes = R.string.active_tunnel_update_failed
}
class KernelWireguardNotSupported : BackendCoreException() {
override val stringRes = R.string.kernel_wireguard_unsupported
}
@@ -3,4 +3,5 @@ package com.zaneschepke.wireguardautotunnel.domain.model
data class AppState(
val isLocationDisclosureShown: Boolean = false,
val isBatteryOptimizationDisableShown: Boolean = false,
val shouldShowDonationSnackbar: Boolean = false,
)
@@ -6,4 +6,5 @@ data class DnsSettings(
val id: Int = 0,
val dnsProtocol: DnsProtocol = DnsProtocol.fromValue(0),
val dnsEndpoint: String? = null,
val isGlobalTunnelDnsEnabled: Boolean = false,
)
@@ -8,7 +8,7 @@ data class GeneralSettings(
val isShortcutsEnabled: Boolean = false,
val isRestoreOnBootEnabled: Boolean = false,
val isMultiTunnelEnabled: Boolean = false,
val isTunnelGlobalsEnabled: Boolean = false,
val isGlobalSplitTunnelEnabled: Boolean = false,
val appMode: AppMode = AppMode.fromValue(0),
val theme: Theme = Theme.AUTOMATIC,
val locale: String? = null,
@@ -16,6 +16,6 @@ data class GeneralSettings(
val isRemoteControlEnabled: Boolean = false,
val isPinLockEnabled: Boolean = false,
val isAlwaysOnVpnEnabled: Boolean = false,
val isLanOnKillSwitchEnabled: Boolean = false,
val customSplitPackages: Map<String, String> = emptyMap(),
val isKillSwitchMetered: Boolean = true,
val alreadyDonated: Boolean = false,
)
@@ -0,0 +1,8 @@
package com.zaneschepke.wireguardautotunnel.domain.model
data class LockdownSettings(
val id: Long = 0L,
val bypassLan: Boolean = false,
val metered: Boolean = false,
val dualStack: Boolean = false,
)
@@ -27,8 +27,8 @@ data class TunnelConfig(
val isIpv4Preferred: Boolean = true,
val position: Int = 0,
val autoTunnelApps: Set<String> = setOf(),
val isMetered: Boolean = false,
) {
val isNameKernelCompatible: Boolean = (name.length <= 15)
override fun equals(other: Any?): Boolean {
if (this === other) return true
@@ -43,7 +43,8 @@ data class TunnelConfig(
pingTarget == other.pingTarget &&
restartOnPingFailure == other.restartOnPingFailure &&
tunnelNetworks == other.tunnelNetworks &&
isIpv4Preferred == other.isIpv4Preferred
isIpv4Preferred == other.isIpv4Preferred &&
isMetered == other.isMetered
}
override fun hashCode(): Int {
@@ -66,7 +67,11 @@ data class TunnelConfig(
return configFromWgQuick(wgQuick)
}
fun copyWithGlobalValues(globalTunnel: TunnelConfig): TunnelConfig {
fun copyWithGlobalValues(
globalTunnel: TunnelConfig,
includeDns: Boolean,
includeSpitTunneling: Boolean,
): TunnelConfig {
val existingConfig = toAmConfig()
val globalConfig = globalTunnel.toAmConfig()
@@ -101,76 +106,30 @@ data class TunnelConfig(
existingConfig.`interface`.transportPacketMagicHeader.ifPresent {
setTransportPacketMagicHeader(it)
}
existingConfig.`interface`.i1.ifPresent { setI1(it) }
existingConfig.`interface`.i2.ifPresent { setI2(it) }
existingConfig.`interface`.i3.ifPresent { setI3(it) }
existingConfig.`interface`.i4.ifPresent { setI4(it) }
existingConfig.`interface`.i5.ifPresent { setI5(it) }
existingConfig.`interface`.j1.ifPresent { setJ1(it) }
existingConfig.`interface`.j2.ifPresent { setJ2(it) }
existingConfig.`interface`.j3.ifPresent { setJ3(it) }
existingConfig.`interface`.itime.ifPresent { setItime(it) }
existingConfig.`interface`.cookieReplyPacketJunkSize.ifPresent {
setCookieReplyPacketJunkSize(it)
}
existingConfig.`interface`.transportPacketJunkSize.ifPresent {
setTransportPacketJunkSize(it)
}
existingConfig.`interface`.specialJunkI1.ifPresent { setSpecialJunkI1(it) }
existingConfig.`interface`.specialJunkI2.ifPresent { setSpecialJunkI2(it) }
existingConfig.`interface`.specialJunkI3.ifPresent { setSpecialJunkI3(it) }
existingConfig.`interface`.specialJunkI4.ifPresent { setSpecialJunkI4(it) }
existingConfig.`interface`.specialJunkI5.ifPresent { setSpecialJunkI5(it) }
setPreUp(existingConfig.`interface`.preUp)
setPostUp(existingConfig.`interface`.postUp)
setPreDown(existingConfig.`interface`.preDown)
setPostDown(existingConfig.`interface`.postDown)
globalConfig.`interface`.mtu.ifPresent { setMtu(it) }
if (globalConfig.`interface`.dnsServers.isNotEmpty()) {
if (includeDns) {
setDnsServers(globalConfig.`interface`.dnsServers)
}
if (globalConfig.`interface`.dnsSearchDomains.isNotEmpty()) {
setDnsSearchDomains(globalConfig.`interface`.dnsSearchDomains)
}
if (globalConfig.`interface`.excludedApplications.isNotEmpty()) {
if (includeSpitTunneling) {
setExcludedApplications(globalConfig.`interface`.excludedApplications)
}
if (!globalConfig.`interface`.includedApplications.isEmpty()) {
setIncludedApplications(globalConfig.`interface`.includedApplications)
}
if (globalConfig.`interface`.preUp.isNotEmpty()) {
setPreUp(globalConfig.`interface`.preUp)
}
if (globalConfig.`interface`.postUp.isNotEmpty()) {
setPostUp(globalConfig.`interface`.postUp)
}
if (globalConfig.`interface`.preDown.isNotEmpty()) {
setPreDown(globalConfig.`interface`.preDown)
}
if (globalConfig.`interface`.postDown.isNotEmpty()) {
setPostDown(globalConfig.`interface`.postDown)
}
globalConfig.`interface`.junkPacketCount.ifPresent { setJunkPacketCount(it) }
globalConfig.`interface`.junkPacketMinSize.ifPresent { setJunkPacketMinSize(it) }
globalConfig.`interface`.junkPacketMaxSize.ifPresent { setJunkPacketMaxSize(it) }
globalConfig.`interface`.initPacketJunkSize.ifPresent { setInitPacketJunkSize(it) }
globalConfig.`interface`.responsePacketJunkSize.ifPresent {
setResponsePacketJunkSize(it)
}
globalConfig.`interface`.initPacketMagicHeader.ifPresent {
setInitPacketMagicHeader(it)
}
globalConfig.`interface`.responsePacketMagicHeader.ifPresent {
setResponsePacketMagicHeader(it)
}
globalConfig.`interface`.underloadPacketMagicHeader.ifPresent {
setUnderloadPacketMagicHeader(it)
}
globalConfig.`interface`.transportPacketMagicHeader.ifPresent {
setTransportPacketMagicHeader(it)
}
globalConfig.`interface`.i1.ifPresent { setI1(it) }
globalConfig.`interface`.i2.ifPresent { setI2(it) }
globalConfig.`interface`.i3.ifPresent { setI3(it) }
globalConfig.`interface`.i4.ifPresent { setI4(it) }
globalConfig.`interface`.i5.ifPresent { setI5(it) }
globalConfig.`interface`.j1.ifPresent { setJ1(it) }
globalConfig.`interface`.j2.ifPresent { setJ2(it) }
globalConfig.`interface`.j3.ifPresent { setJ3(it) }
globalConfig.`interface`.itime.ifPresent { setItime(it) }
}
val newInterface = newInterfaceBuilder.build()
@@ -12,5 +12,9 @@ interface AppStateRepository {
suspend fun setBatteryOptimizationDisableShown(shown: Boolean)
suspend fun setShouldShowDonationSnackbar(show: Boolean)
suspend fun shouldShowDonationSnackbar(): Boolean
val flow: Flow<AppState>
}
@@ -0,0 +1,12 @@
package com.zaneschepke.wireguardautotunnel.domain.repository
import com.zaneschepke.wireguardautotunnel.domain.model.LockdownSettings
import kotlinx.coroutines.flow.Flow
interface LockdownSettingsRepository {
suspend fun upsert(lockdownSettings: LockdownSettings)
val flow: Flow<LockdownSettings>
suspend fun getLockdownSettings(): LockdownSettings
}
@@ -2,12 +2,19 @@ package com.zaneschepke.wireguardautotunnel.domain.sideeffect
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarType
import com.zaneschepke.wireguardautotunnel.util.StringValue
import java.io.File
sealed class GlobalSideEffect {
data class Snackbar(val message: StringValue) : GlobalSideEffect()
data class Snackbar(
val message: StringValue,
val type: SnackbarType? = null,
val actionLabel: String? = null,
val onAction: (() -> Unit)? = null,
val durationMs: Long? = null,
) : GlobalSideEffect()
data class Toast(val message: StringValue) : GlobalSideEffect()
@@ -25,29 +25,27 @@ data class AutoTunnelState(
is NetworkChange,
is SettingsChange -> {
// Compute desired tunnel based on network conditions
var desiredTunnel: TunnelConfig? = null
if (networkState.isEthernetConnected && settings.isTunnelOnEthernetEnabled) {
desiredTunnel = preferredEthernetTunnel()
} else if (isMobileDataActive() && settings.isTunnelOnMobileDataEnabled) {
desiredTunnel = preferredMobileDataTunnel()
} else if (
isWifiActive() && settings.isTunnelOnWifiEnabled && !isCurrentSSIDTrusted()
) {
desiredTunnel = preferredWifiTunnel()
var preferredTunnel: TunnelConfig? = null
if (ethernetActive && settings.isTunnelOnEthernetEnabled) {
preferredTunnel = preferredEthernetTunnel()
} else if (mobileDataActive && settings.isTunnelOnMobileDataEnabled) {
preferredTunnel = preferredMobileDataTunnel()
} else if (wifiActive && settings.isTunnelOnWifiEnabled && !isWifiTrusted()) {
preferredTunnel = preferredWifiTunnel()
}
// Override for no connectivity if enabled
if (isNoConnectivity() && settings.isStopOnNoInternetEnabled) {
desiredTunnel = null
if (!networkState.hasInternet() && settings.isStopOnNoInternetEnabled) {
preferredTunnel = null
}
// Determine current active tunnel (assuming only one can be active)
val currentTunnel = activeTunnels.entries.firstOrNull()?.key
// Handle tunnel start/stop/change
if (desiredTunnel != null) {
if (currentTunnel != desiredTunnel.id) {
return Start(desiredTunnel)
if (preferredTunnel != null) {
if (currentTunnel != preferredTunnel.id) {
return Start(preferredTunnel)
}
} else {
if (currentTunnel != null) {
@@ -61,12 +59,9 @@ data class AutoTunnelState(
return DoNothing
}
// also need to check for Wi-Fi state as there is some overlap when they are both connected
private fun isMobileDataActive(): Boolean {
return !networkState.isEthernetConnected &&
!networkState.isWifiConnected &&
networkState.isMobileDataConnected
}
private val ethernetActive: Boolean = networkState.activeNetwork is ActiveNetwork.Ethernet
private val mobileDataActive: Boolean = networkState.activeNetwork is ActiveNetwork.Cellular
private val wifiActive: Boolean = networkState.activeNetwork is ActiveNetwork.Wifi
private fun preferredMobileDataTunnel(): TunnelConfig? {
return tunnels.firstOrNull { it.isMobileDataTunnel }
@@ -81,27 +76,21 @@ data class AutoTunnelState(
}
private fun preferredWifiTunnel(): TunnelConfig? {
return getTunnelWithMatchingTunnelNetwork()
return getTunnelWithMappedNetwork()
?: tunnels.firstOrNull { it.isPrimaryTunnel }
?: tunnels.firstOrNull()
}
// ignore cellular state as there is overlap where it may still be active, but not prioritized
private fun isWifiActive(): Boolean {
return !networkState.isEthernetConnected && networkState.isWifiConnected
private fun isWifiTrusted(): Boolean {
return with(networkState.activeNetwork) {
this is ActiveNetwork.Wifi && isTrustedNetwork(this.ssid)
}
}
private fun isNoConnectivity(): Boolean {
return !networkState.isEthernetConnected &&
!networkState.isWifiConnected &&
!networkState.isMobileDataConnected
}
private fun isTrustedNetwork(ssid: String): Boolean =
hasMatch(ssid, settings.trustedNetworkSSIDs)
private fun isCurrentSSIDTrusted(): Boolean {
return networkState.wifiName?.let { hasTrustedWifiName(it) } == true
}
private fun hasTrustedWifiName(
private fun hasMatch(
wifiName: String,
wifiNames: Set<String> = settings.trustedNetworkSSIDs,
): Boolean {
@@ -112,9 +101,10 @@ data class AutoTunnelState(
}
}
private fun getTunnelWithMatchingTunnelNetwork(): TunnelConfig? {
return networkState.wifiName?.let { wifiName ->
tunnels.firstOrNull { hasTrustedWifiName(wifiName, it.tunnelNetworks) }
private fun getTunnelWithMappedNetwork(): TunnelConfig? =
when (val network = networkState.activeNetwork) {
is ActiveNetwork.Wifi ->
tunnels.firstOrNull { hasMatch(network.ssid, it.tunnelNetworks) }
else -> null
}
}
}
@@ -1,9 +0,0 @@
package com.zaneschepke.wireguardautotunnel.domain.state
data class ConnectivityState(
val wifiAvailable: Boolean,
val ethernetAvailable: Boolean,
val cellularAvailable: Boolean,
) {
val allOffline = !wifiAvailable && !ethernetAvailable && !cellularAvailable
}
@@ -1,38 +1,48 @@
package com.zaneschepke.wireguardautotunnel.domain.state
import com.zaneschepke.networkmonitor.ActiveNetwork as MonitorActiveNetwork
import com.zaneschepke.networkmonitor.ConnectivityState
import com.zaneschepke.networkmonitor.util.WifiSecurityType
data class NetworkState(
val isWifiConnected: Boolean = false,
val isMobileDataConnected: Boolean = false,
val isEthernetConnected: Boolean = false,
val wifiName: String? = null,
val isWifiSecure: Boolean? = null,
val locationServicesEnabled: Boolean? = null,
val locationPermissionGranted: Boolean? = null,
) {
fun hasNoCapabilities(): Boolean {
return !isWifiConnected && !isMobileDataConnected && !isEthernetConnected
}
sealed class ActiveNetwork {
data object Disconnected : ActiveNetwork()
companion object {
fun from(connectivityState: ConnectivityState): NetworkState {
return NetworkState(
isWifiSecure =
when (connectivityState.wifiState.securityType) {
data object Ethernet : ActiveNetwork()
data object Cellular : ActiveNetwork()
data class Wifi(val ssid: String, val isSecure: Boolean?) : ActiveNetwork()
}
data class NetworkState(
val activeNetwork: ActiveNetwork = ActiveNetwork.Disconnected,
val locationServicesEnabled: Boolean = false,
val locationPermissionGranted: Boolean = false,
) {
fun hasInternet(): Boolean = activeNetwork !is ActiveNetwork.Disconnected
}
fun ConnectivityState.toDomain(): NetworkState {
val domainNetwork: ActiveNetwork =
when (val network = this.activeNetwork) {
is MonitorActiveNetwork.Wifi -> {
val isSecure =
when (network.securityType) {
WifiSecurityType.OPEN,
WifiSecurityType.UNKNOWN -> false
null -> null
else -> true
},
isWifiConnected = connectivityState.wifiState.connected,
isMobileDataConnected = connectivityState.cellularConnected,
isEthernetConnected = connectivityState.ethernetConnected,
wifiName = connectivityState.wifiState.ssid,
locationPermissionGranted = connectivityState.wifiState.locationPermissionsGranted,
locationServicesEnabled = connectivityState.wifiState.locationServicesEnabled,
)
}
ActiveNetwork.Wifi(ssid = network.ssid, isSecure = isSecure)
}
is MonitorActiveNetwork.Cellular -> ActiveNetwork.Cellular
is MonitorActiveNetwork.Ethernet -> ActiveNetwork.Ethernet
is MonitorActiveNetwork.Disconnected -> ActiveNetwork.Disconnected
}
}
return NetworkState(
activeNetwork = domainNetwork,
locationPermissionGranted = this.locationPermissionsGranted,
locationServicesEnabled = this.locationServicesEnabled,
)
}
@@ -12,6 +12,8 @@ data class TunnelState(
) {
fun health(): Health {
if (status !is TunnelStatus.Up) return Health.UNKNOWN
val uptime = uptime()
val now = System.currentTimeMillis()
if (pingStates == null && logHealthState == null && statistics == null)
@@ -37,13 +39,21 @@ data class TunnelState(
// Stats health if no logs or pings
statistics?.let { stats ->
if (stats.isTunnelStale()) return Health.STALE
if (stats.rx() == 0L) return Health.UNKNOWN
val rx = stats.rx()
if (uptime >= STATS_HEALTH_SUCCESS_TIMEOUT_MS && rx == 0L) return Health.UNHEALTHY
if (rx == 0L) return Health.UNKNOWN
return Health.HEALTHY
}
return Health.UNKNOWN
}
fun uptime(): Long {
val up = status as? TunnelStatus.Up ?: return 0L
if (up.startTime == 0L) return 0L
return System.currentTimeMillis() - up.startTime
}
enum class Health {
UNKNOWN,
UNHEALTHY,
@@ -53,5 +63,6 @@ data class TunnelState(
companion object {
const val LOG_HEALTH_SUCCESS_TIMEOUT_MS = 2 * 60 * 1000L // 2 minutes
const val STATS_HEALTH_SUCCESS_TIMEOUT_MS = 15 * 1000L // 15 sec
}
}
@@ -0,0 +1,46 @@
package com.zaneschepke.wireguardautotunnel.ui.common.button
import android.R.attr.onClick
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ExpandMore
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.VerticalDivider
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
@Composable
fun SheetButtonWithDivider(
showDivider: Boolean = true,
modifier: Modifier = Modifier,
onClick: () -> Unit,
) {
Row(
modifier = Modifier.height(IntrinsicSize.Min),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
if (showDivider) {
VerticalDivider(
modifier = Modifier.fillMaxHeight().padding(horizontal = 8.dp, vertical = 8.dp),
color = MaterialTheme.colorScheme.outline,
)
}
Box(modifier = Modifier.pointerInput(Unit) { detectTapGestures {} }) {
IconButton(onClick = onClick, modifier) {
Icon(
Icons.Outlined.ExpandMore,
contentDescription = stringResource(R.string.select),
)
}
}
}
}
@@ -1,5 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.common.button
import android.view.KeyEvent
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
@@ -16,10 +17,15 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
import com.zaneschepke.wireguardautotunnel.ui.theme.Disabled
@Composable
fun SurfaceRow(
@@ -28,13 +34,14 @@ fun SurfaceRow(
onClick: (() -> Unit)? = null,
description: @Composable (() -> Unit)? = null,
expandedContent: @Composable (() -> Unit)? = null,
onLongClick: () -> Unit = {},
onLongClick: (() -> Unit)? = null,
enabled: Boolean = true,
selected: Boolean = false,
leading: @Composable (() -> Unit)? = null,
trailing: @Composable ((Modifier) -> Unit)? = null,
) {
val density = LocalDensity.current
val isTv = LocalIsAndroidTV.current
var leadingPadding by remember { mutableStateOf(0.dp) }
val interactionSource = remember { MutableInteractionSource() }
val mainFocusRequester = remember { FocusRequester() }
@@ -44,7 +51,6 @@ fun SurfaceRow(
modifier =
modifier
.fillMaxWidth()
// .focusGroup()
.indication(interactionSource, ripple())
.background(
if (!selected) MaterialTheme.colorScheme.surface
@@ -62,7 +68,20 @@ fun SurfaceRow(
) {
Row(
modifier =
Modifier.focusRequester(mainFocusRequester)
Modifier.onKeyEvent { event ->
if (onLongClick == null || isTv) {
if (
event.key == Key.DirectionCenter &&
event.nativeKeyEvent.action == KeyEvent.ACTION_DOWN
) {
// Consume the down event to prevent the default long press
// behavior
return@onKeyEvent true
}
}
false
}
.focusRequester(mainFocusRequester)
.focusProperties {
if (onClick != null) {
right = trailingFocusRequester
@@ -109,9 +128,7 @@ fun SurfaceRow(
Text(
text = title,
style = MaterialTheme.typography.bodyLarge,
color =
if (enabled) MaterialTheme.colorScheme.onSurface
else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f),
color = if (enabled) MaterialTheme.colorScheme.onSurface else Disabled,
)
if (description != null) {
description()
@@ -27,7 +27,7 @@ fun SwitchWithDivider(
color = MaterialTheme.colorScheme.outline,
)
Box(modifier = Modifier.pointerInput(Unit) { detectTapGestures {} }) {
ScaledSwitch(
ThemedSwitch(
checked = checked,
onClick = onClick,
enabled = enabled,
@@ -5,9 +5,10 @@ import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.zaneschepke.wireguardautotunnel.ui.theme.Disabled
@Composable
fun ScaledSwitch(
fun ThemedSwitch(
checked: Boolean,
onClick: (checked: Boolean) -> Unit,
enabled: Boolean = true,
@@ -21,12 +22,15 @@ fun ScaledSwitch(
colors =
SwitchDefaults.colors()
.copy(
checkedThumbColor = MaterialTheme.colorScheme.background,
checkedIconColor = MaterialTheme.colorScheme.background,
checkedThumbColor = MaterialTheme.colorScheme.surface,
checkedIconColor = MaterialTheme.colorScheme.surface,
uncheckedTrackColor = MaterialTheme.colorScheme.surface,
uncheckedBorderColor = MaterialTheme.colorScheme.outline,
uncheckedThumbColor = MaterialTheme.colorScheme.outline,
uncheckedIconColor = MaterialTheme.colorScheme.outline,
disabledUncheckedBorderColor = Disabled,
disabledUncheckedThumbColor = Disabled,
disabledUncheckedIconColor = Disabled,
),
)
}
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.common.dialog
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
@@ -11,22 +12,26 @@ import com.zaneschepke.wireguardautotunnel.R
fun InfoDialog(
onAttest: () -> Unit,
onDismiss: () -> Unit,
title: @Composable () -> Unit,
body: @Composable () -> Unit,
confirmText: @Composable () -> Unit,
title: String,
body: @Composable (() -> Unit),
confirmText: String,
modifier: Modifier = Modifier,
) {
MaterialTheme(colorScheme = MaterialTheme.colorScheme.copy()) {
Surface(color = MaterialTheme.colorScheme.surface, tonalElevation = 0.dp) {
AlertDialog(
modifier = modifier,
onDismissRequest = { onDismiss() },
confirmButton = { TextButton(onClick = { onAttest() }) { confirmText() } },
confirmButton = {
TextButton(onClick = { onAttest() }) { Text(text = confirmText) }
},
dismissButton = {
TextButton(onClick = { onDismiss() }) {
Text(text = stringResource(R.string.cancel))
}
},
containerColor = MaterialTheme.colorScheme.surface,
title = { title() },
title = { Text(text = title) },
text = { body() },
properties = DialogProperties(usePlatformDefaultWidth = true),
)
@@ -34,7 +34,7 @@ fun VpnDeniedDialog(show: Boolean, onDismiss: () -> Unit) {
InfoDialog(
onDismiss = { onDismiss() },
onAttest = { onDismiss() },
title = { Text(text = stringResource(R.string.vpn_denied_dialog_title)) },
title = stringResource(R.string.vpn_denied_dialog_title),
body = {
Text(
text = alwaysOnDescription,
@@ -44,7 +44,7 @@ fun VpnDeniedDialog(show: Boolean, onDismiss: () -> Unit) {
),
)
},
confirmText = { Text(text = stringResource(R.string.okay)) },
confirmText = stringResource(R.string.okay),
)
}
}
@@ -1,11 +1,10 @@
package com.zaneschepke.wireguardautotunnel.ui.common.label
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import java.util.*
import androidx.compose.ui.text.intl.Locale
@Composable
fun lowercaseLabel(text: String): String {
val locale = remember { Locale.getDefault() }
val locale = Locale.current.platformLocale
return text.lowercase(locale)
}
@@ -14,6 +14,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
@Composable
fun SheetOption(
@@ -50,9 +51,12 @@ fun SheetOption(
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CustomBottomSheet(options: List<SheetOption>, onDismiss: () -> Unit) {
val isTv = LocalIsAndroidTV.current
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = isTv)
ModalBottomSheet(
containerColor = MaterialTheme.colorScheme.surface,
onDismissRequest = onDismiss,
sheetState = sheetState,
) {
options.forEachIndexed { index, option ->
SheetOption(option.label, option.leadingIcon, option.onClick, option.selected)
@@ -3,54 +3,89 @@ package com.zaneschepke.wireguardautotunnel.ui.common.snackbar
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Favorite
import androidx.compose.material.icons.rounded.Close
import androidx.compose.material.icons.rounded.Info
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Snackbar
import androidx.compose.material3.Text
import androidx.compose.material.icons.rounded.Warning
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
@Composable
fun CustomSnackBar(
message: String,
isRtl: Boolean = true,
message: AnnotatedString,
onDismiss: () -> Unit,
modifier: Modifier = Modifier,
type: SnackbarType = SnackbarType.INFO,
containerColor: Color = MaterialTheme.colorScheme.surface,
) {
val isTv = LocalIsAndroidTV.current
val icon =
when (type) {
SnackbarType.INFO -> Icons.Rounded.Info
SnackbarType.WARNING -> Icons.Rounded.Warning
SnackbarType.THANK_YOU -> Icons.Outlined.Favorite
}
val iconDescription =
when (type) {
SnackbarType.INFO -> stringResource(R.string.info)
SnackbarType.WARNING -> stringResource(R.string.warning)
SnackbarType.THANK_YOU -> stringResource(R.string.thank_you)
}
Snackbar(
containerColor = containerColor,
modifier = Modifier.fillMaxWidth(if (isTv) 1 / 3f else 2 / 3f).padding(bottom = 100.dp),
modifier =
modifier
.wrapContentHeight(align = Alignment.Top)
.padding(horizontal = if (isTv) 48.dp else 16.dp),
shape = RoundedCornerShape(16.dp),
) {
CompositionLocalProvider(
LocalLayoutDirection provides if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr
Row(
modifier =
Modifier.fillMaxWidth()
.height(IntrinsicSize.Min)
.width(IntrinsicSize.Min)
.padding(vertical = 16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Row(
modifier = Modifier.width(IntrinsicSize.Max).height(IntrinsicSize.Min),
modifier = Modifier.fillMaxWidth().weight(1f),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start,
) {
val icon = Icons.Rounded.Info
Icon(
icon,
contentDescription = icon.name,
contentDescription = iconDescription,
tint = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(end = 10.dp),
)
Spacer(modifier = Modifier.width(16.dp))
Text(
message,
text = message,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(end = 5.dp),
maxLines = 8,
overflow = TextOverflow.Ellipsis,
)
}
Row {
IconButton(onClick = onDismiss, modifier = Modifier.size(24.dp)) {
Icon(
Icons.Rounded.Close,
contentDescription = stringResource(R.string.stop),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
}
}
}
@@ -0,0 +1,70 @@
package com.zaneschepke.wireguardautotunnel.ui.common.snackbar
import androidx.compose.foundation.layout.Box
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@Composable
fun rememberCustomSnackbarState(): CustomSnackbarState {
return remember { CustomSnackbarState() }
}
class CustomSnackbarState {
private val _snackbars = Channel<SnackbarInfo>(Channel.BUFFERED)
val snackbars: Channel<SnackbarInfo> = _snackbars
private var currentSnackbar by mutableStateOf<SnackbarInfo?>(null)
private var isShowing by mutableStateOf(false)
fun showSnackbar(info: SnackbarInfo) {
_snackbars.trySend(info)
}
fun dismissCurrent() {
currentSnackbar = null
isShowing = false
}
@Composable
fun SnackbarHost(
modifier: Modifier = Modifier,
snackbar: @Composable (SnackbarInfo) -> Unit = { info ->
CustomSnackBar(
message = info.message,
type = info.type,
onDismiss = { dismissCurrent() },
modifier = Modifier,
containerColor = MaterialTheme.colorScheme.surface.copy(.1f),
)
},
) {
val scope = rememberCoroutineScope()
LaunchedEffect(Unit) {
for (info in snackbars) {
currentSnackbar = info
isShowing = true
scope.launch {
delay(info.durationMs)
if (currentSnackbar?.id == info.id) {
dismissCurrent()
}
}
while (isShowing && currentSnackbar?.id == info.id) {
delay(100)
}
}
}
currentSnackbar?.let { info ->
if (isShowing) {
Box(modifier = modifier) { snackbar(info) }
}
}
}
}
@@ -0,0 +1,16 @@
package com.zaneschepke.wireguardautotunnel.ui.common.snackbar
import androidx.compose.ui.text.AnnotatedString
enum class SnackbarType {
INFO,
WARNING,
THANK_YOU,
}
data class SnackbarInfo(
val message: AnnotatedString,
val type: SnackbarType = SnackbarType.INFO,
val durationMs: Long = 4000L,
val id: String = System.currentTimeMillis().toString(),
)
@@ -5,21 +5,32 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.AnnotatedString
import com.zaneschepke.wireguardautotunnel.ui.theme.Disabled
@Composable
fun DescriptionText(text: String, modifier: Modifier = Modifier) {
fun DescriptionText(text: String, modifier: Modifier = Modifier, disabled: Boolean = false) {
Text(
text = text,
style = MaterialTheme.typography.bodySmall.copy(color = MaterialTheme.colorScheme.outline),
style =
MaterialTheme.typography.bodySmall.copy(
color = if (disabled) Disabled else MaterialTheme.colorScheme.outline
),
modifier = modifier,
)
}
@Composable
fun DescriptionText(text: AnnotatedString, modifier: Modifier = Modifier) {
fun DescriptionText(
text: AnnotatedString,
modifier: Modifier = Modifier,
disabled: Boolean = false,
) {
Text(
text = text,
style = MaterialTheme.typography.bodySmall.copy(color = MaterialTheme.colorScheme.outline),
style =
MaterialTheme.typography.bodySmall.copy(
color = if (disabled) Disabled else MaterialTheme.colorScheme.outline
),
modifier = modifier,
)
}
@@ -2,7 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.common.textbox
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.MaterialTheme
@@ -12,6 +12,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@Composable
@@ -39,7 +40,7 @@ fun ConfigurationTextBox(
isError = isError,
textStyle =
MaterialTheme.typography.bodySmall.copy(color = MaterialTheme.colorScheme.onSurface),
modifier = modifier.fillMaxWidth().height(48.dp),
modifier = modifier.fillMaxWidth().heightIn(48.dp),
value = value,
visualTransformation = visualTransformation,
singleLine = singleLine,
@@ -50,6 +51,8 @@ fun ConfigurationTextBox(
label,
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.labelMedium,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
},
containerColor = MaterialTheme.colorScheme.surface,
@@ -58,6 +61,8 @@ fun ConfigurationTextBox(
hint,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.outline,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
},
keyboardOptions = keyboardOptions,
@@ -52,10 +52,18 @@ fun CustomTextField(
val editable = enabled && !readOnly
val mainFocusRequester = remember { FocusRequester() }
val trailingFocusRequester = remember { FocusRequester() }
val disabledAlpha = 0.38f
val disabledBorderAlpha = 0.12f
val effectiveTextStyle =
if (enabled) {
textStyle
} else {
textStyle.copy(color = textStyle.color.copy(alpha = disabledAlpha))
}
BasicTextField(
value = value,
textStyle = textStyle,
textStyle = effectiveTextStyle,
onValueChange = { onValueChange(it) },
keyboardActions = keyboardActions,
keyboardOptions = keyboardOptions,
@@ -105,7 +113,18 @@ fun CustomTextField(
colors =
TextFieldDefaults.colors()
.copy(
disabledLabelColor = MaterialTheme.colorScheme.onSurface,
disabledTextColor =
MaterialTheme.colorScheme.onSurface.copy(alpha = disabledAlpha),
disabledLabelColor =
MaterialTheme.colorScheme.onSurface.copy(alpha = disabledAlpha),
disabledPlaceholderColor =
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = disabledAlpha),
disabledLeadingIconColor =
MaterialTheme.colorScheme.onSurface.copy(alpha = disabledAlpha),
disabledTrailingIconColor =
MaterialTheme.colorScheme.onSurface.copy(alpha = disabledAlpha),
disabledSupportingTextColor =
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = disabledAlpha),
disabledContainerColor = containerColor,
focusedLabelColor = MaterialTheme.colorScheme.onSurface,
focusedContainerColor = containerColor,
@@ -127,8 +146,13 @@ fun CustomTextField(
TextFieldDefaults.colors()
.copy(
errorContainerColor = containerColor,
disabledLabelColor = MaterialTheme.colorScheme.onSurface,
disabledLabelColor =
MaterialTheme.colorScheme.onSurface.copy(alpha = disabledAlpha),
disabledContainerColor = containerColor,
disabledIndicatorColor =
MaterialTheme.colorScheme.onSurface.copy(
alpha = disabledBorderAlpha
),
focusedIndicatorColor = MaterialTheme.colorScheme.primary,
unfocusedIndicatorColor = MaterialTheme.colorScheme.outline,
focusedLabelColor = MaterialTheme.colorScheme.onSurface,
@@ -22,24 +22,14 @@ class NavController(
return false
}
fun popUpTo(route: NavKey, inclusive: Boolean = false) {
fun popUpTo(route: NavKey) {
onChange(currentRoute)
val targetRoute =
if (route is Route.AutoTunnel && !isDisclosureShown) Route.LocationDisclosure else route
val index = backStack.indexOfLast { it == targetRoute }
if (index != -1) {
val popUpToIndex = if (inclusive) index else index + 1
while (backStack.size > popUpToIndex) {
backStack.removeLastOrNull()
}
} else {
// Only add if it's not already the top
if (backStack.lastOrNull() != targetRoute) {
backStack.add(targetRoute)
}
}
backStack.clear()
if (route is Route.Tunnels) backStack.add(targetRoute)
else backStack.addAll(setOf(Route.Tunnels, targetRoute))
}
val currentRoute: NavKey?
@@ -34,7 +34,7 @@ sealed class Route : NavKey {
@Keep @Serializable data object Tunnels : Route()
@Keep @Serializable data class TunnelOptions(val id: Int) : Route()
@Keep @Serializable data class TunnelSettings(val id: Int) : Route()
@Keep @Serializable data class Config(val id: Int?) : Route()
@@ -42,8 +42,6 @@ sealed class Route : NavKey {
@Keep @Serializable data class ConfigGlobal(val id: Int?) : Route()
@Keep @Serializable data class TunnelGlobals(val id: Int) : Route()
@Keep @Serializable data class SplitTunnelGlobal(val id: Int) : Route()
@Keep @Serializable data object Sort : Route()
@@ -58,6 +56,8 @@ sealed class Route : NavKey {
@Keep @Serializable data object ProxySettings : Route()
@Keep @Serializable data object LockdownSettings : Route()
@Keep @Serializable data object AutoTunnel : Route()
@Keep @Serializable data object AdvancedAutoTunnel : Route()
@@ -107,7 +107,7 @@ enum class Tab(
when (route) {
is Route.Tunnels,
Route.Sort,
is Route.TunnelOptions,
is Route.TunnelSettings,
is Route.Config,
is Route.Lock,
is Route.SplitTunnel -> TUNNELS
@@ -121,14 +121,14 @@ enum class Tab(
Route.TunnelMonitoring,
Route.AndroidIntegrations,
Route.Dns,
is Route.TunnelGlobals,
is Route.ConfigGlobal,
is Route.SplitTunnelGlobal,
Route.ProxySettings,
Route.LockdownSettings,
Route.Appearance,
Route.Language,
Route.Display,
Route.PingTarget,
is Route.ConfigGlobal,
Route.Logs -> SETTINGS
is Route.Support,
Route.License,
@@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.automirrored.rounded.Sort
import androidx.compose.material.icons.outlined.ContentPasteGo
import androidx.compose.material.icons.outlined.CopyAll
import androidx.compose.material.icons.rounded.*
import androidx.compose.material3.Icon
@@ -113,6 +114,29 @@ fun currentRouteAsNavbarState(
showBottomItems = true,
topTitle = context.getString(R.string.language),
)
LockdownSettings ->
NavbarState(
topLeading = {
IconButton(onClick = { navController.pop() }) {
Icon(
Icons.AutoMirrored.Rounded.ArrowBack,
stringResource(R.string.back),
)
}
},
showBottomItems = true,
topTitle = context.getString(R.string.lockdown_settings),
topTrailing = {
IconButton(
onClick = {
keyboardController?.hide()
sharedViewModel.postSideEffect(LocalSideEffect.SaveChanges)
}
) {
Icon(Icons.Rounded.Save, stringResource(R.string.save))
}
},
)
License ->
NavbarState(
topLeading = {
@@ -211,8 +235,11 @@ fun currentRouteAsNavbarState(
}
},
)
is Config -> {
val tunnelName = sharedState.tunnels.find { it.id == route.id }?.name
is Config,
is ConfigGlobal -> {
val tunnelName =
if (route is Config) sharedState.tunnels.find { it.id == route.id }?.name
else context.getString(R.string.global_dns_servers)
NavbarState(
topLeading = {
IconButton(onClick = { navController.pop() }) {
@@ -236,8 +263,12 @@ fun currentRouteAsNavbarState(
},
)
}
is SplitTunnel -> {
val tunnelName = sharedState.tunnels.find { it.id == route.id }?.name
is SplitTunnel,
is SplitTunnelGlobal -> {
val tunnelName =
if (route is SplitTunnel)
sharedState.tunnels.find { it.id == route.id }?.name
else context.getString(R.string.global_split_tunneling)
NavbarState(
topLeading = {
IconButton(onClick = { navController.pop() }) {
@@ -249,63 +280,31 @@ fun currentRouteAsNavbarState(
},
topTitle = tunnelName ?: "",
topTrailing = {
IconButton(
onClick = {
sharedViewModel.postSideEffect(LocalSideEffect.SaveChanges)
Row {
IconButton(
onClick = {
sharedViewModel.postSideEffect(
LocalSideEffect.Modal.SelectTunnel
)
}
) {
Icon(
Icons.Outlined.ContentPasteGo,
stringResource(R.string.copy_from),
)
}
IconButton(
onClick = {
sharedViewModel.postSideEffect(LocalSideEffect.SaveChanges)
}
) {
Icon(Icons.Rounded.Save, stringResource(R.string.save))
}
) {
Icon(Icons.Rounded.Save, stringResource(R.string.save))
}
},
showBottomItems = true,
)
}
is SplitTunnelGlobal -> {
NavbarState(
topLeading = {
IconButton(onClick = { navController.pop() }) {
Icon(
Icons.AutoMirrored.Rounded.ArrowBack,
stringResource(R.string.back),
)
}
},
topTitle = context.getString(R.string.splt_tunneling),
topTrailing = {
IconButton(
onClick = {
sharedViewModel.postSideEffect(LocalSideEffect.SaveChanges)
}
) {
Icon(Icons.Rounded.Save, stringResource(R.string.save))
}
},
showBottomItems = true,
)
}
is ConfigGlobal -> {
NavbarState(
topLeading = {
IconButton(onClick = { navController.pop() }) {
Icon(
Icons.AutoMirrored.Rounded.ArrowBack,
stringResource(R.string.back),
)
}
},
showBottomItems = true,
topTitle = context.getString(R.string.configuration),
topTrailing = {
IconButton(
onClick = {
sharedViewModel.postSideEffect(LocalSideEffect.SaveChanges)
}
) {
Icon(Icons.Rounded.Save, stringResource(R.string.save))
}
},
)
}
Support ->
NavbarState(
topTitle = context.getString(R.string.support),
@@ -337,7 +336,7 @@ fun currentRouteAsNavbarState(
topTitle = context.getString(R.string.ping_monitor),
showBottomItems = true,
)
is TunnelOptions -> {
is TunnelSettings -> {
val tunnelName = sharedState.tunnels.find { it.id == route.id }?.name
NavbarState(
topLeading = {
@@ -497,20 +496,6 @@ fun currentRouteAsNavbarState(
showBottomItems = true,
)
}
is TunnelGlobals -> {
NavbarState(
topLeading = {
IconButton(onClick = { navController.pop() }) {
Icon(
Icons.AutoMirrored.Rounded.ArrowBack,
stringResource(R.string.back),
)
}
},
topTitle = context.getString(R.string.global_overrides),
showBottomItems = true,
)
}
is WifiPreferences -> {
NavbarState(
topLeading = {
@@ -36,13 +36,13 @@ import androidx.core.net.toUri
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.zaneschepke.networkmonitor.ActiveNetwork
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.domain.enums.NetworkType
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.common.button.SwitchWithDivider
import com.zaneschepke.wireguardautotunnel.ui.common.button.ThemedSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberClipboardHelper
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.common.text.DescriptionText
@@ -59,9 +59,9 @@ fun AutoTunnelScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
val clipboard = rememberClipboardHelper()
val sharedUiState by shareViewModel.container.stateFlow.collectAsStateWithLifecycle()
val autoTunnelState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
if (autoTunnelState.isLoading) return
if (uiState.isLoading) return
val batteryActivity =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
@@ -79,11 +79,11 @@ fun AutoTunnelScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
}
val (ethernetTunnel, mobileDataTunnel, mappedTunnels) =
remember(autoTunnelState.tunnels) {
remember(uiState.tunnels) {
Triple(
autoTunnelState.tunnels.firstOrNull { it.isEthernetTunnel },
autoTunnelState.tunnels.firstOrNull { it.isMobileDataTunnel },
autoTunnelState.tunnels.any { it.tunnelNetworks.isNotEmpty() },
uiState.tunnels.firstOrNull { it.isEthernetTunnel },
uiState.tunnels.firstOrNull { it.isMobileDataTunnel },
uiState.tunnels.any { it.tunnelNetworks.isNotEmpty() },
)
}
@@ -94,8 +94,8 @@ fun AutoTunnelScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
) {
Column {
val (title, buttonText, icon) =
remember(autoTunnelState.autoTunnelActive) {
when (autoTunnelState.autoTunnelActive) {
remember(uiState.autoTunnelActive) {
when (uiState.autoTunnelActive) {
true ->
Triple(
context.getString(R.string.auto_tunnel_running),
@@ -112,7 +112,7 @@ fun AutoTunnelScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
}
fun onAutoTunnelClick() {
if (sharedUiState.isBatteryOptimizationShown)
if (!sharedUiState.isBatteryOptimizationShown)
return requestDisableBatteryOptimizations()
viewModel.toggleAutoTunnel(sharedUiState.settings.appMode)
}
@@ -140,35 +140,20 @@ fun AutoTunnelScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
stringResource(R.string.networks),
modifier = Modifier.padding(horizontal = 16.dp),
)
val activeNetworkType by
remember(autoTunnelState.connectivityState) {
val localizedNetworkType by
remember(uiState.connectivityState) {
derivedStateOf {
val connectivity = autoTunnelState.connectivityState
when {
connectivity?.ethernetConnected == true -> NetworkType.ETHERNET
connectivity?.wifiState?.connected == true -> NetworkType.WIFI
connectivity?.cellularConnected == true -> NetworkType.MOBILE_DATA
else -> NetworkType.NONE
when (uiState.connectivityState?.activeNetwork) {
is ActiveNetwork.Wifi -> context.getString(R.string.wifi)
is ActiveNetwork.Ethernet -> context.getString(R.string.ethernet)
is ActiveNetwork.Cellular -> context.getString(R.string.mobile_data)
is ActiveNetwork.Disconnected -> context.getString(R.string.no_network)
null -> context.getString(R.string.no_network)
}
}
}
val localizedNetworkType =
when (activeNetworkType) {
NetworkType.WIFI -> stringResource(R.string.wifi)
NetworkType.ETHERNET -> stringResource(R.string.ethernet)
NetworkType.MOBILE_DATA -> stringResource(R.string.mobile_data)
NetworkType.NONE -> stringResource(R.string.no_network)
}
val ssid by
remember(autoTunnelState.connectivityState) {
derivedStateOf {
autoTunnelState.connectivityState?.wifiState?.ssid
?: context.getString(R.string.unknown)
}
}
SurfaceRow(
leading = {
Icon(ImageVector.vectorResource(R.drawable.globe), contentDescription = null)
@@ -181,7 +166,7 @@ fun AutoTunnelScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
}
},
description =
if (activeNetworkType == NetworkType.WIFI) {
(uiState.connectivityState?.activeNetwork as? ActiveNetwork.Wifi)?.let {
{
Column {
DescriptionText(
@@ -189,10 +174,8 @@ fun AutoTunnelScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
append(stringResource(R.string.security_type))
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
append(
autoTunnelState.connectivityState
?.wifiState
?.securityType
?.name ?: stringResource(R.string.unknown)
it.securityType?.name
?: stringResource(R.string.unknown)
)
}
}
@@ -201,21 +184,24 @@ fun AutoTunnelScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
buildAnnotatedString {
append(stringResource(R.string.network_name))
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
append(ssid)
append(it.ssid)
}
}
)
}
}
} else null,
},
trailing =
if (activeNetworkType == NetworkType.WIFI) {
if (uiState.connectivityState?.activeNetwork is ActiveNetwork.Wifi) {
{ Icon(Icons.Outlined.ContentCopy, contentDescription = null) }
} else null,
onClick =
if (activeNetworkType == NetworkType.WIFI) {
{ clipboard.copy(ssid, context.getString(R.string.wifi)) }
} else null,
onClick = {
when (val network = uiState.connectivityState?.activeNetwork) {
is ActiveNetwork.Wifi ->
clipboard.copy(network.ssid, context.getString(R.string.wifi))
else -> Unit
}
},
)
SurfaceRow(
@@ -223,7 +209,7 @@ fun AutoTunnelScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
title = stringResource(R.string.tunnel_on_wifi),
trailing = { modifier ->
SwitchWithDivider(
checked = autoTunnelState.autoTunnelSettings.isTunnelOnWifiEnabled,
checked = uiState.autoTunnelSettings.isTunnelOnWifiEnabled,
onClick = { viewModel.setAutoTunnelOnWifiEnabled(it) },
modifier = modifier,
)
@@ -248,7 +234,7 @@ fun AutoTunnelScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
title = stringResource(R.string.tunnel_mobile_data),
trailing = { modifier ->
SwitchWithDivider(
checked = autoTunnelState.autoTunnelSettings.isTunnelOnMobileDataEnabled,
checked = uiState.autoTunnelSettings.isTunnelOnMobileDataEnabled,
onClick = { viewModel.setTunnelOnCellular(it) },
modifier = modifier,
)
@@ -271,7 +257,7 @@ fun AutoTunnelScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
title = stringResource(R.string.tunnel_on_ethernet),
trailing = { modifier ->
SwitchWithDivider(
checked = autoTunnelState.autoTunnelSettings.isTunnelOnEthernetEnabled,
checked = uiState.autoTunnelSettings.isTunnelOnEthernetEnabled,
onClick = { viewModel.setTunnelOnEthernet(it) },
modifier = modifier,
)
@@ -294,14 +280,14 @@ fun AutoTunnelScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
title = stringResource(R.string.stop_on_no_internet),
description = { DescriptionText(stringResource(R.string.stop_on_internet_loss)) },
trailing = {
ScaledSwitch(
checked = autoTunnelState.autoTunnelSettings.isStopOnNoInternetEnabled,
ThemedSwitch(
checked = uiState.autoTunnelSettings.isStopOnNoInternetEnabled,
onClick = { viewModel.setStopOnNoInternetEnabled(it) },
)
},
onClick = {
viewModel.setStopOnNoInternetEnabled(
!autoTunnelState.autoTunnelSettings.isStopOnNoInternetEnabled
!uiState.autoTunnelSettings.isStopOnNoInternetEnabled
)
},
)
@@ -315,14 +301,12 @@ fun AutoTunnelScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
leading = { Icon(Icons.Outlined.RestartAlt, contentDescription = null) },
title = stringResource(R.string.restart_at_boot),
trailing = {
ScaledSwitch(
checked = autoTunnelState.autoTunnelSettings.startOnBoot,
ThemedSwitch(
checked = uiState.autoTunnelSettings.startOnBoot,
onClick = { viewModel.setStartAtBoot(it) },
)
},
onClick = {
viewModel.setStartAtBoot(!autoTunnelState.autoTunnelSettings.startOnBoot)
},
onClick = { viewModel.setStartAtBoot(!uiState.autoTunnelSettings.startOnBoot) },
)
SurfaceRow(
leading = { Icon(Icons.Outlined.Settings, contentDescription = null) },
@@ -26,8 +26,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.banner.WarningBanner
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.common.button.ThemedSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.common.text.DescriptionText
@@ -47,35 +47,37 @@ fun WifiSettingsScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
val context = LocalContext.current
val navController = LocalNavController.current
val autoTunnelState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
if (autoTunnelState.isLoading) return
if (uiState.isLoading) return
var showLocationDialog by remember { mutableStateOf(false) }
var currentText by rememberSaveable { mutableStateOf("") }
LaunchedEffect(autoTunnelState.autoTunnelSettings.trustedNetworkSSIDs) { currentText = "" }
LaunchedEffect(uiState.autoTunnelSettings.trustedNetworkSSIDs) { currentText = "" }
val warnings by
remember(
autoTunnelState.connectivityState?.wifiState,
autoTunnelState.autoTunnelSettings.trustedNetworkSSIDs,
autoTunnelState.autoTunnelSettings.wifiDetectionMethod,
autoTunnelState.tunnels,
uiState.connectivityState,
uiState.autoTunnelSettings.trustedNetworkSSIDs,
uiState.autoTunnelSettings.wifiDetectionMethod,
uiState.tunnels,
) {
derivedStateOf {
val wifiState = autoTunnelState.connectivityState?.wifiState
val needsLocation =
autoTunnelState.autoTunnelSettings.wifiDetectionMethod
.needsLocationPermissions()
uiState.autoTunnelSettings.wifiDetectionMethod.needsLocationPermissions()
val hasConfigs =
autoTunnelState.autoTunnelSettings.trustedNetworkSSIDs.isNotEmpty() ||
autoTunnelState.tunnels.any { it.tunnelNetworks.isNotEmpty() }
uiState.autoTunnelSettings.trustedNetworkSSIDs.isNotEmpty() ||
uiState.tunnels.any { it.tunnelNetworks.isNotEmpty() }
val showServicesWarning =
(wifiState?.locationServicesEnabled == false) && needsLocation && hasConfigs
(uiState.connectivityState?.locationServicesEnabled == false) &&
needsLocation &&
hasConfigs
val showPermissionsWarning =
(wifiState?.locationPermissionsGranted == false) && needsLocation && hasConfigs
(uiState.connectivityState?.locationPermissionsGranted == false) &&
needsLocation &&
hasConfigs
showServicesWarning to showPermissionsWarning
}
@@ -88,9 +90,9 @@ fun WifiSettingsScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
showLocationDialog = false
},
onDismiss = { showLocationDialog = false },
title = { Text(stringResource(R.string.location_permissions)) },
title = stringResource(R.string.location_permissions),
body = { Text(stringResource(R.string.location_justification)) },
confirmText = { Text(stringResource(R.string.open_settings)) },
confirmText = stringResource(R.string.open_settings),
)
}
@@ -138,9 +140,7 @@ fun WifiSettingsScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
DescriptionText(
stringResource(
R.string.current_template,
autoTunnelState.autoTunnelSettings.wifiDetectionMethod.asTitleString(
context
),
uiState.autoTunnelSettings.wifiDetectionMethod.asTitleString(context),
)
)
},
@@ -156,15 +156,13 @@ fun WifiSettingsScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
)
},
trailing = {
ScaledSwitch(
checked = autoTunnelState.autoTunnelSettings.isWildcardsEnabled,
ThemedSwitch(
checked = uiState.autoTunnelSettings.isWildcardsEnabled,
onClick = { viewModel.setWildcardsEnabled(it) },
)
},
onClick = {
viewModel.setWildcardsEnabled(
!autoTunnelState.autoTunnelSettings.isWildcardsEnabled
)
viewModel.setWildcardsEnabled(!uiState.autoTunnelSettings.isWildcardsEnabled)
},
)
}
@@ -174,14 +172,13 @@ fun WifiSettingsScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
title = stringResource(R.string.trusted_wifi_names),
expandedContent = {
TrustedNetworkTextBox(
autoTunnelState.autoTunnelSettings.trustedNetworkSSIDs,
uiState.autoTunnelSettings.trustedNetworkSSIDs,
onDelete = { viewModel.removeTrustedNetworkName(it) },
currentText = currentText,
onSave = { ssid -> viewModel.saveTrustedNetworkName(ssid) },
onValueChange = { currentText = it },
supporting = {
if (autoTunnelState.autoTunnelSettings.isWildcardsEnabled)
WildcardsLabel()
if (uiState.autoTunnelSettings.isWildcardsEnabled) WildcardsLabel()
},
modifier = Modifier.padding(top = 4.dp),
)
@@ -7,20 +7,21 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.CallSplit
import androidx.compose.material.icons.automirrored.outlined.ViewQuilt
import androidx.compose.material.icons.outlined.*
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -29,21 +30,22 @@ import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.SheetButtonWithDivider
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.common.button.SwitchWithDivider
import com.zaneschepke.wireguardautotunnel.ui.common.button.ThemedSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.common.text.DescriptionText
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.BackupBottomSheet
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.proxy.compoents.AppModeBottomSheet
import com.zaneschepke.wireguardautotunnel.ui.theme.Disabled
import com.zaneschepke.wireguardautotunnel.util.StringValue
import com.zaneschepke.wireguardautotunnel.util.extensions.asString
import com.zaneschepke.wireguardautotunnel.util.extensions.asTitleString
import com.zaneschepke.wireguardautotunnel.util.extensions.capitalize
import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
import com.zaneschepke.wireguardautotunnel.viewmodel.SettingsViewModel
import java.util.*
@Composable
fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
@@ -51,31 +53,26 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
val navController = LocalNavController.current
val sharedViewModel = LocalSharedVm.current
val locale = remember { Locale.getDefault() }
val locale = Locale.current.platformLocale
val sharedState by sharedViewModel.container.stateFlow.collectAsStateWithLifecycle()
val settingsState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
val sharedUiState by sharedViewModel.container.stateFlow.collectAsStateWithLifecycle()
val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
if (settingsState.isLoading) return
if (uiState.isLoading) return
var showBackupSheet by rememberSaveable { mutableStateOf(false) }
var showAppModeSheet by rememberSaveable { mutableStateOf(false) }
val appMode = settingsState.settings.appMode
val appMode = uiState.settings.appMode
val dnsEnabled by rememberSaveable(appMode) { mutableStateOf(appMode != AppMode.KERNEL) }
val showProxySettings by
val showModeDivider by
remember(appMode) {
derivedStateOf {
when (appMode) {
AppMode.PROXY -> true
else -> false
}
}
derivedStateOf { appMode == AppMode.PROXY || appMode == AppMode.LOCK_DOWN }
}
fun performBackupRestore(action: () -> Unit) {
if (sharedState.activeTunnels.isNotEmpty() || sharedState.isAutoTunnelActive)
if (sharedUiState.activeTunnels.isNotEmpty() || sharedUiState.isAutoTunnelActive)
return context.showToast(R.string.all_services_disabled)
showBackupSheet = false
action()
@@ -89,22 +86,10 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
showBackupSheet = false
}
if (showAppModeSheet)
AppModeBottomSheet(sharedViewModel::setAppMode, settingsState.settings.appMode) {
AppModeBottomSheet(sharedViewModel::setAppMode, uiState.settings.appMode) {
showAppModeSheet = false
}
val isPingMonitoringAvailable by
remember(settingsState.settings.appMode) {
derivedStateOf {
settingsState.settings.appMode != AppMode.PROXY &&
settingsState.settings.appMode != AppMode.LOCK_DOWN
}
}
LaunchedEffect(isPingMonitoringAvailable) {
if (!isPingMonitoringAvailable) viewModel.setPingEnabled(false)
}
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Top),
@@ -119,11 +104,8 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
leading = {
Icon(ImageVector.vectorResource(R.drawable.sdk), contentDescription = null)
},
trailing = {
Icon(
Icons.Outlined.ExpandMore,
contentDescription = stringResource(R.string.select),
)
trailing = { modifier ->
SheetButtonWithDivider(showModeDivider, modifier) { showAppModeSheet = true }
},
title = stringResource(R.string.backend_mode),
description = {
@@ -131,40 +113,23 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
stringResource(R.string.current_template, appMode.asTitleString(context))
)
},
onClick = { showAppModeSheet = true },
onClick = {
when (appMode) {
AppMode.PROXY -> navController.push(Route.ProxySettings)
AppMode.LOCK_DOWN -> navController.push(Route.LockdownSettings)
AppMode.KERNEL,
AppMode.VPN -> showAppModeSheet = true
}
},
)
if (appMode == AppMode.LOCK_DOWN) {
SurfaceRow(
leading = { Icon(Icons.Outlined.Lan, contentDescription = null) },
title = stringResource(R.string.allow_lan_traffic),
description = {
Text(
text = stringResource(R.string.bypass_lan_for_kill_switch),
style =
MaterialTheme.typography.bodySmall.copy(
MaterialTheme.colorScheme.outline
),
)
},
trailing = {
ScaledSwitch(
checked = settingsState.settings.isLanOnKillSwitchEnabled,
onClick = { viewModel.setLanKillSwitchEnabled(it) },
)
},
onClick = {
viewModel.setLanKillSwitchEnabled(
!settingsState.settings.isLanOnKillSwitchEnabled
)
},
)
}
SurfaceRow(
leading = {
Icon(
Icons.Outlined.Dns,
null,
tint = if (dnsEnabled) MaterialTheme.colorScheme.onSurface else Color.Gray,
tint =
if (dnsEnabled) MaterialTheme.colorScheme.onSurface
else MaterialTheme.colorScheme.outline,
)
},
title = stringResource(R.string.dns_settings),
@@ -182,29 +147,39 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
)
SurfaceRow(
leading = {
Icon(ImageVector.vectorResource(R.drawable.globe), contentDescription = null)
},
title = stringResource(R.string.global_overrides),
trailing = { modifier ->
SwitchWithDivider(
checked = settingsState.settings.isTunnelGlobalsEnabled,
onClick = { viewModel.setTunnelGlobals(it) },
modifier = modifier,
Icon(
Icons.AutoMirrored.Outlined.CallSplit,
contentDescription = null,
tint =
if (sharedUiState.proxyEnabled) Disabled
else MaterialTheme.colorScheme.onSurface,
)
},
enabled = !sharedUiState.proxyEnabled,
title = stringResource(R.string.global_split_tunneling),
trailing = { modifier ->
SwitchWithDivider(
checked = uiState.settings.isGlobalSplitTunnelEnabled,
onClick = { viewModel.setGlobalSplitTunneling(it) },
modifier = modifier,
enabled = !sharedUiState.proxyEnabled,
)
},
description =
if (sharedUiState.proxyEnabled) {
{
DescriptionText(
stringResource(R.string.unavailable_in_mode),
disabled = true,
)
}
} else null,
onClick = {
settingsState.globalTunnelConfig?.let {
navController.push(Route.TunnelGlobals(it.id))
uiState.globalTunnelConfig?.let {
navController.push(Route.SplitTunnelGlobal(id = it.id))
}
},
)
if (showProxySettings) {
SurfaceRow(
leading = { Icon(ImageVector.vectorResource(R.drawable.proxy), null) },
title = stringResource(R.string.proxy_settings),
onClick = { navController.push(Route.ProxySettings) },
)
}
SurfaceRow(
leading = { Icon(Icons.Outlined.Android, null) },
title = stringResource(R.string.android_integrations),
@@ -222,17 +197,27 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
Icons.Outlined.NetworkPing,
contentDescription = null,
tint =
if (isPingMonitoringAvailable) MaterialTheme.colorScheme.onSurface
else Color.Gray,
if (!sharedUiState.proxyEnabled) MaterialTheme.colorScheme.onSurface
else Disabled,
)
},
title = stringResource(R.string.ping_monitor),
enabled = isPingMonitoringAvailable,
trailing = {
enabled = !sharedUiState.proxyEnabled,
description =
if (sharedUiState.proxyEnabled) {
{
DescriptionText(
stringResource(R.string.unavailable_in_mode),
disabled = true,
)
}
} else null,
trailing = { modifier ->
SwitchWithDivider(
checked = settingsState.monitoring.isPingEnabled,
checked = uiState.monitoring.isPingEnabled,
onClick = { viewModel.setPingEnabled(it) },
enabled = isPingMonitoringAvailable,
enabled = !sharedUiState.proxyEnabled,
modifier = modifier,
)
},
onClick = { navController.push(Route.TunnelMonitoring) },
@@ -242,7 +227,7 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
title = stringResource(R.string.local_logging),
trailing = { modifier ->
SwitchWithDivider(
checked = settingsState.monitoring.isLocalLogsEnabled,
checked = uiState.monitoring.isLocalLogsEnabled,
onClick = { viewModel.setLocalLogging(it) },
modifier = modifier,
)
@@ -266,8 +251,8 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
leading = { Icon(Icons.Outlined.Pin, contentDescription = null) },
title = stringResource(R.string.enable_app_lock),
trailing = {
ScaledSwitch(
checked = settingsState.isPinLockEnabled,
ThemedSwitch(
checked = uiState.isPinLockEnabled,
onClick = {
if (it) {
navController.push(Route.Lock)
@@ -278,7 +263,7 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
)
},
onClick = {
if (!settingsState.isPinLockEnabled) {
if (!uiState.isPinLockEnabled) {
navController.push(Route.Lock)
} else {
sharedViewModel.setPinLockEnabled(false)
@@ -289,11 +274,13 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
leading = { Icon(Icons.Outlined.SettingsBackupRestore, contentDescription = null) },
title = stringResource(R.string.backup_and_restore),
onClick = { showBackupSheet = true },
trailing = {
Icon(
Icons.Outlined.ExpandMore,
contentDescription = stringResource(R.string.select),
)
trailing = { modifier ->
IconButton(modifier = modifier, onClick = { showBackupSheet = true }) {
Icon(
Icons.Outlined.ExpandMore,
contentDescription = stringResource(R.string.select),
)
}
},
)
}
@@ -14,13 +14,13 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.intl.Locale
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.util.LocaleUtil
import java.text.Collator
import java.util.*
@Composable
fun LanguageScreen() {
@@ -28,11 +28,11 @@ fun LanguageScreen() {
val sharedViewModel = LocalSharedVm.current
val appState by sharedViewModel.container.stateFlow.collectAsStateWithLifecycle()
val collator = Collator.getInstance(Locale.getDefault())
val collator = Collator.getInstance(Locale.current.platformLocale)
val locales =
LocaleUtil.supportedLocales.map {
val tag = it.replace("_", "-")
Locale.forLanguageTag(tag)
java.util.Locale.forLanguageTag(tag)
}
val sortedLocales =
@@ -46,7 +46,7 @@ fun LanguageScreen() {
remember(appState.locale, sortedLocales) {
if (appState.locale == LocaleUtil.OPTION_PHONE_LANGUAGE) 0
else {
val selectedLocale = Locale.forLanguageTag(appState.locale)
val selectedLocale = java.util.Locale.forLanguageTag(appState.locale)
sortedLocales.indexOfFirst {
it.toLanguageTag() == selectedLocale.toLanguageTag()
} + 1
@@ -4,6 +4,7 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
@@ -14,30 +15,42 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.model.DnsProtocol
import com.zaneschepke.wireguardautotunnel.data.model.DnsProvider
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.common.button.SwitchWithDivider
import com.zaneschepke.wireguardautotunnel.ui.common.dropdown.LabelledDropdown
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
import com.zaneschepke.wireguardautotunnel.util.extensions.capitalize
import com.zaneschepke.wireguardautotunnel.viewmodel.DnsViewModel
@Composable
fun DnsSettingsScreen(viewModel: DnsViewModel = hiltViewModel()) {
val context = LocalContext.current
val navController = LocalNavController.current
val dnsUiState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
if (dnsUiState.isLoading) return
val locale = Locale.current.platformLocale
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.Top),
verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Top),
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()),
) {
Column {
GroupLabel(stringResource(R.string.endpoint), Modifier.padding(horizontal = 16.dp))
LabelledDropdown(
title = stringResource(R.string.dns_protocol),
leading = { Icon(Icons.Outlined.Dns, contentDescription = null) },
@@ -59,5 +72,27 @@ fun DnsSettingsScreen(viewModel: DnsViewModel = hiltViewModel()) {
)
}
}
Column {
GroupLabel(
stringResource(R.string.tunnel).capitalize(locale),
Modifier.padding(horizontal = 16.dp),
)
SurfaceRow(
leading = {
Icon(ImageVector.vectorResource(R.drawable.host), contentDescription = null)
},
title = stringResource(R.string.global_dns_servers),
trailing = { modifier ->
SwitchWithDivider(
checked = dnsUiState.dnsSettings.isGlobalTunnelDnsEnabled,
onClick = { viewModel.setGlobalTunnelDnsEnabled(it) },
modifier = modifier,
)
},
onClick = {
dnsUiState.globalConfig?.let { navController.push(Route.ConfigGlobal(it.id)) }
},
)
}
}
}
@@ -1,46 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.globals
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.CallSplit
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
@Composable
fun TunnelGlobalsScreen(globalTunnelId: Int) {
val navController = LocalNavController.current
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Top),
modifier = Modifier.verticalScroll(rememberScrollState()).fillMaxSize(),
) {
Column {
SurfaceRow(
leading = { Icon(Icons.Outlined.Settings, contentDescription = null) },
title = stringResource(R.string.configuration),
onClick = { navController.push(Route.ConfigGlobal(globalTunnelId)) },
)
SurfaceRow(
leading = {
Icon(Icons.AutoMirrored.Outlined.CallSplit, contentDescription = null)
},
title = stringResource(R.string.splt_tunneling),
onClick = { navController.push(Route.SplitTunnelGlobal(id = globalTunnelId)) },
)
}
}
}
@@ -16,7 +16,6 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
@@ -25,8 +24,8 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.common.button.ThemedSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberClipboardHelper
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.common.security.SecureScreenFromRecording
@@ -72,7 +71,7 @@ fun AndroidIntegrationsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
SurfaceRow(
leading = { Icon(Icons.Outlined.VpnLock, contentDescription = null) },
trailing = {
ScaledSwitch(
ThemedSwitch(
checked = isAlwaysOnEnabled,
onClick = { viewModel.setAlwaysOnVpnEnabled(it) },
)
@@ -94,12 +93,12 @@ fun AndroidIntegrationsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
Icons.Outlined.Restore,
contentDescription = null,
tint =
if (isAlwaysOnEnabled) Color.Gray
if (isAlwaysOnEnabled) MaterialTheme.colorScheme.outline
else MaterialTheme.colorScheme.onSurface,
)
},
trailing = {
ScaledSwitch(
ThemedSwitch(
checked = settingsState.settings.isRestoreOnBootEnabled,
onClick = { viewModel.setRestoreOnBootEnabled(it) },
enabled = !isAlwaysOnEnabled,
@@ -117,7 +116,7 @@ fun AndroidIntegrationsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
SurfaceRow(
leading = { Icon(Icons.Filled.AppShortcut, contentDescription = null) },
trailing = {
ScaledSwitch(
ThemedSwitch(
checked = settingsState.settings.isShortcutsEnabled,
onClick = { viewModel.setShortcutsEnabled(it) },
)
@@ -130,7 +129,7 @@ fun AndroidIntegrationsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
SurfaceRow(
leading = { Icon(Icons.Filled.SmartToy, contentDescription = null) },
trailing = {
ScaledSwitch(
ThemedSwitch(
checked = settingsState.isRemoteEnabled,
onClick = { viewModel.setRemoteEnabled(it) },
)
@@ -0,0 +1,126 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.lockdown
import android.os.Build
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 },
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
SurfaceRow(
leading = { Icon(Icons.Outlined.DataUsage, contentDescription = null) },
title = stringResource(R.string.metered_tunnel),
trailing = { ThemedSwitch(checked = metered, onClick = { metered = it }) },
onClick = { metered = !metered },
)
}
SurfaceRow(
leading = {
Icon(ImageVector.vectorResource(R.drawable.host), contentDescription = null)
},
title = stringResource(R.string.dual_stack),
description = { DescriptionText(stringResource(R.string.dual_stack_description)) },
trailing = { ThemedSwitch(checked = dualStack, onClick = { dualStack = it }) },
onClick = { dualStack = !dualStack },
)
}
}
}
@@ -21,8 +21,8 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.common.button.ThemedSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.dropdown.LabelledDropdown
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
@@ -86,7 +86,7 @@ fun TunnelMonitoringScreen(viewModel: MonitoringViewModel = hiltViewModel()) {
leading = { Icon(Icons.Outlined.QueryStats, contentDescription = null) },
title = stringResource(R.string.display_detailed_ping_stats),
trailing = {
ScaledSwitch(
ThemedSwitch(
checked = monitoringUiState.monitoringSettings.showDetailedPingStats,
onClick = { viewModel.setDetailedPingStats(it) },
)

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