mirror of
https://github.com/wgtunnel/android.git
synced 2026-07-03 14:07:49 +02:00
Compare commits
162 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c8adb380b | |||
| 614f97fd14 | |||
| fbd470f5d2 | |||
| 5f89b2ed31 | |||
| 9503a3284b | |||
| 68c1a19bd3 | |||
| f3bb6667c3 | |||
| 244a990c37 | |||
| cbf07600b4 | |||
| ec8d90d13d | |||
| 85acca8604 | |||
| 0a9773d202 | |||
| 3cb4480a65 | |||
| a7f3255a76 | |||
| 7d7b99f448 | |||
| 74e9e462bb | |||
| 619e3c1cde | |||
| 77f8a8215b | |||
| 8772036dd7 | |||
| 63625ccbd7 | |||
| 9ac7ae77b3 | |||
| e062fbb34d | |||
| 16d5586433 | |||
| 48a3ad64f4 | |||
| e5796d641d | |||
| daf5eebdd2 | |||
| 4c725491f4 | |||
| 7529c11172 | |||
| 83f530df42 | |||
| 8083ab9526 | |||
| 7d1312da0f | |||
| d4dbc43c70 | |||
| 294f2624c7 | |||
| 0603cb2fdd | |||
| 48ddbcbb0e | |||
| e6c3e3f5b3 | |||
| 0d75699b40 | |||
| 5c98aab9e0 | |||
| a1e3489ba2 | |||
| bcd19b5494 | |||
| 160a6ca84d | |||
| aaf7ebd326 | |||
| b8c75a45e4 | |||
| ac17a09e19 | |||
| c51a7ee393 | |||
| c534516e33 | |||
| 9c999cc62c | |||
| cc3c865211 | |||
| 8648a67fdc | |||
| 9ee1fa69ed | |||
| 379ffdcbbf | |||
| 6e3c1324b2 | |||
| 660bea0104 | |||
| 2b8610fa8a | |||
| 944034ac74 | |||
| 9f394aeffb | |||
| 6c3c6891eb | |||
| af1848f12d | |||
| 96cffdfa7d | |||
| afebd975ea | |||
| 588a2a18bd | |||
| 221b38a119 | |||
| 0008d8b9bb | |||
| 9f85638b9a | |||
| fe54c9cd0e | |||
| 554499f9de | |||
| 12c9b52653 | |||
| 03712a6c1d | |||
| 5f03a97fcc | |||
| 6788b05fa0 | |||
| 9494853dee | |||
| 01695c3286 | |||
| bb6e45ed92 | |||
| 63e257f419 | |||
| e145cd95e1 | |||
| 7e790acbfe | |||
| 334aaa1c2b | |||
| 6201671dd0 | |||
| 517d90c3bf | |||
| d78443e7fa | |||
| 40d0466c14 | |||
| 5220c1a10c | |||
| 0e4e421628 | |||
| abdbf74755 | |||
| 5bc49eec50 | |||
| c7040b8081 | |||
| 26ecfec3fc | |||
| 5408cf3954 | |||
| 22c4a303fc | |||
| 89435dc648 | |||
| 1af404329a | |||
| acb14d4b61 | |||
| c453ae9e0a | |||
| bf9fca953c | |||
| 923f29c27b | |||
| 40a76bb670 | |||
| 4c320f89a6 | |||
| 7789e73951 | |||
| a8d7518881 | |||
| b0fe9d8520 | |||
| d80ea167f4 | |||
| cbc582df53 | |||
| 94d78a60c7 | |||
| e790959a3d | |||
| c03a8bbf94 | |||
| e17b6e322f | |||
| 7d0fda332f | |||
| 443f450f46 | |||
| 6066eb2e7e | |||
| 971ded36bb | |||
| e35a8657e6 | |||
| 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 |
@@ -70,15 +70,16 @@ jobs:
|
|||||||
outputs:
|
outputs:
|
||||||
UPLOAD_DIR_ANDROID: ${{ env.UPLOAD_DIR_ANDROID }}
|
UPLOAD_DIR_ANDROID: ${{ env.UPLOAD_DIR_ANDROID }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
- name: Set up JDK 17
|
- name: Set up JDK 21
|
||||||
uses: actions/setup-java@v5
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: '17'
|
java-version: '21'
|
||||||
cache: gradle
|
cache: gradle
|
||||||
|
|
||||||
- name: Grant execute permission for gradlew
|
- name: Grant execute permission for gradlew
|
||||||
@@ -86,7 +87,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Decode Keystore
|
- name: Decode Keystore
|
||||||
id: decode_keystore
|
id: decode_keystore
|
||||||
uses: timheuer/base64-to-file@v1.2
|
uses: timheuer/base64-to-file@v2.0
|
||||||
with:
|
with:
|
||||||
fileName: ${{ env.KEY_STORE_FILE }}
|
fileName: ${{ env.KEY_STORE_FILE }}
|
||||||
fileDir: ${{ env.KEY_STORE_LOCATION }}
|
fileDir: ${{ env.KEY_STORE_LOCATION }}
|
||||||
@@ -122,7 +123,7 @@ jobs:
|
|||||||
echo "path=$AAB_PATH" >> $GITHUB_OUTPUT
|
echo "path=$AAB_PATH" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Upload AAB Artifact
|
- name: Upload AAB Artifact
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: google-play-aab
|
name: google-play-aab
|
||||||
path: ${{ steps.aab-path.outputs.path }}
|
path: ${{ steps.aab-path.outputs.path }}
|
||||||
|
|||||||
@@ -72,20 +72,21 @@ jobs:
|
|||||||
outputs:
|
outputs:
|
||||||
UPLOAD_DIR_ANDROID: ${{ env.UPLOAD_DIR_ANDROID }}
|
UPLOAD_DIR_ANDROID: ${{ env.UPLOAD_DIR_ANDROID }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Set up JDK 17
|
submodules: recursive
|
||||||
|
- name: Set up JDK 21
|
||||||
uses: actions/setup-java@v5
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: '17'
|
java-version: '21'
|
||||||
cache: gradle
|
cache: gradle
|
||||||
- name: Grant execute permission for gradlew
|
- name: Grant execute permission for gradlew
|
||||||
run: chmod +x gradlew
|
run: chmod +x gradlew
|
||||||
- name: Decode Keystore
|
- name: Decode Keystore
|
||||||
id: decode_keystore
|
id: decode_keystore
|
||||||
uses: timheuer/base64-to-file@v1.2
|
uses: timheuer/base64-to-file@v2.0
|
||||||
with:
|
with:
|
||||||
fileName: ${{ env.KEY_STORE_FILE }}
|
fileName: ${{ env.KEY_STORE_FILE }}
|
||||||
fileDir: ${{ env.KEY_STORE_LOCATION }}
|
fileDir: ${{ env.KEY_STORE_LOCATION }}
|
||||||
@@ -111,11 +112,14 @@ jobs:
|
|||||||
./gradlew :app:assemble${flavor^}Debug --stacktrace
|
./gradlew :app:assemble${flavor^}Debug --stacktrace
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
env:
|
||||||
|
GITHUB_SHA: ${{ github.sha }}
|
||||||
|
GITHUB_RUN_NUMBER: ${{ github.run_number }}
|
||||||
- name: Get release apk path
|
- name: Get release apk path
|
||||||
id: apk-path
|
id: apk-path
|
||||||
run: echo "path=$(find . -regex '^.*/build/outputs/apk/${{ inputs.flavor }}/${{ inputs.build_type }}/.*\.apk$' -type f | head -1 | tail -c+2)" >> $GITHUB_OUTPUT
|
run: echo "path=$(find . -regex '^.*/build/outputs/apk/${{ inputs.flavor }}/${{ inputs.build_type }}/.*\.apk$' -type f | head -1 | tail -c+2)" >> $GITHUB_OUTPUT
|
||||||
- name: Upload All APK Artifacts
|
- name: Upload All APK Artifacts
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: android_artifacts_${{ inputs.flavor }}
|
name: android_artifacts_${{ inputs.flavor }}
|
||||||
path: >-
|
path: >-
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ jobs:
|
|||||||
has_new_commits: ${{ steps.check.outputs.new_commits }}
|
has_new_commits: ${{ steps.check.outputs.new_commits }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
- name: Check for new commits
|
- name: Check for new commits
|
||||||
id: check
|
id: check
|
||||||
env:
|
env:
|
||||||
@@ -43,7 +43,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
- name: Install system dependencies
|
- name: Install system dependencies
|
||||||
run: |
|
run: |
|
||||||
@@ -71,7 +73,7 @@ jobs:
|
|||||||
run: mkdir ${{ github.workspace }}/temp
|
run: mkdir ${{ github.workspace }}/temp
|
||||||
|
|
||||||
- name: Download artifacts
|
- name: Download artifacts
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
pattern: android_artifacts_*
|
pattern: android_artifacts_*
|
||||||
path: ${{ github.workspace }}/temp
|
path: ${{ github.workspace }}/temp
|
||||||
@@ -101,7 +103,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Create nightly release
|
- name: Create nightly release
|
||||||
id: create_release
|
id: create_release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v3
|
||||||
with:
|
with:
|
||||||
body: |
|
body: |
|
||||||
${{ env.RELEASE_NOTES }}
|
${{ env.RELEASE_NOTES }}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
name: notifications
|
name: notifications
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
packages: write
|
packages: write
|
||||||
@@ -12,6 +13,9 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
notify:
|
notify:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
PROJECT_NAME: Android
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Send to Telegram - New Issue
|
- name: Send to Telegram - New Issue
|
||||||
if: github.event_name == 'issues' && github.event.action == 'opened'
|
if: github.event_name == 'issues' && github.event.action == 'opened'
|
||||||
@@ -22,8 +26,8 @@ jobs:
|
|||||||
BODY: ${{ github.event.issue.body || 'No body provided' }}
|
BODY: ${{ github.event.issue.body || 'No body provided' }}
|
||||||
URL: ${{ github.event.issue.html_url }}
|
URL: ${{ github.event.issue.html_url }}
|
||||||
run: |
|
run: |
|
||||||
BODY_TRUNC="${BODY:0:200}" # Truncate to avoid spam
|
BODY_TRUNC="${BODY:0:200}"
|
||||||
TEXT=$(echo -e "🆕 New Issue #$NUMBER: *$TITLE* by $USER\n\n$BODY_TRUNC\n\n[View Issue]($URL)")
|
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" \
|
curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN }}/sendMessage" \
|
||||||
-d chat_id="${{ vars.TELEGRAM_CHAT_ID }}" \
|
-d chat_id="${{ vars.TELEGRAM_CHAT_ID }}" \
|
||||||
${{ vars.TELEGRAM_THREAD_ID && format('-d message_thread_id="{0}"', vars.TELEGRAM_THREAD_ID) || '' }} \
|
${{ vars.TELEGRAM_THREAD_ID && format('-d message_thread_id="{0}"', vars.TELEGRAM_THREAD_ID) || '' }} \
|
||||||
@@ -38,7 +42,7 @@ jobs:
|
|||||||
USER: ${{ github.event.issue.user.login }}
|
USER: ${{ github.event.issue.user.login }}
|
||||||
URL: ${{ github.event.issue.html_url }}
|
URL: ${{ github.event.issue.html_url }}
|
||||||
run: |
|
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" \
|
curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN }}/sendMessage" \
|
||||||
-d chat_id="${{ vars.TELEGRAM_CHAT_ID }}" \
|
-d chat_id="${{ vars.TELEGRAM_CHAT_ID }}" \
|
||||||
${{ vars.TELEGRAM_THREAD_ID && format('-d message_thread_id="{0}"', vars.TELEGRAM_THREAD_ID) || '' }} \
|
${{ vars.TELEGRAM_THREAD_ID && format('-d message_thread_id="{0}"', vars.TELEGRAM_THREAD_ID) || '' }} \
|
||||||
@@ -54,7 +58,7 @@ jobs:
|
|||||||
URL: ${{ github.event.release.html_url }}
|
URL: ${{ github.event.release.html_url }}
|
||||||
ACTION: ${{ github.event.action }}
|
ACTION: ${{ github.event.action }}
|
||||||
run: |
|
run: |
|
||||||
BODY_TRUNC="${BODY:0:200}" # Truncate to avoid spam
|
BODY_TRUNC="${BODY:0:200}"
|
||||||
if [ "$ACTION" == "prereleased" ]; then
|
if [ "$ACTION" == "prereleased" ]; then
|
||||||
ICON="🌙"
|
ICON="🌙"
|
||||||
PREFIX="New Nightly Release"
|
PREFIX="New Nightly Release"
|
||||||
@@ -62,7 +66,7 @@ jobs:
|
|||||||
ICON="🚀"
|
ICON="🚀"
|
||||||
PREFIX="New Release"
|
PREFIX="New Release"
|
||||||
fi
|
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" \
|
curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN }}/sendMessage" \
|
||||||
-d chat_id="${{ vars.TELEGRAM_CHAT_ID }}" \
|
-d chat_id="${{ vars.TELEGRAM_CHAT_ID }}" \
|
||||||
${{ vars.TELEGRAM_THREAD_ID && format('-d message_thread_id="{0}"', vars.TELEGRAM_THREAD_ID) || '' }} \
|
${{ vars.TELEGRAM_THREAD_ID && format('-d message_thread_id="{0}"', vars.TELEGRAM_THREAD_ID) || '' }} \
|
||||||
@@ -78,8 +82,8 @@ jobs:
|
|||||||
BODY: ${{ github.event.issue.body || 'No body provided' }}
|
BODY: ${{ github.event.issue.body || 'No body provided' }}
|
||||||
URL: ${{ github.event.issue.html_url }}
|
URL: ${{ github.event.issue.html_url }}
|
||||||
run: |
|
run: |
|
||||||
PLAIN_MESSAGE=$(echo -e "🆕 New Issue #$NUMBER: $TITLE by $USER\n\n$BODY\n\nView Issue: $URL")
|
PLAIN_MESSAGE=$(echo -e "🆕 **${PROJECT_NAME}** — 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>")
|
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}"
|
PLAIN_MESSAGE="${PLAIN_MESSAGE:0:220}"
|
||||||
PAYLOAD=$(jq -n --arg body "$PLAIN_MESSAGE" --arg formatted "$HTML_MESSAGE" '{
|
PAYLOAD=$(jq -n --arg body "$PLAIN_MESSAGE" --arg formatted "$HTML_MESSAGE" '{
|
||||||
"msgtype": "m.text",
|
"msgtype": "m.text",
|
||||||
@@ -101,8 +105,8 @@ jobs:
|
|||||||
USER: ${{ github.event.issue.user.login }}
|
USER: ${{ github.event.issue.user.login }}
|
||||||
URL: ${{ github.event.issue.html_url }}
|
URL: ${{ github.event.issue.html_url }}
|
||||||
run: |
|
run: |
|
||||||
PLAIN_MESSAGE=$(echo -e "✅ Issue Closed #$NUMBER: $TITLE by $USER\n\nView Issue: $URL")
|
PLAIN_MESSAGE=$(echo -e "✅ **${PROJECT_NAME}** — 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>")
|
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}"
|
PLAIN_MESSAGE="${PLAIN_MESSAGE:0:220}"
|
||||||
PAYLOAD=$(jq -n --arg body "$PLAIN_MESSAGE" --arg formatted "$HTML_MESSAGE" '{
|
PAYLOAD=$(jq -n --arg body "$PLAIN_MESSAGE" --arg formatted "$HTML_MESSAGE" '{
|
||||||
"msgtype": "m.text",
|
"msgtype": "m.text",
|
||||||
@@ -132,8 +136,8 @@ jobs:
|
|||||||
ICON="🚀"
|
ICON="🚀"
|
||||||
PREFIX="New Release"
|
PREFIX="New Release"
|
||||||
fi
|
fi
|
||||||
PLAIN_MESSAGE=$(echo -e "$ICON $PREFIX $NAME ($TAG)\n\n$BODY\n\nView Release: $URL")
|
PLAIN_MESSAGE=$(echo -e "$ICON **${PROJECT_NAME}** — $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>")
|
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}"
|
PLAIN_MESSAGE="${PLAIN_MESSAGE:0:220}"
|
||||||
PAYLOAD=$(jq -n --arg body "$PLAIN_MESSAGE" --arg formatted "$HTML_MESSAGE" '{
|
PAYLOAD=$(jq -n --arg body "$PLAIN_MESSAGE" --arg formatted "$HTML_MESSAGE" '{
|
||||||
"msgtype": "m.text",
|
"msgtype": "m.text",
|
||||||
|
|||||||
@@ -1,25 +1,30 @@
|
|||||||
name: on-pr
|
name: on-pr
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
format_check:
|
format_check:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
- name: Set up JDK 17
|
|
||||||
|
- name: Verify Gradle Wrapper
|
||||||
|
uses: gradle/actions/wrapper-validation@v6
|
||||||
|
|
||||||
|
- name: Set up JDK 21
|
||||||
uses: actions/setup-java@v5
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: '17'
|
java-version: '21'
|
||||||
cache: gradle
|
cache: gradle
|
||||||
|
|
||||||
- name: Grant execute permission for gradlew
|
- name: Grant execute permission for gradlew
|
||||||
run: chmod +x gradlew
|
run: chmod +x gradlew
|
||||||
|
|
||||||
- name: Run ktfmt
|
- name: Run ktfmt
|
||||||
run: ./gradlew ktfmtCheck
|
run: ./gradlew ktfmtCheck
|
||||||
@@ -78,7 +78,7 @@ jobs:
|
|||||||
name: publish-github
|
name: publish-github
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event_name == 'push' && github.ref || 'master' }}
|
ref: ${{ github.event_name == 'push' && github.ref || 'master' }}
|
||||||
- name: Install system dependencies
|
- name: Install system dependencies
|
||||||
@@ -115,7 +115,7 @@ jobs:
|
|||||||
run: mkdir ${{ github.workspace }}/temp
|
run: mkdir ${{ github.workspace }}/temp
|
||||||
|
|
||||||
- name: Download artifacts
|
- name: Download artifacts
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
pattern: android_artifacts_*
|
pattern: android_artifacts_*
|
||||||
path: ${{ github.workspace }}/temp
|
path: ${{ github.workspace }}/temp
|
||||||
@@ -143,7 +143,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Create Release
|
- name: Create Release
|
||||||
id: create_release
|
id: create_release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v3
|
||||||
with:
|
with:
|
||||||
body: |
|
body: |
|
||||||
${{ env.RELEASE_NOTES }}
|
${{ env.RELEASE_NOTES }}
|
||||||
@@ -187,57 +187,61 @@ jobs:
|
|||||||
repository: wgtunnel/fdroid
|
repository: wgtunnel/fdroid
|
||||||
event-type: fdroid-update
|
event-type: fdroid-update
|
||||||
|
|
||||||
|
build-google-aab:
|
||||||
|
if: >-
|
||||||
|
${{
|
||||||
|
github.event_name == 'push' ||
|
||||||
|
inputs.track != 'none'
|
||||||
|
}}
|
||||||
|
uses: ./.github/workflows/build-aab.yml
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
build_type: release
|
||||||
|
flavor: google
|
||||||
|
|
||||||
publish-play:
|
publish-play:
|
||||||
if: ${{ github.event_name == 'push' || inputs.track != 'none' }}
|
if: ${{ github.event_name == 'push' || inputs.track != 'none' }}
|
||||||
name: Publish to Google Play
|
name: Publish to Google Play
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
needs: build-google-aab
|
||||||
env:
|
|
||||||
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
|
|
||||||
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
|
|
||||||
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
|
|
||||||
KEY_STORE_FILE: 'android_keystore.jks'
|
|
||||||
KEY_STORE_LOCATION: ${{ github.workspace }}/app/keystore/
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
- name: Set up JDK 17
|
|
||||||
uses: actions/setup-java@v5
|
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
ref: ${{ github.event_name == 'push' && github.ref || 'master' }}
|
||||||
java-version: '17'
|
|
||||||
cache: gradle
|
|
||||||
|
|
||||||
- name: Grant execute permission for gradlew
|
- name: Download AAB artifact
|
||||||
run: chmod +x gradlew
|
uses: actions/download-artifact@v8
|
||||||
|
|
||||||
# Here we need to decode keystore.jks from base64 string and place it
|
|
||||||
# in the folder specified in the release signing configuration
|
|
||||||
- name: Decode Keystore
|
|
||||||
id: decode_keystore
|
|
||||||
uses: timheuer/base64-to-file@v1.2
|
|
||||||
with:
|
with:
|
||||||
fileName: ${{ env.KEY_STORE_FILE }}
|
name: google-play-aab
|
||||||
fileDir: ${{ env.KEY_STORE_LOCATION }}
|
path: ${{ github.workspace }}/aab
|
||||||
encodedString: ${{ secrets.KEYSTORE }}
|
|
||||||
|
|
||||||
# create keystore path for gradle to read
|
- name: Find exact AAB file path
|
||||||
- name: Create keystore path env var
|
id: find-aab
|
||||||
run: |
|
run: |
|
||||||
store_path=${{ env.KEY_STORE_LOCATION }}${{ env.KEY_STORE_FILE }}
|
AAB_PATH=$(find "${{ github.workspace }}/aab" -name "*.aab" -type f | head -1)
|
||||||
echo "KEY_STORE_PATH=$store_path" >> $GITHUB_ENV
|
if [ -z "$AAB_PATH" ]; then
|
||||||
|
echo "ERROR: No .aab file found after download!"
|
||||||
|
find "${{ github.workspace }}/aab" -type f
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Found AAB: $AAB_PATH"
|
||||||
|
echo "aab_path=$AAB_PATH" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Create service_account.json
|
- name: Create service_account.json
|
||||||
id: createServiceAccount
|
|
||||||
run: echo '${{ secrets.SERVICE_ACCOUNT_JSON }}' > service_account.json
|
run: echo '${{ secrets.SERVICE_ACCOUNT_JSON }}' > service_account.json
|
||||||
|
|
||||||
- name: Deploy with fastlane
|
- name: Set up Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: '3.2' # Not needed with a .ruby-version file
|
ruby-version: '3.4'
|
||||||
bundler-cache: true
|
bundler-cache: true
|
||||||
|
|
||||||
- name: Distribute app to Prod track 🚀
|
- name: Upload to Google Play
|
||||||
run: |
|
run: |
|
||||||
track=${{ github.event_name == 'push' && 'production' || inputs.track }}
|
track=${{ github.event_name == 'push' && 'production' || inputs.track }}
|
||||||
(cd ${{ github.workspace }} && bundle install && bundle exec fastlane $track)
|
bundle exec fastlane run upload_to_play_store \
|
||||||
|
track:"$track" \
|
||||||
|
aab:"${{ steps.find-aab.outputs.aab_path }}" \
|
||||||
|
json_key:"service_account.json" \
|
||||||
|
package_name:"com.zaneschepke.wireguardautotunnel" \
|
||||||
|
skip_upload_apk:true
|
||||||
@@ -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
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
source "https://rubygems.org"
|
source "https://rubygems.org"
|
||||||
|
|
||||||
gem "fastlane"
|
gem "fastlane"
|
||||||
|
gem "multi_json"
|
||||||
@@ -21,7 +21,7 @@ and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/)
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
[](https://play.google.com/store/apps/details?id=com.zaneschepke.wireguardautotunnel)
|
[](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)
|
[](https://apps.obtainium.imranr.dev/redirect?r=obtainium://app/%7B%22id%22%3A%22com.zaneschepke.wireguardautotunnel%22%2C%22url%22%3A%22https%3A%2F%2Fgithub.com%2Fzaneschepke%2Fwgtunnel%22%2C%22author%22%3A%22zaneschepke%22%2C%22name%22%3A%22WG%20Tunnel%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Atrue%2C%5C%22sortMethodChoice%5C%22%3A%5C%22date%5C%22%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Atrue%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22WG%20Tunnel%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22Zane%20Schepke%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%7D%22%2C%22overrideSource%22%3Anull%7D)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -49,8 +49,8 @@ and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/)
|
|||||||
|
|
||||||
## About
|
## About
|
||||||
|
|
||||||
WG Tunnel is an alternative Android client for WireGuard and AmneziaWG, inspired by the official WireGuard Android app. It fills gaps in the official client by adding advanced features like auto-tunneling (on-demand VPN activation), while seamlessly supporting both protocols across app modes—including Kernel (for direct WireGuard kernel integration; AmneziaWG not supported), VPN (standard system-level tunneling), Lockdown (a custom kill switch for leak prevention), and Proxy (built-in HTTP/SOCKS5 forwarding)—for enhanced privacy, censorship resistance, and flexibility.
|
WG Tunnel is an alternative Android client for WireGuard and AmneziaWG, inspired by the official WireGuard Android app. It fills gaps in the official client by adding advanced features like auto-tunneling, AmneziaWG support, different app modes like **Lockdown** (a custom kill switch for leak prevention), and **Local Proxy** (expose a tunnel over a local SOCKS5/HTTP proxy server) for enhanced privacy, censorship resistance, and flexibility.
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="text-align: left;">
|
<div style="text-align: left;">
|
||||||
@@ -67,21 +67,18 @@ WG Tunnel is an alternative Android client for WireGuard and AmneziaWG, inspired
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Tunnel Import Methods**: Easily add tunnels using .conf files, ZIP archives, manual entry, or QR code scanning.
|
- **Auto-Tunneling:** Automatically activate tunnels based on your device's active network details.
|
||||||
- **Auto-Tunneling**: Automatically activate tunnels based on Wi-Fi SSID, Ethernet connections, or mobile data networks.
|
- **Deferred Endpoint Bootstrapping:** Safely resolves endpoints and updates peers after the tunnel is up for better reliability and leak protection on startup.
|
||||||
- **Split Tunneling**: Flexible support for routing specific apps or traffic through the VPN.
|
- **Handshake Monitoring:** Real-time handshake monitoring for instant tunnel health feedback.
|
||||||
- **WireGuard Modes**: Full compatibility with WireGuard in both kernel and userspace implementations.
|
- **AmneziaWG Support:** Full support for AmneziaWG 2.0, providing robust censorship protection.
|
||||||
- **AmneziaWG Integration**: Userspace mode for AmneziaWG, providing robust censorship evasion.
|
- **Split Tunneling:** Flexible support for routing specific apps or traffic through the VPN.
|
||||||
- **Always-On VPN**: Ensures continuous protection with Android's Always-On VPN feature.
|
- **Local Proxy Mode:** Expose WireGuard tunnels over a local SOCKS5 or HTTP proxy to browsers or firewall apps (like AdGuard).
|
||||||
- **Quick Controls**: Quick Settings tile and home screen shortcuts for easy VPN toggling.
|
- **Lockdown Mode:** Advanced in-app kill switch that blocks all traffic while the tunnel is down.
|
||||||
- **Automation Support**: Intent-based automation for controlling tunnels.
|
- **Quick Controls:** Quick Settings tile and home screen shortcuts for easy toggling.
|
||||||
- **Auto-Restore**: Seamlessly restores auto-tunneling and active tunnels after device restarts or app updates.
|
- **Remote Control Support:** Intent-based automation for controlling tunnels and auto-tunneling from automation apps (like Tasker).
|
||||||
- **Proxying Options**: Built-in HTTP and SOCKS5 proxy support within tunnels.
|
- **Dynamic DNS Handling:** Automatically detect and update endpoints on server IP changes without requiring a restart.
|
||||||
- **Lockdown Mode**: Custom kill switch for maximum leak prevention and security.
|
- **IPv6 Endpoints:** Automatically upgrade to IPv6 endpoints or fall back to IPv4 based on network conditions without requiring a restart.
|
||||||
- **Dynamic DNS Handling**: Detects and updates DNS changes without tunnel restarts.
|
- **Android TV Support:** Full support for nearly all features on Android TV.
|
||||||
- **Monitoring Tools**: Advanced tunnel monitoring features for tunnel performance monitoring.
|
|
||||||
- **Android TV Support**: Android TV support for secure streaming and browsing.
|
|
||||||
- **Advanced DNS**: DNS over HTTPS support for tunnel endpoint resolutions.
|
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
|
|||||||
+67
-52
@@ -1,9 +1,8 @@
|
|||||||
import com.android.build.gradle.internal.api.BaseVariantOutputImpl
|
import com.android.build.api.dsl.ApplicationExtension
|
||||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
import com.android.build.api.variant.FilterConfiguration
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
alias(libs.plugins.kotlin.android)
|
|
||||||
alias(libs.plugins.kotlinxSerialization)
|
alias(libs.plugins.kotlinxSerialization)
|
||||||
alias(libs.plugins.ksp)
|
alias(libs.plugins.ksp)
|
||||||
alias(libs.plugins.compose.compiler)
|
alias(libs.plugins.compose.compiler)
|
||||||
@@ -11,7 +10,19 @@ plugins {
|
|||||||
alias(libs.plugins.licensee)
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
configure<ApplicationExtension> {
|
||||||
namespace = Constants.APP_ID
|
namespace = Constants.APP_ID
|
||||||
compileSdk = Constants.TARGET_SDK
|
compileSdk = Constants.TARGET_SDK
|
||||||
|
|
||||||
@@ -22,17 +33,16 @@ android {
|
|||||||
includeInBundle = false
|
includeInBundle = false
|
||||||
}
|
}
|
||||||
|
|
||||||
ksp { arg("room.schemaLocation", "$projectDir/schemas") }
|
|
||||||
|
|
||||||
// fix okhttp proguard issue
|
// fix okhttp proguard issue
|
||||||
packaging { resources { pickFirsts.add("okhttp3/internal/publicsuffix/publicsuffixes.gz") } }
|
packaging { resources { pickFirsts.add("okhttp3/internal/publicsuffix/publicsuffixes.gz") } }
|
||||||
|
|
||||||
splits {
|
splits {
|
||||||
abi {
|
abi {
|
||||||
isEnable = !project.hasProperty("noSplits")
|
val noSplits = providers.gradleProperty("noSplits").isPresent
|
||||||
|
isEnable = !noSplits
|
||||||
reset()
|
reset()
|
||||||
include("armeabi-v7a", "arm64-v8a")
|
include("armeabi-v7a", "arm64-v8a")
|
||||||
isUniversalApk = !project.hasProperty("noSplits")
|
isUniversalApk = !noSplits
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,12 +50,17 @@ android {
|
|||||||
applicationId = Constants.APP_ID
|
applicationId = Constants.APP_ID
|
||||||
minSdk = Constants.MIN_SDK
|
minSdk = Constants.MIN_SDK
|
||||||
targetSdk = Constants.TARGET_SDK
|
targetSdk = Constants.TARGET_SDK
|
||||||
versionCode = computeVersionCode()
|
versionCode = Constants.VERSION_CODE
|
||||||
versionName = computeVersionName()
|
versionName = Constants.VERSION_NAME
|
||||||
|
|
||||||
sourceSets { getByName("debug").assets.srcDirs(files("$projectDir/schemas")) }
|
experimentalProperties["android.experimental.disableGitVersion"] = true
|
||||||
|
|
||||||
val languagesArray = buildLanguagesArray(languageList())
|
sourceSets {
|
||||||
|
getByName("debug").assets.directories += "$projectDir/schemas"
|
||||||
|
}
|
||||||
|
|
||||||
|
val languagesProvider = project.languageListProvider()
|
||||||
|
val languagesArray = buildLanguagesArray(languagesProvider.get())
|
||||||
buildConfigField("String[]", "LANGUAGES", "new String[]{ $languagesArray }")
|
buildConfigField("String[]", "LANGUAGES", "new String[]{ $languagesArray }")
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
@@ -115,56 +130,56 @@ android {
|
|||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
isCoreLibraryDesugaringEnabled = true
|
isCoreLibraryDesugaringEnabled = true
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
|
||||||
}
|
|
||||||
|
|
||||||
kotlin {
|
|
||||||
compilerOptions {
|
|
||||||
jvmTarget = JvmTarget.JVM_17
|
|
||||||
freeCompilerArgs = listOf("-XXLanguage:+PropertyParamAnnotationDefaultTargetMode")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose = true
|
compose = true
|
||||||
buildConfig = true
|
buildConfig = true
|
||||||
|
resValues = true
|
||||||
}
|
}
|
||||||
packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } }
|
packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } }
|
||||||
|
}
|
||||||
|
|
||||||
licensee {
|
androidComponents {
|
||||||
allowedLicenses().forEach { allow(it) }
|
onVariants { variant ->
|
||||||
allowedLicenseUrls().forEach { allowUrl(it) }
|
val isNightly = project.isNightlyBuild()
|
||||||
// foss, but missing license
|
|
||||||
ignoreDependencies("com.github.T8RIN.QuickieExtended")
|
|
||||||
}
|
|
||||||
|
|
||||||
android.applicationVariants.all {
|
if (isNightly) {
|
||||||
val variant = this
|
variant.outputs.forEach { output ->
|
||||||
|
|
||||||
val abiNameMap =
|
output.versionCode.set(
|
||||||
mapOf(
|
output.versionCode.get() + project.getVersionCodeIncrement()
|
||||||
"armeabi-v7a" to "armv7",
|
)
|
||||||
"arm64-v8a" to "arm64",
|
|
||||||
"x86" to "x86",
|
|
||||||
"x86_64" to "x64",
|
|
||||||
)
|
|
||||||
|
|
||||||
variant.outputs.all {
|
val currentVersion = output.versionName.get()
|
||||||
val output = this as BaseVariantOutputImpl
|
val nextVersion = bumpToNextPatchVersion(currentVersion)
|
||||||
val abi = output.getFilter("ABI")
|
val gitHash = project.getGitCommitHash()
|
||||||
|
|
||||||
val baseFileName = "${Constants.APP_NAME}-${variant.flavorName}-v${variant.versionName}"
|
output.versionName.set("$nextVersion-nightly+git.$gitHash")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val outputFileName =
|
val abiNameMap = mapOf(
|
||||||
if (!abi.isNullOrEmpty()) {
|
"armeabi-v7a" to "armv7",
|
||||||
val shortAbiName = abiNameMap.getOrDefault(abi, abi)
|
"arm64-v8a" to "arm64",
|
||||||
"${baseFileName}-${shortAbiName}.apk"
|
"x86" to "x86",
|
||||||
} else {
|
"x86_64" to "x64",
|
||||||
"${baseFileName}.apk"
|
)
|
||||||
}
|
|
||||||
|
|
||||||
output.outputFileName = outputFileName
|
variant.outputs.forEach { output ->
|
||||||
|
val abi = output.filters.find { it.filterType == FilterConfiguration.FilterType.ABI }?.identifier
|
||||||
|
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()) {
|
||||||
|
val shortAbiName = abiNameMap.getOrDefault(abi, abi)
|
||||||
|
"${baseFileName}-${shortAbiName}.apk"
|
||||||
|
} else {
|
||||||
|
"${baseFileName}.apk"
|
||||||
|
}
|
||||||
|
|
||||||
|
output.outputFileName.set(outputFileName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -172,6 +187,7 @@ android {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":logcatter"))
|
implementation(project(":logcatter"))
|
||||||
implementation(project(":networkmonitor"))
|
implementation(project(":networkmonitor"))
|
||||||
|
implementation(project(":tunnel"))
|
||||||
|
|
||||||
// Core foundations
|
// Core foundations
|
||||||
implementation(libs.bundles.androidx.core.full)
|
implementation(libs.bundles.androidx.core.full)
|
||||||
@@ -208,14 +224,13 @@ dependencies {
|
|||||||
// State management
|
// State management
|
||||||
implementation(libs.bundles.orbit.mvi)
|
implementation(libs.bundles.orbit.mvi)
|
||||||
|
|
||||||
// Tunnel
|
|
||||||
implementation(libs.bundles.wireguard.tunnel)
|
|
||||||
|
|
||||||
// Shizuku
|
// Shizuku
|
||||||
implementation(libs.bundles.shizuku)
|
implementation(libs.bundles.shizuku)
|
||||||
|
|
||||||
// UI utilities
|
// UI utilities
|
||||||
implementation(libs.bundles.ui.utilities)
|
implementation(libs.bundles.ui.utilities)
|
||||||
|
implementation(libs.lottie.compose)
|
||||||
|
implementation(libs.sonner)
|
||||||
|
|
||||||
// Misc utilities
|
// Misc utilities
|
||||||
implementation(libs.bundles.misc.utilities)
|
implementation(libs.bundles.misc.utilities)
|
||||||
@@ -268,7 +283,7 @@ tasks.register<Copy>("copyLicenseeJsonToAssets") {
|
|||||||
tasks.named("preBuild") { dependsOn("copyLicenseeJsonToAssets") }
|
tasks.named("preBuild") { dependsOn("copyLicenseeJsonToAssets") }
|
||||||
|
|
||||||
// https://gist.github.com/obfusk/61046e09cee352ae6dd109911534b12e#fix-proposed-by-linsui-disable-baseline-profiles
|
// https://gist.github.com/obfusk/61046e09cee352ae6dd109911534b12e#fix-proposed-by-linsui-disable-baseline-profiles
|
||||||
tasks.whenTaskAdded {
|
tasks.configureEach {
|
||||||
if (name.contains("ArtProfile")) {
|
if (name.contains("ArtProfile")) {
|
||||||
enabled = false
|
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')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,513 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 31,
|
||||||
|
"identityHash": "1dee3799f1c6526c48723fd2fee58d11",
|
||||||
|
"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, `disable_on_captive_portal` INTEGER NOT NULL DEFAULT 1)",
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "disableTunnelOnCaptivePortal",
|
||||||
|
"columnName": "disable_on_captive_portal",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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, '1dee3799f1c6526c48723fd2fee58d11')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,13 +6,11 @@
|
|||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
|
||||||
<!--for split tunneling-->
|
<!--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-->
|
<!--foreground service special use for non VPN service tunnels, android 14-->
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||||
<!--foreground service special use for VPN service tunnels, android 14-->
|
|
||||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
|
|
||||||
|
|
||||||
<!--foreground service permissions-->
|
<!--foreground service permissions-->
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
@@ -55,11 +53,13 @@
|
|||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.App.Start"
|
android:theme="@style/Theme.App.Start"
|
||||||
tools:targetApi="tiramisu">
|
tools:targetApi="tiramisu">
|
||||||
|
|
||||||
|
<meta-data android:name="android.telephony.PROPERTY_SATELLITE_DATA_OPTIMIZED"
|
||||||
|
android:value="${applicationId}" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
@@ -73,6 +73,13 @@
|
|||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
<data android:scheme="wg" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<action android:name="android.intent.action.SHOW_APP_INFO" />
|
<action android:name="android.intent.action.SHOW_APP_INFO" />
|
||||||
@@ -81,6 +88,60 @@
|
|||||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||||
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
|
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<!-- .zip files -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<action android:name="android.intent.action.EDIT" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="content" />
|
||||||
|
<data android:host="*" />
|
||||||
|
<data android:mimeType="application/zip" />
|
||||||
|
|
||||||
|
<data android:scheme="content" />
|
||||||
|
<data android:host="*" />
|
||||||
|
<data android:mimeType="application/x-zip-compressed" />
|
||||||
|
|
||||||
|
<data android:scheme="file" />
|
||||||
|
<data android:host="*" />
|
||||||
|
<data android:mimeType="application/zip" />
|
||||||
|
|
||||||
|
<data android:scheme="file" />
|
||||||
|
<data android:host="*" />
|
||||||
|
<data android:mimeType="application/x-zip-compressed" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<!-- Share sheet for .zip -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.SEND" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<data android:mimeType="application/zip" />
|
||||||
|
<data android:mimeType="application/x-zip-compressed" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<!-- .conf files -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<action android:name="android.intent.action.EDIT" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="content" />
|
||||||
|
<data android:host="*" />
|
||||||
|
<data android:mimeType="*/*" />
|
||||||
|
|
||||||
|
<data android:scheme="file" />
|
||||||
|
<data android:host="*" />
|
||||||
|
<data android:mimeType="*/*" />
|
||||||
|
|
||||||
|
<!-- Path patterns for .conf extension matching -->
|
||||||
|
<data android:pathPattern=".*\\.conf" />
|
||||||
|
<data android:pathPattern=".*\\..*\\.conf" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\.conf" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\..*\\.conf" />
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
@@ -114,9 +175,9 @@
|
|||||||
tools:node="remove" />
|
tools:node="remove" />
|
||||||
</provider>
|
</provider>
|
||||||
<service
|
<service
|
||||||
android:name=".core.service.tile.TunnelControlTile"
|
android:name=".service.tile.TunnelControlTile"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:icon="@drawable/ic_notification"
|
android:icon="@drawable/ic_qs_logo"
|
||||||
android:label="@string/tunnel_control"
|
android:label="@string/tunnel_control"
|
||||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||||
<meta-data
|
<meta-data
|
||||||
@@ -131,9 +192,9 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
<service
|
<service
|
||||||
android:name=".core.service.tile.AutoTunnelControlTile"
|
android:name=".service.tile.AutoTunnelControlTile"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:icon="@drawable/ic_notification"
|
android:icon="@drawable/ic_qs_logo"
|
||||||
android:label="@string/auto_tunnel"
|
android:label="@string/auto_tunnel"
|
||||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||||
<meta-data
|
<meta-data
|
||||||
@@ -148,41 +209,7 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
<service
|
<service
|
||||||
android:name=".core.service.tile.TunnelControlTile"
|
android:name=".service.autotunnel.AutoTunnelService"
|
||||||
android:exported="true"
|
|
||||||
android:icon="@drawable/ic_notification"
|
|
||||||
android:label="@string/tunnel_control"
|
|
||||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
|
||||||
<meta-data
|
|
||||||
android:name="android.service.quicksettings.ACTIVE_TILE"
|
|
||||||
android:value="true" />
|
|
||||||
<meta-data
|
|
||||||
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
|
|
||||||
android:value="true" />
|
|
||||||
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
|
||||||
</intent-filter>
|
|
||||||
</service>
|
|
||||||
<service
|
|
||||||
android:name=".core.service.tile.AutoTunnelControlTile"
|
|
||||||
android:exported="true"
|
|
||||||
android:icon="@drawable/ic_notification"
|
|
||||||
android:label="@string/auto_tunnel"
|
|
||||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
|
||||||
<meta-data
|
|
||||||
android:name="android.service.quicksettings.ACTIVE_TILE"
|
|
||||||
android:value="true" />
|
|
||||||
<meta-data
|
|
||||||
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
|
|
||||||
android:value="true" />
|
|
||||||
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
|
||||||
</intent-filter>
|
|
||||||
</service>
|
|
||||||
<service
|
|
||||||
android:name=".core.service.autotunnel.AutoTunnelService"
|
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:foregroundServiceType="specialUse"
|
android:foregroundServiceType="specialUse"
|
||||||
@@ -197,33 +224,6 @@
|
|||||||
network connectivity monitoring."/>
|
network connectivity monitoring."/>
|
||||||
</service>
|
</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
|
<receiver
|
||||||
android:name=".core.broadcast.RestartReceiver"
|
android:name=".core.broadcast.RestartReceiver"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
@@ -238,14 +238,6 @@
|
|||||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</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-->
|
<!--custom security solution for easier user integration-->
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".core.broadcast.RemoteControlReceiver"
|
android:name=".core.broadcast.RemoteControlReceiver"
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel
|
package com.zaneschepke.wireguardautotunnel
|
||||||
|
|
||||||
import ProxySettingsScreen
|
import ProxySettingsScreen
|
||||||
|
import android.Manifest
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
|
import android.net.Uri
|
||||||
import android.net.VpnService
|
import android.net.VpnService
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.provider.Settings
|
||||||
|
import android.view.WindowManager
|
||||||
import androidx.activity.SystemBarStyle
|
import androidx.activity.SystemBarStyle
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
@@ -17,18 +21,29 @@ import androidx.compose.animation.fadeOut
|
|||||||
import androidx.compose.animation.slideInHorizontally
|
import androidx.compose.animation.slideInHorizontally
|
||||||
import androidx.compose.animation.slideOutHorizontally
|
import androidx.compose.animation.slideOutHorizontally
|
||||||
import androidx.compose.animation.togetherWith
|
import androidx.compose.animation.togetherWith
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.imePadding
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.wrapContentHeight
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.CheckCircleOutline
|
||||||
|
import androidx.compose.material.icons.outlined.ErrorOutline
|
||||||
|
import androidx.compose.material.icons.outlined.FavoriteBorder
|
||||||
|
import androidx.compose.material.icons.outlined.Info
|
||||||
|
import androidx.compose.material.icons.outlined.WarningAmber
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.surfaceColorAtElevation
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -37,31 +52,33 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.LinkAnnotation
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
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.intl.Locale
|
||||||
import androidx.compose.ui.text.style.TextDecoration
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.text.withLink
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.zIndex
|
import androidx.compose.ui.zIndex
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
|
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
|
||||||
import androidx.navigation3.runtime.entryProvider
|
import androidx.navigation3.runtime.entryProvider
|
||||||
import androidx.navigation3.runtime.rememberNavBackStack
|
import androidx.navigation3.runtime.rememberNavBackStack
|
||||||
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
||||||
import androidx.navigation3.ui.NavDisplay
|
import androidx.navigation3.ui.NavDisplay
|
||||||
|
import com.dokar.sonner.TextToastAction
|
||||||
|
import com.dokar.sonner.ToastType
|
||||||
|
import com.dokar.sonner.Toaster
|
||||||
|
import com.dokar.sonner.rememberToasterState
|
||||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||||
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
||||||
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.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||||
@@ -69,19 +86,17 @@ import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
|||||||
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
|
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
|
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.banner.AppAlertBanner
|
import com.zaneschepke.wireguardautotunnel.ui.common.banner.AppAlertBanner
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.LocalNetworkPermissionDialog
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.VpnDeniedDialog
|
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.VpnDeniedDialog
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.CustomSnackBar
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarInfo
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarType
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.rememberCustomSnackbarState
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
|
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.navigation.SecureRoute
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.Tab
|
import com.zaneschepke.wireguardautotunnel.ui.navigation.Tab
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.components.BottomNavbar
|
import com.zaneschepke.wireguardautotunnel.ui.navigation.components.BottomNavbar
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.components.DynamicTopAppBar
|
import com.zaneschepke.wireguardautotunnel.ui.navigation.components.DynamicTopAppBar
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.components.currentRouteAsNavbarState
|
import com.zaneschepke.wireguardautotunnel.ui.navigation.components.currentRouteAsNavbarState
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.functions.rememberNavController
|
import com.zaneschepke.wireguardautotunnel.ui.navigation.functions.rememberNavController
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.AutoTunnelScreen
|
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.detection.WifiDetectionMethodScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.disclosure.LocationDisclosureScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.disclosure.LocationDisclosureScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.preferred.PreferredTunnelScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.preferred.PreferredTunnelScreen
|
||||||
@@ -92,40 +107,57 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.Appear
|
|||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.display.DisplayScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.display.DisplayScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.language.LanguageScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.language.LanguageScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.dns.DnsSettingsScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.dns.DnsSettingsScreen
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.globals.TunnelGlobalsScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.integrations.AndroidIntegrationsScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.integrations.AndroidIntegrationsScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.lockdown.LockdownSettingsScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.lockdown.LockdownSettingsScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.TunnelMonitoringScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.logs.LogsScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.logs.LogsScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.MonitoringScreen
|
||||||
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.SupportScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.donate.DonateScreen
|
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.donate.crypto.AddressesScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.license.LicenseScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.support.license.LicenseScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.TunnelsScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.TunnelsScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.config.ConfigScreen
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.settings.TunnelSettingsScreen
|
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.sort.SortScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.splittunnel.SplitTunnelScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.splittunnel.SplitTunnelScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.AlertRed
|
import com.zaneschepke.wireguardautotunnel.ui.theme.AlertRed
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.theme.Heart
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.OffWhite
|
import com.zaneschepke.wireguardautotunnel.ui.theme.OffWhite
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.theme.Straw
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
|
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||||
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
|
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.installApk
|
import com.zaneschepke.wireguardautotunnel.util.extensions.installApk
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.restartApp
|
import com.zaneschepke.wireguardautotunnel.util.extensions.restartApp
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
|
import com.zaneschepke.wireguardautotunnel.util.permission.LocalNetworkPermissionHelper
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.ConfigViewModel
|
import com.zaneschepke.wireguardautotunnel.viewmodel.ConfigEditViewModel
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
|
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SplitTunnelViewModel
|
import com.zaneschepke.wireguardautotunnel.viewmodel.SplitTunnelViewModel
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelViewModel
|
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelViewModel
|
||||||
|
import de.raphaelebner.roomdatabasebackup.core.OnCompleteListener.Companion.EXIT_CODE_ERROR
|
||||||
|
import de.raphaelebner.roomdatabasebackup.core.OnCompleteListener.Companion.EXIT_CODE_ERROR_DECRYPTION_ERROR
|
||||||
|
import de.raphaelebner.roomdatabasebackup.core.OnCompleteListener.Companion.EXIT_CODE_ERROR_RESTORE_BACKUP_IS_ENCRYPTED
|
||||||
|
import de.raphaelebner.roomdatabasebackup.core.OnCompleteListener.Companion.EXIT_CODE_ERROR_WRONG_DECRYPTION_PASSWORD
|
||||||
import de.raphaelebner.roomdatabasebackup.core.RoomBackup
|
import de.raphaelebner.roomdatabasebackup.core.RoomBackup
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
import kotlinx.coroutines.awaitCancellation
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
import org.koin.core.parameter.parametersOf
|
import org.koin.core.parameter.parametersOf
|
||||||
|
import org.orbitmvi.orbit.compose.collectAsState
|
||||||
|
import timber.log.Timber
|
||||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
@@ -149,7 +181,10 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
roomBackup = RoomBackup(this)
|
roomBackup = RoomBackup(this).database(appDatabase).enableLogDebug(true).maxFileCount(5)
|
||||||
|
|
||||||
|
handleConfigFileIntent(intent)
|
||||||
|
handleWgDeepLinkIntent(intent)
|
||||||
|
|
||||||
installSplashScreen().apply {
|
installSplashScreen().apply {
|
||||||
setKeepOnScreenCondition { !viewModel.container.stateFlow.value.isAppLoaded }
|
setKeepOnScreenCondition { !viewModel.container.stateFlow.value.isAppLoaded }
|
||||||
@@ -158,7 +193,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
setContent {
|
setContent {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val isTv = isRunningOnTv()
|
val isTv = isRunningOnTv()
|
||||||
val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
|
val uiState by viewModel.collectAsState()
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
LaunchedEffect(uiState.isAppLoaded) {
|
LaunchedEffect(uiState.isAppLoaded) {
|
||||||
@@ -167,11 +202,53 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val snackbarState = rememberCustomSnackbarState()
|
val toaster = rememberToasterState()
|
||||||
var showVpnPermissionDialog by remember { mutableStateOf(false) }
|
var showVpnPermissionDialog by remember { mutableStateOf(false) }
|
||||||
var vpnPermissionDenied by remember { mutableStateOf(false) }
|
var vpnPermissionDenied by remember { mutableStateOf(false) }
|
||||||
var requestingAppMode by remember {
|
var requestingTunnelMode by remember {
|
||||||
mutableStateOf<Pair<AppMode?, TunnelConfig?>>(Pair(null, null))
|
mutableStateOf<Pair<TunnelMode?, TunnelConfig?>>(Pair(null, null))
|
||||||
|
}
|
||||||
|
var showLocalNetworkRationale by remember { mutableStateOf(false) }
|
||||||
|
var hasPromptedLocalNetwork by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val localNetworkPermissionLauncher =
|
||||||
|
rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.RequestPermission()
|
||||||
|
) { isGranted ->
|
||||||
|
if (!isGranted) {
|
||||||
|
val canAskAgain =
|
||||||
|
ActivityCompat.shouldShowRequestPermissionRationale(
|
||||||
|
this,
|
||||||
|
Manifest.permission.ACCESS_LOCAL_NETWORK,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!canAskAgain) {
|
||||||
|
val intent =
|
||||||
|
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||||
|
data = Uri.fromParts("package", packageName, null)
|
||||||
|
}
|
||||||
|
startActivity(intent)
|
||||||
|
} else {
|
||||||
|
toaster.show(
|
||||||
|
message =
|
||||||
|
context.getString(R.string.local_network_permission_denied),
|
||||||
|
type = ToastType.Warning,
|
||||||
|
duration = 6000.milliseconds,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(uiState.isAppLoaded) {
|
||||||
|
if (
|
||||||
|
uiState.isAppLoaded &&
|
||||||
|
!hasPromptedLocalNetwork &&
|
||||||
|
LocalNetworkPermissionHelper.shouldRequestPermission() &&
|
||||||
|
!LocalNetworkPermissionHelper.isPermissionGranted(context)
|
||||||
|
) {
|
||||||
|
hasPromptedLocalNetwork = true
|
||||||
|
showLocalNetworkRationale = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val startingStack = buildList {
|
val startingStack = buildList {
|
||||||
@@ -201,14 +278,14 @@ class MainActivity : AppCompatActivity() {
|
|||||||
} else {
|
} else {
|
||||||
vpnPermissionDenied = false
|
vpnPermissionDenied = false
|
||||||
showVpnPermissionDialog = false
|
showVpnPermissionDialog = false
|
||||||
val (appMode, config) = requestingAppMode
|
val (appMode, config) = requestingTunnelMode
|
||||||
when (appMode) {
|
when (appMode) {
|
||||||
AppMode.VPN -> if (config != null) viewModel.startTunnel(config)
|
TunnelMode.VPN -> if (config != null) viewModel.startTunnel(config)
|
||||||
AppMode.LOCK_DOWN -> viewModel.setAppMode(AppMode.LOCK_DOWN)
|
TunnelMode.LOCK_DOWN -> viewModel.setAppMode(TunnelMode.LOCK_DOWN)
|
||||||
else -> Unit
|
else -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
requestingAppMode = Pair(null, null)
|
requestingTunnelMode = Pair(null, null)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -218,27 +295,24 @@ class MainActivity : AppCompatActivity() {
|
|||||||
GlobalSideEffect.ConfigChanged -> restartApp()
|
GlobalSideEffect.ConfigChanged -> restartApp()
|
||||||
GlobalSideEffect.PopBackStack -> navController.pop()
|
GlobalSideEffect.PopBackStack -> navController.pop()
|
||||||
is GlobalSideEffect.RequestVpnPermission -> {
|
is GlobalSideEffect.RequestVpnPermission -> {
|
||||||
requestingAppMode = Pair(sideEffect.requestingMode, sideEffect.config)
|
requestingTunnelMode =
|
||||||
|
Pair(sideEffect.requestingMode, sideEffect.config)
|
||||||
vpnActivity.launch(VpnService.prepare(this@MainActivity))
|
vpnActivity.launch(VpnService.prepare(this@MainActivity))
|
||||||
}
|
}
|
||||||
|
|
||||||
is GlobalSideEffect.Snackbar -> {
|
is GlobalSideEffect.Snackbar -> {
|
||||||
scope.launch {
|
when (sideEffect.type) {
|
||||||
snackbarState.showSnackbar(
|
ToastType.Warning,
|
||||||
SnackbarInfo(
|
ToastType.Error -> toaster.dismissAll()
|
||||||
message =
|
else -> Unit
|
||||||
buildAnnotatedString {
|
|
||||||
append(sideEffect.message.asString(context))
|
|
||||||
},
|
|
||||||
type = sideEffect.type ?: SnackbarType.INFO,
|
|
||||||
durationMs = sideEffect.durationMs ?: 4000L,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
is GlobalSideEffect.Toast ->
|
toaster.show(
|
||||||
scope.launch { context.showToast(sideEffect.message.asString(context)) }
|
message = sideEffect.message.asString(context),
|
||||||
|
type = sideEffect.type,
|
||||||
|
duration = (sideEffect.durationMs ?: 4000L).milliseconds,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
is GlobalSideEffect.LaunchUrl -> context.openWebUrl(sideEffect.url)
|
is GlobalSideEffect.LaunchUrl -> context.openWebUrl(sideEffect.url)
|
||||||
is GlobalSideEffect.InstallApk -> context.installApk(sideEffect.apk)
|
is GlobalSideEffect.InstallApk -> context.installApk(sideEffect.apk)
|
||||||
@@ -266,60 +340,96 @@ class MainActivity : AppCompatActivity() {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
val annotatedMessage = buildAnnotatedString {
|
if (showLocalNetworkRationale) {
|
||||||
append(context.getString(R.string.donation_prompt_prefix))
|
LocalNetworkPermissionDialog(
|
||||||
append(" ")
|
onDismiss = {
|
||||||
withLink(
|
showLocalNetworkRationale = false
|
||||||
LinkAnnotation.Clickable(
|
toaster.show(
|
||||||
tag = context.getString(R.string.support),
|
message =
|
||||||
styles =
|
context.getString(R.string.local_network_permission_denied),
|
||||||
TextLinkStyles(
|
type = ToastType.Warning,
|
||||||
style =
|
duration = 6000.milliseconds,
|
||||||
SpanStyle(
|
)
|
||||||
textDecoration = TextDecoration.Underline,
|
},
|
||||||
color = MaterialTheme.colorScheme.primary,
|
onAttest = {
|
||||||
),
|
showLocalNetworkRationale = false
|
||||||
focusedStyle =
|
|
||||||
SpanStyle(
|
localNetworkPermissionLauncher.launch(
|
||||||
textDecoration = TextDecoration.Underline,
|
Manifest.permission.ACCESS_LOCAL_NETWORK
|
||||||
color = MaterialTheme.colorScheme.primary,
|
)
|
||||||
background =
|
},
|
||||||
MaterialTheme.colorScheme.primary.copy(
|
)
|
||||||
alpha = 0.2f
|
}
|
||||||
),
|
|
||||||
),
|
uiState.pendingWgImportUrl?.let { url ->
|
||||||
),
|
val host = Uri.parse(url).host ?: url
|
||||||
) {
|
InfoDialog(
|
||||||
snackbarState.dismissCurrent()
|
onDismiss = { viewModel.dismissWgImport() },
|
||||||
navController.push(Route.Donate)
|
onAttest = { viewModel.importFromUrl(url) },
|
||||||
}
|
title = stringResource(R.string.add_from_url),
|
||||||
) {
|
body = { Text(stringResource(R.string.wg_url_confirm_message, host)) },
|
||||||
append(context.getString(R.string.donation_prompt_link))
|
confirmText = stringResource(R.string.okay),
|
||||||
}
|
)
|
||||||
append(" ")
|
|
||||||
append(context.getString(R.string.donation_prompt_suffix))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
if (uiState.shouldShowDonationSnackbar && !uiState.alreadyDonated) {
|
if (uiState.shouldShowDonationSnackbar && !uiState.alreadyDonated) {
|
||||||
viewModel.setShouldShowDonationSnackbar(false)
|
viewModel.setShouldShowDonationSnackbar(false)
|
||||||
snackbarState.showSnackbar(
|
toaster.show(
|
||||||
SnackbarInfo(
|
message =
|
||||||
message = annotatedMessage,
|
context.getString(R.string.donation_prompt_prefix) +
|
||||||
type = SnackbarType.THANK_YOU,
|
" " +
|
||||||
durationMs = 30_000L,
|
context.getString(R.string.donation_prompt_link) +
|
||||||
)
|
" " +
|
||||||
|
context.getString(R.string.donation_prompt_suffix),
|
||||||
|
type = ToastType.Normal,
|
||||||
|
duration = 30_000L.milliseconds,
|
||||||
|
action =
|
||||||
|
TextToastAction(
|
||||||
|
text = context.getString(R.string.donate_title),
|
||||||
|
onClick = { toastId ->
|
||||||
|
toaster.dismiss(toastId)
|
||||||
|
navController.push(Route.Donate)
|
||||||
|
},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
if (showLock) {
|
||||||
PinManager.initialize(context = this@MainActivity)
|
PinManager.initialize(context = this@MainActivity)
|
||||||
PinLockScreen()
|
PinLockScreen()
|
||||||
} else {
|
} else {
|
||||||
val currentRoute by remember {
|
|
||||||
derivedStateOf { backStack.lastOrNull() as? Route }
|
|
||||||
}
|
|
||||||
val currentTab by remember {
|
val currentTab by remember {
|
||||||
derivedStateOf { Tab.fromRoute(currentRoute ?: Route.Tunnels) }
|
derivedStateOf { Tab.fromRoute(currentRoute ?: Route.Tunnels) }
|
||||||
}
|
}
|
||||||
@@ -332,7 +442,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
if (uiState.appMode == AppMode.LOCK_DOWN) {
|
if (uiState.tunnelMode == TunnelMode.LOCK_DOWN) {
|
||||||
AppAlertBanner(
|
AppAlertBanner(
|
||||||
stringResource(R.string.locked_down)
|
stringResource(R.string.locked_down)
|
||||||
.uppercase(Locale.current.platformLocale),
|
.uppercase(Locale.current.platformLocale),
|
||||||
@@ -342,29 +452,6 @@ class MainActivity : AppCompatActivity() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
Scaffold(
|
Scaffold(
|
||||||
snackbarHost = {
|
|
||||||
snackbarState.SnackbarHost(
|
|
||||||
modifier =
|
|
||||||
Modifier.align(Alignment.BottomCenter)
|
|
||||||
.padding(
|
|
||||||
bottom =
|
|
||||||
if (LocalIsAndroidTV.current) 120.dp
|
|
||||||
else 80.dp
|
|
||||||
)
|
|
||||||
) { info ->
|
|
||||||
CustomSnackBar(
|
|
||||||
message = info.message,
|
|
||||||
type = info.type,
|
|
||||||
onDismiss = { snackbarState.dismissCurrent() },
|
|
||||||
containerColor =
|
|
||||||
MaterialTheme.colorScheme.surfaceColorAtElevation(
|
|
||||||
2.dp
|
|
||||||
),
|
|
||||||
modifier =
|
|
||||||
Modifier.wrapContentHeight(align = Alignment.Top),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
topBar = { DynamicTopAppBar(navState) },
|
topBar = { DynamicTopAppBar(navState) },
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
if (navState.showBottomItems) {
|
if (navState.showBottomItems) {
|
||||||
@@ -387,7 +474,6 @@ class MainActivity : AppCompatActivity() {
|
|||||||
bottom = padding.calculateBottomPadding(),
|
bottom = padding.calculateBottomPadding(),
|
||||||
)
|
)
|
||||||
.consumeWindowInsets(padding)
|
.consumeWindowInsets(padding)
|
||||||
.imePadding()
|
|
||||||
) {
|
) {
|
||||||
NavDisplay(
|
NavDisplay(
|
||||||
backStack = backStack,
|
backStack = backStack,
|
||||||
@@ -438,6 +524,13 @@ class MainActivity : AppCompatActivity() {
|
|||||||
)
|
)
|
||||||
TunnelSettingsScreen(viewModel)
|
TunnelSettingsScreen(viewModel)
|
||||||
}
|
}
|
||||||
|
entry<Route.Config> { key ->
|
||||||
|
val viewModel: TunnelViewModel =
|
||||||
|
koinViewModel(
|
||||||
|
parameters = { parametersOf(key.id) }
|
||||||
|
)
|
||||||
|
ConfigScreen(viewModel, key.live)
|
||||||
|
}
|
||||||
entry<Route.SplitTunnel> { key ->
|
entry<Route.SplitTunnel> { key ->
|
||||||
val viewModel: SplitTunnelViewModel =
|
val viewModel: SplitTunnelViewModel =
|
||||||
koinViewModel(
|
koinViewModel(
|
||||||
@@ -445,12 +538,12 @@ class MainActivity : AppCompatActivity() {
|
|||||||
)
|
)
|
||||||
SplitTunnelScreen(viewModel)
|
SplitTunnelScreen(viewModel)
|
||||||
}
|
}
|
||||||
entry<Route.Config> { key ->
|
entry<Route.ConfigEdit> { key ->
|
||||||
val viewModel: ConfigViewModel =
|
val viewModel: ConfigEditViewModel =
|
||||||
koinViewModel(
|
koinViewModel(
|
||||||
parameters = { parametersOf(key.id) }
|
parameters = { parametersOf(key.id) }
|
||||||
)
|
)
|
||||||
ConfigScreen(viewModel)
|
ConfigEditScreen(viewModel)
|
||||||
}
|
}
|
||||||
entry<Route.LocationDisclosure> {
|
entry<Route.LocationDisclosure> {
|
||||||
LocationDisclosureScreen()
|
LocationDisclosureScreen()
|
||||||
@@ -459,26 +552,20 @@ class MainActivity : AppCompatActivity() {
|
|||||||
entry<Route.WifiPreferences> {
|
entry<Route.WifiPreferences> {
|
||||||
WifiSettingsScreen()
|
WifiSettingsScreen()
|
||||||
}
|
}
|
||||||
entry<Route.AdvancedAutoTunnel> {
|
|
||||||
AutoTunnelAdvancedScreen()
|
|
||||||
}
|
|
||||||
entry<Route.WifiDetectionMethod> {
|
entry<Route.WifiDetectionMethod> {
|
||||||
WifiDetectionMethodScreen()
|
WifiDetectionMethodScreen()
|
||||||
}
|
}
|
||||||
entry<Route.Settings> { SettingsScreen() }
|
entry<Route.Settings> { SettingsScreen() }
|
||||||
entry<Route.TunnelMonitoring> {
|
|
||||||
TunnelMonitoringScreen()
|
|
||||||
}
|
|
||||||
entry<Route.AndroidIntegrations> {
|
entry<Route.AndroidIntegrations> {
|
||||||
AndroidIntegrationsScreen()
|
AndroidIntegrationsScreen()
|
||||||
}
|
}
|
||||||
entry<Route.Dns> { DnsSettingsScreen() }
|
entry<Route.Dns> { DnsSettingsScreen() }
|
||||||
entry<Route.ConfigGlobal> { key ->
|
entry<Route.ConfigGlobal> { key ->
|
||||||
val viewModel: ConfigViewModel =
|
val viewModel: ConfigEditViewModel =
|
||||||
koinViewModel(
|
koinViewModel(
|
||||||
parameters = { parametersOf(key.id) }
|
parameters = { parametersOf(key.id) }
|
||||||
)
|
)
|
||||||
ConfigScreen(viewModel)
|
ConfigEditScreen(viewModel)
|
||||||
}
|
}
|
||||||
entry<Route.SplitTunnelGlobal> { key ->
|
entry<Route.SplitTunnelGlobal> { key ->
|
||||||
val viewModel: SplitTunnelViewModel =
|
val viewModel: SplitTunnelViewModel =
|
||||||
@@ -487,6 +574,13 @@ class MainActivity : AppCompatActivity() {
|
|||||||
)
|
)
|
||||||
SplitTunnelScreen(viewModel)
|
SplitTunnelScreen(viewModel)
|
||||||
}
|
}
|
||||||
|
entry<Route.IPv6> { key ->
|
||||||
|
val viewModel: TunnelViewModel =
|
||||||
|
koinViewModel(
|
||||||
|
parameters = { parametersOf(key.id) }
|
||||||
|
)
|
||||||
|
IPv6Screen(viewModel)
|
||||||
|
}
|
||||||
entry<Route.LockdownSettings> {
|
entry<Route.LockdownSettings> {
|
||||||
LockdownSettingsScreen()
|
LockdownSettingsScreen()
|
||||||
}
|
}
|
||||||
@@ -502,11 +596,77 @@ class MainActivity : AppCompatActivity() {
|
|||||||
entry<Route.PreferredTunnel> { key ->
|
entry<Route.PreferredTunnel> { key ->
|
||||||
PreferredTunnelScreen(key.tunnelNetwork)
|
PreferredTunnelScreen(key.tunnelNetwork)
|
||||||
}
|
}
|
||||||
entry<Route.PingTarget> { PingTargetScreen() }
|
entry<Route.TunnelGlobals> { TunnelGlobalsScreen() }
|
||||||
|
entry<Route.Security> { SecurityScreen() }
|
||||||
|
entry<Route.Monitoring> { MonitoringScreen() }
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Toaster(
|
||||||
|
state = toaster,
|
||||||
|
alignment = Alignment.BottomCenter,
|
||||||
|
offset = IntOffset(0, -220),
|
||||||
|
richColors = true,
|
||||||
|
background = {
|
||||||
|
Brush.linearGradient(
|
||||||
|
listOf(
|
||||||
|
MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp),
|
||||||
|
MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
elevation = 1.dp,
|
||||||
|
shadowAmbientColor =
|
||||||
|
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.08f),
|
||||||
|
shadowSpotColor =
|
||||||
|
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
|
||||||
|
border = {
|
||||||
|
BorderStroke(
|
||||||
|
0.dp,
|
||||||
|
androidx.compose.ui.graphics.Color.Transparent,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
actionSlot = { toast ->
|
||||||
|
(toast.action as? TextToastAction)?.let { action ->
|
||||||
|
TextButton(
|
||||||
|
onClick = { action.onClick(toast) },
|
||||||
|
colors =
|
||||||
|
ButtonDefaults.textButtonColors(
|
||||||
|
contentColor = MaterialTheme.colorScheme.primary
|
||||||
|
),
|
||||||
|
contentPadding = PaddingValues(horizontal = 12.dp),
|
||||||
|
) {
|
||||||
|
Text(text = action.text, fontWeight = FontWeight.Medium)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
iconSlot = { toast ->
|
||||||
|
val (icon, color) =
|
||||||
|
when (toast.type) {
|
||||||
|
ToastType.Success ->
|
||||||
|
Icons.Outlined.CheckCircleOutline to SilverTree
|
||||||
|
ToastType.Error ->
|
||||||
|
Icons.Outlined.ErrorOutline to AlertRed
|
||||||
|
ToastType.Warning ->
|
||||||
|
Icons.Outlined.WarningAmber to Straw
|
||||||
|
ToastType.Info ->
|
||||||
|
Icons.Outlined.Info to
|
||||||
|
MaterialTheme.colorScheme.onSurface
|
||||||
|
ToastType.Normal ->
|
||||||
|
Icons.Outlined.FavoriteBorder to Heart
|
||||||
|
}
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = color,
|
||||||
|
modifier = Modifier.padding(end = 12.dp),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
contentColor = { MaterialTheme.colorScheme.onSurface },
|
||||||
|
shape = { RoundedCornerShape(16.dp) },
|
||||||
|
showCloseButton = true,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -514,70 +674,121 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleWgDeepLinkIntent(intent: Intent) {
|
||||||
|
if (intent.action == Intent.ACTION_VIEW) {
|
||||||
|
val uri = intent.data ?: return
|
||||||
|
if (uri.scheme == "wg") {
|
||||||
|
val httpsUrl = uri.toString().replaceFirst("wg://", "https://")
|
||||||
|
viewModel.promptWgImport(httpsUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
networkMonitor.checkPermissionsAndUpdateState()
|
networkMonitor.checkPermissionsAndUpdateState()
|
||||||
WireGuardAutoTunnel.setUiActive(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
fun performBackup(encrypt: Boolean = false, password: String? = null) {
|
||||||
super.onPause()
|
roomBackup
|
||||||
WireGuardAutoTunnel.setUiActive(false)
|
.backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG)
|
||||||
|
.apply {
|
||||||
|
if (encrypt && !password.isNullOrBlank()) {
|
||||||
|
backupIsEncrypted(true)
|
||||||
|
customEncryptPassword(password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onCompleteListener { success, _, _ ->
|
||||||
|
lifecycleScope.launch {
|
||||||
|
val sideEffect =
|
||||||
|
if (success) {
|
||||||
|
GlobalSideEffect.Snackbar(
|
||||||
|
StringValue.StringResource(R.string.backup_success),
|
||||||
|
ToastType.Success,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
GlobalSideEffect.Snackbar(
|
||||||
|
StringValue.StringResource(R.string.backup_failed),
|
||||||
|
ToastType.Error,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
viewModel.postSideEffect(sideEffect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.backup()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun performBackup() =
|
fun performRestore(encrypt: Boolean = false, password: String? = null) {
|
||||||
lifecycleScope.launch {
|
roomBackup
|
||||||
// reset active tuns before backup to prevent trying to start them without permission on
|
.backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG)
|
||||||
// restore
|
.apply {
|
||||||
tunnelRepository.resetActiveTunnels()
|
if (encrypt && !password.isNullOrBlank()) {
|
||||||
roomBackup
|
backupIsEncrypted(true)
|
||||||
.database(appDatabase)
|
customEncryptPassword(password)
|
||||||
.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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.backup()
|
}
|
||||||
}
|
.onCompleteListener { success, message, exitCode ->
|
||||||
|
lifecycleScope.launch {
|
||||||
|
if (success) {
|
||||||
|
viewModel.postSideEffect(
|
||||||
|
GlobalSideEffect.Snackbar(
|
||||||
|
StringValue.StringResource(R.string.restore_success),
|
||||||
|
ToastType.Success,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
roomBackup.restartApp(Intent(this@MainActivity, MainActivity::class.java))
|
||||||
|
} else {
|
||||||
|
Timber.w("Restore failed, exitCode=$exitCode, message=$message")
|
||||||
|
|
||||||
fun performRestore() =
|
val errorMessage =
|
||||||
lifecycleScope.launch {
|
when (exitCode) {
|
||||||
roomBackup
|
EXIT_CODE_ERROR_WRONG_DECRYPTION_PASSWORD ->
|
||||||
.database(appDatabase)
|
getString(R.string.restore_failed_wrong_password)
|
||||||
.enableLogDebug(true)
|
|
||||||
.backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG)
|
EXIT_CODE_ERROR,
|
||||||
.apply {
|
EXIT_CODE_ERROR_DECRYPTION_ERROR,
|
||||||
onCompleteListener { success, _, _ ->
|
EXIT_CODE_ERROR_RESTORE_BACKUP_IS_ENCRYPTED ->
|
||||||
lifecycleScope.launch {
|
getString(R.string.restore_failed_invalid_file)
|
||||||
if (success) {
|
|
||||||
showToast(
|
else -> getString(R.string.restore_failed)
|
||||||
getString(
|
|
||||||
R.string.restore_success,
|
|
||||||
getString(R.string.restarting_app),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
restartApp()
|
|
||||||
} else {
|
|
||||||
showToast(R.string.restore_failed)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
viewModel.postSideEffect(
|
||||||
|
GlobalSideEffect.Snackbar(
|
||||||
|
StringValue.DynamicString(errorMessage),
|
||||||
|
ToastType.Error,
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.restore()
|
}
|
||||||
|
.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNewIntent(intent: Intent) {
|
||||||
|
super.onNewIntent(intent)
|
||||||
|
setIntent(intent)
|
||||||
|
handleConfigFileIntent(intent)
|
||||||
|
handleWgDeepLinkIntent(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleConfigFileIntent(intent: Intent?) {
|
||||||
|
intent ?: return
|
||||||
|
when (intent.action) {
|
||||||
|
Intent.ACTION_VIEW,
|
||||||
|
Intent.ACTION_EDIT,
|
||||||
|
Intent.ACTION_SEND -> {
|
||||||
|
val uri: Uri? = intent.data ?: return
|
||||||
|
val name = uri?.lastPathSegment?.lowercase() ?: return
|
||||||
|
if (
|
||||||
|
!name.endsWith(FileUtils.CONF_FILE_EXTENSION) &&
|
||||||
|
!name.endsWith(FileUtils.ZIP_FILE_EXTENSION)
|
||||||
|
) {
|
||||||
|
Timber.d("Ignoring non-config URI in handleIncomingIntent: $uri")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
viewModel.importFromUri(uri)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,29 +2,35 @@ package com.zaneschepke.wireguardautotunnel
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.os.StrictMode
|
import android.os.StrictMode
|
||||||
import com.zaneschepke.logcatter.LogReader
|
import com.zaneschepke.tunnel.backend.Backend
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationMonitor
|
import com.zaneschepke.tunnel.di.tunnelModule
|
||||||
|
import com.zaneschepke.tunnel.service.VpnService
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.event.TunnelEventDispatcher
|
||||||
|
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.Dispatcher
|
||||||
import com.zaneschepke.wireguardautotunnel.di.Scope
|
import com.zaneschepke.wireguardautotunnel.di.Scope
|
||||||
import com.zaneschepke.wireguardautotunnel.di.appModule
|
import com.zaneschepke.wireguardautotunnel.di.appModule
|
||||||
|
import com.zaneschepke.wireguardautotunnel.di.coordinatorModule
|
||||||
import com.zaneschepke.wireguardautotunnel.di.databaseModule
|
import com.zaneschepke.wireguardautotunnel.di.databaseModule
|
||||||
import com.zaneschepke.wireguardautotunnel.di.dispatchersModule
|
import com.zaneschepke.wireguardautotunnel.di.dispatchersModule
|
||||||
import com.zaneschepke.wireguardautotunnel.di.networkModule
|
import com.zaneschepke.wireguardautotunnel.di.networkModule
|
||||||
import com.zaneschepke.wireguardautotunnel.di.tunnelModule
|
import com.zaneschepke.wireguardautotunnel.di.tunnelBackendProviderModule
|
||||||
import com.zaneschepke.wireguardautotunnel.di.workerModule
|
import com.zaneschepke.wireguardautotunnel.di.workerModule
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository
|
import com.zaneschepke.wireguardautotunnel.notification.NotificationService
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelTileRefresher
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelTileRefresher
|
||||||
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
|
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.koin.android.ext.android.get
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import org.koin.android.ext.koin.androidContext
|
import org.koin.android.ext.koin.androidContext
|
||||||
import org.koin.android.ext.koin.androidLogger
|
import org.koin.android.ext.koin.androidLogger
|
||||||
import org.koin.androidx.workmanager.koin.workManagerFactory
|
import org.koin.androidx.workmanager.koin.workManagerFactory
|
||||||
|
import org.koin.core.annotation.KoinViewModelScopeApi
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.context.GlobalContext.startKoin
|
import org.koin.core.context.GlobalContext.startKoin
|
||||||
import org.koin.core.lazyModules
|
import org.koin.core.lazyModules
|
||||||
@@ -36,74 +42,81 @@ class WireGuardAutoTunnel : Application(), KoinComponent {
|
|||||||
|
|
||||||
private val applicationScope: CoroutineScope by inject(named(Scope.APPLICATION))
|
private val applicationScope: CoroutineScope by inject(named(Scope.APPLICATION))
|
||||||
private val ioDispatcher: CoroutineDispatcher by inject(named(Dispatcher.IO))
|
private val ioDispatcher: CoroutineDispatcher by inject(named(Dispatcher.IO))
|
||||||
private val logReader: LogReader by inject()
|
|
||||||
|
|
||||||
private val monitoringRepository: MonitoringSettingsRepository by inject()
|
private val boostrapCoordinator: AppBoostrapCoordinator by inject()
|
||||||
private val notificationMonitor: NotificationMonitor by inject()
|
|
||||||
|
|
||||||
|
private val notificationService: NotificationService by inject()
|
||||||
|
|
||||||
|
private val tunnelCoordinator: TunnelCoordinator by inject()
|
||||||
|
|
||||||
|
private val backend: Backend by inject()
|
||||||
|
|
||||||
|
private val alwaysOnCallback =
|
||||||
|
object : VpnService.AlwaysOnCallback {
|
||||||
|
override fun alwaysOnTriggered() {
|
||||||
|
applicationScope.launch { tunnelCoordinator.startDefault() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(KoinViewModelScopeApi::class)
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
startKoin {
|
startKoin {
|
||||||
androidContext(this@WireGuardAutoTunnel)
|
androidContext(this@WireGuardAutoTunnel)
|
||||||
if (BuildConfig.DEBUG) androidLogger()
|
if (BuildConfig.DEBUG) androidLogger()
|
||||||
workManagerFactory()
|
workManagerFactory()
|
||||||
modules(dispatchersModule, appModule, databaseModule, tunnelModule, workerModule)
|
modules(
|
||||||
|
dispatchersModule,
|
||||||
|
appModule,
|
||||||
|
databaseModule,
|
||||||
|
tunnelBackendProviderModule,
|
||||||
|
tunnelModule,
|
||||||
|
workerModule,
|
||||||
|
coordinatorModule,
|
||||||
|
)
|
||||||
options(viewModelScopeFactory())
|
options(viewModelScopeFactory())
|
||||||
lazyModules(networkModule)
|
lazyModules(networkModule)
|
||||||
}
|
}
|
||||||
instance = this
|
instance = this
|
||||||
|
|
||||||
|
notificationService.createAllChannels()
|
||||||
|
|
||||||
|
syncTiles()
|
||||||
|
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
Timber.plant(Timber.DebugTree())
|
Timber.plant(Timber.DebugTree())
|
||||||
StrictMode.setThreadPolicy(
|
StrictMode.setThreadPolicy(
|
||||||
StrictMode.ThreadPolicy.Builder()
|
StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog().build()
|
||||||
.detectAll()
|
|
||||||
.penaltyLog()
|
|
||||||
.penaltyFlashScreen()
|
|
||||||
.build()
|
|
||||||
)
|
)
|
||||||
StrictMode.setVmPolicy(StrictMode.VmPolicy.Builder().detectAll().penaltyLog().build())
|
StrictMode.setVmPolicy(StrictMode.VmPolicy.Builder().detectAll().penaltyLog().build())
|
||||||
} else {
|
} else {
|
||||||
Timber.plant(ReleaseTree())
|
Timber.plant(ReleaseTree())
|
||||||
}
|
}
|
||||||
|
|
||||||
applicationScope.launch(ioDispatcher) {
|
backend.setAlwaysOnCallback(alwaysOnCallback)
|
||||||
launch {
|
|
||||||
monitoringRepository.flow
|
val dispatcher = get<TunnelEventDispatcher>()
|
||||||
.distinctUntilChangedBy { it.isLocalLogsEnabled }
|
val coordinator = get<TunnelCoordinator>()
|
||||||
.collect { settings ->
|
val provider = get<TunnelProvider>()
|
||||||
if (settings.isLocalLogsEnabled) {
|
|
||||||
logReader.start()
|
// for notifications
|
||||||
} else {
|
dispatcher.bind(
|
||||||
logReader.stop()
|
applicationScope,
|
||||||
}
|
provider.events,
|
||||||
}
|
provider.backendStatus,
|
||||||
}
|
coordinator.errors,
|
||||||
launch { notificationMonitor.handleApplicationNotifications() }
|
tunnelCoordinator.tunnelDisplayStates,
|
||||||
}
|
)
|
||||||
|
|
||||||
|
applicationScope.launch(ioDispatcher) { boostrapCoordinator.bootstrap() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun syncTiles() {
|
||||||
|
AutoTunnelTileRefresher.refresh(this)
|
||||||
|
TunnelTileRefresher.refresh(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
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
|
lateinit var instance: WireGuardAutoTunnel
|
||||||
private set
|
private set
|
||||||
}
|
}
|
||||||
|
|||||||
-36
@@ -1,36 +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.Scope
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.koin.core.component.KoinComponent
|
|
||||||
import org.koin.core.component.inject
|
|
||||||
import org.koin.core.qualifier.named
|
|
||||||
|
|
||||||
class KernelReceiver : BroadcastReceiver(), KoinComponent {
|
|
||||||
|
|
||||||
private val applicationScope: CoroutineScope by inject(named(Scope.APPLICATION))
|
|
||||||
private val tunnelRepository: TunnelRepository by inject()
|
|
||||||
private val tunnelManager: TunnelManager by inject()
|
|
||||||
|
|
||||||
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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+25
-11
@@ -3,11 +3,11 @@ package com.zaneschepke.wireguardautotunnel.core.broadcast
|
|||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
import com.zaneschepke.wireguardautotunnel.core.orchestration.AutoTunnelCoordinator
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||||
import com.zaneschepke.wireguardautotunnel.di.Scope
|
import com.zaneschepke.wireguardautotunnel.di.Scope
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
import com.zaneschepke.wireguardautotunnel.notification.NotificationService
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
@@ -17,20 +17,34 @@ import org.koin.core.qualifier.named
|
|||||||
|
|
||||||
class NotificationActionReceiver : BroadcastReceiver(), KoinComponent {
|
class NotificationActionReceiver : BroadcastReceiver(), KoinComponent {
|
||||||
|
|
||||||
private val tunnelManager: TunnelManager by inject()
|
private val tunnelCoordinator: TunnelCoordinator by inject()
|
||||||
private val autoTunnelRepository: AutoTunnelSettingsRepository by inject()
|
|
||||||
|
private val autoTunnelCoordinator: AutoTunnelCoordinator by inject()
|
||||||
|
|
||||||
private val applicationScope: CoroutineScope = get(named(Scope.APPLICATION))
|
private val applicationScope: CoroutineScope = get(named(Scope.APPLICATION))
|
||||||
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
|
||||||
applicationScope.launch {
|
applicationScope.launch {
|
||||||
when (intent.action) {
|
when (intent.action) {
|
||||||
NotificationAction.AUTO_TUNNEL_OFF.name ->
|
NotificationAction.AUTO_TUNNEL_OFF.name -> {
|
||||||
autoTunnelRepository.updateAutoTunnelEnabled(false)
|
autoTunnelCoordinator.disable()
|
||||||
|
}
|
||||||
|
|
||||||
NotificationAction.TUNNEL_OFF.name -> {
|
NotificationAction.TUNNEL_OFF.name -> {
|
||||||
val tunnelId = intent.getIntExtra(NotificationManager.EXTRA_ID, 0)
|
|
||||||
if (tunnelId == STOP_ALL_TUNNELS_ID)
|
val tunnelId =
|
||||||
return@launch tunnelManager.stopActiveTunnels()
|
intent.getIntExtra(NotificationService.EXTRA_ID, STOP_ALL_TUNNELS_ID)
|
||||||
tunnelManager.stopTunnel(tunnelId)
|
|
||||||
|
if (tunnelId == STOP_ALL_TUNNELS_ID) {
|
||||||
|
tunnelCoordinator.stopActiveTunnels()
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
tunnelCoordinator.stopTunnel(tunnelId)
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationAction.STOP_ALL.name -> {
|
||||||
|
tunnelCoordinator.stopActiveTunnels()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+48
-29
@@ -3,9 +3,10 @@ package com.zaneschepke.wireguardautotunnel.core.broadcast
|
|||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
import com.zaneschepke.wireguardautotunnel.core.orchestration.AutoTunnelCoordinator
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||||
import com.zaneschepke.wireguardautotunnel.di.Scope
|
import com.zaneschepke.wireguardautotunnel.di.Scope
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
@@ -14,15 +15,15 @@ import kotlinx.coroutines.launch
|
|||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
import org.koin.core.qualifier.named
|
import org.koin.core.qualifier.named
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
class RemoteControlReceiver : BroadcastReceiver(), KoinComponent {
|
class RemoteControlReceiver : BroadcastReceiver(), KoinComponent {
|
||||||
|
|
||||||
private val applicationScope: CoroutineScope by inject(named(Scope.APPLICATION))
|
private val applicationScope: CoroutineScope by inject(named(Scope.APPLICATION))
|
||||||
|
|
||||||
private val settingsRepository: GeneralSettingRepository by inject()
|
private val settingsRepository: GeneralSettingRepository by inject()
|
||||||
private val tunnelsRepository: TunnelRepository by inject()
|
private val tunnelsRepository: TunnelRepository by inject()
|
||||||
private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository by inject()
|
private val tunnelCoordinator: TunnelCoordinator by inject()
|
||||||
private val tunnelManager: TunnelManager by inject()
|
private val autoTunnelCoordinator: AutoTunnelCoordinator by inject()
|
||||||
|
|
||||||
enum class Action(private val suffix: String) {
|
enum class Action(private val suffix: String) {
|
||||||
START_TUNNEL("START_TUNNEL"),
|
START_TUNNEL("START_TUNNEL"),
|
||||||
@@ -47,45 +48,63 @@ class RemoteControlReceiver : BroadcastReceiver(), KoinComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
Timber.i("onReceive")
|
|
||||||
val action = intent.action ?: return
|
val action = intent.action ?: return
|
||||||
val appAction = Action.fromAction(action) ?: return Timber.w("Unknown action $action")
|
val appAction = Action.fromAction(action) ?: return
|
||||||
|
|
||||||
applicationScope.launch {
|
applicationScope.launch {
|
||||||
val settings = settingsRepository.getGeneralSettings()
|
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 (!settings.isRemoteControlEnabled) return@launch
|
||||||
if (key != intent.getStringExtra(EXTRA_KEY)?.trim())
|
|
||||||
return@launch Timber.w("Invalid remote control key")
|
if (!validateKey(settings, intent)) return@launch
|
||||||
|
|
||||||
when (appAction) {
|
when (appAction) {
|
||||||
Action.START_TUNNEL -> {
|
Action.START_TUNNEL -> {
|
||||||
val tunnelName =
|
|
||||||
intent.getStringExtra(EXTRA_TUN_NAME) ?: return@launch startDefaultTunnel()
|
|
||||||
val tunnel =
|
val tunnel =
|
||||||
tunnelsRepository.findByTunnelName(tunnelName)
|
resolveTunnel(intent)
|
||||||
?: return@launch startDefaultTunnel()
|
?: tunnelsRepository.getDefaultTunnel()
|
||||||
tunnelManager.startTunnel(tunnel)
|
?: return@launch
|
||||||
|
|
||||||
|
tunnelCoordinator.startTunnel(tunnel)
|
||||||
}
|
}
|
||||||
|
|
||||||
Action.STOP_TUNNEL -> {
|
Action.STOP_TUNNEL -> {
|
||||||
val tunnelName =
|
val tunnelName = intent.getStringExtra(EXTRA_TUN_NAME)
|
||||||
intent.getStringExtra(EXTRA_TUN_NAME)
|
|
||||||
?: return@launch tunnelManager.stopActiveTunnels()
|
if (tunnelName == null) {
|
||||||
val tunnel =
|
tunnelCoordinator.stopActiveTunnels()
|
||||||
tunnelsRepository.findByTunnelName(tunnelName)
|
return@launch
|
||||||
?: return@launch tunnelManager.stopActiveTunnels()
|
}
|
||||||
tunnelManager.stopTunnel(tunnel.id)
|
|
||||||
|
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() {
|
private fun validateKey(settings: GeneralSettings, intent: Intent): Boolean {
|
||||||
tunnelsRepository.getDefaultTunnel()?.let { tunnel -> tunnelManager.startTunnel(tunnel) }
|
|
||||||
|
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 {
|
companion object {
|
||||||
const val EXTRA_TUN_NAME = "tunnelName"
|
const val EXTRA_TUN_NAME = "tunnelName"
|
||||||
const val EXTRA_KEY = "key"
|
const val EXTRA_KEY = "key"
|
||||||
|
|||||||
+4
-4
@@ -4,7 +4,7 @@ import android.content.BroadcastReceiver
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.zaneschepke.logcatter.LogReader
|
import com.zaneschepke.logcatter.LogReader
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
import com.zaneschepke.wireguardautotunnel.core.orchestration.StartupCoordinator
|
||||||
import com.zaneschepke.wireguardautotunnel.di.Scope
|
import com.zaneschepke.wireguardautotunnel.di.Scope
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -19,7 +19,7 @@ class RestartReceiver : BroadcastReceiver(), KoinComponent {
|
|||||||
|
|
||||||
private val applicationScope: CoroutineScope = get(named(Scope.APPLICATION))
|
private val applicationScope: CoroutineScope = get(named(Scope.APPLICATION))
|
||||||
|
|
||||||
private val tunnelManager: TunnelManager by inject()
|
private val startupCoordinator: StartupCoordinator by inject()
|
||||||
|
|
||||||
private val appStateRepository: AppStateRepository by inject()
|
private val appStateRepository: AppStateRepository by inject()
|
||||||
|
|
||||||
@@ -32,11 +32,11 @@ class RestartReceiver : BroadcastReceiver(), KoinComponent {
|
|||||||
Intent.ACTION_BOOT_COMPLETED,
|
Intent.ACTION_BOOT_COMPLETED,
|
||||||
"android.intent.action.QUICKBOOT_POWERON",
|
"android.intent.action.QUICKBOOT_POWERON",
|
||||||
"com.htc.intent.action.QUICKBOOT_POWERON" -> {
|
"com.htc.intent.action.QUICKBOOT_POWERON" -> {
|
||||||
tunnelManager.handleReboot()
|
startupCoordinator.applyStartupPolicy()
|
||||||
}
|
}
|
||||||
Intent.ACTION_MY_PACKAGE_REPLACED -> {
|
Intent.ACTION_MY_PACKAGE_REPLACED -> {
|
||||||
Timber.i("Restoring state on package upgrade")
|
Timber.i("Restoring state on package upgrade")
|
||||||
tunnelManager.handleRestore()
|
startupCoordinator.applyStartupPolicy()
|
||||||
logReader.deleteAndClearLogs()
|
logReader.deleteAndClearLogs()
|
||||||
appStateRepository.setShouldShowDonationSnackbar(true)
|
appStateRepository.setShouldShowDonationSnackbar(true)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.core.event
|
||||||
|
|
||||||
|
import com.zaneschepke.tunnel.util.BackendException
|
||||||
|
|
||||||
|
sealed interface TunnelErrorEvent {
|
||||||
|
data class VpnPermissionDenied(val tunnelId: Int) : 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.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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+298
@@ -0,0 +1,298 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.core.event
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.dokar.sonner.ToastType
|
||||||
|
import com.zaneschepke.tunnel.event.TunnelEvent
|
||||||
|
import com.zaneschepke.tunnel.model.BackendMode
|
||||||
|
import com.zaneschepke.tunnel.state.BackendStatus
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
||||||
|
import com.zaneschepke.wireguardautotunnel.lifecyle.AppVisibilityObserver
|
||||||
|
import com.zaneschepke.wireguardautotunnel.notification.TunnelNotificationLine
|
||||||
|
import com.zaneschepke.wireguardautotunnel.notification.TunnelNotificationService
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.FlowPreview
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.debounce
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class TunnelEventDispatcher(
|
||||||
|
private val notificationManager: TunnelNotificationService,
|
||||||
|
private val tunnelRepository: TunnelRepository,
|
||||||
|
private val context: Context,
|
||||||
|
private val appVisibilityObserver: AppVisibilityObserver,
|
||||||
|
private val globalEffectRepository: GlobalEffectRepository,
|
||||||
|
) {
|
||||||
|
|
||||||
|
@OptIn(FlowPreview::class)
|
||||||
|
fun bind(
|
||||||
|
scope: CoroutineScope,
|
||||||
|
providerEvents: Flow<TunnelEvent>,
|
||||||
|
providerStatus: StateFlow<BackendStatus>,
|
||||||
|
coordinatorErrors: Flow<TunnelErrorEvent>,
|
||||||
|
tunnelDisplayStates: StateFlow<Map<Int, DisplayTunnelState>>,
|
||||||
|
) {
|
||||||
|
|
||||||
|
// Informational events from tunnel backend
|
||||||
|
providerEvents
|
||||||
|
.onEach { event ->
|
||||||
|
when (event) {
|
||||||
|
is TunnelEvent.FallbackToIpv4 -> {
|
||||||
|
val name = getTunnelName(event.tunnelId)
|
||||||
|
showOrNotify(
|
||||||
|
scope = scope,
|
||||||
|
foregroundAction = {
|
||||||
|
globalEffectRepository.post(
|
||||||
|
GlobalSideEffect.Snackbar(
|
||||||
|
message =
|
||||||
|
StringValue.DynamicString(
|
||||||
|
context.getString(
|
||||||
|
R.string.notification_ipv4_fallback_message,
|
||||||
|
name,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
type = ToastType.Info,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
backgroundAction = { notificationManager.showIpv4Fallback(name) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is TunnelEvent.RecoveredToIpv6 -> {
|
||||||
|
val name = getTunnelName(event.tunnelId)
|
||||||
|
showOrNotify(
|
||||||
|
scope = scope,
|
||||||
|
foregroundAction = {
|
||||||
|
globalEffectRepository.post(
|
||||||
|
GlobalSideEffect.Snackbar(
|
||||||
|
message =
|
||||||
|
StringValue.DynamicString(
|
||||||
|
context.getString(
|
||||||
|
R.string.notification_ipv6_recovery_message,
|
||||||
|
name,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
type = ToastType.Success,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
backgroundAction = { notificationManager.showIpv6Recovery(name) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is TunnelEvent.DynamicDnsUpdate -> {
|
||||||
|
val name = getTunnelName(event.tunnelId)
|
||||||
|
showOrNotify(
|
||||||
|
scope = scope,
|
||||||
|
foregroundAction = {
|
||||||
|
globalEffectRepository.post(
|
||||||
|
GlobalSideEffect.Snackbar(
|
||||||
|
message =
|
||||||
|
StringValue.DynamicString(
|
||||||
|
context.getString(
|
||||||
|
R.string.notification_dynamic_dns_message,
|
||||||
|
name,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
type = ToastType.Info,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
backgroundAction = { notificationManager.showDynamicDnsUpdate(name) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is TunnelEvent.NoRootShellAccess -> {
|
||||||
|
showOrNotify(
|
||||||
|
scope = scope,
|
||||||
|
foregroundAction = {
|
||||||
|
globalEffectRepository.post(
|
||||||
|
GlobalSideEffect.Snackbar(
|
||||||
|
message =
|
||||||
|
StringValue.DynamicString(
|
||||||
|
context.getString(R.string.error_root_denied)
|
||||||
|
),
|
||||||
|
type = ToastType.Error,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
backgroundAction = { notificationManager.showRootShellAccess() },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.launchIn(scope)
|
||||||
|
|
||||||
|
// Errors from our tunnel coordinator
|
||||||
|
coordinatorErrors
|
||||||
|
.onEach { error ->
|
||||||
|
when (error) {
|
||||||
|
is TunnelErrorEvent.VpnPermissionDenied -> {
|
||||||
|
showOrNotify(
|
||||||
|
scope = scope,
|
||||||
|
foregroundAction = {
|
||||||
|
globalEffectRepository.post(
|
||||||
|
GlobalSideEffect.Snackbar(
|
||||||
|
message =
|
||||||
|
StringValue.DynamicString(
|
||||||
|
context.getString(R.string.vpn_permission_required)
|
||||||
|
),
|
||||||
|
type = ToastType.Error,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
backgroundAction = { notificationManager.showVpnRequired() },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is TunnelErrorEvent.InternalFailure -> {
|
||||||
|
showOrNotify(
|
||||||
|
scope = scope,
|
||||||
|
foregroundAction = {
|
||||||
|
globalEffectRepository.post(
|
||||||
|
GlobalSideEffect.Snackbar(
|
||||||
|
message = StringValue.DynamicString(error.message),
|
||||||
|
type = ToastType.Error,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
backgroundAction = { notificationManager.showError(error.message) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is TunnelErrorEvent.Socks5PortUnavailable -> {
|
||||||
|
val name = getTunnelName(error.tunnelId)
|
||||||
|
val message =
|
||||||
|
context.getString(R.string.error_socks5_port_unavailable, error.port)
|
||||||
|
|
||||||
|
showOrNotify(
|
||||||
|
scope = scope,
|
||||||
|
foregroundAction = {
|
||||||
|
globalEffectRepository.post(
|
||||||
|
GlobalSideEffect.Snackbar(
|
||||||
|
message = StringValue.DynamicString(message),
|
||||||
|
type = ToastType.Error,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
backgroundAction = {
|
||||||
|
notificationManager.showSocks5PortUnavailable(error.port, name)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is TunnelErrorEvent.HttpPortUnavailable -> {
|
||||||
|
val name = getTunnelName(error.tunnelId)
|
||||||
|
val message =
|
||||||
|
context.getString(R.string.error_http_port_unavailable, error.port)
|
||||||
|
|
||||||
|
showOrNotify(
|
||||||
|
scope = scope,
|
||||||
|
foregroundAction = {
|
||||||
|
globalEffectRepository.post(
|
||||||
|
GlobalSideEffect.Snackbar(
|
||||||
|
message = StringValue.DynamicString(message),
|
||||||
|
type = ToastType.Error,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
backgroundAction = {
|
||||||
|
notificationManager.showHttpPortUnavailable(error.port, name)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.launchIn(scope)
|
||||||
|
|
||||||
|
// vpn
|
||||||
|
combine(
|
||||||
|
providerStatus.map { it.activeTunnels },
|
||||||
|
tunnelRepository.userTunnelsFlow,
|
||||||
|
tunnelDisplayStates,
|
||||||
|
) { activeTunnels, allTunnels, displayStates ->
|
||||||
|
activeTunnels
|
||||||
|
.mapNotNull { (id, activeTunnel) ->
|
||||||
|
val mode = activeTunnel.mode ?: return@mapNotNull null
|
||||||
|
if (
|
||||||
|
mode !is BackendMode.Vpn && mode !is BackendMode.Proxy.KillSwitchPrimary
|
||||||
|
) {
|
||||||
|
return@mapNotNull null
|
||||||
|
}
|
||||||
|
val tunnel = allTunnels.find { it.id == id } ?: return@mapNotNull null
|
||||||
|
|
||||||
|
val displayState =
|
||||||
|
displayStates[id] ?: DisplayTunnelState.from(activeTunnel)
|
||||||
|
|
||||||
|
TunnelNotificationLine(
|
||||||
|
id = id,
|
||||||
|
name = tunnel.name,
|
||||||
|
displayState = displayState,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.associateBy { it.id }
|
||||||
|
}
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.debounce(500.milliseconds) // give the service notification time to display
|
||||||
|
.onEach { vpnLines -> notificationManager.updateVpnPersistentNotification(vpnLines) }
|
||||||
|
.launchIn(scope)
|
||||||
|
|
||||||
|
// proxy
|
||||||
|
combine(
|
||||||
|
providerStatus.map { it.activeTunnels },
|
||||||
|
tunnelRepository.userTunnelsFlow,
|
||||||
|
tunnelDisplayStates,
|
||||||
|
) { activeTunnels, allTunnels, displayStates ->
|
||||||
|
activeTunnels
|
||||||
|
.mapNotNull { (id, activeTunnel) ->
|
||||||
|
val mode = activeTunnel.mode ?: return@mapNotNull null
|
||||||
|
if (mode !is BackendMode.Proxy.Standard) return@mapNotNull null
|
||||||
|
|
||||||
|
val tunnel = allTunnels.find { it.id == id } ?: return@mapNotNull null
|
||||||
|
val displayState =
|
||||||
|
displayStates[id] ?: DisplayTunnelState.from(activeTunnel)
|
||||||
|
|
||||||
|
TunnelNotificationLine(
|
||||||
|
id = id,
|
||||||
|
name = tunnel.name,
|
||||||
|
displayState = displayState,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.associateBy { it.id }
|
||||||
|
}
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.debounce(500.milliseconds) // give the service notification time to display
|
||||||
|
.onEach { proxyLines ->
|
||||||
|
notificationManager.updateProxyPersistentNotification(proxyLines)
|
||||||
|
}
|
||||||
|
.launchIn(scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showOrNotify(
|
||||||
|
scope: CoroutineScope,
|
||||||
|
foregroundAction: suspend () -> Unit,
|
||||||
|
backgroundAction: () -> Unit,
|
||||||
|
) {
|
||||||
|
if (appVisibilityObserver.isForeground.value) {
|
||||||
|
scope.launch { foregroundAction() }
|
||||||
|
} else {
|
||||||
|
backgroundAction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getTunnelName(tunnelId: Int): String {
|
||||||
|
return tunnelRepository.getById(tunnelId)?.name ?: context.getString(R.string.unknown)
|
||||||
|
}
|
||||||
|
}
|
||||||
-61
@@ -1,61 +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 kotlinx.coroutines.coroutineScope
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class NotificationMonitor(
|
|
||||||
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.stringRes,
|
|
||||||
),
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-172
@@ -1,172 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.notification
|
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.app.Notification
|
|
||||||
import android.app.NotificationChannel
|
|
||||||
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.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.domain.enums.NotificationAction
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
|
||||||
|
|
||||||
class WireGuardNotification(override val context: Context) : NotificationManager {
|
|
||||||
|
|
||||||
enum class NotificationChannels {
|
|
||||||
VPN,
|
|
||||||
AUTO_TUNNEL,
|
|
||||||
}
|
|
||||||
|
|
||||||
private val notificationManager = NotificationManagerCompat.from(context)
|
|
||||||
|
|
||||||
override fun createNotification(
|
|
||||||
channel: NotificationChannels,
|
|
||||||
title: String,
|
|
||||||
actions: Collection<NotificationCompat.Action>,
|
|
||||||
description: String,
|
|
||||||
showTimestamp: Boolean,
|
|
||||||
importance: Int,
|
|
||||||
onGoing: Boolean,
|
|
||||||
onlyAlertOnce: Boolean,
|
|
||||||
groupKey: String?,
|
|
||||||
isGroupSummary: Boolean,
|
|
||||||
): Notification {
|
|
||||||
notificationManager.createNotificationChannel(channel.asChannel(importance))
|
|
||||||
return channel
|
|
||||||
.asBuilder()
|
|
||||||
.apply {
|
|
||||||
actions.forEach { addAction(it) }
|
|
||||||
setContentTitle(title)
|
|
||||||
setContentIntent(
|
|
||||||
PendingIntent.getActivity(
|
|
||||||
context,
|
|
||||||
0,
|
|
||||||
Intent(context, MainActivity::class.java)
|
|
||||||
.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP),
|
|
||||||
PendingIntent.FLAG_IMMUTABLE,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
setContentText(description)
|
|
||||||
setOnlyAlertOnce(onlyAlertOnce)
|
|
||||||
setOngoing(onGoing)
|
|
||||||
setPriority(NotificationCompat.PRIORITY_LOW)
|
|
||||||
setShowWhen(showTimestamp)
|
|
||||||
setSmallIcon(R.drawable.ic_notification)
|
|
||||||
if (groupKey != null) {
|
|
||||||
setGroup(groupKey)
|
|
||||||
if (isGroupSummary) {
|
|
||||||
setGroupSummary(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createNotification(
|
|
||||||
channel: NotificationChannels,
|
|
||||||
title: StringValue,
|
|
||||||
actions: Collection<NotificationCompat.Action>,
|
|
||||||
description: StringValue,
|
|
||||||
showTimestamp: Boolean,
|
|
||||||
importance: Int,
|
|
||||||
onGoing: Boolean,
|
|
||||||
onlyAlertOnce: Boolean,
|
|
||||||
groupKey: String?,
|
|
||||||
isGroupSummary: Boolean,
|
|
||||||
): Notification {
|
|
||||||
return createNotification(
|
|
||||||
channel,
|
|
||||||
title.asString(context),
|
|
||||||
actions,
|
|
||||||
description.asString(context),
|
|
||||||
showTimestamp,
|
|
||||||
importance,
|
|
||||||
onGoing,
|
|
||||||
onlyAlertOnce,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createNotificationAction(
|
|
||||||
notificationAction: NotificationAction,
|
|
||||||
extraId: Int?,
|
|
||||||
): NotificationCompat.Action {
|
|
||||||
val pendingIntent =
|
|
||||||
PendingIntent.getBroadcast(
|
|
||||||
context,
|
|
||||||
extraId ?: 0,
|
|
||||||
Intent(context, NotificationActionReceiver::class.java).apply {
|
|
||||||
action = notificationAction.name
|
|
||||||
if (extraId != null) putExtra(EXTRA_ID, extraId)
|
|
||||||
},
|
|
||||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
|
|
||||||
)
|
|
||||||
return NotificationCompat.Action.Builder(
|
|
||||||
R.drawable.ic_notification,
|
|
||||||
notificationAction.title(context).uppercase(),
|
|
||||||
pendingIntent,
|
|
||||||
)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun remove(notificationId: Int) {
|
|
||||||
notificationManager.cancel(notificationId)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun show(notificationId: Int, notification: Notification) {
|
|
||||||
with(notificationManager) {
|
|
||||||
if (
|
|
||||||
ActivityCompat.checkSelfPermission(
|
|
||||||
context,
|
|
||||||
Manifest.permission.POST_NOTIFICATIONS,
|
|
||||||
) != PackageManager.PERMISSION_GRANTED
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
notify(notificationId, notification)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+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.domain.repository.AutoTunnelSettingsRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.ServiceManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.autotunnel.AutoTunnelStateHolder
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
+272
@@ -0,0 +1,272 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.core.orchestration
|
||||||
|
|
||||||
|
import com.zaneschepke.tunnel.model.BackendMode
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.event.TunnelErrorEvent
|
||||||
|
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.LockdownSettings
|
||||||
|
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.LockdownSettingsRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.repository.ProxySettingsRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.ServiceManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.FlowPreview
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.debounce
|
||||||
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
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,
|
||||||
|
lockdownModeRepository: LockdownSettingsRepository,
|
||||||
|
scope: CoroutineScope,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val _userOverrideFlow = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
|
||||||
|
val userOverrideFlow = _userOverrideFlow.asSharedFlow()
|
||||||
|
|
||||||
|
@OptIn(FlowPreview::class)
|
||||||
|
val tunnelDisplayStates: StateFlow<Map<Int, DisplayTunnelState>> =
|
||||||
|
tunnelProvider.backendStatus
|
||||||
|
.map { status ->
|
||||||
|
status.activeTunnels.mapValues { (_, activeTunnel) ->
|
||||||
|
DisplayTunnelState.from(activeTunnel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.debounce(400L.milliseconds)
|
||||||
|
.stateIn(scope = scope, started = SharingStarted.Eagerly, initialValue = emptyMap())
|
||||||
|
|
||||||
|
data class RuntimeSettingsSnapshot(
|
||||||
|
val general: GeneralSettings,
|
||||||
|
val dns: DnsSettings,
|
||||||
|
val monitoring: MonitoringSettings,
|
||||||
|
val proxy: ProxySettings,
|
||||||
|
val lockdown: LockdownSettings,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val runtimeSettingsSnapshot =
|
||||||
|
combine(
|
||||||
|
settingsRepository.flow,
|
||||||
|
dnsSettingsRepository.flow,
|
||||||
|
monitoringSettingsRepository.flow,
|
||||||
|
proxyRepository.flow,
|
||||||
|
lockdownModeRepository.flow,
|
||||||
|
) { general, dns, monitoring, proxy, lockdown ->
|
||||||
|
RuntimeSettingsSnapshot(
|
||||||
|
general = general,
|
||||||
|
dns = dns,
|
||||||
|
monitoring = monitoring,
|
||||||
|
proxy = proxy,
|
||||||
|
lockdown = lockdown,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 }
|
||||||
|
|
||||||
|
if (source == TunnelActionSource.USER) {
|
||||||
|
_userOverrideFlow.tryEmit(Unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// enforce single tunnel, for now
|
||||||
|
if (backendStatus.value.activeTunnels.isNotEmpty()) {
|
||||||
|
stopActiveTunnelsInternal(source)
|
||||||
|
}
|
||||||
|
|
||||||
|
startTunnelInternal(config, source)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun stopTunnel(id: Int, source: TunnelActionSource = TunnelActionSource.USER) =
|
||||||
|
tunnelMutex.withLock {
|
||||||
|
if (source == TunnelActionSource.USER) {
|
||||||
|
_userOverrideFlow.tryEmit(Unit)
|
||||||
|
}
|
||||||
|
stopTunnelInternal(id, source)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun stopActiveTunnels(source: TunnelActionSource = TunnelActionSource.USER) =
|
||||||
|
tunnelMutex.withLock {
|
||||||
|
if (source == TunnelActionSource.USER) {
|
||||||
|
_userOverrideFlow.tryEmit(Unit)
|
||||||
|
}
|
||||||
|
stopActiveTunnelsInternal(source)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 lockdownSettings = snapshot.lockdown
|
||||||
|
|
||||||
|
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,
|
||||||
|
lockdownSettings.toKillSwitchConfig(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
if (source == TunnelActionSource.USER) {
|
||||||
|
_userOverrideFlow.tryEmit(Unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
val active = tunnelProvider.backendStatus.value.activeTunnels
|
||||||
|
if (active.isNotEmpty()) {
|
||||||
|
lastActiveTunnels = active.keys.toList()
|
||||||
|
|
||||||
|
active.keys.forEach { id ->
|
||||||
|
_actions.emit(TunnelActionEvent.Stopped(tunnelId = id, source = source))
|
||||||
|
}
|
||||||
|
|
||||||
|
stopActiveTunnelsInternal(source)
|
||||||
|
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(
|
||||||
|
source: TunnelActionSource = TunnelActionSource.USER
|
||||||
|
) {
|
||||||
|
val active = tunnelProvider.backendStatus.value.activeTunnels
|
||||||
|
|
||||||
|
active.keys.forEach { id ->
|
||||||
|
_actions.emit(TunnelActionEvent.Stopped(tunnelId = id, source = source))
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
-163
@@ -1,163 +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.di.Dispatcher
|
|
||||||
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 kotlinx.coroutines.CoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.koin.android.ext.android.inject
|
|
||||||
import org.koin.core.qualifier.named
|
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
abstract class BaseTunnelForegroundService : LifecycleService(), TunnelService {
|
|
||||||
|
|
||||||
private val notificationManager: NotificationManager by inject()
|
|
||||||
|
|
||||||
private val serviceManager: ServiceManager by inject()
|
|
||||||
|
|
||||||
private val tunnelManager: TunnelManager by inject()
|
|
||||||
|
|
||||||
private val ioDispatcher: CoroutineDispatcher by inject(named(Dispatcher.IO))
|
|
||||||
|
|
||||||
private val settingsRepository: GeneralSettingRepository by inject()
|
|
||||||
|
|
||||||
private val tunnelsRepository: TunnelRepository by inject()
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
@@ -1,192 +0,0 @@
|
|||||||
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.domain.repository.AutoTunnelSettingsRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
|
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.flow.combine
|
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
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(
|
|
||||||
private val context: Context,
|
|
||||||
ioDispatcher: CoroutineDispatcher,
|
|
||||||
applicationScope: CoroutineScope,
|
|
||||||
private val mainDispatcher: CoroutineDispatcher,
|
|
||||||
private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
|
|
||||||
) {
|
|
||||||
|
|
||||||
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() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 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 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-6
@@ -1,6 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.service
|
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
-6
@@ -1,6 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.service
|
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
|
||||||
|
|
||||||
class VpnForegroundService(override val fgsType: Int = Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID) :
|
|
||||||
BaseTunnelForegroundService()
|
|
||||||
-393
@@ -1,393 +0,0 @@
|
|||||||
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.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.Dispatcher
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.AutoTunnelSettings
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.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 java.lang.ref.WeakReference
|
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.FlowPreview
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.combine
|
|
||||||
import kotlinx.coroutines.flow.debounce
|
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
|
||||||
import kotlinx.coroutines.flow.flowOn
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.flow.merge
|
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
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
|
|
||||||
|
|
||||||
class AutoTunnelService : LifecycleService() {
|
|
||||||
|
|
||||||
private val networkMonitor: NetworkMonitor by inject()
|
|
||||||
|
|
||||||
private val notificationManager: NotificationManager by inject()
|
|
||||||
|
|
||||||
private val ioDispatcher: CoroutineDispatcher by inject(named(Dispatcher.IO))
|
|
||||||
|
|
||||||
private val serviceManager: ServiceManager by inject()
|
|
||||||
|
|
||||||
private val tunnelManager: TunnelManager by inject()
|
|
||||||
|
|
||||||
private val autoTunnelRepository: AutoTunnelSettingsRepository by inject()
|
|
||||||
private val settingsRepository: GeneralSettingRepository by inject()
|
|
||||||
private val tunnelsRepository: TunnelRepository by inject()
|
|
||||||
|
|
||||||
private val defaultState = AutoTunnelState()
|
|
||||||
|
|
||||||
private val autoTunMutex = Mutex()
|
|
||||||
|
|
||||||
private val autoTunnelStateFlow = MutableStateFlow(defaultState)
|
|
||||||
|
|
||||||
private var autoTunnelJob: Job? = null
|
|
||||||
private var permissionsJob: Job? = null
|
|
||||||
private var autoTunnelFailoverJob: Job? = null
|
|
||||||
|
|
||||||
class LocalBinder(service: AutoTunnelService) : Binder() {
|
|
||||||
private val serviceRef = WeakReference(service)
|
|
||||||
|
|
||||||
val service: AutoTunnelService?
|
|
||||||
get() = serviceRef.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val binder = LocalBinder(this)
|
|
||||||
|
|
||||||
override fun onCreate() {
|
|
||||||
super.onCreate()
|
|
||||||
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")
|
|
||||||
start()
|
|
||||||
return START_STICKY
|
|
||||||
}
|
|
||||||
|
|
||||||
fun start() {
|
|
||||||
launchWatcherNotification()
|
|
||||||
autoTunnelJob?.cancel()
|
|
||||||
autoTunnelJob = startAutoTunnelStateJob()
|
|
||||||
permissionsJob?.cancel()
|
|
||||||
permissionsJob = startLocationPermissionsNotificationJob()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun stop() {
|
|
||||||
stopSelf()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
serviceManager.handleAutoTunnelServiceDestroy()
|
|
||||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
|
||||||
super.onDestroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun launchWatcherNotification(
|
|
||||||
description: String = getString(R.string.monitoring_state_changes)
|
|
||||||
) {
|
|
||||||
val notification =
|
|
||||||
notificationManager.createNotification(
|
|
||||||
WireGuardNotification.NotificationChannels.AUTO_TUNNEL,
|
|
||||||
title = getString(R.string.auto_tunnel_title),
|
|
||||||
description = description,
|
|
||||||
actions =
|
|
||||||
listOf(
|
|
||||||
notificationManager.createNotificationAction(
|
|
||||||
NotificationAction.AUTO_TUNNEL_OFF
|
|
||||||
)
|
|
||||||
),
|
|
||||||
onGoing = true,
|
|
||||||
groupKey = NotificationManager.AUTO_TUNNEL_GROUP_KEY,
|
|
||||||
isGroupSummary = true,
|
|
||||||
)
|
|
||||||
ServiceCompat.startForeground(
|
|
||||||
this,
|
|
||||||
NotificationManager.AUTO_TUNNEL_NOTIFICATION_ID,
|
|
||||||
notification,
|
|
||||||
Constants.SPECIAL_USE_SERVICE_TYPE_ID,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startAutoTunnelStateJob(): Job =
|
|
||||||
lifecycleScope.launch(ioDispatcher) {
|
|
||||||
val networkFlow =
|
|
||||||
debouncedConnectivityStateFlow
|
|
||||||
.flowOn(ioDispatcher)
|
|
||||||
.map { it.toDomain() }
|
|
||||||
.map(::NetworkChange)
|
|
||||||
.distinctUntilChanged()
|
|
||||||
|
|
||||||
val settingsFlow =
|
|
||||||
combineSettings().map { (appMode, settings, tunnels) ->
|
|
||||||
SettingsChange(appMode, settings, tunnels)
|
|
||||||
}
|
|
||||||
|
|
||||||
val tunnelsFlow = tunnelManager.activeTunnels.map(::ActiveTunnelsChange)
|
|
||||||
|
|
||||||
var reevaluationJob: Job? = null
|
|
||||||
|
|
||||||
// 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))
|
|
||||||
|
|
||||||
// re-evaluate network state after a short duration to prevent missed state changes
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun combineSettings(): Flow<Triple<AppMode, AutoTunnelSettings, List<TunnelConfig>>> {
|
|
||||||
return combine(
|
|
||||||
settingsRepository.flow.map { it.appMode }.distinctUntilChanged(),
|
|
||||||
autoTunnelRepository.flow,
|
|
||||||
tunnelsRepository.userTunnelsFlow.map { tunnels ->
|
|
||||||
// isActive is ignored for equality checks so user can manually toggle off
|
|
||||||
// tunnel with auto-tunnel
|
|
||||||
tunnels.map { it.copy(isActive = false) }
|
|
||||||
},
|
|
||||||
) { 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()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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).onFailure { e ->
|
|
||||||
Timber.e(e, "Auto-tunnel start failed for ${it.name}")
|
|
||||||
// TODO notify or retry
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is AutoTunnelEvent.Stop -> tunnelManager.stopActiveTunnels()
|
|
||||||
AutoTunnelEvent.DoNothing -> Timber.i("Auto-tunneling: nothing to do")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// restart network flow on debounce changes
|
|
||||||
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
|
|
||||||
private val debouncedConnectivityStateFlow: Flow<ConnectivityState> by lazy {
|
|
||||||
autoTunnelRepository.flow
|
|
||||||
.map { it.debounceDelaySeconds.toMillis() }
|
|
||||||
.distinctUntilChanged()
|
|
||||||
.flatMapLatest { debounceMillis ->
|
|
||||||
networkMonitor.connectivityStateFlow.debounce(debounceMillis)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val REEVALUATE_CHECK_DELAY = 3_000L
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-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
|
|
||||||
-109
@@ -1,109 +0,0 @@
|
|||||||
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.*
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
|
||||||
import kotlin.concurrent.atomics.AtomicBoolean
|
|
||||||
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.koin.android.ext.android.inject
|
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
class AutoTunnelControlTile : TileService(), LifecycleOwner {
|
|
||||||
|
|
||||||
private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository by inject()
|
|
||||||
|
|
||||||
private val serviceManager: ServiceManager by inject()
|
|
||||||
|
|
||||||
@OptIn(ExperimentalAtomicApi::class) val isCollecting = AtomicBoolean(false)
|
|
||||||
|
|
||||||
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
|
|
||||||
|
|
||||||
override fun onCreate() {
|
|
||||||
super.onCreate()
|
|
||||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTileAdded() {
|
|
||||||
super.onTileAdded()
|
|
||||||
initTileState()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStopListening() {
|
|
||||||
super.onStopListening()
|
|
||||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalAtomicApi::class)
|
|
||||||
private fun initTileState() {
|
|
||||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
|
|
||||||
Timber.d("Start listening called for auto tunnel tile")
|
|
||||||
if (isCollecting.compareAndSet(expectedValue = false, newValue = true)) {
|
|
||||||
lifecycleScope.launch {
|
|
||||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
|
||||||
serviceManager.autoTunnelService.collect {
|
|
||||||
if (it != null) return@collect setActive()
|
|
||||||
setInactive()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStartListening() {
|
|
||||||
super.onStartListening()
|
|
||||||
initTileState()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onClick() {
|
|
||||||
super.onClick()
|
|
||||||
unlockAndRun {
|
|
||||||
lifecycleScope.launch {
|
|
||||||
if (serviceManager.autoTunnelService.value != null) {
|
|
||||||
autoTunnelSettingsRepository.updateAutoTunnelEnabled(false)
|
|
||||||
setInactive()
|
|
||||||
} else {
|
|
||||||
autoTunnelSettingsRepository.updateAutoTunnelEnabled(true)
|
|
||||||
setActive()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setActive() {
|
|
||||||
qsTile?.let {
|
|
||||||
it.state = Tile.STATE_ACTIVE
|
|
||||||
it.updateTile()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setInactive() {
|
|
||||||
qsTile?.let {
|
|
||||||
it.state = Tile.STATE_INACTIVE
|
|
||||||
it.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
|
|
||||||
}
|
|
||||||
-213
@@ -1,213 +0,0 @@
|
|||||||
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 androidx.lifecycle.repeatOnLifecycle
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
|
||||||
import kotlin.concurrent.atomics.AtomicBoolean
|
|
||||||
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
|
||||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import org.koin.android.ext.android.inject
|
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
class TunnelControlTile : TileService(), LifecycleOwner {
|
|
||||||
|
|
||||||
private val tunnelsRepository: TunnelRepository by inject()
|
|
||||||
|
|
||||||
private val serviceManager: ServiceManager by inject()
|
|
||||||
|
|
||||||
private val tunnelManager: TunnelManager by inject()
|
|
||||||
|
|
||||||
@OptIn(ExperimentalAtomicApi::class) val isCollecting = AtomicBoolean(false)
|
|
||||||
|
|
||||||
private val startLock = Mutex()
|
|
||||||
|
|
||||||
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
|
|
||||||
|
|
||||||
override fun onCreate() {
|
|
||||||
super.onCreate()
|
|
||||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTileAdded() {
|
|
||||||
super.onTileAdded()
|
|
||||||
initTileState()
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalAtomicApi::class)
|
|
||||||
private fun initTileState() {
|
|
||||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
|
|
||||||
Timber.d("Start listening called for tunnel tile")
|
|
||||||
if (isCollecting.compareAndSet(expectedValue = false, newValue = true)) {
|
|
||||||
lifecycleScope.launch {
|
|
||||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
|
||||||
tunnelManager.activeTunnels
|
|
||||||
.distinctUntilChangedBy { it.size }
|
|
||||||
.collect { updateTileState() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStartListening() {
|
|
||||||
super.onStartListening()
|
|
||||||
initTileState()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStopListening() {
|
|
||||||
super.onStopListening()
|
|
||||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun updateTileState() {
|
|
||||||
try {
|
|
||||||
val tunnels = tunnelsRepository.getAll()
|
|
||||||
if (tunnels.isEmpty()) {
|
|
||||||
setUnavailable()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val activeTunnels =
|
|
||||||
tunnelManager.activeTunnels.value.filter { it.value.status.isUpOrStarting() }
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
else -> updateTileForLastActiveTunnels()
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e, "Failed to update tunnel state")
|
|
||||||
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 onClick() {
|
|
||||||
super.onClick()
|
|
||||||
unlockAndRun {
|
|
||||||
lifecycleScope.launch {
|
|
||||||
startLock.withLock {
|
|
||||||
if (tunnelManager.activeTunnels.value.isNotEmpty())
|
|
||||||
return@launch tunnelManager.stopActiveTunnels()
|
|
||||||
val lastActive = WireGuardAutoTunnel.getLastActiveTunnels()
|
|
||||||
if (lastActive.isEmpty()) {
|
|
||||||
tunnelsRepository.getStartTunnel()?.let { tunnelManager.startTunnel(it) }
|
|
||||||
} else {
|
|
||||||
lastActive.forEach { id ->
|
|
||||||
tunnelsRepository.getById(id)?.let { tunnelManager.startTunnel(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setActive() {
|
|
||||||
qsTile?.let {
|
|
||||||
it.state = Tile.STATE_ACTIVE
|
|
||||||
it.updateTile()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setInactive() {
|
|
||||||
qsTile?.let {
|
|
||||||
it.state = Tile.STATE_INACTIVE
|
|
||||||
it.updateTile()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setUnavailable() {
|
|
||||||
qsTile?.let {
|
|
||||||
it.state = Tile.STATE_UNAVAILABLE
|
|
||||||
setTileDescription("")
|
|
||||||
it.updateTile()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setTileDescription(description: String) {
|
|
||||||
qsTile?.let {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
||||||
it.subtitle = description
|
|
||||||
it.stateDescription = description
|
|
||||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
||||||
it.subtitle = description
|
|
||||||
}
|
|
||||||
it.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
|
|
||||||
}
|
|
||||||
+4
-4
@@ -34,7 +34,7 @@ class DynamicShortcutManager(
|
|||||||
intent =
|
intent =
|
||||||
Intent(context, ShortcutsActivity::class.java).apply {
|
Intent(context, ShortcutsActivity::class.java).apply {
|
||||||
putExtra("className", "WireGuardTunnelService")
|
putExtra("className", "WireGuardTunnelService")
|
||||||
action = ShortcutsActivity.Action.STOP.name
|
action = ShortcutContract.Action.STOP.name
|
||||||
},
|
},
|
||||||
shortcutIcon = R.drawable.vpn_off,
|
shortcutIcon = R.drawable.vpn_off,
|
||||||
),
|
),
|
||||||
@@ -45,7 +45,7 @@ class DynamicShortcutManager(
|
|||||||
intent =
|
intent =
|
||||||
Intent(context, ShortcutsActivity::class.java).apply {
|
Intent(context, ShortcutsActivity::class.java).apply {
|
||||||
putExtra("className", "WireGuardTunnelService")
|
putExtra("className", "WireGuardTunnelService")
|
||||||
action = ShortcutsActivity.Action.START.name
|
action = ShortcutContract.Action.START.name
|
||||||
},
|
},
|
||||||
shortcutIcon = R.drawable.vpn_on,
|
shortcutIcon = R.drawable.vpn_on,
|
||||||
),
|
),
|
||||||
@@ -56,7 +56,7 @@ class DynamicShortcutManager(
|
|||||||
intent =
|
intent =
|
||||||
Intent(context, ShortcutsActivity::class.java).apply {
|
Intent(context, ShortcutsActivity::class.java).apply {
|
||||||
putExtra("className", "WireGuardConnectivityWatcherService")
|
putExtra("className", "WireGuardConnectivityWatcherService")
|
||||||
action = ShortcutsActivity.Action.START.name
|
action = ShortcutContract.Action.START.name
|
||||||
},
|
},
|
||||||
shortcutIcon = R.drawable.auto_play,
|
shortcutIcon = R.drawable.auto_play,
|
||||||
),
|
),
|
||||||
@@ -67,7 +67,7 @@ class DynamicShortcutManager(
|
|||||||
intent =
|
intent =
|
||||||
Intent(context, ShortcutsActivity::class.java).apply {
|
Intent(context, ShortcutsActivity::class.java).apply {
|
||||||
putExtra("className", "WireGuardConnectivityWatcherService")
|
putExtra("className", "WireGuardConnectivityWatcherService")
|
||||||
action = ShortcutsActivity.Action.STOP.name
|
action = ShortcutContract.Action.STOP.name
|
||||||
},
|
},
|
||||||
shortcutIcon = R.drawable.auto_pause,
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
-54
@@ -2,73 +2,25 @@ package com.zaneschepke.wireguardautotunnel.core.shortcut
|
|||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelService
|
import com.zaneschepke.wireguardautotunnel.core.orchestration.ShortcutCoordinator
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
|
||||||
import com.zaneschepke.wireguardautotunnel.di.Scope
|
import com.zaneschepke.wireguardautotunnel.di.Scope
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import org.koin.core.qualifier.named
|
import org.koin.core.qualifier.named
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
class ShortcutsActivity : ComponentActivity() {
|
class ShortcutsActivity : ComponentActivity() {
|
||||||
|
|
||||||
private val settingsRepository: GeneralSettingRepository by inject()
|
private val shortcutCoordinator: ShortcutCoordinator by inject()
|
||||||
private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository by inject()
|
|
||||||
private val tunnelsRepository: TunnelRepository by inject()
|
|
||||||
private val tunnelManager: TunnelManager by inject()
|
|
||||||
private val applicationScope: CoroutineScope by inject(named(Scope.APPLICATION))
|
private val applicationScope: CoroutineScope by inject(named(Scope.APPLICATION))
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finish()
|
finish()
|
||||||
}
|
|
||||||
|
|
||||||
enum class Action {
|
applicationScope.launch { shortcutCoordinator.handle(intent) }
|
||||||
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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+116
@@ -0,0 +1,116 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import com.zaneschepke.tunnel.ApplicationProvider
|
||||||
|
import com.zaneschepke.tunnel.model.BackendMode
|
||||||
|
import com.zaneschepke.tunnel.state.BackendStatus
|
||||||
|
import com.zaneschepke.wireguardautotunnel.MainActivity
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.notification.AndroidNotificationService
|
||||||
|
import com.zaneschepke.wireguardautotunnel.notification.NotificationService
|
||||||
|
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.PROXY_GROUP_KEY
|
||||||
|
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.VPN_GROUP_KEY
|
||||||
|
import com.zaneschepke.wireguardautotunnel.notification.TunnelNotificationLine
|
||||||
|
import com.zaneschepke.wireguardautotunnel.notification.TunnelNotificationService
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelTileRefresher
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
|
||||||
|
class AndroidApplicationProvider(
|
||||||
|
private val notificationService: NotificationService,
|
||||||
|
private val tunnelNotificationService: TunnelNotificationService,
|
||||||
|
private val tunnelRepository: TunnelRepository,
|
||||||
|
) : ApplicationProvider {
|
||||||
|
|
||||||
|
private val context: Context = notificationService.context
|
||||||
|
|
||||||
|
override fun refreshTile(context: Context) {
|
||||||
|
TunnelTileRefresher.refresh(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createVpnConfigurePendingIntent(context: Context): PendingIntent {
|
||||||
|
return PendingIntent.getActivity(
|
||||||
|
context,
|
||||||
|
0,
|
||||||
|
Intent(context, MainActivity::class.java).apply {
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||||
|
},
|
||||||
|
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val vpnInitNotification: Notification
|
||||||
|
get() =
|
||||||
|
notificationService.createNotification(
|
||||||
|
channel = AndroidNotificationService.NotificationChannels.Tunnel.VPN,
|
||||||
|
title = context.getString(R.string.initializing),
|
||||||
|
onGoing = true,
|
||||||
|
groupKey = VPN_GROUP_KEY,
|
||||||
|
)
|
||||||
|
|
||||||
|
override val proxyInitNotification: Notification
|
||||||
|
get() =
|
||||||
|
notificationService.createNotification(
|
||||||
|
channel = AndroidNotificationService.NotificationChannels.Tunnel.Proxy,
|
||||||
|
title = context.getString(R.string.initializing),
|
||||||
|
onGoing = true,
|
||||||
|
groupKey = PROXY_GROUP_KEY,
|
||||||
|
)
|
||||||
|
|
||||||
|
override val vpnNotificationId: Int
|
||||||
|
get() = NotificationService.VPN_NOTIFICATION_ID
|
||||||
|
|
||||||
|
override val proxyNotificationId: Int
|
||||||
|
get() = NotificationService.PROXY_NOTIFICATION_ID
|
||||||
|
|
||||||
|
override suspend fun buildVpnPersistentNotification(
|
||||||
|
currentStatus: BackendStatus
|
||||||
|
): Notification {
|
||||||
|
val lines = computeVpnNotificationLines(currentStatus)
|
||||||
|
return tunnelNotificationService.buildVpnPersistentNotification(lines)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun buildProxyPersistentNotification(
|
||||||
|
currentStatus: BackendStatus
|
||||||
|
): Notification {
|
||||||
|
val lines = computeProxyNotificationLines(currentStatus)
|
||||||
|
return tunnelNotificationService.buildProxyPersistentNotification(lines)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun computeVpnNotificationLines(
|
||||||
|
status: BackendStatus
|
||||||
|
): Map<Int, TunnelNotificationLine> {
|
||||||
|
val activeTunnels = status.activeTunnels
|
||||||
|
val allTunnels = tunnelRepository.userTunnelsFlow.first()
|
||||||
|
return activeTunnels
|
||||||
|
.mapNotNull { (id, activeTunnel) ->
|
||||||
|
val mode = activeTunnel.mode ?: return@mapNotNull null
|
||||||
|
if (mode !is BackendMode.Vpn && mode !is BackendMode.Proxy.KillSwitchPrimary)
|
||||||
|
return@mapNotNull null
|
||||||
|
val tunnel = allTunnels.find { it.id == id } ?: return@mapNotNull null
|
||||||
|
val displayState = DisplayTunnelState.from(activeTunnel)
|
||||||
|
TunnelNotificationLine(id, tunnel.name, displayState)
|
||||||
|
}
|
||||||
|
.associateBy { it.id }
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun computeProxyNotificationLines(
|
||||||
|
status: BackendStatus
|
||||||
|
): Map<Int, TunnelNotificationLine> {
|
||||||
|
val activeTunnels = status.activeTunnels
|
||||||
|
val allTunnels = tunnelRepository.userTunnelsFlow.first()
|
||||||
|
return activeTunnels
|
||||||
|
.mapNotNull { (id, activeTunnel) ->
|
||||||
|
val mode = activeTunnel.mode ?: return@mapNotNull null
|
||||||
|
if (mode !is BackendMode.Proxy.Standard) return@mapNotNull null
|
||||||
|
val tunnel = allTunnels.find { it.id == id } ?: return@mapNotNull null
|
||||||
|
val displayState = DisplayTunnelState.from(activeTunnel)
|
||||||
|
TunnelNotificationLine(id, tunnel.name, displayState)
|
||||||
|
}
|
||||||
|
.associateBy { it.id }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 is 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)
|
|
||||||
}
|
|
||||||
+52
@@ -0,0 +1,52 @@
|
|||||||
|
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.model.LockdownSettings
|
||||||
|
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
-187
@@ -1,187 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.backend.TunnelBackend
|
|
||||||
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.UnknownError
|
|
||||||
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 kotlinx.coroutines.CancellationException
|
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
class TunnelLifecycleManager(
|
|
||||||
private val backend: TunnelBackend,
|
|
||||||
private val applicationScope: CoroutineScope,
|
|
||||||
private val ioDispatcher: CoroutineDispatcher,
|
|
||||||
private val sharedActiveTunnels: MutableStateFlow<Map<Int, TunnelState>>,
|
|
||||||
) : TunnelProvider {
|
|
||||||
|
|
||||||
override val activeTunnels: StateFlow<Map<Int, TunnelState>> = sharedActiveTunnels.asStateFlow()
|
|
||||||
|
|
||||||
private val _errorEvents = MutableSharedFlow<Pair<String?, BackendCoreException>>()
|
|
||||||
override val errorEvents: SharedFlow<Pair<String?, BackendCoreException>> =
|
|
||||||
_errorEvents.asSharedFlow()
|
|
||||||
|
|
||||||
private val _messageEvents = MutableSharedFlow<Pair<String?, BackendMessage>>()
|
|
||||||
override val messageEvents: SharedFlow<Pair<String?, BackendMessage>> =
|
|
||||||
_messageEvents.asSharedFlow()
|
|
||||||
|
|
||||||
private val tunnelJobs = ConcurrentHashMap<Int, Job>()
|
|
||||||
private val tunMutex = Mutex()
|
|
||||||
private val tunStatusMutex = Mutex()
|
|
||||||
|
|
||||||
override suspend fun startTunnel(tunnelConfig: TunnelConfig): Result<Unit> =
|
|
||||||
tunMutex.withLock {
|
|
||||||
val id = tunnelConfig.id
|
|
||||||
if (sharedActiveTunnels.value.containsKey(id)) {
|
|
||||||
Timber.w("Tunnel is already running: ${tunnelConfig.name}")
|
|
||||||
return Result.failure(IllegalStateException("Tunnel already running"))
|
|
||||||
}
|
|
||||||
|
|
||||||
val startupCompleted = CompletableDeferred<Result<Unit>>()
|
|
||||||
|
|
||||||
val job =
|
|
||||||
applicationScope.launch(ioDispatcher) {
|
|
||||||
try {
|
|
||||||
updateTunnelStatus(id, TunnelStatus.Starting)
|
|
||||||
backend.tunnelStateFlow(tunnelConfig).collect { status ->
|
|
||||||
updateTunnelStatus(id, status)
|
|
||||||
|
|
||||||
if (status != TunnelStatus.Starting && !startupCompleted.isCompleted) {
|
|
||||||
if (status is TunnelStatus.Up) {
|
|
||||||
startupCompleted.complete(Result.success(Unit))
|
|
||||||
} else {
|
|
||||||
startupCompleted.complete(Result.failure(UnknownError()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: BackendCoreException) {
|
|
||||||
_errorEvents.emit(tunnelConfig.name to e)
|
|
||||||
updateTunnelStatus(id, TunnelStatus.Down)
|
|
||||||
startupCompleted.complete(Result.failure(e))
|
|
||||||
} catch (_: CancellationException) {} finally {
|
|
||||||
tunnelJobs.remove(id)
|
|
||||||
sharedActiveTunnels.update { it - id }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tunnelJobs[id] = job
|
|
||||||
job.invokeOnCompletion { tunnelJobs.remove(id) }
|
|
||||||
|
|
||||||
try {
|
|
||||||
startupCompleted.await()
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
job.cancel()
|
|
||||||
Result.failure(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun stopTunnel(tunnelId: Int) =
|
|
||||||
tunMutex.withLock {
|
|
||||||
val currentState = sharedActiveTunnels.value[tunnelId]?.status ?: return@withLock
|
|
||||||
updateTunnelStatus(tunnelId, TunnelStatus.Stopping)
|
|
||||||
tunnelJobs[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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun forceStopTunnel(tunnelId: Int) {
|
|
||||||
backend.forceStopTunnel(tunnelId)
|
|
||||||
tunnelJobs[tunnelId]?.cancel()
|
|
||||||
tunnelJobs.remove(tunnelId)
|
|
||||||
sharedActiveTunnels.update { it - tunnelId }
|
|
||||||
updateTunnelStatus(tunnelId, TunnelStatus.Down)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun stopActiveTunnels() {
|
|
||||||
sharedActiveTunnels.value.forEach { (id, state) ->
|
|
||||||
if (state.status.isUpOrStarting()) {
|
|
||||||
stopTunnel(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun updateTunnelStatus(
|
|
||||||
tunnelId: Int,
|
|
||||||
status: TunnelStatus?,
|
|
||||||
stats: TunnelStatistics?,
|
|
||||||
pingStates: Map<String, PingState>?,
|
|
||||||
logHealthState: LogHealthState?,
|
|
||||||
) =
|
|
||||||
tunStatusMutex.withLock {
|
|
||||||
sharedActiveTunnels.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")
|
|
||||||
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 fun setBackendMode(backendMode: BackendMode) = backend.setBackendMode(backendMode)
|
|
||||||
|
|
||||||
override fun getBackendMode(): BackendMode = backend.getBackendMode()
|
|
||||||
|
|
||||||
override suspend fun runningTunnelNames(): Set<String> = backend.runningTunnelNames()
|
|
||||||
|
|
||||||
override fun handleDnsReresolve(tunnelConfig: TunnelConfig): Boolean =
|
|
||||||
backend.handleDnsReresolve(tunnelConfig)
|
|
||||||
|
|
||||||
override fun getStatistics(tunnelId: Int): TunnelStatistics? = backend.getStatistics(tunnelId)
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val STOP_TIMEOUT_MS: Long = 5_000L
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,359 +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.core.service.ServiceManager
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.backend.TunnelBackend
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.handler.DynamicDnsHandler
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.handler.TunnelActiveStatePersister
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.handler.TunnelMonitorHandler
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.handler.TunnelServiceHandler
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
|
|
||||||
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.MonitoringSettingsRepository
|
|
||||||
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 com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils
|
|
||||||
import kotlin.concurrent.atomics.AtomicBoolean
|
|
||||||
import kotlin.concurrent.atomics.AtomicReference
|
|
||||||
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
|
||||||
import kotlinx.coroutines.flow.filterNot
|
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
|
||||||
import kotlinx.coroutines.flow.merge
|
|
||||||
import kotlinx.coroutines.flow.shareIn
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.plus
|
|
||||||
import kotlinx.coroutines.supervisorScope
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class, ExperimentalAtomicApi::class)
|
|
||||||
class TunnelManager(
|
|
||||||
kernelBackend: TunnelBackend,
|
|
||||||
userspaceBackend: TunnelBackend,
|
|
||||||
proxyUserspaceBackend: TunnelBackend,
|
|
||||||
networkMonitor: NetworkMonitor,
|
|
||||||
networkUtils: NetworkUtils,
|
|
||||||
powerManager: PowerManager,
|
|
||||||
logReader: LogReader,
|
|
||||||
monitoringSettingsRepository: MonitoringSettingsRepository,
|
|
||||||
private val serviceManager: ServiceManager,
|
|
||||||
private val settingsRepository: GeneralSettingRepository,
|
|
||||||
private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
|
|
||||||
private val lockdownSettingsRepository: LockdownSettingsRepository,
|
|
||||||
private val tunnelsRepository: TunnelRepository,
|
|
||||||
private val applicationScope: CoroutineScope,
|
|
||||||
private val ioDispatcher: CoroutineDispatcher,
|
|
||||||
) : TunnelProvider {
|
|
||||||
|
|
||||||
private val _activeTunnels = MutableStateFlow<Map<Int, TunnelState>>(emptyMap())
|
|
||||||
override val activeTunnels: StateFlow<Map<Int, TunnelState>> = _activeTunnels.asStateFlow()
|
|
||||||
|
|
||||||
@OptIn(ExperimentalAtomicApi::class) val currentAppMode = AtomicReference(AppMode.VPN)
|
|
||||||
|
|
||||||
private val defaultManager =
|
|
||||||
TunnelLifecycleManager(userspaceBackend, applicationScope, ioDispatcher, _activeTunnels)
|
|
||||||
|
|
||||||
private val lifecycleManagers: Map<AppMode, TunnelLifecycleManager> =
|
|
||||||
mapOf(
|
|
||||||
AppMode.KERNEL to
|
|
||||||
TunnelLifecycleManager(
|
|
||||||
kernelBackend,
|
|
||||||
applicationScope,
|
|
||||||
ioDispatcher,
|
|
||||||
_activeTunnels,
|
|
||||||
),
|
|
||||||
AppMode.VPN to defaultManager,
|
|
||||||
AppMode.PROXY to
|
|
||||||
TunnelLifecycleManager(
|
|
||||||
proxyUserspaceBackend,
|
|
||||||
applicationScope,
|
|
||||||
ioDispatcher,
|
|
||||||
_activeTunnels,
|
|
||||||
),
|
|
||||||
AppMode.LOCK_DOWN to
|
|
||||||
TunnelLifecycleManager(
|
|
||||||
proxyUserspaceBackend,
|
|
||||||
applicationScope,
|
|
||||||
ioDispatcher,
|
|
||||||
_activeTunnels,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
@OptIn(ExperimentalAtomicApi::class)
|
|
||||||
private fun getProvider(): TunnelProvider {
|
|
||||||
return lifecycleManagers[currentAppMode.load()] ?: defaultManager
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun startTunnel(tunnelConfig: TunnelConfig): Result<Unit> =
|
|
||||||
getProvider().startTunnel(tunnelConfig)
|
|
||||||
|
|
||||||
override suspend fun stopTunnel(tunnelId: Int) = getProvider().stopTunnel(tunnelId)
|
|
||||||
|
|
||||||
override suspend fun forceStopTunnel(tunnelId: Int) = getProvider().forceStopTunnel(tunnelId)
|
|
||||||
|
|
||||||
override suspend fun stopActiveTunnels() = getProvider().stopActiveTunnels()
|
|
||||||
|
|
||||||
override fun setBackendMode(backendMode: BackendMode) =
|
|
||||||
getProvider().setBackendMode(backendMode)
|
|
||||||
|
|
||||||
override fun getBackendMode(): BackendMode = getProvider().getBackendMode()
|
|
||||||
|
|
||||||
override suspend fun runningTunnelNames(): Set<String> = getProvider().runningTunnelNames()
|
|
||||||
|
|
||||||
override fun handleDnsReresolve(tunnelConfig: TunnelConfig): Boolean =
|
|
||||||
getProvider().handleDnsReresolve(tunnelConfig)
|
|
||||||
|
|
||||||
override fun getStatistics(tunnelId: Int): TunnelStatistics? =
|
|
||||||
getProvider().getStatistics(tunnelId)
|
|
||||||
|
|
||||||
override suspend fun updateTunnelStatus(
|
|
||||||
tunnelId: Int,
|
|
||||||
status: TunnelStatus?,
|
|
||||||
stats: TunnelStatistics?,
|
|
||||||
pingStates: Map<String, PingState>?,
|
|
||||||
logHealthState: LogHealthState?,
|
|
||||||
) = getProvider().updateTunnelStatus(tunnelId, status, stats, pingStates, logHealthState)
|
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
private val localErrorEvents = MutableSharedFlow<Pair<String?, BackendCoreException>>()
|
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
private val localMessageEvents = MutableSharedFlow<Pair<String?, BackendMessage>>()
|
|
||||||
|
|
||||||
override val errorEvents: SharedFlow<Pair<String?, BackendCoreException>> =
|
|
||||||
merge(localErrorEvents, *lifecycleManagers.values.map { it.errorEvents }.toTypedArray())
|
|
||||||
.shareIn(
|
|
||||||
scope = applicationScope + ioDispatcher,
|
|
||||||
started = SharingStarted.Eagerly,
|
|
||||||
replay = 0,
|
|
||||||
)
|
|
||||||
|
|
||||||
override val messageEvents: SharedFlow<Pair<String?, BackendMessage>> =
|
|
||||||
merge(localMessageEvents, *lifecycleManagers.values.map { it.messageEvents }.toTypedArray())
|
|
||||||
.shareIn(
|
|
||||||
scope = applicationScope.plus(ioDispatcher),
|
|
||||||
started = SharingStarted.Eagerly,
|
|
||||||
replay = 0,
|
|
||||||
)
|
|
||||||
|
|
||||||
private val tunnelServiceHandler =
|
|
||||||
TunnelServiceHandler(
|
|
||||||
activeTunnels = activeTunnels,
|
|
||||||
settingsRepository = settingsRepository,
|
|
||||||
serviceManager = serviceManager,
|
|
||||||
applicationScope = applicationScope,
|
|
||||||
ioDispatcher = ioDispatcher,
|
|
||||||
)
|
|
||||||
|
|
||||||
private val tunnelActiveStatePersister =
|
|
||||||
TunnelActiveStatePersister(
|
|
||||||
activeTunnels = activeTunnels,
|
|
||||||
tunnelsRepository = tunnelsRepository,
|
|
||||||
applicationScope = applicationScope,
|
|
||||||
ioDispatcher = ioDispatcher,
|
|
||||||
)
|
|
||||||
|
|
||||||
private val dynamicDnsHandler =
|
|
||||||
DynamicDnsHandler(
|
|
||||||
activeTunnels = activeTunnels,
|
|
||||||
tunnelsRepository = tunnelsRepository,
|
|
||||||
settingsRepository = settingsRepository,
|
|
||||||
localMessageEvents = localMessageEvents,
|
|
||||||
handleDnsReresolve = { config -> handleDnsReresolve(config) },
|
|
||||||
applicationScope = applicationScope,
|
|
||||||
ioDispatcher = ioDispatcher,
|
|
||||||
)
|
|
||||||
|
|
||||||
private val fullTunnelMonitorHandler =
|
|
||||||
TunnelMonitorHandler(
|
|
||||||
activeTunnels = activeTunnels,
|
|
||||||
tunnelsRepository = tunnelsRepository,
|
|
||||||
settingsRepository = settingsRepository,
|
|
||||||
monitoringSettingsRepository = monitoringSettingsRepository,
|
|
||||||
networkMonitor = networkMonitor,
|
|
||||||
networkUtils = networkUtils,
|
|
||||||
powerManager = powerManager,
|
|
||||||
logReader = logReader,
|
|
||||||
getStatistics = { id -> getStatistics(id) },
|
|
||||||
updateTunnelStatus = { id, status, stats, pings, logHealth ->
|
|
||||||
updateTunnelStatus(id, status, stats, pings, logHealth)
|
|
||||||
},
|
|
||||||
applicationScope = applicationScope,
|
|
||||||
ioDispatcher = ioDispatcher,
|
|
||||||
)
|
|
||||||
|
|
||||||
init {
|
|
||||||
applicationScope.launch(ioDispatcher) {
|
|
||||||
val initialEmit = AtomicBoolean(true)
|
|
||||||
settingsRepository.flow
|
|
||||||
.filterNotNull()
|
|
||||||
.filterNot { it == GeneralSettings() }
|
|
||||||
.distinctUntilChangedBy { it.appMode }
|
|
||||||
.collect { settings ->
|
|
||||||
val isInitialEmit = initialEmit.exchange(false)
|
|
||||||
val previousMode = currentAppMode.exchange(settings.appMode)
|
|
||||||
|
|
||||||
if (isInitialEmit) {
|
|
||||||
return@collect handleRestore(settings)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (previousMode != settings.appMode) {
|
|
||||||
handleModeChangeCleanup(previousMode)
|
|
||||||
}
|
|
||||||
if (settings.appMode == AppMode.LOCK_DOWN) {
|
|
||||||
handleLockDownModeInit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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()) {
|
|
||||||
setBackendMode(
|
|
||||||
BackendMode.KillSwitch(
|
|
||||||
allowedIps,
|
|
||||||
lockdownSettings.metered,
|
|
||||||
lockdownSettings.dualStack,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
throw NotAuthorized()
|
|
||||||
}
|
|
||||||
} catch (e: BackendCoreException) {
|
|
||||||
localErrorEvents.tryEmit(null to e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun handleModeChangeCleanup(previousAppMode: AppMode) {
|
|
||||||
lifecycleManagers[previousAppMode]?.stopActiveTunnels()
|
|
||||||
if (previousAppMode == AppMode.LOCK_DOWN) {
|
|
||||||
lifecycleManagers[previousAppMode]?.setBackendMode(BackendMode.Inactive)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun handleRestore(settings: GeneralSettings? = null) =
|
|
||||||
withContext(ioDispatcher) {
|
|
||||||
val currentSettings = settings ?: settingsRepository.getGeneralSettings()
|
|
||||||
val autoTunnelSettings = autoTunnelSettingsRepository.getAutoTunnelSettings()
|
|
||||||
val tunnels = tunnelsRepository.userTunnelsFlow.firstOrNull()
|
|
||||||
if (autoTunnelSettings.isAutoTunnelEnabled)
|
|
||||||
return@withContext restoreAutoTunnel(autoTunnelSettings)
|
|
||||||
if (currentSettings.appMode == AppMode.LOCK_DOWN) handleLockDownModeInit()
|
|
||||||
if (tunnels?.any { it.isActive } == true) {
|
|
||||||
if (currentSettings.appMode == AppMode.VPN && !serviceManager.hasVpnPermission())
|
|
||||||
return@withContext localErrorEvents.emit(null to NotAuthorized())
|
|
||||||
when (currentSettings.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.getDefaultTunnel()
|
|
||||||
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) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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}") }
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val RESTART_TUNNEL_DELAY = 300L
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+13
-32
@@ -1,45 +1,26 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
|
import com.zaneschepke.tunnel.Tunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
import com.zaneschepke.tunnel.event.TunnelEvent
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException
|
import com.zaneschepke.tunnel.model.BackendMode
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage
|
import com.zaneschepke.tunnel.state.BackendStatus
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.domain.model.LockdownSettings
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.LogHealthState
|
import kotlinx.coroutines.flow.Flow
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.PingState
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
interface TunnelProvider {
|
interface TunnelProvider {
|
||||||
suspend fun startTunnel(tunnelConfig: TunnelConfig): Result<Unit>
|
|
||||||
|
|
||||||
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>
|
||||||
|
|
||||||
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
|
val events: Flow<TunnelEvent>
|
||||||
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
-128
@@ -1,128 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.tunnel.backend
|
|
||||||
|
|
||||||
import com.wireguard.android.backend.Backend
|
|
||||||
import com.wireguard.android.backend.BackendException
|
|
||||||
import com.wireguard.android.backend.Tunnel
|
|
||||||
import com.wireguard.android.backend.WgQuickBackend
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
|
||||||
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.KernelWireguardNotSupported
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.events.UnknownError
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.WireGuardStatistics
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendCoreException
|
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
|
||||||
import java.util.regex.Pattern
|
|
||||||
import kotlinx.coroutines.TimeoutCancellationException
|
|
||||||
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.launch
|
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
class KernelTunnel(private val runConfigHelper: RunConfigHelper, private val backend: Backend) :
|
|
||||||
TunnelBackend {
|
|
||||||
|
|
||||||
private val runtimeTunnels = ConcurrentHashMap<Int, Tunnel>()
|
|
||||||
|
|
||||||
private fun validateWireGuardInterfaceName(name: String): Result<Unit> {
|
|
||||||
if (name.isEmpty() || name.length > 15)
|
|
||||||
return Result.failure(KernelTunnelName(R.string.kernel_name_error))
|
|
||||||
if (name == "." || name == "..") {
|
|
||||||
return Result.failure(KernelTunnelName(R.string.kernel_name_dots))
|
|
||||||
}
|
|
||||||
val pattern = Pattern.compile("^[a-zA-Z0-9_=+.-]{1,15}$")
|
|
||||||
if (!pattern.matcher(name).matches()) {
|
|
||||||
return Result.failure(KernelTunnelName(R.string.kernel_name_special_characters))
|
|
||||||
}
|
|
||||||
return Result.success(Unit)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun tunnelStateFlow(tunnelConfig: TunnelConfig): Flow<TunnelStatus> = callbackFlow {
|
|
||||||
if (!WgQuickBackend.hasKernelSupport()) throw KernelWireguardNotSupported()
|
|
||||||
validateWireGuardInterfaceName(tunnelConfig.name).onFailure { throw it }
|
|
||||||
|
|
||||||
val stateChannel = Channel<Tunnel.State>()
|
|
||||||
|
|
||||||
val runtimeTunnel = RuntimeWgTunnel(tunnelConfig, stateChannel)
|
|
||||||
runtimeTunnels[tunnelConfig.id] = runtimeTunnel
|
|
||||||
|
|
||||||
val consumerJob = launch {
|
|
||||||
stateChannel.consumeAsFlow().collect { state -> trySend(state.asTunnelState()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
val runConfig = runConfigHelper.buildWgRunConfig(tunnelConfig)
|
|
||||||
backend.setState(runtimeTunnel, Tunnel.State.UP, runConfig)
|
|
||||||
} catch (e: TimeoutCancellationException) {
|
|
||||||
Timber.Forest.e("Startup timed out for ${tunnelConfig.name}")
|
|
||||||
throw DnsFailure()
|
|
||||||
} catch (e: BackendException) {
|
|
||||||
throw e.toBackendCoreException()
|
|
||||||
} catch (e: IllegalArgumentException) {
|
|
||||||
Timber.Forest.e(e, "Invalid backend arguments")
|
|
||||||
throw InvalidConfig()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.Forest.e(e, "Error while setting tunnel state")
|
|
||||||
throw UnknownError()
|
|
||||||
}
|
|
||||||
|
|
||||||
awaitClose {
|
|
||||||
try {
|
|
||||||
backend.setState(runtimeTunnel, Tunnel.State.DOWN, null)
|
|
||||||
} catch (e: BackendException) {
|
|
||||||
// Errors are emitted by caller (lifecycle manager)
|
|
||||||
} finally {
|
|
||||||
consumerJob.cancel()
|
|
||||||
stateChannel.close()
|
|
||||||
runtimeTunnels.remove(tunnelConfig.id)
|
|
||||||
trySend(TunnelStatus.Down)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getStatistics(tunnelId: Int): TunnelStatistics? {
|
|
||||||
return try {
|
|
||||||
val runtimeTunnel = runtimeTunnels[tunnelId] ?: return null
|
|
||||||
WireGuardStatistics(backend.getStatistics(runtimeTunnel))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.Forest.e(e, "Failed to get stats for $tunnelId")
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setBackendMode(backendMode: BackendMode) {
|
|
||||||
Timber.Forest.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, Tunnel.State.DOWN, null)
|
|
||||||
} catch (e: BackendException) {
|
|
||||||
Timber.Forest.e(e, "Force stop failed for $tunnelId")
|
|
||||||
} finally {
|
|
||||||
runtimeTunnels.remove(tunnelId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-101
@@ -1,101 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.tunnel.backend
|
|
||||||
|
|
||||||
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.Optional
|
|
||||||
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(
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-21
@@ -1,21 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.tunnel.backend
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
-19
@@ -1,19 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.tunnel.backend
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
-23
@@ -1,23 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.tunnel.backend
|
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
|
|
||||||
interface TunnelBackend {
|
|
||||||
fun tunnelStateFlow(tunnelConfig: TunnelConfig): Flow<TunnelStatus>
|
|
||||||
|
|
||||||
fun getStatistics(tunnelId: Int): TunnelStatistics?
|
|
||||||
|
|
||||||
fun setBackendMode(backendMode: BackendMode)
|
|
||||||
|
|
||||||
fun getBackendMode(): BackendMode
|
|
||||||
|
|
||||||
fun handleDnsReresolve(tunnelConfig: TunnelConfig): Boolean
|
|
||||||
|
|
||||||
suspend fun runningTunnelNames(): Set<String>
|
|
||||||
|
|
||||||
suspend fun forceStopTunnel(tunnelId: Int)
|
|
||||||
}
|
|
||||||
-119
@@ -1,119 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.tunnel.backend
|
|
||||||
|
|
||||||
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.ServiceNotRunning
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.events.UnknownError
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.events.VpnUnauthorized
|
|
||||||
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 kotlinx.coroutines.TimeoutCancellationException
|
|
||||||
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.launch
|
|
||||||
import org.amnezia.awg.backend.Backend
|
|
||||||
import org.amnezia.awg.backend.BackendException
|
|
||||||
import org.amnezia.awg.backend.Tunnel
|
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
class UserspaceTunnel(private val backend: Backend, private val runConfigHelper: RunConfigHelper) :
|
|
||||||
TunnelBackend {
|
|
||||||
|
|
||||||
private val runtimeTunnels = ConcurrentHashMap<Int, Tunnel>()
|
|
||||||
|
|
||||||
override fun tunnelStateFlow(tunnelConfig: TunnelConfig): Flow<TunnelStatus> = callbackFlow {
|
|
||||||
val stateChannel = Channel<Tunnel.State>()
|
|
||||||
|
|
||||||
val runtimeTunnel = RuntimeAwgTunnel(tunnelConfig, stateChannel)
|
|
||||||
runtimeTunnels[tunnelConfig.id] = runtimeTunnel
|
|
||||||
|
|
||||||
val consumerJob = launch {
|
|
||||||
stateChannel.consumeAsFlow().collect { awgState -> trySend(awgState.asTunnelState()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
val runConfig = runConfigHelper.buildAmRunConfig(tunnelConfig)
|
|
||||||
backend.setState(runtimeTunnel, Tunnel.State.UP, runConfig)
|
|
||||||
} catch (_: TimeoutCancellationException) {
|
|
||||||
Timber.e("Startup timed out for ${tunnelConfig.name} (likely DNS hang)")
|
|
||||||
throw DnsFailure()
|
|
||||||
} catch (e: BackendException) {
|
|
||||||
throw e.toBackendCoreException()
|
|
||||||
} catch (_: IllegalArgumentException) {
|
|
||||||
throw InvalidConfig()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e, "Error while setting tunnel state")
|
|
||||||
throw UnknownError()
|
|
||||||
}
|
|
||||||
|
|
||||||
awaitClose {
|
|
||||||
try {
|
|
||||||
backend.setState(runtimeTunnel, Tunnel.State.DOWN, null)
|
|
||||||
} catch (e: BackendException) {
|
|
||||||
// Errors emitted by caller
|
|
||||||
} finally {
|
|
||||||
consumerJob.cancel()
|
|
||||||
stateChannel.close()
|
|
||||||
runtimeTunnels.remove(tunnelConfig.id)
|
|
||||||
trySend(TunnelStatus.Down)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setBackendMode(backendMode: BackendMode) {
|
|
||||||
Timber.d("Setting backend mode: $backendMode")
|
|
||||||
try {
|
|
||||||
backend.backendMode = backendMode.asAmBackendMode()
|
|
||||||
} catch (e: BackendException) {
|
|
||||||
throw e.toBackendCoreException()
|
|
||||||
} catch (_: 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, Tunnel.State.DOWN, null)
|
|
||||||
} catch (e: BackendException) {
|
|
||||||
Timber.e(e, "Force stop failed for $tunnelId")
|
|
||||||
} finally {
|
|
||||||
runtimeTunnels.remove(tunnelId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-114
@@ -1,114 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.tunnel.handler
|
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.combine
|
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.flow.stateIn
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.plus
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
class DynamicDnsHandler(
|
|
||||||
private val activeTunnels: StateFlow<Map<Int, TunnelState>>,
|
|
||||||
private val tunnelsRepository: TunnelRepository,
|
|
||||||
private val settingsRepository: GeneralSettingRepository,
|
|
||||||
private val localMessageEvents: MutableSharedFlow<Pair<String?, BackendMessage>>,
|
|
||||||
private val handleDnsReresolve: (TunnelConfig) -> Boolean,
|
|
||||||
private val applicationScope: CoroutineScope,
|
|
||||||
private val ioDispatcher: CoroutineDispatcher,
|
|
||||||
) {
|
|
||||||
private val mutex = Mutex()
|
|
||||||
private val jobs = ConcurrentHashMap<Int, Job>()
|
|
||||||
|
|
||||||
init {
|
|
||||||
applicationScope.launch(ioDispatcher) {
|
|
||||||
combine(activeTunnels, settingsRepository.flow.filterNotNull()) { active, settings ->
|
|
||||||
active to settings
|
|
||||||
}
|
|
||||||
.collect { (activeTuns, settings) ->
|
|
||||||
mutex.withLock {
|
|
||||||
val activeIds =
|
|
||||||
activeTuns.keys
|
|
||||||
.filter { id ->
|
|
||||||
val config =
|
|
||||||
tunnelsRepository.getById(id) ?: return@filter false
|
|
||||||
config.restartOnPingFailure &&
|
|
||||||
settings.appMode != AppMode.KERNEL
|
|
||||||
}
|
|
||||||
.toSet()
|
|
||||||
|
|
||||||
(jobs.keys - activeIds).forEach { id ->
|
|
||||||
Timber.d("Shutting down Dynamic DNS monitoring job for tunnelId: $id")
|
|
||||||
jobs.remove(id)?.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
activeIds.forEach { id ->
|
|
||||||
if (jobs.containsKey(id)) return@forEach
|
|
||||||
val config = tunnelsRepository.getById(id) ?: return@forEach
|
|
||||||
val tunStateFlow =
|
|
||||||
activeTunnels
|
|
||||||
.map { it[id] }
|
|
||||||
.stateIn(applicationScope + ioDispatcher)
|
|
||||||
Timber.d("Starting Dynamic DNS monitoring job for tunnelId: $id")
|
|
||||||
jobs[id] =
|
|
||||||
applicationScope.launch(ioDispatcher) {
|
|
||||||
monitorDynamicDns(config, tunStateFlow)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun monitorDynamicDns(
|
|
||||||
config: TunnelConfig,
|
|
||||||
tunStateFlow: StateFlow<TunnelState?>,
|
|
||||||
) {
|
|
||||||
var backoff = BASE_BACKOFF
|
|
||||||
while (true) {
|
|
||||||
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(config)
|
|
||||||
if (updated) {
|
|
||||||
localMessageEvents.emit(config.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 ${config.name}") }
|
|
||||||
|
|
||||||
delay(backoff)
|
|
||||||
backoff = (backoff * 1.5).toLong().coerceAtMost(MAX_BACKOFF_TIME)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val BASE_BACKOFF = 30_000L
|
|
||||||
const val MAX_BACKOFF_TIME = 300_000L
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-47
@@ -1,47 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.tunnel.handler
|
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.supervisorScope
|
|
||||||
|
|
||||||
class TunnelActiveStatePersister(
|
|
||||||
private val activeTunnels: StateFlow<Map<Int, TunnelState>>,
|
|
||||||
private val tunnelsRepository: TunnelRepository,
|
|
||||||
applicationScope: CoroutineScope,
|
|
||||||
ioDispatcher: CoroutineDispatcher,
|
|
||||||
) {
|
|
||||||
private var previousActiveIds: Set<Int> = emptySet()
|
|
||||||
|
|
||||||
init {
|
|
||||||
applicationScope.launch(ioDispatcher) {
|
|
||||||
activeTunnels.collect { currentActive ->
|
|
||||||
val currentActiveIds = currentActive.keys
|
|
||||||
if (currentActiveIds == previousActiveIds) return@collect
|
|
||||||
|
|
||||||
val tunnels = tunnelsRepository.userTunnelsFlow.firstOrNull() ?: return@collect
|
|
||||||
val tunnelsById = tunnels.associateBy { it.id }
|
|
||||||
|
|
||||||
val relevantIds = previousActiveIds + currentActiveIds
|
|
||||||
|
|
||||||
supervisorScope {
|
|
||||||
relevantIds.forEach { id ->
|
|
||||||
launch {
|
|
||||||
val config = tunnelsById[id] ?: return@launch
|
|
||||||
val wasActive = previousActiveIds.contains(id)
|
|
||||||
val isActive = currentActiveIds.contains(id)
|
|
||||||
if (wasActive != isActive) {
|
|
||||||
tunnelsRepository.save(config.copy(isActive = isActive))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
previousActiveIds = currentActiveIds.toSet()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-390
@@ -1,390 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.tunnel.handler
|
|
||||||
|
|
||||||
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.FailureReason
|
|
||||||
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 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.ConcurrentMap
|
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.FlowPreview
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.coroutineScope
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.ensureActive
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
import kotlinx.coroutines.flow.combine
|
|
||||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
|
||||||
import kotlinx.coroutines.flow.filter
|
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.flow.mapNotNull
|
|
||||||
import kotlinx.coroutines.flow.stateIn
|
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import kotlinx.coroutines.isActive
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.plus
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import kotlinx.coroutines.withTimeout
|
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
class TunnelMonitorHandler(
|
|
||||||
private val activeTunnels: StateFlow<Map<Int, TunnelState>>,
|
|
||||||
private val tunnelsRepository: TunnelRepository,
|
|
||||||
private val settingsRepository: GeneralSettingRepository,
|
|
||||||
private val monitoringSettingsRepository: MonitoringSettingsRepository,
|
|
||||||
private val networkMonitor: NetworkMonitor,
|
|
||||||
private val networkUtils: NetworkUtils,
|
|
||||||
private val logReader: LogReader,
|
|
||||||
private val powerManager: PowerManager,
|
|
||||||
private val getStatistics: (Int) -> TunnelStatistics?,
|
|
||||||
private val updateTunnelStatus:
|
|
||||||
suspend (
|
|
||||||
Int, TunnelStatus?, TunnelStatistics?, Map<String, PingState>?, LogHealthState?,
|
|
||||||
) -> Unit,
|
|
||||||
private val applicationScope: CoroutineScope,
|
|
||||||
private val ioDispatcher: CoroutineDispatcher,
|
|
||||||
) {
|
|
||||||
private val mutex = Mutex()
|
|
||||||
private val jobs = ConcurrentHashMap<Int, Job>()
|
|
||||||
|
|
||||||
init {
|
|
||||||
applicationScope.launch(ioDispatcher) {
|
|
||||||
activeTunnels.collect { activeTuns ->
|
|
||||||
mutex.withLock {
|
|
||||||
val activeIds = activeTuns.keys.toSet()
|
|
||||||
(jobs.keys - activeIds).forEach { id ->
|
|
||||||
Timber.d("Shutting down tunnel monitoring job for tunnelId: $id")
|
|
||||||
jobs.remove(id)?.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
val tunnels = tunnelsRepository.flow.firstOrNull() ?: return@collect
|
|
||||||
val tunnelsById = tunnels.associateBy { it.id }
|
|
||||||
|
|
||||||
activeIds.forEach { id ->
|
|
||||||
if (jobs.containsKey(id)) return@forEach
|
|
||||||
val config = tunnelsById[id] ?: return@forEach
|
|
||||||
val settings = settingsRepository.flow.filterNotNull().first()
|
|
||||||
val tunStateFlow =
|
|
||||||
activeTunnels.map { it[id] }.stateIn(applicationScope + ioDispatcher)
|
|
||||||
jobs[id] =
|
|
||||||
applicationScope.launch(ioDispatcher) {
|
|
||||||
Timber.d("Starting tunnel monitoring job for tunnelId: $id")
|
|
||||||
startMonitoring(
|
|
||||||
config = config,
|
|
||||||
withLogs = settings.appMode != AppMode.KERNEL,
|
|
||||||
tunStateFlow = tunStateFlow,
|
|
||||||
getStatistics = { tunnelId -> getStatistics(tunnelId) },
|
|
||||||
updateTunnelStatus = { tid, _, stats, pings, logHealth ->
|
|
||||||
updateTunnelStatus(tid, null, stats, pings, logHealth)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(FlowPreview::class)
|
|
||||||
private suspend fun startMonitoring(
|
|
||||||
config: TunnelConfig,
|
|
||||||
withLogs: Boolean,
|
|
||||||
tunStateFlow: StateFlow<TunnelState?>,
|
|
||||||
getStatistics: suspend (Int) -> TunnelStatistics?,
|
|
||||||
updateTunnelStatus:
|
|
||||||
suspend (
|
|
||||||
Int, TunnelStatus?, TunnelStatistics?, Map<String, PingState>?, LogHealthState?,
|
|
||||||
) -> Unit,
|
|
||||||
) = coroutineScope {
|
|
||||||
launch { startPingMonitor(config, tunStateFlow, updateTunnelStatus) }
|
|
||||||
launch { startWgStatsPoll(config.id, 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 is TunnelStatus.Up }.first()
|
|
||||||
|
|
||||||
// small delay to make sure tunnel is fully up before we actively monitor
|
|
||||||
delay(PING_MONITOR_START_DELAY)
|
|
||||||
|
|
||||||
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
|
|
||||||
const val PING_MONITOR_START_DELAY = 5_000L
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-36
@@ -1,36 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.tunnel.handler
|
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
class TunnelServiceHandler(
|
|
||||||
private val activeTunnels: StateFlow<Map<Int, TunnelState>>,
|
|
||||||
private val settingsRepository: GeneralSettingRepository,
|
|
||||||
private val serviceManager: ServiceManager,
|
|
||||||
applicationScope: CoroutineScope,
|
|
||||||
ioDispatcher: CoroutineDispatcher,
|
|
||||||
) {
|
|
||||||
init {
|
|
||||||
applicationScope.launch(ioDispatcher) {
|
|
||||||
activeTunnels.collect { activeTuns ->
|
|
||||||
if (activeTuns.isEmpty()) {
|
|
||||||
Timber.d("Stopping tunnel service, no tunnels active.")
|
|
||||||
serviceManager.stopTunnelService()
|
|
||||||
} else if (serviceManager.tunnelService.value == null) {
|
|
||||||
val settings = settingsRepository.flow.firstOrNull() ?: GeneralSettings()
|
|
||||||
Timber.d("Starting tunnel foreground service for active tunnel.")
|
|
||||||
serviceManager.startTunnelService(settings.appMode)
|
|
||||||
}
|
|
||||||
serviceManager.updateTunnelTile()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,8 +6,9 @@ import androidx.work.ExistingPeriodicWorkPolicy
|
|||||||
import androidx.work.PeriodicWorkRequestBuilder
|
import androidx.work.PeriodicWorkRequestBuilder
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.ServiceManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.autotunnel.AutoTunnelStateHolder
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@@ -16,6 +17,7 @@ class ServiceWorker(
|
|||||||
params: WorkerParameters,
|
params: WorkerParameters,
|
||||||
private val serviceManager: ServiceManager,
|
private val serviceManager: ServiceManager,
|
||||||
private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
|
private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
|
||||||
|
private val autoTunnelStateHolder: AutoTunnelStateHolder,
|
||||||
) : CoroutineWorker(context, params) {
|
) : CoroutineWorker(context, params) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -42,14 +44,18 @@ class ServiceWorker(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
Timber.i("Service worker started")
|
Timber.i("AutoTunnel reconciliation worker running")
|
||||||
with(autoTunnelSettingsRepository.getAutoTunnelSettings()) {
|
|
||||||
Timber.i("Checking to see if auto-tunnel has been killed by system")
|
val settings = autoTunnelSettingsRepository.getAutoTunnelSettings()
|
||||||
if (isAutoTunnelEnabled && serviceManager.autoTunnelService.value == null) {
|
|
||||||
Timber.i("Service has been killed by system, restoring.")
|
if (!settings.isAutoTunnelEnabled) {
|
||||||
serviceManager.startAutoTunnelService()
|
|
||||||
}
|
|
||||||
return Result.success()
|
return Result.success()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (autoTunnelStateHolder.active.value) return Result.success()
|
||||||
|
|
||||||
|
serviceManager.startAutoTunnelService()
|
||||||
|
|
||||||
|
return Result.success()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,27 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.data
|
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.room.migration.AutoMigrationSpec
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
import com.zaneschepke.wireguardautotunnel.data.dao.*
|
import com.zaneschepke.wireguardautotunnel.data.dao.AutoTunnelSettingsDao
|
||||||
import com.zaneschepke.wireguardautotunnel.data.entity.*
|
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(
|
@Database(
|
||||||
entities =
|
entities =
|
||||||
@@ -17,7 +34,7 @@ import com.zaneschepke.wireguardautotunnel.data.entity.*
|
|||||||
DnsSettings::class,
|
DnsSettings::class,
|
||||||
LockdownSettings::class,
|
LockdownSettings::class,
|
||||||
],
|
],
|
||||||
version = 29,
|
version = 31,
|
||||||
autoMigrations =
|
autoMigrations =
|
||||||
[
|
[
|
||||||
AutoMigration(from = 1, to = 2),
|
AutoMigration(from = 1, to = 2),
|
||||||
@@ -45,6 +62,8 @@ import com.zaneschepke.wireguardautotunnel.data.entity.*
|
|||||||
AutoMigration(from = 24, to = 25),
|
AutoMigration(from = 24, to = 25),
|
||||||
AutoMigration(from = 26, to = 27, spec = GlobalsMigration::class),
|
AutoMigration(from = 26, to = 27, spec = GlobalsMigration::class),
|
||||||
AutoMigration(from = 27, to = 28, spec = DonationMigration::class),
|
AutoMigration(from = 27, to = 28, spec = DonationMigration::class),
|
||||||
|
AutoMigration(from = 29, to = 30, spec = SingleConfigMigration::class),
|
||||||
|
AutoMigration(from = 30, to = 31),
|
||||||
],
|
],
|
||||||
exportSchema = true,
|
exportSchema = true,
|
||||||
)
|
)
|
||||||
@@ -129,3 +148,60 @@ class GlobalsMigration : AutoMigrationSpec
|
|||||||
|
|
||||||
@DeleteColumn(tableName = "general_settings", columnName = "custom_split_packages")
|
@DeleteColumn(tableName = "general_settings", columnName = "custom_split_packages")
|
||||||
class DonationMigration : AutoMigrationSpec
|
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()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.data
|
package com.zaneschepke.wireguardautotunnel.data
|
||||||
|
|
||||||
import androidx.room.TypeConverter
|
import androidx.room.TypeConverter
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
|
import com.zaneschepke.wireguardautotunnel.domain.enums.DnsProtocol
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.DnsProtocol
|
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.WifiDetectionMethod
|
import com.zaneschepke.wireguardautotunnel.domain.enums.WifiDetectionMethod
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
class DatabaseConverters {
|
class DatabaseConverters {
|
||||||
@@ -57,9 +57,9 @@ class DatabaseConverters {
|
|||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun toStatus(value: Int): WifiDetectionMethod = WifiDetectionMethod.fromValue(value)
|
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)
|
@TypeConverter fun toDnsProtocol(value: Int): DnsProtocol = DnsProtocol.fromValue(value)
|
||||||
|
|
||||||
|
|||||||
+3
@@ -18,4 +18,7 @@ interface AutoTunnelSettingsDao {
|
|||||||
|
|
||||||
@Query("UPDATE auto_tunnel_settings SET is_tunnel_enabled = :enabled")
|
@Query("UPDATE auto_tunnel_settings SET is_tunnel_enabled = :enabled")
|
||||||
suspend fun updateAutoTunnelEnabled(enabled: Boolean)
|
suspend fun updateAutoTunnelEnabled(enabled: Boolean)
|
||||||
|
|
||||||
|
@Query("UPDATE auto_tunnel_settings SET disable_on_captive_portal = :enabled")
|
||||||
|
suspend fun updateDisableOnCaptivePortal(enabled: Boolean)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,4 +13,7 @@ interface DnsSettingsDao {
|
|||||||
@Upsert suspend fun upsert(dnsSettings: DnsSettings)
|
@Upsert suspend fun upsert(dnsSettings: DnsSettings)
|
||||||
|
|
||||||
@Query("SELECT * FROM dns_settings LIMIT 1") fun getDnsSettingsFlow(): Flow<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)
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-3
@@ -4,7 +4,7 @@ import androidx.room.Dao
|
|||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import androidx.room.Upsert
|
import androidx.room.Upsert
|
||||||
import com.zaneschepke.wireguardautotunnel.data.entity.GeneralSettings
|
import com.zaneschepke.wireguardautotunnel.data.entity.GeneralSettings
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
|
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
@@ -26,6 +26,12 @@ interface GeneralSettingsDao {
|
|||||||
@Query("UPDATE general_settings SET is_pin_lock_enabled = :enabled WHERE id = 1")
|
@Query("UPDATE general_settings SET is_pin_lock_enabled = :enabled WHERE id = 1")
|
||||||
suspend fun updatePinLockEnabled(enabled: Boolean)
|
suspend fun updatePinLockEnabled(enabled: Boolean)
|
||||||
|
|
||||||
@Query("UPDATE general_settings SET app_mode = :appMode WHERE id = 1")
|
@Query("UPDATE general_settings SET app_mode = :tunnelMode WHERE id = 1")
|
||||||
suspend fun updateAppMode(appMode: AppMode)
|
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")
|
@Query("SELECT * FROM monitoring_settings LIMIT 1")
|
||||||
fun getMonitoringSettingsFlow(): Flow<MonitoringSettings?>
|
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
|
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 com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
@@ -11,17 +16,17 @@ interface TunnelConfigDao {
|
|||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun saveAll(t: List<TunnelConfig>)
|
@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")
|
@Query("UPDATE tunnel_config SET dynamic_dns = :value WHERE id = :id")
|
||||||
suspend fun resetActiveTunnels()
|
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")
|
@Query("SELECT * FROM tunnel_config WHERE name=:name")
|
||||||
suspend fun getByName(name: String): TunnelConfig?
|
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>
|
@Query("SELECT * FROM tunnel_config") suspend fun getAll(): List<TunnelConfig>
|
||||||
|
|
||||||
@Delete suspend fun delete(t: TunnelConfig)
|
@Delete suspend fun delete(t: TunnelConfig)
|
||||||
@@ -50,30 +55,15 @@ interface TunnelConfigDao {
|
|||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM tunnel_config
|
SELECT *
|
||||||
WHERE name != '${TunnelConfig.GLOBAL_CONFIG_NAME}'
|
FROM tunnel_config
|
||||||
ORDER BY
|
WHERE name != '${TunnelConfig.GLOBAL_CONFIG_NAME}'
|
||||||
CASE WHEN is_primary_tunnel = 1 THEN 0 ELSE 1 END,
|
ORDER BY is_primary_tunnel DESC, position ASC
|
||||||
position ASC
|
LIMIT 1
|
||||||
LIMIT 1
|
"""
|
||||||
"""
|
|
||||||
)
|
)
|
||||||
suspend fun getDefaultTunnel(): TunnelConfig?
|
suspend fun getDefaultTunnel(): TunnelConfig?
|
||||||
|
|
||||||
@Query(
|
|
||||||
"""
|
|
||||||
SELECT * FROM tunnel_config
|
|
||||||
WHERE name != '${TunnelConfig.GLOBAL_CONFIG_NAME}'
|
|
||||||
ORDER BY
|
|
||||||
CASE WHEN is_Active = 1 THEN 0
|
|
||||||
WHEN is_primary_tunnel = 1 THEN 1
|
|
||||||
ELSE 2 END,
|
|
||||||
position ASC
|
|
||||||
LIMIT 1
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
suspend fun getStartTunnel(): TunnelConfig?
|
|
||||||
|
|
||||||
@Query("SELECT * FROM tunnel_config ORDER BY position")
|
@Query("SELECT * FROM tunnel_config ORDER BY position")
|
||||||
fun getAllFlow(): Flow<List<TunnelConfig>>
|
fun getAllFlow(): Flow<List<TunnelConfig>>
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -3,7 +3,7 @@ package com.zaneschepke.wireguardautotunnel.data.entity
|
|||||||
import androidx.room.ColumnInfo
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.WifiDetectionMethod
|
import com.zaneschepke.wireguardautotunnel.domain.enums.WifiDetectionMethod
|
||||||
|
|
||||||
@Entity(tableName = "auto_tunnel_settings")
|
@Entity(tableName = "auto_tunnel_settings")
|
||||||
data class AutoTunnelSettings(
|
data class AutoTunnelSettings(
|
||||||
@@ -22,11 +22,11 @@ data class AutoTunnelSettings(
|
|||||||
val isWildcardsEnabled: Boolean = false,
|
val isWildcardsEnabled: Boolean = false,
|
||||||
@ColumnInfo(name = "is_stop_on_no_internet_enabled", defaultValue = "0")
|
@ColumnInfo(name = "is_stop_on_no_internet_enabled", defaultValue = "0")
|
||||||
val isStopOnNoInternetEnabled: Boolean = false,
|
val isStopOnNoInternetEnabled: Boolean = false,
|
||||||
@ColumnInfo(name = "debounce_delay_seconds", defaultValue = "3")
|
|
||||||
val debounceDelaySeconds: Int = 3,
|
|
||||||
@ColumnInfo(name = "is_tunnel_on_unsecure_enabled", defaultValue = "0")
|
@ColumnInfo(name = "is_tunnel_on_unsecure_enabled", defaultValue = "0")
|
||||||
val isTunnelOnUnsecureEnabled: Boolean = false,
|
val isTunnelOnUnsecureEnabled: Boolean = false,
|
||||||
@ColumnInfo(name = "wifi_detection_method", defaultValue = "0")
|
@ColumnInfo(name = "wifi_detection_method", defaultValue = "0")
|
||||||
val wifiDetectionMethod: WifiDetectionMethod = WifiDetectionMethod.fromValue(0),
|
val wifiDetectionMethod: WifiDetectionMethod = WifiDetectionMethod.fromValue(0),
|
||||||
@ColumnInfo(name = "start_on_boot", defaultValue = "0") val startOnBoot: Boolean = false,
|
@ColumnInfo(name = "start_on_boot", defaultValue = "0") val startOnBoot: Boolean = false,
|
||||||
|
@ColumnInfo(name = "disable_on_captive_portal", defaultValue = "1")
|
||||||
|
val disableTunnelOnCaptivePortal: Boolean = true,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package com.zaneschepke.wireguardautotunnel.data.entity
|
|||||||
import androidx.room.ColumnInfo
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.DnsProtocol
|
import com.zaneschepke.wireguardautotunnel.domain.enums.DnsProtocol
|
||||||
|
|
||||||
@Entity(tableName = "dns_settings")
|
@Entity(tableName = "dns_settings")
|
||||||
data class DnsSettings(
|
data class DnsSettings(
|
||||||
|
|||||||
+9
-2
@@ -3,7 +3,7 @@ package com.zaneschepke.wireguardautotunnel.data.entity
|
|||||||
import androidx.room.ColumnInfo
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
|
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||||
|
|
||||||
@Entity(tableName = "general_settings")
|
@Entity(tableName = "general_settings")
|
||||||
data class GeneralSettings(
|
data class GeneralSettings(
|
||||||
@@ -16,7 +16,8 @@ data class GeneralSettings(
|
|||||||
val isMultiTunnelEnabled: Boolean = false,
|
val isMultiTunnelEnabled: Boolean = false,
|
||||||
@ColumnInfo(name = "global_split_tunnel_enabled", defaultValue = "0")
|
@ColumnInfo(name = "global_split_tunnel_enabled", defaultValue = "0")
|
||||||
val isGlobalSplitTunnelEnabled: Boolean = false,
|
val isGlobalSplitTunnelEnabled: Boolean = false,
|
||||||
@ColumnInfo(name = "app_mode", defaultValue = "0") val appMode: AppMode = AppMode.fromValue(0),
|
@ColumnInfo(name = "app_mode", defaultValue = "0")
|
||||||
|
val tunnelMode: TunnelMode = TunnelMode.fromValue(0),
|
||||||
@ColumnInfo(name = "theme", defaultValue = "AUTOMATIC") val theme: String = "AUTOMATIC",
|
@ColumnInfo(name = "theme", defaultValue = "AUTOMATIC") val theme: String = "AUTOMATIC",
|
||||||
@ColumnInfo(name = "locale") val locale: String? = null,
|
@ColumnInfo(name = "locale") val locale: String? = null,
|
||||||
@ColumnInfo(name = "remote_key") val remoteKey: 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")
|
@ColumnInfo(name = "is_always_on_vpn_enabled", defaultValue = "0")
|
||||||
val isAlwaysOnVpnEnabled: Boolean = false,
|
val isAlwaysOnVpnEnabled: Boolean = false,
|
||||||
@ColumnInfo(name = "already_donated", defaultValue = "0") val alreadyDonated: 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")
|
@Entity(tableName = "monitoring_settings")
|
||||||
data class MonitoringSettings(
|
data class MonitoringSettings(
|
||||||
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
@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")
|
@ColumnInfo(name = "is_local_logs_enabled", defaultValue = "0")
|
||||||
val isLocalLogsEnabled: Boolean = false,
|
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(
|
data class TunnelConfig(
|
||||||
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
||||||
@ColumnInfo(name = "name") val name: String,
|
@ColumnInfo(name = "name") val name: String,
|
||||||
@ColumnInfo(name = "wg_quick") val wgQuick: String,
|
|
||||||
@ColumnInfo(name = "tunnel_networks", defaultValue = "")
|
@ColumnInfo(name = "tunnel_networks", defaultValue = "")
|
||||||
val tunnelNetworks: Set<String> = setOf(),
|
val tunnelNetworks: Set<String> = setOf(),
|
||||||
@ColumnInfo(name = "is_mobile_data_tunnel", defaultValue = "false")
|
@ColumnInfo(name = "is_mobile_data_tunnel", defaultValue = "false")
|
||||||
val isMobileDataTunnel: Boolean = false,
|
val isMobileDataTunnel: Boolean = false,
|
||||||
@ColumnInfo(name = "is_primary_tunnel", defaultValue = "false")
|
@ColumnInfo(name = "is_primary_tunnel", defaultValue = "false")
|
||||||
val isPrimaryTunnel: Boolean = false,
|
val isPrimaryTunnel: Boolean = false,
|
||||||
@ColumnInfo(name = "am_quick", defaultValue = "") val amQuick: String = "",
|
@ColumnInfo(name = "quick_config", defaultValue = "") val quickConfig: String = "",
|
||||||
@ColumnInfo(name = "is_Active", defaultValue = "false") val isActive: Boolean = false,
|
@ColumnInfo(name = "dynamic_dns", defaultValue = "false")
|
||||||
@ColumnInfo(name = "restart_on_ping_failure", defaultValue = "false")
|
val dynamicDnsEnabled: Boolean = false,
|
||||||
val restartOnPingFailure: Boolean = false,
|
|
||||||
@ColumnInfo(name = "ping_target", defaultValue = "null") var pingTarget: String? = null,
|
|
||||||
@ColumnInfo(name = "is_ethernet_tunnel", defaultValue = "false")
|
@ColumnInfo(name = "is_ethernet_tunnel", defaultValue = "false")
|
||||||
val isEthernetTunnel: Boolean = false,
|
val isEthernetTunnel: Boolean = false,
|
||||||
@ColumnInfo(name = "is_ipv4_preferred", defaultValue = "true")
|
@ColumnInfo(name = "prefer_ipv6", defaultValue = "false") val isIpv6Preferred: Boolean = false,
|
||||||
val isIpv4Preferred: Boolean = true,
|
|
||||||
@ColumnInfo(name = "position", defaultValue = "0") val position: Int = 0,
|
@ColumnInfo(name = "position", defaultValue = "0") val position: Int = 0,
|
||||||
@ColumnInfo(name = "auto_tunnel_apps", defaultValue = "[]")
|
@ColumnInfo(name = "auto_tunnel_apps", defaultValue = "[]")
|
||||||
val autoTunnelApps: Set<String> = emptySet(),
|
val autoTunnelApps: Set<String> = emptySet(),
|
||||||
@ColumnInfo(name = "is_metered", defaultValue = "false") val isMetered: Boolean = false,
|
@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 {
|
companion object {
|
||||||
const val GLOBAL_CONFIG_NAME = "4675ab06-903a-438b-8485-6ea4187a9512"
|
const val GLOBAL_CONFIG_NAME = "4675ab06-903a-438b-8485-6ea4187a9512"
|
||||||
|
|||||||
-2
@@ -13,7 +13,6 @@ fun Entity.toDomain(): Domain =
|
|||||||
isTunnelOnWifiEnabled = isTunnelOnWifiEnabled,
|
isTunnelOnWifiEnabled = isTunnelOnWifiEnabled,
|
||||||
isWildcardsEnabled = isWildcardsEnabled,
|
isWildcardsEnabled = isWildcardsEnabled,
|
||||||
isStopOnNoInternetEnabled = isStopOnNoInternetEnabled,
|
isStopOnNoInternetEnabled = isStopOnNoInternetEnabled,
|
||||||
debounceDelaySeconds = debounceDelaySeconds,
|
|
||||||
isTunnelOnUnsecureEnabled = isTunnelOnUnsecureEnabled,
|
isTunnelOnUnsecureEnabled = isTunnelOnUnsecureEnabled,
|
||||||
wifiDetectionMethod = wifiDetectionMethod,
|
wifiDetectionMethod = wifiDetectionMethod,
|
||||||
startOnBoot = startOnBoot,
|
startOnBoot = startOnBoot,
|
||||||
@@ -29,7 +28,6 @@ fun Domain.toEntity(): Entity =
|
|||||||
isTunnelOnWifiEnabled = isTunnelOnWifiEnabled,
|
isTunnelOnWifiEnabled = isTunnelOnWifiEnabled,
|
||||||
isWildcardsEnabled = isWildcardsEnabled,
|
isWildcardsEnabled = isWildcardsEnabled,
|
||||||
isStopOnNoInternetEnabled = isStopOnNoInternetEnabled,
|
isStopOnNoInternetEnabled = isStopOnNoInternetEnabled,
|
||||||
debounceDelaySeconds = debounceDelaySeconds,
|
|
||||||
isTunnelOnUnsecureEnabled = isTunnelOnUnsecureEnabled,
|
isTunnelOnUnsecureEnabled = isTunnelOnUnsecureEnabled,
|
||||||
wifiDetectionMethod = wifiDetectionMethod,
|
wifiDetectionMethod = wifiDetectionMethod,
|
||||||
startOnBoot = startOnBoot,
|
startOnBoot = startOnBoot,
|
||||||
|
|||||||
+4
-12
@@ -6,23 +6,15 @@ import com.zaneschepke.wireguardautotunnel.domain.model.MonitoringSettings as Do
|
|||||||
fun Entity.toDomain(): Domain =
|
fun Entity.toDomain(): Domain =
|
||||||
Domain(
|
Domain(
|
||||||
id = id,
|
id = id,
|
||||||
isPingEnabled = isPingEnabled,
|
tunnelStatisticsEnabled = tunnelStatisticsEnabled,
|
||||||
isPingMonitoringEnabled = isPingMonitoringEnabled,
|
tunnelStatisticsPollInterval = tunnelStatisticsPollInterval,
|
||||||
tunnelPingIntervalSeconds = tunnelPingIntervalSeconds,
|
|
||||||
tunnelPingAttempts = tunnelPingAttempts,
|
|
||||||
tunnelPingTimeoutSeconds = tunnelPingTimeoutSeconds,
|
|
||||||
showDetailedPingStats = showDetailedPingStats,
|
|
||||||
isLocalLogsEnabled = isLocalLogsEnabled,
|
isLocalLogsEnabled = isLocalLogsEnabled,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun Domain.toEntity(): Entity =
|
fun Domain.toEntity(): Entity =
|
||||||
Entity(
|
Entity(
|
||||||
id = id,
|
id = id,
|
||||||
isPingEnabled = isPingEnabled,
|
tunnelStatisticsEnabled = tunnelStatisticsEnabled,
|
||||||
isPingMonitoringEnabled = isPingMonitoringEnabled,
|
tunnelStatisticsPollInterval = tunnelStatisticsPollInterval,
|
||||||
tunnelPingIntervalSeconds = tunnelPingIntervalSeconds,
|
|
||||||
tunnelPingAttempts = tunnelPingAttempts,
|
|
||||||
tunnelPingTimeoutSeconds = tunnelPingTimeoutSeconds,
|
|
||||||
showDetailedPingStats = showDetailedPingStats,
|
|
||||||
isLocalLogsEnabled = isLocalLogsEnabled,
|
isLocalLogsEnabled = isLocalLogsEnabled,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ fun Entity.toDomain(): Domain =
|
|||||||
isRestoreOnBootEnabled = isRestoreOnBootEnabled,
|
isRestoreOnBootEnabled = isRestoreOnBootEnabled,
|
||||||
isMultiTunnelEnabled = isMultiTunnelEnabled,
|
isMultiTunnelEnabled = isMultiTunnelEnabled,
|
||||||
isGlobalSplitTunnelEnabled = isGlobalSplitTunnelEnabled,
|
isGlobalSplitTunnelEnabled = isGlobalSplitTunnelEnabled,
|
||||||
appMode = appMode,
|
tunnelMode = tunnelMode,
|
||||||
theme = Theme.valueOf(theme.uppercase()),
|
theme = Theme.valueOf(theme.uppercase()),
|
||||||
locale = locale,
|
locale = locale,
|
||||||
remoteKey = remoteKey,
|
remoteKey = remoteKey,
|
||||||
@@ -19,6 +19,9 @@ fun Entity.toDomain(): Domain =
|
|||||||
isPinLockEnabled = isPinLockEnabled,
|
isPinLockEnabled = isPinLockEnabled,
|
||||||
isAlwaysOnVpnEnabled = isAlwaysOnVpnEnabled,
|
isAlwaysOnVpnEnabled = isAlwaysOnVpnEnabled,
|
||||||
alreadyDonated = alreadyDonated,
|
alreadyDonated = alreadyDonated,
|
||||||
|
screenRecordingSecurityEnabled = screenRecordingSecurityEnabled,
|
||||||
|
isGlobalAmneziaEnabled = isGlobalAmneziaEnabled,
|
||||||
|
tunnelScriptingEnabled = tunnelScriptingEnabled,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun Domain.toEntity(): Entity =
|
fun Domain.toEntity(): Entity =
|
||||||
@@ -28,7 +31,7 @@ fun Domain.toEntity(): Entity =
|
|||||||
isRestoreOnBootEnabled = isRestoreOnBootEnabled,
|
isRestoreOnBootEnabled = isRestoreOnBootEnabled,
|
||||||
isMultiTunnelEnabled = isMultiTunnelEnabled,
|
isMultiTunnelEnabled = isMultiTunnelEnabled,
|
||||||
isGlobalSplitTunnelEnabled = isGlobalSplitTunnelEnabled,
|
isGlobalSplitTunnelEnabled = isGlobalSplitTunnelEnabled,
|
||||||
appMode = appMode,
|
tunnelMode = tunnelMode,
|
||||||
theme = theme.name,
|
theme = theme.name,
|
||||||
locale = locale,
|
locale = locale,
|
||||||
remoteKey = remoteKey,
|
remoteKey = remoteKey,
|
||||||
@@ -36,4 +39,7 @@ fun Domain.toEntity(): Entity =
|
|||||||
isPinLockEnabled = isPinLockEnabled,
|
isPinLockEnabled = isPinLockEnabled,
|
||||||
isAlwaysOnVpnEnabled = isAlwaysOnVpnEnabled,
|
isAlwaysOnVpnEnabled = isAlwaysOnVpnEnabled,
|
||||||
alreadyDonated = alreadyDonated,
|
alreadyDonated = alreadyDonated,
|
||||||
|
screenRecordingSecurityEnabled = screenRecordingSecurityEnabled,
|
||||||
|
isGlobalAmneziaEnabled = isGlobalAmneziaEnabled,
|
||||||
|
tunnelScriptingEnabled = tunnelScriptingEnabled,
|
||||||
)
|
)
|
||||||
|
|||||||
+10
-12
@@ -7,36 +7,34 @@ fun Entity.toDomain(): Domain =
|
|||||||
Domain(
|
Domain(
|
||||||
id = id,
|
id = id,
|
||||||
name = name,
|
name = name,
|
||||||
wgQuick = wgQuick,
|
|
||||||
tunnelNetworks = tunnelNetworks,
|
tunnelNetworks = tunnelNetworks,
|
||||||
isMobileDataTunnel = isMobileDataTunnel,
|
isMobileDataTunnel = isMobileDataTunnel,
|
||||||
isPrimaryTunnel = isPrimaryTunnel,
|
isPrimaryTunnel = isPrimaryTunnel,
|
||||||
amQuick = amQuick,
|
quickConfig = quickConfig,
|
||||||
isActive = isActive,
|
dynamicDnsEnabled = dynamicDnsEnabled,
|
||||||
restartOnPingFailure = restartOnPingFailure,
|
|
||||||
pingTarget = pingTarget,
|
|
||||||
isEthernetTunnel = isEthernetTunnel,
|
isEthernetTunnel = isEthernetTunnel,
|
||||||
isIpv4Preferred = isIpv4Preferred,
|
isIpv6Preferred = isIpv6Preferred,
|
||||||
position = position,
|
position = position,
|
||||||
autoTunnelApps = autoTunnelApps,
|
autoTunnelApps = autoTunnelApps,
|
||||||
isMetered = isMetered,
|
isMetered = isMetered,
|
||||||
|
ipv4FallbackEnabled = ipv4FallbackEnabled,
|
||||||
|
ipv6RestoreEnabled = ipv6RestoreEnabled,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun Domain.toEntity(): Entity =
|
fun Domain.toEntity(): Entity =
|
||||||
Entity(
|
Entity(
|
||||||
id = id,
|
id = id,
|
||||||
name = name,
|
name = name,
|
||||||
wgQuick = wgQuick,
|
|
||||||
tunnelNetworks = tunnelNetworks,
|
tunnelNetworks = tunnelNetworks,
|
||||||
isMobileDataTunnel = isMobileDataTunnel,
|
isMobileDataTunnel = isMobileDataTunnel,
|
||||||
isPrimaryTunnel = isPrimaryTunnel,
|
isPrimaryTunnel = isPrimaryTunnel,
|
||||||
amQuick = amQuick,
|
quickConfig = quickConfig,
|
||||||
isActive = isActive,
|
dynamicDnsEnabled = dynamicDnsEnabled,
|
||||||
restartOnPingFailure = restartOnPingFailure,
|
|
||||||
pingTarget = pingTarget,
|
|
||||||
isEthernetTunnel = isEthernetTunnel,
|
isEthernetTunnel = isEthernetTunnel,
|
||||||
isIpv4Preferred = isIpv4Preferred,
|
isIpv6Preferred = isIpv6Preferred,
|
||||||
position = position,
|
position = position,
|
||||||
autoTunnelApps = autoTunnelApps,
|
autoTunnelApps = autoTunnelApps,
|
||||||
isMetered = isMetered,
|
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
|
package com.zaneschepke.wireguardautotunnel.data.network
|
||||||
|
|
||||||
import io.ktor.client.*
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.engine.okhttp.*
|
import io.ktor.client.engine.okhttp.OkHttp
|
||||||
import io.ktor.client.plugins.*
|
import io.ktor.client.plugins.HttpTimeout
|
||||||
import io.ktor.client.plugins.contentnegotiation.*
|
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||||
import io.ktor.serialization.kotlinx.json.*
|
import io.ktor.serialization.kotlinx.json.json
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
object KtorClient {
|
object KtorClient {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.data.network
|
package com.zaneschepke.wireguardautotunnel.data.network
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.entity.GitHubRelease
|
import com.zaneschepke.wireguardautotunnel.data.entity.GitHubRelease
|
||||||
import io.ktor.client.*
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.call.*
|
import io.ktor.client.call.body
|
||||||
import io.ktor.client.plugins.*
|
import io.ktor.client.plugins.ClientRequestException
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.get
|
||||||
import io.ktor.http.*
|
import io.ktor.http.HttpStatusCode
|
||||||
|
|
||||||
class KtorGitHubApi(private val client: HttpClient) : GitHubApi {
|
class KtorGitHubApi(private val client: HttpClient) : GitHubApi {
|
||||||
override suspend fun getLatestRelease(owner: String, repo: String): Result<GitHubRelease> {
|
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()
|
client.get("https://api.github.com/repos/$owner/$repo/releases").body()
|
||||||
|
|
||||||
// Find the first release with "nightly" in the tag_name (case-insensitive)
|
// Find the first release with "nightly" in the tag_name (case-insensitive)
|
||||||
val nightlyRelease =
|
val nightlyRelease = releases.firstOrNull { release ->
|
||||||
releases.firstOrNull { release ->
|
release.tagName.contains("nightly", ignoreCase = true)
|
||||||
release.tagName.contains("nightly", ignoreCase = true)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (nightlyRelease != null) {
|
if (nightlyRelease != null) {
|
||||||
Result.success(nightlyRelease)
|
Result.success(nightlyRelease)
|
||||||
|
|||||||
+17
-18
@@ -28,25 +28,24 @@ class InstalledAndroidPackageRepository(
|
|||||||
withContext(ioDispatcher) {
|
withContext(ioDispatcher) {
|
||||||
val packages = context.packageManager.getInstalledPackages(0)
|
val packages = context.packageManager.getInstalledPackages(0)
|
||||||
|
|
||||||
val installedPackages =
|
val installedPackages = packages.mapNotNull { packageInfo ->
|
||||||
packages.mapNotNull { packageInfo ->
|
try {
|
||||||
try {
|
val appInfo =
|
||||||
val appInfo =
|
context.packageManager.getApplicationInfo(packageInfo.packageName, 0)
|
||||||
context.packageManager.getApplicationInfo(packageInfo.packageName, 0)
|
InstalledPackage(
|
||||||
InstalledPackage(
|
name =
|
||||||
name =
|
context.packageManager.getFriendlyAppName(
|
||||||
context.packageManager.getFriendlyAppName(
|
packageInfo.packageName,
|
||||||
packageInfo.packageName,
|
appInfo,
|
||||||
appInfo,
|
),
|
||||||
),
|
packageName = packageInfo.packageName,
|
||||||
packageName = packageInfo.packageName,
|
uId = appInfo.uid,
|
||||||
uId = appInfo.uid,
|
)
|
||||||
)
|
} catch (e: PackageManager.NameNotFoundException) {
|
||||||
} catch (e: PackageManager.NameNotFoundException) {
|
Timber.e(e)
|
||||||
Timber.e(e)
|
null
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
cachedPackages = installedPackages
|
cachedPackages = installedPackages
|
||||||
|
|
||||||
|
|||||||
+4
@@ -26,4 +26,8 @@ class RoomAutoTunnelSettingsRepository(private val autoTunnelSettingsDao: AutoTu
|
|||||||
override suspend fun updateAutoTunnelEnabled(enabled: Boolean) {
|
override suspend fun updateAutoTunnelEnabled(enabled: Boolean) {
|
||||||
autoTunnelSettingsDao.updateAutoTunnelEnabled(enabled)
|
autoTunnelSettingsDao.updateAutoTunnelEnabled(enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun updateDisableOnCaptivePortal(enabled: Boolean) {
|
||||||
|
autoTunnelSettingsDao.updateDisableOnCaptivePortal(enabled)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
@@ -21,4 +21,8 @@ class RoomDnsSettingsRepository(private val dnsSettingsDao: DnsSettingsDao) :
|
|||||||
override suspend fun getDnsSettings(): Domain {
|
override suspend fun getDnsSettings(): Domain {
|
||||||
return (dnsSettingsDao.getDnsSettings() ?: Entity()).toDomain()
|
return (dnsSettingsDao.getDnsSettings() ?: Entity()).toDomain()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun updateGlobalDnsEnabled(enabled: Boolean) {
|
||||||
|
dnsSettingsDao.updateGlobalDnsEnabled(enabled)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
@@ -22,4 +22,12 @@ class RoomMonitoringSettingsRepository(private val monitoringSettingsDao: Monito
|
|||||||
override suspend fun getMonitoringSettings(): Domain {
|
override suspend fun getMonitoringSettings(): Domain {
|
||||||
return (monitoringSettingsDao.getMonitoringSettings() ?: Entity()).toDomain()
|
return (monitoringSettingsDao.getMonitoringSettings() ?: Entity()).toDomain()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun updateStatisticRefresh(statisticRefresh: Int) {
|
||||||
|
monitoringSettingsDao.updateStatisticsInterval(statisticRefresh)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun updateStatisticsEnabled(enabled: Boolean) {
|
||||||
|
monitoringSettingsDao.updateStatisticsEnabled(enabled)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-3
@@ -4,7 +4,7 @@ import com.zaneschepke.wireguardautotunnel.data.dao.GeneralSettingsDao
|
|||||||
import com.zaneschepke.wireguardautotunnel.data.entity.GeneralSettings as Entity
|
import com.zaneschepke.wireguardautotunnel.data.entity.GeneralSettings as Entity
|
||||||
import com.zaneschepke.wireguardautotunnel.data.mapper.toDomain
|
import com.zaneschepke.wireguardautotunnel.data.mapper.toDomain
|
||||||
import com.zaneschepke.wireguardautotunnel.data.mapper.toEntity
|
import com.zaneschepke.wireguardautotunnel.data.mapper.toEntity
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
|
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings as Domain
|
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings as Domain
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
|
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
|
||||||
@@ -34,7 +34,15 @@ class RoomSettingsRepository(private val settingsDao: GeneralSettingsDao) :
|
|||||||
settingsDao.updatePinLockEnabled(enabled)
|
settingsDao.updatePinLockEnabled(enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun updateAppMode(appMode: AppMode) {
|
override suspend fun updateAppMode(tunnelMode: TunnelMode) {
|
||||||
settingsDao.updateAppMode(appMode)
|
settingsDao.updateAppMode(tunnelMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun updateGlobalAmneziaEnabled(enabled: Boolean) {
|
||||||
|
settingsDao.updateGlobalAmneziaEnabled(enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun updateScreenRecordingSecurity(enabled: Boolean) {
|
||||||
|
settingsDao.updateScreenRecordingSecurity(enabled)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-12
@@ -6,6 +6,7 @@ import com.zaneschepke.wireguardautotunnel.data.mapper.toEntity
|
|||||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig as Domain
|
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig as Domain
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
class RoomTunnelRepository(private val tunnelConfigDao: TunnelConfigDao) : TunnelRepository {
|
class RoomTunnelRepository(private val tunnelConfigDao: TunnelConfigDao) : TunnelRepository {
|
||||||
@@ -25,6 +26,14 @@ class RoomTunnelRepository(private val tunnelConfigDao: TunnelConfigDao) : Tunne
|
|||||||
return 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) {
|
override suspend fun save(tunnelConfig: Domain) {
|
||||||
tunnelConfigDao.upsert(tunnelConfig.toEntity())
|
tunnelConfigDao.upsert(tunnelConfig.toEntity())
|
||||||
}
|
}
|
||||||
@@ -38,10 +47,6 @@ class RoomTunnelRepository(private val tunnelConfigDao: TunnelConfigDao) : Tunne
|
|||||||
tunnelConfig?.let { save(it.copy(isPrimaryTunnel = true)) }
|
tunnelConfig?.let { save(it.copy(isPrimaryTunnel = true)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun resetActiveTunnels() {
|
|
||||||
tunnelConfigDao.resetActiveTunnels()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun updateMobileDataTunnel(tunnelConfig: Domain?) {
|
override suspend fun updateMobileDataTunnel(tunnelConfig: Domain?) {
|
||||||
tunnelConfigDao.resetMobileDataTunnel()
|
tunnelConfigDao.resetMobileDataTunnel()
|
||||||
tunnelConfig?.let { save(it.copy(isMobileDataTunnel = true)) }
|
tunnelConfig?.let { save(it.copy(isMobileDataTunnel = true)) }
|
||||||
@@ -60,18 +65,10 @@ class RoomTunnelRepository(private val tunnelConfigDao: TunnelConfigDao) : Tunne
|
|||||||
return tunnelConfigDao.getById(id.toLong())?.toDomain()
|
return tunnelConfigDao.getById(id.toLong())?.toDomain()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getActive(): List<Domain> {
|
|
||||||
return tunnelConfigDao.getActive().map { it.toDomain() }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getDefaultTunnel(): Domain? {
|
override suspend fun getDefaultTunnel(): Domain? {
|
||||||
return tunnelConfigDao.getDefaultTunnel()?.toDomain()
|
return tunnelConfigDao.getDefaultTunnel()?.toDomain()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getStartTunnel(): Domain? {
|
|
||||||
return tunnelConfigDao.getStartTunnel()?.toDomain()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun count(): Int {
|
override suspend fun count(): Int {
|
||||||
return tunnelConfigDao.count().toInt()
|
return tunnelConfigDao.count().toInt()
|
||||||
}
|
}
|
||||||
@@ -95,4 +92,10 @@ class RoomTunnelRepository(private val tunnelConfigDao: TunnelConfigDao) : Tunne
|
|||||||
override suspend fun delete(tunnels: List<Domain>) {
|
override suspend fun delete(tunnels: List<Domain>) {
|
||||||
tunnelConfigDao.delete(tunnels.map { it.toEntity() })
|
tunnelConfigDao.delete(tunnels.map { it.toEntity() })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun ensureGlobalConfigExists() {
|
||||||
|
if (globalTunnelFlow.firstOrNull() == null) {
|
||||||
|
save(Domain.generateDefaultGlobalConfig())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,20 +2,22 @@ package com.zaneschepke.wireguardautotunnel.di
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
|
import android.os.StrictMode
|
||||||
import com.zaneschepke.logcatter.LogReader
|
import com.zaneschepke.logcatter.LogReader
|
||||||
import com.zaneschepke.logcatter.LogcatReader
|
import com.zaneschepke.logcatter.LogcatReader
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationMonitor
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.shortcut.DynamicShortcutManager
|
import com.zaneschepke.wireguardautotunnel.core.shortcut.DynamicShortcutManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager
|
import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.SelectedTunnelsRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.SelectedTunnelsRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.notification.AndroidNotificationService
|
||||||
|
import com.zaneschepke.wireguardautotunnel.notification.NotificationService
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.ServiceManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.autotunnel.AutoTunnelStateHolder
|
||||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||||
import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils
|
import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AutoTunnelViewModel
|
import com.zaneschepke.wireguardautotunnel.viewmodel.AutoTunnelViewModel
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.ConfigViewModel
|
import com.zaneschepke.wireguardautotunnel.viewmodel.ConfigEditViewModel
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.DnsViewModel
|
import com.zaneschepke.wireguardautotunnel.viewmodel.DnsViewModel
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.LicenseViewModel
|
import com.zaneschepke.wireguardautotunnel.viewmodel.LicenseViewModel
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.LockdownViewModel
|
import com.zaneschepke.wireguardautotunnel.viewmodel.LockdownViewModel
|
||||||
@@ -27,57 +29,56 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
|
|||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SplitTunnelViewModel
|
import com.zaneschepke.wireguardautotunnel.viewmodel.SplitTunnelViewModel
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SupportViewModel
|
import com.zaneschepke.wireguardautotunnel.viewmodel.SupportViewModel
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelViewModel
|
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelViewModel
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import org.koin.android.ext.koin.androidContext
|
import org.koin.android.ext.koin.androidContext
|
||||||
import org.koin.core.annotation.KoinExperimentalAPI
|
import org.koin.core.annotation.KoinExperimentalAPI
|
||||||
import org.koin.core.module.dsl.scopedOf
|
|
||||||
import org.koin.core.module.dsl.singleOf
|
import org.koin.core.module.dsl.singleOf
|
||||||
import org.koin.core.module.dsl.viewModel
|
import org.koin.core.module.dsl.viewModel
|
||||||
import org.koin.core.module.dsl.viewModelOf
|
import org.koin.core.module.dsl.viewModelOf
|
||||||
import org.koin.core.qualifier.named
|
import org.koin.core.qualifier.named
|
||||||
import org.koin.dsl.bind
|
import org.koin.dsl.bind
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
import org.koin.viewmodel.scope.viewModelScope
|
|
||||||
|
|
||||||
@OptIn(KoinExperimentalAPI::class)
|
@OptIn(KoinExperimentalAPI::class)
|
||||||
val appModule = module {
|
val appModule = module {
|
||||||
single<CoroutineScope>(named(Scope.APPLICATION)) {
|
single<CoroutineScope>(named(Scope.APPLICATION)) {
|
||||||
CoroutineScope(SupervisorJob() + get<CoroutineDispatcher>(named(Dispatcher.DEFAULT)))
|
CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||||
|
}
|
||||||
|
single<LogReader> {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
val readPolicy = StrictMode.allowThreadDiskReads()
|
||||||
|
val writePolicy = StrictMode.allowThreadDiskWrites()
|
||||||
|
try {
|
||||||
|
val storageDir = androidContext().filesDir.absolutePath
|
||||||
|
LogcatReader.init(storageDir = storageDir)
|
||||||
|
} finally {
|
||||||
|
StrictMode.setThreadPolicy(readPolicy)
|
||||||
|
StrictMode.setThreadPolicy(writePolicy)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val storageDir = androidContext().filesDir.absolutePath
|
||||||
|
LogcatReader.init(storageDir = storageDir)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
single<LogReader> { LogcatReader.init(storageDir = androidContext().filesDir.absolutePath) }
|
|
||||||
|
|
||||||
single<PowerManager> {
|
single<PowerManager> {
|
||||||
androidContext().getSystemService(Context.POWER_SERVICE) as PowerManager
|
androidContext().getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||||
}
|
}
|
||||||
singleOf(::NotificationMonitor)
|
singleOf(::AndroidNotificationService) bind NotificationService::class
|
||||||
singleOf(::WireGuardNotification) bind NotificationManager::class
|
single { ServiceManager(androidContext()) }
|
||||||
single {
|
|
||||||
ServiceManager(
|
|
||||||
androidContext(),
|
|
||||||
get(named(Dispatcher.IO)),
|
|
||||||
get(named(Scope.APPLICATION)),
|
|
||||||
get(named(Dispatcher.MAIN)),
|
|
||||||
get(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
singleOf(::GlobalEffectRepository)
|
singleOf(::GlobalEffectRepository)
|
||||||
|
|
||||||
viewModelScope {
|
single { FileUtils(androidContext(), get(named(Dispatcher.IO))) }
|
||||||
scoped { FileUtils(androidContext(), get(named(Dispatcher.IO))) }
|
single<ShortcutManager> { DynamicShortcutManager(androidContext(), get(named(Dispatcher.IO))) }
|
||||||
scoped<ShortcutManager> {
|
singleOf(::SelectedTunnelsRepository)
|
||||||
DynamicShortcutManager(androidContext(), get(named(Dispatcher.IO)))
|
|
||||||
}
|
|
||||||
scopedOf(::SelectedTunnelsRepository)
|
|
||||||
}
|
|
||||||
|
|
||||||
single { NetworkUtils(get(named(Dispatcher.IO))) }
|
single { NetworkUtils(get(named(Dispatcher.IO))) }
|
||||||
|
|
||||||
viewModelOf(::AutoTunnelViewModel)
|
viewModelOf(::AutoTunnelViewModel)
|
||||||
viewModel { (id: Int) -> ConfigViewModel(get(), get(), get(), id) }
|
viewModel { (id: Int?) -> ConfigEditViewModel(get(), get(), get(), get(), get(), id) }
|
||||||
viewModelOf(::DnsViewModel)
|
viewModelOf(::DnsViewModel)
|
||||||
viewModelOf(::LicenseViewModel)
|
viewModelOf(::LicenseViewModel)
|
||||||
viewModelOf(::LockdownViewModel)
|
viewModelOf(::LockdownViewModel)
|
||||||
@@ -89,4 +90,6 @@ val appModule = module {
|
|||||||
viewModel { (id: Int) -> SplitTunnelViewModel(get(), get(), get(), id) }
|
viewModel { (id: Int) -> SplitTunnelViewModel(get(), get(), get(), id) }
|
||||||
viewModel { SupportViewModel(get(), get(named(Dispatcher.MAIN)), get()) }
|
viewModel { SupportViewModel(get(), get(named(Dispatcher.MAIN)), get()) }
|
||||||
viewModel { (id: Int) -> TunnelViewModel(get(), get(), id) }
|
viewModel { (id: Int) -> TunnelViewModel(get(), get(), id) }
|
||||||
|
|
||||||
|
singleOf(::AutoTunnelStateHolder)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
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(),
|
||||||
|
get(named(Scope.APPLICATION)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
singleOf(::AppBoostrapCoordinator)
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.di
|
package com.zaneschepke.wireguardautotunnel.di
|
||||||
|
|
||||||
|
import android.os.StrictMode
|
||||||
|
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.data.network.GitHubApi
|
import com.zaneschepke.wireguardautotunnel.data.network.GitHubApi
|
||||||
import com.zaneschepke.wireguardautotunnel.data.network.KtorClient
|
import com.zaneschepke.wireguardautotunnel.data.network.KtorClient
|
||||||
import com.zaneschepke.wireguardautotunnel.data.network.KtorGitHubApi
|
import com.zaneschepke.wireguardautotunnel.data.network.KtorGitHubApi
|
||||||
@@ -12,7 +14,20 @@ import org.koin.dsl.bind
|
|||||||
import org.koin.dsl.lazyModule
|
import org.koin.dsl.lazyModule
|
||||||
|
|
||||||
val networkModule = lazyModule {
|
val networkModule = lazyModule {
|
||||||
single { KtorClient.create() }
|
single {
|
||||||
|
val client =
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
val oldPolicy = StrictMode.allowThreadDiskReads()
|
||||||
|
try {
|
||||||
|
KtorClient.create()
|
||||||
|
} finally {
|
||||||
|
StrictMode.setThreadPolicy(oldPolicy)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
KtorClient.create()
|
||||||
|
}
|
||||||
|
client
|
||||||
|
}
|
||||||
singleOf(::KtorGitHubApi) bind GitHubApi::class
|
singleOf(::KtorGitHubApi) bind GitHubApi::class
|
||||||
|
|
||||||
single<UpdateRepository> {
|
single<UpdateRepository> {
|
||||||
|
|||||||
@@ -12,14 +12,3 @@ enum class Dispatcher {
|
|||||||
enum class Scope {
|
enum class Scope {
|
||||||
APPLICATION
|
APPLICATION
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class Shell {
|
|
||||||
APP,
|
|
||||||
TUNNEL,
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class Core {
|
|
||||||
KERNEL,
|
|
||||||
PROXY_USERSPACE,
|
|
||||||
USERSPACE,
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,108 +1,82 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.di
|
package com.zaneschepke.wireguardautotunnel.di
|
||||||
|
|
||||||
import com.wireguard.android.backend.WgQuickBackend
|
|
||||||
import com.wireguard.android.util.RootShell
|
|
||||||
import com.wireguard.android.util.ToolsInstaller
|
|
||||||
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
|
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
|
||||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
import com.zaneschepke.networkmonitor.StableNetworkEngine
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.backend.KernelTunnel
|
import com.zaneschepke.tunnel.ApplicationProvider
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.backend.RunConfigHelper
|
import com.zaneschepke.tunnel.util.RootShell
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.backend.TunnelBackend
|
import com.zaneschepke.tunnel.util.RootShellException
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.backend.UserspaceTunnel
|
import com.zaneschepke.wireguardautotunnel.core.event.TunnelEventDispatcher
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.AndroidApplicationProvider
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelBackendProvider
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.util.RootShellUtils
|
import com.zaneschepke.wireguardautotunnel.lifecyle.AppVisibilityObserver
|
||||||
|
import com.zaneschepke.wireguardautotunnel.notification.AndroidTunnelNotificationService
|
||||||
|
import com.zaneschepke.wireguardautotunnel.notification.TunnelNotificationService
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.to
|
import com.zaneschepke.wireguardautotunnel.util.extensions.to
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import org.amnezia.awg.backend.Backend
|
import kotlinx.coroutines.withContext
|
||||||
import org.amnezia.awg.backend.GoBackend
|
import kotlinx.coroutines.withTimeout
|
||||||
import org.amnezia.awg.backend.ProxyGoBackend
|
|
||||||
import org.amnezia.awg.backend.RootTunnelActionHandler
|
|
||||||
import org.koin.android.ext.koin.androidContext
|
import org.koin.android.ext.koin.androidContext
|
||||||
import org.koin.core.module.dsl.singleOf
|
import org.koin.core.module.dsl.singleOf
|
||||||
import org.koin.core.qualifier.named
|
import org.koin.core.qualifier.named
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
val tunnelModule = module {
|
val tunnelBackendProviderModule = module {
|
||||||
single(named(Shell.TUNNEL)) { RootShell(androidContext()) }
|
single<TunnelNotificationService> { AndroidTunnelNotificationService(get()) }
|
||||||
single(named(Shell.APP)) { RootShell(androidContext()) }
|
single { AppVisibilityObserver() }
|
||||||
|
singleOf(::TunnelEventDispatcher)
|
||||||
|
|
||||||
single { RootShellUtils(get(named(Shell.APP)), get(named(Dispatcher.IO))) }
|
single<ApplicationProvider> {
|
||||||
|
AndroidApplicationProvider(
|
||||||
singleOf(::RunConfigHelper)
|
notificationService = get(),
|
||||||
|
tunnelNotificationService = get(),
|
||||||
single<Backend>(named(Core.USERSPACE)) {
|
tunnelRepository = get(),
|
||||||
GoBackend(
|
|
||||||
androidContext(),
|
|
||||||
RootTunnelActionHandler(org.amnezia.awg.util.RootShell(androidContext())),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
single<Backend>(named(Core.PROXY_USERSPACE)) {
|
single {
|
||||||
ProxyGoBackend(
|
StableNetworkEngine(
|
||||||
androidContext(),
|
get<CoroutineScope>(named(Scope.APPLICATION)),
|
||||||
RootTunnelActionHandler(org.amnezia.awg.util.RootShell(androidContext())),
|
get<NetworkMonitor>().connectivityStateFlow,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
single<com.wireguard.android.backend.Backend> {
|
|
||||||
val shell = get<RootShell>(named(Shell.TUNNEL))
|
|
||||||
WgQuickBackend(
|
|
||||||
androidContext(),
|
|
||||||
shell,
|
|
||||||
ToolsInstaller(androidContext(), shell),
|
|
||||||
com.wireguard.android.backend.RootTunnelActionHandler(shell),
|
|
||||||
)
|
|
||||||
.apply { setMultipleTunnels(true) }
|
|
||||||
}
|
|
||||||
|
|
||||||
single<TunnelBackend>(named(Core.KERNEL)) {
|
|
||||||
KernelTunnel(get(), get<com.wireguard.android.backend.Backend>())
|
|
||||||
}
|
|
||||||
|
|
||||||
single<TunnelBackend>(qualifier = named(Core.USERSPACE)) {
|
|
||||||
UserspaceTunnel(get<Backend>(named(Core.USERSPACE)), get())
|
|
||||||
}
|
|
||||||
|
|
||||||
single<TunnelBackend>(qualifier = named(Core.PROXY_USERSPACE)) {
|
|
||||||
UserspaceTunnel(get<Backend>(named(Core.PROXY_USERSPACE)), get())
|
|
||||||
}
|
|
||||||
|
|
||||||
single<NetworkMonitor> {
|
single<NetworkMonitor> {
|
||||||
AndroidNetworkMonitor(
|
AndroidNetworkMonitor(
|
||||||
androidContext(),
|
androidContext(),
|
||||||
object : AndroidNetworkMonitor.ConfigurationListener {
|
object : AndroidNetworkMonitor.ConfigurationListener {
|
||||||
|
override suspend fun runRootShellCommand(cmd: String): String? {
|
||||||
|
return try {
|
||||||
|
withTimeout(3_000.milliseconds) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val result = RootShell.run(cmd)
|
||||||
|
result.output
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: RootShellException) {
|
||||||
|
Timber.e(e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override val detectionMethod =
|
override val detectionMethod =
|
||||||
get<AutoTunnelSettingsRepository>()
|
get<AutoTunnelSettingsRepository>()
|
||||||
.flow
|
.flow
|
||||||
.distinctUntilChangedBy { it.wifiDetectionMethod }
|
.distinctUntilChangedBy { it.wifiDetectionMethod }
|
||||||
.map { it.wifiDetectionMethod.to() }
|
.map { it.wifiDetectionMethod.to() }
|
||||||
|
|
||||||
override val rootShell = get<RootShell>(named(Shell.APP))
|
|
||||||
},
|
},
|
||||||
get<CoroutineScope>(named(Scope.APPLICATION)),
|
get<CoroutineScope>(named(Scope.APPLICATION)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
single {
|
single<TunnelProvider> {
|
||||||
TunnelManager(
|
TunnelBackendProvider(get(), get(named(Scope.APPLICATION)), get(named(Dispatcher.IO)))
|
||||||
get(named(Core.KERNEL)),
|
|
||||||
get(named(Core.USERSPACE)),
|
|
||||||
get(named(Core.PROXY_USERSPACE)),
|
|
||||||
get(),
|
|
||||||
get(),
|
|
||||||
get(),
|
|
||||||
get(),
|
|
||||||
get(),
|
|
||||||
get(),
|
|
||||||
get(),
|
|
||||||
get(),
|
|
||||||
get(),
|
|
||||||
get(),
|
|
||||||
get(named(Scope.APPLICATION)),
|
|
||||||
get(named(Dispatcher.IO)),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.domain.enums
|
|
||||||
|
|
||||||
sealed class BackendMode {
|
|
||||||
data object Inactive : BackendMode()
|
|
||||||
|
|
||||||
data class KillSwitch(
|
|
||||||
val allowedIps: Set<String>,
|
|
||||||
val isMetered: Boolean,
|
|
||||||
val dualStack: Boolean,
|
|
||||||
) : BackendMode()
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.domain.enums
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
|
||||||
|
enum class DnsProtocol(val value: Int) {
|
||||||
|
SYSTEM(0),
|
||||||
|
DOH(1),
|
||||||
|
DOT(2),
|
||||||
|
UDP(3);
|
||||||
|
|
||||||
|
fun asString(context: Context): String {
|
||||||
|
return when (this) {
|
||||||
|
SYSTEM -> context.getString(R.string.system)
|
||||||
|
DOH -> context.getString(R.string.doh)
|
||||||
|
DOT -> context.getString(R.string.dot)
|
||||||
|
UDP -> context.getString(R.string.plain_dns)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromValue(value: Int): DnsProtocol = entries.find { it.value == value } ?: SYSTEM
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.domain.enums
|
||||||
|
|
||||||
|
enum class MimicMode {
|
||||||
|
QUIC,
|
||||||
|
DNS,
|
||||||
|
SIP,
|
||||||
|
}
|
||||||
+3
-1
@@ -5,12 +5,14 @@ import com.zaneschepke.wireguardautotunnel.R
|
|||||||
|
|
||||||
enum class NotificationAction {
|
enum class NotificationAction {
|
||||||
TUNNEL_OFF,
|
TUNNEL_OFF,
|
||||||
AUTO_TUNNEL_OFF;
|
AUTO_TUNNEL_OFF,
|
||||||
|
STOP_ALL;
|
||||||
|
|
||||||
fun title(context: Context): String {
|
fun title(context: Context): String {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
TUNNEL_OFF -> context.getString(R.string.stop)
|
TUNNEL_OFF -> context.getString(R.string.stop)
|
||||||
AUTO_TUNNEL_OFF -> context.getString(R.string.stop)
|
AUTO_TUNNEL_OFF -> context.getString(R.string.stop)
|
||||||
|
STOP_ALL -> context.getString(R.string.stop_all)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.domain.enums
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
|
||||||
|
enum class StatisticRefresh(val value: Int) {
|
||||||
|
LIVE(1),
|
||||||
|
BALANCED(3),
|
||||||
|
BATTERY_SAVER(10);
|
||||||
|
|
||||||
|
fun asString(context: Context): String {
|
||||||
|
return when (this) {
|
||||||
|
LIVE -> context.getString(R.string.live)
|
||||||
|
BALANCED -> context.getString(R.string.balanced)
|
||||||
|
BATTERY_SAVER -> context.getString(R.string.balance_saver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromValue(value: Int): StatisticRefresh = entries.find { it.value == value } ?: BALANCED
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.domain.enums
|
||||||
|
|
||||||
|
enum class TunnelActionSource {
|
||||||
|
USER,
|
||||||
|
AUTO_TUNNEL,
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.domain.enums
|
||||||
|
|
||||||
|
enum class TunnelMode(val value: Int) {
|
||||||
|
VPN(0),
|
||||||
|
PROXY(1),
|
||||||
|
LOCK_DOWN(2);
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromValue(value: Int): TunnelMode = entries.find { it.value == value } ?: VPN
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.domain.enums
|
|
||||||
|
|
||||||
sealed class TunnelStatus {
|
|
||||||
|
|
||||||
data class Up(val startTime: Long) : TunnelStatus()
|
|
||||||
|
|
||||||
data object Down : TunnelStatus()
|
|
||||||
|
|
||||||
data object Stopping : TunnelStatus()
|
|
||||||
|
|
||||||
data object Starting : TunnelStatus()
|
|
||||||
|
|
||||||
fun isDown(): Boolean {
|
|
||||||
return this == Down
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isUp(): Boolean {
|
|
||||||
return this is Up
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isUpOrStarting(): Boolean {
|
|
||||||
return this is Up || this == Starting
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isDownOrStopping(): Boolean {
|
|
||||||
return this == Down || this is Stopping
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user