Compare commits

..

1 Commits

Author SHA1 Message Date
Zane Schepke 594ed85a71 start sticky 2025-02-22 07:52:16 -05:00
1490 changed files with 15848 additions and 42285 deletions
+97
View File
@@ -0,0 +1,97 @@
root = true
[*]
charset = utf-8
indent_size = 4
indent_style = tab
max_line_length = 150
trim_trailing_whitespace = true
insert_final_newline = true
[{*.kt,*.kts}]
ij_continuation_indent_size = 4
ij_java_names_count_to_use_import_on_demand = 9999
ij_kotlin_align_in_columns_case_branch = false
ij_kotlin_align_multiline_binary_operation = false
ij_kotlin_align_multiline_extends_list = false
ij_kotlin_align_multiline_method_parentheses = false
ij_kotlin_align_multiline_parameters = true
ij_kotlin_align_multiline_parameters_in_calls = false
ij_kotlin_assignment_wrap = normal
ij_kotlin_blank_lines_after_class_header = 0
ij_kotlin_blank_lines_around_block_when_branches = 0
ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1
ij_kotlin_block_comment_at_first_column = true
ij_kotlin_call_parameters_new_line_after_left_paren = true
ij_kotlin_call_parameters_right_paren_on_new_line = false
ij_kotlin_catch_on_new_line = false
ij_kotlin_continuation_indent_for_chained_calls = true
ij_kotlin_continuation_indent_for_expression_bodies = true
ij_kotlin_continuation_indent_in_argument_lists = true
ij_kotlin_continuation_indent_in_elvis = false
ij_kotlin_continuation_indent_in_if_conditions = false
ij_kotlin_continuation_indent_in_parameter_lists = false
ij_kotlin_continuation_indent_in_supertype_lists = false
ij_kotlin_else_on_new_line = false
ij_kotlin_enum_constants_wrap = off
ij_kotlin_extends_list_wrap = normal
ij_kotlin_field_annotation_wrap = split_into_lines
ij_kotlin_finally_on_new_line = false
ij_kotlin_if_rparen_on_new_line = false
ij_kotlin_import_nested_classes = false
ij_kotlin_insert_whitespaces_in_simple_one_line_method = true
ij_kotlin_keep_blank_lines_before_right_brace = 2
ij_kotlin_keep_blank_lines_in_code = 2
ij_kotlin_keep_blank_lines_in_declarations = 2
ij_kotlin_keep_first_column_comment = true
ij_kotlin_keep_indents_on_empty_lines = false
ij_kotlin_keep_line_breaks = true
ij_kotlin_lbrace_on_next_line = false
ij_kotlin_line_comment_add_space = false
ij_kotlin_line_comment_at_first_column = true
ij_kotlin_method_annotation_wrap = split_into_lines
ij_kotlin_method_call_chain_wrap = normal
ij_kotlin_method_parameters_new_line_after_left_paren = true
ij_kotlin_method_parameters_right_paren_on_new_line = true
ij_kotlin_name_count_to_use_star_import = 9999
ij_kotlin_name_count_to_use_star_import_for_members = 9999
ij_kotlin_parameter_annotation_wrap = off
ij_kotlin_space_after_comma = true
ij_kotlin_space_after_extend_colon = true
ij_kotlin_space_after_type_colon = true
ij_kotlin_space_before_catch_parentheses = true
ij_kotlin_space_before_comma = false
ij_kotlin_space_before_extend_colon = true
ij_kotlin_space_before_for_parentheses = true
ij_kotlin_space_before_if_parentheses = true
ij_kotlin_space_before_lambda_arrow = true
ij_kotlin_space_before_type_colon = false
ij_kotlin_space_before_when_parentheses = true
ij_kotlin_space_before_while_parentheses = true
ij_kotlin_spaces_around_additive_operators = true
ij_kotlin_spaces_around_assignment_operators = true
ij_kotlin_spaces_around_equality_operators = true
ij_kotlin_spaces_around_function_type_arrow = true
ij_kotlin_spaces_around_logical_operators = true
ij_kotlin_spaces_around_multiplicative_operators = true
ij_kotlin_spaces_around_range = false
ij_kotlin_spaces_around_relational_operators = true
ij_kotlin_spaces_around_unary_operator = false
ij_kotlin_spaces_around_when_arrow = true
ij_kotlin_variable_annotation_wrap = off
ij_kotlin_while_on_new_line = false
ij_kotlin_wrap_elvis_expressions = 1
ij_kotlin_wrap_expression_body_functions = 1
ij_kotlin_wrap_first_method_in_call_chain = false
#compose
ktlint_standard_filename = disabled
ktlint_standard_no-wildcard-imports = disabled
ktlint_standard_function-naming = disabled
ktlint_standard_property-naming = disabled
ktlint_standard_package-naming = disabled
ktlint_function_naming_ignore_when_annotated_with = Composable
ktlint_code_style = android_studio
ktlint_standard_import-ordering = disabled
ktlint_standard_package-naming = disabled
ij_kotlin_allow_trailing_comma = true
ij_kotlin_allow_trailing_comma_on_call_site = true
+22
View File
@@ -0,0 +1,22 @@
# Contributor Code of Conduct
## Pledge
We as individuals involved in this project, pledge to participate in this
community in a respectful, constructive, and civil manner as we work towards a common goal
of delivering free, open source, and value adding software for all.
## Standard
The standard for this community is the Golden Rule.
> “Do unto others as you would have them do unto you.”
## Scope
This Code of Conduct applies to all spaces related to WG Tunnel.
## Incidents or Concerns
For any incidents or concerns, reach out to Zane at
<support@zaneschepke.com>.
-1
View File
@@ -1,4 +1,3 @@
ko_fi: zaneschepke ko_fi: zaneschepke
liberapay: zaneschepke liberapay: zaneschepke
github: zaneschepke github: zaneschepke
custom: ["https://wgtunnel.com/donate/"]
+1 -1
View File
@@ -15,7 +15,7 @@ A clear and concise description of what the bug is.
- Device: [e.g. Pixel 4a] - Device: [e.g. Pixel 4a]
- Android Version: [e.g. Android 13] - Android Version: [e.g. Android 13]
- App Version [e.g. 3.3.3] - App Version [e.g. 3.3.3]
- App mode: [e.g. Kernel, VPN, Proxy, Lockdown] - Backend: [e.g. Kernel, Userspace]
**To Reproduce** **To Reproduce**
Steps to reproduce the behavior: Steps to reproduce the behavior:
-130
View File
@@ -1,130 +0,0 @@
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
+43 -42
View File
@@ -1,7 +1,4 @@
name: build name: build
permissions:
contents: read
on: on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
@@ -12,16 +9,9 @@ on:
default: debug default: debug
options: options:
- debug - debug
- prerelease
- nightly - nightly
- release - release
flavor:
type: choice
description: "Product flavor"
required: true
default: fdroid
options:
- fdroid
- standalone
secrets: secrets:
SIGNING_KEY_ALIAS: SIGNING_KEY_ALIAS:
required: false required: false
@@ -40,11 +30,6 @@ on:
description: "Build type" description: "Build type"
required: true required: true
default: debug default: debug
flavor:
type: string
description: "Product flavor"
required: false
default: fdroid
secrets: secrets:
SIGNING_KEY_ALIAS: SIGNING_KEY_ALIAS:
required: false required: false
@@ -56,7 +41,6 @@ on:
required: false required: false
KEYSTORE: KEYSTORE:
required: false required: false
env: env:
UPLOAD_DIR_ANDROID: android_artifacts UPLOAD_DIR_ANDROID: android_artifacts
@@ -72,17 +56,18 @@ jobs:
outputs: outputs:
UPLOAD_DIR_ANDROID: ${{ env.UPLOAD_DIR_ANDROID }} UPLOAD_DIR_ANDROID: ${{ env.UPLOAD_DIR_ANDROID }}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up JDK 17 - name: Set up JDK 17
uses: actions/setup-java@v5 uses: actions/setup-java@v4
with: with:
distribution: 'temurin' distribution: 'temurin'
java-version: '17' java-version: '17'
cache: gradle cache: gradle
- name: Grant execute permission for gradlew - name: Grant execute permission for gradlew
run: chmod +x gradlew run: chmod +x gradlew
# Here we need to decode keystore.jks from base64 string and place it
# in the folder specified in the release signing configuration
- name: Decode Keystore - name: Decode Keystore
id: decode_keystore id: decode_keystore
uses: timheuer/base64-to-file@v1.2 uses: timheuer/base64-to-file@v1.2
@@ -90,35 +75,51 @@ jobs:
fileName: ${{ env.KEY_STORE_FILE }} fileName: ${{ env.KEY_STORE_FILE }}
fileDir: ${{ env.KEY_STORE_LOCATION }} fileDir: ${{ env.KEY_STORE_LOCATION }}
encodedString: ${{ secrets.KEYSTORE }} encodedString: ${{ secrets.KEYSTORE }}
# create keystore path for gradle to read
- name: Create keystore path env var - name: Create keystore path env var
if: ${{ inputs.build_type != 'debug' }} if: ${{ inputs.build_type != 'debug' }}
run: | run: |
store_path=${{ env.KEY_STORE_LOCATION }}${{ env.KEY_STORE_FILE }} store_path=${{ env.KEY_STORE_LOCATION }}${{ env.KEY_STORE_FILE }}
echo "KEY_STORE_PATH=$store_path" >> $GITHUB_ENV echo "KEY_STORE_PATH=$store_path" >> $GITHUB_ENV
- name: Build APK - name: Create service_account.json
if: ${{ inputs.build_type != 'debug' }}
id: createServiceAccount
run: echo '${{ secrets.ANDROID_SERVICE_ACCOUNT_JSON }}' > service_account.json
- name: Build Fdroid Release APK
if: ${{ inputs.build_type == 'release' }}
run: ./gradlew :app:assembleFdroidRelease --info
- name: Build Fdroid Prerelease APK
if: ${{ inputs.build_type == 'prerelease' }}
run: ./gradlew :app:assembleFdroidPrerelease --info
- name: Build Fdroid Nightly APK
if: ${{ inputs.build_type == 'nightly' }}
run: ./gradlew :app:assembleFdroidNightly --info
- name: Build Debug APK
if: ${{ inputs.build_type == 'debug' }}
run: ./gradlew :app:assembleFdroidDebug --stacktrace
# bump versionCode for nightly and prerelease builds
- name: Commit and push versionCode changes
if: ${{ inputs.build_type == 'nightly' || inputs.build_type == 'prerelease' }}
run: | run: |
flavor=${{ inputs.flavor }} git config --global user.name 'GitHub Actions'
build_type=${{ inputs.build_type }} git config --global user.email 'actions@github.com'
case $build_type in git add versionCode.txt
"release") git commit -m "Automated build update"
./gradlew :app:assemble${flavor^}Release --info
;;
"nightly")
./gradlew :app:assemble${flavor^}Nightly --info
;;
"debug")
./gradlew :app:assemble${flavor^}Debug --stacktrace
;;
esac
- name: Get release apk path - name: Get release apk path
id: apk-path id: apk-path
run: echo "path=$(find . -regex '^.*/build/outputs/apk/${{ inputs.flavor }}/${{ inputs.build_type }}/.*\.apk$' -type f | head -1 | tail -c+2)" >> $GITHUB_OUTPUT run: echo "path=$(find . -regex '^.*/build/outputs/apk/fdroid/${{ inputs.build_type }}/.*\.apk$' -type f | head -1 | tail -c+2)" >> $GITHUB_OUTPUT
- name: Upload All APK Artifacts
uses: actions/upload-artifact@v6 - name: Upload release apk
uses: actions/upload-artifact@v4
with: with:
name: android_artifacts_${{ inputs.flavor }} name: ${{ env.UPLOAD_DIR_ANDROID }}
path: >- path: ${{github.workspace}}/${{ steps.apk-path.outputs.path }}
app/build/outputs/apk/${{ inputs.flavor }}/${{ inputs.build_type }}/*.apk
retention-days: 1 retention-days: 1
if-no-files-found: warn
-129
View File
@@ -1,129 +0,0 @@
name: nightly
permissions:
contents: write
packages: write
on:
workflow_dispatch:
schedule:
- cron: "4 3 * * *"
jobs:
check_commits:
name: Check for New Commits
runs-on: ubuntu-latest
outputs:
has_new_commits: ${{ steps.check.outputs.new_commits }}
steps:
- name: Checkout Repository
uses: actions/checkout@v6
- name: Check for new commits
id: check
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
run: |
NEW_COMMITS=$(git rev-list --count --after="$(date -Iseconds -d '23 hours ago')" ${{ github.sha }})
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:
build_type: "nightly"
flavor: standalone
publish:
needs:
- 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@v6
- name: Install system dependencies
run: |
sudo apt update && sudo apt install -y gh apksigner
- name: Set latest tag
uses: rickstaa/action-create-tag@v1
id: tag_creation
with:
tag: "latest"
message: "Automated tag for HEAD commit"
force_push_tag: true
github_token: ${{ github.token }}
tag_exists_error: false
- name: Generate Changelog
id: changelog
uses: requarks/changelog-action@v1
with:
token: ${{ github.token }}
toTag: "nightly"
fromTag: "latest"
writeToFile: false
- name: Make download dir
run: mkdir ${{ github.workspace }}/temp
- name: Download artifacts
uses: actions/download-artifact@v7
with:
pattern: android_artifacts_*
path: ${{ github.workspace }}/temp
- name: Set release notes
run: |
echo "RELEASE_NOTES=Nightly build for the latest development version of the app." >> $GITHUB_ENV
- name: Delete previous nightly version
uses: ClementTsang/delete-tag-and-release@v0.4.0
with:
tag_name: "nightly"
delete_release: true
env:
GITHUB_TOKEN: ${{ github.token }}
- name: Get checksum
id: checksum
run: |
file_path=$(find ${{ github.workspace }}/temp -type f -iname "*.apk" | head -n 1)
if [ -z "$file_path" ]; then
echo "No APK file found"
exit 1
fi
checksum=$(apksigner verify --print-certs "$file_path" | grep -Po "(?<=SHA-256 digest:) .*" | tr -d "[:blank:]")
echo "checksum=$checksum" >> $GITHUB_OUTPUT
- name: Create nightly release
id: create_release
uses: softprops/action-gh-release@v2
with:
body: |
${{ env.RELEASE_NOTES }}
SHA-256 fingerprints for the 4096-bit signing certificate:
```sh
${{ steps.checksum.outputs.checksum }}
```
To verify fingerprint:
```sh
apksigner verify --print-certs [path to APK file] | grep SHA-256
```
### Changelog
${{ steps.changelog.outputs.changes }}
tag_name: nightly
name: nightly
draft: false
prerelease: true
make_latest: false
files: |
${{ github.workspace }}/temp/**/*.apk
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
-148
View File
@@ -1,148 +0,0 @@
name: notifications
permissions:
contents: write
packages: write
on:
issues:
types: [opened, closed]
release:
types: [published, prereleased]
jobs:
notify:
runs-on: ubuntu-latest
steps:
- 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: |
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 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: |
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"
+20
View File
@@ -0,0 +1,20 @@
name: on-issue
on:
issues:
types: [ opened, closed, reopened ]
jobs:
on-issue:
name: On new issue
runs-on: ubuntu-latest
steps:
- name: Send Telegram Message
run: |
msg_text='${{ github.actor }} updated an issue:
status: ${{ github.event.issue.state }} - #${{ github.event.issue.number }} ${{ github.event.issue.title }}
https://github.com/zaneschepke/wgtunnel/issues/${{ github.event.issue.number }}'
curl -s -X POST 'https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN }}/sendMessage' \
-d "chat_id=${{ secrets.TELEGRAM_TO }}&text=${msg_text}&message_thread_id=${{ secrets.TELEGRAM_TOPIC }}"
@@ -1,6 +1,4 @@
name: on-pr name: on-pr
permissions:
contents: read
on: on:
workflow_dispatch: workflow_dispatch:
@@ -10,9 +8,9 @@ jobs:
format_check: format_check:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- name: Set up JDK 17 - name: Set up JDK 17
uses: actions/setup-java@v5 uses: actions/setup-java@v4
with: with:
distribution: 'temurin' distribution: 'temurin'
java-version: '17' java-version: '17'
@@ -21,5 +19,5 @@ jobs:
- name: Grant execute permission for gradlew - name: Grant execute permission for gradlew
run: chmod +x gradlew run: chmod +x gradlew
- name: Run ktfmt - name: Run ktlint
run: ./gradlew ktfmtCheck run: ./gradlew ktlintCheck
+21
View File
@@ -0,0 +1,21 @@
name: on-publish
on:
release:
types: [ published ]
jobs:
on-publish:
name: On publish
runs-on: ubuntu-latest
steps:
- name: Send Telegram Message
run: |
msg_text='${{ github.actor }} published a new release:
Release: ${{ github.event.release.tag_name }}
${{ github.event.release.body }}
https://github.com/zaneschepke/wgtunnel/releases/tag/${{ github.event.release.tag_name }}'
curl -s -X POST 'https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN }}/sendMessage' \
-d "chat_id=${{ secrets.TELEGRAM_TO }}&text=${msg_text}&message_thread_id=${{ secrets.TELEGRAM_TOPIC }}"
+108 -88
View File
@@ -1,17 +1,13 @@
name: publish name: publish
permissions:
contents: write
packages: write
on: on:
push: schedule:
tags: - cron: "4 3 * * *"
- '[0-9]*.[0-9]*.[0-9]*'
workflow_dispatch: workflow_dispatch:
inputs: inputs:
track: track:
type: choice type: choice
description: "Google Play release track" description: "Google play release track"
options: options:
- none - none
- internal - internal
@@ -25,130 +21,159 @@ on:
description: "GitHub release type" description: "GitHub release type"
options: options:
- none - none
- prerelease
- nightly
- release - release
default: release default: release
required: true required: true
tag_name: tag_name:
description: "Tag name for release" description: "Tag name for release"
required: false required: false
default: 1.1.1 default: nightly
workflow_call: workflow_call:
inputs: env:
flavor: UPLOAD_DIR_ANDROID: android_artifacts
type: string
description: "Product flavor"
required: false
default: standalone
jobs: jobs:
check_commits:
name: Check for New Commits
runs-on: ubuntu-latest
outputs:
has_new_commits: ${{ steps.check.outputs.new_commits }}
build-fdroid: steps:
if: >- - name: Checkout Repository
${{ uses: actions/checkout@v3
github.event_name == 'push' || with:
inputs.release_type != 'none' fetch-depth: 0 # This fetches all history so we can check commits
}}
- name: Check for new commits
id: check
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# This script checks for commits newer than 23 hours ago
NEW_COMMITS=$(git rev-list --count --after="$(date -Iseconds -d '23 hours ago')" ${{ github.sha }})
echo "new_commits=$NEW_COMMITS" >> $GITHUB_OUTPUT
build:
if: ${{ inputs.release_type != 'none' }}
uses: ./.github/workflows/build.yml uses: ./.github/workflows/build.yml
secrets: inherit secrets: inherit
with: with:
build_type: ${{ github.event_name == 'push' && 'release' || inputs.release_type }} build_type: ${{ inputs.release_type == '' && 'nightly' || inputs.release_type }}
flavor: fdroid
build-standalone: publish:
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-github:
if: >-
${{
github.event_name == 'push' ||
inputs.release_type != 'none'
}}
needs: needs:
- build-fdroid - check_commits
- build-standalone - build
if: ${{ needs.check_commits.outputs.has_new_commits > 0 && inputs.release_type != 'none' }}
name: publish-github name: publish-github
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
GH_USER: ${{ secrets.GH_USER }}
# GH needed for gh cli
GH_TOKEN: ${{ secrets.GH_TOKEN }}
GH_REPO: ${{ github.repository }}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'push' && github.ref || 'master' }}
- name: Install system dependencies - name: Install system dependencies
run: | run: |
sudo apt update && sudo apt install -y gh apksigner sudo apt update && sudo apt install -y gh apksigner
# update latest tag
- name: Set latest tag - name: Set latest tag
uses: rickstaa/action-create-tag@v1 uses: rickstaa/action-create-tag@v1
id: tag_creation id: tag_creation
with: with:
tag: "latest" tag: "latest" # or any tag name you wish to use
message: "Automated tag for HEAD commit" message: "Automated tag for HEAD commit"
force_push_tag: true force_push_tag: true
github_token: ${{ github.token }}
tag_exists_error: false tag_exists_error: false
- name: Get latest release - name: Get latest release
id: latest_release id: latest_release
uses: kaliber5/action-get-release@v1 uses: kaliber5/action-get-release@v1
with: with:
token: ${{ github.token }} token: ${{ secrets.GITHUB_TOKEN }}
latest: true latest: true
- name: Generate Changelog - name: Generate Changelog
id: changelog id: changelog
uses: requarks/changelog-action@v1 uses: requarks/changelog-action@v1
with: with:
token: ${{ github.token }} token: ${{ secrets.GITHUB_TOKEN }}
toTag: ${{ steps.latest_release.outputs.tag_name }} toTag: ${{ github.event_name == 'schedule' && 'nightly' || steps.latest_release.outputs.tag_name }}
fromTag: "latest" fromTag: "latest"
writeToFile: false writeToFile: false # we won't write to file, just output
- name: Get version code
if: ${{ inputs.release_type == 'release' }}
run: |
version_code=$(grep "VERSION_CODE" buildSrc/src/main/kotlin/Constants.kt | awk '{print $5}' | tr -d '\n')
echo "VERSION_CODE=$version_code" >> $GITHUB_ENV
- name: Push changes
if: ${{ inputs.release_type == '' || inputs.release_type == 'nightly' || inputs.release_type == 'prerelease' }}
uses: ad-m/github-push-action@master
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
branch: ${{ github.ref }}
- name: Make download dir - name: Make download dir
run: mkdir ${{ github.workspace }}/temp run: mkdir ${{ github.workspace }}/temp
- name: Download artifacts - name: Download artifacts
uses: actions/download-artifact@v7 uses: actions/download-artifact@v4
with: with:
pattern: android_artifacts_* name: ${{ env.UPLOAD_DIR_ANDROID }}
path: ${{ github.workspace }}/temp path: ${{ github.workspace }}/temp
merge-multiple: true
# Setup TAG_NAME, which is used as a general "name"
- if: github.event_name == 'workflow_dispatch'
run: echo "TAG_NAME=${{ github.event.inputs.tag_name }}" >> $GITHUB_ENV
- if: github.event_name == 'schedule'
run: echo "TAG_NAME=nightly" >> $GITHUB_ENV
- name: Set version release notes - name: Set version release notes
if: ${{ github.event_name == 'push' || inputs.release_type == 'release' }} if: ${{ inputs.release_type == 'release' }}
run: | run: |
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/${{ env.VERSION_CODE }}.txt)"
RELEASE_NOTES="$(cat ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/${VERSION_CODE}.txt || echo "No changelog found for ${VERSION_CODE}")"
echo "RELEASE_NOTES<<EOF" >> $GITHUB_ENV echo "RELEASE_NOTES<<EOF" >> $GITHUB_ENV
echo "$RELEASE_NOTES" >> $GITHUB_ENV echo "$RELEASE_NOTES" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV echo "EOF" >> $GITHUB_ENV
- name: On nightly release notes
if: ${{ contains(env.TAG_NAME, 'nightly') }}
run: |
echo "RELEASE_NOTES=Nightly build for the latest development version of the app." >> $GITHUB_ENV
gh release delete nightly --yes || true
git push origin :nightly || true
- name: On prerelease release notes
if: ${{ inputs.release_type == 'prerelease' }}
run: |
echo "RELEASE_NOTES=Testing version of app for specific feature." >> $GITHUB_ENV
gh release delete ${{ github.event.inputs.tag_name }} --yes || true
- name: Get checksum - name: Get checksum
id: checksum id: checksum
run: | run: |
file_path=$(find ${{ github.workspace }}/temp -type f -iname "*.apk" | head -n 1) file_path=$(find ${{ github.workspace }}/temp -type f -iname "*.apk" | tail -n1)
if [ -z "$file_path" ]; then echo "checksum=$(apksigner verify -print-certs $file_path | grep -Po "(?<=SHA-256 digest:) .*" | tr -d "[:blank:]")" >> $GITHUB_OUTPUT
echo "No APK file found"
exit 1
fi
checksum=$(apksigner verify --print-certs "$file_path" | grep -Po "(?<=SHA-256 digest:) .*" | tr -d "[:blank:]")
echo "checksum=$checksum" >> $GITHUB_OUTPUT
- name: Create Release
- name: Create Release with Fastlane changelog notes
id: create_release id: create_release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
body: | body: |
${{ env.RELEASE_NOTES }} ${{ env.RELEASE_NOTES }}
SHA-256 fingerprints for the 4096-bit signing certificate: SHA-256 fingerprint for the 4096-bit signing certificate:
```sh ```sh
${{ steps.checksum.outputs.checksum }} ${{ steps.checksum.outputs.checksum }}
``` ```
@@ -160,35 +185,29 @@ jobs:
### Changelog ### Changelog
${{ steps.changelog.outputs.changes }} ${{ steps.changelog.outputs.changes }}
tag_name: ${{ github.event_name == 'push' && github.ref_name || github.event.inputs.tag_name }} tag_name: ${{ env.TAG_NAME }}
name: ${{ github.event_name == 'push' && github.ref_name || github.event.inputs.tag_name }} name: ${{ env.TAG_NAME }}
draft: false draft: false
prerelease: false prerelease: ${{ inputs.release_type == 'prerelease' || inputs.release_type == '' || inputs.release_type == 'nightly' }}
make_latest: true make_latest: ${{ inputs.release_type == 'release' }}
files: | files: |
${{ github.workspace }}/temp/**/*.apk ${{ github.workspace }}/temp/*
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
publish-fdroid-public: publish-fdroid:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: >-
${{
github.event_name == 'push' ||
inputs.release_type != 'none'
}}
needs: needs:
- publish-github - build
if: inputs.release_type == 'release'
steps: steps:
- name: Dispatch update for fdroid repo - name: Dispatch update for fdroid repo
uses: peter-evans/repository-dispatch@v4 uses: peter-evans/repository-dispatch@v3
with: with:
token: ${{ secrets.PAT }} token: ${{ secrets.PAT }}
repository: wgtunnel/fdroid repository: zaneschepke/fdroid
event-type: fdroid-update event-type: fdroid-update
publish-play: publish-play:
if: ${{ github.event_name == 'push' || inputs.track != 'none' }} if: ${{ inputs.track != 'none' && inputs.track != '' }}
name: Publish to Google Play name: Publish to Google Play
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -198,11 +217,13 @@ jobs:
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }} SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
KEY_STORE_FILE: 'android_keystore.jks' KEY_STORE_FILE: 'android_keystore.jks'
KEY_STORE_LOCATION: ${{ github.workspace }}/app/keystore/ KEY_STORE_LOCATION: ${{ github.workspace }}/app/keystore/
GH_USER: ${{ secrets.GH_USER }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- name: Set up JDK 17 - name: Set up JDK 17
uses: actions/setup-java@v5 uses: actions/setup-java@v4
with: with:
distribution: 'temurin' distribution: 'temurin'
java-version: '17' java-version: '17'
@@ -238,6 +259,5 @@ jobs:
bundler-cache: true bundler-cache: true
- name: Distribute app to Prod track 🚀 - name: Distribute app to Prod track 🚀
run: | run: (cd ${{ github.workspace }} && bundle install && bundle exec fastlane ${{ inputs.track }})
track=${{ github.event_name == 'push' && 'production' || inputs.track }}
(cd ${{ github.workspace }} && bundle install && bundle exec fastlane $track)
+1 -1
View File
@@ -70,5 +70,5 @@ lint/tmp/
app/release/output.json app/release/output.json
.idea/codeStyles/ .idea/codeStyles/
# where we keep our signing secrets locally # where we keep our signing secrets locally
app/signing.properties
/.kotlin/ /.kotlin/
/app/keystore/
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License MIT License
Copyright © 2023-2025 Zane Schepke Copyright (c) 2023 WG Auto Tunnel
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
+41 -38
View File
@@ -4,7 +4,7 @@ WG Tunnel
<div align="center"> <div align="center">
An alternative FOSS Android client for [WireGuard](https://www.wireguard.com/) An alternative Android client app for [WireGuard®](https://www.wireguard.com/)
and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/)
<br /> <br />
<br /> <br />
@@ -21,26 +21,27 @@ and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/)
<div align="center"> <div align="center">
[![Google Play](https://img.shields.io/badge/Google_Play-414141?style=for-the-badge&logo=google-play&logoColor=white)](https://play.google.com/store/apps/details?id=com.zaneschepke.wireguardautotunnel) [![Google Play](https://img.shields.io/badge/Google_Play-414141?style=for-the-badge&logo=google-play&logoColor=white)](https://play.google.com/store/apps/details?id=com.zaneschepke.wireguardautotunnel)
[![F-Droid](https://img.shields.io/static/v1?style=for-the-badge&message=F-Droid&color=1976D2&logo=F-Droid&logoColor=FFFFFF&label=)](https://github.com/zaneschepke/fdroid) [![F-Droid](https://img.shields.io/static/v1?style=for-the-badge&message=F-Droid&color=1976D2&logo=F-Droid&logoColor=FFFFFF&label=)](https://f-droid.org/packages/com.zaneschepke.wireguardautotunnel/)
[![Obtainium](https://img.shields.io/badge/Obtainium-414141?style=for-the-badge&logo=Obtainium&logoColor=white)](https://apps.obtainium.imranr.dev/redirect?r=obtainium://app/%7B%22id%22%3A%22com.zaneschepke.wireguardautotunnel%22%2C%22url%22%3A%22https%3A%2F%2Fgithub.com%2Fzaneschepke%2Fwgtunnel%22%2C%22author%22%3A%22zaneschepke%22%2C%22name%22%3A%22WG%20Tunnel%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Atrue%2C%5C%22sortMethodChoice%5C%22%3A%5C%22date%5C%22%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Atrue%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22WG%20Tunnel%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22Zane%20Schepke%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%7D%22%2C%22overrideSource%22%3Anull%7D) [![Personal](https://img.shields.io/static/v1?style=for-the-badge&message=Personal&color=1976D2&logo=F-Droid&logoColor=FFFFFF&label=)](https://github.com/zaneschepke/fdroid)
</div> </div>
<div align="center"> <div align="center">
[<img src="https://img.shields.io/badge/Telegram-26A5E4.svg?style=for-the-badge&logo=Telegram&logoColor=white">](https://t.me/wgtunnel) [![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?style=for-the-badge&logo=discord&logoColor=white)](https://discord.gg/rbRRNh6H7V)
[<img src="https://img.shields.io/badge/Matrix-000000.svg?style=for-the-badge&logo=Matrix&logoColor=white">](https://matrix.to/#/#wg-tunnel-space:matrix.org) [![Telegram](https://img.shields.io/badge/Telegram-2CA5E0?style=for-the-badge&logo=telegram&logoColor=white)](https://t.me/wgtunnel)
</div> </div>
<details open="open"> <details open="open">
<summary>Table of Contents</summary> <summary>Table of Contents</summary>
- [About](#about) - [About](#about)
- [Acknowledgements](#acknowledgements)
- [Screenshots](#screenshots) - [Screenshots](#screenshots)
- [Features](#features) - [Features](#features)
- [Building](#building) - [Building](#building)
- [Translation](#translation) - [Translation](#translation)
- [Acknowledgements](#acknowledgements)
- [Contributing](#contributing) - [Contributing](#contributing)
</details> </details>
@@ -48,45 +49,56 @@ and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/)
<div style="text-align: left;"> <div style="text-align: left;">
## About ## About
Inspired by the official [wireguard-android](https://github.com/WireGuard/wireguard-android) app, WG Tunnel was created to address features and support missing from the official app. This app combines support for both [WireGuard®](https://www.wireguard.com/)
WG Tunnel is an alternative Android client for WireGuard and AmneziaWG, inspired by the official WireGuard Android app. It fills gaps in the official client by adding advanced features like auto-tunneling (on-demand VPN activation), while seamlessly supporting both protocols across app modes—including Kernel (for direct WireGuard kernel integration; AmneziaWG not supported), VPN (standard system-level tunneling), Lockdown (a custom kill switch for leak prevention), and Proxy (built-in HTTP/SOCKS5 forwarding)—for enhanced privacy, censorship resistance, and flexibility. and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/), with its primary feature of auto-tunneling (on-demand tunneling).
</div> </div>
<div style="text-align: left;"> <div style="text-align: left;">
## Acknowledgements
Thank you to the following:
- All of the users that have helped contribute to the project with ideas, translations, feedback, bug reports, testing, and donations.
- [WireGuard®](https://www.wireguard.com/) - © Jason A. Donenfeld (https://github.com/WireGuard/wireguard-android)
- [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) - Amnezia Team (https://github.com/amnezia-vpn/amneziawg-android)
## Screenshots ## Screenshots
</div> </div>
<div style="display: flex; flex-wrap: wrap; justify-content: left; gap: 10px;"> <div style="display: flex; flex-wrap: wrap; justify-content: center; gap: 10px;">
<img label="Main" src="fastlane/metadata/android/en-US/images/phoneScreenshots/main_screen.png" width="200" alt="Main"/> <img label="Main" src="fastlane/metadata/android/en-US/images/phoneScreenshots/main_screen.png" width="200" />
<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" />
<img label="Settings" src="fastlane/metadata/android/en-US/images/phoneScreenshots/settings_screen.png" width="200" alt="Settings"/> <img label="Auto" src="fastlane/metadata/android/en-US/images/phoneScreenshots/auto_screen.png" width="200" />
<img label="Auto-tunnel" src="fastlane/metadata/android/en-US/images/phoneScreenshots/auto_screen.png" width="200" alt="Auto-tunnel"/> <img label="Config" src="fastlane/metadata/android/en-US/images/phoneScreenshots/config_screen.png" width="200" />
</div> </div>
<div style="text-align: left;">
## Features ## Features
- **Tunnel Import Methods**: Easily add tunnels using .conf files, ZIP archives, manual entry, or QR code scanning. * Add tunnels via .conf file, zip, manual entry, clipboard, or QR code
- **Auto-Tunneling**: Automatically activate tunnels based on Wi-Fi SSID, Ethernet connections, or mobile data networks. * Auto-tunnel based on Wi-Fi SSID, ethernet, or mobile data
- **Split Tunneling**: Flexible support for routing specific apps or traffic through the VPN. * Split tunneling by application with search
- **WireGuard Modes**: Full compatibility with WireGuard in both kernel and userspace implementations. * Support for kernel and userspace modes
- **AmneziaWG Integration**: Userspace mode for AmneziaWG, providing robust censorship evasion. * Amnezia support for userspace mode for DPI/censorship protection
- **Always-On VPN**: Ensures continuous protection with Android's Always-On VPN feature. * Pre/Post Up/Down scripts support for all modes on a rooted device
- **Quick Controls**: Quick Settings tile and home screen shortcuts for easy VPN toggling. * Always-On VPN support
- **Automation Support**: Intent-based automation for controlling tunnels. * Export tunnels to zip
- **Auto-Restore**: Seamlessly restores auto-tunneling and active tunnels after device restarts or app updates. * Quick tile support for tunnel toggling, auto-tunneling
- **Proxying Options**: Built-in HTTP and SOCKS5 proxy support within tunnels. * Shortcuts support for tunnel toggling, auto-tunneling
- **Lockdown Mode**: Custom kill switch for maximum leak prevention and security. * Intent automation support for all tunnels
- **Dynamic DNS Handling**: Detects and updates DNS changes without tunnel restarts. * In app VPN kill switch with LAN bypass
- **Monitoring Tools**: Advanced tunnel monitoring features for tunnel performance monitoring. * Automatic auto-tunneling service and/or tunnel restart after reboot or app update
- **Android TV Support**: Android TV support for secure streaming and browsing. * Battery preservation measures
- **Advanced DNS**: DNS over HTTPS support for tunnel endpoint resolutions. * Restart tunnel on ping failure
## Building ## Building
```sh ```sh
git clone https://github.com/wgtunnel/wgtunnel git clone https://github.com/zaneschepke/wgtunnel
cd wgtunnel cd wgtunnel
``` ```
@@ -102,15 +114,6 @@ Help translate WG Tunnel into your language
at [Hosted Weblate](https://hosted.weblate.org/engage/wg-tunnel/).\ at [Hosted Weblate](https://hosted.weblate.org/engage/wg-tunnel/).\
[![Translation status](https://hosted.weblate.org/widgets/wg-tunnel/-/multi-auto.svg)](https://hosted.weblate.org/engage/wg-tunnel/) [![Translation status](https://hosted.weblate.org/widgets/wg-tunnel/-/multi-auto.svg)](https://hosted.weblate.org/engage/wg-tunnel/)
## Acknowledgements
Thank you to the following:
- All of the users that have helped contribute to the project with ideas, translations, feedback, bug reports, testing, and donations.
- [WireGuard](https://www.wireguard.com/) - Jason A. Donenfeld (https://github.com/WireGuard/wireguard-android)
- [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) - Amnezia Team (https://github.com/amnezia-vpn/amneziawg-android)
- [JetBrains](https://jetbrains.com) - For supporting open-source developers with free software licenses.
## Contributing ## Contributing
Any contributions in the form of feedback, issues, code, or translations are welcome and much Any contributions in the form of feedback, issues, code, or translations are welcome and much
+1 -2
View File
@@ -1,3 +1,2 @@
/build /build
/release /release
/src/main/assets/licenses.json
+211 -231
View File
@@ -1,275 +1,255 @@
import com.android.build.gradle.internal.api.BaseVariantOutputImpl
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins { plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlinxSerialization) alias(libs.plugins.hilt.android)
alias(libs.plugins.ksp) alias(libs.plugins.kotlinxSerialization)
alias(libs.plugins.compose.compiler) alias(libs.plugins.ksp)
alias(libs.plugins.grgit) alias(libs.plugins.compose.compiler)
alias(libs.plugins.licensee) alias(libs.plugins.grgit)
}
val versionFile = file("$rootDir/versionCode.txt")
val versionCodeIncrement = with(getBuildTaskName().lowercase()) {
when {
this.contains(Constants.NIGHTLY) || this.contains(Constants.PRERELEASE) -> {
if (versionFile.exists()) {
versionFile.readText().trim().toInt() + 1
} else {
1
}
}
else -> 0
}
} }
android { android {
namespace = Constants.APP_ID namespace = Constants.APP_ID
compileSdk = Constants.TARGET_SDK compileSdk = Constants.TARGET_SDK
androidResources { generateLocaleConfig = true } androidResources {
generateLocaleConfig = true
}
dependenciesInfo { // reproducibility
includeInApk = false dependenciesInfo {
includeInBundle = false // Disables dependency metadata when building APKs.
} includeInApk = false
// Disables dependency metadata when building Android App Bundles.
includeInBundle = false
}
ksp { arg("room.schemaLocation", "$projectDir/schemas") } defaultConfig {
applicationId = Constants.APP_ID
minSdk = Constants.MIN_SDK
targetSdk = Constants.TARGET_SDK
versionCode = Constants.VERSION_CODE + versionCodeIncrement
versionName = determineVersionName()
// fix okhttp proguard issue ksp { arg("room.schemaLocation", "$projectDir/schemas") }
packaging { resources { pickFirsts.add("okhttp3/internal/publicsuffix/publicsuffixes.gz") } }
splits { sourceSets {
abi { getByName("debug").assets.srcDirs(files("$projectDir/schemas")) // Room
isEnable = !project.hasProperty("noSplits") }
reset()
include("armeabi-v7a", "arm64-v8a")
isUniversalApk = !project.hasProperty("noSplits")
}
}
defaultConfig { buildConfigField("String[]", "LANGUAGES", "new String[]{ ${languageList().joinToString(separator = ", ") { "\"$it\"" }} }")
applicationId = Constants.APP_ID
minSdk = Constants.MIN_SDK
targetSdk = Constants.TARGET_SDK
versionCode = computeVersionCode()
versionName = computeVersionName()
sourceSets { getByName("debug").assets.srcDirs(files("$projectDir/schemas")) } testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { useSupportLibrary = true }
}
val languagesArray = buildLanguagesArray(languageList()) signingConfigs {
buildConfigField("String[]", "LANGUAGES", "new String[]{ $languagesArray }") create(Constants.RELEASE) {
storeFile = getStoreFile()
storePassword = getSigningProperty(Constants.STORE_PASS_VAR)
keyAlias = getSigningProperty(Constants.KEY_ALIAS_VAR)
keyPassword = getSigningProperty(Constants.KEY_PASS_VAR)
}
}
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" buildTypes {
vectorDrawables { useSupportLibrary = true } // don't strip
} packaging.jniLibs.keepDebugSymbols.addAll(
listOf("libwg-go.so", "libwg-quick.so", "libwg.so"),
)
signingConfigs { release {
create(Constants.RELEASE) { isDebuggable = false
storeFile = file(System.getenv("KEY_STORE_PATH") ?: "keystore/android_keystore.jks") isMinifyEnabled = true
storePassword = isShrinkResources = true
LocalProperties.get("SIGNING_STORE_PASSWORD") proguardFiles(
?: System.getenv("SIGNING_STORE_PASSWORD") getDefaultProguardFile("proguard-android-optimize.txt"),
keyAlias = "proguard-rules.pro",
LocalProperties.get("SIGNING_KEY_ALIAS") ?: System.getenv("SIGNING_KEY_ALIAS") )
keyPassword = signingConfig = signingConfigs.getByName(Constants.RELEASE)
LocalProperties.get("SIGNING_KEY_PASSWORD") ?: System.getenv("SIGNING_KEY_PASSWORD") resValue("string", "provider", "\"${Constants.APP_NAME}.provider\"")
} }
} debug {
applicationIdSuffix = ".debug"
versionNameSuffix = "-debug"
resValue("string", "app_name", "WG Tunnel - Debug")
isDebuggable = true
resValue("string", "provider", "\"${Constants.APP_NAME}.provider.debug\"")
}
buildTypes { create(Constants.PRERELEASE) {
packaging.jniLibs.keepDebugSymbols.addAll( initWith(buildTypes.getByName(Constants.RELEASE))
listOf("libwg-go.so", "libwg-quick.so", "libwg.so") applicationIdSuffix = ".prerelease"
) versionNameSuffix = "-pre"
resValue("string", "app_name", "WG Tunnel - Pre")
resValue("string", "provider", "\"${Constants.APP_NAME}.provider.pre\"")
}
release { create(Constants.NIGHTLY) {
isDebuggable = false initWith(buildTypes.getByName(Constants.RELEASE))
isMinifyEnabled = true applicationIdSuffix = ".nightly"
isShrinkResources = true versionNameSuffix = "-nightly"
proguardFiles( resValue("string", "app_name", "WG Tunnel - Nightly")
getDefaultProguardFile("proguard-android-optimize.txt"), resValue("string", "provider", "\"${Constants.APP_NAME}.provider.nightly\"")
"proguard-rules.pro", }
)
signingConfig = signingConfigs.getByName(Constants.RELEASE)
resValue("string", "provider", "\"${Constants.APP_NAME}.provider\"")
}
debug { applicationVariants.all {
applicationIdSuffix = ".debug" val variant = this
resValue("string", "app_name", "WG Tunnel Debug") variant.outputs
isDebuggable = true .map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl }
resValue("string", "provider", "\"${Constants.APP_NAME}.provider.debug\"") .forEach { output ->
} val outputFileName =
"${Constants.APP_NAME}-${variant.flavorName}-" +
create(Constants.NIGHTLY) { "${variant.buildType.name}-${variant.versionName}.apk"
initWith(buildTypes.getByName(Constants.RELEASE)) output.outputFileName = outputFileName
applicationIdSuffix = ".nightly" }
resValue("string", "app_name", "WG Tunnel Nightly") }
resValue("string", "provider", "\"${Constants.APP_NAME}.provider.nightly\"") }
} flavorDimensions.add(Constants.TYPE)
} productFlavors {
create("fdroid") {
flavorDimensions.add("type") dimension = Constants.TYPE
productFlavors { proguardFile("fdroid-rules.pro")
create("fdroid") { }
dimension = "type" create("general") {
buildConfigField("String", "FLAVOR", "\"fdroid\"") dimension = Constants.TYPE
} }
create("google") { }
dimension = "type" compileOptions {
buildConfigField("String", "FLAVOR", "\"google\"") sourceCompatibility = JavaVersion.VERSION_17
} targetCompatibility = JavaVersion.VERSION_17
create("standalone") { isCoreLibraryDesugaringEnabled = true
dimension = "type" }
buildConfigField("String", "FLAVOR", "\"standalone\"") kotlinOptions { jvmTarget = Constants.JVM_TARGET }
} buildFeatures {
} compose = true
buildConfig = true
compileOptions { }
isCoreLibraryDesugaringEnabled = true packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } }
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlin {
compilerOptions {
jvmTarget = JvmTarget.JVM_17
freeCompilerArgs = listOf("-XXLanguage:+PropertyParamAnnotationDefaultTargetMode")
}
}
buildFeatures {
compose = true
buildConfig = true
}
packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } }
licensee {
allowedLicenses().forEach { allow(it) }
allowedLicenseUrls().forEach { allowUrl(it) }
// foss, but missing license
ignoreDependencies("com.github.T8RIN.QuickieExtended")
}
android.applicationVariants.all {
val variant = this
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
}
}
} }
dependencies { dependencies {
implementation(project(":logcatter"))
implementation(project(":networkmonitor"))
// Core foundations implementation(project(":logcatter"))
implementation(libs.bundles.androidx.core.full)
implementation(libs.bundles.androidx.lifecycle.core)
implementation(libs.bundles.androidx.appcompat)
implementation(libs.bundles.androidx.storage)
// Compose setup implementation(libs.androidx.core.ktx)
implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.bundles.androidx.compose.ui)
implementation(libs.bundles.androidx.compose.material)
implementation(libs.androidx.activity.compose)
// Navigation // helpers for implementing LifecycleOwner in a Service
implementation(libs.bundles.androidx.navigation3) implementation(libs.androidx.lifecycle.service)
implementation(libs.bundles.navigation.lifecycle) implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation(libs.androidx.appcompat)
implementation(libs.material)
// Material and icons // test
implementation(libs.bundles.google.material) testImplementation(libs.junit)
implementation(libs.bundles.material.icons) testImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.compose.ui.test)
androidTestImplementation(libs.androidx.room.testing)
debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.manifest)
// Database // tunnel
implementation(libs.bundles.androidx.room) implementation(libs.tunnel)
implementation(libs.bundles.androidx.datastore) implementation(libs.amneziawg.android)
ksp(libs.androidx.room.compiler) coreLibraryDesugaring(libs.desugar.jdk.libs)
implementation(libs.bundles.androidx.work) // logging
implementation(libs.timber)
// Networking and serialization // compose navigation
implementation(libs.bundles.ktor.client) implementation(libs.androidx.navigation.compose)
implementation(libs.bundles.kotlinx.serialization) implementation(libs.androidx.hilt.navigation.compose)
implementation(libs.ipaddress)
// State management // hilt
implementation(libs.bundles.orbit.mvi) implementation(libs.hilt.android)
ksp(libs.hilt.android.compiler)
ksp(libs.androidx.hilt.compiler)
// Tunnel // accompanist
implementation(libs.bundles.wireguard.tunnel) implementation(libs.accompanist.permissions)
implementation(libs.accompanist.drawablepainter)
// Shizuku // storage
implementation(libs.bundles.shizuku) implementation(libs.androidx.room.runtime)
ksp(libs.androidx.room.compiler)
implementation(libs.androidx.room.ktx)
implementation(libs.androidx.datastore.preferences)
// UI utilities // lifecycle
implementation(libs.bundles.ui.utilities) implementation(libs.lifecycle.runtime.compose)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.process)
// Misc utilities // icons
implementation(libs.bundles.misc.utilities) implementation(libs.material.icons.extended)
coreLibraryDesugaring(libs.desugar.jdk.libs) // serialization
implementation(libs.kotlinx.serialization.json)
// Accompanist // barcode scanning
implementation(libs.bundles.accompanist) implementation(libs.zxing.android.embedded)
// Lifecycle Compose // bio
implementation(libs.lifecycle.runtime.compose) implementation(libs.androidx.biometric.ktx)
implementation(libs.pin.lock.compose)
// Testing // shortcuts
testImplementation(libs.junit) implementation(libs.androidx.core)
testImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.compose.ui.test)
androidTestImplementation(libs.androidx.room.testing)
debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.manifest)
debugImplementation(libs.leakcanary.android) // splash
implementation(libs.androidx.core.splashscreen)
// Room database backup // worker
implementation(libs.roomdatabasebackup) { implementation(libs.androidx.work.runtime)
exclude(group = "org.reactivestreams", module = "reactive-streams") implementation(libs.androidx.hilt.work)
}
// DI
implementation(platform(libs.koin.bom))
implementation(libs.koin.core)
implementation(libs.koin.android)
implementation(libs.koin.compose.viewmodel)
implementation(libs.koin.androidx.compose)
implementation(libs.koin.androidx.navigation)
implementation(libs.koin.lazy)
implementation(libs.koin.worker)
} }
tasks.register<Copy>("copyLicenseeJsonToAssets") { fun determineVersionName(): String {
dependsOn("licensee") return with(getBuildTaskName().lowercase()) {
val outputAssets = layout.projectDirectory.dir("src/main/assets") when {
from(layout.buildDirectory.file("reports/licensee/androidFdroidRelease/artifacts.json")) { contains(Constants.NIGHTLY) || contains(Constants.PRERELEASE) ->
rename("artifacts.json", "licenses.json") Constants.VERSION_NAME +
} "-${grgitService.service.get().grgit.head().abbreviatedId}"
into(outputAssets) else -> Constants.VERSION_NAME
}
}
} }
tasks.named("preBuild") { dependsOn("copyLicenseeJsonToAssets") } val incrementVersionCode by tasks.registering {
doLast {
val versionFile = file("$rootDir/versionCode.txt")
if (versionFile.exists()) {
versionFile.writeText(versionCodeIncrement.toString())
println("Incremented versionCode to $versionCodeIncrement")
}
}
}
// https://gist.github.com/obfusk/61046e09cee352ae6dd109911534b12e#fix-proposed-by-linsui-disable-baseline-profiles
tasks.whenTaskAdded { tasks.whenTaskAdded {
if (name.contains("ArtProfile")) { if (name.startsWith("assemble") && !name.lowercase().contains("debug")) {
enabled = false dependsOn(incrementVersionCode)
} }
} }
+5
View File
@@ -0,0 +1,5 @@
-dontwarn com.google.errorprone.annotations.**
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
<fields>;
}
+24 -1
View File
@@ -1 +1,24 @@
-dontwarn javax.lang.model.** # Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
<fields>;
}
@@ -155,7 +155,9 @@
"columnNames": [ "columnNames": [
"id" "id"
] ]
} },
"indices": [],
"foreignKeys": []
}, },
{ {
"tableName": "TunnelConfig", "tableName": "TunnelConfig",
@@ -225,18 +227,21 @@
"fieldPath": "pingInterval", "fieldPath": "pingInterval",
"columnName": "ping_interval", "columnName": "ping_interval",
"affinity": "INTEGER", "affinity": "INTEGER",
"notNull": false,
"defaultValue": "null" "defaultValue": "null"
}, },
{ {
"fieldPath": "pingCooldown", "fieldPath": "pingCooldown",
"columnName": "ping_cooldown", "columnName": "ping_cooldown",
"affinity": "INTEGER", "affinity": "INTEGER",
"notNull": false,
"defaultValue": "null" "defaultValue": "null"
}, },
{ {
"fieldPath": "pingIp", "fieldPath": "pingIp",
"columnName": "ping_ip", "columnName": "ping_ip",
"affinity": "TEXT", "affinity": "TEXT",
"notNull": false,
"defaultValue": "null" "defaultValue": "null"
}, },
{ {
@@ -270,9 +275,11 @@
"orders": [], "orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)" "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
} }
] ],
"foreignKeys": []
} }
], ],
"views": [],
"setupQueries": [ "setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "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, 'ae51793c4d09ea3194ecd26f0606f35c')" "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ae51793c4d09ea3194ecd26f0606f35c')"
@@ -1,295 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 17,
"identityHash": "380d82359c99933cc9ce783347c4ec31",
"entities": [
{
"tableName": "Settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `is_amnezia_enabled` INTEGER NOT NULL DEFAULT false, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT false, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT false, `is_vpn_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_lan_on_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `debounce_delay_seconds` INTEGER NOT NULL DEFAULT 3, `is_disable_kill_switch_on_trusted_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_unsecure_enabled` INTEGER NOT NULL DEFAULT false, `split_tunnel_apps` TEXT NOT NULL DEFAULT '', `wifi_detection_method` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isAutoTunnelEnabled",
"columnName": "is_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnMobileDataEnabled",
"columnName": "is_tunnel_on_mobile_data_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "trustedNetworkSSIDs",
"columnName": "trusted_network_ssids",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isAlwaysOnVpnEnabled",
"columnName": "is_always_on_vpn_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnEthernetEnabled",
"columnName": "is_tunnel_on_ethernet_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isShortcutsEnabled",
"columnName": "is_shortcuts_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isTunnelOnWifiEnabled",
"columnName": "is_tunnel_on_wifi_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isKernelEnabled",
"columnName": "is_kernel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isRestoreOnBootEnabled",
"columnName": "is_restore_on_boot_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isMultiTunnelEnabled",
"columnName": "is_multi_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isAmneziaEnabled",
"columnName": "is_amnezia_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isWildcardsEnabled",
"columnName": "is_wildcards_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isStopOnNoInternetEnabled",
"columnName": "is_stop_on_no_internet_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isVpnKillSwitchEnabled",
"columnName": "is_vpn_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isKernelKillSwitchEnabled",
"columnName": "is_kernel_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isLanOnKillSwitchEnabled",
"columnName": "is_lan_on_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "debounceDelaySeconds",
"columnName": "debounce_delay_seconds",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "3"
},
{
"fieldPath": "isDisableKillSwitchOnTrustedEnabled",
"columnName": "is_disable_kill_switch_on_trusted_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isTunnelOnUnsecureEnabled",
"columnName": "is_tunnel_on_unsecure_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "splitTunnelApps",
"columnName": "split_tunnel_apps",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "wifiDetectionMethod",
"columnName": "wifi_detection_method",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "TunnelConfig",
"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, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `ping_interval` INTEGER DEFAULT null, `ping_cooldown` INTEGER DEFAULT null, `ping_ip` TEXT DEFAULT null, `is_ethernet_tunnel` INTEGER NOT NULL DEFAULT false, `is_ipv4_preferred` 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": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "pingInterval",
"columnName": "ping_interval",
"affinity": "INTEGER",
"defaultValue": "null"
},
{
"fieldPath": "pingCooldown",
"columnName": "ping_cooldown",
"affinity": "INTEGER",
"defaultValue": "null"
},
{
"fieldPath": "pingIp",
"columnName": "ping_ip",
"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"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_TunnelConfig_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
}
]
}
],
"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, '380d82359c99933cc9ce783347c4ec31')"
]
}
}
@@ -1,302 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 18,
"identityHash": "505728bad740c12bab998a066b569333",
"entities": [
{
"tableName": "Settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `is_amnezia_enabled` INTEGER NOT NULL DEFAULT false, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT false, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT false, `is_vpn_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_lan_on_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `debounce_delay_seconds` INTEGER NOT NULL DEFAULT 3, `is_disable_kill_switch_on_trusted_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_unsecure_enabled` INTEGER NOT NULL DEFAULT false, `split_tunnel_apps` TEXT NOT NULL DEFAULT '', `wifi_detection_method` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isAutoTunnelEnabled",
"columnName": "is_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnMobileDataEnabled",
"columnName": "is_tunnel_on_mobile_data_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "trustedNetworkSSIDs",
"columnName": "trusted_network_ssids",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isAlwaysOnVpnEnabled",
"columnName": "is_always_on_vpn_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnEthernetEnabled",
"columnName": "is_tunnel_on_ethernet_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isShortcutsEnabled",
"columnName": "is_shortcuts_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isTunnelOnWifiEnabled",
"columnName": "is_tunnel_on_wifi_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isKernelEnabled",
"columnName": "is_kernel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isRestoreOnBootEnabled",
"columnName": "is_restore_on_boot_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isMultiTunnelEnabled",
"columnName": "is_multi_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isAmneziaEnabled",
"columnName": "is_amnezia_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isWildcardsEnabled",
"columnName": "is_wildcards_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isStopOnNoInternetEnabled",
"columnName": "is_stop_on_no_internet_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isVpnKillSwitchEnabled",
"columnName": "is_vpn_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isKernelKillSwitchEnabled",
"columnName": "is_kernel_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isLanOnKillSwitchEnabled",
"columnName": "is_lan_on_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "debounceDelaySeconds",
"columnName": "debounce_delay_seconds",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "3"
},
{
"fieldPath": "isDisableKillSwitchOnTrustedEnabled",
"columnName": "is_disable_kill_switch_on_trusted_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isTunnelOnUnsecureEnabled",
"columnName": "is_tunnel_on_unsecure_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "splitTunnelApps",
"columnName": "split_tunnel_apps",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "wifiDetectionMethod",
"columnName": "wifi_detection_method",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "TunnelConfig",
"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, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `ping_interval` INTEGER DEFAULT null, `ping_cooldown` INTEGER DEFAULT null, `ping_ip` 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)",
"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": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "pingInterval",
"columnName": "ping_interval",
"affinity": "INTEGER",
"defaultValue": "null"
},
{
"fieldPath": "pingCooldown",
"columnName": "ping_cooldown",
"affinity": "INTEGER",
"defaultValue": "null"
},
{
"fieldPath": "pingIp",
"columnName": "ping_ip",
"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"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_TunnelConfig_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
}
]
}
],
"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, '505728bad740c12bab998a066b569333')"
]
}
}
@@ -1,316 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 19,
"identityHash": "82bdb96b7a9f8695a34ad1ec21d9aea8",
"entities": [
{
"tableName": "Settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `is_amnezia_enabled` INTEGER NOT NULL DEFAULT false, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT false, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT false, `is_vpn_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_lan_on_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `debounce_delay_seconds` INTEGER NOT NULL DEFAULT 3, `is_disable_kill_switch_on_trusted_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_unsecure_enabled` INTEGER NOT NULL DEFAULT false, `wifi_detection_method` INTEGER NOT NULL DEFAULT 0, `is_ping_monitoring_enabled` INTEGER NOT NULL DEFAULT true, `tunnel_ping_interval_sec` INTEGER NOT NULL DEFAULT 30, `tunnel_ping_attempts` INTEGER NOT NULL DEFAULT 3, `tunnel_ping_timeout_sec` INTEGER)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isAutoTunnelEnabled",
"columnName": "is_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnMobileDataEnabled",
"columnName": "is_tunnel_on_mobile_data_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "trustedNetworkSSIDs",
"columnName": "trusted_network_ssids",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isAlwaysOnVpnEnabled",
"columnName": "is_always_on_vpn_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnEthernetEnabled",
"columnName": "is_tunnel_on_ethernet_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isShortcutsEnabled",
"columnName": "is_shortcuts_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isTunnelOnWifiEnabled",
"columnName": "is_tunnel_on_wifi_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isKernelEnabled",
"columnName": "is_kernel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isRestoreOnBootEnabled",
"columnName": "is_restore_on_boot_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isMultiTunnelEnabled",
"columnName": "is_multi_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isAmneziaEnabled",
"columnName": "is_amnezia_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isWildcardsEnabled",
"columnName": "is_wildcards_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isStopOnNoInternetEnabled",
"columnName": "is_stop_on_no_internet_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isVpnKillSwitchEnabled",
"columnName": "is_vpn_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isKernelKillSwitchEnabled",
"columnName": "is_kernel_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isLanOnKillSwitchEnabled",
"columnName": "is_lan_on_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "debounceDelaySeconds",
"columnName": "debounce_delay_seconds",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "3"
},
{
"fieldPath": "isDisableKillSwitchOnTrustedEnabled",
"columnName": "is_disable_kill_switch_on_trusted_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isTunnelOnUnsecureEnabled",
"columnName": "is_tunnel_on_unsecure_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "wifiDetectionMethod",
"columnName": "wifi_detection_method",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isPingMonitoringEnabled",
"columnName": "is_ping_monitoring_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "true"
},
{
"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"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "TunnelConfig",
"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_TunnelConfig_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
}
]
}
],
"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, '82bdb96b7a9f8695a34ad1ec21d9aea8')"
]
}
}
@@ -1,359 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 20,
"identityHash": "51f828868c0ea2f0f5c987410ff5c5a1",
"entities": [
{
"tableName": "Settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT false, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT false, `is_lan_on_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `debounce_delay_seconds` INTEGER NOT NULL DEFAULT 3, `is_disable_kill_switch_on_trusted_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_unsecure_enabled` INTEGER NOT NULL DEFAULT false, `wifi_detection_method` INTEGER NOT NULL DEFAULT 0, `is_ping_monitoring_enabled` INTEGER NOT NULL DEFAULT true, `tunnel_ping_interval_sec` INTEGER NOT NULL DEFAULT 30, `tunnel_ping_attempts` INTEGER NOT NULL DEFAULT 3, `tunnel_ping_timeout_sec` INTEGER, `app_mode` INTEGER NOT NULL DEFAULT 0, `dns_protocol` INTEGER NOT NULL DEFAULT 0, `dns_endpoint` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isAutoTunnelEnabled",
"columnName": "is_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnMobileDataEnabled",
"columnName": "is_tunnel_on_mobile_data_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "trustedNetworkSSIDs",
"columnName": "trusted_network_ssids",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isAlwaysOnVpnEnabled",
"columnName": "is_always_on_vpn_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnEthernetEnabled",
"columnName": "is_tunnel_on_ethernet_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isShortcutsEnabled",
"columnName": "is_shortcuts_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isTunnelOnWifiEnabled",
"columnName": "is_tunnel_on_wifi_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isRestoreOnBootEnabled",
"columnName": "is_restore_on_boot_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isMultiTunnelEnabled",
"columnName": "is_multi_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isWildcardsEnabled",
"columnName": "is_wildcards_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isStopOnNoInternetEnabled",
"columnName": "is_stop_on_no_internet_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isLanOnKillSwitchEnabled",
"columnName": "is_lan_on_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "debounceDelaySeconds",
"columnName": "debounce_delay_seconds",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "3"
},
{
"fieldPath": "isDisableKillSwitchOnTrustedEnabled",
"columnName": "is_disable_kill_switch_on_trusted_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isTunnelOnUnsecureEnabled",
"columnName": "is_tunnel_on_unsecure_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "wifiDetectionMethod",
"columnName": "wifi_detection_method",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isPingMonitoringEnabled",
"columnName": "is_ping_monitoring_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "true"
},
{
"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": "appMode",
"columnName": "app_mode",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "dnsProtocol",
"columnName": "dns_protocol",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "dnsEndpoint",
"columnName": "dns_endpoint",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "TunnelConfig",
"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_TunnelConfig_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_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 false, `socks5_proxy_bind_address` TEXT, `http_proxy_enable` INTEGER NOT NULL DEFAULT false, `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": "false"
},
{
"fieldPath": "socks5ProxyBindAddress",
"columnName": "socks5_proxy_bind_address",
"affinity": "TEXT"
},
{
"fieldPath": "httpProxyEnabled",
"columnName": "http_proxy_enable",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"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"
]
}
}
],
"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, '51f828868c0ea2f0f5c987410ff5c5a1')"
]
}
}
@@ -1,359 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 21,
"identityHash": "51f828868c0ea2f0f5c987410ff5c5a1",
"entities": [
{
"tableName": "Settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT false, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT false, `is_lan_on_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `debounce_delay_seconds` INTEGER NOT NULL DEFAULT 3, `is_disable_kill_switch_on_trusted_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_unsecure_enabled` INTEGER NOT NULL DEFAULT false, `wifi_detection_method` INTEGER NOT NULL DEFAULT 0, `is_ping_monitoring_enabled` INTEGER NOT NULL DEFAULT true, `tunnel_ping_interval_sec` INTEGER NOT NULL DEFAULT 30, `tunnel_ping_attempts` INTEGER NOT NULL DEFAULT 3, `tunnel_ping_timeout_sec` INTEGER, `app_mode` INTEGER NOT NULL DEFAULT 0, `dns_protocol` INTEGER NOT NULL DEFAULT 0, `dns_endpoint` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isAutoTunnelEnabled",
"columnName": "is_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnMobileDataEnabled",
"columnName": "is_tunnel_on_mobile_data_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "trustedNetworkSSIDs",
"columnName": "trusted_network_ssids",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isAlwaysOnVpnEnabled",
"columnName": "is_always_on_vpn_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnEthernetEnabled",
"columnName": "is_tunnel_on_ethernet_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isShortcutsEnabled",
"columnName": "is_shortcuts_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isTunnelOnWifiEnabled",
"columnName": "is_tunnel_on_wifi_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isRestoreOnBootEnabled",
"columnName": "is_restore_on_boot_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isMultiTunnelEnabled",
"columnName": "is_multi_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isWildcardsEnabled",
"columnName": "is_wildcards_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isStopOnNoInternetEnabled",
"columnName": "is_stop_on_no_internet_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isLanOnKillSwitchEnabled",
"columnName": "is_lan_on_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "debounceDelaySeconds",
"columnName": "debounce_delay_seconds",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "3"
},
{
"fieldPath": "isDisableKillSwitchOnTrustedEnabled",
"columnName": "is_disable_kill_switch_on_trusted_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isTunnelOnUnsecureEnabled",
"columnName": "is_tunnel_on_unsecure_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "wifiDetectionMethod",
"columnName": "wifi_detection_method",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isPingMonitoringEnabled",
"columnName": "is_ping_monitoring_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "true"
},
{
"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": "appMode",
"columnName": "app_mode",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "dnsProtocol",
"columnName": "dns_protocol",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "dnsEndpoint",
"columnName": "dns_endpoint",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "TunnelConfig",
"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_TunnelConfig_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_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 false, `socks5_proxy_bind_address` TEXT, `http_proxy_enable` INTEGER NOT NULL DEFAULT false, `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": "false"
},
{
"fieldPath": "socks5ProxyBindAddress",
"columnName": "socks5_proxy_bind_address",
"affinity": "TEXT"
},
{
"fieldPath": "httpProxyEnabled",
"columnName": "http_proxy_enable",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"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"
]
}
}
],
"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, '51f828868c0ea2f0f5c987410ff5c5a1')"
]
}
}
@@ -1,364 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 22,
"identityHash": "db93d0490401ccbef25ca39f27bafa29",
"entities": [
{
"tableName": "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_always_on_vpn_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL DEFAULT 0, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_wifi_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_ping_enabled` INTEGER NOT NULL DEFAULT 0, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT 0, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT 0, `is_lan_on_kill_switch_enabled` INTEGER NOT NULL DEFAULT 0, `debounce_delay_seconds` INTEGER NOT NULL DEFAULT 3, `is_disable_kill_switch_on_trusted_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_unsecure_enabled` INTEGER NOT NULL DEFAULT 0, `wifi_detection_method` 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, `app_mode` INTEGER NOT NULL DEFAULT 0, `dns_protocol` INTEGER NOT NULL DEFAULT 0, `dns_endpoint` TEXT)",
"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": "isAlwaysOnVpnEnabled",
"columnName": "is_always_on_vpn_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isTunnelOnEthernetEnabled",
"columnName": "is_tunnel_on_ethernet_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isShortcutsEnabled",
"columnName": "is_shortcuts_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isTunnelOnWifiEnabled",
"columnName": "is_tunnel_on_wifi_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": "isPingEnabled",
"columnName": "is_ping_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": "isLanOnKillSwitchEnabled",
"columnName": "is_lan_on_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "debounceDelaySeconds",
"columnName": "debounce_delay_seconds",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "3"
},
{
"fieldPath": "isDisableKillSwitchOnTrustedEnabled",
"columnName": "is_disable_kill_switch_on_trusted_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isTunnelOnUnsecureEnabled",
"columnName": "is_tunnel_on_unsecure_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "wifiDetectionMethod",
"columnName": "wifi_detection_method",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "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": "appMode",
"columnName": "app_mode",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "dnsProtocol",
"columnName": "dns_protocol",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "dnsEndpoint",
"columnName": "dns_endpoint",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "TunnelConfig",
"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_TunnelConfig_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_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"
]
}
}
],
"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, 'db93d0490401ccbef25ca39f27bafa29')"
]
}
}
@@ -1,371 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 23,
"identityHash": "c94fe51e6c318edf8bda81ab854c85e5",
"entities": [
{
"tableName": "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_always_on_vpn_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL DEFAULT 0, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_wifi_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_ping_enabled` INTEGER NOT NULL DEFAULT 0, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT 0, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT 0, `is_lan_on_kill_switch_enabled` INTEGER NOT NULL DEFAULT 0, `debounce_delay_seconds` INTEGER NOT NULL DEFAULT 3, `is_disable_kill_switch_on_trusted_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_unsecure_enabled` INTEGER NOT NULL DEFAULT 0, `wifi_detection_method` 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, `app_mode` INTEGER NOT NULL DEFAULT 0, `dns_protocol` INTEGER NOT NULL DEFAULT 0, `dns_endpoint` TEXT, `is_tunnel_globals_enabled` 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": "isAlwaysOnVpnEnabled",
"columnName": "is_always_on_vpn_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isTunnelOnEthernetEnabled",
"columnName": "is_tunnel_on_ethernet_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isShortcutsEnabled",
"columnName": "is_shortcuts_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isTunnelOnWifiEnabled",
"columnName": "is_tunnel_on_wifi_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": "isPingEnabled",
"columnName": "is_ping_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": "isLanOnKillSwitchEnabled",
"columnName": "is_lan_on_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "debounceDelaySeconds",
"columnName": "debounce_delay_seconds",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "3"
},
{
"fieldPath": "isDisableKillSwitchOnTrustedEnabled",
"columnName": "is_disable_kill_switch_on_trusted_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isTunnelOnUnsecureEnabled",
"columnName": "is_tunnel_on_unsecure_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "wifiDetectionMethod",
"columnName": "wifi_detection_method",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "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": "appMode",
"columnName": "app_mode",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "dnsProtocol",
"columnName": "dns_protocol",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "dnsEndpoint",
"columnName": "dns_endpoint",
"affinity": "TEXT"
},
{
"fieldPath": "isTunnelGlobalsEnabled",
"columnName": "is_tunnel_globals_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "TunnelConfig",
"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_TunnelConfig_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_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"
]
}
}
],
"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, 'c94fe51e6c318edf8bda81ab854c85e5')"
]
}
}
@@ -1,463 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 24,
"identityHash": "545fe5e4cfa7f19ec10911ab5c603339",
"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, `is_lan_on_kill_switch_enabled` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isShortcutsEnabled",
"columnName": "is_shortcuts_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isRestoreOnBootEnabled",
"columnName": "is_restore_on_boot_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isMultiTunnelEnabled",
"columnName": "is_multi_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "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": "isLanOnKillSwitchEnabled",
"columnName": "is_lan_on_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "auto_tunnel_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL DEFAULT 0, `trusted_network_ssids` TEXT NOT NULL DEFAULT '', `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT 0, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT 0, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT 0, `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)",
"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"
}
],
"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"
]
}
}
],
"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, '545fe5e4cfa7f19ec10911ab5c603339')"
]
}
}
@@ -1,477 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 25,
"identityHash": "2ea437642cca24af74dc57904899909a",
"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, `is_lan_on_kill_switch_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": "isLanOnKillSwitchEnabled",
"columnName": "is_lan_on_kill_switch_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"
]
}
}
],
"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, '2ea437642cca24af74dc57904899909a')"
]
}
}
@@ -1,509 +0,0 @@
{
"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')"
]
}
}
@@ -1,523 +0,0 @@
{
"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')"
]
}
}
@@ -1,523 +0,0 @@
{
"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')"
]
}
}
@@ -1,523 +0,0 @@
{
"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')"
]
}
}
@@ -13,10 +13,10 @@ import org.junit.runner.RunWith
*/ */
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest { class ExampleInstrumentedTest {
@Test @Test
fun useAppContext() { fun useAppContext() {
// Context of the app under test. // Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.zaneschepke.wireguardautotunnel", appContext.packageName) assertEquals("com.zaneschepke.wireguardautotunnel", appContext.packageName)
} }
} }
@@ -4,33 +4,41 @@ import androidx.room.testing.MigrationTestHelper
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import com.zaneschepke.wireguardautotunnel.data.AppDatabase import com.zaneschepke.wireguardautotunnel.data.AppDatabase
import java.io.IOException import com.zaneschepke.wireguardautotunnel.data.Queries
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import java.io.IOException
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class MigrationTest { class MigrationTest {
private val dbName = "migration-test" private val dbName = "migration-test"
@get:Rule @get:Rule
val helper: MigrationTestHelper = val helper: MigrationTestHelper =
MigrationTestHelper(InstrumentationRegistry.getInstrumentation(), AppDatabase::class.java) MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
AppDatabase::class.java,
)
@Test @Test
@Throws(IOException::class) @Throws(IOException::class)
fun migrate6To7() { fun migrate6To7() {
helper.createDatabase(dbName, 6).apply { helper.createDatabase(dbName, 6).apply {
// Database has schema version 1. Insert some data using SQL queries. // Database has schema version 1. Insert some data using SQL queries.
// You can't use DAO classes because they expect the latest schema. // You can't use DAO classes because they expect the latest schema.
// Prepare for the next version. execSQL(Queries.createDefaultSettings())
close() execSQL(
} Queries.createTunnelConfig(),
)
// Prepare for the next version.
close()
}
// Re-open the database with version 2 and provide // Re-open the database with version 2 and provide
// MIGRATION_1_2 as the migration process. // MIGRATION_1_2 as the migration process.
helper.runMigrationsAndValidate(dbName, 7, true) helper.runMigrationsAndValidate(dbName, 7, true)
// MigrationTestHelper automatically verifies the schema changes, // MigrationTestHelper automatically verifies the schema changes,
// but you need to validate that the data was migrated properly. // but you need to validate that the data was migrated properly.
} }
} }
-4
View File
@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#648DB3</color>
</resources>
+49 -114
View File
@@ -2,29 +2,29 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!--for split tunneling--> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!--foreground service special use for non VPN service tunnels, android 14--> <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" /> <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<!--foreground service special use for VPN service tunnels, android 14--> <!--foreground service exempt android 14-->
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"
tools:ignore="ProtectedPermissions" />
<!--foreground service permissions--> <!--foreground service permissions-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<!--start service on boot permission--> <!--start service on boot permission-->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<!--android tv support-->
<permission <permission
android:name="${applicationId}.permission.CONTROL_TUNNELS" android:name="${applicationId}.permission.CONTROL_TUNNELS"
android:label="@string/app_permission_title"
android:description="@string/app_permission_description"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:protectionLevel="dangerous" /> android:protectionLevel="dangerous" />
@@ -46,10 +46,16 @@
<uses-feature android:name="android.hardware.wifi" <uses-feature android:name="android.hardware.wifi"
android:required="false"/> android:required="false"/>
<queries>
<intent>
<action android:name="android.intent.action.MAIN" />
</intent>
</queries>
<application <application
android:name=".WireGuardAutoTunnel" android:name=".WireGuardAutoTunnel"
android:allowBackup="false" android:allowBackup="false"
android:banner="@mipmap/ic_banner" android:banner="@drawable/ic_banner"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:enableOnBackInvokedCallback="true" android:enableOnBackInvokedCallback="true"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
@@ -59,7 +65,6 @@
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.App.Start" android:theme="@style/Theme.App.Start"
tools:targetApi="tiramisu"> tools:targetApi="tiramisu">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
@@ -67,12 +72,6 @@
android:theme="@style/Theme.WireguardAutoTunnel" android:theme="@style/Theme.WireguardAutoTunnel"
android:configChanges="orientation|screenSize|keyboardHidden" android:configChanges="orientation|screenSize|keyboardHidden"
> >
<intent-filter>
<action android:name="android.intent.action.APPLICATION_PREFERENCES" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.SHOW_APP_INFO" /> <action android:name="android.intent.action.SHOW_APP_INFO" />
@@ -82,11 +81,15 @@
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" /> <action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name="com.journeyapps.barcodescanner.CaptureActivity"
android:screenOrientation="portrait"
tools:replace="screenOrientation" />
<activity <activity
android:name=".core.shortcut.ShortcutsActivity" android:name=".core.shortcut.ShortcutsActivity"
android:enabled="true" android:enabled="true"
android:exported="false" android:exported="true"
android:noHistory="true" android:noHistory="true"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:finishOnTaskLaunch="true" android:finishOnTaskLaunch="true"
@@ -103,20 +106,17 @@
android:resource="@xml/file_paths" /> android:resource="@xml/file_paths" />
</provider> </provider>
<provider <provider
android:name="androidx.startup.InitializationProvider" android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup" android:authorities="${applicationId}.androidx-startup"
android:exported="false" android:multiprocess="true"
tools:node="merge"> tools:node="remove">
<meta-data </provider>
android:name="androidx.work.WorkManagerInitializer"
android:value="androidx.startup"
tools:node="remove" />
</provider>
<service <service
android:name=".core.service.tile.TunnelControlTile" android:name=".core.service.tile.TunnelControlTile"
android:exported="true" android:exported="true"
android:icon="@drawable/ic_notification" android:icon="@drawable/ic_launcher"
android:label="@string/tunnel_control" android:label="@string/tunnel_control"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"> android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<meta-data <meta-data
@@ -133,41 +133,7 @@
<service <service
android:name=".core.service.tile.AutoTunnelControlTile" android:name=".core.service.tile.AutoTunnelControlTile"
android:exported="true" android:exported="true"
android:icon="@drawable/ic_notification" android:icon="@drawable/ic_launcher"
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"
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:label="@string/auto_tunnel"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"> android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<meta-data <meta-data
@@ -185,39 +151,16 @@
android:name=".core.service.autotunnel.AutoTunnelService" android:name=".core.service.autotunnel.AutoTunnelService"
android:enabled="true" android:enabled="true"
android:exported="false" android:exported="false"
android:foregroundServiceType="specialUse" android:foregroundServiceType="systemExempted"
android:persistent="true" android:persistent="true"
android:stopWithTask="false" android:stopWithTask="false"
tools:node="merge"> tools:node="merge" />
<property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="This service monitors network changes to automatically
establish and maintain WireGuard VPN tunnels on demand, ensuring seamless connectivity.
It requires persistent foreground execution to detect real-time events,
which cannot be achieved with standard background APIs due to timing and reliability needs for
network connectivity monitoring."/>
</service>
<service
android:name=".core.service.TunnelForegroundService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="specialUse"
android:persistent="true"
android:stopWithTask="false"
tools:node="merge">
<property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="This service sustains non-VpnService virtual tunnels (using gVisor/netstack for
isolated networking), keeping connections alive for continuous secure data routing.
Persistent foreground operation is essential to handle
low-level tunnel maintenance and avoid interruptions, beyond the capabilities of other
service types or background work."/>
</service>
<service <service
android:name=".core.service.VpnForegroundService" android:name=".core.service.TunnelForegroundService"
android:exported="false" android:exported="false"
android:persistent="true" android:persistent="true"
android:foregroundServiceType="systemExempted" android:foregroundServiceType="systemExempted"
android:permission="android.permission.BIND_VPN_SERVICE"> android:permission="android.permission.BIND_VPN_SERVICE">
<intent-filter> <intent-filter>
<action android:name="android.net.VpnService" /> <action android:name="android.net.VpnService" />
@@ -225,19 +168,25 @@
</service> </service>
<receiver <receiver
android:name=".core.broadcast.RestartReceiver" android:name=".core.broadcast.BootReceiver"
android:enabled="true" android:enabled="true"
android:exported="false"> android:exported="false">
<intent-filter> <intent-filter>
<category android:name="android.intent.category.DEFAULT" />
<action android:name="android.intent.action.BOOT_COMPLETED" /> <action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.ACTION_BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" /> <action android:name="android.intent.action.QUICKBOOT_POWERON" />
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" /> <action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
</receiver> </receiver>
<receiver
android:name=".core.broadcast.AppUpdateReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
</receiver>
<receiver <receiver
android:name=".core.broadcast.KernelReceiver" android:name=".core.broadcast.KernelReceiver"
android:exported="false" android:exported="false"
@@ -246,20 +195,6 @@
<action android:name="com.wireguard.android.action.REFRESH_TUNNEL_STATES" /> <action android:name="com.wireguard.android.action.REFRESH_TUNNEL_STATES" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<!--custom security solution for easier user integration-->
<receiver
android:name=".core.broadcast.RemoteControlReceiver"
android:enabled="true"
android:exported="true" tools:ignore="ExportedReceiver">
<intent-filter>
<action android:name="com.zaneschepke.wireguardautotunnel.START_TUNNEL" />
<action android:name="com.zaneschepke.wireguardautotunnel.STOP_TUNNEL" />
<action android:name="com.zaneschepke.wireguardautotunnel.START_AUTO_TUNNEL" />
<action android:name="com.zaneschepke.wireguardautotunnel.STOP_AUTO_TUNNEL" />
<action android:name="com.zaneschepke.wireguardautotunnel.START_KILL_SWITCH" />
<action android:name="com.zaneschepke.wireguardautotunnel.STOP_KILL_SWITCH" />
</intent-filter>
</receiver>
<receiver <receiver
android:name=".core.broadcast.NotificationActionReceiver" android:name=".core.broadcast.NotificationActionReceiver"
android:exported="false" android:exported="false"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 16 KiB

