mirror of
https://github.com/wgtunnel/android.git
synced 2026-06-02 08:33:40 +02:00
Compare commits
99 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9124fcc133 | |||
| fed9537f5c | |||
| 9f4e801aad | |||
| 9cb5796f79 | |||
| b3ab9f6aae | |||
| c1760fda10 | |||
| 53278243e8 | |||
| 0963626164 | |||
| 82bda83464 | |||
| 7e264a6f19 | |||
| c18b3b7ba0 | |||
| 5f03b190dd | |||
| f3a5f14b0e | |||
| 70ce1adda4 | |||
| 87be6fa9ea | |||
| 03df457b55 | |||
| c14556a347 | |||
| f83559f910 | |||
| bf432cca0d | |||
| 68dc57422c | |||
| d528c9b56d | |||
| a42178258e | |||
| fee2878fa0 | |||
| 9d312afdba | |||
| 49f0d7f272 | |||
| 947cd23960 | |||
| d8521bc4c7 | |||
| 82afe54b99 | |||
| f20355e0f8 | |||
| 7d6e55e06e | |||
| 42221da443 | |||
| db920555ce | |||
| 0a3acf04c5 | |||
| 6a9370966c | |||
| 80d63db31a | |||
| 560f6a998b | |||
| 208df1914b | |||
| c24c33c95c | |||
| c130247df1 | |||
| 074229b6b4 | |||
| 585176f08d | |||
| 2ed06728e3 | |||
| 2d9c5ece4a | |||
| 3b69f620fb | |||
| 6369d8975c | |||
| 0c57bea2ff | |||
| 5f8f699ab5 | |||
| d0f58615b0 | |||
| 35982aa345 | |||
| 0344b8fde8 | |||
| bdd7c9689c | |||
| b641539af8 | |||
| 9f9a15a97c | |||
| eeeec5613f | |||
| af21a6a3cf | |||
| 0bf52ad378 | |||
| 0cf39fed68 | |||
| 590985d5cd | |||
| c16a1b9b55 | |||
| 679f6abbcb | |||
| bbc62a26e7 | |||
| e475fd27d9 | |||
| e2dd27e70c | |||
| a994e8e2c1 | |||
| 16d0642a51 | |||
| eac674c996 | |||
| 394188b55f | |||
| d61a4213cf | |||
| 90a693abb4 | |||
| 05b950fbb3 | |||
| 5310cf17e6 | |||
| 3ffc8c5e39 | |||
| e538ec80d0 | |||
| 6b24cca551 | |||
| f0c885708b | |||
| c028b9b5b6 | |||
| de8b39d848 | |||
| a573208db6 | |||
| 0e9a63c850 | |||
| 341a806f29 | |||
| 8d257a604c | |||
| 0b674684ba | |||
| 85a27f48a2 | |||
| 1f978cdf96 | |||
| 4f816fa175 | |||
| ee4ac4e968 | |||
| ff53454966 | |||
| 22c17ef66b | |||
| 7a60b90d2b | |||
| 5fd3f89a59 | |||
| 9510f43252 | |||
| 064aa6aa74 | |||
| 0c09add0e4 | |||
| fd0fd33f71 | |||
| aaeb251bbf | |||
| e563608e49 | |||
| 584f0386b6 | |||
| cf49c34bff | |||
| a0f89d40f5 |
@@ -1,3 +1,4 @@
|
||||
ko_fi: zaneschepke
|
||||
liberapay: zaneschepke
|
||||
github: zaneschepke
|
||||
custom: ["https://wgtunnel.com/donate/"]
|
||||
|
||||
@@ -70,9 +70,10 @@ jobs:
|
||||
outputs:
|
||||
UPLOAD_DIR_ANDROID: ${{ env.UPLOAD_DIR_ANDROID }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v5
|
||||
@@ -86,7 +87,7 @@ jobs:
|
||||
|
||||
- name: Decode Keystore
|
||||
id: decode_keystore
|
||||
uses: timheuer/base64-to-file@v1.2
|
||||
uses: timheuer/base64-to-file@v2.0
|
||||
with:
|
||||
fileName: ${{ env.KEY_STORE_FILE }}
|
||||
fileDir: ${{ env.KEY_STORE_LOCATION }}
|
||||
@@ -122,7 +123,7 @@ jobs:
|
||||
echo "path=$AAB_PATH" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Upload AAB Artifact
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: google-play-aab
|
||||
path: ${{ steps.aab-path.outputs.path }}
|
||||
|
||||
@@ -72,9 +72,10 @@ jobs:
|
||||
outputs:
|
||||
UPLOAD_DIR_ANDROID: ${{ env.UPLOAD_DIR_ANDROID }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
@@ -85,7 +86,7 @@ jobs:
|
||||
run: chmod +x gradlew
|
||||
- name: Decode Keystore
|
||||
id: decode_keystore
|
||||
uses: timheuer/base64-to-file@v1.2
|
||||
uses: timheuer/base64-to-file@v2.0
|
||||
with:
|
||||
fileName: ${{ env.KEY_STORE_FILE }}
|
||||
fileDir: ${{ env.KEY_STORE_LOCATION }}
|
||||
@@ -115,7 +116,7 @@ jobs:
|
||||
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 All APK Artifacts
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: android_artifacts_${{ inputs.flavor }}
|
||||
path: >-
|
||||
|
||||
@@ -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:
|
||||
@@ -43,7 +43,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
@@ -71,7 +73,7 @@ jobs:
|
||||
run: mkdir ${{ github.workspace }}/temp
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: android_artifacts_*
|
||||
path: ${{ github.workspace }}/temp
|
||||
@@ -101,7 +103,7 @@ jobs:
|
||||
|
||||
- name: Create nightly release
|
||||
id: create_release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
body: |
|
||||
${{ env.RELEASE_NOTES }}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
name: notifications
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
@@ -12,6 +13,9 @@ on:
|
||||
jobs:
|
||||
notify:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
PROJECT_NAME: Android
|
||||
|
||||
steps:
|
||||
- name: Send to Telegram - New Issue
|
||||
if: github.event_name == 'issues' && github.event.action == 'opened'
|
||||
@@ -22,8 +26,8 @@ jobs:
|
||||
BODY: ${{ github.event.issue.body || 'No body provided' }}
|
||||
URL: ${{ github.event.issue.html_url }}
|
||||
run: |
|
||||
BODY_TRUNC="${BODY:0:200}" # Truncate to avoid spam
|
||||
TEXT=$(echo -e "🆕 New Issue #$NUMBER: *$TITLE* by $USER\n\n$BODY_TRUNC\n\n[View Issue]($URL)")
|
||||
BODY_TRUNC="${BODY:0:200}"
|
||||
TEXT=$(echo -e "🆕 **${PROJECT_NAME}** — 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) || '' }} \
|
||||
@@ -38,7 +42,7 @@ jobs:
|
||||
USER: ${{ github.event.issue.user.login }}
|
||||
URL: ${{ github.event.issue.html_url }}
|
||||
run: |
|
||||
TEXT=$(echo -e "✅ Issue Closed #$NUMBER: *$TITLE* by $USER\n\n[View Issue]($URL)")
|
||||
TEXT=$(echo -e "✅ **${PROJECT_NAME}** — 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) || '' }} \
|
||||
@@ -54,7 +58,7 @@ jobs:
|
||||
URL: ${{ github.event.release.html_url }}
|
||||
ACTION: ${{ github.event.action }}
|
||||
run: |
|
||||
BODY_TRUNC="${BODY:0:200}" # Truncate to avoid spam
|
||||
BODY_TRUNC="${BODY:0:200}"
|
||||
if [ "$ACTION" == "prereleased" ]; then
|
||||
ICON="🌙"
|
||||
PREFIX="New Nightly Release"
|
||||
@@ -62,7 +66,7 @@ jobs:
|
||||
ICON="🚀"
|
||||
PREFIX="New Release"
|
||||
fi
|
||||
TEXT=$(echo -e "$ICON $PREFIX *$NAME* ($TAG)\n\n$BODY_TRUNC\n\n[View Release]($URL)")
|
||||
TEXT=$(echo -e "$ICON **${PROJECT_NAME}** — $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) || '' }} \
|
||||
@@ -78,8 +82,8 @@ jobs:
|
||||
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=$(echo -e "🆕 **${PROJECT_NAME}** — New Issue #$NUMBER: $TITLE by $USER\n\n$BODY\n\nView Issue: $URL")
|
||||
HTML_MESSAGE=$(echo -e "<p>🆕 <strong>${PROJECT_NAME}</strong> — 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",
|
||||
@@ -101,8 +105,8 @@ jobs:
|
||||
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=$(echo -e "✅ **${PROJECT_NAME}** — Issue Closed #$NUMBER: $TITLE by $USER\n\nView Issue: $URL")
|
||||
HTML_MESSAGE=$(echo -e "<p>✅ <strong>${PROJECT_NAME}</strong> — 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",
|
||||
@@ -132,8 +136,8 @@ jobs:
|
||||
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=$(echo -e "$ICON **${PROJECT_NAME}** — $PREFIX $NAME ($TAG)\n\n$BODY\n\nView Release: $URL")
|
||||
HTML_MESSAGE=$(echo -e "<p>$ICON <strong>${PROJECT_NAME}</strong> — $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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -109,7 +115,7 @@ jobs:
|
||||
run: mkdir ${{ github.workspace }}/temp
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: android_artifacts_*
|
||||
path: ${{ github.workspace }}/temp
|
||||
@@ -118,7 +124,7 @@ jobs:
|
||||
- name: Set version release notes
|
||||
if: ${{ github.event_name == 'push' || inputs.release_type == 'release' }}
|
||||
run: |
|
||||
VERSION_CODE=$(grep "const val VERSION_CODE" buildSrc/src/main/kotlin/Constants.kt | awk '{print $5}')
|
||||
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
|
||||
@@ -137,7 +143,7 @@ jobs:
|
||||
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
body: |
|
||||
${{ env.RELEASE_NOTES }}
|
||||
@@ -166,9 +172,13 @@ jobs:
|
||||
|
||||
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:
|
||||
@@ -205,7 +215,7 @@ jobs:
|
||||
# in the folder specified in the release signing configuration
|
||||
- name: Decode Keystore
|
||||
id: decode_keystore
|
||||
uses: timheuer/base64-to-file@v1.2
|
||||
uses: timheuer/base64-to-file@v2.0
|
||||
with:
|
||||
fileName: ${{ env.KEY_STORE_FILE }}
|
||||
fileDir: ${{ env.KEY_STORE_LOCATION }}
|
||||
@@ -230,4 +240,4 @@ jobs:
|
||||
- name: Distribute app to Prod track 🚀
|
||||
run: |
|
||||
track=${{ github.event_name == 'push' && 'production' || inputs.track }}
|
||||
(cd ${{ github.workspace }} && bundle install && bundle exec fastlane $track)
|
||||
(cd ${{ github.workspace }} && bundle install && bundle exec fastlane $track --verbose)
|
||||
@@ -0,0 +1,6 @@
|
||||
[submodule "hevtunnel/src/main/jni/hev-socks5-tunnel"]
|
||||
path = hevtunnel/src/main/jni/hev-socks5-tunnel
|
||||
url = https://github.com/heiher/hev-socks5-tunnel
|
||||
[submodule "tunnel/tools/amneziawg-tools"]
|
||||
path = tunnel/tools/amneziawg-tools
|
||||
url = https://github.com/amnezia-vpn/amneziawg-tools
|
||||
@@ -21,7 +21,7 @@ and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/)
|
||||
<div align="center">
|
||||
|
||||
[](https://play.google.com/store/apps/details?id=com.zaneschepke.wireguardautotunnel)
|
||||
[](https://github.com/zaneschepke/fdroid)
|
||||
[](https://apt.izzysoft.de/fdroid/index/apk/com.zaneschepke.wireguardautotunnel)
|
||||
[](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>
|
||||
|
||||
+46
-34
@@ -1,10 +1,9 @@
|
||||
import com.android.build.gradle.internal.api.BaseVariantOutputImpl
|
||||
import com.android.build.api.dsl.ApplicationExtension
|
||||
import com.android.build.api.variant.FilterConfiguration
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.hilt.android)
|
||||
alias(libs.plugins.kotlinxSerialization)
|
||||
alias(libs.plugins.ksp)
|
||||
alias(libs.plugins.compose.compiler)
|
||||
@@ -12,7 +11,26 @@ plugins {
|
||||
alias(libs.plugins.licensee)
|
||||
}
|
||||
|
||||
android {
|
||||
ksp {
|
||||
arg("room.schemaLocation", "$projectDir/schemas")
|
||||
}
|
||||
|
||||
licensee {
|
||||
allowedLicenses().forEach { allow(it) }
|
||||
allowedLicenseUrls().forEach { allowUrl(it) }
|
||||
// foss, but missing licenses
|
||||
ignoreDependencies("com.github.T8RIN.QuickieExtended")
|
||||
ignoreDependencies("com.github.topjohnwu.libsu")
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget = JvmTarget.JVM_17
|
||||
freeCompilerArgs = listOf("-XXLanguage:+PropertyParamAnnotationDefaultTargetMode")
|
||||
}
|
||||
}
|
||||
|
||||
configure<ApplicationExtension> {
|
||||
namespace = Constants.APP_ID
|
||||
compileSdk = Constants.TARGET_SDK
|
||||
|
||||
@@ -23,8 +41,6 @@ android {
|
||||
includeInBundle = false
|
||||
}
|
||||
|
||||
ksp { arg("room.schemaLocation", "$projectDir/schemas") }
|
||||
|
||||
// fix okhttp proguard issue
|
||||
packaging { resources { pickFirsts.add("okhttp3/internal/publicsuffix/publicsuffixes.gz") } }
|
||||
|
||||
@@ -120,26 +136,16 @@ android {
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget = JvmTarget.JVM_17
|
||||
freeCompilerArgs = listOf("-XXLanguage:+PropertyParamAnnotationDefaultTargetMode")
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
compose = true
|
||||
buildConfig = true
|
||||
resValues = true
|
||||
}
|
||||
packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } }
|
||||
}
|
||||
|
||||
licensee {
|
||||
allowedLicenses().forEach { allow(it) }
|
||||
allowedLicenseUrls().forEach { allowUrl(it) }
|
||||
}
|
||||
|
||||
android.applicationVariants.all {
|
||||
val variant = this
|
||||
androidComponents {
|
||||
onVariants { variant ->
|
||||
|
||||
val abiNameMap =
|
||||
mapOf(
|
||||
@@ -149,11 +155,14 @@ android {
|
||||
"x86_64" to "x64",
|
||||
)
|
||||
|
||||
variant.outputs.all {
|
||||
val output = this as BaseVariantOutputImpl
|
||||
val abi = output.getFilter("ABI")
|
||||
variant.outputs.forEach { output ->
|
||||
val abi = output.filters.find { it.filterType == FilterConfiguration.FilterType.ABI }?.identifier
|
||||
|
||||
val baseFileName = "${Constants.APP_NAME}-${variant.flavorName}-v${variant.versionName}"
|
||||
val flavorName = variant.productFlavors.joinToString("-") { it.second }
|
||||
|
||||
val versionName = output.versionName.get()
|
||||
|
||||
val baseFileName = "${Constants.APP_NAME}-${flavorName}-v${versionName}"
|
||||
|
||||
val outputFileName =
|
||||
if (!abi.isNullOrEmpty()) {
|
||||
@@ -163,7 +172,7 @@ android {
|
||||
"${baseFileName}.apk"
|
||||
}
|
||||
|
||||
output.outputFileName = outputFileName
|
||||
output.outputFileName.set(outputFileName)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -171,6 +180,7 @@ android {
|
||||
dependencies {
|
||||
implementation(project(":logcatter"))
|
||||
implementation(project(":networkmonitor"))
|
||||
implementation(project(":tunnel"))
|
||||
|
||||
// Core foundations
|
||||
implementation(libs.bundles.androidx.core.full)
|
||||
@@ -187,7 +197,6 @@ dependencies {
|
||||
// Navigation
|
||||
implementation(libs.bundles.androidx.navigation3)
|
||||
implementation(libs.bundles.navigation.lifecycle)
|
||||
implementation(libs.bundles.androidx.hilt)
|
||||
|
||||
// Material and icons
|
||||
implementation(libs.bundles.google.material)
|
||||
@@ -198,11 +207,7 @@ dependencies {
|
||||
implementation(libs.bundles.androidx.datastore)
|
||||
ksp(libs.androidx.room.compiler)
|
||||
|
||||
// DI and work
|
||||
implementation(libs.bundles.hilt.android)
|
||||
implementation(libs.bundles.androidx.work)
|
||||
ksp(libs.hilt.android.compiler)
|
||||
ksp(libs.androidx.hilt.compiler)
|
||||
|
||||
// Networking and serialization
|
||||
implementation(libs.bundles.ktor.client)
|
||||
@@ -212,9 +217,6 @@ dependencies {
|
||||
// State management
|
||||
implementation(libs.bundles.orbit.mvi)
|
||||
|
||||
// Tunnel
|
||||
implementation(libs.bundles.wireguard.tunnel)
|
||||
|
||||
// Shizuku
|
||||
implementation(libs.bundles.shizuku)
|
||||
|
||||
@@ -248,6 +250,16 @@ dependencies {
|
||||
implementation(libs.roomdatabasebackup) {
|
||||
exclude(group = "org.reactivestreams", module = "reactive-streams")
|
||||
}
|
||||
|
||||
// DI
|
||||
implementation(platform(libs.koin.bom))
|
||||
implementation(libs.koin.core)
|
||||
implementation(libs.koin.android)
|
||||
implementation(libs.koin.compose.viewmodel)
|
||||
implementation(libs.koin.androidx.compose)
|
||||
implementation(libs.koin.androidx.navigation)
|
||||
implementation(libs.koin.lazy)
|
||||
implementation(libs.koin.worker)
|
||||
}
|
||||
|
||||
tasks.register<Copy>("copyLicenseeJsonToAssets") {
|
||||
@@ -262,7 +274,7 @@ tasks.register<Copy>("copyLicenseeJsonToAssets") {
|
||||
tasks.named("preBuild") { dependsOn("copyLicenseeJsonToAssets") }
|
||||
|
||||
// https://gist.github.com/obfusk/61046e09cee352ae6dd109911534b12e#fix-proposed-by-linsui-disable-baseline-profiles
|
||||
tasks.whenTaskAdded {
|
||||
tasks.configureEach {
|
||||
if (name.contains("ArtProfile")) {
|
||||
enabled = false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,506 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 30,
|
||||
"identityHash": "28560c6b408d8f5ef28844723e940395",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "tunnel_config",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` 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, `quick_config` TEXT NOT NULL DEFAULT '', `dynamic_dns` INTEGER NOT NULL DEFAULT false, `is_ethernet_tunnel` INTEGER NOT NULL DEFAULT false, `prefer_ipv6` INTEGER NOT NULL DEFAULT false, `position` INTEGER NOT NULL DEFAULT 0, `auto_tunnel_apps` TEXT NOT NULL DEFAULT '[]', `is_metered` INTEGER NOT NULL DEFAULT false, `ipv4_fallback` INTEGER NOT NULL DEFAULT false, `ipv6_restore` INTEGER NOT NULL DEFAULT false)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"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": "quickConfig",
|
||||
"columnName": "quick_config",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "''"
|
||||
},
|
||||
{
|
||||
"fieldPath": "dynamicDnsEnabled",
|
||||
"columnName": "dynamic_dns",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isEthernetTunnel",
|
||||
"columnName": "is_ethernet_tunnel",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isIpv6Preferred",
|
||||
"columnName": "prefer_ipv6",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"fieldPath": "ipv4FallbackEnabled",
|
||||
"columnName": "ipv4_fallback",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "ipv6RestoreEnabled",
|
||||
"columnName": "ipv6_restore",
|
||||
"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, `screen_recording_security` INTEGER NOT NULL DEFAULT 1, `global_amnezia_enabled` INTEGER NOT NULL DEFAULT 0, `tunnel_scripting_enabled` INTEGER NOT NULL DEFAULT 0)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isShortcutsEnabled",
|
||||
"columnName": "is_shortcuts_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isRestoreOnBootEnabled",
|
||||
"columnName": "is_restore_on_boot_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isMultiTunnelEnabled",
|
||||
"columnName": "is_multi_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isGlobalSplitTunnelEnabled",
|
||||
"columnName": "global_split_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "tunnelMode",
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"fieldPath": "screenRecordingSecurityEnabled",
|
||||
"columnName": "screen_recording_security",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "1"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isGlobalAmneziaEnabled",
|
||||
"columnName": "global_amnezia_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "tunnelScriptingEnabled",
|
||||
"columnName": "tunnel_scripting_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "auto_tunnel_settings",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL DEFAULT 0, `trusted_network_ssids` TEXT NOT NULL DEFAULT '', `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT 0, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT 0, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT 0, `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": "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_local_logs_enabled` INTEGER NOT NULL DEFAULT 0, `tunnel_statistics_enabled` INTEGER NOT NULL DEFAULT 1, `tunnel_statistics_poll_interval` INTEGER NOT NULL DEFAULT 3)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isLocalLogsEnabled",
|
||||
"columnName": "is_local_logs_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "tunnelStatisticsEnabled",
|
||||
"columnName": "tunnel_statistics_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "1"
|
||||
},
|
||||
{
|
||||
"fieldPath": "tunnelStatisticsPollInterval",
|
||||
"columnName": "tunnel_statistics_poll_interval",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "3"
|
||||
}
|
||||
],
|
||||
"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, '28560c6b408d8f5ef28844723e940395')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -6,13 +6,11 @@
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<!--for split tunneling-->
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:ignore="QueryAllPackagesPermission" />
|
||||
|
||||
<!--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-->
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
|
||||
|
||||
<!--foreground service permissions-->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
@@ -59,10 +57,10 @@
|
||||
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" />
|
||||
|
||||
<meta-data android:name="android.telephony.PROPERTY_SATELLITE_DATA_OPTIMIZED"
|
||||
android:value="${applicationId}" />
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
@@ -106,13 +104,16 @@
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
|
||||
<provider
|
||||
android:name="androidx.startup.InitializationProvider"
|
||||
android:authorities="${applicationId}.androidx-startup"
|
||||
android:multiprocess="true"
|
||||
tools:node="remove">
|
||||
</provider>
|
||||
|
||||
<provider
|
||||
android:name="androidx.startup.InitializationProvider"
|
||||
android:authorities="${applicationId}.androidx-startup"
|
||||
android:exported="false"
|
||||
tools:node="merge">
|
||||
<meta-data
|
||||
android:name="androidx.work.WorkManagerInitializer"
|
||||
android:value="androidx.startup"
|
||||
tools:node="remove" />
|
||||
</provider>
|
||||
<service
|
||||
android:name=".core.service.tile.TunnelControlTile"
|
||||
android:exported="true"
|
||||
@@ -163,33 +164,6 @@
|
||||
network connectivity monitoring."/>
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".core.service.TunnelForegroundService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="specialUse"
|
||||
android:persistent="true"
|
||||
android:stopWithTask="false"
|
||||
tools:node="merge">
|
||||
<property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="This service sustains non-VpnService virtual tunnels (using gVisor/netstack for
|
||||
isolated networking), keeping connections alive for continuous secure data routing.
|
||||
Persistent foreground operation is essential to handle
|
||||
low-level tunnel maintenance and avoid interruptions, beyond the capabilities of other
|
||||
service types or background work."/>
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".core.service.VpnForegroundService"
|
||||
android:exported="false"
|
||||
android:persistent="true"
|
||||
android:foregroundServiceType="systemExempted"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE">
|
||||
<intent-filter>
|
||||
<action android:name="android.net.VpnService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<receiver
|
||||
android:name=".core.broadcast.RestartReceiver"
|
||||
android:enabled="true"
|
||||
@@ -198,17 +172,12 @@
|
||||
<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
|
||||
android:name=".core.broadcast.KernelReceiver"
|
||||
android:exported="false"
|
||||
android:permission="${applicationId}.permission.CONTROL_TUNNELS">
|
||||
<intent-filter>
|
||||
<action android:name="com.wireguard.android.action.REFRESH_TUNNEL_STATES" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<!--custom security solution for easier user integration-->
|
||||
<receiver
|
||||
android:name=".core.broadcast.RemoteControlReceiver"
|
||||
|
||||
@@ -6,18 +6,38 @@ import android.graphics.Color
|
||||
import android.net.VpnService
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.SystemBarStyle
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@@ -26,31 +46,27 @@ 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
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import androidx.navigation3.runtime.entryProvider
|
||||
import androidx.navigation3.runtime.rememberNavBackStack
|
||||
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
||||
import androidx.navigation3.ui.NavDisplay
|
||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||
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.enums.TunnelMode
|
||||
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
|
||||
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
|
||||
@@ -58,13 +74,13 @@ 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.SecureRoute
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.Tab
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.components.BottomNavbar
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.components.DynamicTopAppBar
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.components.currentRouteAsNavbarState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.functions.rememberNavController
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.AutoTunnelScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.advanced.AutoTunnelAdvancedScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.detection.WifiDetectionMethodScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.disclosure.LocationDisclosureScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.preferred.PreferredTunnelScreen
|
||||
@@ -75,45 +91,56 @@ 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.MonitoringScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.logs.LogsScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.ping.PingTargetScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.security.SecurityScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.donate.DonateScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.donate.crypto.AddressesScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.license.LicenseScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.TunnelsScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.config.ConfigScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.settings.TunnelSettingsScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.settings.config.ConfigScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.settings.config.edit.ConfigEditScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.settings.ipv6.IPv6Screen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.sort.SortScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.splittunnel.SplitTunnelScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.AlertRed
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.OffWhite
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
|
||||
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.*
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.ConfigViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.installApk
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.restartApp
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.ConfigEditViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
|
||||
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.awaitCancellation
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
import org.koin.core.parameter.parametersOf
|
||||
import org.orbitmvi.orbit.compose.collectAsState
|
||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
@Inject lateinit var appStateRepository: AppStateRepository
|
||||
@Inject lateinit var tunnelRepository: TunnelRepository
|
||||
@Inject lateinit var appDatabase: AppDatabase
|
||||
@Inject lateinit var networkMonitor: NetworkMonitor
|
||||
private val appStateRepository: AppStateRepository by inject()
|
||||
private val tunnelRepository: TunnelRepository by inject()
|
||||
private val appDatabase: AppDatabase by inject()
|
||||
private val networkMonitor: NetworkMonitor by inject()
|
||||
|
||||
val viewModel by viewModel<SharedAppViewModel>()
|
||||
private lateinit var roomBackup: RoomBackup
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -129,8 +156,6 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
roomBackup = RoomBackup(this)
|
||||
|
||||
val viewModel by viewModels<SharedAppViewModel>()
|
||||
|
||||
installSplashScreen().apply {
|
||||
setKeepOnScreenCondition { !viewModel.container.stateFlow.value.isAppLoaded }
|
||||
}
|
||||
@@ -138,7 +163,7 @@ class MainActivity : AppCompatActivity() {
|
||||
setContent {
|
||||
val context = LocalContext.current
|
||||
val isTv = isRunningOnTv()
|
||||
val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
|
||||
val uiState by viewModel.collectAsState()
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
LaunchedEffect(uiState.isAppLoaded) {
|
||||
@@ -150,8 +175,8 @@ class MainActivity : AppCompatActivity() {
|
||||
val snackbarState = rememberCustomSnackbarState()
|
||||
var showVpnPermissionDialog by remember { mutableStateOf(false) }
|
||||
var vpnPermissionDenied by remember { mutableStateOf(false) }
|
||||
var requestingAppMode by remember {
|
||||
mutableStateOf<Pair<AppMode?, TunnelConfig?>>(Pair(null, null))
|
||||
var requestingTunnelMode by remember {
|
||||
mutableStateOf<Pair<TunnelMode?, TunnelConfig?>>(Pair(null, null))
|
||||
}
|
||||
|
||||
val startingStack = buildList {
|
||||
@@ -164,10 +189,12 @@ class MainActivity : AppCompatActivity() {
|
||||
var previousRoute by remember { mutableStateOf<Route?>(null) }
|
||||
|
||||
val navController =
|
||||
rememberNavController<NavKey>(backStack, uiState.isLocationDisclosureShown) {
|
||||
previousKey ->
|
||||
previousRoute = previousKey as? Route
|
||||
}
|
||||
rememberNavController(
|
||||
backStack,
|
||||
uiState.isLocationDisclosureShown,
|
||||
onChange = { previousKey -> previousRoute = previousKey as? Route },
|
||||
onExitApp = { finish() },
|
||||
)
|
||||
|
||||
val vpnActivity =
|
||||
rememberLauncherForActivityResult(
|
||||
@@ -179,14 +206,14 @@ class MainActivity : AppCompatActivity() {
|
||||
} else {
|
||||
vpnPermissionDenied = false
|
||||
showVpnPermissionDialog = false
|
||||
val (appMode, config) = requestingAppMode
|
||||
val (appMode, config) = requestingTunnelMode
|
||||
when (appMode) {
|
||||
AppMode.VPN -> if (config != null) viewModel.startTunnel(config)
|
||||
AppMode.LOCK_DOWN -> viewModel.setAppMode(AppMode.LOCK_DOWN)
|
||||
TunnelMode.VPN -> if (config != null) viewModel.startTunnel(config)
|
||||
TunnelMode.LOCK_DOWN -> viewModel.setAppMode(TunnelMode.LOCK_DOWN)
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
requestingAppMode = Pair(null, null)
|
||||
requestingTunnelMode = Pair(null, null)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -196,7 +223,8 @@ class MainActivity : AppCompatActivity() {
|
||||
GlobalSideEffect.ConfigChanged -> restartApp()
|
||||
GlobalSideEffect.PopBackStack -> navController.pop()
|
||||
is GlobalSideEffect.RequestVpnPermission -> {
|
||||
requestingAppMode = Pair(sideEffect.requestingMode, sideEffect.config)
|
||||
requestingTunnelMode =
|
||||
Pair(sideEffect.requestingMode, sideEffect.config)
|
||||
vpnActivity.launch(VpnService.prepare(this@MainActivity))
|
||||
}
|
||||
|
||||
@@ -233,7 +261,6 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalIsAndroidTV provides isTv,
|
||||
LocalSharedVm provides viewModel,
|
||||
LocalNavController provides navController,
|
||||
) {
|
||||
WireguardAutoTunnelTheme(theme = uiState.theme) {
|
||||
@@ -279,10 +306,8 @@ class MainActivity : AppCompatActivity() {
|
||||
append(context.getString(R.string.donation_prompt_suffix))
|
||||
}
|
||||
|
||||
LaunchedEffect(uiState.shouldShowDonationSnackbar) {
|
||||
if (
|
||||
uiState.shouldShowDonationSnackbar && !uiState.settings.alreadyDonated
|
||||
) {
|
||||
LaunchedEffect(Unit) {
|
||||
if (uiState.shouldShowDonationSnackbar && !uiState.alreadyDonated) {
|
||||
viewModel.setShouldShowDonationSnackbar(false)
|
||||
snackbarState.showSnackbar(
|
||||
SnackbarInfo(
|
||||
@@ -294,13 +319,40 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
val isPinVisible by remember { derivedStateOf { showLock } }
|
||||
|
||||
val currentRoute by remember {
|
||||
derivedStateOf { backStack.lastOrNull() as? Route }
|
||||
}
|
||||
|
||||
LaunchedEffect(
|
||||
uiState.isScreenRecordingProtectionEnabled,
|
||||
currentRoute,
|
||||
isPinVisible,
|
||||
) {
|
||||
val isSecureRoute = currentRoute is SecureRoute
|
||||
|
||||
val shouldProtect =
|
||||
uiState.isScreenRecordingProtectionEnabled &&
|
||||
(isSecureRoute || isPinVisible)
|
||||
|
||||
if (shouldProtect) {
|
||||
window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_SECURE,
|
||||
WindowManager.LayoutParams.FLAG_SECURE,
|
||||
)
|
||||
} else {
|
||||
delay(500L)
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
awaitCancellation()
|
||||
}
|
||||
}
|
||||
|
||||
if (showLock) {
|
||||
PinManager.initialize(context = this@MainActivity)
|
||||
PinLockScreen()
|
||||
} else {
|
||||
val currentRoute by remember {
|
||||
derivedStateOf { backStack.lastOrNull() as? Route }
|
||||
}
|
||||
|
||||
val currentTab by remember {
|
||||
derivedStateOf { Tab.fromRoute(currentRoute ?: Route.Tunnels) }
|
||||
}
|
||||
@@ -313,10 +365,10 @@ class MainActivity : AppCompatActivity() {
|
||||
)
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
if (uiState.settings.appMode == AppMode.LOCK_DOWN) {
|
||||
if (uiState.tunnelMode == TunnelMode.LOCK_DOWN) {
|
||||
AppAlertBanner(
|
||||
stringResource(R.string.locked_down)
|
||||
.uppercase(Locale.getDefault()),
|
||||
.uppercase(Locale.current.platformLocale),
|
||||
OffWhite,
|
||||
AlertRed,
|
||||
modifier = Modifier.fillMaxWidth().zIndex(2f),
|
||||
@@ -327,11 +379,7 @@ class MainActivity : AppCompatActivity() {
|
||||
snackbarState.SnackbarHost(
|
||||
modifier =
|
||||
Modifier.align(Alignment.BottomCenter)
|
||||
.padding(
|
||||
bottom =
|
||||
if (LocalIsAndroidTV.current) 120.dp
|
||||
else 80.dp
|
||||
)
|
||||
.padding(bottom = 80.dp)
|
||||
) { info ->
|
||||
CustomSnackBar(
|
||||
message = info.message,
|
||||
@@ -368,7 +416,6 @@ class MainActivity : AppCompatActivity() {
|
||||
bottom = padding.calculateBottomPadding(),
|
||||
)
|
||||
.consumeWindowInsets(padding)
|
||||
.imePadding()
|
||||
) {
|
||||
NavDisplay(
|
||||
backStack = backStack,
|
||||
@@ -413,40 +460,32 @@ class MainActivity : AppCompatActivity() {
|
||||
entry<Route.Tunnels> { TunnelsScreen() }
|
||||
entry<Route.Sort> { SortScreen() }
|
||||
entry<Route.TunnelSettings> { key ->
|
||||
val viewModel =
|
||||
hiltViewModel<
|
||||
TunnelViewModel,
|
||||
TunnelViewModel.Factory,
|
||||
>(
|
||||
creationCallback = { factory ->
|
||||
factory.create(key.id)
|
||||
}
|
||||
val viewModel: TunnelViewModel =
|
||||
koinViewModel(
|
||||
parameters = { parametersOf(key.id) }
|
||||
)
|
||||
TunnelSettingsScreen(viewModel)
|
||||
}
|
||||
entry<Route.Config> { key ->
|
||||
val viewModel: TunnelViewModel =
|
||||
koinViewModel(
|
||||
parameters = { parametersOf(key.id) }
|
||||
)
|
||||
ConfigScreen(viewModel, key.live)
|
||||
}
|
||||
entry<Route.SplitTunnel> { key ->
|
||||
val viewModel =
|
||||
hiltViewModel<
|
||||
SplitTunnelViewModel,
|
||||
SplitTunnelViewModel.Factory,
|
||||
>(
|
||||
creationCallback = { factory ->
|
||||
factory.create(key.id)
|
||||
}
|
||||
val viewModel: SplitTunnelViewModel =
|
||||
koinViewModel(
|
||||
parameters = { parametersOf(key.id) }
|
||||
)
|
||||
SplitTunnelScreen(viewModel)
|
||||
}
|
||||
entry<Route.Config> { key ->
|
||||
val viewModel =
|
||||
hiltViewModel<
|
||||
ConfigViewModel,
|
||||
ConfigViewModel.Factory,
|
||||
>(
|
||||
creationCallback = { factory ->
|
||||
factory.create(key.id)
|
||||
}
|
||||
entry<Route.ConfigEdit> { key ->
|
||||
val viewModel: ConfigEditViewModel =
|
||||
koinViewModel(
|
||||
parameters = { parametersOf(key.id) }
|
||||
)
|
||||
ConfigScreen(viewModel)
|
||||
ConfigEditScreen(viewModel)
|
||||
}
|
||||
entry<Route.LocationDisclosure> {
|
||||
LocationDisclosureScreen()
|
||||
@@ -455,44 +494,35 @@ class MainActivity : AppCompatActivity() {
|
||||
entry<Route.WifiPreferences> {
|
||||
WifiSettingsScreen()
|
||||
}
|
||||
entry<Route.AdvancedAutoTunnel> {
|
||||
AutoTunnelAdvancedScreen()
|
||||
}
|
||||
entry<Route.WifiDetectionMethod> {
|
||||
WifiDetectionMethodScreen()
|
||||
}
|
||||
entry<Route.Settings> { SettingsScreen() }
|
||||
entry<Route.TunnelMonitoring> {
|
||||
TunnelMonitoringScreen()
|
||||
}
|
||||
entry<Route.AndroidIntegrations> {
|
||||
AndroidIntegrationsScreen()
|
||||
}
|
||||
entry<Route.Dns> { DnsSettingsScreen() }
|
||||
entry<Route.ConfigGlobal> { key ->
|
||||
val viewModel =
|
||||
hiltViewModel<
|
||||
ConfigViewModel,
|
||||
ConfigViewModel.Factory,
|
||||
>(
|
||||
creationCallback = { factory ->
|
||||
factory.create(key.id)
|
||||
}
|
||||
val viewModel: ConfigEditViewModel =
|
||||
koinViewModel(
|
||||
parameters = { parametersOf(key.id) }
|
||||
)
|
||||
ConfigScreen(viewModel)
|
||||
ConfigEditScreen(viewModel)
|
||||
}
|
||||
entry<Route.SplitTunnelGlobal> { key ->
|
||||
val viewModel =
|
||||
hiltViewModel<
|
||||
SplitTunnelViewModel,
|
||||
SplitTunnelViewModel.Factory,
|
||||
>(
|
||||
creationCallback = { factory ->
|
||||
factory.create(key.id)
|
||||
}
|
||||
val viewModel: SplitTunnelViewModel =
|
||||
koinViewModel(
|
||||
parameters = { parametersOf(key.id) }
|
||||
)
|
||||
SplitTunnelScreen(viewModel)
|
||||
}
|
||||
entry<Route.IPv6> { key ->
|
||||
val viewModel: TunnelViewModel =
|
||||
koinViewModel(
|
||||
parameters = { parametersOf(key.id) }
|
||||
)
|
||||
IPv6Screen(viewModel)
|
||||
}
|
||||
entry<Route.LockdownSettings> {
|
||||
LockdownSettingsScreen()
|
||||
}
|
||||
@@ -508,7 +538,9 @@ class MainActivity : AppCompatActivity() {
|
||||
entry<Route.PreferredTunnel> { key ->
|
||||
PreferredTunnelScreen(key.tunnelNetwork)
|
||||
}
|
||||
entry<Route.PingTarget> { PingTargetScreen() }
|
||||
entry<Route.TunnelGlobals> { TunnelGlobalsScreen() }
|
||||
entry<Route.Security> { SecurityScreen() }
|
||||
entry<Route.Monitoring> { MonitoringScreen() }
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -520,70 +552,54 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
networkMonitor.checkPermissionsAndUpdateState()
|
||||
WireGuardAutoTunnel.setUiActive(true)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
WireGuardAutoTunnel.setUiActive(false)
|
||||
}
|
||||
|
||||
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)
|
||||
.enableLogDebug(true)
|
||||
.maxFileCount(5)
|
||||
.apply {
|
||||
onCompleteListener { success, _, _ ->
|
||||
lifecycleScope.launch {
|
||||
if (success) {
|
||||
showToast(
|
||||
getString(
|
||||
R.string.backup_success,
|
||||
getString(R.string.restarting_app),
|
||||
)
|
||||
fun performBackup() = lifecycleScope.launch {
|
||||
roomBackup
|
||||
.database(appDatabase)
|
||||
.backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG)
|
||||
.enableLogDebug(true)
|
||||
.maxFileCount(5)
|
||||
.apply {
|
||||
onCompleteListener { success, _, _ ->
|
||||
lifecycleScope.launch {
|
||||
if (success) {
|
||||
showToast(
|
||||
getString(
|
||||
R.string.backup_success,
|
||||
getString(R.string.restarting_app),
|
||||
)
|
||||
restartApp()
|
||||
} else {
|
||||
showToast(R.string.backup_failed)
|
||||
}
|
||||
)
|
||||
restartApp()
|
||||
} else {
|
||||
showToast(R.string.backup_failed)
|
||||
}
|
||||
}
|
||||
}
|
||||
.backup()
|
||||
}
|
||||
}
|
||||
.backup()
|
||||
}
|
||||
|
||||
fun performRestore() =
|
||||
lifecycleScope.launch {
|
||||
roomBackup
|
||||
.database(appDatabase)
|
||||
.enableLogDebug(true)
|
||||
.backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG)
|
||||
.apply {
|
||||
onCompleteListener { success, _, _ ->
|
||||
lifecycleScope.launch {
|
||||
if (success) {
|
||||
showToast(
|
||||
getString(
|
||||
R.string.restore_success,
|
||||
getString(R.string.restarting_app),
|
||||
)
|
||||
fun performRestore() = lifecycleScope.launch {
|
||||
roomBackup
|
||||
.database(appDatabase)
|
||||
.enableLogDebug(true)
|
||||
.backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG)
|
||||
.apply {
|
||||
onCompleteListener { success, _, _ ->
|
||||
lifecycleScope.launch {
|
||||
if (success) {
|
||||
showToast(
|
||||
getString(
|
||||
R.string.restore_success,
|
||||
getString(R.string.restarting_app),
|
||||
)
|
||||
restartApp()
|
||||
} else {
|
||||
showToast(R.string.restore_failed)
|
||||
}
|
||||
)
|
||||
restartApp()
|
||||
} else {
|
||||
showToast(R.string.restore_failed)
|
||||
}
|
||||
}
|
||||
}
|
||||
.restore()
|
||||
}
|
||||
}
|
||||
.restore()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,95 +2,112 @@ package com.zaneschepke.wireguardautotunnel
|
||||
|
||||
import android.app.Application
|
||||
import android.os.StrictMode
|
||||
import android.os.StrictMode.ThreadPolicy
|
||||
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.worker.ServiceWorker
|
||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository
|
||||
import com.zaneschepke.tunnel.backend.Backend
|
||||
import com.zaneschepke.tunnel.di.tunnelModule
|
||||
import com.zaneschepke.tunnel.service.VpnService
|
||||
import com.zaneschepke.wireguardautotunnel.core.event.TunnelEventDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.AppBoostrapCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
||||
import com.zaneschepke.wireguardautotunnel.di.Dispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.di.Scope
|
||||
import com.zaneschepke.wireguardautotunnel.di.appModule
|
||||
import com.zaneschepke.wireguardautotunnel.di.coordinatorModule
|
||||
import com.zaneschepke.wireguardautotunnel.di.databaseModule
|
||||
import com.zaneschepke.wireguardautotunnel.di.dispatchersModule
|
||||
import com.zaneschepke.wireguardautotunnel.di.networkModule
|
||||
import com.zaneschepke.wireguardautotunnel.di.tunnelBackendProviderModule
|
||||
import com.zaneschepke.wireguardautotunnel.di.workerModule
|
||||
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.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.android.ext.android.get
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.android.ext.koin.androidLogger
|
||||
import org.koin.androidx.workmanager.koin.workManagerFactory
|
||||
import org.koin.core.annotation.KoinViewModelScopeApi
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.context.GlobalContext.startKoin
|
||||
import org.koin.core.lazyModules
|
||||
import org.koin.core.option.viewModelScopeFactory
|
||||
import org.koin.core.qualifier.named
|
||||
import timber.log.Timber
|
||||
|
||||
@HiltAndroidApp
|
||||
class WireGuardAutoTunnel : Application(), Configuration.Provider {
|
||||
class WireGuardAutoTunnel : Application(), KoinComponent {
|
||||
|
||||
@Inject lateinit var workerFactory: HiltWorkerFactory
|
||||
private val applicationScope: CoroutineScope by inject(named(Scope.APPLICATION))
|
||||
private val ioDispatcher: CoroutineDispatcher by inject(named(Dispatcher.IO))
|
||||
|
||||
override val workManagerConfiguration: Configuration
|
||||
get() = Configuration.Builder().setWorkerFactory(workerFactory).build()
|
||||
private val boostrapCoordinator: AppBoostrapCoordinator by inject()
|
||||
|
||||
@Inject @ApplicationScope lateinit var applicationScope: CoroutineScope
|
||||
private val notificationService: NotificationService by inject()
|
||||
|
||||
@Inject lateinit var logReader: LogReader
|
||||
private val tunnelCoordinator: TunnelCoordinator by inject()
|
||||
|
||||
@Inject @IoDispatcher lateinit var ioDispatcher: CoroutineDispatcher
|
||||
|
||||
@Inject lateinit var monitoringRepository: MonitoringSettingsRepository
|
||||
|
||||
@Inject lateinit var notificationMonitor: NotificationMonitor
|
||||
private val backend: Backend by inject()
|
||||
|
||||
@OptIn(KoinViewModelScopeApi::class)
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
startKoin {
|
||||
androidContext(this@WireGuardAutoTunnel)
|
||||
if (BuildConfig.DEBUG) androidLogger()
|
||||
workManagerFactory()
|
||||
modules(
|
||||
dispatchersModule,
|
||||
appModule,
|
||||
databaseModule,
|
||||
tunnelBackendProviderModule,
|
||||
tunnelModule,
|
||||
workerModule,
|
||||
coordinatorModule,
|
||||
)
|
||||
options(viewModelScopeFactory())
|
||||
lazyModules(networkModule)
|
||||
}
|
||||
instance = this
|
||||
notificationService.createAllChannels()
|
||||
if (BuildConfig.DEBUG) {
|
||||
Timber.plant(Timber.DebugTree())
|
||||
StrictMode.setThreadPolicy(
|
||||
ThreadPolicy.Builder()
|
||||
.detectDiskReads()
|
||||
.detectDiskWrites()
|
||||
.detectNetwork()
|
||||
StrictMode.ThreadPolicy.Builder()
|
||||
.detectAll()
|
||||
.penaltyLog()
|
||||
.penaltyFlashScreen()
|
||||
.build()
|
||||
)
|
||||
StrictMode.setVmPolicy(StrictMode.VmPolicy.Builder().detectAll().penaltyLog().build())
|
||||
} else {
|
||||
Timber.plant(ReleaseTree())
|
||||
}
|
||||
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
launch {
|
||||
val monitoringSettings = monitoringRepository.getMonitoringSettings()
|
||||
if (monitoringSettings.isLocalLogsEnabled) logReader.start()
|
||||
backend.setAlwaysOnCallback(
|
||||
object : VpnService.AlwaysOnCallback {
|
||||
override fun alwaysOnTriggered() {
|
||||
applicationScope.launch { tunnelCoordinator.startDefault() }
|
||||
}
|
||||
}
|
||||
launch { notificationMonitor.handleApplicationNotifications() }
|
||||
}
|
||||
)
|
||||
|
||||
ServiceWorker.start(this)
|
||||
val dispatcher = get<TunnelEventDispatcher>()
|
||||
val coordinator = get<TunnelCoordinator>()
|
||||
val provider = get<TunnelProvider>()
|
||||
|
||||
// for notifications
|
||||
dispatcher.bind(
|
||||
applicationScope,
|
||||
provider.events,
|
||||
provider.backendStatus,
|
||||
coordinator.errors,
|
||||
)
|
||||
|
||||
applicationScope.launch(ioDispatcher) { boostrapCoordinator.bootstrap() }
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val _uiActive = MutableStateFlow(false)
|
||||
|
||||
val uiActive: StateFlow<Boolean>
|
||||
get() = _uiActive
|
||||
|
||||
fun setUiActive(active: Boolean) {
|
||||
_uiActive.update { active }
|
||||
}
|
||||
|
||||
@Volatile private var lastActiveTunnels: List<Int> = emptyList()
|
||||
|
||||
@Synchronized
|
||||
fun getLastActiveTunnels(): List<Int> {
|
||||
return lastActiveTunnels
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun setLastActiveTunnels(newTunnels: List<Int>) {
|
||||
lastActiveTunnels = newTunnels
|
||||
}
|
||||
|
||||
lateinit var instance: WireGuardAutoTunnel
|
||||
private set
|
||||
}
|
||||
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.broadcast
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@AndroidEntryPoint
|
||||
class KernelReceiver : BroadcastReceiver() {
|
||||
|
||||
@Inject @ApplicationScope lateinit var applicationScope: CoroutineScope
|
||||
|
||||
@Inject lateinit var tunnelRepository: TunnelRepository
|
||||
|
||||
@Inject lateinit var tunnelManager: TunnelManager
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val action = intent.action ?: return
|
||||
applicationScope.launch {
|
||||
if (action == REFRESH_TUNNELS_ACTION) {
|
||||
tunnelManager.runningTunnelNames().forEach { name ->
|
||||
val tunnel = tunnelRepository.findByTunnelName(name)
|
||||
tunnel?.let { tunnelRepository.save(it.copy(isActive = true)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val REFRESH_TUNNELS_ACTION = "com.wireguard.android.action.REFRESH_TUNNEL_STATES"
|
||||
}
|
||||
}
|
||||
+26
-20
@@ -3,38 +3,44 @@ package com.zaneschepke.wireguardautotunnel.core.broadcast
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.AutoTunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.di.Scope
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
import org.koin.core.component.inject
|
||||
import org.koin.core.qualifier.named
|
||||
|
||||
@AndroidEntryPoint
|
||||
class NotificationActionReceiver : BroadcastReceiver() {
|
||||
class NotificationActionReceiver : BroadcastReceiver(), KoinComponent {
|
||||
|
||||
@Inject lateinit var tunnelManager: TunnelManager
|
||||
private val tunnelCoordinator: TunnelCoordinator by inject()
|
||||
|
||||
@Inject lateinit var tunnelRepository: TunnelRepository
|
||||
private val autoTunnelCoordinator: AutoTunnelCoordinator by inject()
|
||||
|
||||
@Inject lateinit var autoTunnelRepository: AutoTunnelSettingsRepository
|
||||
|
||||
@Inject @ApplicationScope lateinit var applicationScope: CoroutineScope
|
||||
private val applicationScope: CoroutineScope = get(named(Scope.APPLICATION))
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
|
||||
applicationScope.launch {
|
||||
when (intent.action) {
|
||||
NotificationAction.AUTO_TUNNEL_OFF.name ->
|
||||
autoTunnelRepository.updateAutoTunnelEnabled(false)
|
||||
NotificationAction.AUTO_TUNNEL_OFF.name -> {
|
||||
autoTunnelCoordinator.disable()
|
||||
}
|
||||
|
||||
NotificationAction.TUNNEL_OFF.name -> {
|
||||
val tunnelId = intent.getIntExtra(NotificationManager.EXTRA_ID, 0)
|
||||
if (tunnelId == STOP_ALL_TUNNELS_ID)
|
||||
return@launch tunnelManager.stopActiveTunnels()
|
||||
tunnelManager.stopTunnel(tunnelId)
|
||||
|
||||
val tunnelId =
|
||||
intent.getIntExtra(NotificationService.EXTRA_ID, STOP_ALL_TUNNELS_ID)
|
||||
|
||||
if (tunnelId == STOP_ALL_TUNNELS_ID) {
|
||||
tunnelCoordinator.stopActiveTunnels()
|
||||
return@launch
|
||||
}
|
||||
tunnelCoordinator.stopTunnel(tunnelId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+55
-38
@@ -3,28 +3,27 @@ package com.zaneschepke.wireguardautotunnel.core.broadcast
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.AutoTunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.di.Scope
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import org.koin.core.qualifier.named
|
||||
|
||||
@AndroidEntryPoint
|
||||
class RemoteControlReceiver : BroadcastReceiver() {
|
||||
class RemoteControlReceiver : BroadcastReceiver(), KoinComponent {
|
||||
|
||||
@Inject @ApplicationScope lateinit var applicationScope: CoroutineScope
|
||||
private val applicationScope: CoroutineScope by inject(named(Scope.APPLICATION))
|
||||
|
||||
@Inject lateinit var settingsRepository: GeneralSettingRepository
|
||||
@Inject lateinit var tunnelsRepository: TunnelRepository
|
||||
@Inject lateinit var autoTunnelSettingsRepository: AutoTunnelSettingsRepository
|
||||
|
||||
@Inject lateinit var tunnelManager: TunnelManager
|
||||
private val settingsRepository: GeneralSettingRepository by inject()
|
||||
private val tunnelsRepository: TunnelRepository by inject()
|
||||
private val tunnelCoordinator: TunnelCoordinator by inject()
|
||||
private val autoTunnelCoordinator: AutoTunnelCoordinator by inject()
|
||||
|
||||
enum class Action(private val suffix: String) {
|
||||
START_TUNNEL("START_TUNNEL"),
|
||||
@@ -49,45 +48,63 @@ class RemoteControlReceiver : BroadcastReceiver() {
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
Timber.i("onReceive")
|
||||
|
||||
val action = intent.action ?: return
|
||||
val appAction = Action.fromAction(action) ?: return Timber.w("Unknown action $action")
|
||||
val appAction = Action.fromAction(action) ?: return
|
||||
|
||||
applicationScope.launch {
|
||||
val settings = settingsRepository.getGeneralSettings()
|
||||
if (!settings.isRemoteControlEnabled) return@launch Timber.w("Remote control disabled")
|
||||
val key = settings.remoteKey ?: return@launch Timber.w("Remote control key missing")
|
||||
if (key != intent.getStringExtra(EXTRA_KEY)?.trim())
|
||||
return@launch Timber.w("Invalid remote control key")
|
||||
|
||||
if (!settings.isRemoteControlEnabled) return@launch
|
||||
|
||||
if (!validateKey(settings, intent)) return@launch
|
||||
|
||||
when (appAction) {
|
||||
Action.START_TUNNEL -> {
|
||||
val tunnelName =
|
||||
intent.getStringExtra(EXTRA_TUN_NAME) ?: return@launch startDefaultTunnel()
|
||||
val tunnel =
|
||||
tunnelsRepository.findByTunnelName(tunnelName)
|
||||
?: return@launch startDefaultTunnel()
|
||||
tunnelManager.startTunnel(tunnel)
|
||||
resolveTunnel(intent)
|
||||
?: tunnelsRepository.getDefaultTunnel()
|
||||
?: return@launch
|
||||
|
||||
tunnelCoordinator.startTunnel(tunnel)
|
||||
}
|
||||
|
||||
Action.STOP_TUNNEL -> {
|
||||
val tunnelName =
|
||||
intent.getStringExtra(EXTRA_TUN_NAME)
|
||||
?: return@launch tunnelManager.stopActiveTunnels()
|
||||
val tunnel =
|
||||
tunnelsRepository.findByTunnelName(tunnelName)
|
||||
?: return@launch tunnelManager.stopActiveTunnels()
|
||||
tunnelManager.stopTunnel(tunnel.id)
|
||||
val tunnelName = intent.getStringExtra(EXTRA_TUN_NAME)
|
||||
|
||||
if (tunnelName == null) {
|
||||
tunnelCoordinator.stopActiveTunnels()
|
||||
return@launch
|
||||
}
|
||||
|
||||
val tunnel = tunnelsRepository.findByTunnelName(tunnelName) ?: return@launch
|
||||
|
||||
tunnelCoordinator.stopTunnel(tunnel.id)
|
||||
}
|
||||
|
||||
Action.START_AUTO_TUNNEL -> {
|
||||
autoTunnelCoordinator.enable()
|
||||
}
|
||||
|
||||
Action.STOP_AUTO_TUNNEL -> {
|
||||
autoTunnelCoordinator.disable()
|
||||
}
|
||||
Action.START_AUTO_TUNNEL ->
|
||||
autoTunnelSettingsRepository.updateAutoTunnelEnabled(true)
|
||||
Action.STOP_AUTO_TUNNEL ->
|
||||
autoTunnelSettingsRepository.updateAutoTunnelEnabled(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun startDefaultTunnel() {
|
||||
tunnelsRepository.getDefaultTunnel()?.let { tunnel -> tunnelManager.startTunnel(tunnel) }
|
||||
private fun validateKey(settings: GeneralSettings, intent: Intent): Boolean {
|
||||
|
||||
val expected = settings.remoteKey?.trim() ?: return false
|
||||
|
||||
val actual = intent.getStringExtra(EXTRA_KEY)?.trim()
|
||||
|
||||
return expected == actual
|
||||
}
|
||||
|
||||
private suspend fun resolveTunnel(intent: Intent) =
|
||||
intent.getStringExtra(EXTRA_TUN_NAME)?.let { tunnelsRepository.findByTunnelName(it) }
|
||||
|
||||
companion object {
|
||||
const val EXTRA_TUN_NAME = "tunnelName"
|
||||
const val EXTRA_KEY = "key"
|
||||
|
||||
+14
-12
@@ -4,25 +4,26 @@ import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
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.core.orchestration.StartupCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.di.Scope
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
import org.koin.core.component.inject
|
||||
import org.koin.core.qualifier.named
|
||||
import timber.log.Timber
|
||||
|
||||
@AndroidEntryPoint
|
||||
class RestartReceiver : BroadcastReceiver() {
|
||||
class RestartReceiver : BroadcastReceiver(), KoinComponent {
|
||||
|
||||
@Inject @ApplicationScope lateinit var applicationScope: CoroutineScope
|
||||
private val applicationScope: CoroutineScope = get(named(Scope.APPLICATION))
|
||||
|
||||
@Inject lateinit var tunnelManager: TunnelManager
|
||||
private val startupCoordinator: StartupCoordinator by inject()
|
||||
|
||||
@Inject lateinit var appStateRepository: AppStateRepository
|
||||
private val appStateRepository: AppStateRepository by inject()
|
||||
|
||||
@Inject lateinit var logReader: LogReader
|
||||
private val logReader: LogReader by inject()
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
Timber.d("RestartReceiver triggered with action: ${intent.action}")
|
||||
@@ -31,10 +32,11 @@ class RestartReceiver : BroadcastReceiver() {
|
||||
Intent.ACTION_BOOT_COMPLETED,
|
||||
"android.intent.action.QUICKBOOT_POWERON",
|
||||
"com.htc.intent.action.QUICKBOOT_POWERON" -> {
|
||||
tunnelManager.handleReboot()
|
||||
startupCoordinator.applyStartupPolicy()
|
||||
}
|
||||
Intent.ACTION_MY_PACKAGE_REPLACED -> {
|
||||
tunnelManager.handleRestore()
|
||||
Timber.i("Restoring state on package upgrade")
|
||||
startupCoordinator.applyStartupPolicy()
|
||||
logReader.deleteAndClearLogs()
|
||||
appStateRepository.setShouldShowDonationSnackbar(true)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.event
|
||||
|
||||
import com.zaneschepke.tunnel.util.BackendException
|
||||
|
||||
sealed interface TunnelErrorEvent {
|
||||
data class VpnPermissionDenied(val tunnelId: Int) : TunnelErrorEvent
|
||||
|
||||
data class StateConflict(val tunnelId: Int, val message: String) : TunnelErrorEvent
|
||||
|
||||
data class InternalFailure(val tunnelId: Int, val message: String) : TunnelErrorEvent
|
||||
|
||||
data class Socks5PortUnavailable(val tunnelId: Int, val port: Int) : TunnelErrorEvent
|
||||
|
||||
data class HttpPortUnavailable(val tunnelId: Int, val port: Int) : TunnelErrorEvent
|
||||
|
||||
companion object {
|
||||
fun from(throwable: Throwable, id: Int): TunnelErrorEvent {
|
||||
return when (throwable) {
|
||||
is BackendException.StateConflict -> {
|
||||
StateConflict(id, throwable.message)
|
||||
}
|
||||
is BackendException.Unauthorized -> {
|
||||
VpnPermissionDenied(id)
|
||||
}
|
||||
is BackendException.InternalError -> {
|
||||
InternalFailure(id, throwable.message)
|
||||
}
|
||||
is BackendException.Socks5PortUnavailable -> {
|
||||
Socks5PortUnavailable(id, throwable.port)
|
||||
}
|
||||
is BackendException.HttpPortUnavailable -> {
|
||||
HttpPortUnavailable(id, throwable.port)
|
||||
}
|
||||
else -> InternalFailure(id, throwable.message ?: "Unknown")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.event
|
||||
|
||||
import com.zaneschepke.tunnel.event.TunnelEvent
|
||||
import com.zaneschepke.tunnel.state.BackendStatus
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.TunnelNotificationService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
class TunnelEventDispatcher(private val notificationManager: TunnelNotificationService) {
|
||||
|
||||
fun bind(
|
||||
scope: CoroutineScope,
|
||||
providerEvents: Flow<TunnelEvent>,
|
||||
providerStatus: StateFlow<BackendStatus>,
|
||||
coordinatorErrors: Flow<TunnelErrorEvent>,
|
||||
) {
|
||||
|
||||
// informational events
|
||||
providerEvents
|
||||
.distinctUntilChanged()
|
||||
.onEach { event ->
|
||||
when (event) {
|
||||
is TunnelEvent.FallbackToIpv4 -> {
|
||||
notificationManager.showIpv4Fallback(event.tunnelId)
|
||||
}
|
||||
|
||||
is TunnelEvent.RecoveredToIpv6 -> {
|
||||
notificationManager.showIpv6Recovery(event.tunnelId)
|
||||
}
|
||||
|
||||
is TunnelEvent.DynamicDnsUpdate -> {
|
||||
notificationManager.showDynamicDnsUpdate(event.tunnelId)
|
||||
}
|
||||
|
||||
is TunnelEvent.NoRootShellAccess -> {
|
||||
notificationManager.showRootShellAccess()
|
||||
}
|
||||
}
|
||||
}
|
||||
.launchIn(scope)
|
||||
|
||||
// errors from the coordinator
|
||||
coordinatorErrors
|
||||
.distinctUntilChanged()
|
||||
.onEach { error ->
|
||||
when (error) {
|
||||
is TunnelErrorEvent.VpnPermissionDenied -> {
|
||||
notificationManager.showVpnRequired()
|
||||
}
|
||||
|
||||
is TunnelErrorEvent.StateConflict -> {
|
||||
notificationManager.showStateConflict(error.tunnelId)
|
||||
}
|
||||
|
||||
is TunnelErrorEvent.InternalFailure -> {
|
||||
notificationManager.showError(error.message)
|
||||
}
|
||||
|
||||
is TunnelErrorEvent.Socks5PortUnavailable -> {
|
||||
notificationManager.showSocks5PortUnavailable(error.port)
|
||||
}
|
||||
|
||||
is TunnelErrorEvent.HttpPortUnavailable -> {
|
||||
notificationManager.showHttpPortUnavailable(error.port)
|
||||
}
|
||||
}
|
||||
}
|
||||
.launchIn(scope)
|
||||
|
||||
// update persistent notification for services with the tunnel states
|
||||
providerStatus
|
||||
.map { it.activeTunnels }
|
||||
.distinctUntilChangedBy { map ->
|
||||
val stateSignature =
|
||||
map.entries
|
||||
.sortedBy { it.key }
|
||||
.map { (_, tunnel) -> tunnel.transportState to tunnel.bootstrapState }
|
||||
map.size to stateSignature
|
||||
}
|
||||
.onEach { status -> notificationManager.updatePersistentNotifications(status) }
|
||||
.launchIn(scope)
|
||||
}
|
||||
}
|
||||
+54
-46
@@ -3,50 +3,46 @@ package com.zaneschepke.wireguardautotunnel.core.notification
|
||||
import android.Manifest
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager.IMPORTANCE_LOW
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationCompat.Action
|
||||
import androidx.core.app.NotificationCompat.Builder
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.zaneschepke.wireguardautotunnel.MainActivity
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.broadcast.NotificationActionReceiver
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager.Companion.EXTRA_ID
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.EXTRA_ID
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class WireGuardNotification @Inject constructor(@ApplicationContext override val context: Context) :
|
||||
NotificationManager {
|
||||
|
||||
enum class NotificationChannels {
|
||||
VPN,
|
||||
AUTO_TUNNEL,
|
||||
}
|
||||
class AndroidNotificationService(override val context: Context) : NotificationService {
|
||||
|
||||
private val notificationManager = NotificationManagerCompat.from(context)
|
||||
|
||||
override fun createNotification(
|
||||
channel: NotificationChannels,
|
||||
title: String,
|
||||
actions: Collection<NotificationCompat.Action>,
|
||||
subText: String?,
|
||||
actions: Collection<Action>,
|
||||
description: String,
|
||||
showTimestamp: Boolean,
|
||||
importance: Int,
|
||||
onGoing: Boolean,
|
||||
onlyAlertOnce: Boolean,
|
||||
groupKey: String?,
|
||||
isGroupSummary: Boolean,
|
||||
): Notification {
|
||||
notificationManager.createNotificationChannel(channel.asChannel(importance))
|
||||
notificationManager.createNotificationChannel(channel.asChannel())
|
||||
return channel
|
||||
.asBuilder()
|
||||
.apply {
|
||||
actions.forEach { addAction(it) }
|
||||
setContentTitle(title)
|
||||
setSubText(subText)
|
||||
setContentIntent(
|
||||
PendingIntent.getActivity(
|
||||
context,
|
||||
@@ -59,7 +55,6 @@ class WireGuardNotification @Inject constructor(@ApplicationContext override val
|
||||
setContentText(description)
|
||||
setOnlyAlertOnce(onlyAlertOnce)
|
||||
setOngoing(onGoing)
|
||||
setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
setShowWhen(showTimestamp)
|
||||
setSmallIcon(R.drawable.ic_notification)
|
||||
if (groupKey != null) {
|
||||
@@ -75,10 +70,10 @@ class WireGuardNotification @Inject constructor(@ApplicationContext override val
|
||||
override fun createNotification(
|
||||
channel: NotificationChannels,
|
||||
title: StringValue,
|
||||
actions: Collection<NotificationCompat.Action>,
|
||||
subText: String?,
|
||||
actions: Collection<Action>,
|
||||
description: StringValue,
|
||||
showTimestamp: Boolean,
|
||||
importance: Int,
|
||||
onGoing: Boolean,
|
||||
onlyAlertOnce: Boolean,
|
||||
groupKey: String?,
|
||||
@@ -87,19 +82,21 @@ class WireGuardNotification @Inject constructor(@ApplicationContext override val
|
||||
return createNotification(
|
||||
channel,
|
||||
title.asString(context),
|
||||
subText,
|
||||
actions,
|
||||
description.asString(context),
|
||||
showTimestamp,
|
||||
importance,
|
||||
onGoing,
|
||||
onlyAlertOnce,
|
||||
groupKey,
|
||||
isGroupSummary,
|
||||
)
|
||||
}
|
||||
|
||||
override fun createNotificationAction(
|
||||
notificationAction: NotificationAction,
|
||||
extraId: Int?,
|
||||
): NotificationCompat.Action {
|
||||
): Action {
|
||||
val pendingIntent =
|
||||
PendingIntent.getBroadcast(
|
||||
context,
|
||||
@@ -112,7 +109,7 @@ class WireGuardNotification @Inject constructor(@ApplicationContext override val
|
||||
)
|
||||
return NotificationCompat.Action.Builder(
|
||||
R.drawable.ic_notification,
|
||||
notificationAction.title(context).uppercase(),
|
||||
notificationAction.title(context),
|
||||
pendingIntent,
|
||||
)
|
||||
.build()
|
||||
@@ -138,38 +135,49 @@ class WireGuardNotification @Inject constructor(@ApplicationContext override val
|
||||
|
||||
private fun NotificationChannels.asBuilder(): NotificationCompat.Builder {
|
||||
return when (this) {
|
||||
NotificationChannels.AUTO_TUNNEL -> {
|
||||
NotificationCompat.Builder(
|
||||
context,
|
||||
context.getString(R.string.auto_tunnel_channel_id),
|
||||
)
|
||||
}
|
||||
NotificationChannels.VPN -> {
|
||||
NotificationCompat.Builder(context, context.getString(R.string.vpn_channel_id))
|
||||
}
|
||||
NotificationChannels.AUTO_TUNNEL ->
|
||||
Builder(context, context.getString(R.string.auto_tunnel_channel_id))
|
||||
NotificationChannels.VPN -> Builder(context, context.getString(R.string.vpn_channel_id))
|
||||
|
||||
NotificationChannels.PROXY ->
|
||||
Builder(context, context.getString(R.string.proxy_channel_id))
|
||||
}
|
||||
}
|
||||
|
||||
private fun NotificationChannels.asChannel(importance: Int): NotificationChannel {
|
||||
return when (this) {
|
||||
NotificationChannels.VPN -> {
|
||||
NotificationChannel(
|
||||
context.getString(R.string.vpn_channel_id),
|
||||
context.getString(R.string.vpn_channel_name),
|
||||
importance,
|
||||
)
|
||||
.apply { description = context.getString(R.string.vpn_channel_description) }
|
||||
}
|
||||
NotificationChannels.AUTO_TUNNEL -> {
|
||||
NotificationChannel(
|
||||
context.getString(R.string.auto_tunnel_channel_id),
|
||||
context.getString(R.string.auto_tunnel_channel_name),
|
||||
importance,
|
||||
)
|
||||
.apply {
|
||||
description = context.getString(R.string.auto_tunnel_channel_description)
|
||||
enum class NotificationChannels(val channelId: Int, val importance: Int) {
|
||||
VPN(R.string.vpn_channel_id, IMPORTANCE_LOW),
|
||||
AUTO_TUNNEL(R.string.auto_tunnel_channel_id, IMPORTANCE_LOW),
|
||||
PROXY(R.string.proxy_channel_id, IMPORTANCE_LOW),
|
||||
}
|
||||
|
||||
fun NotificationChannels.asChannel(): NotificationChannel {
|
||||
return NotificationChannel(
|
||||
context.getString(channelId),
|
||||
context.getString(
|
||||
when (this) {
|
||||
NotificationChannels.VPN -> R.string.vpn
|
||||
NotificationChannels.AUTO_TUNNEL -> R.string.auto_tunnel
|
||||
NotificationChannels.PROXY -> R.string.proxy
|
||||
}
|
||||
),
|
||||
importance,
|
||||
)
|
||||
.apply {
|
||||
description =
|
||||
context.getString(
|
||||
when (this@asChannel) {
|
||||
NotificationChannels.VPN -> R.string.vpn_channel_description
|
||||
NotificationChannels.AUTO_TUNNEL ->
|
||||
R.string.auto_tunnel_channel_description
|
||||
NotificationChannels.PROXY -> R.string.proxy_channel_description
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun createAllChannels() {
|
||||
NotificationChannels.entries.forEach { channel ->
|
||||
notificationManager.createNotificationChannel(channel.asChannel())
|
||||
}
|
||||
}
|
||||
}
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.notification
|
||||
|
||||
import com.zaneschepke.tunnel.model.BackendMode
|
||||
import com.zaneschepke.tunnel.state.ActiveTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.AndroidNotificationService.NotificationChannels
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.PROXY_GROUP_KEY
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.PROXY_NOTIFICATION_ID
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.TUNNEL_ERROR_NOTIFICATION_ID
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.TUNNEL_MESSAGES_NOTIFICATION_ID
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.VPN_GROUP_KEY
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.VPN_NOTIFICATION_ID
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState
|
||||
|
||||
class AndroidTunnelNotificationService(
|
||||
private val notificationService: NotificationService,
|
||||
private val tunnelRepository: TunnelRepository,
|
||||
) : TunnelNotificationService {
|
||||
|
||||
override suspend fun updatePersistentNotifications(activeTunnels: Map<Int, ActiveTunnel>) {
|
||||
|
||||
val vpnTunnels = activeTunnels.filterValues { it.mode is BackendMode.Vpn }
|
||||
|
||||
val proxyTunnels = activeTunnels.filterValues { it.mode is BackendMode.Proxy }
|
||||
|
||||
updateGroupNotification(
|
||||
tunnels = vpnTunnels,
|
||||
notificationId = VPN_NOTIFICATION_ID,
|
||||
channel = NotificationChannels.VPN,
|
||||
groupKey = VPN_GROUP_KEY,
|
||||
)
|
||||
|
||||
updateGroupNotification(
|
||||
tunnels = proxyTunnels,
|
||||
notificationId = PROXY_NOTIFICATION_ID,
|
||||
channel = NotificationChannels.PROXY,
|
||||
groupKey = PROXY_GROUP_KEY,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun updateGroupNotification(
|
||||
tunnels: Map<Int, ActiveTunnel>,
|
||||
notificationId: Int,
|
||||
channel: NotificationChannels,
|
||||
groupKey: String,
|
||||
) {
|
||||
|
||||
if (tunnels.isEmpty()) {
|
||||
notificationService.remove(notificationId)
|
||||
return
|
||||
}
|
||||
|
||||
val context = notificationService.context
|
||||
|
||||
val lines = tunnels.mapNotNull { (id, activeTunnel) ->
|
||||
val tunnel = tunnelRepository.getById(id) ?: return@mapNotNull null
|
||||
val display = DisplayTunnelState.from(activeTunnel)
|
||||
|
||||
context.getString(
|
||||
R.string.notification_tunnel_status_format,
|
||||
tunnel.name,
|
||||
display.asLocalizedString(context),
|
||||
)
|
||||
}
|
||||
|
||||
val description = lines.joinToString("\n")
|
||||
|
||||
val stopActions =
|
||||
tunnels.keys.map {
|
||||
notificationService.createNotificationAction(
|
||||
notificationAction = NotificationAction.TUNNEL_OFF,
|
||||
extraId = it,
|
||||
)
|
||||
}
|
||||
|
||||
val title =
|
||||
when (channel) {
|
||||
NotificationChannels.VPN -> context.getString(R.string.vpn)
|
||||
|
||||
NotificationChannels.PROXY -> context.getString(R.string.proxy)
|
||||
|
||||
NotificationChannels.AUTO_TUNNEL -> context.getString(R.string.auto_tunnel)
|
||||
}
|
||||
|
||||
val notification =
|
||||
notificationService.createNotification(
|
||||
channel = channel,
|
||||
title = title,
|
||||
description = description,
|
||||
actions = stopActions,
|
||||
onGoing = true,
|
||||
onlyAlertOnce = true,
|
||||
groupKey = groupKey,
|
||||
)
|
||||
|
||||
notificationService.show(notificationId, notification)
|
||||
}
|
||||
|
||||
override suspend fun showIpv4Fallback(tunnelId: Int) {
|
||||
|
||||
val context = notificationService.context
|
||||
val name = tunnelName(tunnelId)
|
||||
|
||||
showMessage(
|
||||
title = context.getString(R.string.ipv4_fallback),
|
||||
message = context.getString(R.string.notification_ipv4_fallback_message, name),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun showIpv6Recovery(tunnelId: Int) {
|
||||
|
||||
val context = notificationService.context
|
||||
val name = tunnelName(tunnelId)
|
||||
|
||||
showMessage(
|
||||
title = context.getString(R.string.ipv6_recovery),
|
||||
message = context.getString(R.string.notification_ipv6_recovery_message, name),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun showDynamicDnsUpdate(tunnelId: Int) {
|
||||
|
||||
val context = notificationService.context
|
||||
val name = tunnelName(tunnelId)
|
||||
|
||||
showMessage(
|
||||
title = context.getString(R.string.dynamic_dns_update),
|
||||
message = context.getString(R.string.notification_dynamic_dns_message, name),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun showVpnRequired() {
|
||||
|
||||
showError(notificationService.context.getString(R.string.vpn_permission_required))
|
||||
}
|
||||
|
||||
override suspend fun showStateConflict(tunnelId: Int) {
|
||||
|
||||
val context = notificationService.context
|
||||
val name = tunnelName(tunnelId)
|
||||
|
||||
showError(context.getString(R.string.notification_tunnel_already_running, name))
|
||||
}
|
||||
|
||||
override suspend fun showRootShellAccess() {
|
||||
// TODO could improve with fix action
|
||||
val context = notificationService.context
|
||||
showError(context.getString(R.string.error_root_denied))
|
||||
}
|
||||
|
||||
override suspend fun showSocks5PortUnavailable(port: Int) {
|
||||
val context = notificationService.context
|
||||
val message = context.getString(R.string.error_socks5_port_unavailable, port)
|
||||
|
||||
showError(message)
|
||||
}
|
||||
|
||||
override suspend fun showHttpPortUnavailable(port: Int) {
|
||||
val context = notificationService.context
|
||||
val message = context.getString(R.string.error_http_port_unavailable, port)
|
||||
|
||||
showError(message)
|
||||
}
|
||||
|
||||
override suspend fun showError(message: String) {
|
||||
|
||||
val notification =
|
||||
notificationService.createNotification(
|
||||
channel = NotificationChannels.VPN,
|
||||
title = notificationService.context.getString(R.string.error),
|
||||
description = message,
|
||||
onGoing = false,
|
||||
onlyAlertOnce = true,
|
||||
groupKey = VPN_GROUP_KEY,
|
||||
)
|
||||
|
||||
notificationService.show(TUNNEL_ERROR_NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
private fun showMessage(title: String, message: String) {
|
||||
|
||||
val notification =
|
||||
notificationService.createNotification(
|
||||
channel = NotificationChannels.VPN,
|
||||
title = title,
|
||||
description = message,
|
||||
onGoing = false,
|
||||
onlyAlertOnce = true,
|
||||
groupKey = VPN_GROUP_KEY,
|
||||
)
|
||||
|
||||
notificationService.show(TUNNEL_MESSAGES_NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
private suspend fun tunnelName(id: Int): String {
|
||||
|
||||
val context = notificationService.context
|
||||
|
||||
return tunnelRepository.getById(id)?.name ?: context.getString(R.string.unknown, id)
|
||||
}
|
||||
}
|
||||
-64
@@ -1,64 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.notification
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import jakarta.inject.Inject
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class NotificationMonitor
|
||||
@Inject
|
||||
constructor(
|
||||
private val tunnelManager: TunnelManager,
|
||||
private val notificationManager: NotificationManager,
|
||||
) {
|
||||
suspend fun handleApplicationNotifications() = coroutineScope {
|
||||
launch { handleTunnelErrors() }
|
||||
launch { handleTunnelMessages() }
|
||||
}
|
||||
|
||||
private suspend fun handleTunnelErrors() =
|
||||
tunnelManager.errorEvents.collectLatest { (tunName, error) ->
|
||||
if (!WireGuardAutoTunnel.uiActive.value) {
|
||||
val notification =
|
||||
notificationManager.createNotification(
|
||||
WireGuardNotification.NotificationChannels.VPN,
|
||||
title =
|
||||
tunName?.let { StringValue.DynamicString(it) }
|
||||
?: StringValue.StringResource(R.string.tunnel),
|
||||
description =
|
||||
StringValue.StringResource(
|
||||
R.string.tunnel_error_template,
|
||||
error.toStringValue(),
|
||||
),
|
||||
groupKey = NotificationManager.VPN_GROUP_KEY,
|
||||
)
|
||||
notificationManager.show(
|
||||
NotificationManager.TUNNEL_ERROR_NOTIFICATION_ID,
|
||||
notification,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleTunnelMessages() =
|
||||
tunnelManager.messageEvents.collectLatest { (tunName, message) ->
|
||||
if (!WireGuardAutoTunnel.uiActive.value) {
|
||||
val notification =
|
||||
notificationManager.createNotification(
|
||||
WireGuardNotification.NotificationChannels.VPN,
|
||||
title =
|
||||
tunName?.let { StringValue.DynamicString(it) }
|
||||
?: StringValue.StringResource(R.string.tunnel),
|
||||
description = message.toStringValue(),
|
||||
groupKey = NotificationManager.VPN_GROUP_KEY,
|
||||
)
|
||||
notificationManager.show(
|
||||
NotificationManager.TUNNEL_MESSAGES_NOTIFICATION_ID,
|
||||
notification,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
-5
@@ -1,23 +1,22 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.notification
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification.NotificationChannels
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.AndroidNotificationService.NotificationChannels
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
|
||||
interface NotificationManager {
|
||||
interface NotificationService {
|
||||
val context: Context
|
||||
|
||||
fun createNotification(
|
||||
channel: NotificationChannels,
|
||||
title: String = "",
|
||||
subText: String? = null,
|
||||
actions: Collection<NotificationCompat.Action> = emptyList(),
|
||||
description: String = "",
|
||||
showTimestamp: Boolean = true,
|
||||
importance: Int = NotificationManager.IMPORTANCE_LOW,
|
||||
onGoing: Boolean = false,
|
||||
onlyAlertOnce: Boolean = true,
|
||||
groupKey: String? = null,
|
||||
@@ -27,16 +26,18 @@ interface NotificationManager {
|
||||
fun createNotification(
|
||||
channel: NotificationChannels,
|
||||
title: StringValue,
|
||||
subText: String? = null,
|
||||
actions: Collection<NotificationCompat.Action> = emptyList(),
|
||||
description: StringValue,
|
||||
showTimestamp: Boolean = true,
|
||||
importance: Int = NotificationManager.IMPORTANCE_LOW,
|
||||
onGoing: Boolean = false,
|
||||
onlyAlertOnce: Boolean = true,
|
||||
groupKey: String? = null,
|
||||
isGroupSummary: Boolean = false,
|
||||
): Notification
|
||||
|
||||
fun createAllChannels()
|
||||
|
||||
fun createNotificationAction(
|
||||
notificationAction: NotificationAction,
|
||||
extraId: Int? = null,
|
||||
@@ -48,6 +49,7 @@ interface NotificationManager {
|
||||
|
||||
companion object {
|
||||
const val VPN_GROUP_KEY = "VPN_GROUP"
|
||||
const val PROXY_GROUP_KEY = "PROXY_GROUP"
|
||||
const val AUTO_TUNNEL_GROUP_KEY = "AUTO_TUNNEL_GROUP"
|
||||
const val AUTO_TUNNEL_LOCATION_PERMISSION_ID = 123
|
||||
const val AUTO_TUNNEL_LOCATION_SERVICES_ID = 124
|
||||
@@ -55,6 +57,7 @@ interface NotificationManager {
|
||||
const val AUTO_TUNNEL_NOTIFICATION_ID = 122
|
||||
// for tunnel foreground notification
|
||||
const val VPN_NOTIFICATION_ID = 100
|
||||
const val PROXY_NOTIFICATION_ID = 103
|
||||
const val TUNNEL_ERROR_NOTIFICATION_ID = 101
|
||||
const val TUNNEL_MESSAGES_NOTIFICATION_ID = 102
|
||||
const val EXTRA_ID = "id"
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.notification
|
||||
|
||||
import com.zaneschepke.tunnel.state.ActiveTunnel
|
||||
|
||||
interface TunnelNotificationService {
|
||||
|
||||
suspend fun updatePersistentNotifications(activeTunnels: Map<Int, ActiveTunnel>)
|
||||
|
||||
suspend fun showIpv4Fallback(tunnelId: Int)
|
||||
|
||||
suspend fun showIpv6Recovery(tunnelId: Int)
|
||||
|
||||
suspend fun showDynamicDnsUpdate(tunnelId: Int)
|
||||
|
||||
suspend fun showVpnRequired()
|
||||
|
||||
suspend fun showStateConflict(tunnelId: Int)
|
||||
|
||||
suspend fun showSocks5PortUnavailable(port: Int)
|
||||
|
||||
suspend fun showHttpPortUnavailable(port: Int)
|
||||
|
||||
suspend fun showRootShellAccess()
|
||||
|
||||
suspend fun showError(message: String)
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.orchestration
|
||||
|
||||
import com.zaneschepke.logcatter.LogReader
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.DnsSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.LockdownSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
class AppBoostrapCoordinator(
|
||||
private val monitoringRepository: MonitoringSettingsRepository,
|
||||
private val settingsRepository: GeneralSettingRepository,
|
||||
private val dnsRepository: DnsSettingsRepository,
|
||||
private val tunnelRepository: TunnelRepository,
|
||||
private val lockdownRepository: LockdownSettingsRepository,
|
||||
private val tunnelProvider: TunnelProvider,
|
||||
private val dnsSettingsCoordinator: DnsSettingsCoordinator,
|
||||
private val logReader: LogReader,
|
||||
) {
|
||||
|
||||
private val _isReady = MutableStateFlow(false)
|
||||
val isReady: StateFlow<Boolean> = _isReady.asStateFlow()
|
||||
|
||||
suspend fun bootstrap() = coroutineScope {
|
||||
launch { bootstrapLogging() }
|
||||
|
||||
val criticalTasks =
|
||||
listOf(
|
||||
async { bootstrapDns() },
|
||||
async { ensureGlobalConfig() },
|
||||
async { restoreLockdown() },
|
||||
)
|
||||
|
||||
try {
|
||||
criticalTasks.awaitAll()
|
||||
_isReady.value = true
|
||||
Timber.d("App bootstrap completed successfully")
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "One or more critical bootstrap tasks failed")
|
||||
_isReady.value = true
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun bootstrapDns() {
|
||||
val dnsSettings = dnsRepository.getDnsSettings()
|
||||
dnsSettingsCoordinator.appyDnsSettings(dnsSettings)
|
||||
}
|
||||
|
||||
private suspend fun bootstrapLogging() {
|
||||
monitoringRepository.flow
|
||||
.distinctUntilChangedBy { it.isLocalLogsEnabled }
|
||||
.collect { settings ->
|
||||
if (settings.isLocalLogsEnabled) {
|
||||
logReader.start()
|
||||
} else {
|
||||
logReader.stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun ensureGlobalConfig() {
|
||||
tunnelRepository.ensureGlobalConfigExists()
|
||||
}
|
||||
|
||||
private suspend fun restoreLockdown() {
|
||||
val settings = settingsRepository.getGeneralSettings()
|
||||
|
||||
when (settings.tunnelMode) {
|
||||
TunnelMode.LOCK_DOWN -> {
|
||||
val lockdownSettings = lockdownRepository.getLockdownSettings()
|
||||
tunnelProvider.setLockDown(lockdownSettings).onFailure {
|
||||
Timber.w(it, "Failed to restore lockdown/kill-switch on startup")
|
||||
}
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.orchestration
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelStateHolder
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
||||
|
||||
class AutoTunnelCoordinator(
|
||||
private val repository: AutoTunnelSettingsRepository,
|
||||
private val serviceManager: ServiceManager,
|
||||
private val autoTunnelStateHolder: AutoTunnelStateHolder,
|
||||
) {
|
||||
|
||||
suspend fun shouldRestore(): Boolean {
|
||||
val settings = repository.getAutoTunnelSettings()
|
||||
return settings.startOnBoot && settings.isAutoTunnelEnabled
|
||||
}
|
||||
|
||||
fun start() {
|
||||
serviceManager.startAutoTunnelService()
|
||||
}
|
||||
|
||||
suspend fun enable() {
|
||||
repository.updateAutoTunnelEnabled(true)
|
||||
serviceManager.startAutoTunnelService()
|
||||
}
|
||||
|
||||
suspend fun toggle() {
|
||||
val running = autoTunnelStateHolder.active.value
|
||||
if (running) {
|
||||
disable()
|
||||
} else enable()
|
||||
}
|
||||
|
||||
suspend fun disable() {
|
||||
repository.updateAutoTunnelEnabled(false)
|
||||
serviceManager.stopAutoTunnelService()
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.orchestration
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.parser.Config
|
||||
import com.zaneschepke.wireguardautotunnel.parser.InterfaceSection
|
||||
|
||||
object ConfigReconciler {
|
||||
private fun mergeInterface(
|
||||
base: InterfaceSection,
|
||||
global: InterfaceSection,
|
||||
policy: ConfigReconcilePolicy,
|
||||
): InterfaceSection {
|
||||
return base.copy(
|
||||
dns = if (policy.dns) global.dns else base.dns,
|
||||
includedApplications =
|
||||
if (policy.splitTunnel) global.includedApplications else base.includedApplications,
|
||||
excludedApplications =
|
||||
if (policy.splitTunnel) global.excludedApplications else base.excludedApplications,
|
||||
jC = if (policy.amnezia) global.jC else base.jC,
|
||||
jMin = if (policy.amnezia) global.jMin else base.jMin,
|
||||
jMax = if (policy.amnezia) global.jMax else base.jMax,
|
||||
s1 = if (policy.amnezia) global.s1 else base.s1,
|
||||
s2 = if (policy.amnezia) global.s2 else base.s2,
|
||||
s3 = if (policy.amnezia) global.s3 else base.s3,
|
||||
s4 = if (policy.amnezia) global.s4 else base.s4,
|
||||
h1 = if (policy.amnezia) global.h1 else base.h1,
|
||||
h2 = if (policy.amnezia) global.h2 else base.h2,
|
||||
h3 = if (policy.amnezia) global.h3 else base.h3,
|
||||
h4 = if (policy.amnezia) global.h4 else base.h4,
|
||||
i1 = if (policy.amnezia) global.i1 else base.i1,
|
||||
i2 = if (policy.amnezia) global.i2 else base.i2,
|
||||
i3 = if (policy.amnezia) global.i3 else base.i3,
|
||||
i4 = if (policy.amnezia) global.i4 else base.i4,
|
||||
i5 = if (policy.amnezia) global.i5 else base.i5,
|
||||
)
|
||||
}
|
||||
|
||||
fun reconcileConfig(base: Config, global: Config?, policy: ConfigReconcilePolicy): Config {
|
||||
if (global == null) return base
|
||||
if (!policy.hasAnyOverrides) return base
|
||||
|
||||
return base.copy(`interface` = mergeInterface(base.`interface`, global.`interface`, policy))
|
||||
}
|
||||
|
||||
data class ConfigReconcilePolicy(
|
||||
val dns: Boolean,
|
||||
val splitTunnel: Boolean,
|
||||
val amnezia: Boolean,
|
||||
) {
|
||||
val hasAnyOverrides
|
||||
get() = dns || splitTunnel || amnezia
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.orchestration
|
||||
|
||||
import com.zaneschepke.tunnel.backend.Backend
|
||||
import com.zaneschepke.tunnel.model.DnsBoostrapConfig
|
||||
import com.zaneschepke.tunnel.model.DnsBoostrapMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.DnsProtocol
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.DnsSettings
|
||||
|
||||
class DnsSettingsCoordinator(private val backend: Backend) {
|
||||
|
||||
suspend fun appyDnsSettings(dnsSettings: DnsSettings) {
|
||||
val mode =
|
||||
when (dnsSettings.dnsProtocol) {
|
||||
DnsProtocol.SYSTEM -> DnsBoostrapMode.System
|
||||
DnsProtocol.DOH ->
|
||||
DnsBoostrapMode.Custom(DnsBoostrapConfig.DoH(dnsSettings.dnsEndpoint))
|
||||
DnsProtocol.DOT ->
|
||||
DnsBoostrapMode.Custom(DnsBoostrapConfig.DoT(dnsSettings.dnsEndpoint))
|
||||
DnsProtocol.UDP ->
|
||||
DnsBoostrapMode.Custom(DnsBoostrapConfig.Plain(dnsSettings.dnsEndpoint))
|
||||
}
|
||||
|
||||
backend.setBootstrapDnsMode(mode)
|
||||
}
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.orchestration
|
||||
|
||||
import android.content.Intent
|
||||
import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutContract
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
|
||||
class ShortcutCoordinator(
|
||||
private val settingsRepository: GeneralSettingRepository,
|
||||
private val tunnelsRepository: TunnelRepository,
|
||||
private val tunnelCoordinator: TunnelCoordinator,
|
||||
private val autoTunnelCoordinator: AutoTunnelCoordinator,
|
||||
) {
|
||||
|
||||
suspend fun handle(intent: Intent) {
|
||||
|
||||
val settings = settingsRepository.getGeneralSettings()
|
||||
|
||||
if (!settings.isShortcutsEnabled) return
|
||||
|
||||
val shortcutType =
|
||||
intent.getStringExtra(ShortcutContract.EXTRA_SHORTCUT_TYPE)
|
||||
?: legacyShortcutType(intent)
|
||||
|
||||
when (shortcutType) {
|
||||
ShortcutContract.ShortcutType.TUNNEL.value -> {
|
||||
handleTunnelShortcut(intent)
|
||||
}
|
||||
|
||||
ShortcutContract.ShortcutType.AUTO_TUNNEL.value -> {
|
||||
handleAutoTunnelShortcut(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleAutoTunnelShortcut(intent: Intent) {
|
||||
|
||||
when (intent.action) {
|
||||
ShortcutContract.Action.START.name -> {
|
||||
autoTunnelCoordinator.enable()
|
||||
}
|
||||
|
||||
ShortcutContract.Action.STOP.name -> {
|
||||
autoTunnelCoordinator.disable()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun legacyShortcutType(intent: Intent): String? {
|
||||
|
||||
return when (intent.getStringExtra(ShortcutContract.EXTRA_CLASS_NAME)) {
|
||||
ShortcutContract.Legacy.AUTO_TUNNEL_SERVICE_CLASS_NAME,
|
||||
ShortcutContract.Legacy.AUTO_TUNNEL_SERVICE_NAME ->
|
||||
ShortcutContract.ShortcutType.AUTO_TUNNEL.value
|
||||
|
||||
ShortcutContract.Legacy.TUNNEL_PROVIDER_NAME,
|
||||
ShortcutContract.Legacy.TUNNEL_SERVICE_NAME ->
|
||||
ShortcutContract.ShortcutType.TUNNEL.value
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleTunnelShortcut(intent: Intent) {
|
||||
|
||||
val tunnelName = intent.getStringExtra(ShortcutContract.EXTRA_TUNNEL_NAME)
|
||||
|
||||
val tunnel =
|
||||
tunnelName?.let { tunnelsRepository.findByTunnelName(it) }
|
||||
?: tunnelsRepository.getDefaultTunnel()
|
||||
|
||||
tunnel ?: return
|
||||
|
||||
when (intent.action) {
|
||||
ShortcutContract.Action.START.name -> {
|
||||
tunnelCoordinator.startTunnel(config = tunnel)
|
||||
}
|
||||
|
||||
ShortcutContract.Action.STOP.name -> {
|
||||
tunnelCoordinator.stopActiveTunnels()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.orchestration
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
class StartupCoordinator(
|
||||
private val tunnelCoordinator: TunnelCoordinator,
|
||||
private val settingsRepository: GeneralSettingRepository,
|
||||
private val autoTunnelCoordinator: AutoTunnelCoordinator,
|
||||
private val tunnelRepository: TunnelRepository,
|
||||
private val bootstrapCoordinator: AppBoostrapCoordinator,
|
||||
) {
|
||||
|
||||
suspend fun applyStartupPolicy(): Result<Unit> = runCatching {
|
||||
val shouldRestoreAutoTunnel = autoTunnelCoordinator.shouldRestore()
|
||||
val settings = settingsRepository.getGeneralSettings()
|
||||
val shouldRestoreDefaultTunnel = settings.isRestoreOnBootEnabled
|
||||
|
||||
if (shouldRestoreAutoTunnel || shouldRestoreDefaultTunnel) {
|
||||
// Wait for app critical bootstrap to finish
|
||||
bootstrapCoordinator.isReady.first { it }
|
||||
} else {
|
||||
return Result.success(Unit)
|
||||
}
|
||||
|
||||
if (shouldRestoreAutoTunnel) {
|
||||
autoTunnelCoordinator.start()
|
||||
return Result.success(Unit)
|
||||
}
|
||||
|
||||
val defaultTunnel = tunnelRepository.getDefaultTunnel() ?: return Result.success(Unit)
|
||||
tunnelCoordinator.startTunnel(defaultTunnel)
|
||||
return Result.success(Unit)
|
||||
}
|
||||
}
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.orchestration
|
||||
|
||||
import com.zaneschepke.tunnel.model.BackendMode
|
||||
import com.zaneschepke.wireguardautotunnel.core.event.TunnelErrorEvent
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.RoomDnsSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelActionSource
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.TunnelActionEvent
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.DnsSettings
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.MonitoringSettings
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.ProxySettings
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.ProxySettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
class TunnelCoordinator(
|
||||
private val tunnelProvider: TunnelProvider,
|
||||
private val serviceManager: ServiceManager,
|
||||
private val bootstrapCoordinator: AppBoostrapCoordinator,
|
||||
settingsRepository: GeneralSettingRepository,
|
||||
private val tunnelRepository: TunnelRepository,
|
||||
dnsSettingsRepository: RoomDnsSettingsRepository,
|
||||
monitoringSettingsRepository: MonitoringSettingsRepository,
|
||||
proxyRepository: ProxySettingsRepository,
|
||||
scope: CoroutineScope,
|
||||
) {
|
||||
|
||||
data class RuntimeSettingsSnapshot(
|
||||
val general: GeneralSettings,
|
||||
val dns: DnsSettings,
|
||||
val monitoring: MonitoringSettings,
|
||||
val proxy: ProxySettings,
|
||||
)
|
||||
|
||||
private val runtimeSettingsSnapshot =
|
||||
combine(
|
||||
settingsRepository.flow,
|
||||
dnsSettingsRepository.flow,
|
||||
monitoringSettingsRepository.flow,
|
||||
proxyRepository.flow,
|
||||
) { general, dns, monitoring, proxy ->
|
||||
RuntimeSettingsSnapshot(
|
||||
general = general,
|
||||
dns = dns,
|
||||
monitoring = monitoring,
|
||||
proxy = proxy,
|
||||
)
|
||||
}
|
||||
|
||||
private val _actions = MutableSharedFlow<TunnelActionEvent>()
|
||||
val actions = _actions.asSharedFlow()
|
||||
|
||||
private val runtimeSettingsSnapshotState =
|
||||
runtimeSettingsSnapshot.stateIn(
|
||||
scope = scope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = null,
|
||||
)
|
||||
|
||||
private suspend fun getSnapshot(): RuntimeSettingsSnapshot {
|
||||
return runtimeSettingsSnapshotState.filterNotNull().first()
|
||||
}
|
||||
|
||||
private var lastActiveTunnels: List<Int> = emptyList()
|
||||
private val tunnelMutex = Mutex()
|
||||
private val _errors = MutableSharedFlow<TunnelErrorEvent>()
|
||||
val errors = _errors.asSharedFlow()
|
||||
|
||||
val backendStatus = tunnelProvider.backendStatus
|
||||
|
||||
suspend fun startTunnel(
|
||||
config: TunnelConfig,
|
||||
source: TunnelActionSource = TunnelActionSource.USER,
|
||||
) = tunnelMutex.withLock {
|
||||
// wait for app to be bootstrapped
|
||||
bootstrapCoordinator.isReady.first { it }
|
||||
startTunnelInternal(config, source)
|
||||
}
|
||||
|
||||
suspend fun stopTunnel(id: Int, source: TunnelActionSource = TunnelActionSource.USER) =
|
||||
tunnelMutex.withLock {
|
||||
stopTunnelInternal(id, source)
|
||||
}
|
||||
|
||||
suspend fun stopActiveTunnels() = tunnelMutex.withLock { stopActiveTunnelsInternal() }
|
||||
|
||||
private suspend fun startTunnelInternal(
|
||||
tunnelConfig: TunnelConfig,
|
||||
source: TunnelActionSource,
|
||||
) {
|
||||
|
||||
val snapshot = getSnapshot()
|
||||
val settings = snapshot.general
|
||||
val dnsSettings = snapshot.dns
|
||||
val proxySettings = snapshot.proxy
|
||||
val monitoringSettings = snapshot.monitoring
|
||||
|
||||
val config = tunnelConfig.getConfig()
|
||||
val policy =
|
||||
ConfigReconciler.ConfigReconcilePolicy(
|
||||
dnsSettings.isGlobalTunnelDnsEnabled,
|
||||
settings.isGlobalSplitTunnelEnabled,
|
||||
settings.isGlobalAmneziaEnabled,
|
||||
)
|
||||
|
||||
val runConfig =
|
||||
if (policy.hasAnyOverrides) {
|
||||
val globalConfig = tunnelRepository.globalTunnelFlow.firstOrNull()?.getConfig()
|
||||
ConfigReconciler.reconcileConfig(config, globalConfig, policy)
|
||||
} else config
|
||||
|
||||
val backendMode =
|
||||
when (settings.tunnelMode) {
|
||||
TunnelMode.VPN -> {
|
||||
|
||||
if (!serviceManager.hasVpnPermission()) {
|
||||
_errors.emit(TunnelErrorEvent.VpnPermissionDenied(tunnelConfig.id))
|
||||
return
|
||||
}
|
||||
|
||||
BackendMode.Vpn(runConfig)
|
||||
}
|
||||
|
||||
TunnelMode.PROXY -> {
|
||||
BackendMode.Proxy.Standard(
|
||||
config = runConfig,
|
||||
proxyConfig = proxySettings.toProxyConfig(),
|
||||
)
|
||||
}
|
||||
|
||||
TunnelMode.LOCK_DOWN -> {
|
||||
|
||||
BackendMode.Proxy.KillSwitchPrimary(runConfig)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO for now, enforce single tunnel until multi-tunneling is implement
|
||||
stopActiveTunnelsInternal()
|
||||
|
||||
tunnelProvider
|
||||
.startTunnel(
|
||||
tunnel =
|
||||
tunnelConfig.toBackendTunnel(
|
||||
monitoringSettings,
|
||||
settings.tunnelScriptingEnabled,
|
||||
),
|
||||
mode = backendMode,
|
||||
)
|
||||
.onSuccess {
|
||||
_actions.emit(
|
||||
TunnelActionEvent.Started(tunnelId = tunnelConfig.id, source = source)
|
||||
)
|
||||
}
|
||||
.onFailure { _errors.emit(TunnelErrorEvent.from(it, tunnelConfig.id)) }
|
||||
}
|
||||
|
||||
suspend fun startDefault() {
|
||||
tunnelRepository.getDefaultTunnel()?.let { tunnel -> startTunnel(tunnel) }
|
||||
}
|
||||
|
||||
suspend fun toggleTunnels(source: TunnelActionSource = TunnelActionSource.USER) =
|
||||
tunnelMutex.withLock {
|
||||
val active = tunnelProvider.backendStatus.value.activeTunnels
|
||||
|
||||
if (active.isNotEmpty()) {
|
||||
lastActiveTunnels = active.keys.toList()
|
||||
stopActiveTunnelsInternal()
|
||||
return@withLock
|
||||
}
|
||||
|
||||
val tunnelsToStart =
|
||||
when {
|
||||
lastActiveTunnels.isNotEmpty() -> {
|
||||
lastActiveTunnels.mapNotNull { tunnelRepository.getById(it) }
|
||||
}
|
||||
|
||||
else -> {
|
||||
tunnelRepository.getDefaultTunnel()?.let(::listOf) ?: emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
tunnelsToStart.forEach { startTunnelInternal(it, source) }
|
||||
}
|
||||
|
||||
private suspend fun stopTunnelInternal(id: Int, source: TunnelActionSource) {
|
||||
tunnelProvider
|
||||
.stopTunnel(id)
|
||||
.onSuccess { _actions.emit(TunnelActionEvent.Stopped(tunnelId = id, source = source)) }
|
||||
.onFailure { _errors.emit(TunnelErrorEvent.from(it, id)) }
|
||||
}
|
||||
|
||||
private suspend fun stopActiveTunnelsInternal() {
|
||||
tunnelProvider.stopActiveTunnels()
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.orchestration
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.LockdownSettingsRepository
|
||||
|
||||
class TunnelModeCoordinator(
|
||||
private val tunnelProvider: TunnelProvider,
|
||||
private val settingsRepository: GeneralSettingRepository,
|
||||
private val lockdownRepository: LockdownSettingsRepository,
|
||||
) {
|
||||
|
||||
suspend fun changeMode(newMode: TunnelMode): Result<Unit> {
|
||||
|
||||
val settings = settingsRepository.getGeneralSettings()
|
||||
val oldMode = settings.tunnelMode
|
||||
|
||||
if (oldMode == newMode) {
|
||||
return Result.success(Unit)
|
||||
}
|
||||
|
||||
return runCatching {
|
||||
tunnelProvider.stopActiveTunnels().getOrThrow()
|
||||
exitMode(oldMode)
|
||||
enterMode(newMode)
|
||||
|
||||
settingsRepository.upsert(settings.copy(tunnelMode = newMode))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun exitMode(oldMode: TunnelMode) {
|
||||
when (oldMode) {
|
||||
TunnelMode.LOCK_DOWN -> {
|
||||
tunnelProvider.disableLockDown().getOrThrow()
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun enterMode(newMode: TunnelMode) {
|
||||
when (newMode) {
|
||||
TunnelMode.LOCK_DOWN -> {
|
||||
val lockdownSettings = lockdownRepository.getLockdownSettings()
|
||||
|
||||
tunnelProvider.setLockDown(lockdownSettings).getOrThrow()
|
||||
}
|
||||
|
||||
TunnelMode.VPN,
|
||||
TunnelMode.PROXY -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
-167
@@ -1,167 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service
|
||||
|
||||
import android.app.Notification
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.lifecycle.LifecycleService
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelMonitor
|
||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.distinctByKeys
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
@AndroidEntryPoint
|
||||
abstract class BaseTunnelForegroundService : LifecycleService(), TunnelService {
|
||||
|
||||
@Inject lateinit var notificationManager: NotificationManager
|
||||
|
||||
@Inject lateinit var serviceManager: ServiceManager
|
||||
|
||||
@Inject lateinit var tunnelManager: TunnelManager
|
||||
|
||||
@Inject lateinit var tunnelMonitor: TunnelMonitor
|
||||
|
||||
@Inject @IoDispatcher lateinit var ioDispatcher: CoroutineDispatcher
|
||||
|
||||
@Inject lateinit var settingsRepository: GeneralSettingRepository
|
||||
|
||||
@Inject lateinit var tunnelsRepository: TunnelRepository
|
||||
|
||||
protected abstract val fgsType: Int
|
||||
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
super.onBind(intent)
|
||||
return LocalBinder(this)
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
ServiceCompat.startForeground(
|
||||
this,
|
||||
NotificationManager.VPN_NOTIFICATION_ID,
|
||||
onCreateNotification(),
|
||||
fgsType,
|
||||
)
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
super.onStartCommand(intent, flags, startId)
|
||||
ServiceCompat.startForeground(
|
||||
this,
|
||||
NotificationManager.VPN_NOTIFICATION_ID,
|
||||
onCreateNotification(),
|
||||
fgsType,
|
||||
)
|
||||
if (
|
||||
intent == null ||
|
||||
intent.component == null ||
|
||||
(intent.component?.packageName != this.packageName)
|
||||
) {
|
||||
Timber.d("Service started by Always-on VPN feature")
|
||||
lifecycleScope.launch {
|
||||
val settings = settingsRepository.getGeneralSettings()
|
||||
if (settings.isAlwaysOnVpnEnabled) {
|
||||
val tunnel = tunnelsRepository.getDefaultTunnel()
|
||||
tunnel?.let { tunnelManager.startTunnel(it) }
|
||||
} else {
|
||||
Timber.w("Always-on VPN is not enabled in app settings")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
start()
|
||||
}
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
override fun start() {
|
||||
lifecycleScope.launch(ioDispatcher) {
|
||||
tunnelManager.activeTunnels.distinctByKeys().collect { activeTunnels ->
|
||||
val activeTunConfigs = activeTunnels.keys
|
||||
val tunnels = tunnelsRepository.getAll()
|
||||
val activeConfigs = tunnels.filter { activeTunConfigs.contains(it.id) }
|
||||
updateServiceNotification(activeConfigs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Would be cool to have this include kill switch
|
||||
private fun updateServiceNotification(activeConfigs: List<TunnelConfig>) {
|
||||
val notification =
|
||||
when (activeConfigs.size) {
|
||||
0 -> onCreateNotification()
|
||||
1 -> createTunnelNotification(activeConfigs.first())
|
||||
else -> createTunnelsNotification()
|
||||
}
|
||||
ServiceCompat.startForeground(
|
||||
this,
|
||||
NotificationManager.VPN_NOTIFICATION_ID,
|
||||
notification,
|
||||
fgsType,
|
||||
)
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
Timber.d("Stop called")
|
||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
serviceManager.handleTunnelServiceDestroy()
|
||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||
Timber.d("onDestroy")
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun createTunnelNotification(tunnelConfig: TunnelConfig): Notification {
|
||||
return notificationManager.createNotification(
|
||||
WireGuardNotification.NotificationChannels.VPN,
|
||||
title = "${getString(R.string.tunnel_running)} - ${tunnelConfig.name}",
|
||||
actions =
|
||||
listOf(
|
||||
notificationManager.createNotificationAction(
|
||||
NotificationAction.TUNNEL_OFF,
|
||||
tunnelConfig.id,
|
||||
)
|
||||
),
|
||||
onGoing = true,
|
||||
groupKey = NotificationManager.VPN_GROUP_KEY,
|
||||
isGroupSummary = true,
|
||||
)
|
||||
}
|
||||
|
||||
private fun createTunnelsNotification(): Notification {
|
||||
return notificationManager.createNotification(
|
||||
WireGuardNotification.NotificationChannels.VPN,
|
||||
title = "${getString(R.string.tunnel_running)} - ${getString(R.string.multiple)}",
|
||||
actions =
|
||||
listOf(
|
||||
notificationManager.createNotificationAction(NotificationAction.TUNNEL_OFF, 0)
|
||||
),
|
||||
groupKey = NotificationManager.VPN_GROUP_KEY,
|
||||
isGroupSummary = true,
|
||||
)
|
||||
}
|
||||
|
||||
private fun onCreateNotification(): Notification {
|
||||
return notificationManager.createNotification(
|
||||
WireGuardNotification.NotificationChannels.VPN,
|
||||
title = getString(R.string.tunnel_starting),
|
||||
groupKey = NotificationManager.VPN_GROUP_KEY,
|
||||
isGroupSummary = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service
|
||||
|
||||
import android.os.Binder
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
class LocalBinder(service: TunnelService) : Binder() {
|
||||
private val serviceRef = WeakReference(service)
|
||||
|
||||
val service: TunnelService?
|
||||
get() = serviceRef.get()
|
||||
}
|
||||
+6
-174
@@ -1,189 +1,21 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.net.VpnService
|
||||
import android.os.IBinder
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelService
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
|
||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
|
||||
import jakarta.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.*
|
||||
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
|
||||
@Inject
|
||||
constructor(
|
||||
private val context: Context,
|
||||
@IoDispatcher ioDispatcher: CoroutineDispatcher,
|
||||
@ApplicationScope applicationScope: CoroutineScope,
|
||||
private val mainDispatcher: CoroutineDispatcher,
|
||||
private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
|
||||
) {
|
||||
class ServiceManager(private val context: Context) {
|
||||
|
||||
private val autoTunnelMutex = Mutex()
|
||||
private val tunnelMutex = Mutex()
|
||||
|
||||
private val _tunnelService = MutableStateFlow<TunnelService?>(null)
|
||||
private val _autoTunnelService = MutableStateFlow<AutoTunnelService?>(null)
|
||||
val autoTunnelService = _autoTunnelService.asStateFlow()
|
||||
val tunnelService = _tunnelService.asStateFlow()
|
||||
|
||||
init {
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
_autoTunnelService
|
||||
.onEach { _ -> withContext(mainDispatcher) { updateAutoTunnelTile() } }
|
||||
.launchIn(this)
|
||||
}
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
combine(
|
||||
autoTunnelSettingsRepository.flow
|
||||
.map { it.isAutoTunnelEnabled }
|
||||
.distinctUntilChanged(),
|
||||
_autoTunnelService,
|
||||
) { enabled, service ->
|
||||
enabled to (service != null)
|
||||
}
|
||||
.collect { (enabled, isRunning) ->
|
||||
when {
|
||||
enabled && !isRunning -> {
|
||||
autoTunnelMutex.withLock { startServiceInternal() }
|
||||
}
|
||||
!enabled && isRunning -> {
|
||||
autoTunnelMutex.withLock { stopServiceInternal() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fun startAutoTunnelService() {
|
||||
context.startForegroundService(Intent(context, AutoTunnelService::class.java))
|
||||
}
|
||||
|
||||
private val tunnelServiceConnection =
|
||||
object : ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName, service: IBinder) {
|
||||
val binder = service as? LocalBinder
|
||||
_tunnelService.update { binder?.service }
|
||||
val serviceClass =
|
||||
when {
|
||||
name.className.contains("VpnForegroundService") -> "VpnForegroundService"
|
||||
name.className.contains("TunnelForegroundService") ->
|
||||
"TunnelForegroundService"
|
||||
else -> "Unknown"
|
||||
}
|
||||
Timber.d("$serviceClass connected")
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName) {
|
||||
_tunnelService.update { null }
|
||||
val serviceClass =
|
||||
when {
|
||||
name.className.contains("VpnForegroundService") -> "VpnForegroundService"
|
||||
name.className.contains("TunnelForegroundService") ->
|
||||
"TunnelForegroundService"
|
||||
else -> "Unknown"
|
||||
}
|
||||
Timber.d("$serviceClass disconnected")
|
||||
}
|
||||
}
|
||||
|
||||
private val autoTunnelServiceConnection =
|
||||
object : ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName, service: IBinder) {
|
||||
val binder = service as? AutoTunnelService.LocalBinder
|
||||
_autoTunnelService.update { binder?.service }
|
||||
Timber.d("AutoTunnelService connected")
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName) {
|
||||
_autoTunnelService.update { null }
|
||||
Timber.d("AutoTunnelService disconnected")
|
||||
}
|
||||
}
|
||||
fun stopAutoTunnelService() {
|
||||
context.stopService(Intent(context, AutoTunnelService::class.java))
|
||||
}
|
||||
|
||||
fun hasVpnPermission(): Boolean {
|
||||
return VpnService.prepare(context) == null
|
||||
}
|
||||
|
||||
private fun startServiceInternal() {
|
||||
if (autoTunnelService.value == null) {
|
||||
val intent = Intent(context, AutoTunnelService::class.java)
|
||||
context.startForegroundService(intent)
|
||||
context.bindService(intent, autoTunnelServiceConnection, Context.BIND_AUTO_CREATE)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun startAutoTunnelService() = autoTunnelMutex.withLock { startServiceInternal() }
|
||||
|
||||
private fun stopServiceInternal() {
|
||||
_autoTunnelService.value?.stop()
|
||||
try {
|
||||
context.unbindService(autoTunnelServiceConnection)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Failed to unbind AutoTunnelService")
|
||||
}
|
||||
_autoTunnelService.update { null }
|
||||
}
|
||||
|
||||
suspend fun startTunnelService(appMode: AppMode) =
|
||||
tunnelMutex.withLock {
|
||||
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() =
|
||||
tunnelMutex.withLock {
|
||||
_tunnelService.value?.let { service ->
|
||||
service.stop()
|
||||
try {
|
||||
context.unbindService(tunnelServiceConnection)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Failed to unbind Tunnel Service")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateAutoTunnelTile() {
|
||||
context.requestAutoTunnelTileServiceUpdate()
|
||||
}
|
||||
|
||||
fun updateTunnelTile() {
|
||||
context.requestTunnelTileServiceStateUpdate()
|
||||
}
|
||||
|
||||
fun handleTunnelServiceDestroy() {
|
||||
_tunnelService.update { null }
|
||||
}
|
||||
|
||||
fun handleAutoTunnelServiceDestroy() {
|
||||
_autoTunnelService.update { null }
|
||||
}
|
||||
}
|
||||
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
@AndroidEntryPoint
|
||||
class TunnelForegroundService(override val fgsType: Int = Constants.SPECIAL_USE_SERVICE_TYPE_ID) :
|
||||
BaseTunnelForegroundService()
|
||||
@@ -1,7 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service
|
||||
|
||||
interface TunnelService {
|
||||
fun start()
|
||||
|
||||
fun stop()
|
||||
}
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
@AndroidEntryPoint
|
||||
class VpnForegroundService(override val fgsType: Int = Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID) :
|
||||
BaseTunnelForegroundService()
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.autotunnel
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.ActiveNetwork
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.AutoTunnelState
|
||||
|
||||
class AutoTunnelEngine {
|
||||
|
||||
fun evaluate(state: AutoTunnelState): AutoTunnelEvent {
|
||||
return when (val decision = decide(state)) {
|
||||
is Decision.Sync -> {
|
||||
if (decision.start.isEmpty() && decision.stop.isEmpty()) {
|
||||
AutoTunnelEvent.DoNothing
|
||||
} else {
|
||||
AutoTunnelEvent.Sync(start = decision.start, stop = decision.stop)
|
||||
}
|
||||
}
|
||||
Decision.None -> AutoTunnelEvent.DoNothing
|
||||
}
|
||||
}
|
||||
|
||||
private fun decide(state: AutoTunnelState): Decision {
|
||||
val network = state.networkState
|
||||
val settings = state.settings
|
||||
val backend = state.backendStatus
|
||||
|
||||
val activeTunnelIds = backend.activeTunnels.keys.toSet()
|
||||
|
||||
val desiredTunnels = resolveDesiredTunnels(state).map { it.id }.toSet()
|
||||
|
||||
// stop condition overrides everything
|
||||
if (!network.hasInternet() && settings.isStopOnNoInternetEnabled) {
|
||||
return Decision.Sync(start = emptySet(), stop = activeTunnelIds)
|
||||
}
|
||||
|
||||
val toStart = desiredTunnels - activeTunnelIds
|
||||
val toStop = activeTunnelIds - desiredTunnels
|
||||
|
||||
if (toStart.isEmpty() && toStop.isEmpty()) {
|
||||
return Decision.None
|
||||
}
|
||||
|
||||
return Decision.Sync(
|
||||
start = state.tunnels.filter { it.id in toStart }.toSet(),
|
||||
stop = toStop,
|
||||
)
|
||||
}
|
||||
|
||||
private fun resolveDesiredTunnels(state: AutoTunnelState): List<TunnelConfig> {
|
||||
val network = state.networkState
|
||||
val settings = state.settings
|
||||
|
||||
val wifiActive = network.activeNetwork is ActiveNetwork.Wifi
|
||||
val mobileActive = network.activeNetwork is ActiveNetwork.Cellular
|
||||
val ethernetActive = network.activeNetwork is ActiveNetwork.Ethernet
|
||||
|
||||
return when {
|
||||
ethernetActive && settings.isTunnelOnEthernetEnabled ->
|
||||
resolveByPriority(state) { it.isEthernetTunnel }
|
||||
|
||||
mobileActive && settings.isTunnelOnMobileDataEnabled ->
|
||||
resolveByPriority(state) { it.isMobileDataTunnel }
|
||||
|
||||
wifiActive && settings.isTunnelOnWifiEnabled && !isWifiTrusted(state) ->
|
||||
resolveWifiTunnels(state)
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveByPriority(
|
||||
state: AutoTunnelState,
|
||||
predicate: (TunnelConfig) -> Boolean,
|
||||
): List<TunnelConfig> {
|
||||
return listOfNotNull(state.tunnels.firstOrNull(predicate) ?: defaultTunnel(state))
|
||||
}
|
||||
|
||||
private fun resolveWifiTunnels(state: AutoTunnelState): List<TunnelConfig> {
|
||||
val wifi = state.networkState.activeNetwork as? ActiveNetwork.Wifi ?: return emptyList()
|
||||
|
||||
val matched = state.tunnels.filter { state.matchesNetwork(wifi.ssid, it.tunnelNetworks) }
|
||||
|
||||
return matched.ifEmpty { listOfNotNull(defaultTunnel(state)) }
|
||||
}
|
||||
|
||||
private fun isWifiTrusted(state: AutoTunnelState): Boolean {
|
||||
val wifi = state.networkState.activeNetwork as? ActiveNetwork.Wifi ?: return false
|
||||
return state.matchesNetwork(wifi.ssid, state.settings.trustedNetworkSSIDs)
|
||||
}
|
||||
|
||||
private fun defaultTunnel(state: AutoTunnelState): TunnelConfig? {
|
||||
return state.tunnels.firstOrNull { it.isPrimaryTunnel } ?: state.tunnels.firstOrNull()
|
||||
}
|
||||
|
||||
private sealed interface Decision {
|
||||
data class Sync(val start: Set<TunnelConfig>, val stop: Set<Int>) : Decision
|
||||
|
||||
data object None : Decision
|
||||
}
|
||||
}
|
||||
+233
-257
@@ -1,23 +1,22 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.autotunnel
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Binder
|
||||
import android.os.IBinder
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.lifecycle.LifecycleService
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
|
||||
import com.zaneschepke.networkmonitor.ConnectivityState
|
||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||
import com.zaneschepke.networkmonitor.StableNetworkEngine
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
|
||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.AndroidNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.tile.AutoTunnelTileRefresher
|
||||
import com.zaneschepke.wireguardautotunnel.di.Dispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelActionSource
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.TunnelActionEvent
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.AutoTunnelSettings
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
||||
@@ -27,62 +26,85 @@ import com.zaneschepke.wireguardautotunnel.domain.state.AutoTunnelState
|
||||
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.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.core.qualifier.named
|
||||
import timber.log.Timber
|
||||
|
||||
@AndroidEntryPoint
|
||||
class AutoTunnelService : LifecycleService() {
|
||||
|
||||
@Inject lateinit var networkMonitor: NetworkMonitor
|
||||
private val engine = AutoTunnelEngine()
|
||||
|
||||
@Inject lateinit var notificationManager: NotificationManager
|
||||
private val reconciliationMutex = Mutex()
|
||||
|
||||
@Inject @IoDispatcher lateinit var ioDispatcher: CoroutineDispatcher
|
||||
private val networkEngine: StableNetworkEngine by inject()
|
||||
|
||||
@Inject lateinit var serviceManager: ServiceManager
|
||||
private val notificationService: NotificationService by inject()
|
||||
|
||||
@Inject lateinit var tunnelManager: TunnelManager
|
||||
private val ioDispatcher: CoroutineDispatcher by inject(named(Dispatcher.IO))
|
||||
|
||||
@Inject lateinit var autoTunnelRepository: Provider<AutoTunnelSettingsRepository>
|
||||
@Inject lateinit var settingsRepository: GeneralSettingRepository
|
||||
@Inject lateinit var tunnelsRepository: TunnelRepository
|
||||
|
||||
private val defaultState = AutoTunnelState()
|
||||
|
||||
private val autoTunMutex = Mutex()
|
||||
|
||||
private val autoTunnelStateFlow = MutableStateFlow(defaultState)
|
||||
private val stateHolder: AutoTunnelStateHolder by inject()
|
||||
|
||||
private val autoTunnelRepository: AutoTunnelSettingsRepository by inject()
|
||||
private val settingsRepository: GeneralSettingRepository by inject()
|
||||
private val tunnelsRepository: TunnelRepository by inject()
|
||||
private val tunnelCoordinator: TunnelCoordinator by inject()
|
||||
private var autoTunnelJob: Job? = null
|
||||
private var permissionsJob: Job? = null
|
||||
private var overridesJob: Job? = null
|
||||
|
||||
class LocalBinder(service: AutoTunnelService) : Binder() {
|
||||
private val serviceRef = WeakReference(service)
|
||||
@Volatile private var manualOverrideState = ManualOverrideState()
|
||||
|
||||
val service: AutoTunnelService?
|
||||
get() = serviceRef.get()
|
||||
private data class PermissionWarningState(
|
||||
val detectionMethod: AndroidNetworkMonitor.WifiDetectionMethod,
|
||||
val locationServicesEnabled: Boolean,
|
||||
val locationPermissionsEnabled: Boolean,
|
||||
val ssidReadRequired: Boolean,
|
||||
)
|
||||
|
||||
private data class ManualOverrideState(
|
||||
val fingerprint: AutoTunnelState.NetworkFingerprint? = null,
|
||||
val stoppedTunnelIds: Set<Int> = emptySet(),
|
||||
val startedTunnelIds: Set<Int> = emptySet(),
|
||||
)
|
||||
|
||||
private val autoTunnelStateFlow: Flow<AutoTunnelState> by lazy {
|
||||
val networkFlow = networkEngine.stableState.mapNotNull { it?.state?.toDomain() }
|
||||
|
||||
val settingsFlow = combineSettings()
|
||||
|
||||
val backendFlow =
|
||||
tunnelCoordinator.backendStatus.distinctUntilChangedBy { it.activeTunnels.keys.toSet() }
|
||||
|
||||
combine(networkFlow, settingsFlow, backendFlow) { network, settings, backend ->
|
||||
AutoTunnelState(
|
||||
networkState = network,
|
||||
settings = settings.second,
|
||||
tunnelMode = settings.first,
|
||||
tunnels = settings.third,
|
||||
backendStatus = backend,
|
||||
)
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
private val binder = LocalBinder(this)
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
stateHolder.setActive(true)
|
||||
launchWatcherNotification()
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
super.onBind(intent)
|
||||
return binder
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
super.onStartCommand(intent, flags, startId)
|
||||
Timber.d("onStartCommand executed with startId: $startId")
|
||||
@@ -91,44 +113,90 @@ class AutoTunnelService : LifecycleService() {
|
||||
}
|
||||
|
||||
fun start() {
|
||||
stateHolder.setActive(true)
|
||||
AutoTunnelTileRefresher.refresh(this)
|
||||
launchWatcherNotification()
|
||||
autoTunnelJob?.cancel()
|
||||
autoTunnelJob = startAutoTunnelStateJob()
|
||||
permissionsJob?.cancel()
|
||||
permissionsJob = startLocationPermissionsNotificationJob()
|
||||
overridesJob?.cancel()
|
||||
overridesJob = startOverridesJob()
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
stateHolder.setActive(false)
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
serviceManager.handleAutoTunnelServiceDestroy()
|
||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||
stateHolder.setActive(false)
|
||||
AutoTunnelTileRefresher.refresh(this)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun startOverridesJob(): Job =
|
||||
lifecycleScope.launch(ioDispatcher) {
|
||||
tunnelCoordinator.actions.collect { action ->
|
||||
reconciliationMutex.withLock {
|
||||
manualOverrideState =
|
||||
when (action) {
|
||||
is TunnelActionEvent.Started -> {
|
||||
|
||||
if (action.source != TunnelActionSource.USER) {
|
||||
return@withLock
|
||||
}
|
||||
|
||||
manualOverrideState.copy(
|
||||
startedTunnelIds =
|
||||
manualOverrideState.startedTunnelIds + action.tunnelId,
|
||||
stoppedTunnelIds =
|
||||
manualOverrideState.stoppedTunnelIds - action.tunnelId,
|
||||
)
|
||||
}
|
||||
|
||||
is TunnelActionEvent.Stopped -> {
|
||||
|
||||
if (action.source != TunnelActionSource.USER) {
|
||||
return@withLock
|
||||
}
|
||||
|
||||
manualOverrideState.copy(
|
||||
stoppedTunnelIds =
|
||||
manualOverrideState.stoppedTunnelIds + action.tunnelId,
|
||||
startedTunnelIds =
|
||||
manualOverrideState.startedTunnelIds - action.tunnelId,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Timber.d("Updated manual overrides: $manualOverrideState")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchWatcherNotification(
|
||||
description: String = getString(R.string.monitoring_state_changes)
|
||||
) {
|
||||
val notification =
|
||||
notificationManager.createNotification(
|
||||
WireGuardNotification.NotificationChannels.AUTO_TUNNEL,
|
||||
notificationService.createNotification(
|
||||
AndroidNotificationService.NotificationChannels.AUTO_TUNNEL,
|
||||
title = getString(R.string.auto_tunnel_title),
|
||||
description = description,
|
||||
actions =
|
||||
listOf(
|
||||
notificationManager.createNotificationAction(
|
||||
notificationService.createNotificationAction(
|
||||
NotificationAction.AUTO_TUNNEL_OFF
|
||||
)
|
||||
),
|
||||
onGoing = true,
|
||||
groupKey = NotificationManager.AUTO_TUNNEL_GROUP_KEY,
|
||||
groupKey = NotificationService.AUTO_TUNNEL_GROUP_KEY,
|
||||
isGroupSummary = true,
|
||||
)
|
||||
ServiceCompat.startForeground(
|
||||
this,
|
||||
NotificationManager.AUTO_TUNNEL_NOTIFICATION_ID,
|
||||
NotificationService.AUTO_TUNNEL_NOTIFICATION_ID,
|
||||
notification,
|
||||
Constants.SPECIAL_USE_SERVICE_TYPE_ID,
|
||||
)
|
||||
@@ -136,241 +204,149 @@ class AutoTunnelService : LifecycleService() {
|
||||
|
||||
private fun startAutoTunnelStateJob(): Job =
|
||||
lifecycleScope.launch(ioDispatcher) {
|
||||
val networkFlow =
|
||||
debouncedConnectivityStateFlow
|
||||
.flowOn(ioDispatcher)
|
||||
.map { it.toDomain() }
|
||||
.map(::NetworkChange)
|
||||
.distinctUntilChanged()
|
||||
autoTunnelStateFlow.collectLatest { state ->
|
||||
reconciliationMutex.withLock {
|
||||
updateFingerprintIfNeeded(state)
|
||||
|
||||
val settingsFlow =
|
||||
combineSettings().map { (appMode, settings, tunnels) ->
|
||||
SettingsChange(appMode, settings, tunnels)
|
||||
}
|
||||
val rawEvent = engine.evaluate(state)
|
||||
|
||||
val tunnelsFlow = tunnelManager.activeTunnels.map(::ActiveTunnelsChange)
|
||||
val event = applyOverrides(rawEvent)
|
||||
|
||||
var reevaluationJob: Job? = null
|
||||
Timber.d("AutoTunnel reconciliation event: $event")
|
||||
|
||||
// get everything in sync before we use merge
|
||||
combine(networkFlow, settingsFlow, tunnelsFlow) { network, settings, tunnels ->
|
||||
autoTunnelStateFlow.update {
|
||||
it.copy(
|
||||
activeTunnels = tunnels.activeTunnels,
|
||||
networkState = network.networkState,
|
||||
settings = settings.settings,
|
||||
tunnels = settings.tunnels,
|
||||
)
|
||||
}
|
||||
}
|
||||
.first()
|
||||
|
||||
val initialState = autoTunnelStateFlow.value
|
||||
if (initialState != defaultState) {
|
||||
handleAutoTunnelEvent(
|
||||
initialState.determineAutoTunnelEvent(NetworkChange(initialState.networkState))
|
||||
)
|
||||
}
|
||||
|
||||
// use merge to limit the noise of a combine and also increase the scalability of auto
|
||||
// tunnel handling new states
|
||||
merge(networkFlow, settingsFlow, tunnelsFlow).collect { change ->
|
||||
if (change !is ActiveTunnelsChange) {
|
||||
Timber.d("New state changed to ${change.javaClass.simpleName}")
|
||||
}
|
||||
|
||||
val previousState = autoTunnelStateFlow.value
|
||||
|
||||
when (change) {
|
||||
is NetworkChange -> {
|
||||
Timber.d("Network change: ${change.networkState}")
|
||||
reevaluationJob?.cancel()
|
||||
autoTunnelStateFlow.update { it.copy(networkState = change.networkState) }
|
||||
if (previousState.networkState == change.networkState) {
|
||||
Timber.d("Duplicate network state change detected, ignoring")
|
||||
return@collect
|
||||
}
|
||||
}
|
||||
is SettingsChange -> {
|
||||
reevaluationJob?.cancel()
|
||||
autoTunnelStateFlow.update {
|
||||
it.copy(settings = change.settings, tunnels = change.tunnels)
|
||||
}
|
||||
if (
|
||||
previousState.settings == change.settings &&
|
||||
previousState.tunnels == change.tunnels
|
||||
) {
|
||||
Timber.d("Duplicate settings change detected, ignoring")
|
||||
return@collect
|
||||
}
|
||||
}
|
||||
is ActiveTunnelsChange -> {
|
||||
autoTunnelStateFlow.update { it.copy(activeTunnels = change.activeTunnels) }
|
||||
return@collect
|
||||
}
|
||||
}
|
||||
|
||||
handleAutoTunnelEvent(autoTunnelStateFlow.value.determineAutoTunnelEvent(change))
|
||||
|
||||
reevaluationJob = launch {
|
||||
val snapshotNetwork = autoTunnelStateFlow.value.networkState
|
||||
delay(REEVALUATE_CHECK_DELAY)
|
||||
val currentState = autoTunnelStateFlow.value
|
||||
if (
|
||||
currentState != defaultState && currentState.networkState != snapshotNetwork
|
||||
) {
|
||||
Timber.d(
|
||||
"Re-evaluating auto-tunnel state.. (network changed since snapshot)"
|
||||
)
|
||||
handleAutoTunnelEvent(currentState.determineAutoTunnelEvent(change))
|
||||
} else {
|
||||
Timber.d("Skipping re-eval: network unchanged or default state")
|
||||
}
|
||||
handleAutoTunnelEvent(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun combineSettings(): Flow<Triple<AppMode, AutoTunnelSettings, List<TunnelConfig>>> {
|
||||
private fun updateFingerprintIfNeeded(state: AutoTunnelState) {
|
||||
val fingerprint = state.networkFingerPrint
|
||||
|
||||
if (manualOverrideState.fingerprint != fingerprint) {
|
||||
Timber.d("Network changed, clearing overrides")
|
||||
|
||||
manualOverrideState = ManualOverrideState(fingerprint = fingerprint)
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyOverrides(event: AutoTunnelEvent): AutoTunnelEvent {
|
||||
|
||||
if (event !is AutoTunnelEvent.Sync) {
|
||||
return event
|
||||
}
|
||||
|
||||
val filteredStart =
|
||||
event.start.filterNot { it.id in manualOverrideState.stoppedTunnelIds }.toSet()
|
||||
|
||||
val filteredStop =
|
||||
event.stop.filterNot { it in manualOverrideState.startedTunnelIds }.toSet()
|
||||
|
||||
if (filteredStart.isEmpty() && filteredStop.isEmpty()) {
|
||||
return AutoTunnelEvent.DoNothing
|
||||
}
|
||||
|
||||
return event.copy(start = filteredStart, stop = filteredStop)
|
||||
}
|
||||
|
||||
private fun combineSettings():
|
||||
Flow<Triple<TunnelMode, AutoTunnelSettings, List<TunnelConfig>>> {
|
||||
return combine(
|
||||
settingsRepository.flow.map { it.appMode }.distinctUntilChanged(),
|
||||
autoTunnelRepository.get().flow,
|
||||
tunnelsRepository.flow.map { tunnels ->
|
||||
// isActive is ignored for equality checks so user can manually toggle off
|
||||
// tunnel with auto-tunnel
|
||||
tunnels.map { it.copy(isActive = false) }
|
||||
},
|
||||
settingsRepository.flow.map { it.tunnelMode }.distinctUntilChanged(),
|
||||
autoTunnelRepository.flow,
|
||||
tunnelsRepository.userTunnelsFlow,
|
||||
) { appMode, autoTunnel, tunnels ->
|
||||
Triple(appMode, autoTunnel, tunnels)
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
private fun areAutoTunnelPermissionsRequiredTheSame(
|
||||
old: AutoTunnelState,
|
||||
new: AutoTunnelState,
|
||||
): Boolean {
|
||||
return (old.settings.wifiDetectionMethod == new.settings.wifiDetectionMethod &&
|
||||
old.networkState.locationPermissionGranted ==
|
||||
new.networkState.locationPermissionGranted &&
|
||||
old.networkState.locationServicesEnabled == new.networkState.locationServicesEnabled &&
|
||||
old.tunnels == new.tunnels &&
|
||||
old.settings.trustedNetworkSSIDs == new.settings.trustedNetworkSSIDs)
|
||||
}
|
||||
|
||||
// watch for changes to location permission and notify user it will impact auto-tunneling
|
||||
// TODO or a recheck button for location permission so we dont have to poll it
|
||||
private fun startLocationPermissionsNotificationJob(): Job =
|
||||
lifecycleScope.launch(ioDispatcher) {
|
||||
var locationServicesShown = false
|
||||
var locationPermissionsShown = false
|
||||
|
||||
data class NetworkPermissionState(
|
||||
val detectionMethod: AndroidNetworkMonitor.WifiDetectionMethod,
|
||||
val locationServicesEnabled: Boolean,
|
||||
val locationPermissionsEnabled: Boolean,
|
||||
val ssidReadRequired: Boolean,
|
||||
)
|
||||
|
||||
autoTunnelStateFlow
|
||||
.distinctUntilChanged(::areAutoTunnelPermissionsRequiredTheSame)
|
||||
.map {
|
||||
NetworkPermissionState(
|
||||
it.settings.wifiDetectionMethod.to(),
|
||||
it.networkState.locationServicesEnabled,
|
||||
it.networkState.locationPermissionGranted,
|
||||
(it.tunnels.any { tunnel -> tunnel.tunnelNetworks.isNotEmpty() } ||
|
||||
it.settings.trustedNetworkSSIDs.isNotEmpty()),
|
||||
.map { state ->
|
||||
PermissionWarningState(
|
||||
detectionMethod = state.settings.wifiDetectionMethod.to(),
|
||||
locationServicesEnabled = state.networkState.locationServicesEnabled,
|
||||
locationPermissionsEnabled = state.networkState.locationPermissionGranted,
|
||||
ssidReadRequired =
|
||||
state.tunnels.any { it.tunnelNetworks.isNotEmpty() } ||
|
||||
state.settings.trustedNetworkSSIDs.isNotEmpty(),
|
||||
)
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.collect { state ->
|
||||
when (state.detectionMethod) {
|
||||
AndroidNetworkMonitor.WifiDetectionMethod.DEFAULT,
|
||||
AndroidNetworkMonitor.WifiDetectionMethod.LEGACY -> {
|
||||
if (
|
||||
!state.locationPermissionsEnabled &&
|
||||
!locationPermissionsShown &&
|
||||
state.ssidReadRequired
|
||||
) {
|
||||
locationPermissionsShown = true
|
||||
val notification =
|
||||
notificationManager.createNotification(
|
||||
WireGuardNotification.NotificationChannels.AUTO_TUNNEL,
|
||||
title = getString(R.string.warning),
|
||||
description =
|
||||
getString(R.string.location_permissions_missing),
|
||||
)
|
||||
notificationManager.show(
|
||||
NotificationManager.AUTO_TUNNEL_LOCATION_PERMISSION_ID,
|
||||
notification,
|
||||
)
|
||||
}
|
||||
if (
|
||||
!state.locationServicesEnabled &&
|
||||
!locationServicesShown &&
|
||||
state.ssidReadRequired
|
||||
) {
|
||||
locationServicesShown = true
|
||||
val notification =
|
||||
notificationManager.createNotification(
|
||||
WireGuardNotification.NotificationChannels.AUTO_TUNNEL,
|
||||
title = getString(R.string.warning),
|
||||
description =
|
||||
getString(R.string.location_services_not_detected),
|
||||
)
|
||||
notificationManager.show(
|
||||
NotificationManager.AUTO_TUNNEL_LOCATION_SERVICES_ID,
|
||||
notification,
|
||||
)
|
||||
}
|
||||
if (state.locationServicesEnabled || !state.ssidReadRequired) {
|
||||
notificationManager.remove(
|
||||
NotificationManager.AUTO_TUNNEL_LOCATION_SERVICES_ID
|
||||
)
|
||||
locationServicesShown = false
|
||||
}
|
||||
if (state.locationPermissionsEnabled || !state.ssidReadRequired) {
|
||||
notificationManager.remove(
|
||||
NotificationManager.AUTO_TUNNEL_LOCATION_PERMISSION_ID
|
||||
)
|
||||
locationPermissionsShown = false
|
||||
}
|
||||
val wifiMode = state.detectionMethod
|
||||
|
||||
if (
|
||||
wifiMode == AndroidNetworkMonitor.WifiDetectionMethod.DEFAULT ||
|
||||
wifiMode == AndroidNetworkMonitor.WifiDetectionMethod.LEGACY
|
||||
) {
|
||||
|
||||
if (!state.ssidReadRequired) {
|
||||
notificationService.remove(
|
||||
NotificationService.AUTO_TUNNEL_LOCATION_SERVICES_ID
|
||||
)
|
||||
notificationService.remove(
|
||||
NotificationService.AUTO_TUNNEL_LOCATION_PERMISSION_ID
|
||||
)
|
||||
return@collect
|
||||
}
|
||||
|
||||
if (!state.locationPermissionsEnabled) {
|
||||
val notification =
|
||||
notificationService.createNotification(
|
||||
AndroidNotificationService.NotificationChannels.AUTO_TUNNEL,
|
||||
title = getString(R.string.warning),
|
||||
description = getString(R.string.location_permissions_missing),
|
||||
)
|
||||
|
||||
notificationService.show(
|
||||
NotificationService.AUTO_TUNNEL_LOCATION_PERMISSION_ID,
|
||||
notification,
|
||||
)
|
||||
} else {
|
||||
notificationService.remove(
|
||||
NotificationService.AUTO_TUNNEL_LOCATION_PERMISSION_ID
|
||||
)
|
||||
}
|
||||
|
||||
if (!state.locationServicesEnabled) {
|
||||
val notification =
|
||||
notificationService.createNotification(
|
||||
AndroidNotificationService.NotificationChannels.AUTO_TUNNEL,
|
||||
title = getString(R.string.warning),
|
||||
description = getString(R.string.location_services_not_detected),
|
||||
)
|
||||
|
||||
notificationService.show(
|
||||
NotificationService.AUTO_TUNNEL_LOCATION_SERVICES_ID,
|
||||
notification,
|
||||
)
|
||||
} else {
|
||||
notificationService.remove(
|
||||
NotificationService.AUTO_TUNNEL_LOCATION_SERVICES_ID
|
||||
)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleAutoTunnelEvent(autoTunnelEvent: AutoTunnelEvent) {
|
||||
autoTunMutex.withLock {
|
||||
when (
|
||||
val event =
|
||||
autoTunnelEvent.also {
|
||||
Timber.i("Auto tunnel event: ${it.javaClass.simpleName}")
|
||||
}
|
||||
) {
|
||||
is AutoTunnelEvent.Start ->
|
||||
(event.tunnelConfig ?: tunnelsRepository.getDefaultTunnel())?.let {
|
||||
tunnelManager.startTunnel(it)
|
||||
}
|
||||
is AutoTunnelEvent.Stop -> tunnelManager.stopActiveTunnels()
|
||||
AutoTunnelEvent.DoNothing -> Timber.i("Auto-tunneling: nothing to do")
|
||||
private suspend fun handleAutoTunnelEvent(event: AutoTunnelEvent) {
|
||||
when (event) {
|
||||
is AutoTunnelEvent.Sync -> {
|
||||
|
||||
event.stop.forEach { tunnelId ->
|
||||
Timber.d("Stopping tunnel: $tunnelId")
|
||||
tunnelCoordinator.stopTunnel(tunnelId, TunnelActionSource.AUTO_TUNNEL)
|
||||
}
|
||||
|
||||
event.start.forEach { config ->
|
||||
Timber.d("Starting tunnel: ${config.name}")
|
||||
tunnelCoordinator.startTunnel(config, TunnelActionSource.AUTO_TUNNEL)
|
||||
}
|
||||
}
|
||||
|
||||
AutoTunnelEvent.DoNothing -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
|
||||
private val debouncedConnectivityStateFlow: Flow<ConnectivityState> by lazy {
|
||||
autoTunnelRepository
|
||||
.get()
|
||||
.flow
|
||||
.map { it.debounceDelaySeconds.toMillis() }
|
||||
.distinctUntilChanged()
|
||||
.flatMapLatest { debounceMillis ->
|
||||
networkMonitor.connectivityStateFlow.debounce(debounceMillis)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val REEVALUATE_CHECK_DELAY = 3_000L
|
||||
}
|
||||
}
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.autotunnel
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
class AutoTunnelStateHolder {
|
||||
|
||||
private val _active = MutableStateFlow(false)
|
||||
val active: StateFlow<Boolean> = _active
|
||||
|
||||
fun setActive(active: Boolean) {
|
||||
_active.value = active
|
||||
}
|
||||
}
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.autotunnel
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.AutoTunnelSettings
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.NetworkState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||
|
||||
sealed interface StateChange
|
||||
|
||||
data class NetworkChange(val networkState: NetworkState) : StateChange
|
||||
|
||||
data class SettingsChange(
|
||||
val appMode: AppMode,
|
||||
val settings: AutoTunnelSettings,
|
||||
val tunnels: List<TunnelConfig>,
|
||||
) : StateChange
|
||||
|
||||
data class ActiveTunnelsChange(val activeTunnels: Map<Int, TunnelState>) : StateChange
|
||||
+48
-76
@@ -1,107 +1,79 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.tile
|
||||
|
||||
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 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 com.zaneschepke.wireguardautotunnel.core.orchestration.AutoTunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelStateHolder
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class AutoTunnelControlTile : TileService(), LifecycleOwner {
|
||||
class AutoTunnelControlTile : TileService() {
|
||||
|
||||
@Inject lateinit var autoTunnelSettingsRepository: AutoTunnelSettingsRepository
|
||||
@Inject lateinit var tunnelsRepository: TunnelRepository
|
||||
private val autoTunnelStateHolder: AutoTunnelStateHolder by inject()
|
||||
private val autoTunnelCoordinator: AutoTunnelCoordinator by inject()
|
||||
|
||||
@Inject lateinit var serviceManager: ServiceManager
|
||||
private val tileScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
|
||||
|
||||
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
|
||||
}
|
||||
private var observerJob: Job? = null
|
||||
|
||||
override fun onDestroy() {
|
||||
tileScope.cancel()
|
||||
super.onDestroy()
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
|
||||
}
|
||||
|
||||
override fun onStartListening() {
|
||||
super.onStartListening()
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
updateTileState()
|
||||
startObserving()
|
||||
}
|
||||
|
||||
override fun onStopListening() {
|
||||
super.onStopListening()
|
||||
observerJob?.cancel()
|
||||
observerJob = null
|
||||
}
|
||||
|
||||
override fun onTileAdded() {
|
||||
super.onTileAdded()
|
||||
updateTileState()
|
||||
startObserving()
|
||||
}
|
||||
|
||||
override fun onClick() {
|
||||
super.onClick()
|
||||
unlockAndRun {
|
||||
lifecycleScope.launch {
|
||||
if (serviceManager.autoTunnelService.value != null) {
|
||||
autoTunnelSettingsRepository.updateAutoTunnelEnabled(false)
|
||||
setInactive()
|
||||
} else {
|
||||
autoTunnelSettingsRepository.updateAutoTunnelEnabled(true)
|
||||
setActive()
|
||||
}
|
||||
unlockAndRun { tileScope.launch { autoTunnelCoordinator.toggle() } }
|
||||
}
|
||||
|
||||
private fun updateTileState() {
|
||||
val isActive = autoTunnelStateHolder.active.value
|
||||
if (isActive) setActive() else setInactive()
|
||||
}
|
||||
|
||||
private fun startObserving() {
|
||||
if (observerJob?.isActive == true) return
|
||||
|
||||
observerJob = tileScope.launch {
|
||||
autoTunnelStateHolder.active.collect { active ->
|
||||
if (active) setActive() else setInactive()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setActive() {
|
||||
runCatching {
|
||||
qsTile.state = Tile.STATE_ACTIVE
|
||||
qsTile.updateTile()
|
||||
qsTile?.apply {
|
||||
state = Tile.STATE_ACTIVE
|
||||
updateTile()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setInactive() {
|
||||
runCatching {
|
||||
qsTile.state = Tile.STATE_INACTIVE
|
||||
qsTile.updateTile()
|
||||
qsTile?.apply {
|
||||
state = Tile.STATE_INACTIVE
|
||||
updateTile()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setUnavailable() {
|
||||
runCatching {
|
||||
qsTile.state = Tile.STATE_UNAVAILABLE
|
||||
qsTile.updateTile()
|
||||
}
|
||||
}
|
||||
|
||||
/* This works around an annoying unsolved frameworks bug some people are hitting. */
|
||||
override fun onBind(intent: Intent): IBinder? {
|
||||
var ret: IBinder? = null
|
||||
try {
|
||||
ret = super.onBind(intent)
|
||||
} catch (_: Throwable) {
|
||||
Timber.e("Failed to bind to AutoTunnelControlTile")
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
override val lifecycle: Lifecycle
|
||||
get() = lifecycleRegistry
|
||||
}
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.tile
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.service.quicksettings.TileService
|
||||
|
||||
object AutoTunnelTileRefresher : TileRefresher {
|
||||
override fun refresh(context: Context) {
|
||||
TileService.requestListeningState(
|
||||
context,
|
||||
ComponentName(context, AutoTunnelControlTile::class.java),
|
||||
)
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.tile
|
||||
|
||||
import android.content.Context
|
||||
|
||||
interface TileRefresher {
|
||||
fun refresh(context: Context)
|
||||
}
|
||||
+112
-136
@@ -1,184 +1,160 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.tile
|
||||
|
||||
import android.content.Intent
|
||||
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 com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class TunnelControlTile : TileService(), LifecycleOwner {
|
||||
class TunnelControlTile : TileService() {
|
||||
|
||||
@Inject lateinit var tunnelsRepository: TunnelRepository
|
||||
private val tunnelsRepository: TunnelRepository by inject()
|
||||
private val tunnelCoordinator: TunnelCoordinator by inject()
|
||||
|
||||
@Inject lateinit var serviceManager: ServiceManager
|
||||
private var collectionJob: Job? = null
|
||||
|
||||
@Inject lateinit var tunnelManager: TunnelManager
|
||||
private val tileScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
|
||||
|
||||
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
|
||||
|
||||
private var isCollecting = false
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
|
||||
}
|
||||
@Volatile private var observing = false
|
||||
|
||||
override fun onDestroy() {
|
||||
tileScope.cancel()
|
||||
super.onDestroy()
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
|
||||
}
|
||||
|
||||
override fun onTileAdded() {
|
||||
super.onTileAdded()
|
||||
updateTileState()
|
||||
startObserving()
|
||||
}
|
||||
|
||||
override fun onStartListening() {
|
||||
super.onStartListening()
|
||||
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() } }
|
||||
updateTileState()
|
||||
startObserving()
|
||||
}
|
||||
|
||||
private suspend fun updateTileState() {
|
||||
try {
|
||||
val tunnels = tunnelsRepository.getAll()
|
||||
if (tunnels.isEmpty()) {
|
||||
setUnavailable()
|
||||
return
|
||||
}
|
||||
private fun startObserving() {
|
||||
if (observing) return
|
||||
observing = true
|
||||
|
||||
val activeTunnels =
|
||||
tunnelManager.activeTunnels.value.filter { it.value.status.isUpOrStarting() }
|
||||
collectionJob = tileScope.launch {
|
||||
val tunnels = withContext(Dispatchers.IO) { tunnelsRepository.getAll() }
|
||||
|
||||
when {
|
||||
activeTunnels.isNotEmpty() -> {
|
||||
val activeIds = activeTunnels.map { it.key }
|
||||
// TODO improvements would be needed to make this work well with toggling
|
||||
// multiple tunnels
|
||||
// this would be better managed elsewhere
|
||||
WireGuardAutoTunnel.setLastActiveTunnels(activeIds)
|
||||
val activeTunNames =
|
||||
tunnels.filter { activeTunnels.keys.contains(it.id) }.map { it.name }
|
||||
updateTileForActiveTunnels(activeTunNames)
|
||||
tunnelCoordinator.backendStatus
|
||||
.distinctUntilChangedBy { it.activeTunnels.keys }
|
||||
.collect { status ->
|
||||
if (tunnels.isEmpty()) {
|
||||
setUnavailable()
|
||||
return@collect
|
||||
}
|
||||
|
||||
val active = status.activeTunnels
|
||||
|
||||
if (active.isNotEmpty()) {
|
||||
val names = tunnels.filter { active.containsKey(it.id) }.map { it.name }
|
||||
|
||||
setActive(names)
|
||||
} else {
|
||||
setInactive()
|
||||
}
|
||||
}
|
||||
else -> updateTileForLastActiveTunnels()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
setUnavailable()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateTileForActiveTunnels(activeTunnelNames: List<String>) {
|
||||
val tileName =
|
||||
when (activeTunnelNames.size) {
|
||||
1 -> activeTunnelNames[0]
|
||||
else -> getString(R.string.multiple)
|
||||
}
|
||||
updateTile(tileName, true)
|
||||
}
|
||||
|
||||
private suspend fun updateTileForLastActiveTunnels() {
|
||||
val lastActiveIds = WireGuardAutoTunnel.getLastActiveTunnels()
|
||||
when {
|
||||
lastActiveIds.isEmpty() -> {
|
||||
tunnelsRepository.getStartTunnel()?.let { config -> updateTile(config.name, false) }
|
||||
?: setUnavailable()
|
||||
}
|
||||
lastActiveIds.size > 1 -> updateTile(getString(R.string.multiple), false)
|
||||
else -> {
|
||||
val tunnelId = lastActiveIds.first()
|
||||
tunnelsRepository.getById(tunnelId)?.let { tunnel ->
|
||||
updateTile(tunnel.name, false)
|
||||
} ?: setUnavailable()
|
||||
}
|
||||
}
|
||||
override fun onStopListening() {
|
||||
super.onStopListening()
|
||||
observing = false
|
||||
collectionJob?.cancel()
|
||||
collectionJob = null
|
||||
}
|
||||
|
||||
override fun onClick() {
|
||||
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) }
|
||||
}
|
||||
}
|
||||
tileScope.launch {
|
||||
tunnelCoordinator.toggleTunnels()
|
||||
updateTileState()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setActive() {
|
||||
runCatching {
|
||||
qsTile.state = Tile.STATE_ACTIVE
|
||||
qsTile.updateTile()
|
||||
private fun updateTileState() {
|
||||
tileScope.launch {
|
||||
val tunnels = tunnelsRepository.getAll()
|
||||
|
||||
if (tunnels.isEmpty()) {
|
||||
setUnavailable()
|
||||
return@launch
|
||||
}
|
||||
|
||||
val active = tunnelCoordinator.backendStatus.value.activeTunnels
|
||||
|
||||
if (active.isNotEmpty()) {
|
||||
val names = tunnels.filter { active.containsKey(it.id) }.map { it.name }
|
||||
|
||||
setActive(names)
|
||||
} else {
|
||||
setInactive()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setActive(names: List<String>) {
|
||||
val label =
|
||||
when {
|
||||
names.isEmpty() -> ""
|
||||
names.size == 1 -> names.first()
|
||||
names.size <= 3 -> names.joinToString(", ")
|
||||
else -> {
|
||||
val visible = names.take(2).joinToString(", ")
|
||||
"$visible +${names.size - 2}"
|
||||
}
|
||||
}
|
||||
|
||||
qsTile?.apply {
|
||||
state = Tile.STATE_ACTIVE
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
subtitle = label
|
||||
}
|
||||
contentDescription = label
|
||||
|
||||
updateTile()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setInactive() {
|
||||
runCatching {
|
||||
qsTile.state = Tile.STATE_INACTIVE
|
||||
qsTile.updateTile()
|
||||
qsTile?.apply {
|
||||
state = Tile.STATE_INACTIVE
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
subtitle = ""
|
||||
}
|
||||
contentDescription = ""
|
||||
|
||||
updateTile()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setUnavailable() {
|
||||
runCatching {
|
||||
qsTile.state = Tile.STATE_UNAVAILABLE
|
||||
setTileDescription("")
|
||||
qsTile.updateTile()
|
||||
}
|
||||
}
|
||||
qsTile?.apply {
|
||||
state = Tile.STATE_UNAVAILABLE
|
||||
|
||||
private fun setTileDescription(description: String) {
|
||||
runCatching {
|
||||
if (qsTile == null) return@runCatching
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
qsTile.subtitle = description
|
||||
qsTile.stateDescription = description
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
qsTile.subtitle = description
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
subtitle = ""
|
||||
}
|
||||
qsTile.updateTile()
|
||||
contentDescription = ""
|
||||
|
||||
updateTile()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateTile(name: String, active: Boolean) {
|
||||
runCatching {
|
||||
setTileDescription(name)
|
||||
if (active) return setActive()
|
||||
setInactive()
|
||||
}
|
||||
.onFailure { Timber.e(it) }
|
||||
}
|
||||
|
||||
/* This works around an annoying unsolved frameworks bug some people are hitting. */
|
||||
override fun onBind(intent: Intent): IBinder? {
|
||||
var ret: IBinder? = null
|
||||
try {
|
||||
ret = super.onBind(intent)
|
||||
} catch (_: Throwable) {
|
||||
Timber.e("Failed to bind to TunnelControlTile")
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
override val lifecycle: Lifecycle
|
||||
get() = lifecycleRegistry
|
||||
}
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.tile
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.service.quicksettings.TileService
|
||||
|
||||
object TunnelTileRefresher : TileRefresher {
|
||||
override fun refresh(context: Context) {
|
||||
TileService.requestListeningState(
|
||||
context,
|
||||
ComponentName(context, TunnelControlTile::class.java),
|
||||
)
|
||||
}
|
||||
}
|
||||
+5
-6
@@ -6,13 +6,12 @@ import androidx.core.content.pm.ShortcutInfoCompat
|
||||
import androidx.core.content.pm.ShortcutManagerCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class DynamicShortcutManager(
|
||||
private val context: Context,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
private val ioDispatcher: CoroutineDispatcher,
|
||||
) : ShortcutManager {
|
||||
override suspend fun addShortcuts() {
|
||||
withContext(ioDispatcher) {
|
||||
@@ -35,7 +34,7 @@ class DynamicShortcutManager(
|
||||
intent =
|
||||
Intent(context, ShortcutsActivity::class.java).apply {
|
||||
putExtra("className", "WireGuardTunnelService")
|
||||
action = ShortcutsActivity.Action.STOP.name
|
||||
action = ShortcutContract.Action.STOP.name
|
||||
},
|
||||
shortcutIcon = R.drawable.vpn_off,
|
||||
),
|
||||
@@ -46,7 +45,7 @@ class DynamicShortcutManager(
|
||||
intent =
|
||||
Intent(context, ShortcutsActivity::class.java).apply {
|
||||
putExtra("className", "WireGuardTunnelService")
|
||||
action = ShortcutsActivity.Action.START.name
|
||||
action = ShortcutContract.Action.START.name
|
||||
},
|
||||
shortcutIcon = R.drawable.vpn_on,
|
||||
),
|
||||
@@ -57,7 +56,7 @@ class DynamicShortcutManager(
|
||||
intent =
|
||||
Intent(context, ShortcutsActivity::class.java).apply {
|
||||
putExtra("className", "WireGuardConnectivityWatcherService")
|
||||
action = ShortcutsActivity.Action.START.name
|
||||
action = ShortcutContract.Action.START.name
|
||||
},
|
||||
shortcutIcon = R.drawable.auto_play,
|
||||
),
|
||||
@@ -68,7 +67,7 @@ class DynamicShortcutManager(
|
||||
intent =
|
||||
Intent(context, ShortcutsActivity::class.java).apply {
|
||||
putExtra("className", "WireGuardConnectivityWatcherService")
|
||||
action = ShortcutsActivity.Action.STOP.name
|
||||
action = ShortcutContract.Action.STOP.name
|
||||
},
|
||||
shortcutIcon = R.drawable.auto_pause,
|
||||
),
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.shortcut
|
||||
|
||||
object ShortcutContract {
|
||||
|
||||
const val EXTRA_SHORTCUT_TYPE = "com.zaneschepke.wireguardautotunnel.shortcut.TYPE"
|
||||
|
||||
const val EXTRA_TUNNEL_NAME = "tunnelName"
|
||||
|
||||
const val EXTRA_CLASS_NAME = "className"
|
||||
|
||||
enum class ShortcutType(val value: String) {
|
||||
TUNNEL("tunnel"),
|
||||
AUTO_TUNNEL("auto_tunnel"),
|
||||
}
|
||||
|
||||
enum class Action {
|
||||
START,
|
||||
STOP,
|
||||
}
|
||||
|
||||
object Legacy {
|
||||
|
||||
const val TUNNEL_PROVIDER_NAME = "TunnelProvider"
|
||||
|
||||
const val AUTO_TUNNEL_SERVICE_CLASS_NAME = "AutoTunnelService"
|
||||
|
||||
const val TUNNEL_SERVICE_NAME = "WireGuardTunnelService"
|
||||
|
||||
const val AUTO_TUNNEL_SERVICE_NAME = "WireGuardConnectivityWatcherService"
|
||||
}
|
||||
}
|
||||
+10
-60
@@ -2,76 +2,26 @@ package com.zaneschepke.wireguardautotunnel.core.shortcut
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelService
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.ShortcutCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.di.Scope
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.core.qualifier.named
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ShortcutsActivity : ComponentActivity() {
|
||||
|
||||
@Inject lateinit var settingsRepository: GeneralSettingRepository
|
||||
@Inject lateinit var autoTunnelSettingsRepository: AutoTunnelSettingsRepository
|
||||
@Inject lateinit var tunnelsRepository: TunnelRepository
|
||||
private val shortcutCoordinator: ShortcutCoordinator by inject()
|
||||
|
||||
@Inject lateinit var tunnelManager: TunnelManager
|
||||
|
||||
@Inject @ApplicationScope lateinit var applicationScope: CoroutineScope
|
||||
private val applicationScope: CoroutineScope by inject(named(Scope.APPLICATION))
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
applicationScope.launch {
|
||||
val settings = settingsRepository.getGeneralSettings()
|
||||
if (settings.isShortcutsEnabled) {
|
||||
when (intent.getStringExtra(CLASS_NAME_EXTRA_KEY)) {
|
||||
LEGACY_TUNNEL_SERVICE_NAME,
|
||||
TunnelProvider::class.java.simpleName -> {
|
||||
val tunnelName = intent.getStringExtra(TUNNEL_NAME_EXTRA_KEY)
|
||||
Timber.d("Tunnel name extra: $tunnelName")
|
||||
val tunnelConfig =
|
||||
tunnelName?.let { tunnelsRepository.findByTunnelName(it) }
|
||||
?: tunnelsRepository.getDefaultTunnel()
|
||||
Timber.d("Shortcut action on name: ${tunnelConfig?.name}")
|
||||
tunnelConfig?.let {
|
||||
when (intent.action) {
|
||||
Action.START.name -> tunnelManager.startTunnel(it)
|
||||
Action.STOP.name -> tunnelManager.stopActiveTunnels()
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
AutoTunnelService::class.java.simpleName,
|
||||
LEGACY_AUTO_TUNNEL_SERVICE_NAME -> {
|
||||
when (intent.action) {
|
||||
Action.START.name ->
|
||||
autoTunnelSettingsRepository.updateAutoTunnelEnabled(true)
|
||||
Action.STOP.name ->
|
||||
autoTunnelSettingsRepository.updateAutoTunnelEnabled(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
shortcutCoordinator.handle(intent)
|
||||
finish()
|
||||
}
|
||||
finish()
|
||||
}
|
||||
|
||||
enum class Action {
|
||||
START,
|
||||
STOP,
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val LEGACY_TUNNEL_SERVICE_NAME = "WireGuardTunnelService"
|
||||
const val LEGACY_AUTO_TUNNEL_SERVICE_NAME = "WireGuardConnectivityWatcherService"
|
||||
const val TUNNEL_NAME_EXTRA_KEY = "tunnelName"
|
||||
const val CLASS_NAME_EXTRA_KEY = "className"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.LogHealthState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.PingState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import timber.log.Timber
|
||||
|
||||
abstract class BaseTunnel(
|
||||
@ApplicationScope protected val applicationScope: CoroutineScope,
|
||||
@IoDispatcher protected val ioDispatcher: CoroutineDispatcher,
|
||||
) : TunnelProvider {
|
||||
|
||||
protected val errors = MutableSharedFlow<Pair<String, BackendCoreException>>()
|
||||
override val errorEvents = errors.asSharedFlow()
|
||||
|
||||
private val _messageEvents = MutableSharedFlow<Pair<String, BackendMessage>>()
|
||||
override val messageEvents = _messageEvents.asSharedFlow()
|
||||
|
||||
protected val activeTuns = MutableStateFlow<Map<Int, TunnelState>>(emptyMap())
|
||||
override val activeTunnels = activeTuns.asStateFlow()
|
||||
|
||||
protected val tunJobs = ConcurrentHashMap<Int, Job>()
|
||||
private val tunMutex = Mutex()
|
||||
private val tunStatusMutex = Mutex()
|
||||
|
||||
abstract fun tunnelStateFlow(tunnelConfig: TunnelConfig): Flow<TunnelStatus>
|
||||
|
||||
abstract override fun setBackendMode(backendMode: BackendMode)
|
||||
|
||||
abstract override fun getBackendMode(): BackendMode
|
||||
|
||||
abstract override suspend fun forceStopTunnel(tunnelId: Int)
|
||||
|
||||
abstract override fun handleDnsReresolve(tunnelConfig: TunnelConfig): Boolean
|
||||
|
||||
abstract override fun getStatistics(tunnelId: Int): TunnelStatistics?
|
||||
|
||||
override suspend fun updateTunnelStatus(
|
||||
tunnelId: Int,
|
||||
status: TunnelStatus?,
|
||||
stats: TunnelStatistics?,
|
||||
pingStates: Map<String, PingState>?,
|
||||
logHealthState: LogHealthState?,
|
||||
) {
|
||||
tunStatusMutex.withLock {
|
||||
activeTuns.update { currentTuns ->
|
||||
if (!currentTuns.containsKey(tunnelId) && status != TunnelStatus.Starting) {
|
||||
Timber.d("Ignoring update for inactive tunnel $tunnelId")
|
||||
return@update currentTuns
|
||||
}
|
||||
val existingState = currentTuns[tunnelId] ?: TunnelState()
|
||||
val newStatus = status ?: existingState.status
|
||||
if (newStatus == TunnelStatus.Down) {
|
||||
Timber.d("Removing tunnel $tunnelId from activeTunnels as state is DOWN")
|
||||
cleanUpTunJob(tunnelId)
|
||||
currentTuns - tunnelId
|
||||
} else if (
|
||||
existingState.status == newStatus &&
|
||||
stats == null &&
|
||||
pingStates == null &&
|
||||
logHealthState == null
|
||||
) {
|
||||
Timber.d("Skipping redundant state update for ${tunnelId}: $newStatus")
|
||||
currentTuns
|
||||
} else {
|
||||
val updated =
|
||||
existingState.copy(
|
||||
status = newStatus,
|
||||
statistics = stats ?: existingState.statistics,
|
||||
pingStates = pingStates ?: existingState.pingStates,
|
||||
logHealthState = logHealthState ?: existingState.logHealthState,
|
||||
)
|
||||
currentTuns + (tunnelId to updated)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun stopActiveTunnels() {
|
||||
activeTunnels.value.forEach { (config, state) ->
|
||||
if (state.status.isUpOrStarting()) {
|
||||
stopTunnel(config)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun startTunnel(tunnelConfig: TunnelConfig) {
|
||||
tunMutex.withLock {
|
||||
if (
|
||||
activeTuns.value.containsKey(tunnelConfig.id) ||
|
||||
tunJobs.containsKey(tunnelConfig.id)
|
||||
) {
|
||||
return Timber.w("Tunnel is already running: ${tunnelConfig.name}")
|
||||
}
|
||||
val job =
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
try {
|
||||
tunnelStateFlow(tunnelConfig).collect { status ->
|
||||
updateTunnelStatus(tunnelConfig.id, status)
|
||||
}
|
||||
} catch (e: BackendCoreException) {
|
||||
errors.emit(tunnelConfig.name to e)
|
||||
updateTunnelStatus(tunnelConfig.id, TunnelStatus.Down)
|
||||
} catch (_: CancellationException) {}
|
||||
}
|
||||
tunJobs[tunnelConfig.id] = job
|
||||
job.invokeOnCompletion {
|
||||
tunJobs.remove(tunnelConfig.id)
|
||||
activeTuns.update { it - tunnelConfig.id }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun stopTunnel(tunnelId: Int) {
|
||||
tunMutex.withLock {
|
||||
val currentState = activeTuns.value[tunnelId]?.status ?: return@withLock
|
||||
updateTunnelStatus(tunnelId, TunnelStatus.Stopping)
|
||||
tunJobs[tunnelId]?.cancel()
|
||||
|
||||
withTimeoutOrNull(STOP_TIMEOUT_MS) {
|
||||
activeTunnels.first {
|
||||
!it.containsKey(tunnelId) || it[tunnelId]!!.status == TunnelStatus.Down
|
||||
}
|
||||
}
|
||||
?: run {
|
||||
Timber.w("Stop timeout for $tunnelId (was $currentState); forcing kill")
|
||||
forceStopTunnel(tunnelId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun cleanUpTunJob(tunnelId: Int) {
|
||||
Timber.d("Removing job for $tunnelId")
|
||||
tunJobs -= tunnelId
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val STARTUP_TIMEOUT_MS: Long = 15_000L
|
||||
const val STOP_TIMEOUT_MS: Long = 5_000L
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
fun Map<TunnelConfig, TunnelState>.allDown(): Boolean {
|
||||
return this.all { it.value.status.isDown() }
|
||||
}
|
||||
|
||||
fun Map<TunnelConfig, TunnelState>.hasActive(): Boolean {
|
||||
return this.any { it.value.status.isUp() }
|
||||
}
|
||||
|
||||
fun Map<TunnelConfig, TunnelState>.getValueById(id: Int): TunnelState? {
|
||||
val key = this.keys.find { it.id == id }
|
||||
return key?.let { this@getValueById[it] }
|
||||
}
|
||||
|
||||
fun Map<TunnelConfig, TunnelState>.getKeyById(id: Int): TunnelConfig? {
|
||||
return this.keys.find { it.id == id }
|
||||
}
|
||||
|
||||
fun Map<TunnelConfig, TunnelState>.isUp(tunnelConfig: TunnelConfig): Boolean {
|
||||
return this.getValueById(tunnelConfig.id)?.status?.isUp() ?: false
|
||||
}
|
||||
|
||||
fun MutableStateFlow<Map<TunnelConfig, TunnelState>>.exists(id: Int): Boolean {
|
||||
return this.value.any { it.key.id == id }
|
||||
}
|
||||
|
||||
fun MutableStateFlow<Map<TunnelConfig, TunnelState>>.isUp(id: Int): Boolean {
|
||||
return this.value.any { it.key.id == id && it.value.status == TunnelStatus.Up }
|
||||
}
|
||||
|
||||
fun MutableStateFlow<Map<TunnelConfig, TunnelState>>.isStarting(id: Int): Boolean {
|
||||
return this.value.any { it.key.id == id && it.value.status == TunnelStatus.Starting }
|
||||
}
|
||||
|
||||
fun MutableStateFlow<Map<TunnelConfig, TunnelState>>.findTunnel(id: Int): TunnelConfig? {
|
||||
return this.value.keys.find { it.id == id }
|
||||
}
|
||||
|
||||
private val URL_PATTERN =
|
||||
Regex("""^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}:[0-9]{1,5}$""")
|
||||
|
||||
fun String.isUrl(): Boolean {
|
||||
return URL_PATTERN.matches(this)
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||
|
||||
import com.wireguard.android.backend.Backend
|
||||
import com.wireguard.android.backend.BackendException
|
||||
import com.wireguard.android.backend.Tunnel as WgTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.di.Kernel
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.DnsFailure
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.InvalidConfig
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.KernelTunnelName
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.UnknownError
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.WireGuardStatistics
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendCoreException
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.regex.Pattern
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.consumeAsFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import timber.log.Timber
|
||||
|
||||
class KernelTunnel
|
||||
@Inject
|
||||
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>()
|
||||
|
||||
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 {
|
||||
validateWireGuardInterfaceName(tunnelConfig.name).onFailure { close(it) }
|
||||
|
||||
val stateChannel = Channel<WgTunnel.State>()
|
||||
|
||||
val runtimeTunnel = RuntimeWgTunnel(tunnelConfig, stateChannel)
|
||||
runtimeTunnels[tunnelConfig.id] = runtimeTunnel
|
||||
|
||||
val consumerJob = launch {
|
||||
stateChannel.consumeAsFlow().collect { state -> trySend(state.asTunnelState()) }
|
||||
}
|
||||
|
||||
try {
|
||||
withTimeout(STARTUP_TIMEOUT_MS) {
|
||||
updateTunnelStatus(tunnelConfig.id, TunnelStatus.Starting)
|
||||
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 DnsFailure())
|
||||
forceStopTunnel(tunnelConfig.id)
|
||||
close()
|
||||
} catch (e: BackendException) {
|
||||
close(e.toBackendCoreException())
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Timber.e(e, "Invalid backend arguments")
|
||||
close(InvalidConfig())
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Error while setting tunnel state")
|
||||
close(UnknownError())
|
||||
}
|
||||
|
||||
awaitClose {
|
||||
try {
|
||||
backend.setState(runtimeTunnel, WgTunnel.State.DOWN, null)
|
||||
} catch (e: BackendException) {
|
||||
errors.tryEmit(tunnelConfig.name to e.toBackendCoreException())
|
||||
} finally {
|
||||
consumerJob.cancel()
|
||||
stateChannel.close()
|
||||
runtimeTunnels.remove(tunnelConfig.id)
|
||||
trySend(TunnelStatus.Down)
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getStatistics(tunnelId: Int): TunnelStatistics? {
|
||||
return try {
|
||||
val runtimeTunnel = runtimeTunnels[tunnelId] ?: return null
|
||||
WireGuardStatistics(backend.getStatistics(runtimeTunnel))
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Failed to get stats for $tunnelId")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun setBackendMode(backendMode: BackendMode) {
|
||||
Timber.w("Not yet implemented for kernel")
|
||||
}
|
||||
|
||||
override fun getBackendMode(): BackendMode {
|
||||
return BackendMode.Inactive
|
||||
}
|
||||
|
||||
override fun handleDnsReresolve(tunnelConfig: TunnelConfig): Boolean {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
override suspend fun runningTunnelNames(): Set<String> {
|
||||
return backend.runningTunnelNames
|
||||
}
|
||||
|
||||
override suspend fun forceStopTunnel(tunnelId: Int) {
|
||||
val runtimeTunnel = runtimeTunnels[tunnelId] ?: return
|
||||
try {
|
||||
backend.setState(runtimeTunnel, WgTunnel.State.DOWN, null)
|
||||
} catch (e: BackendException) {
|
||||
Timber.e(e, "Force stop failed for $tunnelId")
|
||||
} finally {
|
||||
tunJobs[tunnelId]?.cancel()
|
||||
runtimeTunnels.remove(tunnelId)
|
||||
tunJobs.remove(tunnelId)
|
||||
activeTuns.update { it - tunnelId }
|
||||
updateTunnelStatus(tunnelId, TunnelStatus.Down)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import org.amnezia.awg.backend.Tunnel
|
||||
|
||||
class RuntimeAwgTunnel(
|
||||
private val tunnelConfig: TunnelConfig,
|
||||
private val stateChannel: Channel<Tunnel.State>,
|
||||
) : Tunnel {
|
||||
|
||||
override fun getName() = tunnelConfig.name
|
||||
|
||||
override fun onStateChange(newState: Tunnel.State) {
|
||||
stateChannel.trySend(newState)
|
||||
}
|
||||
|
||||
override fun isIpv4ResolutionPreferred() = tunnelConfig.isIpv4Preferred
|
||||
|
||||
override fun isMetered() = tunnelConfig.isMetered
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
|
||||
class RuntimeWgTunnel(
|
||||
private val config: TunnelConfig,
|
||||
private val stateChannel: Channel<Tunnel.State>,
|
||||
) : Tunnel {
|
||||
|
||||
override fun getName() = config.name
|
||||
|
||||
override fun onStateChange(newState: Tunnel.State) {
|
||||
stateChannel.trySend(newState)
|
||||
}
|
||||
|
||||
override fun isIpv4ResolutionPreferred() = config.isIpv4Preferred
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||
|
||||
import com.zaneschepke.tunnel.Tunnel
|
||||
import com.zaneschepke.tunnel.backend.Backend
|
||||
import com.zaneschepke.tunnel.model.BackendMode
|
||||
import com.zaneschepke.tunnel.state.BackendStatus
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.LockdownSettings
|
||||
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.plus
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class, ExperimentalAtomicApi::class)
|
||||
class TunnelBackendProvider(
|
||||
private val backend: Backend,
|
||||
applicationScope: CoroutineScope,
|
||||
ioDispatcher: CoroutineDispatcher,
|
||||
) : TunnelProvider {
|
||||
|
||||
override val backendStatus: StateFlow<BackendStatus> =
|
||||
backend.status.stateIn(
|
||||
scope = applicationScope.plus(ioDispatcher),
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = BackendStatus(),
|
||||
)
|
||||
|
||||
override val events = backend.events
|
||||
|
||||
override suspend fun startTunnel(tunnel: Tunnel, mode: BackendMode): Result<Unit> {
|
||||
return backend.start(tunnel = tunnel, mode = mode)
|
||||
}
|
||||
|
||||
override suspend fun stopTunnel(tunnelId: Int): Result<Unit> {
|
||||
return backend.stop(tunnelId)
|
||||
}
|
||||
|
||||
override suspend fun stopActiveTunnels(): Result<Unit> {
|
||||
return backend.stopAllActiveTunnels()
|
||||
}
|
||||
|
||||
override suspend fun setLockDown(settings: LockdownSettings): Result<Unit> {
|
||||
return backend.setKillSwitch(settings.toKillSwitchConfig())
|
||||
}
|
||||
|
||||
override suspend fun disableLockDown(): Result<Unit> {
|
||||
return backend.disableKillSwitch()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val localErrorEvents = MutableSharedFlow<Pair<String?, BackendCoreException>>()
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val localMessageEvents = MutableSharedFlow<Pair<String?, BackendMessage>>()
|
||||
}
|
||||
@@ -1,558 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
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
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.inject.Inject
|
||||
import kotlin.concurrent.atomics.AtomicBoolean
|
||||
import kotlin.concurrent.atomics.AtomicReference
|
||||
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import timber.log.Timber
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class, ExperimentalAtomicApi::class)
|
||||
class TunnelManager
|
||||
@Inject
|
||||
constructor(
|
||||
@Kernel private val kernelTunnel: TunnelProvider,
|
||||
@Userspace private val userspaceTunnel: TunnelProvider,
|
||||
@ProxyUserspace private val proxyUserspaceTunnel: TunnelProvider,
|
||||
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,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
) : TunnelProvider {
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val localErrorEvents = MutableSharedFlow<Pair<String?, BackendCoreException>>()
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val localMessageEvents = MutableSharedFlow<Pair<String?, BackendMessage>>()
|
||||
|
||||
private val monitoringMutex = Mutex()
|
||||
private val monitoringJobs = ConcurrentHashMap<Int, Job>()
|
||||
|
||||
private val ddnsMutex = Mutex()
|
||||
private val ddnsJobs = ConcurrentHashMap<Int, Job>()
|
||||
|
||||
private data class SideEffectState(
|
||||
val activeTuns: Map<Int, TunnelState>,
|
||||
val tuns: List<TunnelConfig>,
|
||||
val settings: GeneralSettings,
|
||||
val previouslyActive: Map<Int, TunnelState>,
|
||||
)
|
||||
|
||||
private data class SideEffectWithCondition(
|
||||
val effect: suspend (SideEffectState) -> Unit,
|
||||
val condition: (SideEffectState) -> Boolean,
|
||||
)
|
||||
|
||||
private val tunnelProviderFlow: StateFlow<TunnelProvider> = run {
|
||||
val currentBackend = AtomicReference(userspaceTunnel)
|
||||
val currentSettings = AtomicReference(GeneralSettings())
|
||||
val initialEmit = AtomicBoolean(true)
|
||||
|
||||
settingsRepository.flow
|
||||
.filterNotNull()
|
||||
// ignore default state
|
||||
.filterNot { it == GeneralSettings() }
|
||||
.distinctUntilChangedBy { it.appMode }
|
||||
.map { settings ->
|
||||
Timber.d("App mode changes with ${settings.appMode}")
|
||||
val backend =
|
||||
when (settings.appMode) {
|
||||
AppMode.VPN -> userspaceTunnel
|
||||
AppMode.PROXY -> proxyUserspaceTunnel
|
||||
AppMode.LOCK_DOWN -> proxyUserspaceTunnel
|
||||
AppMode.KERNEL -> kernelTunnel
|
||||
}
|
||||
settings to backend
|
||||
}
|
||||
.onEach { (settings, newBackend) ->
|
||||
val isInitialEmit = initialEmit.exchange(false)
|
||||
val previousBackend = currentBackend.exchange(newBackend)
|
||||
val previousSettings = currentSettings.exchange(settings)
|
||||
|
||||
if ((previousSettings.appMode != settings.appMode) && !isInitialEmit) {
|
||||
handleModeChangeCleanup(previousBackend, previousSettings.appMode)
|
||||
}
|
||||
if (settings.appMode == AppMode.LOCK_DOWN) {
|
||||
handleLockDownModeInit()
|
||||
}
|
||||
}
|
||||
.map { (_, backend) -> backend }
|
||||
.stateIn(
|
||||
scope = applicationScope.plus(ioDispatcher),
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = userspaceTunnel,
|
||||
)
|
||||
}
|
||||
|
||||
override val activeTunnels: StateFlow<Map<Int, TunnelState>> = run {
|
||||
val activeTunsReference: AtomicReference<Map<Int, TunnelState>> =
|
||||
AtomicReference(emptyMap())
|
||||
|
||||
tunnelProviderFlow
|
||||
.flatMapLatest { backend ->
|
||||
combine(
|
||||
backend.activeTunnels,
|
||||
tunnelsRepository.flow,
|
||||
settingsRepository.flow.filterNotNull(),
|
||||
) { activeTuns, tuns, settings ->
|
||||
Triple(activeTuns, tuns, settings)
|
||||
}
|
||||
}
|
||||
.onStart { handleRestore() }
|
||||
.onEach { (activeTuns, tuns, settings) ->
|
||||
val previouslyActive = activeTunsReference.exchange(activeTuns)
|
||||
val state = SideEffectState(activeTuns, tuns, settings, previouslyActive)
|
||||
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
supervisorScope {
|
||||
val sideEffects =
|
||||
listOf(
|
||||
SideEffectWithCondition(
|
||||
effect = { s ->
|
||||
handleTunnelServiceChange(s.settings.appMode, s.activeTuns)
|
||||
},
|
||||
condition = { s ->
|
||||
s.activeTuns.size != s.previouslyActive.size
|
||||
},
|
||||
),
|
||||
SideEffectWithCondition(
|
||||
effect = { s ->
|
||||
handleTunnelsActiveChange(
|
||||
s.previouslyActive,
|
||||
s.activeTuns,
|
||||
s.tuns,
|
||||
)
|
||||
},
|
||||
condition = { s ->
|
||||
s.activeTuns.size != s.previouslyActive.size
|
||||
},
|
||||
),
|
||||
SideEffectWithCondition(
|
||||
effect = { s ->
|
||||
handleDynamicDnsMonitoring(s.activeTuns, s.tuns, s.settings)
|
||||
},
|
||||
condition = { s ->
|
||||
s.activeTuns.keys != s.previouslyActive.keys
|
||||
},
|
||||
),
|
||||
SideEffectWithCondition(
|
||||
effect = { s ->
|
||||
handleFullTunnelMonitoring(s.activeTuns, s.tuns, s.settings)
|
||||
},
|
||||
condition = { s ->
|
||||
s.activeTuns.keys != s.previouslyActive.keys
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
sideEffects
|
||||
.filter { it.condition(state) }
|
||||
.forEach { sideEffect ->
|
||||
launch {
|
||||
try {
|
||||
sideEffect.effect(state)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Side effect failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.map { (activeTuns, _, _) -> activeTuns }
|
||||
.stateIn(
|
||||
scope = applicationScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = emptyMap(),
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override val errorEvents: SharedFlow<Pair<String?, BackendCoreException>> =
|
||||
merge(localErrorEvents, tunnelProviderFlow.flatMapLatest { it.errorEvents })
|
||||
.shareIn(
|
||||
scope = applicationScope + ioDispatcher,
|
||||
started = SharingStarted.Eagerly,
|
||||
replay = 0,
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override val messageEvents: SharedFlow<Pair<String?, BackendMessage>> =
|
||||
merge(localMessageEvents, tunnelProviderFlow.flatMapLatest { it.messageEvents })
|
||||
.shareIn(
|
||||
scope = applicationScope.plus(ioDispatcher),
|
||||
started = SharingStarted.Eagerly,
|
||||
replay = 0,
|
||||
)
|
||||
|
||||
override fun getStatistics(tunnelId: Int): TunnelStatistics? {
|
||||
return tunnelProviderFlow.value.getStatistics(tunnelId)
|
||||
}
|
||||
|
||||
override suspend fun startTunnel(tunnelConfig: TunnelConfig) {
|
||||
if (activeTunnels.value.containsKey(tunnelConfig.id)) return
|
||||
val provider = tunnelProviderFlow.value
|
||||
val isKernel = provider is KernelTunnel
|
||||
|
||||
if (!isKernel && activeTunnels.value.isNotEmpty()) {
|
||||
stopActiveTunnels()
|
||||
withTimeoutOrNull(BaseTunnel.STARTUP_TIMEOUT_MS) {
|
||||
activeTunnels.first { it.isEmpty() }
|
||||
} ?: run { activeTunnels.value.keys.forEach { id -> provider.forceStopTunnel(id) } }
|
||||
}
|
||||
tunnelProviderFlow.value.startTunnel(tunnelConfig)
|
||||
}
|
||||
|
||||
override suspend fun stopTunnel(tunnelId: Int) {
|
||||
tunnelProviderFlow.value.stopTunnel(tunnelId)
|
||||
}
|
||||
|
||||
override suspend fun forceStopTunnel(tunnelId: Int) {
|
||||
tunnelProviderFlow.value.forceStopTunnel(tunnelId)
|
||||
}
|
||||
|
||||
override suspend fun stopActiveTunnels() {
|
||||
tunnelProviderFlow.value.stopActiveTunnels()
|
||||
}
|
||||
|
||||
override fun setBackendMode(backendMode: BackendMode) {
|
||||
tunnelProviderFlow.value.setBackendMode(backendMode)
|
||||
}
|
||||
|
||||
override fun getBackendMode(): BackendMode {
|
||||
return tunnelProviderFlow.value.getBackendMode()
|
||||
}
|
||||
|
||||
override suspend fun runningTunnelNames(): Set<String> {
|
||||
return tunnelProviderFlow.value.runningTunnelNames()
|
||||
}
|
||||
|
||||
override fun handleDnsReresolve(tunnelConfig: TunnelConfig): Boolean {
|
||||
return tunnelProviderFlow.value.handleDnsReresolve(tunnelConfig)
|
||||
}
|
||||
|
||||
override suspend fun updateTunnelStatus(
|
||||
tunnelId: Int,
|
||||
status: TunnelStatus?,
|
||||
stats: TunnelStatistics?,
|
||||
pingStates: Map<String, PingState>?,
|
||||
logHealthState: LogHealthState?,
|
||||
) {
|
||||
tunnelProviderFlow.value.updateTunnelStatus(
|
||||
tunnelId,
|
||||
status,
|
||||
stats,
|
||||
pingStates,
|
||||
logHealthState,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun handleTunnelServiceChange(
|
||||
appMode: AppMode,
|
||||
activeTuns: Map<Int, TunnelState>,
|
||||
) {
|
||||
if (activeTuns.isEmpty()) serviceManager.stopTunnelService()
|
||||
if (activeTuns.isNotEmpty() && serviceManager.tunnelService.value == null)
|
||||
serviceManager.startTunnelService(appMode)
|
||||
serviceManager.updateTunnelTile()
|
||||
}
|
||||
|
||||
// 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,
|
||||
lockdownSettings.metered,
|
||||
lockdownSettings.dualStack,
|
||||
)
|
||||
)
|
||||
} else {
|
||||
throw NotAuthorized()
|
||||
}
|
||||
} catch (e: BackendCoreException) {
|
||||
localErrorEvents.tryEmit(null to e)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleModeChangeCleanup(
|
||||
previousBackend: TunnelProvider,
|
||||
previousAppMode: AppMode,
|
||||
) {
|
||||
previousBackend.stopActiveTunnels()
|
||||
// stop lockdown if we switch from that mode
|
||||
if (previousAppMode == AppMode.LOCK_DOWN)
|
||||
proxyUserspaceTunnel.setBackendMode(BackendMode.Inactive)
|
||||
}
|
||||
|
||||
suspend fun handleRestore() =
|
||||
withContext(ioDispatcher) {
|
||||
val settings = settingsRepository.getGeneralSettings()
|
||||
val autoTunnelSettings = autoTunnelSettingsRepository.getAutoTunnelSettings()
|
||||
val tunnels = tunnelsRepository.getAll()
|
||||
if (autoTunnelSettings.isAutoTunnelEnabled)
|
||||
return@withContext restoreAutoTunnel(autoTunnelSettings)
|
||||
if (settings.appMode == AppMode.LOCK_DOWN) handleLockDownModeInit()
|
||||
if (tunnels.any { it.isActive }) {
|
||||
if (settings.appMode == AppMode.VPN && !serviceManager.hasVpnPermission())
|
||||
return@withContext localErrorEvents.emit(null to NotAuthorized())
|
||||
when (settings.appMode) {
|
||||
AppMode.VPN,
|
||||
AppMode.PROXY,
|
||||
AppMode.LOCK_DOWN -> {
|
||||
tunnels.firstOrNull { it.isActive }?.let { startTunnel(it) }
|
||||
}
|
||||
AppMode.KERNEL ->
|
||||
tunnels.filter { it.isActive }.forEach { conf -> startTunnel(conf) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun restoreAutoTunnel(autoTunnelSettings: AutoTunnelSettings) {
|
||||
autoTunnelSettingsRepository.upsert(autoTunnelSettings.copy(isAutoTunnelEnabled = true))
|
||||
serviceManager.startAutoTunnelService()
|
||||
}
|
||||
|
||||
suspend fun handleReboot() =
|
||||
withContext(ioDispatcher) {
|
||||
val settings = settingsRepository.getGeneralSettings()
|
||||
val autoTunnelSettings = autoTunnelSettingsRepository.getAutoTunnelSettings()
|
||||
val defaultTunnel = tunnelsRepository.getStartTunnel()
|
||||
if (autoTunnelSettings.startOnBoot)
|
||||
return@withContext restoreAutoTunnel(autoTunnelSettings)
|
||||
if (settings.isRestoreOnBootEnabled) {
|
||||
tunnelsRepository.resetActiveTunnels()
|
||||
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) }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleTunnelsActiveChange(
|
||||
previousActiveTuns: Map<Int, TunnelState>,
|
||||
activeTuns: Map<Int, TunnelState>,
|
||||
tuns: List<TunnelConfig>,
|
||||
) {
|
||||
val relevantTunnels = previousActiveTuns.keys + activeTuns.keys
|
||||
|
||||
relevantTunnels.forEach { tunnelId ->
|
||||
val wasActive = previousActiveTuns.containsKey(tunnelId)
|
||||
val isActiveNow = activeTuns.containsKey(tunnelId)
|
||||
|
||||
when {
|
||||
!wasActive && isActiveNow -> {
|
||||
tuns
|
||||
.find { it.id == tunnelId }
|
||||
?.let { dbTunnelConf ->
|
||||
tunnelsRepository.save(dbTunnelConf.copy(isActive = true))
|
||||
}
|
||||
}
|
||||
wasActive && !isActiveNow -> {
|
||||
tuns
|
||||
.find { it.id == tunnelId }
|
||||
?.let { dbTunnelConf ->
|
||||
tunnelsRepository.save(dbTunnelConf.copy(isActive = false))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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>,
|
||||
settings: GeneralSettings,
|
||||
) =
|
||||
ddnsMutex.withLock {
|
||||
val activeIds =
|
||||
activeTuns.keys
|
||||
.filter { id ->
|
||||
configs.find { it.id == id }?.restartOnPingFailure == true &&
|
||||
settings.appMode != AppMode.KERNEL
|
||||
}
|
||||
.toSet()
|
||||
|
||||
val currentJobs = ddnsJobs.keys.toSet()
|
||||
val obsoleteIds = currentJobs - activeIds
|
||||
|
||||
Timber.d(
|
||||
"DDNS Monitoring: Active IDs: $activeIds, Obsolete IDs: $obsoleteIds, Total jobs before: ${ddnsJobs.size}"
|
||||
)
|
||||
|
||||
obsoleteIds.forEach { id ->
|
||||
ddnsJobs[id]?.cancel()
|
||||
ddnsJobs.remove(id)
|
||||
}
|
||||
|
||||
activeIds.forEach { id ->
|
||||
if (ddnsJobs.containsKey(id)) return@forEach // Skip if already monitored
|
||||
val conf = configs.find { it.id == id } ?: return@forEach
|
||||
val tunStateFlow =
|
||||
activeTunnels.map { it[id] }.stateIn(applicationScope + ioDispatcher)
|
||||
|
||||
val newJob =
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
var backoff = 30_000L
|
||||
while (isActive) {
|
||||
val state = tunStateFlow.value ?: break
|
||||
if (state.health() != TunnelState.Health.UNHEALTHY) {
|
||||
backoff = BASE_BACKOFF
|
||||
tunStateFlow.first {
|
||||
it?.health() == TunnelState.Health.UNHEALTHY || it == null
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
runCatching {
|
||||
val updated = handleDnsReresolve(conf)
|
||||
if (updated) {
|
||||
localMessageEvents.emit(
|
||||
conf.name to BackendMessage.DynamicDnsSuccess
|
||||
)
|
||||
backoff = BASE_BACKOFF
|
||||
} else {
|
||||
Timber.i(
|
||||
"Dynamic DNS check completed, current endpoint address is already up to date."
|
||||
)
|
||||
}
|
||||
}
|
||||
.onFailure {
|
||||
Timber.e(
|
||||
it,
|
||||
"Failed to handle dns re-resolution for ${conf.name}",
|
||||
)
|
||||
}
|
||||
|
||||
delay(backoff)
|
||||
backoff = (backoff * 1.5).toLong().coerceAtMost(MAX_BACKOFF_TIME)
|
||||
}
|
||||
}
|
||||
ddnsJobs[id] = newJob
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleFullTunnelMonitoring(
|
||||
activeTuns: Map<Int, TunnelState>,
|
||||
configs: List<TunnelConfig>,
|
||||
settings: GeneralSettings,
|
||||
) =
|
||||
monitoringMutex.withLock {
|
||||
val activeIds = activeTuns.keys.toSet()
|
||||
val currentJobs = monitoringJobs.keys.toSet()
|
||||
val obsoleteIds = currentJobs - activeIds
|
||||
|
||||
Timber.d(
|
||||
"Monitoring: Active IDs: $activeIds, Obsolete IDs: $obsoleteIds, Total jobs before: ${monitoringJobs.size}"
|
||||
)
|
||||
|
||||
obsoleteIds.forEach { id ->
|
||||
monitoringJobs[id]?.cancel()
|
||||
monitoringJobs.remove(id)
|
||||
}
|
||||
|
||||
activeIds.forEach { id ->
|
||||
if (monitoringJobs.containsKey(id)) return@forEach // Skip if already monitored
|
||||
configs.find { it.id == id } ?: return@forEach
|
||||
val tunStateFlow =
|
||||
activeTunnels.map { it[id] }.stateIn(applicationScope + ioDispatcher)
|
||||
val newJob =
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
tunnelMonitor.startMonitoring(
|
||||
id,
|
||||
withLogs = settings.appMode != AppMode.KERNEL,
|
||||
tunStateFlow = tunStateFlow,
|
||||
getStatistics = { tunnelId -> getStatistics(tunnelId) },
|
||||
updateTunnelStatus = { tid, _, stats, pings, logHealth ->
|
||||
updateTunnelStatus(tid, null, stats, pings, logHealth)
|
||||
},
|
||||
)
|
||||
}
|
||||
monitoringJobs[id] = newJob
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val BASE_BACKOFF = 30_000L
|
||||
const val MAX_BACKOFF_TIME = 300_000L
|
||||
const val RESTART_TUNNEL_DELAY = 300L
|
||||
}
|
||||
}
|
||||
@@ -1,321 +0,0 @@
|
||||
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
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.*
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.toMillis
|
||||
import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils
|
||||
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
|
||||
|
||||
@Singleton
|
||||
class TunnelMonitor
|
||||
@Inject
|
||||
constructor(
|
||||
private val settingsRepository: GeneralSettingRepository,
|
||||
private val tunnelsRepository: TunnelRepository,
|
||||
private val monitoringSettingsRepository: MonitoringSettingsRepository,
|
||||
private val networkMonitor: NetworkMonitor,
|
||||
private val networkUtils: NetworkUtils,
|
||||
private val logReader: LogReader,
|
||||
private val powerManager: PowerManager,
|
||||
) {
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
suspend fun startMonitoring(
|
||||
tunnelId: Int,
|
||||
withLogs: Boolean,
|
||||
tunStateFlow: StateFlow<TunnelState?>,
|
||||
getStatistics: suspend (Int) -> TunnelStatistics?,
|
||||
updateTunnelStatus:
|
||||
suspend (
|
||||
Int, TunnelStatus?, TunnelStatistics?, Map<String, PingState>?, LogHealthState?,
|
||||
) -> Unit,
|
||||
): Job = coroutineScope {
|
||||
launch {
|
||||
val config = tunnelsRepository.getById(tunnelId) ?: return@launch
|
||||
launch { startPingMonitor(config, tunStateFlow, updateTunnelStatus) }
|
||||
launch { startWgStatsPoll(tunnelId, getStatistics, updateTunnelStatus) }
|
||||
if (withLogs) launch { startLogsMonitor(config, updateTunnelStatus) }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun startLogsMonitor(
|
||||
tunnelConfig: TunnelConfig,
|
||||
updateTunnelStatus:
|
||||
suspend (
|
||||
Int, TunnelStatus?, TunnelStatistics?, Map<String, PingState>?, LogHealthState?,
|
||||
) -> Unit,
|
||||
) {
|
||||
logReader.liveLogs
|
||||
.filter { log -> log.tag.contains(tunnelConfig.name) }
|
||||
.mapNotNull { log ->
|
||||
val now = System.currentTimeMillis()
|
||||
|
||||
when {
|
||||
successLogRegex.containsMatchIn(log.message) ->
|
||||
LogHealthState(isHealthy = true, timestamp = now)
|
||||
|
||||
failureLogRegex.containsMatchIn(log.message) ->
|
||||
LogHealthState(isHealthy = false, timestamp = now)
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
.distinctUntilChangedBy { it.isHealthy }
|
||||
.collect { logHealthState ->
|
||||
Timber.d("Tunnel log health updated for ${tunnelConfig.name}: $logHealthState")
|
||||
updateTunnelStatus(tunnelConfig.id, null, null, null, logHealthState)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun startPingMonitor(
|
||||
tunnelConfig: TunnelConfig,
|
||||
tunStateFlow: StateFlow<TunnelState?>,
|
||||
updateTunnelStatus:
|
||||
suspend (
|
||||
Int, TunnelStatus?, TunnelStatistics?, Map<String, PingState>?, LogHealthState?,
|
||||
) -> Unit,
|
||||
) = coroutineScope {
|
||||
val pingStatsFlow = MutableStateFlow<Map<String, PingState>>(emptyMap())
|
||||
|
||||
val connectivityStateFlow = networkMonitor.connectivityStateFlow.stateIn(this)
|
||||
|
||||
val isNetworkConnected = connectivityStateFlow.map { it.hasInternet() }.stateIn(this)
|
||||
|
||||
combine(
|
||||
settingsRepository.flow.distinctUntilChangedBy { it.appMode },
|
||||
monitoringSettingsRepository.flow,
|
||||
) { settings, monitorSettings ->
|
||||
Pair(settings.appMode, monitorSettings)
|
||||
}
|
||||
.collectLatest { (appMode, settings) ->
|
||||
if (!settings.isPingEnabled) return@collectLatest
|
||||
// TODO for now until we get monitoring for these modes
|
||||
if (appMode == AppMode.LOCK_DOWN || appMode == AppMode.PROXY) return@collectLatest
|
||||
|
||||
Timber.d("Starting pinger for ${tunnelConfig.name} with settings")
|
||||
|
||||
val config = tunnelConfig.toAmConfig()
|
||||
|
||||
val pingablePeers = config.peers.filter { it.allowedIps.isNotEmpty() }
|
||||
if (pingablePeers.isEmpty()) return@collectLatest
|
||||
|
||||
suspend fun performPing() {
|
||||
val updates = ConcurrentMap<String, PingState>()
|
||||
|
||||
pingablePeers
|
||||
.map { it.publicKey.toBase64() to it }
|
||||
.forEach { (key, peer) ->
|
||||
ensureActive()
|
||||
val previousState = pingStatsFlow.value[key] ?: PingState()
|
||||
|
||||
val allowedIpStr = peer.allowedIps.firstOrNull()?.toString()
|
||||
if (allowedIpStr == null) {
|
||||
updates[key] =
|
||||
previousState.copy(
|
||||
isReachable = false,
|
||||
failureReason = FailureReason.NoResolvedEndpoint,
|
||||
lastPingAttemptMillis = System.currentTimeMillis(),
|
||||
)
|
||||
return@forEach
|
||||
}
|
||||
|
||||
val host =
|
||||
tunnelConfig.pingTarget
|
||||
?: run {
|
||||
val parts = allowedIpStr.split("/")
|
||||
val internalIp =
|
||||
if (parts.size == 2) parts[0] else allowedIpStr
|
||||
val prefix =
|
||||
if (parts.size == 2) parts[1].toIntOrNull() ?: 32
|
||||
else 32
|
||||
val cleanedIp = internalIp.removeSurrounding("[", "]")
|
||||
val defaultCloudflare =
|
||||
if (cleanedIp.contains(":")) CLOUDFLARE_IPV6_IP
|
||||
else CLOUDFLARE_IPV4_IP
|
||||
|
||||
if (prefix <= 1) {
|
||||
defaultCloudflare
|
||||
} else {
|
||||
try {
|
||||
val addrStr = IPAddressString(cleanedIp)
|
||||
val addr: IPAddress =
|
||||
addrStr.address
|
||||
?: throw AddressValueException(
|
||||
"Invalid IP: $cleanedIp"
|
||||
)
|
||||
val isIpv6 = addr.isIPv6
|
||||
val cloudflareIp =
|
||||
if (isIpv6) CLOUDFLARE_IPV6_IP
|
||||
else CLOUDFLARE_IPV4_IP
|
||||
val max = if (isIpv6) 128 else 32
|
||||
|
||||
if (prefix == max) {
|
||||
addr.toCanonicalString()
|
||||
} else {
|
||||
val nextAddr: IPAddress? = addr.increment(1)
|
||||
nextAddr?.toCanonicalString() ?: cloudflareIp
|
||||
}
|
||||
} catch (e: AddressValueException) {
|
||||
Timber.e(
|
||||
e,
|
||||
"Failed to parse or increment IP: $cleanedIp",
|
||||
)
|
||||
defaultCloudflare
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val attemptTime = System.currentTimeMillis()
|
||||
val timeout = settings.tunnelPingTimeoutSeconds?.toMillis() ?: 5000L
|
||||
runCatching {
|
||||
withTimeout(
|
||||
settings.tunnelPingTimeoutSeconds?.toMillis() ?: 5000L
|
||||
) {
|
||||
val pingStats =
|
||||
settings.tunnelPingTimeoutSeconds?.let {
|
||||
networkUtils.pingWithStats(
|
||||
host,
|
||||
settings.tunnelPingAttempts,
|
||||
it.toMillis(),
|
||||
)
|
||||
}
|
||||
?: networkUtils.pingWithStats(
|
||||
host,
|
||||
settings.tunnelPingAttempts,
|
||||
)
|
||||
|
||||
updates[key] =
|
||||
previousState.copy(
|
||||
transmitted = pingStats.transmitted,
|
||||
received = pingStats.received,
|
||||
packetLoss = pingStats.packetLoss,
|
||||
rttMin = pingStats.rttMin,
|
||||
rttMax = pingStats.rttMax,
|
||||
rttAvg = pingStats.rttAvg,
|
||||
rttStddev = pingStats.rttStddev,
|
||||
isReachable = pingStats.isReachable,
|
||||
failureReason =
|
||||
if (pingStats.isReachable) null
|
||||
else FailureReason.PingFailed,
|
||||
lastSuccessfulPingMillis =
|
||||
pingStats.lastSuccessfulPingMillis
|
||||
?: previousState.lastSuccessfulPingMillis,
|
||||
pingTarget = host,
|
||||
lastPingAttemptMillis = attemptTime,
|
||||
)
|
||||
Timber.d(
|
||||
"Ping completed for peer ${peer.publicKey.toBase64().substring(0, 5)}.. to host $host with stats: $pingStats"
|
||||
)
|
||||
}
|
||||
}
|
||||
.onFailure {
|
||||
Timber.e(
|
||||
it,
|
||||
"Ping failed for peer ${peer.publicKey} in ${tunnelConfig.name} to host $host",
|
||||
)
|
||||
updates[key] =
|
||||
previousState.copy(
|
||||
isReachable = false,
|
||||
failureReason = FailureReason.PingFailed,
|
||||
pingTarget = host,
|
||||
lastPingAttemptMillis = attemptTime,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (updates.isNotEmpty()) {
|
||||
ensureActive()
|
||||
pingStatsFlow.update { updates }
|
||||
updateTunnelStatus(tunnelConfig.id, null, null, updates, null)
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for the tunnel to be fully active
|
||||
tunStateFlow.filter { state -> state?.status == TunnelStatus.Up }.first()
|
||||
|
||||
// small delay to make sure tunnel is fully up before we actively monitor
|
||||
delay(3_000L)
|
||||
|
||||
while (isActive) {
|
||||
ensureActive()
|
||||
if (!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,
|
||||
)
|
||||
}
|
||||
}
|
||||
delay(settings.tunnelPingIntervalSeconds.toMillis())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun startWgStatsPoll(
|
||||
tunnelId: Int,
|
||||
getStatistics: suspend (Int) -> TunnelStatistics?,
|
||||
updateTunnelStatus:
|
||||
suspend (
|
||||
Int, TunnelStatus?, TunnelStatistics?, Map<String, PingState>?, LogHealthState?,
|
||||
) -> Unit,
|
||||
) = coroutineScope {
|
||||
while (isActive) {
|
||||
ensureActive()
|
||||
if (!powerManager.isDeviceIdleMode) {
|
||||
val stats = getStatistics(tunnelId)
|
||||
ensureActive()
|
||||
updateTunnelStatus(tunnelId, null, stats, null, null)
|
||||
}
|
||||
delay(STATS_DELAY)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val successLogRegex =
|
||||
Regex("Received handshake response|Receiving keepalive packet", RegexOption.IGNORE_CASE)
|
||||
|
||||
private val failureLogRegex =
|
||||
Regex(
|
||||
"Failed to send handshake initiation: write udp|" +
|
||||
"Handshake did not complete after 5 seconds, retrying|" +
|
||||
"Failed to send data packets",
|
||||
RegexOption.IGNORE_CASE,
|
||||
)
|
||||
|
||||
const val CLOUDFLARE_IPV6_IP = "2606:4700:4700::1111"
|
||||
const val CLOUDFLARE_IPV4_IP = "1.1.1.1"
|
||||
const val STATS_DELAY = 1_000L
|
||||
}
|
||||
}
|
||||
+13
-41
@@ -1,54 +1,26 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.LogHealthState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.PingState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import com.zaneschepke.tunnel.Tunnel
|
||||
import com.zaneschepke.tunnel.event.TunnelEvent
|
||||
import com.zaneschepke.tunnel.model.BackendMode
|
||||
import com.zaneschepke.tunnel.state.BackendStatus
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.LockdownSettings
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
interface TunnelProvider {
|
||||
/** Starts the specified tunnel configuration. */
|
||||
suspend fun startTunnel(tunnelConfig: TunnelConfig)
|
||||
|
||||
/**
|
||||
* Stops the specified tunnel.
|
||||
*
|
||||
* @param tunnelId The tunnelConf to stop.
|
||||
*/
|
||||
suspend fun stopTunnel(tunnelId: Int)
|
||||
suspend fun startTunnel(tunnel: Tunnel, mode: BackendMode): Result<Unit>
|
||||
|
||||
suspend fun forceStopTunnel(tunnelId: Int)
|
||||
suspend fun stopTunnel(tunnelId: Int): Result<Unit>
|
||||
|
||||
/** Stops all active tunnels. */
|
||||
suspend fun stopActiveTunnels()
|
||||
suspend fun stopActiveTunnels(): Result<Unit>
|
||||
|
||||
fun setBackendMode(backendMode: BackendMode)
|
||||
suspend fun setLockDown(settings: LockdownSettings): Result<Unit>
|
||||
|
||||
fun getBackendMode(): BackendMode
|
||||
suspend fun disableLockDown(): Result<Unit>
|
||||
|
||||
suspend fun runningTunnelNames(): Set<String>
|
||||
val backendStatus: StateFlow<BackendStatus>
|
||||
|
||||
fun handleDnsReresolve(tunnelConfig: TunnelConfig): Boolean
|
||||
|
||||
fun getStatistics(tunnelId: Int): TunnelStatistics?
|
||||
|
||||
val activeTunnels: StateFlow<Map<Int, TunnelState>>
|
||||
|
||||
val errorEvents: SharedFlow<Pair<String?, BackendCoreException>>
|
||||
|
||||
val messageEvents: SharedFlow<Pair<String?, BackendMessage>>
|
||||
|
||||
suspend fun updateTunnelStatus(
|
||||
tunnelId: Int,
|
||||
status: TunnelStatus? = null,
|
||||
stats: TunnelStatistics? = null,
|
||||
pingStates: Map<String, PingState>? = null,
|
||||
logHealthState: LogHealthState? = null,
|
||||
)
|
||||
val events: Flow<TunnelEvent>
|
||||
}
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||
|
||||
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.*
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.AmneziaStatistics
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asAmBackendMode
|
||||
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.concurrent.ConcurrentHashMap
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.consumeAsFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.amnezia.awg.backend.Backend
|
||||
import org.amnezia.awg.backend.BackendException
|
||||
import org.amnezia.awg.backend.Tunnel as AwgTunnel
|
||||
import timber.log.Timber
|
||||
|
||||
class UserspaceTunnel
|
||||
@Inject
|
||||
constructor(
|
||||
@ApplicationScope applicationScope: CoroutineScope,
|
||||
@IoDispatcher ioDispatcher: CoroutineDispatcher,
|
||||
private val backend: Backend,
|
||||
private val runConfigHelper: RunConfigHelper,
|
||||
) : BaseTunnel(applicationScope, ioDispatcher) {
|
||||
|
||||
private val runtimeTunnels = ConcurrentHashMap<Int, AwgTunnel>()
|
||||
|
||||
override fun tunnelStateFlow(tunnelConfig: TunnelConfig): Flow<TunnelStatus> = callbackFlow {
|
||||
val stateChannel = Channel<AwgTunnel.State>()
|
||||
|
||||
val runtimeTunnel = RuntimeAwgTunnel(tunnelConfig, stateChannel)
|
||||
runtimeTunnels[tunnelConfig.id] = runtimeTunnel
|
||||
|
||||
val consumerJob = launch {
|
||||
stateChannel.consumeAsFlow().collect { awgState -> trySend(awgState.asTunnelState()) }
|
||||
}
|
||||
|
||||
try {
|
||||
withTimeout(STARTUP_TIMEOUT_MS) {
|
||||
updateTunnelStatus(tunnelConfig.id, TunnelStatus.Starting)
|
||||
val runConfig = runConfigHelper.buildAmRunConfig(tunnelConfig)
|
||||
backend.setState(runtimeTunnel, AwgTunnel.State.UP, runConfig)
|
||||
}
|
||||
} catch (_: TimeoutCancellationException) {
|
||||
Timber.e("Startup timed out for ${tunnelConfig.name} (likely DNS hang)")
|
||||
errors.emit(tunnelConfig.name to DnsFailure())
|
||||
forceStopTunnel(tunnelConfig.id)
|
||||
close()
|
||||
} catch (e: BackendException) {
|
||||
close(e.toBackendCoreException())
|
||||
} catch (_: IllegalArgumentException) {
|
||||
close(InvalidConfig())
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Error while setting tunnel state")
|
||||
close(UnknownError())
|
||||
}
|
||||
|
||||
awaitClose {
|
||||
try {
|
||||
backend.setState(runtimeTunnel, AwgTunnel.State.DOWN, null)
|
||||
} catch (e: BackendException) {
|
||||
errors.tryEmit(tunnelConfig.name to e.toBackendCoreException())
|
||||
} finally {
|
||||
consumerJob.cancel()
|
||||
stateChannel.close()
|
||||
runtimeTunnels.remove(tunnelConfig.id)
|
||||
trySend(TunnelStatus.Down)
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun setBackendMode(backendMode: BackendMode) {
|
||||
Timber.d("Setting backend mode: $backendMode")
|
||||
try {
|
||||
backend.backendMode = backendMode.asAmBackendMode()
|
||||
} catch (e: BackendException) {
|
||||
throw e.toBackendCoreException()
|
||||
// TODO this should be mapped to BackendException in the lib
|
||||
} catch (e: IOException) {
|
||||
throw VpnUnauthorized()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getBackendMode(): BackendMode {
|
||||
return backend.backendMode.asBackendMode()
|
||||
}
|
||||
|
||||
override fun handleDnsReresolve(tunnelConfig: TunnelConfig): Boolean {
|
||||
val tunnel = runtimeTunnels[tunnelConfig.id] ?: throw ServiceNotRunning()
|
||||
return backend.resolveDDNS(tunnelConfig.toAmConfig(), tunnel.isIpv4ResolutionPreferred)
|
||||
}
|
||||
|
||||
override suspend fun runningTunnelNames(): Set<String> {
|
||||
return backend.runningTunnelNames
|
||||
}
|
||||
|
||||
override fun getStatistics(tunnelId: Int): TunnelStatistics? {
|
||||
return try {
|
||||
val runtimeTunnel = runtimeTunnels[tunnelId] ?: return null
|
||||
AmneziaStatistics(backend.getStatistics(runtimeTunnel))
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Failed to get stats for $tunnelId")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun forceStopTunnel(tunnelId: Int) {
|
||||
val runtimeTunnel = runtimeTunnels[tunnelId] ?: return
|
||||
try {
|
||||
backend.setState(runtimeTunnel, AwgTunnel.State.DOWN, null)
|
||||
} catch (e: BackendException) {
|
||||
Timber.e(e, "Force stop failed for $tunnelId")
|
||||
} finally {
|
||||
tunJobs[tunnelId]?.cancel()
|
||||
runtimeTunnels.remove(tunnelId)
|
||||
tunJobs.remove(tunnelId)
|
||||
activeTuns.update { it - tunnelId }
|
||||
updateTunnelStatus(tunnelId, TunnelStatus.Down)
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
-26
@@ -1,31 +1,27 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.worker
|
||||
|
||||
import android.content.Context
|
||||
import androidx.hilt.work.HiltWorker
|
||||
import androidx.work.*
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkerParameters
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelStateHolder
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
@HiltWorker
|
||||
class ServiceWorker
|
||||
@AssistedInject
|
||||
constructor(
|
||||
@Assisted private val context: Context,
|
||||
@Assisted private val params: WorkerParameters,
|
||||
class ServiceWorker(
|
||||
context: Context,
|
||||
params: WorkerParameters,
|
||||
private val serviceManager: ServiceManager,
|
||||
private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
private val autoTunnelStateHolder: AutoTunnelStateHolder,
|
||||
) : CoroutineWorker(context, params) {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "service_worker"
|
||||
private const val TAG = "auto_tunnel_service_monitor"
|
||||
|
||||
fun stop(context: Context) {
|
||||
WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
|
||||
@@ -47,16 +43,19 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun doWork(): Result =
|
||||
withContext(ioDispatcher) {
|
||||
Timber.i("Service worker started")
|
||||
with(autoTunnelSettingsRepository.getAutoTunnelSettings()) {
|
||||
Timber.i("Checking to see if auto-tunnel has been killed by system")
|
||||
if (isAutoTunnelEnabled && serviceManager.autoTunnelService.value == null) {
|
||||
Timber.i("Service has been killed by system, restoring.")
|
||||
serviceManager.startAutoTunnelService()
|
||||
}
|
||||
}
|
||||
Result.success()
|
||||
override suspend fun doWork(): Result {
|
||||
Timber.i("AutoTunnel reconciliation worker running")
|
||||
|
||||
val settings = autoTunnelSettingsRepository.getAutoTunnelSettings()
|
||||
|
||||
if (!settings.isAutoTunnelEnabled) {
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
if (autoTunnelStateHolder.active.value) return Result.success()
|
||||
|
||||
serviceManager.startAutoTunnelService()
|
||||
|
||||
return Result.success()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,27 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data
|
||||
|
||||
import androidx.room.*
|
||||
import androidx.room.AutoMigration
|
||||
import androidx.room.Database
|
||||
import androidx.room.DeleteColumn
|
||||
import androidx.room.RenameColumn
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import androidx.room.migration.AutoMigrationSpec
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import com.zaneschepke.wireguardautotunnel.data.dao.*
|
||||
import com.zaneschepke.wireguardautotunnel.data.entity.*
|
||||
import com.zaneschepke.wireguardautotunnel.data.dao.AutoTunnelSettingsDao
|
||||
import com.zaneschepke.wireguardautotunnel.data.dao.DnsSettingsDao
|
||||
import com.zaneschepke.wireguardautotunnel.data.dao.GeneralSettingsDao
|
||||
import com.zaneschepke.wireguardautotunnel.data.dao.LockdownSettingsDao
|
||||
import com.zaneschepke.wireguardautotunnel.data.dao.MonitoringSettingsDao
|
||||
import com.zaneschepke.wireguardautotunnel.data.dao.ProxySettingsDao
|
||||
import com.zaneschepke.wireguardautotunnel.data.dao.TunnelConfigDao
|
||||
import com.zaneschepke.wireguardautotunnel.data.entity.AutoTunnelSettings
|
||||
import com.zaneschepke.wireguardautotunnel.data.entity.DnsSettings
|
||||
import com.zaneschepke.wireguardautotunnel.data.entity.GeneralSettings
|
||||
import com.zaneschepke.wireguardautotunnel.data.entity.LockdownSettings
|
||||
import com.zaneschepke.wireguardautotunnel.data.entity.MonitoringSettings
|
||||
import com.zaneschepke.wireguardautotunnel.data.entity.ProxySettings
|
||||
import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
|
||||
|
||||
@Database(
|
||||
entities =
|
||||
@@ -17,7 +34,7 @@ import com.zaneschepke.wireguardautotunnel.data.entity.*
|
||||
DnsSettings::class,
|
||||
LockdownSettings::class,
|
||||
],
|
||||
version = 29,
|
||||
version = 30,
|
||||
autoMigrations =
|
||||
[
|
||||
AutoMigration(from = 1, to = 2),
|
||||
@@ -45,6 +62,7 @@ import com.zaneschepke.wireguardautotunnel.data.entity.*
|
||||
AutoMigration(from = 24, to = 25),
|
||||
AutoMigration(from = 26, to = 27, spec = GlobalsMigration::class),
|
||||
AutoMigration(from = 27, to = 28, spec = DonationMigration::class),
|
||||
AutoMigration(from = 29, to = 30, spec = SingleConfigMigration::class),
|
||||
],
|
||||
exportSchema = true,
|
||||
)
|
||||
@@ -129,3 +147,60 @@ class GlobalsMigration : AutoMigrationSpec
|
||||
|
||||
@DeleteColumn(tableName = "general_settings", columnName = "custom_split_packages")
|
||||
class DonationMigration : AutoMigrationSpec
|
||||
|
||||
@RenameColumn.Entries(
|
||||
RenameColumn(
|
||||
tableName = "tunnel_config",
|
||||
fromColumnName = "is_ipv4_preferred",
|
||||
toColumnName = "prefer_ipv6",
|
||||
),
|
||||
RenameColumn(
|
||||
tableName = "tunnel_config",
|
||||
fromColumnName = "am_quick",
|
||||
toColumnName = "quick_config",
|
||||
),
|
||||
RenameColumn(
|
||||
tableName = "tunnel_config",
|
||||
fromColumnName = "restart_on_ping_failure",
|
||||
toColumnName = "dynamic_dns",
|
||||
),
|
||||
)
|
||||
@DeleteColumn.Entries(
|
||||
DeleteColumn(tableName = "tunnel_config", columnName = "wg_quick"),
|
||||
DeleteColumn(tableName = "tunnel_config", columnName = "ping_target"),
|
||||
DeleteColumn(tableName = "tunnel_config", columnName = "is_Active"),
|
||||
DeleteColumn(tableName = "monitoring_settings", columnName = "is_ping_enabled"),
|
||||
DeleteColumn(tableName = "monitoring_settings", columnName = "is_ping_monitoring_enabled"),
|
||||
DeleteColumn(tableName = "monitoring_settings", columnName = "tunnel_ping_interval_sec"),
|
||||
DeleteColumn(tableName = "monitoring_settings", columnName = "tunnel_ping_attempts"),
|
||||
DeleteColumn(tableName = "monitoring_settings", columnName = "tunnel_ping_timeout_sec"),
|
||||
DeleteColumn(tableName = "monitoring_settings", columnName = "show_detailed_ping_stats"),
|
||||
DeleteColumn(tableName = "auto_tunnel_settings", columnName = "debounce_delay_seconds"),
|
||||
)
|
||||
class SingleConfigMigration : AutoMigrationSpec {
|
||||
|
||||
override fun onPostMigrate(db: SupportSQLiteDatabase) {
|
||||
db.execSQL(
|
||||
"""
|
||||
UPDATE tunnel_config
|
||||
SET prefer_ipv6 =
|
||||
CASE prefer_ipv6
|
||||
WHEN 1 THEN 0
|
||||
WHEN 0 THEN 1
|
||||
ELSE 0
|
||||
END
|
||||
"""
|
||||
)
|
||||
|
||||
db.execSQL(
|
||||
"""
|
||||
UPDATE general_settings
|
||||
SET app_mode = CASE app_mode
|
||||
WHEN 3 THEN 0
|
||||
ELSE app_mode
|
||||
END
|
||||
"""
|
||||
.trimIndent()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||
import java.io.IOException
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@@ -17,7 +16,7 @@ import timber.log.Timber
|
||||
|
||||
class DataStoreManager(
|
||||
private val context: Context,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
private val ioDispatcher: CoroutineDispatcher,
|
||||
) {
|
||||
private val preferencesKey = "preferences"
|
||||
val Context.dataStore by preferencesDataStore(name = preferencesKey)
|
||||
|
||||
@@ -2,12 +2,9 @@ package com.zaneschepke.wireguardautotunnel.data
|
||||
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
import timber.log.Timber
|
||||
|
||||
class DatabaseCallback @Inject constructor(private val databaseProvider: Provider<AppDatabase>) :
|
||||
RoomDatabase.Callback() {
|
||||
class DatabaseCallback(private val databaseProvider: Lazy<AppDatabase>) : RoomDatabase.Callback() {
|
||||
override fun onCreate(db: SupportSQLiteDatabase) {
|
||||
super.onCreate(db)
|
||||
Timber.d("Database created, inserting default rows")
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data
|
||||
|
||||
import androidx.room.TypeConverter
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.DnsProtocol
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.WifiDetectionMethod
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.DnsProtocol
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.WifiDetectionMethod
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
class DatabaseConverters {
|
||||
@@ -57,9 +57,9 @@ class DatabaseConverters {
|
||||
@TypeConverter
|
||||
fun toStatus(value: Int): WifiDetectionMethod = WifiDetectionMethod.fromValue(value)
|
||||
|
||||
@TypeConverter fun toMode(value: Int): AppMode = AppMode.fromValue(value)
|
||||
@TypeConverter fun toMode(value: Int): TunnelMode = TunnelMode.fromValue(value)
|
||||
|
||||
@TypeConverter fun fromMode(mode: AppMode): Int = mode.value
|
||||
@TypeConverter fun fromMode(mode: TunnelMode): Int = mode.value
|
||||
|
||||
@TypeConverter fun toDnsProtocol(value: Int): DnsProtocol = DnsProtocol.fromValue(value)
|
||||
|
||||
|
||||
@@ -13,4 +13,7 @@ interface DnsSettingsDao {
|
||||
@Upsert suspend fun upsert(dnsSettings: DnsSettings)
|
||||
|
||||
@Query("SELECT * FROM dns_settings LIMIT 1") fun getDnsSettingsFlow(): Flow<DnsSettings?>
|
||||
|
||||
@Query("UPDATE dns_settings SET global_tunnel_dns_enabled = :enabled")
|
||||
suspend fun updateGlobalDnsEnabled(enabled: Boolean)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import androidx.room.Upsert
|
||||
import com.zaneschepke.wireguardautotunnel.data.entity.GeneralSettings
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
@@ -15,4 +16,22 @@ interface GeneralSettingsDao {
|
||||
|
||||
@Query("SELECT * FROM general_settings LIMIT 1")
|
||||
fun getGeneralSettingsFlow(): Flow<GeneralSettings?>
|
||||
|
||||
@Query("UPDATE general_settings SET theme = :theme WHERE id = 1")
|
||||
suspend fun updateTheme(theme: String)
|
||||
|
||||
@Query("UPDATE general_settings SET locale = :locale WHERE id = 1")
|
||||
suspend fun updateLocale(locale: String)
|
||||
|
||||
@Query("UPDATE general_settings SET is_pin_lock_enabled = :enabled WHERE id = 1")
|
||||
suspend fun updatePinLockEnabled(enabled: Boolean)
|
||||
|
||||
@Query("UPDATE general_settings SET app_mode = :tunnelMode WHERE id = 1")
|
||||
suspend fun updateAppMode(tunnelMode: TunnelMode)
|
||||
|
||||
@Query("UPDATE general_settings SET global_amnezia_enabled = :enabled")
|
||||
suspend fun updateGlobalAmneziaEnabled(enabled: Boolean)
|
||||
|
||||
@Query("UPDATE general_settings SET screen_recording_security = :enabled")
|
||||
suspend fun updateScreenRecordingSecurity(enabled: Boolean)
|
||||
}
|
||||
|
||||
+22
@@ -15,4 +15,26 @@ interface MonitoringSettingsDao {
|
||||
|
||||
@Query("SELECT * FROM monitoring_settings LIMIT 1")
|
||||
fun getMonitoringSettingsFlow(): Flow<MonitoringSettings?>
|
||||
|
||||
@Query(
|
||||
"""
|
||||
UPDATE monitoring_settings
|
||||
SET tunnel_statistics_poll_interval = :interval
|
||||
WHERE id = (
|
||||
SELECT id FROM monitoring_settings LIMIT 1
|
||||
)
|
||||
"""
|
||||
)
|
||||
suspend fun updateStatisticsInterval(interval: Int)
|
||||
|
||||
@Query(
|
||||
"""
|
||||
UPDATE monitoring_settings
|
||||
SET tunnel_statistics_enabled = :enabled
|
||||
WHERE id = (
|
||||
SELECT id FROM monitoring_settings LIMIT 1
|
||||
)
|
||||
"""
|
||||
)
|
||||
suspend fun updateStatisticsEnabled(enabled: Boolean)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.dao
|
||||
|
||||
import androidx.room.*
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Upsert
|
||||
import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@@ -11,17 +16,17 @@ interface TunnelConfigDao {
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun saveAll(t: List<TunnelConfig>)
|
||||
|
||||
@Query("SELECT * FROM tunnel_config WHERE id=:id") suspend fun getById(id: Long): TunnelConfig?
|
||||
@Query("UPDATE tunnel_config SET is_metered = :value WHERE id = :id")
|
||||
suspend fun setMetered(id: Int, value: Boolean)
|
||||
|
||||
@Query("UPDATE tunnel_config SET is_Active = 0 WHERE is_Active = 1")
|
||||
suspend fun resetActiveTunnels()
|
||||
@Query("UPDATE tunnel_config SET dynamic_dns = :value WHERE id = :id")
|
||||
suspend fun setDynamicDns(id: Int, value: Boolean)
|
||||
|
||||
@Query("SELECT * FROM tunnel_config WHERE id=:id") suspend fun getById(id: Long): TunnelConfig?
|
||||
|
||||
@Query("SELECT * FROM tunnel_config WHERE name=:name")
|
||||
suspend fun getByName(name: String): TunnelConfig?
|
||||
|
||||
@Query("SELECT * FROM tunnel_config WHERE is_Active=1")
|
||||
suspend fun getActive(): List<TunnelConfig>
|
||||
|
||||
@Query("SELECT * FROM tunnel_config") suspend fun getAll(): List<TunnelConfig>
|
||||
|
||||
@Delete suspend fun delete(t: TunnelConfig)
|
||||
@@ -50,26 +55,15 @@ 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 is_primary_tunnel DESC, position ASC
|
||||
LIMIT 1
|
||||
"""
|
||||
)
|
||||
suspend fun getDefaultTunnel(): TunnelConfig?
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM tunnel_config
|
||||
ORDER BY
|
||||
CASE WHEN is_Active = 1 THEN 0
|
||||
WHEN is_primary_tunnel = 1 THEN 1
|
||||
ELSE 2 END,
|
||||
position ASC
|
||||
LIMIT 1"""
|
||||
)
|
||||
suspend fun getStartTunnel(): TunnelConfig?
|
||||
|
||||
@Query("SELECT * FROM tunnel_config ORDER BY position")
|
||||
fun getAllFlow(): Flow<List<TunnelConfig>>
|
||||
|
||||
|
||||
+1
-3
@@ -3,7 +3,7 @@ package com.zaneschepke.wireguardautotunnel.data.entity
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.WifiDetectionMethod
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.WifiDetectionMethod
|
||||
|
||||
@Entity(tableName = "auto_tunnel_settings")
|
||||
data class AutoTunnelSettings(
|
||||
@@ -22,8 +22,6 @@ data class AutoTunnelSettings(
|
||||
val isWildcardsEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "is_stop_on_no_internet_enabled", defaultValue = "0")
|
||||
val isStopOnNoInternetEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "debounce_delay_seconds", defaultValue = "3")
|
||||
val debounceDelaySeconds: Int = 3,
|
||||
@ColumnInfo(name = "is_tunnel_on_unsecure_enabled", defaultValue = "0")
|
||||
val isTunnelOnUnsecureEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "wifi_detection_method", defaultValue = "0")
|
||||
|
||||
@@ -3,7 +3,7 @@ package com.zaneschepke.wireguardautotunnel.data.entity
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.DnsProtocol
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.DnsProtocol
|
||||
|
||||
@Entity(tableName = "dns_settings")
|
||||
data class DnsSettings(
|
||||
|
||||
+9
-2
@@ -3,7 +3,7 @@ package com.zaneschepke.wireguardautotunnel.data.entity
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||
|
||||
@Entity(tableName = "general_settings")
|
||||
data class GeneralSettings(
|
||||
@@ -16,7 +16,8 @@ data class GeneralSettings(
|
||||
val isMultiTunnelEnabled: 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 = "app_mode", defaultValue = "0")
|
||||
val tunnelMode: TunnelMode = TunnelMode.fromValue(0),
|
||||
@ColumnInfo(name = "theme", defaultValue = "AUTOMATIC") val theme: String = "AUTOMATIC",
|
||||
@ColumnInfo(name = "locale") val locale: String? = null,
|
||||
@ColumnInfo(name = "remote_key") val remoteKey: String? = null,
|
||||
@@ -27,4 +28,10 @@ data class GeneralSettings(
|
||||
@ColumnInfo(name = "is_always_on_vpn_enabled", defaultValue = "0")
|
||||
val isAlwaysOnVpnEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "already_donated", defaultValue = "0") val alreadyDonated: Boolean = false,
|
||||
@ColumnInfo(name = "screen_recording_security", defaultValue = "1")
|
||||
val screenRecordingSecurityEnabled: Boolean = true,
|
||||
@ColumnInfo(name = "global_amnezia_enabled", defaultValue = "0")
|
||||
val isGlobalAmneziaEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "tunnel_scripting_enabled", defaultValue = "0")
|
||||
val tunnelScriptingEnabled: Boolean = true,
|
||||
)
|
||||
|
||||
+4
-9
@@ -7,15 +7,10 @@ import androidx.room.PrimaryKey
|
||||
@Entity(tableName = "monitoring_settings")
|
||||
data class MonitoringSettings(
|
||||
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
||||
@ColumnInfo(name = "is_ping_enabled", defaultValue = "0") val isPingEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "is_ping_monitoring_enabled", defaultValue = "1")
|
||||
val isPingMonitoringEnabled: Boolean = true,
|
||||
@ColumnInfo(name = "tunnel_ping_interval_sec", defaultValue = "30")
|
||||
val tunnelPingIntervalSeconds: Int = 30,
|
||||
@ColumnInfo(name = "tunnel_ping_attempts", defaultValue = "3") val tunnelPingAttempts: Int = 3,
|
||||
@ColumnInfo(name = "tunnel_ping_timeout_sec") val tunnelPingTimeoutSeconds: Int? = null,
|
||||
@ColumnInfo(name = "show_detailed_ping_stats", defaultValue = "0")
|
||||
val showDetailedPingStats: Boolean = false,
|
||||
@ColumnInfo(name = "is_local_logs_enabled", defaultValue = "0")
|
||||
val isLocalLogsEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "tunnel_statistics_enabled", defaultValue = "1")
|
||||
val tunnelStatisticsEnabled: Boolean = true,
|
||||
@ColumnInfo(name = "tunnel_statistics_poll_interval", defaultValue = "3")
|
||||
val tunnelStatisticsPollInterval: Int = 3,
|
||||
)
|
||||
|
||||
@@ -9,26 +9,26 @@ import androidx.room.PrimaryKey
|
||||
data class TunnelConfig(
|
||||
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
||||
@ColumnInfo(name = "name") val name: String,
|
||||
@ColumnInfo(name = "wg_quick") val wgQuick: String,
|
||||
@ColumnInfo(name = "tunnel_networks", defaultValue = "")
|
||||
val tunnelNetworks: Set<String> = setOf(),
|
||||
@ColumnInfo(name = "is_mobile_data_tunnel", defaultValue = "false")
|
||||
val isMobileDataTunnel: Boolean = false,
|
||||
@ColumnInfo(name = "is_primary_tunnel", defaultValue = "false")
|
||||
val isPrimaryTunnel: Boolean = false,
|
||||
@ColumnInfo(name = "am_quick", defaultValue = "") val amQuick: String = "",
|
||||
@ColumnInfo(name = "is_Active", defaultValue = "false") val isActive: Boolean = false,
|
||||
@ColumnInfo(name = "restart_on_ping_failure", defaultValue = "false")
|
||||
val restartOnPingFailure: Boolean = false,
|
||||
@ColumnInfo(name = "ping_target", defaultValue = "null") var pingTarget: String? = null,
|
||||
@ColumnInfo(name = "quick_config", defaultValue = "") val quickConfig: String = "",
|
||||
@ColumnInfo(name = "dynamic_dns", defaultValue = "false")
|
||||
val dynamicDnsEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "is_ethernet_tunnel", defaultValue = "false")
|
||||
val isEthernetTunnel: Boolean = false,
|
||||
@ColumnInfo(name = "is_ipv4_preferred", defaultValue = "true")
|
||||
val isIpv4Preferred: Boolean = true,
|
||||
@ColumnInfo(name = "prefer_ipv6", defaultValue = "false") val isIpv6Preferred: Boolean = false,
|
||||
@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,
|
||||
@ColumnInfo(name = "ipv4_fallback", defaultValue = "false")
|
||||
val ipv4FallbackEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "ipv6_restore", defaultValue = "false")
|
||||
val ipv6RestoreEnabled: Boolean = false,
|
||||
) {
|
||||
companion object {
|
||||
const val GLOBAL_CONFIG_NAME = "4675ab06-903a-438b-8485-6ea4187a9512"
|
||||
|
||||
-2
@@ -13,7 +13,6 @@ fun Entity.toDomain(): Domain =
|
||||
isTunnelOnWifiEnabled = isTunnelOnWifiEnabled,
|
||||
isWildcardsEnabled = isWildcardsEnabled,
|
||||
isStopOnNoInternetEnabled = isStopOnNoInternetEnabled,
|
||||
debounceDelaySeconds = debounceDelaySeconds,
|
||||
isTunnelOnUnsecureEnabled = isTunnelOnUnsecureEnabled,
|
||||
wifiDetectionMethod = wifiDetectionMethod,
|
||||
startOnBoot = startOnBoot,
|
||||
@@ -29,7 +28,6 @@ fun Domain.toEntity(): Entity =
|
||||
isTunnelOnWifiEnabled = isTunnelOnWifiEnabled,
|
||||
isWildcardsEnabled = isWildcardsEnabled,
|
||||
isStopOnNoInternetEnabled = isStopOnNoInternetEnabled,
|
||||
debounceDelaySeconds = debounceDelaySeconds,
|
||||
isTunnelOnUnsecureEnabled = isTunnelOnUnsecureEnabled,
|
||||
wifiDetectionMethod = wifiDetectionMethod,
|
||||
startOnBoot = startOnBoot,
|
||||
|
||||
+4
-12
@@ -6,23 +6,15 @@ import com.zaneschepke.wireguardautotunnel.domain.model.MonitoringSettings as Do
|
||||
fun Entity.toDomain(): Domain =
|
||||
Domain(
|
||||
id = id,
|
||||
isPingEnabled = isPingEnabled,
|
||||
isPingMonitoringEnabled = isPingMonitoringEnabled,
|
||||
tunnelPingIntervalSeconds = tunnelPingIntervalSeconds,
|
||||
tunnelPingAttempts = tunnelPingAttempts,
|
||||
tunnelPingTimeoutSeconds = tunnelPingTimeoutSeconds,
|
||||
showDetailedPingStats = showDetailedPingStats,
|
||||
tunnelStatisticsEnabled = tunnelStatisticsEnabled,
|
||||
tunnelStatisticsPollInterval = tunnelStatisticsPollInterval,
|
||||
isLocalLogsEnabled = isLocalLogsEnabled,
|
||||
)
|
||||
|
||||
fun Domain.toEntity(): Entity =
|
||||
Entity(
|
||||
id = id,
|
||||
isPingEnabled = isPingEnabled,
|
||||
isPingMonitoringEnabled = isPingMonitoringEnabled,
|
||||
tunnelPingIntervalSeconds = tunnelPingIntervalSeconds,
|
||||
tunnelPingAttempts = tunnelPingAttempts,
|
||||
tunnelPingTimeoutSeconds = tunnelPingTimeoutSeconds,
|
||||
showDetailedPingStats = showDetailedPingStats,
|
||||
tunnelStatisticsEnabled = tunnelStatisticsEnabled,
|
||||
tunnelStatisticsPollInterval = tunnelStatisticsPollInterval,
|
||||
isLocalLogsEnabled = isLocalLogsEnabled,
|
||||
)
|
||||
|
||||
@@ -11,7 +11,7 @@ fun Entity.toDomain(): Domain =
|
||||
isRestoreOnBootEnabled = isRestoreOnBootEnabled,
|
||||
isMultiTunnelEnabled = isMultiTunnelEnabled,
|
||||
isGlobalSplitTunnelEnabled = isGlobalSplitTunnelEnabled,
|
||||
appMode = appMode,
|
||||
tunnelMode = tunnelMode,
|
||||
theme = Theme.valueOf(theme.uppercase()),
|
||||
locale = locale,
|
||||
remoteKey = remoteKey,
|
||||
@@ -19,6 +19,9 @@ fun Entity.toDomain(): Domain =
|
||||
isPinLockEnabled = isPinLockEnabled,
|
||||
isAlwaysOnVpnEnabled = isAlwaysOnVpnEnabled,
|
||||
alreadyDonated = alreadyDonated,
|
||||
screenRecordingSecurityEnabled = screenRecordingSecurityEnabled,
|
||||
isGlobalAmneziaEnabled = isGlobalAmneziaEnabled,
|
||||
tunnelScriptingEnabled = tunnelScriptingEnabled,
|
||||
)
|
||||
|
||||
fun Domain.toEntity(): Entity =
|
||||
@@ -28,7 +31,7 @@ fun Domain.toEntity(): Entity =
|
||||
isRestoreOnBootEnabled = isRestoreOnBootEnabled,
|
||||
isMultiTunnelEnabled = isMultiTunnelEnabled,
|
||||
isGlobalSplitTunnelEnabled = isGlobalSplitTunnelEnabled,
|
||||
appMode = appMode,
|
||||
tunnelMode = tunnelMode,
|
||||
theme = theme.name,
|
||||
locale = locale,
|
||||
remoteKey = remoteKey,
|
||||
@@ -36,4 +39,7 @@ fun Domain.toEntity(): Entity =
|
||||
isPinLockEnabled = isPinLockEnabled,
|
||||
isAlwaysOnVpnEnabled = isAlwaysOnVpnEnabled,
|
||||
alreadyDonated = alreadyDonated,
|
||||
screenRecordingSecurityEnabled = screenRecordingSecurityEnabled,
|
||||
isGlobalAmneziaEnabled = isGlobalAmneziaEnabled,
|
||||
tunnelScriptingEnabled = tunnelScriptingEnabled,
|
||||
)
|
||||
|
||||
+10
-12
@@ -7,36 +7,34 @@ fun Entity.toDomain(): Domain =
|
||||
Domain(
|
||||
id = id,
|
||||
name = name,
|
||||
wgQuick = wgQuick,
|
||||
tunnelNetworks = tunnelNetworks,
|
||||
isMobileDataTunnel = isMobileDataTunnel,
|
||||
isPrimaryTunnel = isPrimaryTunnel,
|
||||
amQuick = amQuick,
|
||||
isActive = isActive,
|
||||
restartOnPingFailure = restartOnPingFailure,
|
||||
pingTarget = pingTarget,
|
||||
quickConfig = quickConfig,
|
||||
dynamicDnsEnabled = dynamicDnsEnabled,
|
||||
isEthernetTunnel = isEthernetTunnel,
|
||||
isIpv4Preferred = isIpv4Preferred,
|
||||
isIpv6Preferred = isIpv6Preferred,
|
||||
position = position,
|
||||
autoTunnelApps = autoTunnelApps,
|
||||
isMetered = isMetered,
|
||||
ipv4FallbackEnabled = ipv4FallbackEnabled,
|
||||
ipv6RestoreEnabled = ipv6RestoreEnabled,
|
||||
)
|
||||
|
||||
fun Domain.toEntity(): Entity =
|
||||
Entity(
|
||||
id = id,
|
||||
name = name,
|
||||
wgQuick = wgQuick,
|
||||
tunnelNetworks = tunnelNetworks,
|
||||
isMobileDataTunnel = isMobileDataTunnel,
|
||||
isPrimaryTunnel = isPrimaryTunnel,
|
||||
amQuick = amQuick,
|
||||
isActive = isActive,
|
||||
restartOnPingFailure = restartOnPingFailure,
|
||||
pingTarget = pingTarget,
|
||||
quickConfig = quickConfig,
|
||||
dynamicDnsEnabled = dynamicDnsEnabled,
|
||||
isEthernetTunnel = isEthernetTunnel,
|
||||
isIpv4Preferred = isIpv4Preferred,
|
||||
isIpv6Preferred = isIpv6Preferred,
|
||||
position = position,
|
||||
autoTunnelApps = autoTunnelApps,
|
||||
isMetered = isMetered,
|
||||
ipv4FallbackEnabled = ipv4FallbackEnabled,
|
||||
ipv6RestoreEnabled = ipv6RestoreEnabled,
|
||||
)
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.model
|
||||
|
||||
enum class AppMode(val value: Int) {
|
||||
VPN(0),
|
||||
PROXY(1),
|
||||
LOCK_DOWN(2),
|
||||
KERNEL(3);
|
||||
|
||||
companion object {
|
||||
fun fromValue(value: Int): AppMode = entries.find { it.value == value } ?: VPN
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.model
|
||||
|
||||
import android.content.Context
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
|
||||
enum class DnsProtocol(val value: Int) {
|
||||
SYSTEM(0),
|
||||
DOH(1);
|
||||
|
||||
fun asString(context: Context): String {
|
||||
return when (this) {
|
||||
SYSTEM -> context.getString(R.string.system)
|
||||
DOH -> context.getString(R.string.doh)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromValue(value: Int): DnsProtocol =
|
||||
DnsProtocol.entries.find { it.value == value } ?: SYSTEM
|
||||
}
|
||||
}
|
||||
|
||||
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"),
|
||||
ADGUARD("94.140.14.14", "https://94.140.14.14/dns-query");
|
||||
|
||||
fun asAddress(protocol: DnsProtocol): String {
|
||||
return when (protocol) {
|
||||
DnsProtocol.SYSTEM -> systemAddress
|
||||
DnsProtocol.DOH -> dohAddress
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromAddress(address: String): DnsProvider {
|
||||
return entries.find { it.systemAddress == address || it.dohAddress == address }
|
||||
?: CLOUDFLARE
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.network
|
||||
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.engine.okhttp.*
|
||||
import io.ktor.client.plugins.*
|
||||
import io.ktor.client.plugins.contentnegotiation.*
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.okhttp.OkHttp
|
||||
import io.ktor.client.plugins.HttpTimeout
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
object KtorClient {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.network
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.entity.GitHubRelease
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.plugins.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.plugins.ClientRequestException
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.http.HttpStatusCode
|
||||
|
||||
class KtorGitHubApi(private val client: HttpClient) : GitHubApi {
|
||||
override suspend fun getLatestRelease(owner: String, repo: String): Result<GitHubRelease> {
|
||||
@@ -32,10 +32,9 @@ class KtorGitHubApi(private val client: HttpClient) : GitHubApi {
|
||||
client.get("https://api.github.com/repos/$owner/$repo/releases").body()
|
||||
|
||||
// Find the first release with "nightly" in the tag_name (case-insensitive)
|
||||
val nightlyRelease =
|
||||
releases.firstOrNull { release ->
|
||||
release.tagName.contains("nightly", ignoreCase = true)
|
||||
}
|
||||
val nightlyRelease = releases.firstOrNull { release ->
|
||||
release.tagName.contains("nightly", ignoreCase = true)
|
||||
}
|
||||
|
||||
if (nightlyRelease != null) {
|
||||
Result.success(nightlyRelease)
|
||||
|
||||
+4
-4
@@ -3,8 +3,6 @@ package com.zaneschepke.wireguardautotunnel.data.repository
|
||||
import com.zaneschepke.wireguardautotunnel.data.DataStoreManager
|
||||
import com.zaneschepke.wireguardautotunnel.data.entity.AppState as Entity
|
||||
import com.zaneschepke.wireguardautotunnel.data.mapper.toDomain
|
||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.AppState as Domain
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
@@ -18,8 +16,8 @@ import timber.log.Timber
|
||||
|
||||
class DataStoreAppStateRepository(
|
||||
private val dataStoreManager: DataStoreManager,
|
||||
@ApplicationScope private val applicationScope: CoroutineScope,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
applicationScope: CoroutineScope,
|
||||
ioDispatcher: CoroutineDispatcher,
|
||||
) : AppStateRepository {
|
||||
override suspend fun isLocationDisclosureShown(): Boolean {
|
||||
return dataStoreManager.getFromStore(DataStoreManager.locationDisclosureShown) ?: false
|
||||
@@ -55,6 +53,8 @@ class DataStoreAppStateRepository(
|
||||
pref[DataStoreManager.locationDisclosureShown] ?: false,
|
||||
isBatteryOptimizationDisableShown =
|
||||
pref[DataStoreManager.batteryDisableShown] ?: false,
|
||||
shouldShowDonationSnackbar =
|
||||
pref[DataStoreManager.shouldShowDonationSnackbar] ?: false,
|
||||
)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Timber.e(e)
|
||||
|
||||
+9
-7
@@ -4,16 +4,17 @@ import android.content.Context
|
||||
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.mapper.GitHubReleaseMapper
|
||||
import com.zaneschepke.wireguardautotunnel.data.network.GitHubApi
|
||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.AppUpdate
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.UpdateRepository
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.utils.io.*
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.statement.HttpResponse
|
||||
import io.ktor.client.statement.bodyAsChannel
|
||||
import io.ktor.http.contentLength
|
||||
import io.ktor.utils.io.ByteReadChannel
|
||||
import io.ktor.utils.io.readAvailable
|
||||
import java.io.File
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -25,8 +26,9 @@ class GitHubUpdateRepository(
|
||||
private val githubOwner: String,
|
||||
private val githubRepo: String,
|
||||
private val context: Context,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
private val ioDispatcher: CoroutineDispatcher,
|
||||
) : UpdateRepository {
|
||||
|
||||
override suspend fun checkForUpdate(currentVersion: String): Result<AppUpdate?> =
|
||||
withContext(ioDispatcher) {
|
||||
Timber.i("Checking for update")
|
||||
|
||||
+18
-25
@@ -2,22 +2,16 @@ package com.zaneschepke.wireguardautotunnel.data.repository
|
||||
|
||||
import android.content.Context
|
||||
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.getFriendlyAppName
|
||||
import javax.inject.Singleton
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
@Singleton
|
||||
class InstalledAndroidPackageRepository(
|
||||
private val context: Context,
|
||||
@ApplicationScope val applicationScope: CoroutineScope,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
private val ioDispatcher: CoroutineDispatcher,
|
||||
) : InstalledPackageRepository {
|
||||
|
||||
private var cachedPackages: List<InstalledPackage>? = null
|
||||
@@ -34,25 +28,24 @@ class InstalledAndroidPackageRepository(
|
||||
withContext(ioDispatcher) {
|
||||
val packages = context.packageManager.getInstalledPackages(0)
|
||||
|
||||
val installedPackages =
|
||||
packages.mapNotNull { packageInfo ->
|
||||
try {
|
||||
val appInfo =
|
||||
context.packageManager.getApplicationInfo(packageInfo.packageName, 0)
|
||||
InstalledPackage(
|
||||
name =
|
||||
context.packageManager.getFriendlyAppName(
|
||||
packageInfo.packageName,
|
||||
appInfo,
|
||||
),
|
||||
packageName = packageInfo.packageName,
|
||||
uId = appInfo.uid,
|
||||
)
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
Timber.e(e)
|
||||
null
|
||||
}
|
||||
val installedPackages = packages.mapNotNull { packageInfo ->
|
||||
try {
|
||||
val appInfo =
|
||||
context.packageManager.getApplicationInfo(packageInfo.packageName, 0)
|
||||
InstalledPackage(
|
||||
name =
|
||||
context.packageManager.getFriendlyAppName(
|
||||
packageInfo.packageName,
|
||||
appInfo,
|
||||
),
|
||||
packageName = packageInfo.packageName,
|
||||
uId = appInfo.uid,
|
||||
)
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
Timber.e(e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
cachedPackages = installedPackages
|
||||
|
||||
|
||||
+3
-11
@@ -4,28 +4,20 @@ import com.zaneschepke.wireguardautotunnel.data.dao.AutoTunnelSettingsDao
|
||||
import com.zaneschepke.wireguardautotunnel.data.entity.AutoTunnelSettings as Entity
|
||||
import com.zaneschepke.wireguardautotunnel.data.mapper.toDomain
|
||||
import com.zaneschepke.wireguardautotunnel.data.mapper.toEntity
|
||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.AutoTunnelSettings as Domain
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class RoomAutoTunnelSettingsRepository(
|
||||
private val autoTunnelSettingsDao: AutoTunnelSettingsDao,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
) : AutoTunnelSettingsRepository {
|
||||
class RoomAutoTunnelSettingsRepository(private val autoTunnelSettingsDao: AutoTunnelSettingsDao) :
|
||||
AutoTunnelSettingsRepository {
|
||||
override suspend fun upsert(autoTunnelSettings: Domain) {
|
||||
autoTunnelSettingsDao.upsert(autoTunnelSettings.toEntity())
|
||||
}
|
||||
|
||||
override val flow: Flow<Domain>
|
||||
get() =
|
||||
autoTunnelSettingsDao
|
||||
.getAutoTunnelSettingsFlow()
|
||||
.map { (it ?: Entity()).toDomain() }
|
||||
.flowOn(ioDispatcher)
|
||||
autoTunnelSettingsDao.getAutoTunnelSettingsFlow().map { (it ?: Entity()).toDomain() }
|
||||
|
||||
override suspend fun getAutoTunnelSettings(): Domain {
|
||||
return (autoTunnelSettingsDao.getAutoTunnelSettings() ?: Entity()).toDomain()
|
||||
|
||||
+7
-12
@@ -4,30 +4,25 @@ import com.zaneschepke.wireguardautotunnel.data.dao.DnsSettingsDao
|
||||
import com.zaneschepke.wireguardautotunnel.data.entity.DnsSettings as Entity
|
||||
import com.zaneschepke.wireguardautotunnel.data.mapper.toDomain
|
||||
import com.zaneschepke.wireguardautotunnel.data.mapper.toEntity
|
||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.DnsSettings as Domain
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.DnsSettingsRepository
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class RoomDnsSettingsRepository(
|
||||
private val dnsSettingsDao: DnsSettingsDao,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
) : DnsSettingsRepository {
|
||||
class RoomDnsSettingsRepository(private val dnsSettingsDao: DnsSettingsDao) :
|
||||
DnsSettingsRepository {
|
||||
override suspend fun upsert(dnsSettings: Domain) {
|
||||
dnsSettingsDao.upsert(dnsSettings.toEntity())
|
||||
}
|
||||
|
||||
override val flow: Flow<Domain>
|
||||
get() =
|
||||
dnsSettingsDao
|
||||
.getDnsSettingsFlow()
|
||||
.map { (it ?: Entity()).toDomain() }
|
||||
.flowOn(ioDispatcher)
|
||||
get() = dnsSettingsDao.getDnsSettingsFlow().map { (it ?: Entity()).toDomain() }
|
||||
|
||||
override suspend fun getDnsSettings(): Domain {
|
||||
return (dnsSettingsDao.getDnsSettings() ?: Entity()).toDomain()
|
||||
}
|
||||
|
||||
override suspend fun updateGlobalDnsEnabled(enabled: Boolean) {
|
||||
dnsSettingsDao.updateGlobalDnsEnabled(enabled)
|
||||
}
|
||||
}
|
||||
|
||||
+5
-16
@@ -4,31 +4,20 @@ 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 {
|
||||
class RoomLockdownSettingsRepository(private val lockdownSettingsDao: LockdownSettingsDao) :
|
||||
LockdownSettingsRepository {
|
||||
override suspend fun upsert(lockdownSettings: Domain) {
|
||||
withContext(ioDispatcher) { lockdownSettingsDao.upsert(lockdownSettings.toEntity()) }
|
||||
lockdownSettingsDao.upsert(lockdownSettings.toEntity())
|
||||
}
|
||||
|
||||
override val flow =
|
||||
lockdownSettingsDao
|
||||
.getLockdownSettingsFlow()
|
||||
.map { (it ?: Entity()).toDomain() }
|
||||
.flowOn(ioDispatcher)
|
||||
lockdownSettingsDao.getLockdownSettingsFlow().map { (it ?: Entity()).toDomain() }
|
||||
|
||||
override suspend fun getLockdownSettings(): Domain {
|
||||
return withContext(ioDispatcher) {
|
||||
(lockdownSettingsDao.getLockdownSettings() ?: Entity()).toDomain()
|
||||
}
|
||||
return (lockdownSettingsDao.getLockdownSettings() ?: Entity()).toDomain()
|
||||
}
|
||||
}
|
||||
|
||||
+11
-11
@@ -4,30 +4,30 @@ import com.zaneschepke.wireguardautotunnel.data.dao.MonitoringSettingsDao
|
||||
import com.zaneschepke.wireguardautotunnel.data.entity.MonitoringSettings as Entity
|
||||
import com.zaneschepke.wireguardautotunnel.data.mapper.toDomain
|
||||
import com.zaneschepke.wireguardautotunnel.data.mapper.toEntity
|
||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.MonitoringSettings as Domain
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class RoomMonitoringSettingsRepository(
|
||||
private val monitoringSettingsDao: MonitoringSettingsDao,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
) : MonitoringSettingsRepository {
|
||||
class RoomMonitoringSettingsRepository(private val monitoringSettingsDao: MonitoringSettingsDao) :
|
||||
MonitoringSettingsRepository {
|
||||
override suspend fun upsert(monitoringSettings: Domain) {
|
||||
monitoringSettingsDao.upsert(monitoringSettings.toEntity())
|
||||
}
|
||||
|
||||
override val flow: Flow<Domain>
|
||||
get() =
|
||||
monitoringSettingsDao
|
||||
.getMonitoringSettingsFlow()
|
||||
.map { (it ?: Entity()).toDomain() }
|
||||
.flowOn(ioDispatcher)
|
||||
monitoringSettingsDao.getMonitoringSettingsFlow().map { (it ?: Entity()).toDomain() }
|
||||
|
||||
override suspend fun getMonitoringSettings(): Domain {
|
||||
return (monitoringSettingsDao.getMonitoringSettings() ?: Entity()).toDomain()
|
||||
}
|
||||
|
||||
override suspend fun updateStatisticRefresh(statisticRefresh: Int) {
|
||||
monitoringSettingsDao.updateStatisticsInterval(statisticRefresh)
|
||||
}
|
||||
|
||||
override suspend fun updateStatisticsEnabled(enabled: Boolean) {
|
||||
monitoringSettingsDao.updateStatisticsEnabled(enabled)
|
||||
}
|
||||
}
|
||||
|
||||
+6
-17
@@ -4,31 +4,20 @@ import com.zaneschepke.wireguardautotunnel.data.dao.ProxySettingsDao
|
||||
import com.zaneschepke.wireguardautotunnel.data.entity.ProxySettings as Entity
|
||||
import com.zaneschepke.wireguardautotunnel.data.mapper.toDomain
|
||||
import com.zaneschepke.wireguardautotunnel.data.mapper.toEntity
|
||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.ProxySettings as Domain
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.ProxySettingsRepository
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class RoomProxySettingsRepository(
|
||||
private val proxySettingsDao: ProxySettingsDao,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
) : ProxySettingsRepository {
|
||||
class RoomProxySettingsRepository(private val proxySettingsDao: ProxySettingsDao) :
|
||||
ProxySettingsRepository {
|
||||
|
||||
override suspend fun upsert(proxySettings: Domain) {
|
||||
withContext(ioDispatcher) { proxySettingsDao.upsert(proxySettings.toEntity()) }
|
||||
proxySettingsDao.upsert(proxySettings.toEntity())
|
||||
}
|
||||
|
||||
override val flow =
|
||||
proxySettingsDao
|
||||
.getProxySettingsFlow()
|
||||
.map { (it ?: Entity()).toDomain() }
|
||||
.flowOn(ioDispatcher)
|
||||
override val flow = proxySettingsDao.getProxySettingsFlow().map { (it ?: Entity()).toDomain() }
|
||||
|
||||
override suspend fun getProxySettings(): Domain {
|
||||
return withContext(ioDispatcher) {
|
||||
(proxySettingsDao.getProxySettings() ?: Entity()).toDomain()
|
||||
}
|
||||
return (proxySettingsDao.getProxySettings() ?: Entity()).toDomain()
|
||||
}
|
||||
}
|
||||
|
||||
+31
-18
@@ -4,32 +4,45 @@ import com.zaneschepke.wireguardautotunnel.data.dao.GeneralSettingsDao
|
||||
import com.zaneschepke.wireguardautotunnel.data.entity.GeneralSettings as Entity
|
||||
import com.zaneschepke.wireguardautotunnel.data.mapper.toDomain
|
||||
import com.zaneschepke.wireguardautotunnel.data.mapper.toEntity
|
||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings as Domain
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class RoomSettingsRepository(
|
||||
private val settingsDoa: GeneralSettingsDao,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
) : GeneralSettingRepository {
|
||||
|
||||
class RoomSettingsRepository(private val settingsDao: GeneralSettingsDao) :
|
||||
GeneralSettingRepository {
|
||||
override suspend fun upsert(generalSettings: Domain) {
|
||||
withContext(ioDispatcher) { settingsDoa.upsert(generalSettings.toEntity()) }
|
||||
settingsDao.upsert(generalSettings.toEntity())
|
||||
}
|
||||
|
||||
override val flow =
|
||||
settingsDoa
|
||||
.getGeneralSettingsFlow()
|
||||
.map { (it ?: Entity()).toDomain() }
|
||||
.flowOn(ioDispatcher)
|
||||
override val flow = settingsDao.getGeneralSettingsFlow().map { (it ?: Entity()).toDomain() }
|
||||
|
||||
override suspend fun getGeneralSettings(): Domain {
|
||||
return withContext(ioDispatcher) {
|
||||
(settingsDoa.getGeneralSettings() ?: Entity()).toDomain()
|
||||
}
|
||||
return (settingsDao.getGeneralSettings() ?: Entity()).toDomain()
|
||||
}
|
||||
|
||||
override suspend fun updateTheme(theme: Theme) {
|
||||
settingsDao.updateTheme(theme.name)
|
||||
}
|
||||
|
||||
override suspend fun updateLocale(locale: String) {
|
||||
settingsDao.updateLocale(locale)
|
||||
}
|
||||
|
||||
override suspend fun updatePinLockEnabled(enabled: Boolean) {
|
||||
settingsDao.updatePinLockEnabled(enabled)
|
||||
}
|
||||
|
||||
override suspend fun updateAppMode(tunnelMode: TunnelMode) {
|
||||
settingsDao.updateAppMode(tunnelMode)
|
||||
}
|
||||
|
||||
override suspend fun updateGlobalAmneziaEnabled(enabled: Boolean) {
|
||||
settingsDao.updateGlobalAmneziaEnabled(enabled)
|
||||
}
|
||||
|
||||
override suspend fun updateScreenRecordingSecurity(enabled: Boolean) {
|
||||
settingsDao.updateScreenRecordingSecurity(enabled)
|
||||
}
|
||||
}
|
||||
|
||||
+37
-57
@@ -3,119 +3,99 @@ package com.zaneschepke.wireguardautotunnel.data.repository
|
||||
import com.zaneschepke.wireguardautotunnel.data.dao.TunnelConfigDao
|
||||
import com.zaneschepke.wireguardautotunnel.data.mapper.toDomain
|
||||
import com.zaneschepke.wireguardautotunnel.data.mapper.toEntity
|
||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig as Domain
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class RoomTunnelRepository(
|
||||
private val tunnelConfigDao: TunnelConfigDao,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
) : TunnelRepository {
|
||||
class RoomTunnelRepository(private val tunnelConfigDao: TunnelConfigDao) : TunnelRepository {
|
||||
|
||||
override val flow =
|
||||
tunnelConfigDao.getAllFlow().flowOn(ioDispatcher).map {
|
||||
it.map { tunnelConfig -> tunnelConfig.toDomain() }
|
||||
}
|
||||
tunnelConfigDao.getAllFlow().map { it.map { tunnelConfig -> tunnelConfig.toDomain() } }
|
||||
|
||||
override val userTunnelsFlow =
|
||||
tunnelConfigDao.getAllTunnelsExceptGlobal().flowOn(ioDispatcher).map {
|
||||
tunnelConfigDao.getAllTunnelsExceptGlobal().map {
|
||||
it.map { tunnelConfig -> tunnelConfig.toDomain() }
|
||||
}
|
||||
|
||||
override val globalTunnelFlow: Flow<Domain?> =
|
||||
tunnelConfigDao.getGlobalTunnel().flowOn(ioDispatcher).map { it?.toDomain() }
|
||||
tunnelConfigDao.getGlobalTunnel().map { it?.toDomain() }
|
||||
|
||||
override suspend fun getAll(): List<Domain> {
|
||||
return withContext(ioDispatcher) { tunnelConfigDao.getAll().map { it.toDomain() } }
|
||||
return tunnelConfigDao.getAll().map { it.toDomain() }
|
||||
}
|
||||
|
||||
override suspend fun setMetered(tunnelId: Int, value: Boolean) {
|
||||
tunnelConfigDao.setMetered(tunnelId, value)
|
||||
}
|
||||
|
||||
override suspend fun setDynamicDns(tunnelId: Int, value: Boolean) {
|
||||
tunnelConfigDao.setDynamicDns(tunnelId, value)
|
||||
}
|
||||
|
||||
override suspend fun save(tunnelConfig: Domain) {
|
||||
withContext(ioDispatcher) { tunnelConfigDao.upsert(tunnelConfig.toEntity()) }
|
||||
tunnelConfigDao.upsert(tunnelConfig.toEntity())
|
||||
}
|
||||
|
||||
override suspend fun saveAll(tunnelConfigList: List<Domain>) {
|
||||
withContext(ioDispatcher) {
|
||||
tunnelConfigDao.saveAll(
|
||||
tunnelConfigList.map { tunnelConfig -> tunnelConfig.toEntity() }
|
||||
)
|
||||
}
|
||||
tunnelConfigDao.saveAll(tunnelConfigList.map { tunnelConfig -> tunnelConfig.toEntity() })
|
||||
}
|
||||
|
||||
override suspend fun updatePrimaryTunnel(tunnelConfig: Domain?) {
|
||||
withContext(ioDispatcher) {
|
||||
tunnelConfigDao.resetPrimaryTunnel()
|
||||
tunnelConfig?.let { save(it.copy(isPrimaryTunnel = true)) }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun resetActiveTunnels() {
|
||||
withContext(ioDispatcher) { tunnelConfigDao.resetActiveTunnels() }
|
||||
tunnelConfigDao.resetPrimaryTunnel()
|
||||
tunnelConfig?.let { save(it.copy(isPrimaryTunnel = true)) }
|
||||
}
|
||||
|
||||
override suspend fun updateMobileDataTunnel(tunnelConfig: Domain?) {
|
||||
withContext(ioDispatcher) {
|
||||
tunnelConfigDao.resetMobileDataTunnel()
|
||||
tunnelConfig?.let { save(it.copy(isMobileDataTunnel = true)) }
|
||||
}
|
||||
tunnelConfigDao.resetMobileDataTunnel()
|
||||
tunnelConfig?.let { save(it.copy(isMobileDataTunnel = true)) }
|
||||
}
|
||||
|
||||
override suspend fun updateEthernetTunnel(tunnelConfig: Domain?) {
|
||||
withContext(ioDispatcher) {
|
||||
tunnelConfigDao.resetEthernetTunnel()
|
||||
tunnelConfig?.let { save(it.copy(isEthernetTunnel = true)) }
|
||||
}
|
||||
tunnelConfigDao.resetEthernetTunnel()
|
||||
tunnelConfig?.let { save(it.copy(isEthernetTunnel = true)) }
|
||||
}
|
||||
|
||||
override suspend fun delete(tunnelConfig: Domain) {
|
||||
withContext(ioDispatcher) { tunnelConfigDao.delete(tunnelConfig.toEntity()) }
|
||||
tunnelConfigDao.delete(tunnelConfig.toEntity())
|
||||
}
|
||||
|
||||
override suspend fun getById(id: Int): Domain? {
|
||||
return withContext(ioDispatcher) { tunnelConfigDao.getById(id.toLong())?.toDomain() }
|
||||
}
|
||||
|
||||
override suspend fun getActive(): List<Domain> {
|
||||
return withContext(ioDispatcher) { tunnelConfigDao.getActive().map { it.toDomain() } }
|
||||
return tunnelConfigDao.getById(id.toLong())?.toDomain()
|
||||
}
|
||||
|
||||
override suspend fun getDefaultTunnel(): Domain? {
|
||||
return withContext(ioDispatcher) { tunnelConfigDao.getDefaultTunnel()?.toDomain() }
|
||||
}
|
||||
|
||||
override suspend fun getStartTunnel(): Domain? {
|
||||
return withContext(ioDispatcher) { tunnelConfigDao.getStartTunnel()?.toDomain() }
|
||||
return tunnelConfigDao.getDefaultTunnel()?.toDomain()
|
||||
}
|
||||
|
||||
override suspend fun count(): Int {
|
||||
return withContext(ioDispatcher) { tunnelConfigDao.count().toInt() }
|
||||
return tunnelConfigDao.count().toInt()
|
||||
}
|
||||
|
||||
override suspend fun findByTunnelName(name: String): Domain? {
|
||||
return withContext(ioDispatcher) { tunnelConfigDao.getByName(name)?.toDomain() }
|
||||
return tunnelConfigDao.getByName(name)?.toDomain()
|
||||
}
|
||||
|
||||
override suspend fun findByTunnelNetworksName(name: String): List<Domain> {
|
||||
return withContext(ioDispatcher) {
|
||||
tunnelConfigDao.findByTunnelNetworkName(name).map { it.toDomain() }
|
||||
}
|
||||
return tunnelConfigDao.findByTunnelNetworkName(name).map { it.toDomain() }
|
||||
}
|
||||
|
||||
override suspend fun findByMobileDataTunnel(): List<Domain> {
|
||||
return withContext(ioDispatcher) {
|
||||
tunnelConfigDao.findByMobileDataTunnel().map { it.toDomain() }
|
||||
}
|
||||
return tunnelConfigDao.findByMobileDataTunnel().map { it.toDomain() }
|
||||
}
|
||||
|
||||
override suspend fun findPrimary(): List<Domain> {
|
||||
return withContext(ioDispatcher) { tunnelConfigDao.findByPrimary().map { it.toDomain() } }
|
||||
return tunnelConfigDao.findByPrimary().map { it.toDomain() }
|
||||
}
|
||||
|
||||
override suspend fun delete(tunnels: List<Domain>) {
|
||||
withContext(ioDispatcher) { tunnelConfigDao.delete(tunnels.map { it.toEntity() }) }
|
||||
tunnelConfigDao.delete(tunnels.map { it.toEntity() })
|
||||
}
|
||||
|
||||
override suspend fun ensureGlobalConfigExists() {
|
||||
if (globalTunnelFlow.firstOrNull() == null) {
|
||||
save(Domain.generateDefaultGlobalConfig())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,72 +4,75 @@ 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
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationMonitor
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.AndroidNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelStateHolder
|
||||
import com.zaneschepke.wireguardautotunnel.core.shortcut.DynamicShortcutManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.SelectedTunnelsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||
import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AutoTunnelViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.ConfigEditViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.DnsViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.LicenseViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.LockdownViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.LoggerViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.MonitoringViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.ProxySettingsViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SettingsViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SplitTunnelViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SupportViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelViewModel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.core.annotation.KoinExperimentalAPI
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.core.module.dsl.viewModel
|
||||
import org.koin.core.module.dsl.viewModelOf
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.bind
|
||||
import org.koin.dsl.module
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
class AppModule {
|
||||
|
||||
@Singleton
|
||||
@ApplicationScope
|
||||
@Provides
|
||||
fun providesApplicationScope(
|
||||
@DefaultDispatcher defaultDispatcher: CoroutineDispatcher
|
||||
): CoroutineScope = CoroutineScope(SupervisorJob() + defaultDispatcher)
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideLogCollect(@ApplicationContext context: Context): LogReader {
|
||||
return LogcatReader.init(storageDir = context.filesDir.absolutePath)
|
||||
@OptIn(KoinExperimentalAPI::class)
|
||||
val appModule = module {
|
||||
single<CoroutineScope>(named(Scope.APPLICATION)) {
|
||||
CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
}
|
||||
single<LogReader> { LogcatReader.init(storageDir = androidContext().filesDir.absolutePath) }
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideNotificationService(@ApplicationContext context: Context): NotificationManager {
|
||||
return WireGuardNotification(context)
|
||||
single<PowerManager> {
|
||||
androidContext().getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
}
|
||||
singleOf(::AndroidNotificationService) bind NotificationService::class
|
||||
single { ServiceManager(androidContext()) }
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideShortcutManager(
|
||||
@ApplicationContext context: Context,
|
||||
@IoDispatcher ioDispatcher: CoroutineDispatcher,
|
||||
): ShortcutManager {
|
||||
return DynamicShortcutManager(context, ioDispatcher)
|
||||
}
|
||||
singleOf(::GlobalEffectRepository)
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideNetworkUtils(@IoDispatcher ioDispatcher: CoroutineDispatcher): NetworkUtils {
|
||||
return NetworkUtils(ioDispatcher)
|
||||
}
|
||||
single { FileUtils(androidContext(), get(named(Dispatcher.IO))) }
|
||||
single<ShortcutManager> { DynamicShortcutManager(androidContext(), get(named(Dispatcher.IO))) }
|
||||
singleOf(::SelectedTunnelsRepository)
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideNotificationMonitor(
|
||||
tunnelManager: TunnelManager,
|
||||
notificationManager: NotificationManager,
|
||||
): NotificationMonitor {
|
||||
return NotificationMonitor(tunnelManager, notificationManager)
|
||||
}
|
||||
single { NetworkUtils(get(named(Dispatcher.IO))) }
|
||||
|
||||
@Provides
|
||||
fun providePowerManager(@ApplicationContext context: Context): PowerManager {
|
||||
return context.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
}
|
||||
viewModelOf(::AutoTunnelViewModel)
|
||||
viewModel { (id: Int?) -> ConfigEditViewModel(get(), get(), get(), get(), get(), id) }
|
||||
viewModelOf(::DnsViewModel)
|
||||
viewModelOf(::LicenseViewModel)
|
||||
viewModelOf(::LockdownViewModel)
|
||||
viewModelOf(::LoggerViewModel)
|
||||
viewModelOf(::MonitoringViewModel)
|
||||
viewModelOf(::ProxySettingsViewModel)
|
||||
viewModelOf(::SettingsViewModel)
|
||||
viewModelOf(::SharedAppViewModel)
|
||||
viewModel { (id: Int) -> SplitTunnelViewModel(get(), get(), get(), id) }
|
||||
viewModel { SupportViewModel(get(), get(named(Dispatcher.MAIN)), get()) }
|
||||
viewModel { (id: Int) -> TunnelViewModel(get(), get(), id) }
|
||||
|
||||
singleOf(::AutoTunnelStateHolder)
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.di
|
||||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class TunnelShell
|
||||
|
||||
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class AppShell
|
||||
|
||||
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class Kernel
|
||||
|
||||
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class Userspace
|
||||
|
||||
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class ProxyUserspace
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.zaneschepke.wireguardautotunnel.di
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.AppBoostrapCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.AutoTunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.DnsSettingsCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.ShortcutCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.StartupCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelModeCoordinator
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
|
||||
val coordinatorModule = module {
|
||||
singleOf(::ShortcutCoordinator)
|
||||
singleOf(::TunnelModeCoordinator)
|
||||
singleOf(::StartupCoordinator)
|
||||
singleOf(::AutoTunnelCoordinator)
|
||||
singleOf(::DnsSettingsCoordinator)
|
||||
single {
|
||||
TunnelCoordinator(
|
||||
get(),
|
||||
get(),
|
||||
get(),
|
||||
get(),
|
||||
get(),
|
||||
get(),
|
||||
get(),
|
||||
get(),
|
||||
get(named(Scope.APPLICATION)),
|
||||
)
|
||||
}
|
||||
singleOf(::AppBoostrapCoordinator)
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.di
|
||||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Retention(AnnotationRetention.BINARY) @Qualifier annotation class DefaultDispatcher
|
||||
|
||||
@Retention(AnnotationRetention.BINARY) @Qualifier annotation class IoDispatcher
|
||||
|
||||
@Retention(AnnotationRetention.BINARY) @Qualifier annotation class MainDispatcher
|
||||
|
||||
@Retention(AnnotationRetention.BINARY) @Qualifier annotation class MainImmediateDispatcher
|
||||
|
||||
@Retention(AnnotationRetention.BINARY) @Qualifier annotation class ApplicationScope
|
||||
|
||||
@Retention(AnnotationRetention.BINARY) @Qualifier annotation class ServiceScope
|
||||
-24
@@ -1,24 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.di
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object CoroutinesDispatchersModule {
|
||||
@DefaultDispatcher
|
||||
@Provides
|
||||
fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
|
||||
|
||||
@IoDispatcher @Provides fun providesIoDispatcher(): CoroutineDispatcher = Dispatchers.IO
|
||||
|
||||
@MainDispatcher @Provides fun providesMainDispatcher(): CoroutineDispatcher = Dispatchers.Main
|
||||
|
||||
@MainImmediateDispatcher
|
||||
@Provides
|
||||
fun providesMainImmediateDispatcher(): CoroutineDispatcher = Dispatchers.Main.immediate
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.zaneschepke.wireguardautotunnel.di
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
||||
import com.zaneschepke.wireguardautotunnel.data.DataStoreManager
|
||||
import com.zaneschepke.wireguardautotunnel.data.DatabaseCallback
|
||||
import com.zaneschepke.wireguardautotunnel.data.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.repository.DataStoreAppStateRepository
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.InstalledAndroidPackageRepository
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.RoomAutoTunnelSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.RoomDnsSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.RoomLockdownSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.RoomMonitoringSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.RoomProxySettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.RoomSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.RoomTunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.DnsSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.InstalledPackageRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.LockdownSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.ProxySettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.bind
|
||||
import org.koin.dsl.module
|
||||
|
||||
val databaseModule = module {
|
||||
single<RoomDatabase.Callback> { DatabaseCallback(lazy { get() }) }
|
||||
|
||||
single {
|
||||
Room.databaseBuilder(
|
||||
androidContext(),
|
||||
AppDatabase::class.java,
|
||||
get<Context>().getString(R.string.db_name),
|
||||
)
|
||||
.addMigrations(
|
||||
MIGRATION_23_24(get<DataStoreManager>().dataStore),
|
||||
MIGRATION_25_26,
|
||||
MIGRATION_28_29,
|
||||
)
|
||||
.fallbackToDestructiveMigration(true)
|
||||
.addCallback(get())
|
||||
.build()
|
||||
}
|
||||
|
||||
single { get<AppDatabase>().generalSettingsDao() }
|
||||
single { get<AppDatabase>().lockdownSettingsDao() }
|
||||
single { get<AppDatabase>().dnsSettingsDao() }
|
||||
single { get<AppDatabase>().autoTunnelSettingsDao() }
|
||||
single { get<AppDatabase>().monitoringSettingsDao() }
|
||||
single { get<AppDatabase>().proxySettingsDoa() }
|
||||
single { get<AppDatabase>().tunnelConfigDoa() }
|
||||
|
||||
single { DataStoreManager(androidContext(), get(named(Dispatcher.IO))) }
|
||||
|
||||
single<AppStateRepository> {
|
||||
DataStoreAppStateRepository(get(), get(named(Scope.APPLICATION)), get(named(Dispatcher.IO)))
|
||||
}
|
||||
singleOf(::RoomAutoTunnelSettingsRepository) bind AutoTunnelSettingsRepository::class
|
||||
singleOf(::RoomDnsSettingsRepository) bind DnsSettingsRepository::class
|
||||
singleOf(::RoomLockdownSettingsRepository) bind LockdownSettingsRepository::class
|
||||
singleOf(::RoomMonitoringSettingsRepository) bind MonitoringSettingsRepository::class
|
||||
singleOf(::RoomProxySettingsRepository) bind ProxySettingsRepository::class
|
||||
singleOf(::RoomSettingsRepository) bind GeneralSettingRepository::class
|
||||
singleOf(::RoomTunnelRepository) bind TunnelRepository::class
|
||||
single<InstalledPackageRepository> {
|
||||
InstalledAndroidPackageRepository(androidContext(), get(named(Dispatcher.IO)))
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user