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