@@ -1,583 +1,267 @@
package com.zaneschepke.wireguardautotunnel package com.zaneschepke.wireguardautotunnel
import ProxySettingsScreen
import android.content.Intent import android.content.Intent
import android.graphics.Color import android.graphics.Color
import android.net.VpnService
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.activity.SystemBarStyle import androidx.activity.SystemBarStyle
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.material.icons.Icons
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material.icons.rounded.Home
import androidx.compose.material.icons.rounded.QuestionMark
import androidx.compose.material.icons.rounded.Settings
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarData
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextLinkStyles
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.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.unit.dp
import androidx.compose.ui.zIndex
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope import androidx.navigation.compose.NavHost
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator import androidx.navigation.compose.composable
import androidx.navigation3.runtime.entryProvider import androidx.navigation.compose.rememberNavController
import androidx.navigation3.runtime.rememberNavBackStack import androidx.navigation.toRoute
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager
import androidx.navigation3.ui.NavDisplay import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.networkmonitor.NetworkMonitor import com.zaneschepke.wireguardautotunnel.core.worker.ServiceWorker
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
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.AppStateRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
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.CustomSnackBar
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarInfo import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarControllerProvider
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarType import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigScreen
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.rememberCustomSnackbarState import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route import com.zaneschepke.wireguardautotunnel.ui.screens.main.OptionsScreen
import com.zaneschepke.wireguardautotunnel.ui.navigation.Tab import com.zaneschepke.wireguardautotunnel.ui.screens.main.PinLockScreen
import com.zaneschepke.wireguardautotunnel.ui.navigation.components.BottomNavbar import com.zaneschepke.wireguardautotunnel.ui.screens.main.ScannerScreen
import com.zaneschepke.wireguardautotunnel.ui.navigation.components.DynamicTopAppBar import com.zaneschepke.wireguardautotunnel.ui.screens.main.SplitTunnelScreen
import com.zaneschepke.wireguardautotunnel.ui.navigation.components.currentRouteAsNavbarState import com.zaneschepke.wireguardautotunnel.ui.screens.main.TunnelAutoTunnelScreen
import com.zaneschepke.wireguardautotunnel.ui.navigation.functions.rememberNavController import com.zaneschepke.wireguardautotunnel.ui.screens.settings.AdvancedScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.AutoTunnelScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.AppearanceScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.advanced.AutoTunnelAdvancedScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.DisplayScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.detection.WifiDetectionMethodScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.KillSwitchScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.disclosure.LocationDisclosureScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.LanguageScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.preferred.PreferredTunnelScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.LocationDisclosureScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.wifi.WifiSettingsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.pin.PinLockScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.AppearanceScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.AutoTunnelScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.display.DisplayScreen import com.zaneschepke.wireguardautotunnel.ui.screens.support.LogsScreen
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.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
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.donate.DonateScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.donate.crypto.AddressesScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.license.LicenseScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.TunnelsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.config.ConfigScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.settings.TunnelSettingsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.sort.SortScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.splittunnel.SplitTunnelScreen
import com.zaneschepke.wireguardautotunnel.ui.theme.AlertRed
import com.zaneschepke.wireguardautotunnel.ui.theme.OffWhite
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.installApk import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl import dagger.hilt.android.AndroidEntryPoint
import com.zaneschepke.wireguardautotunnel.util.extensions.restartApp import javax.inject.Inject
import com.zaneschepke.wireguardautotunnel.util.extensions.showToast import kotlin.system.exitProcess
import com.zaneschepke.wireguardautotunnel.viewmodel.ConfigViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.SplitTunnelViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelViewModel
import de.raphaelebner.roomdatabasebackup.core.RoomBackup
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import org.koin.androidx.compose.koinViewModel
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
import xyz.teamgravity.pin_lock_compose.PinManager
@AndroidEntryPoint
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private val appStateRepository: AppStateRepository by inject() @Inject
private val tunnelRepository: TunnelRepository by inject() lateinit var appStateRepository: AppStateRepository
private val appDatabase: AppDatabase by inject()
private val networkMonitor: NetworkMonitor by inject()
val viewModel by viewModel<SharedAppViewModel>() @Inject
private lateinit var roomBackup: RoomBackup lateinit var tunnelManager: TunnelManager
@OptIn(ExperimentalMaterial3Api::class) @Inject
override fun onCreate(savedInstanceState: Bundle?) { lateinit var shortcutManager: ShortcutManager
enableEdgeToEdge(
statusBarStyle = SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT),
navigationBarStyle = SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT),
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
window.isNavigationBarContrastEnforced = false
}
super.onCreate(savedInstanceState)
roomBackup = RoomBackup(this) override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge(
statusBarStyle = SystemBarStyle.Companion.auto(Color.TRANSPARENT, Color.TRANSPARENT),
navigationBarStyle = SystemBarStyle.Companion.auto(Color.TRANSPARENT, Color.TRANSPARENT),
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
window.isNavigationBarContrastEnforced = false
}
super.onCreate(savedInstanceState)
installSplashScreen().apply { val viewModel by viewModels<AppViewModel>()
setKeepOnScreenCondition { !viewModel.container.stateFlow.value.isAppLoaded }
}
setContent { installSplashScreen().apply {
val context = LocalContext.current setKeepOnScreenCondition {
val isTv = isRunningOnTv() !viewModel.isAppReady.value
val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle() }
val scope = rememberCoroutineScope() }
LaunchedEffect(uiState.isAppLoaded) { setContent {
if (uiState.isAppLoaded) { val appUiState by viewModel.uiState.collectAsStateWithLifecycle()
uiState.locale.let { LocaleUtil.changeLocale(it) } val configurationChange by viewModel.configurationChange.collectAsStateWithLifecycle()
} val navController = rememberNavController()
}
val snackbarState = rememberCustomSnackbarState() LaunchedEffect(configurationChange) {
var showVpnPermissionDialog by remember { mutableStateOf(false) } if (configurationChange) {
var vpnPermissionDenied by remember { mutableStateOf(false) } Intent(this@MainActivity, MainActivity::class.java).also {
var requestingAppMode by remember { startActivity(it)
mutableStateOf<Pair<AppMode?, TunnelConfig?>>(Pair(null, null)) exitProcess(0)
} }
}
}
val startingStack = buildList { LaunchedEffect(Unit) {
add(Route.Tunnels) viewModel.getEmitSplitTunnelApps(this@MainActivity)
if (intent?.action == Intent.ACTION_APPLICATION_PREFERENCES) add(Route.Settings) }
if (uiState.pinLockEnabled) add(Route.Lock)
}
val backStack = rememberNavBackStack(*startingStack.toTypedArray()) LaunchedEffect(appUiState.autoTunnelActive) {
var previousRoute by remember { mutableStateOf<Route?>(null) } requestAutoTunnelTileServiceUpdate()
}
val navController = with(appUiState.appSettings) {
rememberNavController( LaunchedEffect(isAutoTunnelEnabled) {
backStack, this@MainActivity.requestAutoTunnelTileServiceUpdate()
uiState.isLocationDisclosureShown, }
onChange = { previousKey -> previousRoute = previousKey as? Route }, LaunchedEffect(isShortcutsEnabled) {
onExitApp = { finish() }, if (!isShortcutsEnabled) return@LaunchedEffect shortcutManager.removeShortcuts()
) shortcutManager.addShortcuts()
}
}
val vpnActivity = ServiceWorker.start(this)
rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult(),
onResult = {
if (it.resultCode != RESULT_OK) {
showVpnPermissionDialog = true
vpnPermissionDenied = true
} else {
vpnPermissionDenied = false
showVpnPermissionDialog = false
val (appMode, config) = requestingAppMode
when (appMode) {
AppMode.VPN -> if (config != null) viewModel.startTunnel(config)
AppMode.LOCK_DOWN -> viewModel.setAppMode(AppMode.LOCK_DOWN)
else -> Unit
}
}
requestingAppMode = Pair(null, null)
},
)
LaunchedEffect(Unit) { CompositionLocalProvider(LocalNavController provides navController) {
viewModel.globalSideEffect.collectLatest { sideEffect -> SnackbarControllerProvider { host ->
when (sideEffect) { WireguardAutoTunnelTheme(theme = appUiState.generalState.theme) {
GlobalSideEffect.ConfigChanged -> restartApp() Scaffold(
GlobalSideEffect.PopBackStack -> navController.pop() contentWindowInsets = WindowInsets(0),
is GlobalSideEffect.RequestVpnPermission -> { snackbarHost = {
requestingAppMode = Pair(sideEffect.requestingMode, sideEffect.config) SnackbarHost(host) { snackbarData: SnackbarData ->
vpnActivity.launch(VpnService.prepare(this@MainActivity)) CustomSnackBar(
} snackbarData.visuals.message,
isRtl = false,
is GlobalSideEffect.Snackbar -> { containerColor =
scope.launch { MaterialTheme.colorScheme.surfaceColorAtElevation(
snackbarState.showSnackbar( 2.dp,
SnackbarInfo( ),
message = )
buildAnnotatedString { }
append(sideEffect.message.asString(context)) },
}, bottomBar = {
type = sideEffect.type ?: SnackbarType.INFO, BottomNavBar(
durationMs = sideEffect.durationMs ?: 4000L, navController,
) listOf(
) BottomNavItem(
} name = stringResource(R.string.tunnels),
} route = Route.Main,
icon = Icons.Rounded.Home,
is GlobalSideEffect.Toast -> ),
scope.launch { context.showToast(sideEffect.message.asString(context)) } BottomNavItem(
name = stringResource(R.string.settings),
is GlobalSideEffect.LaunchUrl -> context.openWebUrl(sideEffect.url) route = Route.Settings,
is GlobalSideEffect.InstallApk -> context.installApk(sideEffect.apk) icon = Icons.Rounded.Settings,
} ),
} BottomNavItem(
} name = stringResource(R.string.support),
route = Route.Support,
if (!uiState.isAppLoaded) return@setContent icon = Icons.Rounded.QuestionMark,
),
var showLock by remember { ),
mutableStateOf(uiState.pinLockEnabled && !uiState.isPinVerified) )
} },
LaunchedEffect(uiState.isPinVerified) { if (uiState.isPinVerified) showLock = false } ) { padding ->
Box(modifier = Modifier.Companion.fillMaxSize().padding(padding)) {
CompositionLocalProvider( NavHost(
LocalIsAndroidTV provides isTv, navController,
LocalNavController provides navController, enterTransition = { fadeIn(tween(Constants.TRANSITION_ANIMATION_TIME)) },
) { exitTransition = { fadeOut(tween(Constants.TRANSITION_ANIMATION_TIME)) },
WireguardAutoTunnelTheme(theme = uiState.theme) { startDestination = (if (appUiState.generalState.isPinLockEnabled) Route.Lock else Route.Main),
VpnDeniedDialog( ) {
showVpnPermissionDialog, composable<Route.Main> {
onDismiss = { MainScreen(
showVpnPermissionDialog = false uiState = appUiState,
vpnPermissionDenied = false )
}, }
) composable<Route.Settings> {
SettingsScreen(
val annotatedMessage = buildAnnotatedString { appViewModel = viewModel,
append(context.getString(R.string.donation_prompt_prefix)) uiState = appUiState,
append(" ") )
withLink( }
LinkAnnotation.Clickable( composable<Route.LocationDisclosure> {
tag = context.getString(R.string.support), LocationDisclosureScreen(viewModel, appUiState)
styles = }
TextLinkStyles( composable<Route.AutoTunnel> {
style = AutoTunnelScreen(
SpanStyle( appUiState.appSettings,
textDecoration = TextDecoration.Underline, )
color = MaterialTheme.colorScheme.primary, }
), composable<Route.Appearance> {
focusedStyle = AppearanceScreen()
SpanStyle( }
textDecoration = TextDecoration.Underline, composable<Route.Language> {
color = MaterialTheme.colorScheme.primary, LanguageScreen(appUiState, viewModel)
background = }
MaterialTheme.colorScheme.primary.copy( composable<Route.Display> {
alpha = 0.2f DisplayScreen(appUiState)
), }
), composable<Route.Support> {
), SupportScreen(appUiState, viewModel)
) { }
snackbarState.dismissCurrent() composable<Route.AutoTunnelAdvanced> {
navController.push(Route.Donate) AdvancedScreen(appUiState.appSettings, viewModel)
} }
) { composable<Route.Logs> {
append(context.getString(R.string.donation_prompt_link)) LogsScreen()
} }
append(" ") composable<Route.Config> {
append(context.getString(R.string.donation_prompt_suffix)) val args = it.toRoute<Route.Config>()
} val config =
appUiState.tunnels.firstOrNull { it.id == args.id }
LaunchedEffect(Unit) { ConfigScreen(config, viewModel)
if (uiState.shouldShowDonationSnackbar && !uiState.alreadyDonated) { }
viewModel.setShouldShowDonationSnackbar(false) composable<Route.TunnelOptions> {
snackbarState.showSnackbar( val args = it.toRoute<Route.TunnelOptions>()
SnackbarInfo( val config = appUiState.tunnels.first { it.id == args.id }
message = annotatedMessage, OptionsScreen(config)
type = SnackbarType.THANK_YOU, }
durationMs = 30_000L, composable<Route.Lock> {
) PinLockScreen(viewModel)
) }
} composable<Route.Scanner> {
} ScannerScreen()
}
if (showLock) { composable<Route.KillSwitch> {
PinManager.initialize(context = this@MainActivity) KillSwitchScreen(appUiState, viewModel)
PinLockScreen() }
} else { composable<Route.SplitTunnel> {
val currentRoute by remember { val args = it.toRoute<Route.SplitTunnel>()
derivedStateOf { backStack.lastOrNull() as? Route } val config = appUiState.tunnels.first { it.id == args.id }
} SplitTunnelScreen(config, viewModel)
val currentTab by remember { }
derivedStateOf { Tab.fromRoute(currentRoute ?: Route.Tunnels) } composable<Route.TunnelAutoTunnel> {
} val args = it.toRoute<Route.TunnelOptions>()
val navState by val config = appUiState.tunnels.first { it.id == args.id }
currentRouteAsNavbarState( TunnelAutoTunnelScreen(config, appUiState.appSettings)
uiState, }
viewModel, }
currentRoute, BackHandler {
navController, if (navController.previousBackStackEntry == null || !navController.popBackStack()) {
) this@MainActivity.finish()
}
Box(modifier = Modifier.fillMaxSize()) { }
if (uiState.appMode == AppMode.LOCK_DOWN) { }
AppAlertBanner( }
stringResource(R.string.locked_down) }
.uppercase(Locale.current.platformLocale), }
OffWhite, }
AlertRed, }
modifier = Modifier.fillMaxWidth().zIndex(2f), }
)
}
Scaffold(
snackbarHost = {
snackbarState.SnackbarHost(
modifier =
Modifier.align(Alignment.BottomCenter)
.padding(
bottom =
if (LocalIsAndroidTV.current) 120.dp
else 80.dp
)
) { info ->
CustomSnackBar(
message = info.message,
type = info.type,
onDismiss = { snackbarState.dismissCurrent() },
containerColor =
MaterialTheme.colorScheme.surfaceColorAtElevation(
2.dp
),
modifier =
Modifier.wrapContentHeight(align = Alignment.Top),
)
}
},
topBar = { DynamicTopAppBar(navState) },
bottomBar = {
if (navState.showBottomItems) {
BottomNavbar(
uiState.isAutoTunnelActive,
currentTab,
onTabSelected = { tab ->
navController.popUpTo(tab.startRoute)
},
)
}
},
) { padding ->
Column(
modifier =
Modifier.fillMaxSize()
.background(MaterialTheme.colorScheme.surface)
.padding(
top = padding.calculateTopPadding().plus(8.dp),
bottom = padding.calculateBottomPadding(),
)
.consumeWindowInsets(padding)
.imePadding()
) {
NavDisplay(
backStack = backStack,
modifier = Modifier.fillMaxSize(),
onBack = { navController.pop() },
transitionSpec = {
val initialIndex =
previousRoute?.let(Tab::fromRoute)?.index ?: 0
val targetIndex =
currentRoute?.let(Tab::fromRoute)?.index ?: 0
if (initialIndex != targetIndex) {
val dir = if (targetIndex > initialIndex) 1 else -1
(slideInHorizontally { dir * it } +
fadeIn()) togetherWith
(slideOutHorizontally { dir * -it } + fadeOut())
} else {
(slideInHorizontally { it } + fadeIn()) togetherWith
(slideOutHorizontally { -it } + fadeOut())
}
},
popTransitionSpec = {
(slideInHorizontally { -it } + fadeIn()) togetherWith
(slideOutHorizontally { it } + fadeOut())
},
predictivePopTransitionSpec = {
(slideInHorizontally { -it } + fadeIn()) togetherWith
(slideOutHorizontally { it } + fadeOut())
},
entryDecorators =
listOf(
rememberSaveableStateHolderNavEntryDecorator(),
rememberViewModelStoreNavEntryDecorator(),
),
entryProvider =
entryProvider {
entry<Route.Lock> {
PinManager.initialize(
context = this@MainActivity
)
PinLockScreen()
}
entry<Route.Tunnels> { TunnelsScreen() }
entry<Route.Sort> { SortScreen() }
entry<Route.TunnelSettings> { key ->
val viewModel: TunnelViewModel =
koinViewModel(
parameters = { parametersOf(key.id) }
)
TunnelSettingsScreen(viewModel)
}
entry<Route.SplitTunnel> { key ->
val viewModel: SplitTunnelViewModel =
koinViewModel(
parameters = { parametersOf(key.id) }
)
SplitTunnelScreen(viewModel)
}
entry<Route.Config> { key ->
val viewModel: ConfigViewModel =
koinViewModel(
parameters = { parametersOf(key.id) }
)
ConfigScreen(viewModel)
}
entry<Route.LocationDisclosure> {
LocationDisclosureScreen()
}
entry<Route.AutoTunnel> { AutoTunnelScreen() }
entry<Route.WifiPreferences> {
WifiSettingsScreen()
}
entry<Route.AdvancedAutoTunnel> {
AutoTunnelAdvancedScreen()
}
entry<Route.WifiDetectionMethod> {
WifiDetectionMethodScreen()
}
entry<Route.Settings> { SettingsScreen() }
entry<Route.TunnelMonitoring> {
TunnelMonitoringScreen()
}
entry<Route.AndroidIntegrations> {
AndroidIntegrationsScreen()
}
entry<Route.Dns> { DnsSettingsScreen() }
entry<Route.ConfigGlobal> { key ->
val viewModel: ConfigViewModel =
koinViewModel(
parameters = { parametersOf(key.id) }
)
ConfigScreen(viewModel)
}
entry<Route.SplitTunnelGlobal> { key ->
val viewModel: SplitTunnelViewModel =
koinViewModel(
parameters = { parametersOf(key.id) }
)
SplitTunnelScreen(viewModel)
}
entry<Route.LockdownSettings> {
LockdownSettingsScreen()
}
entry<Route.ProxySettings> { ProxySettingsScreen() }
entry<Route.Appearance> { AppearanceScreen() }
entry<Route.Language> { LanguageScreen() }
entry<Route.Display> { DisplayScreen() }
entry<Route.Logs> { LogsScreen() }
entry<Route.Support> { SupportScreen() }
entry<Route.License> { LicenseScreen() }
entry<Route.Donate> { DonateScreen() }
entry<Route.Addresses> { AddressesScreen() }
entry<Route.PreferredTunnel> { key ->
PreferredTunnelScreen(key.tunnelNetwork)
}
entry<Route.PingTarget> { PingTargetScreen() }
},
)
}
}
}
}
}
}
}
}
override fun onResume() {
super.onResume()
networkMonitor.checkPermissionsAndUpdateState()
WireGuardAutoTunnel.setUiActive(true)
}
override fun onPause() {
super.onPause()
WireGuardAutoTunnel.setUiActive(false)
}
fun performBackup() =
lifecycleScope.launch {
// reset active tuns before backup to prevent trying to start them without permission on
// restore
tunnelRepository.resetActiveTunnels()
roomBackup
.database(appDatabase)
.backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG)
.enableLogDebug(true)
.maxFileCount(5)
.apply {
onCompleteListener { success, _, _ ->
lifecycleScope.launch {
if (success) {
showToast(
getString(
R.string.backup_success,
getString(R.string.restarting_app),
)
)
restartApp()
} else {
showToast(R.string.backup_failed)
}
}
}
}
.backup()
}
fun performRestore() =
lifecycleScope.launch {
roomBackup
.database(appDatabase)
.enableLogDebug(true)
.backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG)
.apply {
onCompleteListener { success, _, _ ->
lifecycleScope.launch {
if (success) {
showToast(
getString(
R.string.restore_success,
getString(R.string.restarting_app),
)
)
restartApp()
} else {
showToast(R.string.restore_failed)
}
}
}
}
.restore()
}
} }
@@ -2,109 +2,137 @@ package com.zaneschepke.wireguardautotunnel
import android.app.Application import android.app.Application
import android.os.StrictMode import android.os.StrictMode
import android.os.StrictMode.ThreadPolicy
import androidx.hilt.work.HiltWorkerFactory
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.work.Configuration
import com.wireguard.android.backend.GoBackend
import com.zaneschepke.logcatter.LogReader import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationMonitor import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.Dispatcher import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.Scope import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.di.appModule import com.zaneschepke.wireguardautotunnel.di.MainDispatcher
import com.zaneschepke.wireguardautotunnel.di.databaseModule import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.di.dispatchersModule import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.di.networkModule import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
import com.zaneschepke.wireguardautotunnel.di.tunnelModule
import com.zaneschepke.wireguardautotunnel.di.workerModule
import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject import kotlinx.coroutines.withContext
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.androidx.workmanager.koin.workManagerFactory
import org.koin.core.component.KoinComponent
import org.koin.core.context.GlobalContext.startKoin
import org.koin.core.lazyModules
import org.koin.core.option.viewModelScopeFactory
import org.koin.core.qualifier.named
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject
class WireGuardAutoTunnel : Application(), KoinComponent { @HiltAndroidApp
class WireGuardAutoTunnel : Application(), Configuration.Provider {
private val applicationScope: CoroutineScope by inject(named(Scope.APPLICATION)) @Inject
private val ioDispatcher: CoroutineDispatcher by inject(named(Dispatcher.IO)) lateinit var workerFactory: HiltWorkerFactory
private val logReader: LogReader by inject()
private val monitoringRepository: MonitoringSettingsRepository by inject() override val workManagerConfiguration: Configuration
private val notificationMonitor: NotificationMonitor by inject() get() = Configuration.Builder()
.setWorkerFactory(workerFactory)
.build()
override fun onCreate() { @Inject
super.onCreate() @ApplicationScope
startKoin { lateinit var applicationScope: CoroutineScope
androidContext(this@WireGuardAutoTunnel)
if (BuildConfig.DEBUG) androidLogger()
workManagerFactory()
modules(dispatchersModule, appModule, databaseModule, tunnelModule, workerModule)
options(viewModelScopeFactory())
lazyModules(networkModule)
}
instance = this
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyLog()
.penaltyFlashScreen()
.build()
)
StrictMode.setVmPolicy(StrictMode.VmPolicy.Builder().detectAll().penaltyLog().build())
} else {
Timber.plant(ReleaseTree())
}
applicationScope.launch(ioDispatcher) { @Inject
launch { lateinit var logReader: LogReader
monitoringRepository.flow
.distinctUntilChangedBy { it.isLocalLogsEnabled }
.collect { settings ->
if (settings.isLocalLogsEnabled) {
logReader.start()
} else {
logReader.stop()
}
}
}
launch { notificationMonitor.handleApplicationNotifications() }
}
}
companion object { @Inject
private val _uiActive = MutableStateFlow(false) lateinit var appDataRepository: AppDataRepository
val uiActive: StateFlow<Boolean> @Inject
get() = _uiActive @IoDispatcher
lateinit var ioDispatcher: CoroutineDispatcher
fun setUiActive(active: Boolean) { @Inject
_uiActive.update { active } @MainDispatcher
} lateinit var mainDispatcher: CoroutineDispatcher
@Volatile private var lastActiveTunnels: List<Int> = emptyList() @Inject
lateinit var tunnelManager: TunnelManager
@Synchronized override fun onCreate() {
fun getLastActiveTunnels(): List<Int> { super.onCreate()
return lastActiveTunnels instance = this
} ProcessLifecycleOwner.get().lifecycle.addObserver(AppLifecycleObserver())
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
StrictMode.setThreadPolicy(
ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()
.penaltyLog()
.build(),
)
} else {
Timber.plant(ReleaseTree())
}
@Synchronized GoBackend.setAlwaysOnCallback {
fun setLastActiveTunnels(newTunnels: List<Int>) { applicationScope.launch {
lastActiveTunnels = newTunnels val settings = appDataRepository.settings.get()
} if (settings.isAlwaysOnVpnEnabled) {
val tunnel = appDataRepository.getPrimaryOrFirstTunnel()
tunnel?.let {
tunnelManager.startTunnel(it)
}
} else {
Timber.Forest.w("Always-on VPN is not enabled in app settings")
}
}
}
lateinit var instance: WireGuardAutoTunnel applicationScope.launch {
private set withContext(mainDispatcher) {
} if (appDataRepository.appState.isLocalLogsEnabled() && !isRunningOnTv()) logReader.initialize()
}
if (!appDataRepository.settings.get().isKernelEnabled) {
tunnelManager.setBackendState(BackendState.SERVICE_ACTIVE, emptyList())
}
appDataRepository.appState.getLocale()?.let {
withContext(mainDispatcher) {
LocaleUtil.changeLocale(it)
}
}
}
}
override fun onTerminate() {
applicationScope.launch {
tunnelManager.setBackendState(BackendState.SERVICE_ACTIVE, emptyList())
}
super.onTerminate()
}
class AppLifecycleObserver : DefaultLifecycleObserver {
override fun onStart(owner: LifecycleOwner) {
Timber.d("Application entered foreground")
foreground = true
}
override fun onPause(owner: LifecycleOwner) {
Timber.d("Application entered background")
foreground = false
}
}
companion object {
private var foreground = false
fun isForeground(): Boolean {
return foreground
}
lateinit var instance: WireGuardAutoTunnel
private set
}
} }
@@ -0,0 +1,45 @@
package com.zaneschepke.wireguardautotunnel.core.broadcast
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint
class AppUpdateReceiver : BroadcastReceiver() {
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
@Inject
lateinit var appDataRepository: AppDataRepository
@Inject
lateinit var serviceManager: ServiceManager
@Inject
lateinit var tunnelManager: TunnelManager
override fun onReceive(context: Context, intent: Intent) {
if (intent.action != Intent.ACTION_MY_PACKAGE_REPLACED) return
serviceManager.updateTunnelTile()
serviceManager.updateAutoTunnelTile()
applicationScope.launch {
with(appDataRepository.settings.get()) {
if (isRestoreOnBootEnabled) {
// If auto tunnel is enabled, just start it and let auto tunnel start appropriate tun
if (isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) return@launch serviceManager.startAutoTunnel(true)
tunnelManager.restorePreviousState()
}
}
}
}
}
@@ -0,0 +1,44 @@
package com.zaneschepke.wireguardautotunnel.core.broadcast
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint
class BootReceiver : BroadcastReceiver() {
@Inject
lateinit var appDataRepository: AppDataRepository
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
@Inject
lateinit var serviceManager: ServiceManager
@Inject
lateinit var tunnelManager: TunnelManager
override fun onReceive(context: Context, intent: Intent) {
if (Intent.ACTION_BOOT_COMPLETED != intent.action) return
serviceManager.updateTunnelTile()
serviceManager.updateAutoTunnelTile()
applicationScope.launch {
with(appDataRepository.settings.get()) {
if (isRestoreOnBootEnabled) {
// If auto tunnel is enabled, just start it and let auto tunnel start appropriate tun
if (isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) return@launch serviceManager.startAutoTunnel(true)
tunnelManager.restorePreviousState()
}
}
}
}
}
@@ -3,34 +3,47 @@ package com.zaneschepke.wireguardautotunnel.core.broadcast
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.Scope
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent import javax.inject.Inject
import org.koin.core.component.inject
import org.koin.core.qualifier.named
class KernelReceiver : BroadcastReceiver(), KoinComponent { @AndroidEntryPoint
class KernelReceiver : BroadcastReceiver() {
private val applicationScope: CoroutineScope by inject(named(Scope.APPLICATION)) @Inject
private val tunnelRepository: TunnelRepository by inject() @ApplicationScope
private val tunnelManager: TunnelManager by inject() lateinit var applicationScope: CoroutineScope
override fun onReceive(context: Context, intent: Intent) { @Inject
val action = intent.action ?: return lateinit var tunnelRepository: TunnelRepository
applicationScope.launch {
if (action == REFRESH_TUNNELS_ACTION) {
tunnelManager.runningTunnelNames().forEach { name ->
val tunnel = tunnelRepository.findByTunnelName(name)
tunnel?.let { tunnelRepository.save(it.copy(isActive = true)) }
}
}
}
}
companion object { @Inject
const val REFRESH_TUNNELS_ACTION = "com.wireguard.android.action.REFRESH_TUNNEL_STATES" lateinit var serviceManager: ServiceManager
}
@Inject
lateinit var tunnelManager: TunnelManager
override fun onReceive(context: Context, intent: Intent) {
val action = intent.action ?: return
applicationScope.launch {
if (action == REFRESH_TUNNELS_ACTION) {
tunnelManager.runningTunnelNames().forEach {
val tunnel = tunnelRepository.findByTunnelName(it)
tunnel?.let {
tunnelRepository.save(it.copy(isActive = true))
}
}
serviceManager.updateTunnelTile()
}
}
}
companion object {
const val REFRESH_TUNNELS_ACTION = "com.wireguard.android.action.REFRESH_TUNNEL_STATES"
}
} }
@@ -4,39 +4,43 @@ import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.Scope
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent import javax.inject.Inject
import org.koin.core.component.get
import org.koin.core.component.inject
import org.koin.core.qualifier.named
class NotificationActionReceiver : BroadcastReceiver(), KoinComponent { @AndroidEntryPoint
class NotificationActionReceiver : BroadcastReceiver() {
private val tunnelManager: TunnelManager by inject() @Inject
private val autoTunnelRepository: AutoTunnelSettingsRepository by inject() lateinit var serviceManager: ServiceManager
private val applicationScope: CoroutineScope = get(named(Scope.APPLICATION))
override fun onReceive(context: Context, intent: Intent) { @Inject
applicationScope.launch { lateinit var tunnelManager: TunnelManager
when (intent.action) {
NotificationAction.AUTO_TUNNEL_OFF.name ->
autoTunnelRepository.updateAutoTunnelEnabled(false)
NotificationAction.TUNNEL_OFF.name -> {
val tunnelId = intent.getIntExtra(NotificationManager.EXTRA_ID, 0)
if (tunnelId == STOP_ALL_TUNNELS_ID)
return@launch tunnelManager.stopActiveTunnels()
tunnelManager.stopTunnel(tunnelId)
}
}
}
}
companion object { @Inject
const val STOP_ALL_TUNNELS_ID = 0 lateinit var tunnelRepository: TunnelRepository
}
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
override fun onReceive(context: Context, intent: Intent) {
applicationScope.launch {
when (intent.action) {
NotificationAction.AUTO_TUNNEL_OFF.name -> serviceManager.stopAutoTunnel()
NotificationAction.TUNNEL_OFF.name -> {
val tunnelId = intent.getIntExtra(NotificationManager.EXTRA_ID, 0)
if (tunnelId == 0) return@launch tunnelManager.stopTunnel()
val tunnel = tunnelRepository.getById(tunnelId)
tunnelManager.stopTunnel(tunnel)
}
}
}
}
} }
@@ -1,93 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.broadcast
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.Scope
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.util.Constants
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.koin.core.qualifier.named
import timber.log.Timber
class RemoteControlReceiver : BroadcastReceiver(), KoinComponent {
private val applicationScope: CoroutineScope by inject(named(Scope.APPLICATION))
private val settingsRepository: GeneralSettingRepository by inject()
private val tunnelsRepository: TunnelRepository by inject()
private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository by inject()
private val tunnelManager: TunnelManager by inject()
enum class Action(private val suffix: String) {
START_TUNNEL("START_TUNNEL"),
STOP_TUNNEL("STOP_TUNNEL"),
START_AUTO_TUNNEL("START_AUTO_TUNNEL"),
STOP_AUTO_TUNNEL("STOP_AUTO_TUNNEL");
fun getFullAction(): String {
return "${Constants.BASE_PACKAGE}.$suffix"
}
companion object {
fun fromAction(action: String): Action? {
for (a in entries) {
if (a.getFullAction() == action) {
return a
}
}
return null
}
}
}
override fun onReceive(context: Context, intent: Intent) {
Timber.i("onReceive")
val action = intent.action ?: return
val appAction = Action.fromAction(action) ?: return Timber.w("Unknown action $action")
applicationScope.launch {
val settings = settingsRepository.getGeneralSettings()
if (!settings.isRemoteControlEnabled) return@launch Timber.w("Remote control disabled")
val key = settings.remoteKey ?: return@launch Timber.w("Remote control key missing")
if (key != intent.getStringExtra(EXTRA_KEY)?.trim())
return@launch Timber.w("Invalid remote control key")
when (appAction) {
Action.START_TUNNEL -> {
val tunnelName =
intent.getStringExtra(EXTRA_TUN_NAME) ?: return@launch startDefaultTunnel()
val tunnel =
tunnelsRepository.findByTunnelName(tunnelName)
?: return@launch startDefaultTunnel()
tunnelManager.startTunnel(tunnel)
}
Action.STOP_TUNNEL -> {
val tunnelName =
intent.getStringExtra(EXTRA_TUN_NAME)
?: return@launch tunnelManager.stopActiveTunnels()
val tunnel =
tunnelsRepository.findByTunnelName(tunnelName)
?: return@launch tunnelManager.stopActiveTunnels()
tunnelManager.stopTunnel(tunnel.id)
}
Action.START_AUTO_TUNNEL ->
autoTunnelSettingsRepository.updateAutoTunnelEnabled(true)
Action.STOP_AUTO_TUNNEL ->
autoTunnelSettingsRepository.updateAutoTunnelEnabled(false)
}
}
}
private suspend fun startDefaultTunnel() {
tunnelsRepository.getDefaultTunnel()?.let { tunnel -> tunnelManager.startTunnel(tunnel) }
}
companion object {
const val EXTRA_TUN_NAME = "tunnelName"
const val EXTRA_KEY = "key"
}
}
@@ -1,46 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.broadcast
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.Scope
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import org.koin.core.component.inject
import org.koin.core.qualifier.named
import timber.log.Timber
class RestartReceiver : BroadcastReceiver(), KoinComponent {
private val applicationScope: CoroutineScope = get(named(Scope.APPLICATION))
private val tunnelManager: TunnelManager by inject()
private val appStateRepository: AppStateRepository by inject()
private val logReader: LogReader by inject()
override fun onReceive(context: Context, intent: Intent) {
Timber.d("RestartReceiver triggered with action: ${intent.action}")
applicationScope.launch {
when (intent.action) {
Intent.ACTION_BOOT_COMPLETED,
"android.intent.action.QUICKBOOT_POWERON",
"com.htc.intent.action.QUICKBOOT_POWERON" -> {
tunnelManager.handleReboot()
}
Intent.ACTION_MY_PACKAGE_REPLACED -> {
Timber.i("Restoring state on package upgrade")
tunnelManager.handleRestore()
logReader.deleteAndClearLogs()
appStateRepository.setShouldShowDonationSnackbar(true)
}
}
}
}
}
@@ -0,0 +1,161 @@
package com.zaneschepke.wireguardautotunnel.core.network
import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.net.wifi.SupplicantState
import android.net.wifi.WifiManager
import android.os.Build
import com.zaneschepke.wireguardautotunnel.domain.state.ConnectivityState
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.flowOn
import javax.inject.Inject
class InternetConnectivityMonitor
@Inject
constructor(
@ApplicationContext private val context: Context,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : NetworkMonitor {
private val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
@get:Synchronized @set:Synchronized
private var wifiCapabilities: NetworkCapabilities? = null
@get:Synchronized @set:Synchronized
private var wifiNetworkChanged: Boolean = false
override val didWifiChangeSinceLastCapabilitiesQuery: Boolean
get() = wifiNetworkChanged
override val status = callbackFlow {
var wifiState: Boolean = false
var ethernetState: Boolean = false
var cellularState: Boolean = false
fun emitState() {
trySend(ConnectivityState(wifiState, ethernetState, cellularState))
}
val currentNetwork = connectivityManager.activeNetwork
if (currentNetwork == null) {
emitState()
}
fun updateCapabilityState(up: Boolean, network: Network) {
with(connectivityManager.getNetworkCapabilities(network)) {
when {
this == null -> return
hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> wifiState = up
hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ->
cellularState = up
hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) ->
ethernetState = up
}
}
}
fun onWifiChange(network: Network, callback: () -> Unit) {
if (connectivityManager.getNetworkCapabilities(network)?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true) {
callback()
}
}
fun onAvailable(network: Network) {
onWifiChange(network) {
wifiNetworkChanged = true
}
}
fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
onWifiChange(network) {
wifiCapabilities = networkCapabilities
}
updateCapabilityState(true, network)
emitState()
}
val networkStatusCallback =
when (Build.VERSION.SDK_INT) {
in Build.VERSION_CODES.S..Int.MAX_VALUE -> {
object :
ConnectivityManager.NetworkCallback(
FLAG_INCLUDE_LOCATION_INFO,
) {
override fun onAvailable(network: Network) {
onAvailable(network)
}
override fun onLost(network: Network) {
updateCapabilityState(false, network)
emitState()
}
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
onCapabilitiesChanged(network, networkCapabilities)
}
}
}
else -> {
object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
onAvailable(network)
}
override fun onLost(network: Network) {
updateCapabilityState(false, network)
emitState()
}
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
onCapabilitiesChanged(network, networkCapabilities)
}
}
}
}
val request =
NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
.build()
connectivityManager.registerNetworkCallback(request, networkStatusCallback)
awaitClose { connectivityManager.unregisterNetworkCallback(networkStatusCallback) }
}.flowOn(ioDispatcher)
override fun getWifiCapabilities(): NetworkCapabilities? {
wifiNetworkChanged = false
return wifiCapabilities
}
companion object {
fun getNetworkName(networkCapabilities: NetworkCapabilities, context: Context): String? {
var ssid = networkCapabilities.getWifiName()
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S) {
val wifiManager =
context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
@Suppress("DEPRECATION")
val info = wifiManager.connectionInfo
if (info.supplicantState === SupplicantState.COMPLETED) {
ssid = info.ssid
}
}
return ssid?.trim('"')
}
}
}
@@ -0,0 +1,16 @@
package com.zaneschepke.wireguardautotunnel.core.network
import android.net.NetworkCapabilities
import android.net.wifi.WifiInfo
import android.os.Build
fun NetworkCapabilities.getWifiName(): String? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val info: WifiInfo
if (transportInfo is WifiInfo) {
info = transportInfo as WifiInfo
return info.ssid
}
}
return null
}
@@ -0,0 +1,13 @@
package com.zaneschepke.wireguardautotunnel.core.network
import android.net.NetworkCapabilities
import com.zaneschepke.wireguardautotunnel.domain.state.ConnectivityState
import kotlinx.coroutines.flow.Flow
interface NetworkMonitor {
val status: Flow<ConnectivityState>
// util to help limit location queries
val didWifiChangeSinceLastCapabilitiesQuery: Boolean
fun getWifiCapabilities(): NetworkCapabilities?
}
@@ -4,59 +4,44 @@ import android.app.Notification
import android.app.NotificationManager import android.app.NotificationManager
import android.content.Context import android.content.Context
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification.NotificationChannels
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification.NotificationChannels
import com.zaneschepke.wireguardautotunnel.util.StringValue import com.zaneschepke.wireguardautotunnel.util.StringValue
interface NotificationManager { interface NotificationManager {
val context: Context val context: Context
fun createNotification(
channel: NotificationChannels,
title: String = "",
actions: Collection<NotificationCompat.Action> = emptyList(),
description: String = "",
showTimestamp: Boolean = false,
importance: Int = NotificationManager.IMPORTANCE_HIGH,
onGoing: Boolean = true,
onlyAlertOnce: Boolean = true,
): Notification
fun createNotification( fun createNotification(
channel: NotificationChannels, channel: NotificationChannels,
title: String = "", title: StringValue,
actions: Collection<NotificationCompat.Action> = emptyList(), actions: Collection<NotificationCompat.Action> = emptyList(),
description: String = "", description: StringValue,
showTimestamp: Boolean = true, showTimestamp: Boolean = false,
importance: Int = NotificationManager.IMPORTANCE_LOW, importance: Int = NotificationManager.IMPORTANCE_HIGH,
onGoing: Boolean = false, onGoing: Boolean = true,
onlyAlertOnce: Boolean = true, onlyAlertOnce: Boolean = true,
groupKey: String? = null, ): Notification
isGroupSummary: Boolean = false,
): Notification
fun createNotification( fun createNotificationAction(notificationAction: NotificationAction, extraId: Int? = null): NotificationCompat.Action
channel: NotificationChannels,
title: StringValue,
actions: Collection<NotificationCompat.Action> = emptyList(),
description: StringValue,
showTimestamp: Boolean = true,
importance: Int = NotificationManager.IMPORTANCE_LOW,
onGoing: Boolean = false,
onlyAlertOnce: Boolean = true,
groupKey: String? = null,
isGroupSummary: Boolean = false,
): Notification
fun createNotificationAction( fun remove(notificationId: Int)
notificationAction: NotificationAction,
extraId: Int? = null,
): NotificationCompat.Action
fun remove(notificationId: Int) fun show(notificationId: Int, notification: Notification)
fun show(notificationId: Int, notification: Notification) companion object {
const val KERNEL_SERVICE_NOTIFICATION_ID = 123
companion object { const val AUTO_TUNNEL_NOTIFICATION_ID = 122
const val VPN_GROUP_KEY = "VPN_GROUP" const val VPN_NOTIFICATION_ID = 100
const val AUTO_TUNNEL_GROUP_KEY = "AUTO_TUNNEL_GROUP" const val EXTRA_ID = "id"
const val AUTO_TUNNEL_LOCATION_PERMISSION_ID = 123 }
const val AUTO_TUNNEL_LOCATION_SERVICES_ID = 124
// For auto tunnel foreground notification
const val AUTO_TUNNEL_NOTIFICATION_ID = 122
// for tunnel foreground notification
const val VPN_NOTIFICATION_ID = 100
const val TUNNEL_ERROR_NOTIFICATION_ID = 101
const val TUNNEL_MESSAGES_NOTIFICATION_ID = 102
const val EXTRA_ID = "id"
}
} }
@@ -1,61 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.notification
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.util.StringValue
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
class NotificationMonitor(
private val tunnelManager: TunnelManager,
private val notificationManager: NotificationManager,
) {
suspend fun handleApplicationNotifications() = coroutineScope {
launch { handleTunnelErrors() }
launch { handleTunnelMessages() }
}
private suspend fun handleTunnelErrors() =
tunnelManager.errorEvents.collectLatest { (tunName, error) ->
if (!WireGuardAutoTunnel.uiActive.value) {
val notification =
notificationManager.createNotification(
WireGuardNotification.NotificationChannels.VPN,
title =
tunName?.let { StringValue.DynamicString(it) }
?: StringValue.StringResource(R.string.tunnel),
description =
StringValue.StringResource(
R.string.tunnel_error_template,
error.stringRes,
),
groupKey = NotificationManager.VPN_GROUP_KEY,
)
notificationManager.show(
NotificationManager.TUNNEL_ERROR_NOTIFICATION_ID,
notification,
)
}
}
private suspend fun handleTunnelMessages() =
tunnelManager.messageEvents.collectLatest { (tunName, message) ->
if (!WireGuardAutoTunnel.uiActive.value) {
val notification =
notificationManager.createNotification(
WireGuardNotification.NotificationChannels.VPN,
title =
tunName?.let { StringValue.DynamicString(it) }
?: StringValue.StringResource(R.string.tunnel),
description = message.toStringValue(),
groupKey = NotificationManager.VPN_GROUP_KEY,
)
notificationManager.show(
NotificationManager.TUNNEL_MESSAGES_NOTIFICATION_ID,
notification,
)
}
}
}
@@ -3,170 +3,167 @@ package com.zaneschepke.wireguardautotunnel.core.notification
import android.Manifest import android.Manifest
import android.app.Notification import android.app.Notification
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.graphics.Color
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import com.zaneschepke.wireguardautotunnel.MainActivity
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.core.broadcast.NotificationActionReceiver import com.zaneschepke.wireguardautotunnel.core.broadcast.NotificationActionReceiver
import com.zaneschepke.wireguardautotunnel.MainActivity
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager.Companion.EXTRA_ID import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager.Companion.EXTRA_ID
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
import com.zaneschepke.wireguardautotunnel.util.StringValue import com.zaneschepke.wireguardautotunnel.util.StringValue
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
class WireGuardNotification(override val context: Context) : NotificationManager { class WireGuardNotification
@Inject
constructor(
@ApplicationContext override val context: Context,
) : com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager {
enum class NotificationChannels { enum class NotificationChannels {
VPN, VPN,
AUTO_TUNNEL, AUTO_TUNNEL,
} }
private val notificationManager = NotificationManagerCompat.from(context) private val notificationManager = NotificationManagerCompat.from(context)
override fun createNotification( override fun createNotification(
channel: NotificationChannels, channel: NotificationChannels,
title: String, title: String,
actions: Collection<NotificationCompat.Action>, actions: Collection<NotificationCompat.Action>,
description: String, description: String,
showTimestamp: Boolean, showTimestamp: Boolean,
importance: Int, importance: Int,
onGoing: Boolean, onGoing: Boolean,
onlyAlertOnce: Boolean, onlyAlertOnce: Boolean,
groupKey: String?, ): Notification {
isGroupSummary: Boolean, notificationManager.createNotificationChannel(channel.asChannel())
): Notification { return channel.asBuilder().apply {
notificationManager.createNotificationChannel(channel.asChannel(importance)) actions.forEach {
return channel addAction(it)
.asBuilder() }
.apply { setContentTitle(title)
actions.forEach { addAction(it) } setContentIntent(
setContentTitle(title) PendingIntent.getActivity(
setContentIntent( context,
PendingIntent.getActivity( 0,
context, Intent(context, MainActivity::class.java),
0, PendingIntent.FLAG_IMMUTABLE,
Intent(context, MainActivity::class.java) ),
.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), )
PendingIntent.FLAG_IMMUTABLE, setContentText(description)
) setOnlyAlertOnce(onlyAlertOnce)
) setOngoing(onGoing)
setContentText(description) setPriority(NotificationCompat.PRIORITY_HIGH)
setOnlyAlertOnce(onlyAlertOnce) setShowWhen(showTimestamp)
setOngoing(onGoing) setSmallIcon(R.drawable.ic_launcher)
setPriority(NotificationCompat.PRIORITY_LOW) }.build()
setShowWhen(showTimestamp) }
setSmallIcon(R.drawable.ic_notification)
if (groupKey != null) {
setGroup(groupKey)
if (isGroupSummary) {
setGroupSummary(true)
}
}
}
.build()
}
override fun createNotification( override fun createNotification(
channel: NotificationChannels, channel: NotificationChannels,
title: StringValue, title: StringValue,
actions: Collection<NotificationCompat.Action>, actions: Collection<NotificationCompat.Action>,
description: StringValue, description: StringValue,
showTimestamp: Boolean, showTimestamp: Boolean,
importance: Int, importance: Int,
onGoing: Boolean, onGoing: Boolean,
onlyAlertOnce: Boolean, onlyAlertOnce: Boolean,
groupKey: String?, ): Notification {
isGroupSummary: Boolean, return createNotification(
): Notification { channel,
return createNotification( title.asString(context),
channel, actions,
title.asString(context), description.asString(context),
actions, showTimestamp,
description.asString(context), importance,
showTimestamp, onGoing,
importance, onlyAlertOnce,
onGoing, )
onlyAlertOnce, }
)
}
override fun createNotificationAction( override fun createNotificationAction(notificationAction: NotificationAction, extraId: Int?): NotificationCompat.Action {
notificationAction: NotificationAction, val pendingIntent = PendingIntent.getBroadcast(
extraId: Int?, context,
): NotificationCompat.Action { 0,
val pendingIntent = Intent(context, NotificationActionReceiver::class.java).apply {
PendingIntent.getBroadcast( action = notificationAction.name
context, if (extraId != null) putExtra(EXTRA_ID, extraId)
extraId ?: 0, },
Intent(context, NotificationActionReceiver::class.java).apply { PendingIntent.FLAG_IMMUTABLE,
action = notificationAction.name )
if (extraId != null) putExtra(EXTRA_ID, extraId) return NotificationCompat.Action.Builder(
}, R.drawable.ic_launcher,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, notificationAction.title(context).uppercase(),
) pendingIntent,
return NotificationCompat.Action.Builder( ).build()
R.drawable.ic_notification, }
notificationAction.title(context).uppercase(),
pendingIntent,
)
.build()
}
override fun remove(notificationId: Int) { override fun remove(notificationId: Int) {
notificationManager.cancel(notificationId) notificationManager.cancel(notificationId)
} }
override fun show(notificationId: Int, notification: Notification) { override fun show(notificationId: Int, notification: Notification) {
with(notificationManager) { with(notificationManager) {
if ( if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.checkSelfPermission( return
context, }
Manifest.permission.POST_NOTIFICATIONS, notify(notificationId, notification)
) != PackageManager.PERMISSION_GRANTED }
) { }
return
}
notify(notificationId, notification)
}
}
private fun NotificationChannels.asBuilder(): NotificationCompat.Builder { private fun NotificationChannels.asBuilder(): NotificationCompat.Builder {
return when (this) { return when (this) {
NotificationChannels.AUTO_TUNNEL -> { NotificationChannels.AUTO_TUNNEL -> {
NotificationCompat.Builder( NotificationCompat.Builder(
context, context,
context.getString(R.string.auto_tunnel_channel_id), context.getString(R.string.auto_tunnel_channel_id),
) )
} }
NotificationChannels.VPN -> { NotificationChannels.VPN -> {
NotificationCompat.Builder(context, context.getString(R.string.vpn_channel_id)) NotificationCompat.Builder(
} context,
} context.getString(R.string.vpn_channel_id),
} )
}
}
}
private fun NotificationChannels.asChannel(importance: Int): NotificationChannel { private fun NotificationChannels.asChannel(): NotificationChannel {
return when (this) { return when (this) {
NotificationChannels.VPN -> { NotificationChannels.VPN -> {
NotificationChannel( NotificationChannel(
context.getString(R.string.vpn_channel_id), context.getString(R.string.vpn_channel_id),
context.getString(R.string.vpn_channel_name), context.getString(R.string.vpn_channel_name),
importance, NotificationManager.IMPORTANCE_HIGH,
) ).apply {
.apply { description = context.getString(R.string.vpn_channel_description) } description = context.getString(R.string.vpn_channel_description)
} enableLights(true)
NotificationChannels.AUTO_TUNNEL -> { lightColor = Color.WHITE
NotificationChannel( enableVibration(false)
context.getString(R.string.auto_tunnel_channel_id), vibrationPattern = longArrayOf(100, 200, 300)
context.getString(R.string.auto_tunnel_channel_name), }
importance, }
) NotificationChannels.AUTO_TUNNEL -> {
.apply { NotificationChannel(
description = context.getString(R.string.auto_tunnel_channel_description) context.getString(R.string.auto_tunnel_channel_id),
} context.getString(R.string.auto_tunnel_channel_name),
} NotificationManager.IMPORTANCE_HIGH,
} ).apply {
} description = context.getString(R.string.auto_tunnel_channel_description)
enableLights(true)
lightColor = Color.WHITE
enableVibration(false)
vibrationPattern = longArrayOf(100, 200, 300)
}
}
}
}
} }
@@ -1,163 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.service
import android.app.Notification
import android.content.Intent
import android.os.IBinder
import androidx.core.app.ServiceCompat
import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.Dispatcher
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.util.extensions.distinctByKeys
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import org.koin.core.qualifier.named
import timber.log.Timber
abstract class BaseTunnelForegroundService : LifecycleService(), TunnelService {
private val notificationManager: NotificationManager by inject()
private val serviceManager: ServiceManager by inject()
private val tunnelManager: TunnelManager by inject()
private val ioDispatcher: CoroutineDispatcher by inject(named(Dispatcher.IO))
private val settingsRepository: GeneralSettingRepository by inject()
private val tunnelsRepository: TunnelRepository by inject()
protected abstract val fgsType: Int
override fun onBind(intent: Intent): IBinder {
super.onBind(intent)
return LocalBinder(this)
}
override fun onCreate() {
super.onCreate()
ServiceCompat.startForeground(
this,
NotificationManager.VPN_NOTIFICATION_ID,
onCreateNotification(),
fgsType,
)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
ServiceCompat.startForeground(
this,
NotificationManager.VPN_NOTIFICATION_ID,
onCreateNotification(),
fgsType,
)
if (
intent == null ||
intent.component == null ||
(intent.component?.packageName != this.packageName)
) {
Timber.d("Service started by Always-on VPN feature")
lifecycleScope.launch {
val settings = settingsRepository.getGeneralSettings()
if (settings.isAlwaysOnVpnEnabled) {
val tunnel = tunnelsRepository.getDefaultTunnel()
tunnel?.let { tunnelManager.startTunnel(it) }
} else {
Timber.w("Always-on VPN is not enabled in app settings")
}
}
} else {
start()
}
return START_STICKY
}
override fun start() {
lifecycleScope.launch(ioDispatcher) {
tunnelManager.activeTunnels.distinctByKeys().collect { activeTunnels ->
val activeTunConfigs = activeTunnels.keys
val tunnels = tunnelsRepository.getAll()
val activeConfigs = tunnels.filter { activeTunConfigs.contains(it.id) }
updateServiceNotification(activeConfigs)
}
}
}
// TODO Would be cool to have this include kill switch
private fun updateServiceNotification(activeConfigs: List<TunnelConfig>) {
val notification =
when (activeConfigs.size) {
0 -> onCreateNotification()
1 -> createTunnelNotification(activeConfigs.first())
else -> createTunnelsNotification()
}
ServiceCompat.startForeground(
this,
NotificationManager.VPN_NOTIFICATION_ID,
notification,
fgsType,
)
}
override fun stop() {
Timber.d("Stop called")
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
stopSelf()
}
override fun onDestroy() {
serviceManager.handleTunnelServiceDestroy()
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
Timber.d("onDestroy")
super.onDestroy()
}
private fun createTunnelNotification(tunnelConfig: TunnelConfig): Notification {
return notificationManager.createNotification(
WireGuardNotification.NotificationChannels.VPN,
title = "${getString(R.string.tunnel_running)} - ${tunnelConfig.name}",
actions =
listOf(
notificationManager.createNotificationAction(
NotificationAction.TUNNEL_OFF,
tunnelConfig.id,
)
),
onGoing = true,
groupKey = NotificationManager.VPN_GROUP_KEY,
isGroupSummary = true,
)
}
private fun createTunnelsNotification(): Notification {
return notificationManager.createNotification(
WireGuardNotification.NotificationChannels.VPN,
title = "${getString(R.string.tunnel_running)} - ${getString(R.string.multiple)}",
actions =
listOf(
notificationManager.createNotificationAction(NotificationAction.TUNNEL_OFF, 0)
),
groupKey = NotificationManager.VPN_GROUP_KEY,
isGroupSummary = true,
)
}
private fun onCreateNotification(): Notification {
return notificationManager.createNotification(
WireGuardNotification.NotificationChannels.VPN,
title = getString(R.string.tunnel_starting),
groupKey = NotificationManager.VPN_GROUP_KEY,
isGroupSummary = true,
)
}
}
@@ -1,11 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.service
import android.os.Binder
import java.lang.ref.WeakReference
class LocalBinder(service: TunnelService) : Binder() {
private val serviceRef = WeakReference(service)
val service: TunnelService?
get() = serviceRef.get()
}
@@ -1,192 +1,119 @@
package com.zaneschepke.wireguardautotunnel.core.service package com.zaneschepke.wireguardautotunnel.core.service
import android.content.ComponentName import android.app.Service
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.ServiceConnection import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import android.net.VpnService
import android.os.IBinder
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelService import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelService
import com.zaneschepke.wireguardautotunnel.data.model.AppMode import com.zaneschepke.wireguardautotunnel.core.service.tile.AutoTunnelControlTile
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository import com.zaneschepke.wireguardautotunnel.core.service.tile.TunnelControlTile
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
import jakarta.inject.Inject
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import timber.log.Timber import timber.log.Timber
class ServiceManager( @OptIn(ExperimentalCoroutinesApi::class)
private val context: Context, class ServiceManager
ioDispatcher: CoroutineDispatcher, @Inject constructor(private val context: Context, private val ioDispatcher: CoroutineDispatcher, private val appDataRepository: AppDataRepository) {
applicationScope: CoroutineScope,
private val mainDispatcher: CoroutineDispatcher,
private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
) {
private val autoTunnelMutex = Mutex() private val _autoTunnelActive = MutableStateFlow(false)
private val tunnelMutex = Mutex()
private val _tunnelService = MutableStateFlow<TunnelService?>(null) val autoTunnelActive = _autoTunnelActive.asStateFlow()
private val _autoTunnelService = MutableStateFlow<AutoTunnelService?>(null)
val autoTunnelService = _autoTunnelService.asStateFlow()
val tunnelService = _tunnelService.asStateFlow()
init { var autoTunnelService = CompletableDeferred<AutoTunnelService>()
applicationScope.launch(ioDispatcher) { var backgroundService = CompletableDeferred<TunnelForegroundService>()
_autoTunnelService var autoTunnelTile = CompletableDeferred<AutoTunnelControlTile>()
.onEach { _ -> withContext(mainDispatcher) { updateAutoTunnelTile() } } var tunnelControlTile = CompletableDeferred<TunnelControlTile>()
.launchIn(this)
}
applicationScope.launch(ioDispatcher) {
combine(
autoTunnelSettingsRepository.flow
.map { it.isAutoTunnelEnabled }
.distinctUntilChanged(),
_autoTunnelService,
) { enabled, service ->
enabled to (service != null)
}
.collect { (enabled, isRunning) ->
when {
enabled && !isRunning -> {
autoTunnelMutex.withLock { startServiceInternal() }
}
!enabled && isRunning -> {
autoTunnelMutex.withLock { stopServiceInternal() }
}
}
}
}
}
private val tunnelServiceConnection = private fun <T : Service> startService(cls: Class<T>, background: Boolean) {
object : ServiceConnection { runCatching {
override fun onServiceConnected(name: ComponentName, service: IBinder) { val intent = Intent(context, cls)
val binder = service as? LocalBinder if (background) {
_tunnelService.update { binder?.service } context.startForegroundService(intent)
val serviceClass = } else {
when { context.startService(intent)
name.className.contains("VpnForegroundService") -> "VpnForegroundService" }
name.className.contains("TunnelForegroundService") -> }.onFailure { Timber.e(it) }
"TunnelForegroundService" }
else -> "Unknown"
}
Timber.d("$serviceClass connected")
}
override fun onServiceDisconnected(name: ComponentName) { suspend fun startAutoTunnel(background: Boolean) {
_tunnelService.update { null } val settings = appDataRepository.settings.get()
val serviceClass = appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = true))
when { if (autoTunnelService.isCompleted) return _autoTunnelActive.update { true }
name.className.contains("VpnForegroundService") -> "VpnForegroundService" runCatching {
name.className.contains("TunnelForegroundService") -> startService(AutoTunnelService::class.java, background)
"TunnelForegroundService" autoTunnelService.await()
else -> "Unknown" autoTunnelService.getCompleted().start()
} _autoTunnelActive.update { true }
Timber.d("$serviceClass disconnected") updateAutoTunnelTile()
} }.onFailure {
} Timber.e(it)
}
}
private val autoTunnelServiceConnection = suspend fun startBackgroundService(tunnelConf: TunnelConf) {
object : ServiceConnection { if (backgroundService.isCompleted) return
override fun onServiceConnected(name: ComponentName, service: IBinder) { runCatching {
val binder = service as? AutoTunnelService.LocalBinder startService(TunnelForegroundService::class.java, true)
_autoTunnelService.update { binder?.service } backgroundService.await()
Timber.d("AutoTunnelService connected") backgroundService.getCompleted().start(tunnelConf)
} }.onFailure {
Timber.e(it)
}
}
override fun onServiceDisconnected(name: ComponentName) { fun stopBackgroundService() {
_autoTunnelService.update { null } if (!backgroundService.isCompleted) return
Timber.d("AutoTunnelService disconnected") runCatching {
} backgroundService.getCompleted().stop()
} }.onFailure {
Timber.e(it)
}
}
fun hasVpnPermission(): Boolean { suspend fun toggleAutoTunnel(background: Boolean) {
return VpnService.prepare(context) == null withContext(ioDispatcher) {
} if (_autoTunnelActive.value) return@withContext stopAutoTunnel()
startAutoTunnel(background)
}
}
private fun startServiceInternal() { fun updateAutoTunnelTile() {
if (autoTunnelService.value == null) { if (autoTunnelTile.isCompleted) {
val intent = Intent(context, AutoTunnelService::class.java) autoTunnelTile.getCompleted().updateTileState()
context.startForegroundService(intent) } else {
context.bindService(intent, autoTunnelServiceConnection, Context.BIND_AUTO_CREATE) context.requestAutoTunnelTileServiceUpdate()
} }
} }
suspend fun startAutoTunnelService() = autoTunnelMutex.withLock { startServiceInternal() } fun updateTunnelTile() {
if (tunnelControlTile.isCompleted) {
tunnelControlTile.getCompleted().updateTileState()
} else {
context.requestTunnelTileServiceStateUpdate()
}
}
private fun stopServiceInternal() { suspend fun stopAutoTunnel() {
_autoTunnelService.value?.stop() withContext(ioDispatcher) {
try { val settings = appDataRepository.settings.get()
context.unbindService(autoTunnelServiceConnection) appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = false))
} catch (e: Exception) { if (!autoTunnelService.isCompleted) return@withContext
Timber.e(e, "Failed to unbind AutoTunnelService") runCatching {
} autoTunnelService.getCompleted().stop()
_autoTunnelService.update { null } _autoTunnelActive.update { false }
} updateAutoTunnelTile()
}.onFailure {
suspend fun startTunnelService(appMode: AppMode) = Timber.e(it)
tunnelMutex.withLock { }
if (_tunnelService.value != null) { }
Timber.d("Service already exists, waiting for disconnect") }
withTimeoutOrNull(2000L) { _tunnelService.first { it == null } }
?: Timber.w("Timeout waiting for existing service to disconnect")
}
if (_tunnelService.value == null) {
val serviceClass =
when (appMode) {
AppMode.VPN,
AppMode.LOCK_DOWN -> VpnForegroundService::class.java
AppMode.KERNEL,
AppMode.PROXY -> TunnelForegroundService::class.java
}
val intent = Intent(context, serviceClass)
context.startForegroundService(intent)
context.bindService(intent, tunnelServiceConnection, Context.BIND_AUTO_CREATE)
} else {
Timber.e("Service still not null after timeout")
}
}
suspend fun stopTunnelService() =
tunnelMutex.withLock {
_tunnelService.value?.let { service ->
service.stop()
try {
context.unbindService(tunnelServiceConnection)
} catch (e: Exception) {
Timber.e(e, "Failed to unbind Tunnel Service")
}
}
}
fun updateAutoTunnelTile() {
context.requestAutoTunnelTileServiceUpdate()
}
fun updateTunnelTile() {
context.requestTunnelTileServiceStateUpdate()
}
fun handleTunnelServiceDestroy() {
_tunnelService.update { null }
}
fun handleAutoTunnelServiceDestroy() {
_autoTunnelService.update { null }
}
} }
@@ -1,6 +1,71 @@
package com.zaneschepke.wireguardautotunnel.core.service package com.zaneschepke.wireguardautotunnel.core.service
import android.app.Notification
import android.content.Intent
import android.os.IBinder
import androidx.core.app.ServiceCompat
import androidx.lifecycle.LifecycleService
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CompletableDeferred
import javax.inject.Inject
class TunnelForegroundService(override val fgsType: Int = Constants.SPECIAL_USE_SERVICE_TYPE_ID) : @AndroidEntryPoint
BaseTunnelForegroundService() class TunnelForegroundService : LifecycleService() {
@Inject
lateinit var notificationManager: NotificationManager
@Inject
lateinit var serviceManager: ServiceManager
override fun onCreate() {
super.onCreate()
serviceManager.backgroundService.complete(this)
}
override fun onBind(intent: Intent): IBinder? {
super.onBind(intent)
return null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
serviceManager.backgroundService.complete(this)
return START_NOT_STICKY
}
fun start(tunnelConf: TunnelConf) {
ServiceCompat.startForeground(
this@TunnelForegroundService,
NotificationManager.KERNEL_SERVICE_NOTIFICATION_ID,
createNotification(tunnelConf),
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
)
}
fun stop() {
stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf()
}
override fun onDestroy() {
serviceManager.backgroundService = CompletableDeferred()
super.onDestroy()
}
private fun createNotification(tunnelConf: TunnelConf): Notification {
return notificationManager.createNotification(
WireGuardNotification.NotificationChannels.VPN,
title = "${getString(R.string.tunnel_running)} - ${tunnelConf.tunName}",
actions = listOf(
notificationManager.createNotificationAction(NotificationAction.TUNNEL_OFF, tunnelConf.id),
),
)
}
}
@@ -1,7 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.service
interface TunnelService {
fun start()
fun stop()
}
@@ -1,6 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.service
import com.zaneschepke.wireguardautotunnel.util.Constants
class VpnForegroundService(override val fgsType: Int = Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID) :
BaseTunnelForegroundService()
@@ -1,393 +1,255 @@
package com.zaneschepke.wireguardautotunnel.core.service.autotunnel package com.zaneschepke.wireguardautotunnel.core.service.autotunnel
import android.content.Intent import android.content.Intent
import android.os.Binder import android.net.NetworkCapabilities
import android.os.IBinder import android.os.IBinder
import android.os.PowerManager
import androidx.core.app.ServiceCompat import androidx.core.app.ServiceCompat
import androidx.lifecycle.LifecycleService import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor import com.wireguard.android.util.RootShell
import com.zaneschepke.networkmonitor.ConnectivityState
import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.di.AppShell
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.di.MainImmediateDispatcher
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.domain.state.AutoTunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.NetworkState
import com.zaneschepke.wireguardautotunnel.core.network.InternetConnectivityMonitor
import com.zaneschepke.wireguardautotunnel.core.network.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.domain.state.ConnectivityState
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.data.model.AppMode import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
import com.zaneschepke.wireguardautotunnel.di.Dispatcher import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
import com.zaneschepke.wireguardautotunnel.domain.model.AutoTunnelSettings import com.zaneschepke.wireguardautotunnel.domain.events.KillSwitchEvent
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.state.AutoTunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.toDomain
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.to import com.zaneschepke.wireguardautotunnel.util.extensions.Tunnels
import com.zaneschepke.wireguardautotunnel.util.extensions.toMillis import com.zaneschepke.wireguardautotunnel.util.extensions.getCurrentWifiName
import java.lang.ref.WeakReference import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.koin.android.ext.android.inject
import org.koin.core.qualifier.named
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject
import javax.inject.Provider
@AndroidEntryPoint
class AutoTunnelService : LifecycleService() { class AutoTunnelService : LifecycleService() {
private val networkMonitor: NetworkMonitor by inject() @Inject
@AppShell
lateinit var rootShell: Provider<RootShell>
private val notificationManager: NotificationManager by inject() @Inject
lateinit var networkMonitor: NetworkMonitor
private val ioDispatcher: CoroutineDispatcher by inject(named(Dispatcher.IO)) @Inject
lateinit var appDataRepository: Provider<AppDataRepository>
private val serviceManager: ServiceManager by inject() @Inject
lateinit var notificationManager: NotificationManager
private val tunnelManager: TunnelManager by inject() @Inject
@IoDispatcher
lateinit var ioDispatcher: CoroutineDispatcher
private val autoTunnelRepository: AutoTunnelSettingsRepository by inject() @Inject
private val settingsRepository: GeneralSettingRepository by inject() lateinit var serviceManager: ServiceManager
private val tunnelsRepository: TunnelRepository by inject()
private val defaultState = AutoTunnelState() @Inject
@MainImmediateDispatcher
lateinit var mainImmediateDispatcher: CoroutineDispatcher
private val autoTunMutex = Mutex() @Inject
lateinit var tunnelManager: TunnelManager
private val autoTunnelStateFlow = MutableStateFlow(defaultState) private val defaultState = AutoTunnelState()
private var autoTunnelJob: Job? = null private val autoTunnelStateFlow = MutableStateFlow(defaultState)
private var permissionsJob: Job? = null
private var autoTunnelFailoverJob: Job? = null
class LocalBinder(service: AutoTunnelService) : Binder() { private var wakeLock: PowerManager.WakeLock? = null
private val serviceRef = WeakReference(service)
val service: AutoTunnelService? override fun onCreate() {
get() = serviceRef.get() super.onCreate()
} serviceManager.autoTunnelService.complete(this)
lifecycleScope.launch(mainImmediateDispatcher) {
runCatching {
launchWatcherNotification()
}.onFailure {
Timber.e(it)
}
}
}
private val binder = LocalBinder(this) override fun onBind(intent: Intent): IBinder? {
super.onBind(intent)
return null
}
override fun onCreate() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onCreate() super.onStartCommand(intent, flags, startId)
launchWatcherNotification() Timber.d("onStartCommand executed with startId: $startId")
} serviceManager.autoTunnelService.complete(this)
return START_NOT_STICKY
}
override fun onBind(intent: Intent): IBinder { fun start() {
super.onBind(intent) kotlin.runCatching {
return binder lifecycleScope.launch(mainImmediateDispatcher) {
} launchWatcherNotification()
initWakeLock()
}
startAutoTunnelJob()
startAutoTunnelStateJob()
startKillSwitchJob()
}.onFailure {
Timber.e(it)
}
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { fun stop() {
super.onStartCommand(intent, flags, startId) wakeLock?.let { if (it.isHeld) it.release() }
Timber.d("onStartCommand executed with startId: $startId") stopSelf()
start() }
return START_STICKY
}
fun start() { override fun onDestroy() {
launchWatcherNotification() serviceManager.autoTunnelService = CompletableDeferred()
autoTunnelJob?.cancel() super.onDestroy()
autoTunnelJob = startAutoTunnelStateJob() }
permissionsJob?.cancel()
permissionsJob = startLocationPermissionsNotificationJob()
}
fun stop() { private fun launchWatcherNotification(description: String = getString(R.string.monitoring_state_changes)) {
stopSelf() val notification =
} notificationManager.createNotification(
WireGuardNotification.NotificationChannels.AUTO_TUNNEL,
title = getString(R.string.auto_tunnel_title),
description = description,
actions = listOf(
notificationManager.createNotificationAction(NotificationAction.AUTO_TUNNEL_OFF),
),
)
ServiceCompat.startForeground(
this,
NotificationManager.AUTO_TUNNEL_NOTIFICATION_ID,
notification,
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
)
}
override fun onDestroy() { private fun initWakeLock() {
serviceManager.handleAutoTunnelServiceDestroy() wakeLock = (getSystemService(POWER_SERVICE) as PowerManager).run {
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) val tag = this.javaClass.name
super.onDestroy() newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
} try {
Timber.i("Initiating wakelock with 10 min timeout")
acquire(Constants.BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT)
} finally {
release()
}
}
}
}
private fun launchWatcherNotification( private suspend fun buildNetworkState(connectivityState: ConnectivityState): NetworkState {
description: String = getString(R.string.monitoring_state_changes) return with(autoTunnelStateFlow.value.networkState) {
) { val wifiName = when {
val notification = connectivityState.wifiAvailable &&
notificationManager.createNotification( (wifiName == null || wifiName == Constants.UNREADABLE_SSID || networkMonitor.didWifiChangeSinceLastCapabilitiesQuery) -> {
WireGuardNotification.NotificationChannels.AUTO_TUNNEL, networkMonitor.getWifiCapabilities()?.let { getWifiName(it) } ?: wifiName
title = getString(R.string.auto_tunnel_title), }
description = description, !connectivityState.wifiAvailable -> null
actions = else -> wifiName
listOf( }
notificationManager.createNotificationAction( copy(
NotificationAction.AUTO_TUNNEL_OFF isWifiConnected = connectivityState.wifiAvailable,
) isMobileDataConnected = connectivityState.cellularAvailable,
), isEthernetConnected = isEthernetConnected,
onGoing = true, wifiName = wifiName,
groupKey = NotificationManager.AUTO_TUNNEL_GROUP_KEY, )
isGroupSummary = true, }
) }
ServiceCompat.startForeground(
this,
NotificationManager.AUTO_TUNNEL_NOTIFICATION_ID,
notification,
Constants.SPECIAL_USE_SERVICE_TYPE_ID,
)
}
private fun startAutoTunnelStateJob(): Job = private fun startAutoTunnelStateJob() = lifecycleScope.launch(ioDispatcher) {
lifecycleScope.launch(ioDispatcher) { combine(
val networkFlow = combineSettings(),
debouncedConnectivityStateFlow networkMonitor.status.map {
.flowOn(ioDispatcher) buildNetworkState(it)
.map { it.toDomain() } }.distinctUntilChanged(),
.map(::NetworkChange) ) { double, networkState ->
.distinctUntilChanged() AutoTunnelState(tunnelManager.activeTunnels().value, networkState, double.first, double.second)
}.collect { state ->
autoTunnelStateFlow.update {
it.copy(activeTunnels = state.activeTunnels, networkState = state.networkState, settings = state.settings, tunnels = state.tunnels)
}
}
}
val settingsFlow = private suspend fun getWifiName(wifiCapabilities: NetworkCapabilities): String? {
combineSettings().map { (appMode, settings, tunnels) -> val setting = appDataRepository.get().settings.get()
SettingsChange(appMode, settings, tunnels) return if (setting.isWifiNameByShellEnabled) {
} rootShell.get().getCurrentWifiName()
} else {
InternetConnectivityMonitor.getNetworkName(wifiCapabilities, this@AutoTunnelService)
}
}
val tunnelsFlow = tunnelManager.activeTunnels.map(::ActiveTunnelsChange) private fun combineSettings(): Flow<Pair<AppSettings, Tunnels>> {
return combine(
appDataRepository.get().settings.flow,
appDataRepository.get().tunnels.flow.map { tunnels ->
// isActive is ignored for equality checks so user can manually toggle off tunnel with auto-tunnel
tunnels.map { it.copy(isActive = false) }
},
) { settings, tunnels ->
Pair(settings, tunnels)
}.distinctUntilChanged()
}
var reevaluationJob: Job? = null private fun startKillSwitchJob() = lifecycleScope.launch(ioDispatcher) {
autoTunnelStateFlow.collect {
if (it == defaultState) return@collect
when (val event = it.asKillSwitchEvent()) {
KillSwitchEvent.DoNothing -> Unit
is KillSwitchEvent.Start -> {
Timber.d("Starting kill switch")
tunnelManager.setBackendState(BackendState.KILL_SWITCH_ACTIVE, event.allowedIps)
}
KillSwitchEvent.Stop -> {
Timber.d("Stopping kill switch")
tunnelManager.setBackendState(BackendState.SERVICE_ACTIVE, emptySet())
}
}
}
}
// get everything in sync before we use merge @OptIn(FlowPreview::class)
combine(networkFlow, settingsFlow, tunnelsFlow) { network, settings, tunnels -> private fun startAutoTunnelJob() = lifecycleScope.launch(ioDispatcher) {
autoTunnelStateFlow.update { Timber.i("Starting auto-tunnel network event watcher")
it.copy( val settings = appDataRepository.get().settings.get()
activeTunnels = tunnels.activeTunnels, Timber.d("Starting with debounce delay of: ${settings.debounceDelaySeconds} seconds")
networkState = network.networkState, autoTunnelStateFlow.debounce(settings.debounceDelayMillis()).collect { watcherState ->
settings = settings.settings, if (watcherState == defaultState) return@collect
tunnels = settings.tunnels, Timber.d("New auto tunnel state emitted")
) when (val event = watcherState.asAutoTunnelEvent()) {
} is AutoTunnelEvent.Start -> (event.tunnelConf ?: appDataRepository.get().getPrimaryOrFirstTunnel())?.let {
} tunnelManager.startTunnel(it)
.first() }
// TODO improve this to target specific tunnels to better support multi-tunnel
val initialState = autoTunnelStateFlow.value is AutoTunnelEvent.Stop -> tunnelManager.stopTunnel()
if (initialState != defaultState) { AutoTunnelEvent.DoNothing -> Timber.i("Auto-tunneling: no condition met")
handleAutoTunnelEvent( }
initialState.determineAutoTunnelEvent(NetworkChange(initialState.networkState)) }
) }
}
// use merge to limit the noise of a combine and also increase the scalability of auto
// tunnel handling new states
merge(networkFlow, settingsFlow, tunnelsFlow).collect { change ->
if (change !is ActiveTunnelsChange) {
Timber.d("New state changed to ${change.javaClass.simpleName}")
}
val previousState = autoTunnelStateFlow.value
when (change) {
is NetworkChange -> {
Timber.d("Network change: ${change.networkState}")
reevaluationJob?.cancel()
autoTunnelStateFlow.update { it.copy(networkState = change.networkState) }
if (previousState.networkState == change.networkState) {
Timber.d("Duplicate network state change detected, ignoring")
return@collect
}
}
is SettingsChange -> {
reevaluationJob?.cancel()
autoTunnelStateFlow.update {
it.copy(settings = change.settings, tunnels = change.tunnels)
}
if (
previousState.settings == change.settings &&
previousState.tunnels == change.tunnels
) {
Timber.d("Duplicate settings change detected, ignoring")
return@collect
}
}
is ActiveTunnelsChange -> {
autoTunnelStateFlow.update { it.copy(activeTunnels = change.activeTunnels) }
return@collect
}
}
handleAutoTunnelEvent(autoTunnelStateFlow.value.determineAutoTunnelEvent(change))
// re-evaluate network state after a short duration to prevent missed state changes
reevaluationJob = launch {
val snapshotNetwork = autoTunnelStateFlow.value.networkState
delay(REEVALUATE_CHECK_DELAY)
val currentState = autoTunnelStateFlow.value
if (
currentState != defaultState && currentState.networkState != snapshotNetwork
) {
Timber.d(
"Re-evaluating auto-tunnel state.. (network changed since snapshot)"
)
handleAutoTunnelEvent(currentState.determineAutoTunnelEvent(change))
} else {
Timber.d("Skipping re-eval: network unchanged or default state")
}
}
}
}
private fun combineSettings(): Flow<Triple<AppMode, AutoTunnelSettings, List<TunnelConfig>>> {
return combine(
settingsRepository.flow.map { it.appMode }.distinctUntilChanged(),
autoTunnelRepository.flow,
tunnelsRepository.userTunnelsFlow.map { tunnels ->
// isActive is ignored for equality checks so user can manually toggle off
// tunnel with auto-tunnel
tunnels.map { it.copy(isActive = false) }
},
) { appMode, autoTunnel, tunnels ->
Triple(appMode, autoTunnel, tunnels)
}
.distinctUntilChanged()
}
private fun areAutoTunnelPermissionsRequiredTheSame(
old: AutoTunnelState,
new: AutoTunnelState,
): Boolean {
return (old.settings.wifiDetectionMethod == new.settings.wifiDetectionMethod &&
old.networkState.locationPermissionGranted ==
new.networkState.locationPermissionGranted &&
old.networkState.locationServicesEnabled == new.networkState.locationServicesEnabled &&
old.tunnels == new.tunnels &&
old.settings.trustedNetworkSSIDs == new.settings.trustedNetworkSSIDs)
}
// watch for changes to location permission and notify user it will impact auto-tunneling
// TODO or a recheck button for location permission so we dont have to poll it
private fun startLocationPermissionsNotificationJob(): Job =
lifecycleScope.launch(ioDispatcher) {
var locationServicesShown = false
var locationPermissionsShown = false
data class NetworkPermissionState(
val detectionMethod: AndroidNetworkMonitor.WifiDetectionMethod,
val locationServicesEnabled: Boolean,
val locationPermissionsEnabled: Boolean,
val ssidReadRequired: Boolean,
)
autoTunnelStateFlow
.distinctUntilChanged(::areAutoTunnelPermissionsRequiredTheSame)
.map {
NetworkPermissionState(
it.settings.wifiDetectionMethod.to(),
it.networkState.locationServicesEnabled,
it.networkState.locationPermissionGranted,
(it.tunnels.any { tunnel -> tunnel.tunnelNetworks.isNotEmpty() } ||
it.settings.trustedNetworkSSIDs.isNotEmpty()),
)
}
.collect { state ->
when (state.detectionMethod) {
AndroidNetworkMonitor.WifiDetectionMethod.DEFAULT,
AndroidNetworkMonitor.WifiDetectionMethod.LEGACY -> {
if (
!state.locationPermissionsEnabled &&
!locationPermissionsShown &&
state.ssidReadRequired
) {
locationPermissionsShown = true
val notification =
notificationManager.createNotification(
WireGuardNotification.NotificationChannels.AUTO_TUNNEL,
title = getString(R.string.warning),
description =
getString(R.string.location_permissions_missing),
)
notificationManager.show(
NotificationManager.AUTO_TUNNEL_LOCATION_PERMISSION_ID,
notification,
)
}
if (
!state.locationServicesEnabled &&
!locationServicesShown &&
state.ssidReadRequired
) {
locationServicesShown = true
val notification =
notificationManager.createNotification(
WireGuardNotification.NotificationChannels.AUTO_TUNNEL,
title = getString(R.string.warning),
description =
getString(R.string.location_services_not_detected),
)
notificationManager.show(
NotificationManager.AUTO_TUNNEL_LOCATION_SERVICES_ID,
notification,
)
}
if (state.locationServicesEnabled || !state.ssidReadRequired) {
notificationManager.remove(
NotificationManager.AUTO_TUNNEL_LOCATION_SERVICES_ID
)
locationServicesShown = false
}
if (state.locationPermissionsEnabled || !state.ssidReadRequired) {
notificationManager.remove(
NotificationManager.AUTO_TUNNEL_LOCATION_PERMISSION_ID
)
locationPermissionsShown = false
}
}
else -> Unit
}
}
}
private suspend fun handleAutoTunnelEvent(autoTunnelEvent: AutoTunnelEvent) {
autoTunMutex.withLock {
when (
val event =
autoTunnelEvent.also {
Timber.i("Auto tunnel event: ${it.javaClass.simpleName}")
}
) {
is AutoTunnelEvent.Start ->
(event.tunnelConfig ?: tunnelsRepository.getDefaultTunnel())?.let {
tunnelManager.startTunnel(it).onFailure { e ->
Timber.e(e, "Auto-tunnel start failed for ${it.name}")
// TODO notify or retry
}
}
is AutoTunnelEvent.Stop -> tunnelManager.stopActiveTunnels()
AutoTunnelEvent.DoNothing -> Timber.i("Auto-tunneling: nothing to do")
}
}
}
// restart network flow on debounce changes
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
private val debouncedConnectivityStateFlow: Flow<ConnectivityState> by lazy {
autoTunnelRepository.flow
.map { it.debounceDelaySeconds.toMillis() }
.distinctUntilChanged()
.flatMapLatest { debounceMillis ->
networkMonitor.connectivityStateFlow.debounce(debounceMillis)
}
}
companion object {
const val REEVALUATE_CHECK_DELAY = 3_000L
}
} }
@@ -1,19 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.service.autotunnel
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.domain.model.AutoTunnelSettings
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.state.NetworkState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
sealed interface StateChange
data class NetworkChange(val networkState: NetworkState) : StateChange
data class SettingsChange(
val appMode: AppMode,
val settings: AutoTunnelSettings,
val tunnels: List<TunnelConfig>,
) : StateChange
data class ActiveTunnelsChange(val activeTunnels: Map<Int, TunnelState>) : StateChange
@@ -4,106 +4,97 @@ import android.content.Intent
import android.os.IBinder import android.os.IBinder
import android.service.quicksettings.Tile import android.service.quicksettings.Tile
import android.service.quicksettings.TileService import android.service.quicksettings.TileService
import androidx.lifecycle.* import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository import dagger.hilt.android.AndroidEntryPoint
import kotlin.concurrent.atomics.AtomicBoolean import kotlinx.coroutines.CompletableDeferred
import kotlin.concurrent.atomics.ExperimentalAtomicApi import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject
class AutoTunnelControlTile : TileService(), LifecycleOwner { @AndroidEntryPoint
class AutoTunnelControlTile : TileService() {
@Inject
lateinit var appDataRepository: AppDataRepository
private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository by inject() @Inject
lateinit var serviceManager: ServiceManager
private val serviceManager: ServiceManager by inject() @Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
@OptIn(ExperimentalAtomicApi::class) val isCollecting = AtomicBoolean(false) override fun onCreate() {
super.onCreate()
serviceManager.autoTunnelTile.complete(this)
}
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this) override fun onDestroy() {
super.onDestroy()
serviceManager.autoTunnelTile = CompletableDeferred()
}
override fun onCreate() { override fun onStartListening() {
super.onCreate() super.onStartListening()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) serviceManager.autoTunnelTile.complete(this)
} applicationScope.launch {
if (appDataRepository.tunnels.getAll().isEmpty()) return@launch setUnavailable()
updateTileState()
}
}
override fun onDestroy() { fun updateTileState() {
super.onDestroy() serviceManager.autoTunnelActive.value.let {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) if (it) setActive() else setInactive()
} }
}
override fun onTileAdded() { override fun onClick() {
super.onTileAdded() super.onClick()
initTileState() unlockAndRun {
} applicationScope.launch {
if (serviceManager.autoTunnelActive.value) {
serviceManager.stopAutoTunnel()
setInactive()
} else {
serviceManager.startAutoTunnel(true)
setActive()
}
}
}
}
override fun onStopListening() { private fun setActive() {
super.onStopListening() runCatching {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP) qsTile.state = Tile.STATE_ACTIVE
} qsTile.updateTile()
}
}
@OptIn(ExperimentalAtomicApi::class) private fun setInactive() {
private fun initTileState() { runCatching {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START) qsTile.state = Tile.STATE_INACTIVE
Timber.d("Start listening called for auto tunnel tile") qsTile.updateTile()
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() { /* This works around an annoying unsolved frameworks bug some people are hitting. */
super.onStartListening() override fun onBind(intent: Intent): IBinder? {
initTileState() var ret: IBinder? = null
} try {
ret = super.onBind(intent)
} catch (_: Throwable) {
Timber.e("Failed to bind to TunnelControlTile")
}
return ret
}
override fun onClick() { private fun setUnavailable() {
super.onClick() runCatching {
unlockAndRun { qsTile.state = Tile.STATE_UNAVAILABLE
lifecycleScope.launch { qsTile.updateTile()
if (serviceManager.autoTunnelService.value != null) { }
autoTunnelSettingsRepository.updateAutoTunnelEnabled(false) }
setInactive()
} else {
autoTunnelSettingsRepository.updateAutoTunnelEnabled(true)
setActive()
}
}
}
}
private fun setActive() {
qsTile?.let {
it.state = Tile.STATE_ACTIVE
it.updateTile()
}
}
private fun setInactive() {
qsTile?.let {
it.state = Tile.STATE_INACTIVE
it.updateTile()
}
}
/* This works around an annoying unsolved frameworks bug some people are hitting. */
override fun onBind(intent: Intent): IBinder? {
var ret: IBinder? = null
try {
ret = super.onBind(intent)
} catch (_: Throwable) {
Timber.e("Failed to bind to AutoTunnelControlTile")
}
return ret
}
override val lifecycle: Lifecycle
get() = lifecycleRegistry
} }
@@ -5,209 +5,126 @@ import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.service.quicksettings.Tile import android.service.quicksettings.Tile
import android.service.quicksettings.TileService import android.service.quicksettings.TileService
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import kotlin.concurrent.atomics.AtomicBoolean import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import kotlin.concurrent.atomics.ExperimentalAtomicApi import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.distinctUntilChangedBy import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.koin.android.ext.android.inject
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject
class TunnelControlTile : TileService(), LifecycleOwner { @AndroidEntryPoint
class TunnelControlTile : TileService() {
@Inject
lateinit var appDataRepository: AppDataRepository
private val tunnelsRepository: TunnelRepository by inject() @Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
private val serviceManager: ServiceManager by inject() @Inject
lateinit var serviceManager: ServiceManager
private val tunnelManager: TunnelManager by inject() @Inject
lateinit var tunnelManager: TunnelManager
@OptIn(ExperimentalAtomicApi::class) val isCollecting = AtomicBoolean(false) override fun onCreate() {
super.onCreate()
serviceManager.tunnelControlTile.complete(this)
}
private val startLock = Mutex() override fun onDestroy() {
super.onDestroy()
serviceManager.tunnelControlTile = CompletableDeferred()
}
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this) override fun onStartListening() {
super.onStartListening()
Timber.d("Start listening called")
serviceManager.tunnelControlTile.complete(this)
applicationScope.launch {
updateTileState()
}
}
override fun onCreate() { fun updateTileState() = applicationScope.launch {
super.onCreate() if (appDataRepository.tunnels.getAll().isEmpty()) return@launch setUnavailable()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) with(tunnelManager.activeTunnels().value) {
} if (isNotEmpty()) return@launch updateTile(if (size == 1) first().tunName else getString(R.string.multiple), true)
}
appDataRepository.getStartTunnelConfig()?.let {
updateTile(it.tunName, false)
}
}
override fun onDestroy() { override fun onClick() {
super.onDestroy() super.onClick()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) unlockAndRun {
} applicationScope.launch {
if (tunnelManager.activeTunnels().value.isNotEmpty()) return@launch tunnelManager.stopTunnel()
appDataRepository.getStartTunnelConfig()?.let {
tunnelManager.startTunnel(it)
}
}
}
}
override fun onTileAdded() { private fun setActive() {
super.onTileAdded() runCatching {
initTileState() qsTile.state = Tile.STATE_ACTIVE
} qsTile.updateTile()
}
}
@OptIn(ExperimentalAtomicApi::class) private fun setInactive() {
private fun initTileState() { runCatching {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START) qsTile.state = Tile.STATE_INACTIVE
Timber.d("Start listening called for tunnel tile") qsTile.updateTile()
if (isCollecting.compareAndSet(expectedValue = false, newValue = true)) { }
lifecycleScope.launch { }
repeatOnLifecycle(Lifecycle.State.STARTED) {
tunnelManager.activeTunnels
.distinctUntilChangedBy { it.size }
.collect { updateTileState() }
}
}
}
}
override fun onStartListening() { private fun setUnavailable() {
super.onStartListening() runCatching {
initTileState() qsTile.state = Tile.STATE_UNAVAILABLE
} setTileDescription("")
qsTile.updateTile()
}
}
override fun onStopListening() { private fun setTileDescription(description: String) {
super.onStopListening() runCatching {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
} qsTile.subtitle = description
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
qsTile.stateDescription = description
}
qsTile.updateTile()
}
}
private suspend fun updateTileState() { /* This works around an annoying unsolved frameworks bug some people are hitting. */
try { override fun onBind(intent: Intent): IBinder? {
val tunnels = tunnelsRepository.getAll() var ret: IBinder? = null
if (tunnels.isEmpty()) { try {
setUnavailable() ret = super.onBind(intent)
return } catch (_: Throwable) {
} Timber.e("Failed to bind to TunnelControlTile")
}
return ret
}
val activeTunnels = private fun updateTile(name: String, active: Boolean) {
tunnelManager.activeTunnels.value.filter { it.value.status.isUpOrStarting() } runCatching {
setTileDescription(name)
when { if (active) return setActive()
activeTunnels.isNotEmpty() -> { setInactive()
val activeIds = activeTunnels.map { it.key } }.onFailure {
// TODO improvements would be needed to make this work well with toggling Timber.e(it)
// multiple tunnels }
// this would be better managed elsewhere }
WireGuardAutoTunnel.setLastActiveTunnels(activeIds)
val activeTunNames =
tunnels.filter { activeTunnels.keys.contains(it.id) }.map { it.name }
updateTileForActiveTunnels(activeTunNames)
}
else -> updateTileForLastActiveTunnels()
}
} catch (e: Exception) {
Timber.e(e, "Failed to update tunnel state")
setUnavailable()
}
}
private fun updateTileForActiveTunnels(activeTunnelNames: List<String>) {
val tileName =
when (activeTunnelNames.size) {
1 -> activeTunnelNames[0]
else -> getString(R.string.multiple)
}
updateTile(tileName, true)
}
private suspend fun updateTileForLastActiveTunnels() {
val lastActiveIds = WireGuardAutoTunnel.getLastActiveTunnels()
when {
lastActiveIds.isEmpty() -> {
tunnelsRepository.getStartTunnel()?.let { config -> updateTile(config.name, false) }
?: setUnavailable()
}
lastActiveIds.size > 1 -> updateTile(getString(R.string.multiple), false)
else -> {
val tunnelId = lastActiveIds.first()
tunnelsRepository.getById(tunnelId)?.let { tunnel ->
updateTile(tunnel.name, false)
} ?: setUnavailable()
}
}
}
override fun onClick() {
super.onClick()
unlockAndRun {
lifecycleScope.launch {
startLock.withLock {
if (tunnelManager.activeTunnels.value.isNotEmpty())
return@launch tunnelManager.stopActiveTunnels()
val lastActive = WireGuardAutoTunnel.getLastActiveTunnels()
if (lastActive.isEmpty()) {
tunnelsRepository.getStartTunnel()?.let { tunnelManager.startTunnel(it) }
} else {
lastActive.forEach { id ->
tunnelsRepository.getById(id)?.let { tunnelManager.startTunnel(it) }
}
}
}
}
}
}
private fun setActive() {
qsTile?.let {
it.state = Tile.STATE_ACTIVE
it.updateTile()
}
}
private fun setInactive() {
qsTile?.let {
it.state = Tile.STATE_INACTIVE
it.updateTile()
}
}
private fun setUnavailable() {
qsTile?.let {
it.state = Tile.STATE_UNAVAILABLE
setTileDescription("")
it.updateTile()
}
}
private fun setTileDescription(description: String) {
qsTile?.let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
it.subtitle = description
it.stateDescription = description
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
it.subtitle = description
}
it.updateTile()
}
}
private fun updateTile(name: String, active: Boolean) {
runCatching {
setTileDescription(name)
if (active) return setActive()
setInactive()
}
.onFailure { Timber.e(it) }
}
/* This works around an annoying unsolved frameworks bug some people are hitting. */
override fun onBind(intent: Intent): IBinder? {
var ret: IBinder? = null
try {
ret = super.onBind(intent)
} catch (_: Throwable) {
Timber.e("Failed to bind to TunnelControlTile")
}
return ret
}
override val lifecycle: Lifecycle
get() = lifecycleRegistry
} }
@@ -6,86 +6,74 @@ import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
class DynamicShortcutManager( class DynamicShortcutManager(private val context: Context, @IoDispatcher private val ioDispatcher: CoroutineDispatcher) : ShortcutManager {
private val context: Context, override suspend fun addShortcuts() {
private val ioDispatcher: CoroutineDispatcher, withContext(ioDispatcher) {
) : ShortcutManager { ShortcutManagerCompat.setDynamicShortcuts(context, createShortcuts())
override suspend fun addShortcuts() { }
withContext(ioDispatcher) { }
ShortcutManagerCompat.setDynamicShortcuts(context, createShortcuts())
}
}
override suspend fun removeShortcuts() { override suspend fun removeShortcuts() {
withContext(ioDispatcher) { withContext(ioDispatcher) {
ShortcutManagerCompat.removeDynamicShortcuts(context, createShortcuts().map { it.id }) ShortcutManagerCompat.removeDynamicShortcuts(context, createShortcuts().map { it.id })
} }
} }
private fun createShortcuts(): List<ShortcutInfoCompat> { private fun createShortcuts(): List<ShortcutInfoCompat> {
return listOf( return listOf(
buildShortcut( buildShortcut(
context.getString(R.string.vpn_off), context.getString(R.string.vpn_off),
context.getString(R.string.vpn_off), context.getString(R.string.vpn_off),
context.getString(R.string.vpn_off), context.getString(R.string.vpn_off),
intent = intent = Intent(context, ShortcutsActivity::class.java).apply {
Intent(context, ShortcutsActivity::class.java).apply { putExtra("className", "WireGuardTunnelService")
putExtra("className", "WireGuardTunnelService") action = ShortcutsActivity.Action.STOP.name
action = ShortcutsActivity.Action.STOP.name },
}, shortcutIcon = R.drawable.vpn_off,
shortcutIcon = R.drawable.vpn_off, ),
), buildShortcut(
buildShortcut( context.getString(R.string.vpn_on),
context.getString(R.string.vpn_on), context.getString(R.string.vpn_on),
context.getString(R.string.vpn_on), context.getString(R.string.vpn_on),
context.getString(R.string.vpn_on), intent = Intent(context, ShortcutsActivity::class.java).apply {
intent = putExtra("className", "WireGuardTunnelService")
Intent(context, ShortcutsActivity::class.java).apply { action = ShortcutsActivity.Action.START.name
putExtra("className", "WireGuardTunnelService") },
action = ShortcutsActivity.Action.START.name shortcutIcon = R.drawable.vpn_on,
}, ),
shortcutIcon = R.drawable.vpn_on, buildShortcut(
), context.getString(R.string.start_auto),
buildShortcut( context.getString(R.string.start_auto),
context.getString(R.string.start_auto), context.getString(R.string.start_auto),
context.getString(R.string.start_auto), intent = Intent(context, ShortcutsActivity::class.java).apply {
context.getString(R.string.start_auto), putExtra("className", "WireGuardConnectivityWatcherService")
intent = action = ShortcutsActivity.Action.START.name
Intent(context, ShortcutsActivity::class.java).apply { },
putExtra("className", "WireGuardConnectivityWatcherService") shortcutIcon = R.drawable.auto_play,
action = ShortcutsActivity.Action.START.name ),
}, buildShortcut(
shortcutIcon = R.drawable.auto_play, context.getString(R.string.stop_auto),
), context.getString(R.string.stop_auto),
buildShortcut( context.getString(R.string.stop_auto),
context.getString(R.string.stop_auto), intent = Intent(context, ShortcutsActivity::class.java).apply {
context.getString(R.string.stop_auto), putExtra("className", "WireGuardConnectivityWatcherService")
context.getString(R.string.stop_auto), action = ShortcutsActivity.Action.STOP.name
intent = },
Intent(context, ShortcutsActivity::class.java).apply { shortcutIcon = R.drawable.auto_pause,
putExtra("className", "WireGuardConnectivityWatcherService") ),
action = ShortcutsActivity.Action.STOP.name )
}, }
shortcutIcon = R.drawable.auto_pause,
),
)
}
private fun buildShortcut( private fun buildShortcut(id: String, shortLabel: String, longLabel: String, intent: Intent, shortcutIcon: Int): ShortcutInfoCompat {
id: String, return ShortcutInfoCompat.Builder(context, id)
shortLabel: String, .setShortLabel(shortLabel)
longLabel: String, .setLongLabel(longLabel)
intent: Intent, .setIntent(intent)
shortcutIcon: Int, .setIcon(IconCompat.createWithResource(context, shortcutIcon))
): ShortcutInfoCompat { .build()
return ShortcutInfoCompat.Builder(context, id) }
.setShortLabel(shortLabel)
.setLongLabel(longLabel)
.setIntent(intent)
.setIcon(IconCompat.createWithResource(context, shortcutIcon))
.build()
}
} }
@@ -1,7 +1,6 @@
package com.zaneschepke.wireguardautotunnel.core.shortcut package com.zaneschepke.wireguardautotunnel.core.shortcut
interface ShortcutManager { interface ShortcutManager {
suspend fun addShortcuts() suspend fun addShortcuts()
suspend fun removeShortcuts()
suspend fun removeShortcuts()
} }
@@ -2,73 +2,72 @@ package com.zaneschepke.wireguardautotunnel.core.shortcut
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelService import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelService
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
import com.zaneschepke.wireguardautotunnel.di.Scope import dagger.hilt.android.AndroidEntryPoint
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import org.koin.core.qualifier.named
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class ShortcutsActivity : ComponentActivity() { class ShortcutsActivity : ComponentActivity() {
@Inject
lateinit var appDataRepository: AppDataRepository
private val settingsRepository: GeneralSettingRepository by inject() @Inject
private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository by inject() lateinit var serviceManager: ServiceManager
private val tunnelsRepository: TunnelRepository by inject()
private val tunnelManager: TunnelManager by inject()
private val applicationScope: CoroutineScope by inject(named(Scope.APPLICATION))
override fun onCreate(savedInstanceState: Bundle?) { @Inject
super.onCreate(savedInstanceState) @ApplicationScope
applicationScope.launch { lateinit var applicationScope: CoroutineScope
val settings = settingsRepository.getGeneralSettings()
if (settings.isShortcutsEnabled) {
when (intent.getStringExtra(CLASS_NAME_EXTRA_KEY)) {
LEGACY_TUNNEL_SERVICE_NAME,
TunnelProvider::class.java.simpleName -> {
val tunnelName = intent.getStringExtra(TUNNEL_NAME_EXTRA_KEY)
Timber.d("Tunnel name extra: $tunnelName")
val tunnelConfig =
tunnelName?.let { tunnelsRepository.findByTunnelName(it) }
?: tunnelsRepository.getDefaultTunnel()
Timber.d("Shortcut action on name: ${tunnelConfig?.name}")
tunnelConfig?.let {
when (intent.action) {
Action.START.name -> tunnelManager.startTunnel(it)
Action.STOP.name -> tunnelManager.stopActiveTunnels()
else -> Unit
}
}
}
AutoTunnelService::class.java.simpleName,
LEGACY_AUTO_TUNNEL_SERVICE_NAME -> {
when (intent.action) {
Action.START.name ->
autoTunnelSettingsRepository.updateAutoTunnelEnabled(true)
Action.STOP.name ->
autoTunnelSettingsRepository.updateAutoTunnelEnabled(false)
}
}
}
}
}
finish()
}
enum class Action { override fun onCreate(savedInstanceState: Bundle?) {
START, super.onCreate(savedInstanceState)
STOP, applicationScope.launch {
} val settings = appDataRepository.settings.get()
if (settings.isShortcutsEnabled) {
when (intent.getStringExtra(CLASS_NAME_EXTRA_KEY)) {
LEGACY_TUNNEL_SERVICE_NAME, TunnelProvider::class.java.simpleName -> {
val tunnelName = intent.getStringExtra(TUNNEL_NAME_EXTRA_KEY)
Timber.d("Tunnel name extra: $tunnelName")
val tunnelConfig = tunnelName?.let {
appDataRepository.tunnels.getAll()
.firstOrNull { it.tunName == tunnelName }
} ?: appDataRepository.getStartTunnelConfig()
Timber.d("Shortcut action on name: ${tunnelConfig?.tunName}")
// tunnelConfig?.let {
// when (intent.action) {
// Action.START.name -> tunnelService.get().startTunnel(it)
// Action.STOP.name -> tunnelService.get().stopTunnel()
// else -> Unit
// }
// }
}
AutoTunnelService::class.java.simpleName, LEGACY_AUTO_TUNNEL_SERVICE_NAME -> {
when (intent.action) {
Action.START.name -> serviceManager.startAutoTunnel(true)
Action.STOP.name -> serviceManager.stopAutoTunnel()
}
}
}
}
}
finish()
}
companion object { enum class Action {
const val LEGACY_TUNNEL_SERVICE_NAME = "WireGuardTunnelService" START,
const val LEGACY_AUTO_TUNNEL_SERVICE_NAME = "WireGuardConnectivityWatcherService" STOP,
const val TUNNEL_NAME_EXTRA_KEY = "tunnelName" }
const val CLASS_NAME_EXTRA_KEY = "className"
} companion object {
const val LEGACY_TUNNEL_SERVICE_NAME = "WireGuardTunnelService"
const val LEGACY_AUTO_TUNNEL_SERVICE_NAME = "WireGuardConnectivityWatcherService"
const val TUNNEL_NAME_EXTRA_KEY = "tunnelName"
const val CLASS_NAME_EXTRA_KEY = "className"
}
} }
@@ -0,0 +1,219 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.core.network.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider.Companion.CHECK_INTERVAL
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendError
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.StringValue
import com.zaneschepke.wireguardautotunnel.util.extensions.cancelWithMessage
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean
open class BaseTunnel(
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
@ApplicationScope private val applicationScope: CoroutineScope,
private val networkMonitor: NetworkMonitor,
private val appDataRepository: AppDataRepository,
private val serviceManager: ServiceManager,
private val notificationManager: NotificationManager,
) : TunnelProvider {
internal val tunnels = MutableStateFlow<List<TunnelConf>>(emptyList())
private val tunnelJobs = mutableMapOf<TunnelConf, Job>()
private val isNetworkAvailable = AtomicBoolean(false)
init {
applicationScope.launch(ioDispatcher) {
launch {
startNetworkJob()
}
tunnels.collect { tuns ->
val previousTuns = tunnelJobs.keys.toSet()
val newTuns = tuns - previousTuns
val removedItems = previousTuns - tuns.toSet()
newTuns.forEach { tun ->
Timber.d("Starting tunnel jobs for tun ${tun.name}")
tunnelJobs[tun] = startTunnelJobs(tun)
}
removedItems.forEach { tun ->
tunnelJobs[tun]?.cancelWithMessage("Canceling tunnel jobs for tunnel: ${tun.name}")
tunnelJobs.remove(tun)
}
serviceManager.updateTunnelTile()
}
}
}
private fun startTunnelJobs(tunnel: TunnelConf) = applicationScope.launch(ioDispatcher) {
launch {
startTunnelStatisticsJob(tunnel)
}
launch {
startPingJob(tunnel)
}
launch {
startTunnelConfigChangeJob(tunnel)
}
}
override suspend fun bounceTunnel(tunnelConf: TunnelConf) {
if (tunnels.value.any { it.id == tunnelConf.id }) {
toggleTunnel(tunnelConf, TunnelStatus.DOWN)
toggleTunnel(tunnelConf, TunnelStatus.UP)
}
}
override suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
}
override suspend fun runningTunnelNames(): Set<String> {
return emptySet()
}
override suspend fun activeTunnels(): StateFlow<List<TunnelConf>> {
return tunnels.asStateFlow()
}
override suspend fun startTunnel(tunnelConf: TunnelConf) {
if (tunnels.value.any { it.id == tunnelConf.id }) return Timber.w("Tunnel already running")
serviceManager.startBackgroundService(tunnelConf)
appDataRepository.tunnels.save(tunnelConf.copy(isActive = true))
}
override suspend fun stopTunnel(tunnelConf: TunnelConf?) {
}
open suspend fun toggleTunnel(tunnelConf: TunnelConf, state: TunnelStatus) {
}
open suspend fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics {
throw NotImplementedError("Get statistics not implemented in base class")
}
internal suspend fun onTunnelStop(tunnelConf: TunnelConf) {
appDataRepository.tunnels.save(tunnelConf.copy(isActive = false))
removeFromActiveTunnels(tunnelConf)
if (tunnels.value.isEmpty()) serviceManager.stopBackgroundService()
}
internal suspend fun stopAllTunnels() {
tunnels.value.forEach {
stopTunnel(it)
}
}
internal fun addToActiveTunnels(conf: TunnelConf) {
tunnels.update {
it.toMutableList().apply {
add(conf)
}
}
}
private fun removeFromActiveTunnels(conf: TunnelConf) {
tunnels.update {
it.toMutableList().apply {
remove(conf)
}
}
}
private suspend fun startNetworkJob() = coroutineScope {
networkMonitor.status.distinctUntilChanged().collect {
isNetworkAvailable.set(!it.allOffline)
}
}
private suspend fun startPingJob(tunnel: TunnelConf) = coroutineScope {
while (isActive) {
if (isNetworkAvailable.get() && tunnel.isActive) {
val pingResult = tunnel.pingTunnel(ioDispatcher)
handlePingResult(tunnel, pingResult)
}
delay(CHECK_INTERVAL)
}
}
private suspend fun handlePingResult(tunnel: TunnelConf, pingResult: List<Boolean>) {
if (pingResult.contains(false)) {
if (isNetworkAvailable.get()) {
Timber.i("Ping result: target was not reachable, bouncing the tunnel")
bounceTunnel(tunnel)
delay(tunnel.pingCooldown ?: Constants.PING_COOLDOWN)
} else {
Timber.i("Ping result: target was not reachable, but no network available")
}
} else {
Timber.i("Ping result: all ping targets were reached successfully")
}
}
internal fun handleBackendThrowable(backendError: BackendError) {
val message = when (backendError) {
BackendError.Config -> StringValue.StringResource(R.string.start_failed_config)
BackendError.DNS -> StringValue.StringResource(R.string.dns_error)
BackendError.Unauthorized -> StringValue.StringResource(R.string.unauthorized)
}
if (WireGuardAutoTunnel.isForeground()) {
SnackbarController.showMessage(message)
} else {
notificationManager.show(
NotificationManager.VPN_NOTIFICATION_ID,
notificationManager.createNotification(
WireGuardNotification.NotificationChannels.VPN,
title = StringValue.StringResource(R.string.tunne_start_failed_title),
description = message,
),
)
}
}
private suspend fun startTunnelConfigChangeJob(tunnel: TunnelConf) = coroutineScope {
appDataRepository.tunnels.flow.collect { storageTuns ->
storageTuns.firstOrNull { it.id == tunnel.id }?.let { storageTun ->
if (tunnel.isQuickConfigChanged(storageTun) || tunnel.isPingConfigMatching(storageTun)) {
bounceTunnel(tunnel)
}
}
}
}
private suspend fun startTunnelStatisticsJob(tunnel: TunnelConf) = coroutineScope {
while (isActive) {
val stats = getStatistics(tunnel)
tunnel.state.update {
it.copy(statistics = stats)
}
delay(CHECK_INTERVAL)
}
}
}
@@ -1,50 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import kotlinx.coroutines.flow.MutableStateFlow
fun Map<TunnelConfig, TunnelState>.allDown(): Boolean {
return this.all { it.value.status.isDown() }
}
fun Map<TunnelConfig, TunnelState>.hasActive(): Boolean {
return this.any { it.value.status.isUp() }
}
fun Map<TunnelConfig, TunnelState>.getValueById(id: Int): TunnelState? {
val key = this.keys.find { it.id == id }
return key?.let { this@getValueById[it] }
}
fun Map<TunnelConfig, TunnelState>.getKeyById(id: Int): TunnelConfig? {
return this.keys.find { it.id == id }
}
fun Map<TunnelConfig, TunnelState>.isUp(tunnelConfig: TunnelConfig): Boolean {
return this.getValueById(tunnelConfig.id)?.status?.isUp() ?: false
}
fun MutableStateFlow<Map<TunnelConfig, TunnelState>>.exists(id: Int): Boolean {
return this.value.any { it.key.id == id }
}
fun MutableStateFlow<Map<TunnelConfig, TunnelState>>.isUp(id: Int): Boolean {
return this.value.any { it.key.id == id && it.value.status is TunnelStatus.Up }
}
fun MutableStateFlow<Map<TunnelConfig, TunnelState>>.isStarting(id: Int): Boolean {
return this.value.any { it.key.id == id && it.value.status == TunnelStatus.Starting }
}
fun MutableStateFlow<Map<TunnelConfig, TunnelState>>.findTunnel(id: Int): TunnelConfig? {
return this.value.keys.find { it.id == id }
}
private val URL_PATTERN =
Regex("""^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}:[0-9]{1,5}$""")
fun String.isUrl(): Boolean {
return URL_PATTERN.matches(this)
}
@@ -0,0 +1,83 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.wireguard.android.backend.Backend
import com.wireguard.android.backend.BackendException
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.core.network.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.domain.state.WireGuardStatistics
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendError
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject
class KernelTunnel @Inject constructor(
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
@ApplicationScope private val applicationScope: CoroutineScope,
serviceManager: ServiceManager,
appDataRepository: AppDataRepository,
notificationManager: NotificationManager,
private val backend: Backend,
networkMonitor: NetworkMonitor,
) : BaseTunnel(ioDispatcher, applicationScope, networkMonitor, appDataRepository, serviceManager, notificationManager) {
override suspend fun startTunnel(tunnelConf: TunnelConf) {
withContext(ioDispatcher) {
super.startTunnel(tunnelConf)
runCatching {
backend.setState(tunnelConf, Tunnel.State.UP, tunnelConf.toWgConfig())
addToActiveTunnels(tunnelConf)
}.onFailure {
onTunnelStop(tunnelConf)
if (it is BackendException) {
handleBackendThrowable(it.toBackendError())
} else {
Timber.e(it)
}
}
}
}
override suspend fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics {
return WireGuardStatistics(backend.getStatistics(tunnelConf))
}
override suspend fun stopTunnel(tunnelConf: TunnelConf?) {
withContext(ioDispatcher) {
val tunnel = tunnels.value.firstOrNull { it.id == tunnelConf?.id }
runCatching {
tunnel?.let {
backend.setState(it, Tunnel.State.DOWN, it.toWgConfig())
onTunnelStop(it)
} ?: stopAllTunnels()
}.onFailure {
Timber.e(it)
}
}
}
override suspend fun toggleTunnel(tunnelConf: TunnelConf, status: TunnelStatus) {
when (status) {
TunnelStatus.UP -> backend.setState(tunnelConf, Tunnel.State.UP, tunnelConf.toWgConfig())
TunnelStatus.DOWN -> backend.setState(tunnelConf, Tunnel.State.DOWN, tunnelConf.toWgConfig())
}
}
override suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
Timber.w("Not yet implemented for kernel")
}
override suspend fun runningTunnelNames(): Set<String> {
return backend.runningTunnelNames
}
}
@@ -1,187 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.core.tunnel.backend.TunnelBackend
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException
import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage
import com.zaneschepke.wireguardautotunnel.domain.events.UnknownError
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.state.LogHealthState
import com.zaneschepke.wireguardautotunnel.domain.state.PingState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import java.util.concurrent.ConcurrentHashMap
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withTimeoutOrNull
import timber.log.Timber
class TunnelLifecycleManager(
private val backend: TunnelBackend,
private val applicationScope: CoroutineScope,
private val ioDispatcher: CoroutineDispatcher,
private val sharedActiveTunnels: MutableStateFlow<Map<Int, TunnelState>>,
) : TunnelProvider {
override val activeTunnels: StateFlow<Map<Int, TunnelState>> = sharedActiveTunnels.asStateFlow()
private val _errorEvents = MutableSharedFlow<Pair<String?, BackendCoreException>>()
override val errorEvents: SharedFlow<Pair<String?, BackendCoreException>> =
_errorEvents.asSharedFlow()
private val _messageEvents = MutableSharedFlow<Pair<String?, BackendMessage>>()
override val messageEvents: SharedFlow<Pair<String?, BackendMessage>> =
_messageEvents.asSharedFlow()
private val tunnelJobs = ConcurrentHashMap<Int, Job>()
private val tunMutex = Mutex()
private val tunStatusMutex = Mutex()
override suspend fun startTunnel(tunnelConfig: TunnelConfig): Result<Unit> =
tunMutex.withLock {
val id = tunnelConfig.id
if (sharedActiveTunnels.value.containsKey(id)) {
Timber.w("Tunnel is already running: ${tunnelConfig.name}")
return Result.failure(IllegalStateException("Tunnel already running"))
}
val startupCompleted = CompletableDeferred<Result<Unit>>()
val job =
applicationScope.launch(ioDispatcher) {
try {
updateTunnelStatus(id, TunnelStatus.Starting)
backend.tunnelStateFlow(tunnelConfig).collect { status ->
updateTunnelStatus(id, status)
if (status != TunnelStatus.Starting && !startupCompleted.isCompleted) {
if (status is TunnelStatus.Up) {
startupCompleted.complete(Result.success(Unit))
} else {
startupCompleted.complete(Result.failure(UnknownError()))
}
}
}
} catch (e: BackendCoreException) {
_errorEvents.emit(tunnelConfig.name to e)
updateTunnelStatus(id, TunnelStatus.Down)
startupCompleted.complete(Result.failure(e))
} catch (_: CancellationException) {} finally {
tunnelJobs.remove(id)
sharedActiveTunnels.update { it - id }
}
}
tunnelJobs[id] = job
job.invokeOnCompletion { tunnelJobs.remove(id) }
try {
startupCompleted.await()
} catch (e: Throwable) {
job.cancel()
Result.failure(e)
}
}
override suspend fun stopTunnel(tunnelId: Int) =
tunMutex.withLock {
val currentState = sharedActiveTunnels.value[tunnelId]?.status ?: return@withLock
updateTunnelStatus(tunnelId, TunnelStatus.Stopping)
tunnelJobs[tunnelId]?.cancel()
withTimeoutOrNull(STOP_TIMEOUT_MS) {
activeTunnels.first {
!it.containsKey(tunnelId) || it[tunnelId]!!.status == TunnelStatus.Down
}
}
?: run {
Timber.w("Stop timeout for $tunnelId (was $currentState); forcing kill")
forceStopTunnel(tunnelId)
}
}
override suspend fun forceStopTunnel(tunnelId: Int) {
backend.forceStopTunnel(tunnelId)
tunnelJobs[tunnelId]?.cancel()
tunnelJobs.remove(tunnelId)
sharedActiveTunnels.update { it - tunnelId }
updateTunnelStatus(tunnelId, TunnelStatus.Down)
}
override suspend fun stopActiveTunnels() {
sharedActiveTunnels.value.forEach { (id, state) ->
if (state.status.isUpOrStarting()) {
stopTunnel(id)
}
}
}
override suspend fun updateTunnelStatus(
tunnelId: Int,
status: TunnelStatus?,
stats: TunnelStatistics?,
pingStates: Map<String, PingState>?,
logHealthState: LogHealthState?,
) =
tunStatusMutex.withLock {
sharedActiveTunnels.update { currentTuns ->
if (!currentTuns.containsKey(tunnelId) && status != TunnelStatus.Starting) {
Timber.d("Ignoring update for inactive tunnel $tunnelId")
return@update currentTuns
}
val existingState = currentTuns[tunnelId] ?: TunnelState()
val newStatus = status ?: existingState.status
if (newStatus == TunnelStatus.Down) {
Timber.d("Removing tunnel $tunnelId from activeTunnels as state is DOWN")
currentTuns - tunnelId
} else if (
existingState.status == newStatus &&
stats == null &&
pingStates == null &&
logHealthState == null
) {
Timber.d("Skipping redundant state update for ${tunnelId}: $newStatus")
currentTuns
} else {
val updated =
existingState.copy(
status = newStatus,
statistics = stats ?: existingState.statistics,
pingStates = pingStates ?: existingState.pingStates,
logHealthState = logHealthState ?: existingState.logHealthState,
)
currentTuns + (tunnelId to updated)
}
}
}
override fun setBackendMode(backendMode: BackendMode) = backend.setBackendMode(backendMode)
override fun getBackendMode(): BackendMode = backend.getBackendMode()
override suspend fun runningTunnelNames(): Set<String> = backend.runningTunnelNames()
override fun handleDnsReresolve(tunnelConfig: TunnelConfig): Boolean =
backend.handleDnsReresolve(tunnelConfig)
override fun getStatistics(tunnelId: Int): TunnelStatistics? = backend.getStatistics(tunnelId)
companion object {
const val STOP_TIMEOUT_MS: Long = 5_000L
}
}
@@ -1,359 +1,99 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel package com.zaneschepke.wireguardautotunnel.core.tunnel
import android.os.PowerManager import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.logcatter.LogReader import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.networkmonitor.NetworkMonitor import com.zaneschepke.wireguardautotunnel.di.Kernel
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager import com.zaneschepke.wireguardautotunnel.di.Userspace
import com.zaneschepke.wireguardautotunnel.core.tunnel.backend.TunnelBackend import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
import com.zaneschepke.wireguardautotunnel.core.tunnel.handler.DynamicDnsHandler import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.core.tunnel.handler.TunnelActiveStatePersister import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.core.tunnel.handler.TunnelMonitorHandler import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.core.tunnel.handler.TunnelServiceHandler import com.zaneschepke.wireguardautotunnel.util.extensions.withData
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException
import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage
import com.zaneschepke.wireguardautotunnel.domain.events.NotAuthorized
import com.zaneschepke.wireguardautotunnel.domain.model.AutoTunnelSettings
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.LockdownSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.state.LogHealthState
import com.zaneschepke.wireguardautotunnel.domain.state.PingState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils
import kotlin.concurrent.atomics.AtomicBoolean
import kotlin.concurrent.atomics.AtomicReference
import kotlin.concurrent.atomics.ExperimentalAtomicApi
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import kotlinx.coroutines.supervisorScope
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import timber.log.Timber import javax.inject.Inject
@OptIn(ExperimentalCoroutinesApi::class, ExperimentalAtomicApi::class) class TunnelManager @Inject constructor(
class TunnelManager( @Kernel private val kernelTunnel: TunnelProvider,
kernelBackend: TunnelBackend, @Userspace private val userspaceTunnel: TunnelProvider,
userspaceBackend: TunnelBackend, private val appDataRepository: AppDataRepository,
proxyUserspaceBackend: TunnelBackend, @ApplicationScope private val applicationScope: CoroutineScope,
networkMonitor: NetworkMonitor, @IoDispatcher private val ioDispatcher: CoroutineDispatcher,
networkUtils: NetworkUtils,
powerManager: PowerManager,
logReader: LogReader,
monitoringSettingsRepository: MonitoringSettingsRepository,
private val serviceManager: ServiceManager,
private val settingsRepository: GeneralSettingRepository,
private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
private val lockdownSettingsRepository: LockdownSettingsRepository,
private val tunnelsRepository: TunnelRepository,
private val applicationScope: CoroutineScope,
private val ioDispatcher: CoroutineDispatcher,
) : TunnelProvider { ) : TunnelProvider {
private val _activeTunnels = MutableStateFlow<Map<Int, TunnelState>>(emptyMap()) val appSettings: StateFlow<AppSettings?> = appDataRepository.settings.flow.stateIn(
override val activeTunnels: StateFlow<Map<Int, TunnelState>> = _activeTunnels.asStateFlow() scope = applicationScope.plus(ioDispatcher),
started = SharingStarted.Eagerly,
initialValue = null,
)
@OptIn(ExperimentalAtomicApi::class) val currentAppMode = AtomicReference(AppMode.VPN) override suspend fun activeTunnels(): StateFlow<List<TunnelConf>> {
return withContext(ioDispatcher) {
appSettings.filterNotNull().first().let {
if (it.isKernelEnabled) return@withContext kernelTunnel.activeTunnels()
userspaceTunnel.activeTunnels()
}
}
}
private val defaultManager = override suspend fun startTunnel(tunnelConf: TunnelConf) {
TunnelLifecycleManager(userspaceBackend, applicationScope, ioDispatcher, _activeTunnels) appSettings.withData {
if (it.isKernelEnabled) return@withData kernelTunnel.startTunnel(tunnelConf)
userspaceTunnel.startTunnel(tunnelConf)
}
}
private val lifecycleManagers: Map<AppMode, TunnelLifecycleManager> = override suspend fun stopTunnel(tunnelConf: TunnelConf?) {
mapOf( appSettings.withData {
AppMode.KERNEL to if (it.isKernelEnabled) return@withData kernelTunnel.stopTunnel(tunnelConf)
TunnelLifecycleManager( userspaceTunnel.stopTunnel(tunnelConf)
kernelBackend, }
applicationScope, }
ioDispatcher,
_activeTunnels,
),
AppMode.VPN to defaultManager,
AppMode.PROXY to
TunnelLifecycleManager(
proxyUserspaceBackend,
applicationScope,
ioDispatcher,
_activeTunnels,
),
AppMode.LOCK_DOWN to
TunnelLifecycleManager(
proxyUserspaceBackend,
applicationScope,
ioDispatcher,
_activeTunnels,
),
)
@OptIn(ExperimentalAtomicApi::class) override suspend fun bounceTunnel(tunnelConf: TunnelConf) {
private fun getProvider(): TunnelProvider { appSettings.withData {
return lifecycleManagers[currentAppMode.load()] ?: defaultManager if (it.isKernelEnabled) return@withData kernelTunnel.stopTunnel(tunnelConf)
} userspaceTunnel.stopTunnel(tunnelConf)
}
}
override suspend fun startTunnel(tunnelConfig: TunnelConfig): Result<Unit> = override suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
getProvider().startTunnel(tunnelConfig) appSettings.withData {
if (it.isKernelEnabled) return@withData kernelTunnel.setBackendState(backendState, allowedIps)
userspaceTunnel.setBackendState(backendState, allowedIps)
}
}
override suspend fun stopTunnel(tunnelId: Int) = getProvider().stopTunnel(tunnelId) override suspend fun runningTunnelNames(): Set<String> {
appSettings.filterNotNull().first().let {
if (it.isKernelEnabled) return kernelTunnel.runningTunnelNames()
return userspaceTunnel.runningTunnelNames()
}
}
override suspend fun forceStopTunnel(tunnelId: Int) = getProvider().forceStopTunnel(tunnelId) suspend fun restorePreviousState() {
withContext(ioDispatcher) {
override suspend fun stopActiveTunnels() = getProvider().stopActiveTunnels() with(appDataRepository.settings.get()) {
if (isRestoreOnBootEnabled) {
override fun setBackendMode(backendMode: BackendMode) = val previouslyActiveTuns = appDataRepository.tunnels.getActive()
getProvider().setBackendMode(backendMode) // handle kernel mode
val tunsToStart = previouslyActiveTuns.filterNot { tun -> activeTunnels().value.any { tun.id == it.id } }
override fun getBackendMode(): BackendMode = getProvider().getBackendMode() if (isKernelEnabled) {
return@withContext tunsToStart.forEach {
override suspend fun runningTunnelNames(): Set<String> = getProvider().runningTunnelNames() startTunnel(it)
}
override fun handleDnsReresolve(tunnelConfig: TunnelConfig): Boolean = }
getProvider().handleDnsReresolve(tunnelConfig) // handle userspace
if (activeTunnels().value.isEmpty()) tunsToStart.firstOrNull()?.let { startTunnel(it) }
override fun getStatistics(tunnelId: Int): TunnelStatistics? = }
getProvider().getStatistics(tunnelId) }
}
override suspend fun updateTunnelStatus( }
tunnelId: Int,
status: TunnelStatus?,
stats: TunnelStatistics?,
pingStates: Map<String, PingState>?,
logHealthState: LogHealthState?,
) = getProvider().updateTunnelStatus(tunnelId, status, stats, pingStates, logHealthState)
@OptIn(ExperimentalCoroutinesApi::class)
private val localErrorEvents = MutableSharedFlow<Pair<String?, BackendCoreException>>()
@OptIn(ExperimentalCoroutinesApi::class)
private val localMessageEvents = MutableSharedFlow<Pair<String?, BackendMessage>>()
override val errorEvents: SharedFlow<Pair<String?, BackendCoreException>> =
merge(localErrorEvents, *lifecycleManagers.values.map { it.errorEvents }.toTypedArray())
.shareIn(
scope = applicationScope + ioDispatcher,
started = SharingStarted.Eagerly,
replay = 0,
)
override val messageEvents: SharedFlow<Pair<String?, BackendMessage>> =
merge(localMessageEvents, *lifecycleManagers.values.map { it.messageEvents }.toTypedArray())
.shareIn(
scope = applicationScope.plus(ioDispatcher),
started = SharingStarted.Eagerly,
replay = 0,
)
private val tunnelServiceHandler =
TunnelServiceHandler(
activeTunnels = activeTunnels,
settingsRepository = settingsRepository,
serviceManager = serviceManager,
applicationScope = applicationScope,
ioDispatcher = ioDispatcher,
)
private val tunnelActiveStatePersister =
TunnelActiveStatePersister(
activeTunnels = activeTunnels,
tunnelsRepository = tunnelsRepository,
applicationScope = applicationScope,
ioDispatcher = ioDispatcher,
)
private val dynamicDnsHandler =
DynamicDnsHandler(
activeTunnels = activeTunnels,
tunnelsRepository = tunnelsRepository,
settingsRepository = settingsRepository,
localMessageEvents = localMessageEvents,
handleDnsReresolve = { config -> handleDnsReresolve(config) },
applicationScope = applicationScope,
ioDispatcher = ioDispatcher,
)
private val fullTunnelMonitorHandler =
TunnelMonitorHandler(
activeTunnels = activeTunnels,
tunnelsRepository = tunnelsRepository,
settingsRepository = settingsRepository,
monitoringSettingsRepository = monitoringSettingsRepository,
networkMonitor = networkMonitor,
networkUtils = networkUtils,
powerManager = powerManager,
logReader = logReader,
getStatistics = { id -> getStatistics(id) },
updateTunnelStatus = { id, status, stats, pings, logHealth ->
updateTunnelStatus(id, status, stats, pings, logHealth)
},
applicationScope = applicationScope,
ioDispatcher = ioDispatcher,
)
init {
applicationScope.launch(ioDispatcher) {
val initialEmit = AtomicBoolean(true)
settingsRepository.flow
.filterNotNull()
.filterNot { it == GeneralSettings() }
.distinctUntilChangedBy { it.appMode }
.collect { settings ->
val isInitialEmit = initialEmit.exchange(false)
val previousMode = currentAppMode.exchange(settings.appMode)
if (isInitialEmit) {
return@collect handleRestore(settings)
}
if (previousMode != settings.appMode) {
handleModeChangeCleanup(previousMode)
}
if (settings.appMode == AppMode.LOCK_DOWN) {
handleLockDownModeInit()
}
}
}
}
// TODO this can crash if we haven't started foreground service yet, especially for
// workerManager
private suspend fun handleLockDownModeInit() {
val lockdownSettings = lockdownSettingsRepository.getLockdownSettings()
val allowedIps =
if (lockdownSettings.bypassLan) TunnelConfig.IPV4_PUBLIC_NETWORKS else emptySet()
try {
if (serviceManager.hasVpnPermission()) {
setBackendMode(
BackendMode.KillSwitch(
allowedIps,
lockdownSettings.metered,
lockdownSettings.dualStack,
)
)
} else {
throw NotAuthorized()
}
} catch (e: BackendCoreException) {
localErrorEvents.tryEmit(null to e)
}
}
private suspend fun handleModeChangeCleanup(previousAppMode: AppMode) {
lifecycleManagers[previousAppMode]?.stopActiveTunnels()
if (previousAppMode == AppMode.LOCK_DOWN) {
lifecycleManagers[previousAppMode]?.setBackendMode(BackendMode.Inactive)
}
}
suspend fun handleRestore(settings: GeneralSettings? = null) =
withContext(ioDispatcher) {
val currentSettings = settings ?: settingsRepository.getGeneralSettings()
val autoTunnelSettings = autoTunnelSettingsRepository.getAutoTunnelSettings()
val tunnels = tunnelsRepository.userTunnelsFlow.firstOrNull()
if (autoTunnelSettings.isAutoTunnelEnabled)
return@withContext restoreAutoTunnel(autoTunnelSettings)
if (currentSettings.appMode == AppMode.LOCK_DOWN) handleLockDownModeInit()
if (tunnels?.any { it.isActive } == true) {
if (currentSettings.appMode == AppMode.VPN && !serviceManager.hasVpnPermission())
return@withContext localErrorEvents.emit(null to NotAuthorized())
when (currentSettings.appMode) {
AppMode.VPN,
AppMode.PROXY,
AppMode.LOCK_DOWN -> {
tunnels.firstOrNull { it.isActive }?.let { startTunnel(it) }
}
AppMode.KERNEL ->
tunnels.filter { it.isActive }.forEach { conf -> startTunnel(conf) }
}
}
}
private suspend fun restoreAutoTunnel(autoTunnelSettings: AutoTunnelSettings) {
autoTunnelSettingsRepository.upsert(autoTunnelSettings.copy(isAutoTunnelEnabled = true))
serviceManager.startAutoTunnelService()
}
suspend fun handleReboot() =
withContext(ioDispatcher) {
val settings = settingsRepository.getGeneralSettings()
val autoTunnelSettings = autoTunnelSettingsRepository.getAutoTunnelSettings()
val defaultTunnel = tunnelsRepository.getDefaultTunnel()
if (autoTunnelSettings.startOnBoot)
return@withContext restoreAutoTunnel(autoTunnelSettings)
if (settings.isRestoreOnBootEnabled) {
tunnelsRepository.resetActiveTunnels()
when (settings.appMode) {
AppMode.LOCK_DOWN -> handleLockDownModeInit()
AppMode.VPN ->
if (!serviceManager.hasVpnPermission())
return@withContext localErrorEvents.emit(null to NotAuthorized())
AppMode.KERNEL,
AppMode.PROXY -> Unit
}
defaultTunnel?.let { startTunnel(it) }
}
}
suspend fun restartActiveTunnel(id: Int) =
withContext(ioDispatcher) {
val activeIds = activeTunnels.value.keys.toList()
if (activeIds.isEmpty()) return@withContext
if (!activeIds.contains(id)) return@withContext
val tunnel = tunnelsRepository.getById(id) ?: return@withContext
restartTunnel(tunnel)
}
suspend fun restartActiveTunnels() =
withContext(ioDispatcher) {
val activeIds = activeTunnels.value.keys.toList()
if (activeIds.isEmpty()) return@withContext
val tunnels = tunnelsRepository.getAll()
if (tunnels.isEmpty()) return@withContext
supervisorScope {
activeIds.forEach { id ->
val tunnel =
tunnels.find { it.id == id }
?: run {
Timber.w("Tunnel config $id not found; skipping restart")
return@forEach
}
restartTunnel(tunnel)
}
}
}
private suspend fun restartTunnel(tunnel: TunnelConfig) {
runCatching { stopTunnel(tunnel.id) }
.onFailure { e -> Timber.e(e, "Failed to stop tunnel ${tunnel.id} during restart") }
delay(RESTART_TUNNEL_DELAY)
runCatching { startTunnel(tunnel) }
.onFailure { e -> Timber.e(e, "Failed to restart tunnel ${tunnel.id}") }
}
companion object {
const val RESTART_TUNNEL_DELAY = 300L
}
} }
@@ -1,45 +1,24 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException
import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.state.LogHealthState
import com.zaneschepke.wireguardautotunnel.domain.state.PingState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
interface TunnelProvider { interface TunnelProvider {
suspend fun startTunnel(tunnelConfig: TunnelConfig): Result<Unit>
suspend fun stopTunnel(tunnelId: Int) suspend fun activeTunnels(): StateFlow<List<TunnelConf>>
suspend fun forceStopTunnel(tunnelId: Int) suspend fun startTunnel(tunnelConf: TunnelConf)
suspend fun stopActiveTunnels() suspend fun stopTunnel(tunnelConf: TunnelConf? = null)
fun setBackendMode(backendMode: BackendMode) suspend fun bounceTunnel(tunnelConf: TunnelConf)
fun getBackendMode(): BackendMode suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>)
suspend fun runningTunnelNames(): Set<String> suspend fun runningTunnelNames(): Set<String>
fun handleDnsReresolve(tunnelConfig: TunnelConfig): Boolean companion object {
const val CHECK_INTERVAL = 1_000L
fun getStatistics(tunnelId: Int): TunnelStatistics? }
val activeTunnels: StateFlow<Map<Int, TunnelState>>
val errorEvents: SharedFlow<Pair<String?, BackendCoreException>>
val messageEvents: SharedFlow<Pair<String?, BackendMessage>>
suspend fun updateTunnelStatus(
tunnelId: Int,
status: TunnelStatus? = null,
stats: TunnelStatistics? = null,
pingStates: Map<String, PingState>? = null,
logHealthState: LogHealthState? = null,
)
} }
@@ -0,0 +1,83 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.wireguard.android.backend.BackendException
import com.zaneschepke.wireguardautotunnel.core.network.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.state.AmneziaStatistics
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.util.extensions.asAmBackendState
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendError
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.withContext
import org.amnezia.awg.backend.Backend
import org.amnezia.awg.backend.Tunnel
import timber.log.Timber
import javax.inject.Inject
class UserspaceTunnel @Inject constructor(
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
@ApplicationScope private val applicationScope: CoroutineScope,
serviceManager: ServiceManager,
appDataRepository: AppDataRepository,
notificationManager: NotificationManager,
private val backend: Backend,
networkMonitor: NetworkMonitor,
) : TunnelProvider, BaseTunnel(ioDispatcher, applicationScope, networkMonitor, appDataRepository, serviceManager, notificationManager) {
override suspend fun startTunnel(tunnelConf: TunnelConf) {
withContext(ioDispatcher) {
super.startTunnel(tunnelConf)
runCatching {
backend.setState(tunnelConf, Tunnel.State.UP, tunnelConf.toAmConfig())
addToActiveTunnels(tunnelConf)
}.onFailure {
onTunnelStop(tunnelConf)
if (it is BackendException) {
handleBackendThrowable(it.toBackendError())
} else {
Timber.e(it)
}
}
}
}
override suspend fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics {
return AmneziaStatistics(backend.getStatistics(tunnelConf))
}
override suspend fun toggleTunnel(tunnelConf: TunnelConf, status: TunnelStatus) {
when (status) {
TunnelStatus.UP -> backend.setState(tunnelConf, Tunnel.State.UP, tunnelConf.toAmConfig())
TunnelStatus.DOWN -> backend.setState(tunnelConf, Tunnel.State.DOWN, tunnelConf.toAmConfig())
}
}
override suspend fun stopTunnel(tunnelConf: TunnelConf?) {
withContext(ioDispatcher) {
runCatching {
tunnels.value.firstOrNull { it.id == tunnelConf?.id }?.let {
backend.setState(it, Tunnel.State.DOWN, it.toAmConfig())
onTunnelStop(it)
} ?: stopAllTunnels()
}.onFailure {
Timber.e(it)
}
}
}
override suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
backend.setBackendState(backendState.asAmBackendState(), allowedIps)
}
override suspend fun runningTunnelNames(): Set<String> {
return backend.runningTunnelNames
}
}
@@ -1,128 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel.backend
import com.wireguard.android.backend.Backend
import com.wireguard.android.backend.BackendException
import com.wireguard.android.backend.Tunnel
import com.wireguard.android.backend.WgQuickBackend
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.events.DnsFailure
import com.zaneschepke.wireguardautotunnel.domain.events.InvalidConfig
import com.zaneschepke.wireguardautotunnel.domain.events.KernelTunnelName
import com.zaneschepke.wireguardautotunnel.domain.events.KernelWireguardNotSupported
import com.zaneschepke.wireguardautotunnel.domain.events.UnknownError
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.domain.state.WireGuardStatistics
import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendCoreException
import java.util.concurrent.ConcurrentHashMap
import java.util.regex.Pattern
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.launch
import timber.log.Timber
class KernelTunnel(private val runConfigHelper: RunConfigHelper, private val backend: Backend) :
TunnelBackend {
private val runtimeTunnels = ConcurrentHashMap<Int, Tunnel>()
private fun validateWireGuardInterfaceName(name: String): Result<Unit> {
if (name.isEmpty() || name.length > 15)
return Result.failure(KernelTunnelName(R.string.kernel_name_error))
if (name == "." || name == "..") {
return Result.failure(KernelTunnelName(R.string.kernel_name_dots))
}
val pattern = Pattern.compile("^[a-zA-Z0-9_=+.-]{1,15}$")
if (!pattern.matcher(name).matches()) {
return Result.failure(KernelTunnelName(R.string.kernel_name_special_characters))
}
return Result.success(Unit)
}
override fun tunnelStateFlow(tunnelConfig: TunnelConfig): Flow<TunnelStatus> = callbackFlow {
if (!WgQuickBackend.hasKernelSupport()) throw KernelWireguardNotSupported()
validateWireGuardInterfaceName(tunnelConfig.name).onFailure { throw it }
val stateChannel = Channel<Tunnel.State>()
val runtimeTunnel = RuntimeWgTunnel(tunnelConfig, stateChannel)
runtimeTunnels[tunnelConfig.id] = runtimeTunnel
val consumerJob = launch {
stateChannel.consumeAsFlow().collect { state -> trySend(state.asTunnelState()) }
}
try {
val runConfig = runConfigHelper.buildWgRunConfig(tunnelConfig)
backend.setState(runtimeTunnel, Tunnel.State.UP, runConfig)
} catch (e: TimeoutCancellationException) {
Timber.Forest.e("Startup timed out for ${tunnelConfig.name}")
throw DnsFailure()
} catch (e: BackendException) {
throw e.toBackendCoreException()
} catch (e: IllegalArgumentException) {
Timber.Forest.e(e, "Invalid backend arguments")
throw InvalidConfig()
} catch (e: Exception) {
Timber.Forest.e(e, "Error while setting tunnel state")
throw UnknownError()
}
awaitClose {
try {
backend.setState(runtimeTunnel, Tunnel.State.DOWN, null)
} catch (e: BackendException) {
// Errors are emitted by caller (lifecycle manager)
} finally {
consumerJob.cancel()
stateChannel.close()
runtimeTunnels.remove(tunnelConfig.id)
trySend(TunnelStatus.Down)
}
}
}
override fun getStatistics(tunnelId: Int): TunnelStatistics? {
return try {
val runtimeTunnel = runtimeTunnels[tunnelId] ?: return null
WireGuardStatistics(backend.getStatistics(runtimeTunnel))
} catch (e: Exception) {
Timber.Forest.e(e, "Failed to get stats for $tunnelId")
null
}
}
override fun setBackendMode(backendMode: BackendMode) {
Timber.Forest.w("Not yet implemented for kernel")
}
override fun getBackendMode(): BackendMode {
return BackendMode.Inactive
}
override fun handleDnsReresolve(tunnelConfig: TunnelConfig): Boolean {
throw NotImplementedError()
}
override suspend fun runningTunnelNames(): Set<String> {
return backend.runningTunnelNames
}
override suspend fun forceStopTunnel(tunnelId: Int) {
val runtimeTunnel = runtimeTunnels[tunnelId] ?: return
try {
backend.setState(runtimeTunnel, Tunnel.State.DOWN, null)
} catch (e: BackendException) {
Timber.Forest.e(e, "Force stop failed for $tunnelId")
} finally {
runtimeTunnels.remove(tunnelId)
}
}
}
@@ -1,101 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel.backend
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.data.model.DnsProtocol
import com.zaneschepke.wireguardautotunnel.domain.events.InvalidConfig
import com.zaneschepke.wireguardautotunnel.domain.model.DnsSettings
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings
import com.zaneschepke.wireguardautotunnel.domain.model.ProxySettings
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.repository.DnsSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.ProxySettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import java.util.Optional
import kotlinx.coroutines.flow.firstOrNull
import org.amnezia.awg.config.Config
import org.amnezia.awg.config.proxy.HttpProxy
import org.amnezia.awg.config.proxy.Socks5Proxy
class RunConfigHelper(
private val settingsRepository: GeneralSettingRepository,
private val proxySettingsRepository: ProxySettingsRepository,
private val dnsSettingsRepository: DnsSettingsRepository,
private val tunnelsRepository: TunnelRepository,
) {
private data class PrepResult(
val effectiveConfig: TunnelConfig,
val generalSettings: GeneralSettings,
val dnsSettings: DnsSettings,
)
private suspend fun prepare(tunnelConfig: TunnelConfig): PrepResult {
val generalSettings = settingsRepository.getGeneralSettings()
val dnsSettings = dnsSettingsRepository.getDnsSettings()
val effectiveConfig =
if (
generalSettings.isGlobalSplitTunnelEnabled || dnsSettings.isGlobalTunnelDnsEnabled
) {
val globalConfig =
tunnelsRepository.globalTunnelFlow.firstOrNull() ?: throw InvalidConfig()
tunnelConfig.copyWithGlobalValues(
globalConfig,
dnsSettings.isGlobalTunnelDnsEnabled,
generalSettings.isGlobalSplitTunnelEnabled,
)
} else {
tunnelConfig
}
return PrepResult(effectiveConfig, generalSettings, dnsSettings)
}
suspend fun buildAmRunConfig(tunnelConfig: TunnelConfig): Config {
val prep = prepare(tunnelConfig)
val proxies =
if (prep.generalSettings.appMode == AppMode.PROXY) {
val proxySettings = proxySettingsRepository.getProxySettings()
buildList {
if (proxySettings.socks5ProxyEnabled) {
add(
Socks5Proxy(
proxySettings.socks5ProxyBindAddress
?: ProxySettings.DEFAULT_SOCKS_BIND_ADDRESS,
proxySettings.proxyUsername,
proxySettings.proxyPassword,
)
)
}
if (proxySettings.httpProxyEnabled) {
add(
HttpProxy(
proxySettings.httpProxyBindAddress
?: ProxySettings.DEFAULT_HTTP_BIND_ADDRESS,
proxySettings.proxyUsername,
proxySettings.proxyPassword,
)
)
}
}
} else {
emptyList()
}
val amConfig = prep.effectiveConfig.toAmConfig()
return Config.Builder()
.setInterface(amConfig.`interface`)
.addPeers(amConfig.peers)
.addProxies(proxies)
.setDnsSettings(
org.amnezia.awg.config.DnsSettings(
prep.dnsSettings.dnsProtocol == DnsProtocol.DOH,
Optional.ofNullable(prep.dnsSettings.dnsEndpoint),
)
)
.build()
}
suspend fun buildWgRunConfig(tunnelConfig: TunnelConfig): com.wireguard.config.Config {
val prep = prepare(tunnelConfig)
return prep.effectiveConfig.toWgConfig()
}
}
@@ -1,21 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel.backend
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import kotlinx.coroutines.channels.Channel
import org.amnezia.awg.backend.Tunnel
class RuntimeAwgTunnel(
private val tunnelConfig: TunnelConfig,
private val stateChannel: Channel<Tunnel.State>,
) : Tunnel {
override fun getName() = tunnelConfig.name
override fun onStateChange(newState: Tunnel.State) {
stateChannel.trySend(newState)
}
override fun isIpv4ResolutionPreferred() = tunnelConfig.isIpv4Preferred
override fun isMetered() = tunnelConfig.isMetered
}
@@ -1,19 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel.backend
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import kotlinx.coroutines.channels.Channel
class RuntimeWgTunnel(
private val config: TunnelConfig,
private val stateChannel: Channel<Tunnel.State>,
) : Tunnel {
override fun getName() = config.name
override fun onStateChange(newState: Tunnel.State) {
stateChannel.trySend(newState)
}
override fun isIpv4ResolutionPreferred() = config.isIpv4Preferred
}
@@ -1,23 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel.backend
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import kotlinx.coroutines.flow.Flow
interface TunnelBackend {
fun tunnelStateFlow(tunnelConfig: TunnelConfig): Flow<TunnelStatus>
fun getStatistics(tunnelId: Int): TunnelStatistics?
fun setBackendMode(backendMode: BackendMode)
fun getBackendMode(): BackendMode
fun handleDnsReresolve(tunnelConfig: TunnelConfig): Boolean
suspend fun runningTunnelNames(): Set<String>
suspend fun forceStopTunnel(tunnelId: Int)
}
@@ -1,119 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel.backend
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.events.DnsFailure
import com.zaneschepke.wireguardautotunnel.domain.events.InvalidConfig
import com.zaneschepke.wireguardautotunnel.domain.events.ServiceNotRunning
import com.zaneschepke.wireguardautotunnel.domain.events.UnknownError
import com.zaneschepke.wireguardautotunnel.domain.events.VpnUnauthorized
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.state.AmneziaStatistics
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.util.extensions.asAmBackendMode
import com.zaneschepke.wireguardautotunnel.util.extensions.asBackendMode
import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendCoreException
import java.io.IOException
import java.util.concurrent.ConcurrentHashMap
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.launch
import org.amnezia.awg.backend.Backend
import org.amnezia.awg.backend.BackendException
import org.amnezia.awg.backend.Tunnel
import timber.log.Timber
class UserspaceTunnel(private val backend: Backend, private val runConfigHelper: RunConfigHelper) :
TunnelBackend {
private val runtimeTunnels = ConcurrentHashMap<Int, Tunnel>()
override fun tunnelStateFlow(tunnelConfig: TunnelConfig): Flow<TunnelStatus> = callbackFlow {
val stateChannel = Channel<Tunnel.State>()
val runtimeTunnel = RuntimeAwgTunnel(tunnelConfig, stateChannel)
runtimeTunnels[tunnelConfig.id] = runtimeTunnel
val consumerJob = launch {
stateChannel.consumeAsFlow().collect { awgState -> trySend(awgState.asTunnelState()) }
}
try {
val runConfig = runConfigHelper.buildAmRunConfig(tunnelConfig)
backend.setState(runtimeTunnel, Tunnel.State.UP, runConfig)
} catch (_: TimeoutCancellationException) {
Timber.e("Startup timed out for ${tunnelConfig.name} (likely DNS hang)")
throw DnsFailure()
} catch (e: BackendException) {
throw e.toBackendCoreException()
} catch (_: IllegalArgumentException) {
throw InvalidConfig()
} catch (e: Exception) {
Timber.e(e, "Error while setting tunnel state")
throw UnknownError()
}
awaitClose {
try {
backend.setState(runtimeTunnel, Tunnel.State.DOWN, null)
} catch (e: BackendException) {
// Errors emitted by caller
} finally {
consumerJob.cancel()
stateChannel.close()
runtimeTunnels.remove(tunnelConfig.id)
trySend(TunnelStatus.Down)
}
}
}
override fun setBackendMode(backendMode: BackendMode) {
Timber.d("Setting backend mode: $backendMode")
try {
backend.backendMode = backendMode.asAmBackendMode()
} catch (e: BackendException) {
throw e.toBackendCoreException()
} catch (_: IOException) {
throw VpnUnauthorized()
}
}
override fun getBackendMode(): BackendMode {
return backend.backendMode.asBackendMode()
}
override fun handleDnsReresolve(tunnelConfig: TunnelConfig): Boolean {
val tunnel = runtimeTunnels[tunnelConfig.id] ?: throw ServiceNotRunning()
return backend.resolveDDNS(tunnelConfig.toAmConfig(), tunnel.isIpv4ResolutionPreferred)
}
override suspend fun runningTunnelNames(): Set<String> {
return backend.runningTunnelNames
}
override fun getStatistics(tunnelId: Int): TunnelStatistics? {
return try {
val runtimeTunnel = runtimeTunnels[tunnelId] ?: return null
AmneziaStatistics(backend.getStatistics(runtimeTunnel))
} catch (e: Exception) {
Timber.e(e, "Failed to get stats for $tunnelId")
null
}
}
override suspend fun forceStopTunnel(tunnelId: Int) {
val runtimeTunnel = runtimeTunnels[tunnelId] ?: return
try {
backend.setState(runtimeTunnel, Tunnel.State.DOWN, null)
} catch (e: BackendException) {
Timber.e(e, "Force stop failed for $tunnelId")
} finally {
runtimeTunnels.remove(tunnelId)
}
}
}
@@ -1,114 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel.handler
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import java.util.concurrent.ConcurrentHashMap
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import timber.log.Timber
class DynamicDnsHandler(
private val activeTunnels: StateFlow<Map<Int, TunnelState>>,
private val tunnelsRepository: TunnelRepository,
private val settingsRepository: GeneralSettingRepository,
private val localMessageEvents: MutableSharedFlow<Pair<String?, BackendMessage>>,
private val handleDnsReresolve: (TunnelConfig) -> Boolean,
private val applicationScope: CoroutineScope,
private val ioDispatcher: CoroutineDispatcher,
) {
private val mutex = Mutex()
private val jobs = ConcurrentHashMap<Int, Job>()
init {
applicationScope.launch(ioDispatcher) {
combine(activeTunnels, settingsRepository.flow.filterNotNull()) { active, settings ->
active to settings
}
.collect { (activeTuns, settings) ->
mutex.withLock {
val activeIds =
activeTuns.keys
.filter { id ->
val config =
tunnelsRepository.getById(id) ?: return@filter false
config.restartOnPingFailure &&
settings.appMode != AppMode.KERNEL
}
.toSet()
(jobs.keys - activeIds).forEach { id ->
Timber.d("Shutting down Dynamic DNS monitoring job for tunnelId: $id")
jobs.remove(id)?.cancel()
}
activeIds.forEach { id ->
if (jobs.containsKey(id)) return@forEach
val config = tunnelsRepository.getById(id) ?: return@forEach
val tunStateFlow =
activeTunnels
.map { it[id] }
.stateIn(applicationScope + ioDispatcher)
Timber.d("Starting Dynamic DNS monitoring job for tunnelId: $id")
jobs[id] =
applicationScope.launch(ioDispatcher) {
monitorDynamicDns(config, tunStateFlow)
}
}
}
}
}
}
private suspend fun monitorDynamicDns(
config: TunnelConfig,
tunStateFlow: StateFlow<TunnelState?>,
) {
var backoff = BASE_BACKOFF
while (true) {
val state = tunStateFlow.value ?: break
if (state.health() != TunnelState.Health.UNHEALTHY) {
backoff = BASE_BACKOFF
tunStateFlow.first { it?.health() == TunnelState.Health.UNHEALTHY || it == null }
continue
}
runCatching {
val updated = handleDnsReresolve(config)
if (updated) {
localMessageEvents.emit(config.name to BackendMessage.DynamicDnsSuccess)
backoff = BASE_BACKOFF
} else {
Timber.i(
"Dynamic DNS check completed, current endpoint address is already up to date."
)
}
}
.onFailure { Timber.e(it, "Failed to handle dns re-resolution for ${config.name}") }
delay(backoff)
backoff = (backoff * 1.5).toLong().coerceAtMost(MAX_BACKOFF_TIME)
}
}
companion object {
const val BASE_BACKOFF = 30_000L
const val MAX_BACKOFF_TIME = 300_000L
}
}
@@ -1,47 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel.handler
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
class TunnelActiveStatePersister(
private val activeTunnels: StateFlow<Map<Int, TunnelState>>,
private val tunnelsRepository: TunnelRepository,
applicationScope: CoroutineScope,
ioDispatcher: CoroutineDispatcher,
) {
private var previousActiveIds: Set<Int> = emptySet()
init {
applicationScope.launch(ioDispatcher) {
activeTunnels.collect { currentActive ->
val currentActiveIds = currentActive.keys
if (currentActiveIds == previousActiveIds) return@collect
val tunnels = tunnelsRepository.userTunnelsFlow.firstOrNull() ?: return@collect
val tunnelsById = tunnels.associateBy { it.id }
val relevantIds = previousActiveIds + currentActiveIds
supervisorScope {
relevantIds.forEach { id ->
launch {
val config = tunnelsById[id] ?: return@launch
val wasActive = previousActiveIds.contains(id)
val isActive = currentActiveIds.contains(id)
if (wasActive != isActive) {
tunnelsRepository.save(config.copy(isActive = isActive))
}
}
}
}
previousActiveIds = currentActiveIds.toSet()
}
}
}
}
@@ -1,390 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel.handler
import android.os.PowerManager
import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.state.FailureReason
import com.zaneschepke.wireguardautotunnel.domain.state.LogHealthState
import com.zaneschepke.wireguardautotunnel.domain.state.PingState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.util.extensions.toMillis
import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils
import inet.ipaddr.AddressValueException
import inet.ipaddr.IPAddress
import inet.ipaddr.IPAddressString
import io.ktor.util.collections.ConcurrentMap
import java.util.concurrent.ConcurrentHashMap
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withTimeout
import timber.log.Timber
class TunnelMonitorHandler(
private val activeTunnels: StateFlow<Map<Int, TunnelState>>,
private val tunnelsRepository: TunnelRepository,
private val settingsRepository: GeneralSettingRepository,
private val monitoringSettingsRepository: MonitoringSettingsRepository,
private val networkMonitor: NetworkMonitor,
private val networkUtils: NetworkUtils,
private val logReader: LogReader,
private val powerManager: PowerManager,
private val getStatistics: (Int) -> TunnelStatistics?,
private val updateTunnelStatus:
suspend (
Int, TunnelStatus?, TunnelStatistics?, Map<String, PingState>?, LogHealthState?,
) -> Unit,
private val applicationScope: CoroutineScope,
private val ioDispatcher: CoroutineDispatcher,
) {
private val mutex = Mutex()
private val jobs = ConcurrentHashMap<Int, Job>()
init {
applicationScope.launch(ioDispatcher) {
activeTunnels.collect { activeTuns ->
mutex.withLock {
val activeIds = activeTuns.keys.toSet()
(jobs.keys - activeIds).forEach { id ->
Timber.d("Shutting down tunnel monitoring job for tunnelId: $id")
jobs.remove(id)?.cancel()
}
val tunnels = tunnelsRepository.flow.firstOrNull() ?: return@collect
val tunnelsById = tunnels.associateBy { it.id }
activeIds.forEach { id ->
if (jobs.containsKey(id)) return@forEach
val config = tunnelsById[id] ?: return@forEach
val settings = settingsRepository.flow.filterNotNull().first()
val tunStateFlow =
activeTunnels.map { it[id] }.stateIn(applicationScope + ioDispatcher)
jobs[id] =
applicationScope.launch(ioDispatcher) {
Timber.d("Starting tunnel monitoring job for tunnelId: $id")
startMonitoring(
config = config,
withLogs = settings.appMode != AppMode.KERNEL,
tunStateFlow = tunStateFlow,
getStatistics = { tunnelId -> getStatistics(tunnelId) },
updateTunnelStatus = { tid, _, stats, pings, logHealth ->
updateTunnelStatus(tid, null, stats, pings, logHealth)
},
)
}
}
}
}
}
}
@OptIn(FlowPreview::class)
private suspend fun startMonitoring(
config: TunnelConfig,
withLogs: Boolean,
tunStateFlow: StateFlow<TunnelState?>,
getStatistics: suspend (Int) -> TunnelStatistics?,
updateTunnelStatus:
suspend (
Int, TunnelStatus?, TunnelStatistics?, Map<String, PingState>?, LogHealthState?,
) -> Unit,
) = coroutineScope {
launch { startPingMonitor(config, tunStateFlow, updateTunnelStatus) }
launch { startWgStatsPoll(config.id, getStatistics, updateTunnelStatus) }
if (withLogs) launch { startLogsMonitor(config, updateTunnelStatus) }
}
private suspend fun startLogsMonitor(
tunnelConfig: TunnelConfig,
updateTunnelStatus:
suspend (
Int, TunnelStatus?, TunnelStatistics?, Map<String, PingState>?, LogHealthState?,
) -> Unit,
) {
logReader.liveLogs
.filter { log -> log.tag.contains(tunnelConfig.name) }
.mapNotNull { log ->
val now = System.currentTimeMillis()
when {
successLogRegex.containsMatchIn(log.message) ->
LogHealthState(isHealthy = true, timestamp = now)
failureLogRegex.containsMatchIn(log.message) ->
LogHealthState(isHealthy = false, timestamp = now)
else -> null
}
}
.distinctUntilChangedBy { it.isHealthy }
.collect { logHealthState ->
Timber.d("Tunnel log health updated for ${tunnelConfig.name}: $logHealthState")
updateTunnelStatus(tunnelConfig.id, null, null, null, logHealthState)
}
}
private suspend fun startPingMonitor(
tunnelConfig: TunnelConfig,
tunStateFlow: StateFlow<TunnelState?>,
updateTunnelStatus:
suspend (
Int, TunnelStatus?, TunnelStatistics?, Map<String, PingState>?, LogHealthState?,
) -> Unit,
) = coroutineScope {
val pingStatsFlow = MutableStateFlow<Map<String, PingState>>(emptyMap())
val connectivityStateFlow = networkMonitor.connectivityStateFlow.stateIn(this)
val isNetworkConnected = connectivityStateFlow.map { it.hasInternet() }.stateIn(this)
combine(
settingsRepository.flow.distinctUntilChangedBy { it.appMode },
monitoringSettingsRepository.flow,
) { settings, monitorSettings ->
Pair(settings.appMode, monitorSettings)
}
.collectLatest { (appMode, settings) ->
if (!settings.isPingEnabled) return@collectLatest
// TODO for now until we get monitoring for these modes
if (appMode == AppMode.LOCK_DOWN || appMode == AppMode.PROXY) return@collectLatest
Timber.d("Starting pinger for ${tunnelConfig.name} with settings")
val config = tunnelConfig.toAmConfig()
val pingablePeers = config.peers.filter { it.allowedIps.isNotEmpty() }
if (pingablePeers.isEmpty()) return@collectLatest
suspend fun performPing() {
val updates = ConcurrentMap<String, PingState>()
pingablePeers
.map { it.publicKey.toBase64() to it }
.forEach { (key, peer) ->
ensureActive()
val previousState = pingStatsFlow.value[key] ?: PingState()
val allowedIpStr = peer.allowedIps.firstOrNull()?.toString()
if (allowedIpStr == null) {
updates[key] =
previousState.copy(
isReachable = false,
failureReason = FailureReason.NoResolvedEndpoint,
lastPingAttemptMillis = System.currentTimeMillis(),
)
return@forEach
}
val host =
tunnelConfig.pingTarget
?: run {
val parts = allowedIpStr.split("/")
val internalIp =
if (parts.size == 2) parts[0] else allowedIpStr
val prefix =
if (parts.size == 2) parts[1].toIntOrNull() ?: 32
else 32
val cleanedIp = internalIp.removeSurrounding("[", "]")
val defaultCloudflare =
if (cleanedIp.contains(":")) CLOUDFLARE_IPV6_IP
else CLOUDFLARE_IPV4_IP
if (prefix <= 1) {
defaultCloudflare
} else {
try {
val addrStr = IPAddressString(cleanedIp)
val addr: IPAddress =
addrStr.address
?: throw AddressValueException(
"Invalid IP: $cleanedIp"
)
val isIpv6 = addr.isIPv6
val cloudflareIp =
if (isIpv6) CLOUDFLARE_IPV6_IP
else CLOUDFLARE_IPV4_IP
val max = if (isIpv6) 128 else 32
if (prefix == max) {
addr.toCanonicalString()
} else {
val nextAddr: IPAddress? = addr.increment(1)
nextAddr?.toCanonicalString() ?: cloudflareIp
}
} catch (e: AddressValueException) {
Timber.e(
e,
"Failed to parse or increment IP: $cleanedIp",
)
defaultCloudflare
}
}
}
val attemptTime = System.currentTimeMillis()
val timeout = settings.tunnelPingTimeoutSeconds?.toMillis() ?: 5000L
runCatching {
withTimeout(
settings.tunnelPingTimeoutSeconds?.toMillis() ?: 5000L
) {
val pingStats =
settings.tunnelPingTimeoutSeconds?.let {
networkUtils.pingWithStats(
host,
settings.tunnelPingAttempts,
it.toMillis(),
)
}
?: networkUtils.pingWithStats(
host,
settings.tunnelPingAttempts,
)
updates[key] =
previousState.copy(
transmitted = pingStats.transmitted,
received = pingStats.received,
packetLoss = pingStats.packetLoss,
rttMin = pingStats.rttMin,
rttMax = pingStats.rttMax,
rttAvg = pingStats.rttAvg,
rttStddev = pingStats.rttStddev,
isReachable = pingStats.isReachable,
failureReason =
if (pingStats.isReachable) null
else FailureReason.PingFailed,
lastSuccessfulPingMillis =
pingStats.lastSuccessfulPingMillis
?: previousState.lastSuccessfulPingMillis,
pingTarget = host,
lastPingAttemptMillis = attemptTime,
)
Timber.d(
"Ping completed for peer ${peer.publicKey.toBase64().substring(0, 5)}.. to host $host with stats: $pingStats"
)
}
}
.onFailure {
Timber.e(
it,
"Ping failed for peer ${peer.publicKey} in ${tunnelConfig.name} to host $host",
)
updates[key] =
previousState.copy(
isReachable = false,
failureReason = FailureReason.PingFailed,
pingTarget = host,
lastPingAttemptMillis = attemptTime,
)
}
}
if (updates.isNotEmpty()) {
ensureActive()
pingStatsFlow.update { updates }
updateTunnelStatus(tunnelConfig.id, null, null, updates, null)
}
}
// Wait for the tunnel to be fully active
tunStateFlow.filter { state -> state?.status is TunnelStatus.Up }.first()
// small delay to make sure tunnel is fully up before we actively monitor
delay(PING_MONITOR_START_DELAY)
while (isActive) {
ensureActive()
if (!powerManager.isDeviceIdleMode) {
if (isNetworkConnected.value) {
performPing()
} else {
pingStatsFlow.update { current ->
current.mapValues { entry ->
entry.value.copy(
isReachable = false,
failureReason = FailureReason.NoConnectivity,
lastPingAttemptMillis = System.currentTimeMillis(),
)
}
}
ensureActive()
updateTunnelStatus(
tunnelConfig.id,
null,
null,
pingStatsFlow.value,
null,
)
}
}
delay(settings.tunnelPingIntervalSeconds.toMillis())
}
}
}
private suspend fun startWgStatsPoll(
tunnelId: Int,
getStatistics: suspend (Int) -> TunnelStatistics?,
updateTunnelStatus:
suspend (
Int, TunnelStatus?, TunnelStatistics?, Map<String, PingState>?, LogHealthState?,
) -> Unit,
) = coroutineScope {
while (isActive) {
ensureActive()
if (!powerManager.isDeviceIdleMode) {
val stats = getStatistics(tunnelId)
ensureActive()
updateTunnelStatus(tunnelId, null, stats, null, null)
}
delay(STATS_DELAY)
}
}
companion object {
private val successLogRegex =
Regex("Received handshake response|Receiving keepalive packet", RegexOption.IGNORE_CASE)
private val failureLogRegex =
Regex(
"Failed to send handshake initiation: write udp|" +
"Handshake did not complete after 5 seconds, retrying|" +
"Failed to send data packets",
RegexOption.IGNORE_CASE,
)
const val CLOUDFLARE_IPV6_IP = "2606:4700:4700::1111"
const val CLOUDFLARE_IPV4_IP = "1.1.1.1"
const val STATS_DELAY = 1_000L
const val PING_MONITOR_START_DELAY = 5_000L
}
}
@@ -1,36 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel.handler
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
import timber.log.Timber
class TunnelServiceHandler(
private val activeTunnels: StateFlow<Map<Int, TunnelState>>,
private val settingsRepository: GeneralSettingRepository,
private val serviceManager: ServiceManager,
applicationScope: CoroutineScope,
ioDispatcher: CoroutineDispatcher,
) {
init {
applicationScope.launch(ioDispatcher) {
activeTunnels.collect { activeTuns ->
if (activeTuns.isEmpty()) {
Timber.d("Stopping tunnel service, no tunnels active.")
serviceManager.stopTunnelService()
} else if (serviceManager.tunnelService.value == null) {
val settings = settingsRepository.flow.firstOrNull() ?: GeneralSettings()
Timber.d("Starting tunnel foreground service for active tunnel.")
serviceManager.startTunnelService(settings.appMode)
}
serviceManager.updateTunnelTile()
}
}
}
}
@@ -1,55 +1,60 @@
package com.zaneschepke.wireguardautotunnel.core.worker package com.zaneschepke.wireguardautotunnel.core.worker
import android.content.Context import android.content.Context
import androidx.hilt.work.HiltWorker
import androidx.work.CoroutineWorker import androidx.work.CoroutineWorker
import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager import androidx.work.WorkManager
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import java.util.concurrent.TimeUnit import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.TimeUnit
class ServiceWorker( @HiltWorker
context: Context, class ServiceWorker @AssistedInject constructor(
params: WorkerParameters, @Assisted private val context: Context,
private val serviceManager: ServiceManager, @Assisted private val params: WorkerParameters,
private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository, private val serviceManager: ServiceManager,
private val appDataRepository: AppDataRepository,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
private val tunnelManager: TunnelManager,
) : CoroutineWorker(context, params) { ) : CoroutineWorker(context, params) {
companion object { companion object {
private const val TAG = "auto_tunnel_service_monitor" private const val TAG = "service_worker"
fun stop(context: Context) { fun stop(context: Context) {
WorkManager.getInstance(context).cancelAllWorkByTag(TAG) WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
} }
fun start(context: Context) { fun start(context: Context) {
val periodicWorkRequest = val periodicWorkRequest = PeriodicWorkRequestBuilder<ServiceWorker>(
PeriodicWorkRequestBuilder<ServiceWorker>( repeatInterval = 15,
repeatInterval = 15, repeatIntervalTimeUnit = TimeUnit.MINUTES,
repeatIntervalTimeUnit = TimeUnit.MINUTES, ).build()
) WorkManager.getInstance(context)
.build() .enqueueUniquePeriodicWork(
WorkManager.getInstance(context) TAG,
.enqueueUniquePeriodicWork( ExistingPeriodicWorkPolicy.KEEP,
TAG, periodicWorkRequest,
ExistingPeriodicWorkPolicy.KEEP, )
periodicWorkRequest, }
) }
}
}
override suspend fun doWork(): Result { override suspend fun doWork(): Result = withContext(ioDispatcher) {
Timber.i("Service worker started") Timber.i("Service worker started")
with(autoTunnelSettingsRepository.getAutoTunnelSettings()) { with(appDataRepository.settings.get()) {
Timber.i("Checking to see if auto-tunnel has been killed by system") if (isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) return@with serviceManager.startAutoTunnel(true)
if (isAutoTunnelEnabled && serviceManager.autoTunnelService.value == null) { if (tunnelManager.activeTunnels().value.isEmpty()) tunnelManager.restorePreviousState()
Timber.i("Service has been killed by system, restoring.") }
serviceManager.startAutoTunnelService() Result.success()
} }
return Result.success()
}
}
} }
@@ -1,131 +1,90 @@
package com.zaneschepke.wireguardautotunnel.data package com.zaneschepke.wireguardautotunnel.data
import androidx.room.* import androidx.room.AutoMigration
import androidx.room.Database
import androidx.room.DeleteColumn
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import androidx.room.migration.AutoMigrationSpec import androidx.room.migration.AutoMigrationSpec
import androidx.sqlite.db.SupportSQLiteDatabase import com.zaneschepke.wireguardautotunnel.data.dao.SettingsDao
import com.zaneschepke.wireguardautotunnel.data.dao.* import com.zaneschepke.wireguardautotunnel.data.dao.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.data.entity.* import com.zaneschepke.wireguardautotunnel.data.model.Settings
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
@Database( @Database(
entities = entities = [Settings::class, TunnelConfig::class],
[ version = 16,
TunnelConfig::class, autoMigrations =
ProxySettings::class, [
GeneralSettings::class, AutoMigration(from = 1, to = 2),
AutoTunnelSettings::class, AutoMigration(from = 2, to = 3),
MonitoringSettings::class, AutoMigration(
DnsSettings::class, from = 3,
LockdownSettings::class, to = 4,
], ),
version = 29, AutoMigration(
autoMigrations = from = 4,
[ to = 5,
AutoMigration(from = 1, to = 2), ),
AutoMigration(from = 2, to = 3), AutoMigration(
AutoMigration(from = 3, to = 4), from = 5,
AutoMigration(from = 4, to = 5), to = 6,
AutoMigration(from = 5, to = 6), ),
AutoMigration(from = 6, to = 7, spec = RemoveLegacySettingColumnsMigration::class), AutoMigration(
AutoMigration(7, 8), from = 6,
AutoMigration(8, 9), to = 7,
AutoMigration(9, 10), spec = RemoveLegacySettingColumnsMigration::class,
AutoMigration(from = 10, to = 11, spec = RemoveTunnelPauseMigration::class), ),
AutoMigration(from = 11, to = 12), AutoMigration(7, 8),
AutoMigration(from = 12, to = 13), AutoMigration(8, 9),
AutoMigration(from = 13, to = 14), AutoMigration(9, 10),
AutoMigration(from = 14, to = 15), AutoMigration(
AutoMigration(from = 15, to = 16), from = 10,
AutoMigration(from = 16, to = 17, spec = WifiDetectionMigration::class), to = 11,
AutoMigration(from = 17, to = 18), spec = RemoveTunnelPauseMigration::class,
AutoMigration(from = 18, to = 19, spec = PingMigration::class), ),
AutoMigration(from = 19, to = 20, spec = ProxyMigration::class), AutoMigration(
AutoMigration(from = 20, to = 21, spec = FixProxySettingsMigration::class), from = 11,
AutoMigration(from = 21, to = 22), to = 12,
AutoMigration(from = 22, to = 23), ),
AutoMigration(from = 24, to = 25), AutoMigration(
AutoMigration(from = 26, to = 27, spec = GlobalsMigration::class), from = 12,
AutoMigration(from = 27, to = 28, spec = DonationMigration::class), to = 13,
], ),
exportSchema = true, AutoMigration(
from = 13,
to = 14,
),
AutoMigration(
from = 14,
to = 15,
),
AutoMigration(
from = 15,
to = 16,
),
],
exportSchema = true,
) )
@TypeConverters(DatabaseConverters::class) @TypeConverters(DatabaseListConverters::class)
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
abstract fun tunnelConfigDoa(): TunnelConfigDao abstract fun settingDao(): SettingsDao
abstract fun proxySettingsDoa(): ProxySettingsDao abstract fun tunnelConfigDoa(): TunnelConfigDao
abstract fun generalSettingsDao(): GeneralSettingsDao
abstract fun autoTunnelSettingsDao(): AutoTunnelSettingsDao
abstract fun monitoringSettingsDao(): MonitoringSettingsDao
abstract fun lockdownSettingsDao(): LockdownSettingsDao
abstract fun dnsSettingsDao(): DnsSettingsDao
} }
@DeleteColumn(tableName = "Settings", columnName = "default_tunnel") @DeleteColumn(
@DeleteColumn(tableName = "Settings", columnName = "is_battery_saver_enabled") tableName = "Settings",
columnName = "default_tunnel",
)
@DeleteColumn(
tableName = "Settings",
columnName = "is_battery_saver_enabled",
)
class RemoveLegacySettingColumnsMigration : AutoMigrationSpec class RemoveLegacySettingColumnsMigration : AutoMigrationSpec
@DeleteColumn(tableName = "Settings", columnName = "is_auto_tunnel_paused") @DeleteColumn(
tableName = "Settings",
columnName = "is_auto_tunnel_paused",
)
class RemoveTunnelPauseMigration : AutoMigrationSpec class RemoveTunnelPauseMigration : AutoMigrationSpec
@DeleteColumn(tableName = "Settings", columnName = "is_wifi_by_shell_enabled")
class WifiDetectionMigration : AutoMigrationSpec
@DeleteColumn.Entries(
DeleteColumn(tableName = "TunnelConfig", columnName = "ping_interval"),
DeleteColumn(tableName = "TunnelConfig", columnName = "ping_cooldown"),
DeleteColumn(tableName = "Settings", columnName = "split_tunnel_apps"),
)
@RenameColumn.Entries(
RenameColumn(
tableName = "TunnelConfig",
fromColumnName = "is_ping_enabled",
toColumnName = "restart_on_ping_failure",
),
RenameColumn(
tableName = "TunnelConfig",
fromColumnName = "ping_ip",
toColumnName = "ping_target",
),
)
class PingMigration : AutoMigrationSpec
@DeleteColumn.Entries(
DeleteColumn(tableName = "Settings", columnName = "is_amnezia_enabled"),
DeleteColumn(tableName = "Settings", columnName = "is_vpn_kill_switch_enabled"),
DeleteColumn(tableName = "Settings", columnName = "is_kernel_kill_switch_enabled"),
DeleteColumn(tableName = "Settings", columnName = "is_kernel_enabled"),
)
class ProxyMigration : AutoMigrationSpec {
override fun onPostMigrate(db: SupportSQLiteDatabase) {
db.execSQL("INSERT INTO proxy_settings DEFAULT VALUES")
}
}
class FixProxySettingsMigration : AutoMigrationSpec {
override fun onPostMigrate(db: SupportSQLiteDatabase) {
val cursor = db.query("SELECT COUNT(*) FROM proxy_settings")
val count = if (cursor.moveToFirst()) cursor.getInt(0) else 0
cursor.close()
if (count == 0) {
db.execSQL("INSERT INTO proxy_settings DEFAULT VALUES")
}
}
}
@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
@@ -4,76 +4,91 @@ import android.content.Context
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore import androidx.datastore.preferences.preferencesDataStore
import java.io.IOException import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
import java.io.IOException
class DataStoreManager( class DataStoreManager(
private val context: Context, private val context: Context,
private val ioDispatcher: CoroutineDispatcher, @IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) { ) {
private val preferencesKey = "preferences" companion object {
val Context.dataStore by preferencesDataStore(name = preferencesKey) val locationDisclosureShown = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN")
val dataStore = context.dataStore val batteryDisableShown = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN")
val currentSSID = stringPreferencesKey("CURRENT_SSID")
val pinLockEnabled = booleanPreferencesKey("PIN_LOCK_ENABLED")
val tunnelStatsExpanded = booleanPreferencesKey("TUNNEL_STATS_EXPANDED")
val isLocalLogsEnabled = booleanPreferencesKey("LOCAL_LOGS_ENABLED")
val locale = stringPreferencesKey("LOCALE")
val theme = stringPreferencesKey("THEME")
}
companion object { // preferences
val locationDisclosureShown = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN") private val preferencesKey = "preferences"
val batteryDisableShown = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN") private val Context.dataStore by
val shouldShowDonationSnackbar = booleanPreferencesKey("SHOW_DONATION_SNACK") preferencesDataStore(
} name = preferencesKey,
)
suspend fun init() { suspend fun init() {
withContext(ioDispatcher) { withContext(ioDispatcher) {
try { try {
dataStore.data.first() context.dataStore.data.first()
} catch (e: IOException) { } catch (e: IOException) {
Timber.e(e) Timber.Forest.e(e)
} }
} }
} }
suspend fun <T> saveToDataStore(key: Preferences.Key<T>, value: T) { suspend fun <T> saveToDataStore(key: Preferences.Key<T>, value: T) {
withContext(ioDispatcher) { withContext(ioDispatcher) {
try { try {
dataStore.edit { it[key] = value } context.dataStore.edit { it[key] = value }
} catch (e: IOException) { } catch (e: IOException) {
Timber.e(e) Timber.Forest.e(e)
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e) Timber.Forest.e(e)
} }
} }
} }
suspend fun <T> removeFromDataStore(key: Preferences.Key<T>) { suspend fun <T> removeFromDataStore(key: Preferences.Key<T>) {
withContext(ioDispatcher) { withContext(ioDispatcher) {
try { try {
dataStore.edit { it.remove(key) } context.dataStore.edit { it.remove(key) }
} catch (e: IOException) { } catch (e: IOException) {
Timber.e(e) Timber.Forest.e(e)
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e) Timber.Forest.e(e)
} }
} }
} }
fun <T> getFromStoreFlow(key: Preferences.Key<T>) = context.dataStore.data.map { it[key] } fun <T> getFromStoreFlow(key: Preferences.Key<T>) = context.dataStore.data.map { it[key] }
suspend fun <T> getFromStore(key: Preferences.Key<T>): T? { suspend fun <T> getFromStore(key: Preferences.Key<T>): T? {
return withContext(ioDispatcher) { return withContext(ioDispatcher) {
try { try {
dataStore.data.map { it[key] }.first() context.dataStore.data.map { it[key] }.first()
} catch (e: IOException) { } catch (e: IOException) {
Timber.e(e) Timber.Forest.e(e)
null null
} }
} }
} }
val preferencesFlow: Flow<Preferences?> = dataStore.data.flowOn(ioDispatcher) fun <T> getFromStoreBlocking(key: Preferences.Key<T>) = runBlocking {
context.dataStore.data.map { it[key] }.first()
}
val preferencesFlow: Flow<Preferences?> = context.dataStore.data.flowOn(ioDispatcher)
} }
@@ -4,14 +4,18 @@ import androidx.room.RoomDatabase
import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteDatabase
import timber.log.Timber import timber.log.Timber
class DatabaseCallback(private val databaseProvider: Lazy<AppDatabase>) : RoomDatabase.Callback() { class DatabaseCallback : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) { override fun onCreate(db: SupportSQLiteDatabase) = db.run {
super.onCreate(db) // Notice non-ui thread is here
Timber.d("Database created, inserting default rows") beginTransaction()
db.execSQL("INSERT INTO proxy_settings DEFAULT VALUES") try {
db.execSQL("INSERT INTO general_settings DEFAULT VALUES") execSQL(Queries.createDefaultSettings())
db.execSQL("INSERT INTO auto_tunnel_settings DEFAULT VALUES") Timber.i("Bootstrapping settings data")
db.execSQL("INSERT INTO monitoring_settings DEFAULT VALUES") setTransactionSuccessful()
db.execSQL("INSERT INTO dns_settings DEFAULT VALUES") } catch (e: Exception) {
} Timber.e(e)
} finally {
endTransaction()
}
}
} }
@@ -1,67 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data
import androidx.room.TypeConverter
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.data.model.DnsProtocol
import com.zaneschepke.wireguardautotunnel.data.model.WifiDetectionMethod
import kotlinx.serialization.json.Json
class DatabaseConverters {
@TypeConverter
fun listToString(value: List<String>): String {
return Json.encodeToString(value)
}
@TypeConverter
fun stringToList(value: String): List<String> {
if (value.isBlank() || value.isEmpty()) return mutableListOf()
return try {
Json.decodeFromString<List<String>>(value)
} catch (e: Exception) {
val list = value.split(",").toMutableList()
val json = listToString(list)
Json.decodeFromString<List<String>>(json)
}
}
@TypeConverter
fun mapToString(map: Map<String, String>): String {
return Json.encodeToString(map)
}
@TypeConverter
fun stringToMap(json: String): Map<String, String> {
return if (json.isEmpty() || json == "{}") {
emptyMap()
} else {
try {
Json.decodeFromString<Map<String, String>>(json)
} catch (_: Exception) {
emptyMap()
}
}
}
@TypeConverter
fun setToString(value: Set<String>): String {
return listToString(value.toList())
}
@TypeConverter
fun stringToSet(value: String): Set<String> {
return stringToList(value).toSet()
}
@TypeConverter fun fromStatus(status: WifiDetectionMethod): Int = status.value
@TypeConverter
fun toStatus(value: Int): WifiDetectionMethod = WifiDetectionMethod.fromValue(value)
@TypeConverter fun toMode(value: Int): AppMode = AppMode.fromValue(value)
@TypeConverter fun fromMode(mode: AppMode): Int = mode.value
@TypeConverter fun toDnsProtocol(value: Int): DnsProtocol = DnsProtocol.fromValue(value)
@TypeConverter fun fromDnsProtocol(mode: DnsProtocol): Int = mode.value
}
@@ -0,0 +1,24 @@
package com.zaneschepke.wireguardautotunnel.data
import androidx.room.TypeConverter
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
class DatabaseListConverters {
@TypeConverter
fun listToString(value: MutableList<String>): String {
return Json.encodeToString(value)
}
@TypeConverter
fun stringToList(value: String): MutableList<String> {
if (value.isBlank() || value.isEmpty()) return mutableListOf()
return try {
Json.decodeFromString<MutableList<String>>(value)
} catch (e: Exception) {
val list = value.split(",").toMutableList()
val json = listToString(list)
Json.decodeFromString<MutableList<String>>(json)
}
}
}
@@ -0,0 +1,35 @@
package com.zaneschepke.wireguardautotunnel.data
object Queries {
fun createDefaultSettings(): String {
return """
INSERT INTO Settings (is_tunnel_enabled,
is_tunnel_on_mobile_data_enabled,
trusted_network_ssids,
is_always_on_vpn_enabled,
is_tunnel_on_ethernet_enabled,
is_shortcuts_enabled,
is_tunnel_on_wifi_enabled,
is_kernel_enabled,
is_restore_on_boot_enabled,
is_multi_tunnel_enabled)
VALUES
('false',
'false',
'',
'false',
'false',
'false',
'false',
'false',
'false',
'false')
""".trimIndent()
}
fun createTunnelConfig(): String {
return """
INSERT INTO TunnelConfig (name, wg_quick) VALUES ('test', 'test')
""".trimIndent()
}
}
@@ -1,21 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.dao
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Upsert
import com.zaneschepke.wireguardautotunnel.data.entity.AutoTunnelSettings
import kotlinx.coroutines.flow.Flow
@Dao
interface AutoTunnelSettingsDao {
@Query("SELECT * FROM auto_tunnel_settings LIMIT 1")
suspend fun getAutoTunnelSettings(): AutoTunnelSettings?
@Upsert suspend fun upsert(autoTunnelSettings: AutoTunnelSettings)
@Query("SELECT * FROM auto_tunnel_settings LIMIT 1")
fun getAutoTunnelSettingsFlow(): Flow<AutoTunnelSettings?>
@Query("UPDATE auto_tunnel_settings SET is_tunnel_enabled = :enabled")
suspend fun updateAutoTunnelEnabled(enabled: Boolean)
}
@@ -1,16 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.dao
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Upsert
import com.zaneschepke.wireguardautotunnel.data.entity.DnsSettings
import kotlinx.coroutines.flow.Flow
@Dao
interface DnsSettingsDao {
@Query("SELECT * FROM dns_settings LIMIT 1") suspend fun getDnsSettings(): DnsSettings?
@Upsert suspend fun upsert(dnsSettings: DnsSettings)
@Query("SELECT * FROM dns_settings LIMIT 1") fun getDnsSettingsFlow(): Flow<DnsSettings?>
}
@@ -1,31 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.dao
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Upsert
import com.zaneschepke.wireguardautotunnel.data.entity.GeneralSettings
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import kotlinx.coroutines.flow.Flow
@Dao
interface GeneralSettingsDao {
@Query("SELECT * FROM general_settings LIMIT 1")
suspend fun getGeneralSettings(): GeneralSettings?
@Upsert suspend fun upsert(generalSettings: GeneralSettings)
@Query("SELECT * FROM general_settings LIMIT 1")
fun getGeneralSettingsFlow(): Flow<GeneralSettings?>
@Query("UPDATE general_settings SET theme = :theme WHERE id = 1")
suspend fun updateTheme(theme: String)
@Query("UPDATE general_settings SET locale = :locale WHERE id = 1")
suspend fun updateLocale(locale: String)
@Query("UPDATE general_settings SET is_pin_lock_enabled = :enabled WHERE id = 1")
suspend fun updatePinLockEnabled(enabled: Boolean)
@Query("UPDATE general_settings SET app_mode = :appMode WHERE id = 1")
suspend fun updateAppMode(appMode: AppMode)
}
@@ -1,18 +0,0 @@
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?>
}
@@ -1,18 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.dao
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Upsert
import com.zaneschepke.wireguardautotunnel.data.entity.MonitoringSettings
import kotlinx.coroutines.flow.Flow
@Dao
interface MonitoringSettingsDao {
@Query("SELECT * FROM monitoring_settings LIMIT 1")
suspend fun getMonitoringSettings(): MonitoringSettings?
@Upsert suspend fun upsert(monitoringSettings: MonitoringSettings)
@Query("SELECT * FROM monitoring_settings LIMIT 1")
fun getMonitoringSettingsFlow(): Flow<MonitoringSettings?>
}
@@ -1,16 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.dao
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Upsert
import com.zaneschepke.wireguardautotunnel.data.entity.ProxySettings
import kotlinx.coroutines.flow.Flow
@Dao
interface ProxySettingsDao {
@Upsert suspend fun upsert(proxySettings: ProxySettings)
@Query("SELECT * FROM proxy_settings LIMIT 1") suspend fun getProxySettings(): ProxySettings?
@Query("SELECT * FROM proxy_settings LIMIT 1") fun getProxySettingsFlow(): Flow<ProxySettings?>
}
@@ -0,0 +1,36 @@
package com.zaneschepke.wireguardautotunnel.data.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.zaneschepke.wireguardautotunnel.data.model.Settings
import kotlinx.coroutines.flow.Flow
@Dao
interface SettingsDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun save(t: Settings)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun saveAll(t: List<Settings>)
@Query("SELECT * FROM settings WHERE id=:id")
suspend fun getById(id: Long): Settings?
@Query("SELECT * FROM settings")
suspend fun getAll(): List<Settings>
@Query("SELECT * FROM settings LIMIT 1")
fun getSettingsFlow(): Flow<Settings>
@Query("SELECT * FROM settings")
fun getAllFlow(): Flow<MutableList<Settings>>
@Delete
suspend fun delete(t: Settings)
@Query("SELECT COUNT('id') FROM settings")
suspend fun count(): Long
}
@@ -1,87 +1,58 @@
package com.zaneschepke.wireguardautotunnel.data.dao package com.zaneschepke.wireguardautotunnel.data.dao
import androidx.room.* import androidx.room.Dao
import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@Dao @Dao
interface TunnelConfigDao { interface TunnelConfigDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun save(t: TunnelConfig)
@Upsert suspend fun upsert(t: TunnelConfig) @Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun saveAll(t: TunnelConfigs)
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun saveAll(t: List<TunnelConfig>) @Query("SELECT * FROM TunnelConfig WHERE id=:id")
suspend fun getById(id: Long): TunnelConfig?
@Query("SELECT * FROM tunnel_config WHERE id=:id") suspend fun getById(id: Long): TunnelConfig? @Query("SELECT * FROM TunnelConfig WHERE name=:name")
suspend fun getByName(name: String): TunnelConfig?
@Query("UPDATE tunnel_config SET is_Active = 0 WHERE is_Active = 1") @Query("SELECT * FROM TunnelConfig WHERE is_Active=1")
suspend fun resetActiveTunnels() suspend fun getActive(): TunnelConfigs
@Query("SELECT * FROM tunnel_config WHERE name=:name") @Query("SELECT * FROM TunnelConfig")
suspend fun getByName(name: String): TunnelConfig? suspend fun getAll(): TunnelConfigs
@Query("SELECT * FROM tunnel_config WHERE is_Active=1") @Delete
suspend fun getActive(): List<TunnelConfig> suspend fun delete(t: TunnelConfig)
@Query("SELECT * FROM tunnel_config") suspend fun getAll(): List<TunnelConfig> @Query("SELECT COUNT('id') FROM TunnelConfig")
suspend fun count(): Long
@Delete suspend fun delete(t: TunnelConfig) @Query("SELECT * FROM TunnelConfig WHERE tunnel_networks LIKE '%' || :name || '%'")
suspend fun findByTunnelNetworkName(name: String): TunnelConfigs
@Delete suspend fun delete(t: List<TunnelConfig>) @Query("UPDATE TunnelConfig SET is_primary_tunnel = 0 WHERE is_primary_tunnel =1")
suspend fun resetPrimaryTunnel()
@Query("SELECT COUNT('id') FROM tunnel_config") suspend fun count(): Long @Query("UPDATE TunnelConfig SET is_mobile_data_tunnel = 0 WHERE is_mobile_data_tunnel =1")
suspend fun resetMobileDataTunnel()
@Query("SELECT * FROM tunnel_config WHERE tunnel_networks LIKE '%' || :name || '%'") @Query("UPDATE TunnelConfig SET is_ethernet_tunnel = 0 WHERE is_ethernet_tunnel =1")
suspend fun findByTunnelNetworkName(name: String): List<TunnelConfig> suspend fun resetEthernetTunnel()
@Query("UPDATE tunnel_config SET is_primary_tunnel = 0 WHERE is_primary_tunnel =1") @Query("SELECT * FROM TUNNELCONFIG WHERE is_primary_tunnel=1")
suspend fun resetPrimaryTunnel() suspend fun findByPrimary(): TunnelConfigs
@Query("UPDATE tunnel_config SET is_mobile_data_tunnel = 0 WHERE is_mobile_data_tunnel =1") @Query("SELECT * FROM TUNNELCONFIG WHERE is_mobile_data_tunnel=1")
suspend fun resetMobileDataTunnel() suspend fun findByMobileDataTunnel(): TunnelConfigs
@Query("UPDATE tunnel_config SET is_ethernet_tunnel = 0 WHERE is_ethernet_tunnel =1") @Query("SELECT * FROM tunnelconfig")
suspend fun resetEthernetTunnel() fun getAllFlow(): Flow<MutableList<TunnelConfig>>
@Query("SELECT * FROM tunnel_config WHERE is_primary_tunnel=1")
suspend fun findByPrimary(): List<TunnelConfig>
@Query("SELECT * FROM tunnel_config WHERE is_mobile_data_tunnel=1")
suspend fun findByMobileDataTunnel(): List<TunnelConfig>
@Query(
"""
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
WHERE name != '${TunnelConfig.GLOBAL_CONFIG_NAME}'
ORDER BY
CASE WHEN is_Active = 1 THEN 0
WHEN is_primary_tunnel = 1 THEN 1
ELSE 2 END,
position ASC
LIMIT 1
"""
)
suspend fun getStartTunnel(): TunnelConfig?
@Query("SELECT * FROM tunnel_config ORDER BY position")
fun getAllFlow(): Flow<List<TunnelConfig>>
@Query("SELECT * FROM tunnel_config WHERE name != :globalName ORDER BY position")
fun getAllTunnelsExceptGlobal(
globalName: String = TunnelConfig.GLOBAL_CONFIG_NAME
): Flow<List<TunnelConfig>>
@Query("SELECT * FROM tunnel_config WHERE name = :globalName LIMIT 1")
fun getGlobalTunnel(globalName: String = TunnelConfig.GLOBAL_CONFIG_NAME): Flow<TunnelConfig?>
} }
@@ -1,7 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.entity
data class AppState(
val isLocationDisclosureShown: Boolean = false,
val isBatteryOptimizationDisableShown: Boolean = false,
val shouldShowDonationSnackbar: Boolean = false,
)
@@ -1,10 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.entity
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class Asset(
val name: String,
@SerialName("browser_download_url") val browserDownloadUrl: String,
)
@@ -1,32 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.zaneschepke.wireguardautotunnel.data.model.WifiDetectionMethod
@Entity(tableName = "auto_tunnel_settings")
data class AutoTunnelSettings(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
@ColumnInfo(name = "is_tunnel_enabled", defaultValue = "0")
val isAutoTunnelEnabled: Boolean = false,
@ColumnInfo(name = "is_tunnel_on_mobile_data_enabled", defaultValue = "0")
val isTunnelOnMobileDataEnabled: Boolean = false,
@ColumnInfo(name = "trusted_network_ssids", defaultValue = "")
val trustedNetworkSSIDs: Set<String> = emptySet(),
@ColumnInfo(name = "is_tunnel_on_ethernet_enabled", defaultValue = "0")
val isTunnelOnEthernetEnabled: Boolean = false,
@ColumnInfo(name = "is_tunnel_on_wifi_enabled", defaultValue = "0")
val isTunnelOnWifiEnabled: Boolean = false,
@ColumnInfo(name = "is_wildcards_enabled", defaultValue = "0")
val isWildcardsEnabled: Boolean = false,
@ColumnInfo(name = "is_stop_on_no_internet_enabled", defaultValue = "0")
val isStopOnNoInternetEnabled: Boolean = false,
@ColumnInfo(name = "debounce_delay_seconds", defaultValue = "3")
val debounceDelaySeconds: Int = 3,
@ColumnInfo(name = "is_tunnel_on_unsecure_enabled", defaultValue = "0")
val isTunnelOnUnsecureEnabled: Boolean = false,
@ColumnInfo(name = "wifi_detection_method", defaultValue = "0")
val wifiDetectionMethod: WifiDetectionMethod = WifiDetectionMethod.fromValue(0),
@ColumnInfo(name = "start_on_boot", defaultValue = "0") val startOnBoot: Boolean = false,
)

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