Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot] 570d70e441 chore(deps): bump hiltAndroid from 2.56 to 2.56.1
Bumps `hiltAndroid` from 2.56 to 2.56.1.

Updates `com.google.dagger:hilt-android` from 2.56 to 2.56.1
- [Release notes](https://github.com/google/dagger/releases)
- [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md)
- [Commits](https://github.com/google/dagger/compare/dagger-2.56...dagger-2.56.1)

Updates `com.google.dagger:hilt-android-compiler` from 2.56 to 2.56.1
- [Release notes](https://github.com/google/dagger/releases)
- [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md)
- [Commits](https://github.com/google/dagger/compare/dagger-2.56...dagger-2.56.1)

Updates `com.google.dagger.hilt.android` from 2.56 to 2.56.1
- [Release notes](https://github.com/google/dagger/releases)
- [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md)
- [Commits](https://github.com/google/dagger/compare/dagger-2.56...dagger-2.56.1)

---
updated-dependencies:
- dependency-name: com.google.dagger:hilt-android
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.google.dagger:hilt-android-compiler
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.google.dagger.hilt.android
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-31 13:27:24 +00:00
1250 changed files with 16301 additions and 32033 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 -1
View File
@@ -15,7 +15,7 @@ A clear and concise description of what the bug is.
- Device: [e.g. Pixel 4a]
- Android Version: [e.g. Android 13]
- App Version [e.g. 3.3.3]
- App mode: [e.g. Kernel, VPN, Proxy, Lockdown]
- Backend: [e.g. Kernel, Userspace]
**To Reproduce**
Steps to reproduce the behavior:
+43 -42
View File
@@ -1,7 +1,4 @@
name: build
permissions:
contents: read
on:
workflow_dispatch:
inputs:
@@ -12,16 +9,9 @@ on:
default: debug
options:
- debug
- prerelease
- nightly
- release
flavor:
type: choice
description: "Product flavor"
required: true
default: fdroid
options:
- fdroid
- standalone
secrets:
SIGNING_KEY_ALIAS:
required: false
@@ -40,11 +30,6 @@ on:
description: "Build type"
required: true
default: debug
flavor:
type: string
description: "Product flavor"
required: false
default: fdroid
secrets:
SIGNING_KEY_ALIAS:
required: false
@@ -56,7 +41,6 @@ on:
required: false
KEYSTORE:
required: false
env:
UPLOAD_DIR_ANDROID: android_artifacts
@@ -72,17 +56,18 @@ jobs:
outputs:
UPLOAD_DIR_ANDROID: ${{ env.UPLOAD_DIR_ANDROID }}
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v5
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
cache: gradle
- name: Grant execute permission for 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
id: decode_keystore
uses: timheuer/base64-to-file@v1.2
@@ -90,35 +75,51 @@ jobs:
fileName: ${{ env.KEY_STORE_FILE }}
fileDir: ${{ env.KEY_STORE_LOCATION }}
encodedString: ${{ secrets.KEYSTORE }}
# create keystore path for gradle to read
- 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 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: |
flavor=${{ inputs.flavor }}
build_type=${{ inputs.build_type }}
case $build_type in
"release")
./gradlew :app:assemble${flavor^}Release --info
;;
"nightly")
./gradlew :app:assemble${flavor^}Nightly --info
;;
"debug")
./gradlew :app:assemble${flavor^}Debug --stacktrace
;;
esac
git config --global user.name 'GitHub Actions'
git config --global user.email 'actions@github.com'
git add versionCode.txt
git commit -m "Automated build update"
- name: Get release apk path
id: apk-path
run: echo "path=$(find . -regex '^.*/build/outputs/apk/${{ inputs.flavor }}/${{ inputs.build_type }}/.*\.apk$' -type f | head -1 | tail -c+2)" >> $GITHUB_OUTPUT
- name: Upload All APK Artifacts
uses: actions/upload-artifact@v5
run: echo "path=$(find . -regex '^.*/build/outputs/apk/fdroid/${{ inputs.build_type }}/.*\.apk$' -type f | head -1 | tail -c+2)" >> $GITHUB_OUTPUT
- name: Upload release apk
uses: actions/upload-artifact@v4
with:
name: android_artifacts_${{ inputs.flavor }}
path: >-
app/build/outputs/apk/${{ inputs.flavor }}/${{ inputs.build_type }}/*.apk
name: ${{ env.UPLOAD_DIR_ANDROID }}
path: ${{github.workspace}}/${{ steps.apk-path.outputs.path }}
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@v5
- 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@v5
- 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@v6
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
permissions:
contents: read
on:
workflow_dispatch:
@@ -10,9 +8,9 @@ jobs:
format_check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v5
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
@@ -21,5 +19,5 @@ jobs:
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Run ktfmt
run: ./gradlew ktfmtCheck
- name: Run ktlint
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 }}"
+107 -77
View File
@@ -1,17 +1,13 @@
name: publish
permissions:
contents: write
packages: write
on:
push:
tags:
- '[0-9]*.[0-9]*.[0-9]*'
schedule:
- cron: "4 3 * * *"
workflow_dispatch:
inputs:
track:
type: choice
description: "Google Play release track"
description: "Google play release track"
options:
- none
- internal
@@ -25,124 +21,159 @@ on:
description: "GitHub release type"
options:
- none
- prerelease
- nightly
- release
default: release
required: true
tag_name:
description: "Tag name for release"
required: false
default: 1.1.1
flavor:
type: choice
description: "Product flavor"
required: true
default: standalone
options:
- fdroid
- standalone
default: nightly
workflow_call:
inputs:
flavor:
type: string
description: "Product flavor"
required: false
default: standalone
env:
UPLOAD_DIR_ANDROID: android_artifacts
jobs:
check_commits:
name: Check for New Commits
runs-on: ubuntu-latest
outputs:
has_new_commits: ${{ steps.check.outputs.new_commits }}
build-fdroid:
if: ${{ github.event_name == 'push' || inputs.release_type == 'release' || inputs.flavor == 'fdroid' }}
steps:
- name: Checkout Repository
uses: actions/checkout@v3
with:
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
secrets: inherit
with:
build_type: ${{ github.event_name == 'push' && 'release' || inputs.release_type }}
flavor: fdroid
build-standalone:
if: ${{ github.event_name == 'push' || inputs.release_type == 'release' || inputs.release_type == 'debug' || inputs.flavor == 'standalone' }}
uses: ./.github/workflows/build.yml
secrets: inherit
with:
build_type: ${{ github.event_name == 'push' && 'release' || inputs.release_type }}
flavor: standalone
build_type: ${{ inputs.release_type == '' && 'nightly' || inputs.release_type }}
publish:
needs:
- build-standalone
- check_commits
- build
if: ${{ needs.check_commits.outputs.has_new_commits > 0 && inputs.release_type != 'none' }}
name: publish-github
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:
- uses: actions/checkout@v5
with:
ref: ${{ github.event_name == 'push' && github.ref || 'master' }}
- uses: actions/checkout@v4
- name: Install system dependencies
run: |
sudo apt update && sudo apt install -y gh apksigner
# update latest tag
- name: Set latest tag
uses: rickstaa/action-create-tag@v1
id: tag_creation
with:
tag: "latest"
tag: "latest" # or any tag name you wish to use
message: "Automated tag for HEAD commit"
force_push_tag: true
github_token: ${{ github.token }}
tag_exists_error: false
- name: Get latest release
id: latest_release
uses: kaliber5/action-get-release@v1
with:
token: ${{ github.token }}
token: ${{ secrets.GITHUB_TOKEN }}
latest: true
- name: Generate Changelog
id: changelog
uses: requarks/changelog-action@v1
with:
token: ${{ github.token }}
toTag: ${{ steps.latest_release.outputs.tag_name }}
token: ${{ secrets.GITHUB_TOKEN }}
toTag: ${{ github.event_name == 'schedule' && 'nightly' || steps.latest_release.outputs.tag_name }}
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
run: mkdir ${{ github.workspace }}/temp
- name: Download artifacts
uses: actions/download-artifact@v6
uses: actions/download-artifact@v4
with:
pattern: android_artifacts_*
name: ${{ env.UPLOAD_DIR_ANDROID }}
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
if: ${{ github.event_name == 'push' || inputs.release_type == 'release' }}
if: ${{ inputs.release_type == 'release' }}
run: |
VERSION_CODE=$(grep "const val VERSION_CODE" buildSrc/src/main/kotlin/Constants.kt | awk '{print $5}')
RELEASE_NOTES="$(cat ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/${VERSION_CODE}.txt || echo "No changelog found for ${VERSION_CODE}")"
RELEASE_NOTES="$(cat ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/${{ env.VERSION_CODE }}.txt)"
echo "RELEASE_NOTES<<EOF" >> $GITHUB_ENV
echo "$RELEASE_NOTES" >> $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
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
file_path=$(find ${{ github.workspace }}/temp -type f -iname "*.apk" | tail -n1)
echo "checksum=$(apksigner verify -print-certs $file_path | grep -Po "(?<=SHA-256 digest:) .*" | tr -d "[:blank:]")" >> $GITHUB_OUTPUT
- name: Create Release
- name: Create Release with Fastlane changelog notes
id: create_release
uses: softprops/action-gh-release@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
body: |
${{ env.RELEASE_NOTES }}
SHA-256 fingerprints for the 4096-bit signing certificate:
SHA-256 fingerprint for the 4096-bit signing certificate:
```sh
${{ steps.checksum.outputs.checksum }}
```
@@ -154,31 +185,29 @@ jobs:
### Changelog
${{ steps.changelog.outputs.changes }}
tag_name: ${{ github.event_name == 'push' && github.ref_name || github.event.inputs.tag_name }}
name: ${{ github.event_name == 'push' && github.ref_name || github.event.inputs.tag_name }}
tag_name: ${{ env.TAG_NAME }}
name: ${{ env.TAG_NAME }}
draft: false
prerelease: false
make_latest: true
prerelease: ${{ inputs.release_type == 'prerelease' || inputs.release_type == '' || inputs.release_type == 'nightly' }}
make_latest: ${{ inputs.release_type == 'release' }}
files: |
${{ github.workspace }}/temp/**/*.apk
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
${{ github.workspace }}/temp/*
publish-fdroid-public:
publish-fdroid:
runs-on: ubuntu-latest
needs:
- build-fdroid
if: ${{ github.event_name == 'push' || inputs.release_type == 'release' }}
- build
if: inputs.release_type == 'release'
steps:
- name: Dispatch update for fdroid repo
uses: peter-evans/repository-dispatch@v4
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.PAT }}
repository: wgtunnel/fdroid
repository: zaneschepke/fdroid
event-type: fdroid-update
publish-play:
if: ${{ github.event_name == 'push' || inputs.track != 'none' }}
if: ${{ inputs.track != 'none' && inputs.track != '' }}
name: Publish to Google Play
runs-on: ubuntu-latest
@@ -188,11 +217,13 @@ jobs:
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
KEY_STORE_FILE: 'android_keystore.jks'
KEY_STORE_LOCATION: ${{ github.workspace }}/app/keystore/
GH_USER: ${{ secrets.GH_USER }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v5
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
@@ -228,6 +259,5 @@ jobs:
bundler-cache: true
- name: Distribute app to Prod track 🚀
run: |
track=${{ github.event_name == 'push' && 'production' || inputs.track }}
(cd ${{ github.workspace }} && bundle install && bundle exec fastlane $track)
run: (cd ${{ github.workspace }} && bundle install && bundle exec fastlane ${{ inputs.track }})
+1 -1
View File
@@ -70,5 +70,5 @@ lint/tmp/
app/release/output.json
.idea/codeStyles/
# where we keep our signing secrets locally
app/signing.properties
/.kotlin/
/app/keystore/
+1 -1
View File
@@ -1,6 +1,6 @@
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
of this software and associated documentation files (the "Software"), to deal
+37 -34
View File
@@ -4,7 +4,7 @@ WG Tunnel
<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/)
<br />
<br />
@@ -21,7 +21,8 @@ and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/)
<div align="center">
[![Google Play](https://img.shields.io/badge/Google_Play-414141?style=for-the-badge&logo=google-play&logoColor=white)](https://play.google.com/store/apps/details?id=com.zaneschepke.wireguardautotunnel)
[![F-Droid](https://img.shields.io/static/v1?style=for-the-badge&message=F-Droid&color=1976D2&logo=F-Droid&logoColor=FFFFFF&label=)](https://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/)
[![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)
[![Obtainium](https://img.shields.io/badge/Obtainium-414141?style=for-the-badge&logo=Obtainium&logoColor=white)](https://apps.obtainium.imranr.dev/redirect?r=obtainium://app/%7B%22id%22%3A%22com.zaneschepke.wireguardautotunnel%22%2C%22url%22%3A%22https%3A%2F%2Fgithub.com%2Fzaneschepke%2Fwgtunnel%22%2C%22author%22%3A%22zaneschepke%22%2C%22name%22%3A%22WG%20Tunnel%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Atrue%2C%5C%22sortMethodChoice%5C%22%3A%5C%22date%5C%22%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Atrue%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22WG%20Tunnel%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22Zane%20Schepke%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%7D%22%2C%22overrideSource%22%3Anull%7D)
</div>
@@ -36,11 +37,11 @@ and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/)
<summary>Table of Contents</summary>
- [About](#about)
- [Acknowledgements](#acknowledgements)
- [Screenshots](#screenshots)
- [Features](#features)
- [Building](#building)
- [Translation](#translation)
- [Acknowledgements](#acknowledgements)
- [Contributing](#contributing)
</details>
@@ -48,45 +49,56 @@ and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/)
<div style="text-align: left;">
## About
WG Tunnel is an alternative Android client for WireGuard and AmneziaWG, inspired by the official WireGuard Android app. It fills gaps in the official client by adding advanced features like auto-tunneling (on-demand VPN activation), while seamlessly supporting both protocols across app modes—including Kernel (for direct WireGuard kernel integration; AmneziaWG not supported), VPN (standard system-level tunneling), Lockdown (a custom kill switch for leak prevention), and Proxy (built-in HTTP/SOCKS5 forwarding)—for enhanced privacy, censorship resistance, and flexibility.
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/)
and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/), with its primary feature of auto-tunneling (on-demand tunneling).
</div>
<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
</div>
<div style="display: flex; flex-wrap: wrap; justify-content: left; gap: 10px;">
<img label="Main" src="fastlane/metadata/android/en-US/images/phoneScreenshots/main_screen.png" width="200" alt="Main"/>
<img label="Config" src="fastlane/metadata/android/en-US/images/phoneScreenshots/config_screen.png" width="200" alt="Config"/>
<img label="Settings" src="fastlane/metadata/android/en-US/images/phoneScreenshots/settings_screen.png" width="200" alt="Settings"/>
<img label="Auto-tunnel" src="fastlane/metadata/android/en-US/images/phoneScreenshots/auto_screen.png" width="200" alt="Auto-tunnel"/>
<img label="Main" src="fastlane/metadata/android/en-US/images/phoneScreenshots/main_screen.png" width="200" />
<img label="Settings" src="fastlane/metadata/android/en-US/images/phoneScreenshots/settings_screen.png" width="200" />
<img label="Auto" src="fastlane/metadata/android/en-US/images/phoneScreenshots/auto_screen.png" width="200" />
<img label="Config" src="fastlane/metadata/android/en-US/images/phoneScreenshots/config_screen.png" width="200" />
</div>
<div style="text-align: left;">
## Features
- **Tunnel Import Methods**: Easily add tunnels using .conf files, ZIP archives, manual entry, or QR code scanning.
- **Auto-Tunneling**: Automatically activate tunnels based on Wi-Fi SSID, Ethernet connections, or mobile data networks.
- **Split Tunneling**: Flexible support for routing specific apps or traffic through the VPN.
- **WireGuard Modes**: Full compatibility with WireGuard in both kernel and userspace implementations.
- **AmneziaWG Integration**: Userspace mode for AmneziaWG, providing robust censorship evasion.
- **Always-On VPN**: Ensures continuous protection with Android's Always-On VPN feature.
- **Quick Controls**: Quick Settings tile and home screen shortcuts for easy VPN toggling.
- **Automation Support**: Intent-based automation for controlling tunnels.
- **Auto-Restore**: Seamlessly restores auto-tunneling and active tunnels after device restarts or app updates.
- **Proxying Options**: Built-in HTTP and SOCKS5 proxy support within tunnels.
- **Lockdown Mode**: Custom kill switch for maximum leak prevention and security.
- **Dynamic DNS Handling**: Detects and updates DNS changes without tunnel restarts.
- **Monitoring Tools**: Advanced tunnel monitoring features for tunnel performance monitoring.
- **Android TV Support**: Android TV support for secure streaming and browsing.
- **Advanced DNS**: DNS over HTTPS support for tunnel endpoint resolutions.
* Add tunnels via .conf file, zip, manual entry, clipboard, or QR code
* Auto-tunnel based on Wi-Fi SSID, ethernet, or mobile data
* Split tunneling by application with search
* Support for kernel and userspace modes
* Amnezia support for userspace mode for DPI/censorship protection
* Pre/Post Up/Down scripts support for all modes on a rooted device
* Always-On VPN support
* Export tunnels to zip
* Quick tile support for tunnel toggling, auto-tunneling
* Shortcuts support for tunnel toggling, auto-tunneling
* Intent automation support for all tunnels
* In app VPN kill switch with LAN bypass
* Automatic auto-tunneling service and/or tunnel restart after reboot or app update
* Battery preservation measures
* Restart tunnel on ping failure
## Building
```sh
git clone https://github.com/wgtunnel/wgtunnel
git clone https://github.com/zaneschepke/wgtunnel
cd wgtunnel
```
@@ -102,15 +114,6 @@ Help translate WG Tunnel into your language
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/)
## 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
Any contributions in the form of feedback, issues, code, or translations are welcome and much
+1 -2
View File
@@ -1,3 +1,2 @@
/build
/release
/src/main/assets/licenses.json
/release
+212 -226
View File
@@ -1,269 +1,255 @@
import com.android.build.gradle.internal.api.BaseVariantOutputImpl
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.hilt.android)
alias(libs.plugins.kotlinxSerialization)
alias(libs.plugins.ksp)
alias(libs.plugins.compose.compiler)
alias(libs.plugins.grgit)
alias(libs.plugins.licensee)
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.hilt.android)
alias(libs.plugins.kotlinxSerialization)
alias(libs.plugins.ksp)
alias(libs.plugins.compose.compiler)
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 {
namespace = Constants.APP_ID
compileSdk = Constants.TARGET_SDK
namespace = Constants.APP_ID
compileSdk = Constants.TARGET_SDK
androidResources { generateLocaleConfig = true }
androidResources {
generateLocaleConfig = true
}
dependenciesInfo {
includeInApk = false
includeInBundle = false
}
// reproducibility
dependenciesInfo {
// 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
packaging { resources { pickFirsts.add("okhttp3/internal/publicsuffix/publicsuffixes.gz") } }
ksp { arg("room.schemaLocation", "$projectDir/schemas") }
splits {
abi {
isEnable = !project.hasProperty("noSplits")
reset()
include("armeabi-v7a", "arm64-v8a")
isUniversalApk = !project.hasProperty("noSplits")
}
}
sourceSets {
getByName("debug").assets.srcDirs(files("$projectDir/schemas")) // Room
}
defaultConfig {
applicationId = Constants.APP_ID
minSdk = Constants.MIN_SDK
targetSdk = Constants.TARGET_SDK
versionCode = computeVersionCode()
versionName = computeVersionName()
buildConfigField("String[]", "LANGUAGES", "new String[]{ ${languageList().joinToString(separator = ", ") { "\"$it\"" }} }")
sourceSets { getByName("debug").assets.srcDirs(files("$projectDir/schemas")) }
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { useSupportLibrary = true }
}
val languagesArray = buildLanguagesArray(languageList())
buildConfigField("String[]", "LANGUAGES", "new String[]{ $languagesArray }")
signingConfigs {
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"
vectorDrawables { useSupportLibrary = true }
}
buildTypes {
// don't strip
packaging.jniLibs.keepDebugSymbols.addAll(
listOf("libwg-go.so", "libwg-quick.so", "libwg.so"),
)
signingConfigs {
create(Constants.RELEASE) {
storeFile = file(System.getenv("KEY_STORE_PATH") ?: "keystore/android_keystore.jks")
storePassword =
LocalProperties.get("SIGNING_STORE_PASSWORD")
?: System.getenv("SIGNING_STORE_PASSWORD")
keyAlias =
LocalProperties.get("SIGNING_KEY_ALIAS") ?: System.getenv("SIGNING_KEY_ALIAS")
keyPassword =
LocalProperties.get("SIGNING_KEY_PASSWORD") ?: System.getenv("SIGNING_KEY_PASSWORD")
}
}
release {
isDebuggable = false
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro",
)
signingConfig = signingConfigs.getByName(Constants.RELEASE)
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 {
packaging.jniLibs.keepDebugSymbols.addAll(
listOf("libwg-go.so", "libwg-quick.so", "libwg.so")
)
create(Constants.PRERELEASE) {
initWith(buildTypes.getByName(Constants.RELEASE))
applicationIdSuffix = ".prerelease"
versionNameSuffix = "-pre"
resValue("string", "app_name", "WG Tunnel - Pre")
resValue("string", "provider", "\"${Constants.APP_NAME}.provider.pre\"")
}
release {
isDebuggable = false
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro",
)
signingConfig = signingConfigs.getByName(Constants.RELEASE)
resValue("string", "provider", "\"${Constants.APP_NAME}.provider\"")
}
create(Constants.NIGHTLY) {
initWith(buildTypes.getByName(Constants.RELEASE))
applicationIdSuffix = ".nightly"
versionNameSuffix = "-nightly"
resValue("string", "app_name", "WG Tunnel - Nightly")
resValue("string", "provider", "\"${Constants.APP_NAME}.provider.nightly\"")
}
debug {
applicationIdSuffix = ".debug"
resValue("string", "app_name", "WG Tunnel Debug")
isDebuggable = true
resValue("string", "provider", "\"${Constants.APP_NAME}.provider.debug\"")
}
create(Constants.NIGHTLY) {
initWith(buildTypes.getByName(Constants.RELEASE))
applicationIdSuffix = ".nightly"
resValue("string", "app_name", "WG Tunnel Nightly")
resValue("string", "provider", "\"${Constants.APP_NAME}.provider.nightly\"")
}
}
flavorDimensions.add("type")
productFlavors {
create("fdroid") {
dimension = "type"
buildConfigField("String", "FLAVOR", "\"fdroid\"")
}
create("google") {
dimension = "type"
buildConfigField("String", "FLAVOR", "\"google\"")
}
create("standalone") {
dimension = "type"
buildConfigField("String", "FLAVOR", "\"standalone\"")
}
}
compileOptions {
isCoreLibraryDesugaringEnabled = true
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) }
}
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
}
}
applicationVariants.all {
val variant = this
variant.outputs
.map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl }
.forEach { output ->
val outputFileName =
"${Constants.APP_NAME}-${variant.flavorName}-" +
"${variant.buildType.name}-${variant.versionName}.apk"
output.outputFileName = outputFileName
}
}
}
flavorDimensions.add(Constants.TYPE)
productFlavors {
create("fdroid") {
dimension = Constants.TYPE
proguardFile("fdroid-rules.pro")
}
create("general") {
dimension = Constants.TYPE
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
isCoreLibraryDesugaringEnabled = true
}
kotlinOptions { jvmTarget = Constants.JVM_TARGET }
buildFeatures {
compose = true
buildConfig = true
}
packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } }
}
dependencies {
implementation(project(":logcatter"))
implementation(project(":networkmonitor"))
implementation(project(":logcatter"))
implementation(project(":networkmonitor"))
// Core foundations
implementation(libs.bundles.androidx.core.full)
implementation(libs.bundles.androidx.lifecycle.core)
implementation(libs.bundles.androidx.appcompat)
implementation(libs.bundles.androidx.storage)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
// Compose setup
implementation(platform(libs.androidx.compose.bom))
implementation(libs.bundles.androidx.compose.ui)
implementation(libs.bundles.androidx.compose.material)
implementation(libs.androidx.activity.compose)
// helpers for implementing LifecycleOwner in a Service
implementation(libs.androidx.lifecycle.service)
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)
// Navigation
implementation(libs.bundles.androidx.navigation3)
implementation(libs.bundles.navigation.lifecycle)
implementation(libs.bundles.androidx.hilt)
// test
testImplementation(libs.junit)
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)
// Material and icons
implementation(libs.bundles.google.material)
implementation(libs.bundles.material.icons)
// tunnel
implementation(libs.tunnel)
implementation(libs.amneziawg.android)
coreLibraryDesugaring(libs.desugar.jdk.libs)
// Database
implementation(libs.bundles.androidx.room)
implementation(libs.bundles.androidx.datastore)
ksp(libs.androidx.room.compiler)
// logging
implementation(libs.timber)
// DI and work
implementation(libs.bundles.hilt.android)
implementation(libs.bundles.androidx.work)
ksp(libs.hilt.android.compiler)
ksp(libs.androidx.hilt.compiler)
// compose navigation
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.hilt.navigation.compose)
// Networking and serialization
implementation(libs.bundles.ktor.client)
implementation(libs.bundles.kotlinx.serialization)
implementation(libs.ipaddress)
// hilt
implementation(libs.hilt.android)
ksp(libs.hilt.android.compiler)
ksp(libs.androidx.hilt.compiler)
// State management
implementation(libs.bundles.orbit.mvi)
// accompanist
implementation(libs.accompanist.permissions)
implementation(libs.accompanist.drawablepainter)
// Tunnel
implementation(libs.bundles.wireguard.tunnel)
// storage
implementation(libs.androidx.room.runtime)
ksp(libs.androidx.room.compiler)
implementation(libs.androidx.room.ktx)
implementation(libs.androidx.datastore.preferences)
// Shizuku
implementation(libs.bundles.shizuku)
// lifecycle
implementation(libs.lifecycle.runtime.compose)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.process)
// UI utilities
implementation(libs.bundles.ui.utilities)
// icons
implementation(libs.material.icons.extended)
// serialization
implementation(libs.kotlinx.serialization.json)
// Misc utilities
implementation(libs.bundles.misc.utilities)
coreLibraryDesugaring(libs.desugar.jdk.libs)
// barcode scanning
implementation(libs.zxing.android.embedded)
// Accompanist
implementation(libs.bundles.accompanist)
// bio
implementation(libs.androidx.biometric.ktx)
implementation(libs.pin.lock.compose)
// Lifecycle Compose
implementation(libs.lifecycle.runtime.compose)
// shortcuts
implementation(libs.androidx.core)
// Testing
testImplementation(libs.junit)
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)
// splash
implementation(libs.androidx.core.splashscreen)
debugImplementation(libs.leakcanary.android)
// Room database backup
implementation(libs.roomdatabasebackup) {
exclude(group = "org.reactivestreams", module = "reactive-streams")
}
// worker
implementation(libs.androidx.work.runtime)
implementation(libs.androidx.hilt.work)
}
tasks.register<Copy>("copyLicenseeJsonToAssets") {
dependsOn("licensee")
val outputAssets = layout.projectDirectory.dir("src/main/assets")
from(layout.buildDirectory.file("reports/licensee/androidFdroidRelease/artifacts.json")) {
rename("artifacts.json", "licenses.json")
}
into(outputAssets)
fun determineVersionName(): String {
return with(getBuildTaskName().lowercase()) {
when {
contains(Constants.NIGHTLY) || contains(Constants.PRERELEASE) ->
Constants.VERSION_NAME +
"-${grgitService.service.get().grgit.head().abbreviatedId}"
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 {
if (name.contains("ArtProfile")) {
enabled = false
}
if (name.startsWith("assemble") && !name.lowercase().contains("debug")) {
dependsOn(incrementVersionCode)
}
}
+42
View File
@@ -0,0 +1,42 @@
-dontwarn com.google.errorprone.annotations.**
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
<fields>;
}
# Keep all classes in the org.xbill.DNS package and subpackages
-keep class org.xbill.DNS.** { *; }
-dontwarn org.xbill.DNS.**
# Preserve JNA classes if used (e.g., for IPHlpAPI on Windows)
-keep class com.sun.jna.** { *; }
-dontwarn com.sun.jna.**
# Keep DNS resolver configuration classes that might be loaded dynamically
-keep class org.xbill.DNS.config.** { *; }
-dontwarn org.xbill.DNS.config.**
-keep class org.xbill.DNS.** { *; }
# Prevent optimization issues with native or reflection-based calls
-dontoptimize
-dontshrink
# Uncomment the above if errors persist, but use sparingly as theyre broad
# Suppress warnings about missing classes if not all features are used
-dontwarn java.lang.management.**
-dontwarn sun.nio.ch.**
-dontwarn com.google.api.client.http.GenericUrl
-dontwarn com.google.api.client.http.HttpHeaders
-dontwarn com.google.api.client.http.HttpRequest
-dontwarn com.google.api.client.http.HttpRequestFactory
-dontwarn com.google.api.client.http.HttpResponse
-dontwarn com.google.api.client.http.HttpTransport
-dontwarn com.google.api.client.http.javanet.NetHttpTransport$Builder
-dontwarn com.google.api.client.http.javanet.NetHttpTransport
-dontwarn javax.lang.model.element.Modifier
-dontwarn org.joda.time.Instant
-dontwarn org.slf4j.impl.StaticLoggerBinder
-dontwarn org.slf4j.impl.StaticMDCBinder
-dontwarn org.slf4j.impl.StaticMarkerBinder
+61 -1
View File
@@ -1 +1,61 @@
-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>;
}
# Keep all classes in the org.xbill.DNS package and subpackages
-keep class org.xbill.DNS.** { *; }
-dontwarn org.xbill.DNS.**
# Preserve JNA classes if used (e.g., for IPHlpAPI on Windows)
-keep class com.sun.jna.** { *; }
-dontwarn com.sun.jna.**
# Keep DNS resolver configuration classes that might be loaded dynamically
-keep class org.xbill.DNS.config.** { *; }
-dontwarn org.xbill.DNS.config.**
-keep class org.xbill.DNS.** { *; }
# Prevent optimization issues with native or reflection-based calls
-dontoptimize
-dontshrink
# Uncomment the above if errors persist, but use sparingly as theyre broad
# Suppress warnings about missing classes if not all features are used
-dontwarn java.lang.management.**
-dontwarn sun.nio.ch.**
-dontwarn com.google.api.client.http.GenericUrl
-dontwarn com.google.api.client.http.HttpHeaders
-dontwarn com.google.api.client.http.HttpRequest
-dontwarn com.google.api.client.http.HttpRequestFactory
-dontwarn com.google.api.client.http.HttpResponse
-dontwarn com.google.api.client.http.HttpTransport
-dontwarn com.google.api.client.http.javanet.NetHttpTransport$Builder
-dontwarn com.google.api.client.http.javanet.NetHttpTransport
-dontwarn javax.lang.model.element.Modifier
-dontwarn org.joda.time.Instant
-dontwarn org.slf4j.impl.StaticLoggerBinder
-dontwarn org.slf4j.impl.StaticMDCBinder
-dontwarn org.slf4j.impl.StaticMarkerBinder
@@ -155,7 +155,9 @@
"columnNames": [
"id"
]
}
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "TunnelConfig",
@@ -225,18 +227,21 @@
"fieldPath": "pingInterval",
"columnName": "ping_interval",
"affinity": "INTEGER",
"notNull": false,
"defaultValue": "null"
},
{
"fieldPath": "pingCooldown",
"columnName": "ping_cooldown",
"affinity": "INTEGER",
"notNull": false,
"defaultValue": "null"
},
{
"fieldPath": "pingIp",
"columnName": "ping_ip",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "null"
},
{
@@ -270,9 +275,11 @@
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
}
]
],
"foreignKeys": []
}
],
"views": [],
"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, '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)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.zaneschepke.wireguardautotunnel", appContext.packageName)
}
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
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.platform.app.InstrumentationRegistry
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
import java.io.IOException
import com.zaneschepke.wireguardautotunnel.data.Queries
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import java.io.IOException
@RunWith(AndroidJUnit4::class)
class MigrationTest {
private val dbName = "migration-test"
private val dbName = "migration-test"
@get:Rule
val helper: MigrationTestHelper =
MigrationTestHelper(InstrumentationRegistry.getInstrumentation(), AppDatabase::class.java)
@get:Rule
val helper: MigrationTestHelper =
MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
AppDatabase::class.java,
)
@Test
@Throws(IOException::class)
fun migrate6To7() {
helper.createDatabase(dbName, 6).apply {
// Database has schema version 1. Insert some data using SQL queries.
// You can't use DAO classes because they expect the latest schema.
// Prepare for the next version.
close()
}
@Test
@Throws(IOException::class)
fun migrate6To7() {
helper.createDatabase(dbName, 6).apply {
// Database has schema version 1. Insert some data using SQL queries.
// You can't use DAO classes because they expect the latest schema.
execSQL(Queries.createDefaultSettings())
execSQL(
Queries.createTunnelConfig(),
)
// Prepare for the next version.
close()
}
// Re-open the database with version 2 and provide
// MIGRATION_1_2 as the migration process.
helper.runMigrationsAndValidate(dbName, 7, true)
// MigrationTestHelper automatically verifies the schema changes,
// but you need to validate that the data was migrated properly.
}
// Re-open the database with version 2 and provide
// MIGRATION_1_2 as the migration process.
helper.runMigrationsAndValidate(dbName, 7, true)
// MigrationTestHelper automatically verifies the schema changes,
// 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>
+26 -67
View File
@@ -2,29 +2,24 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
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" />
<!--for split tunneling-->
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
<!--foreground service special use for non VPN service tunnels, android 14-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<!--foreground service special use for VPN service tunnels, android 14-->
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<!--foreground service exempt android 14-->
<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-->
<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" />
<!--start service on boot permission-->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<!--android tv support-->
<permission
android:name="${applicationId}.permission.CONTROL_TUNNELS"
android:label="@string/app_permission_title"
android:description="@string/app_permission_description"
android:icon="@mipmap/ic_launcher"
android:protectionLevel="dangerous" />
@@ -46,10 +41,16 @@
<uses-feature android:name="android.hardware.wifi"
android:required="false"/>
<queries>
<intent>
<action android:name="android.intent.action.MAIN" />
</intent>
</queries>
<application
android:name=".WireGuardAutoTunnel"
android:allowBackup="false"
android:banner="@mipmap/ic_banner"
android:banner="@drawable/ic_banner"
android:dataExtractionRules="@xml/data_extraction_rules"
android:enableOnBackInvokedCallback="true"
android:fullBackupContent="@xml/backup_rules"
@@ -59,10 +60,6 @@
android:supportsRtl="true"
android:theme="@style/Theme.App.Start"
tools:targetApi="tiramisu">
<activity
android:name="com.journeyapps.barcodescanner.CaptureActivity"
android:screenOrientation="portrait"
tools:replace="screenOrientation" />
<activity
android:name=".MainActivity"
android:exported="true"
@@ -70,12 +67,6 @@
android:theme="@style/Theme.WireguardAutoTunnel"
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>
<action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.SHOW_APP_INFO" />
@@ -85,11 +76,15 @@
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
</intent-filter>
</activity>
<activity
android:name="com.journeyapps.barcodescanner.CaptureActivity"
android:screenOrientation="portrait"
tools:replace="screenOrientation" />
<activity
android:name=".core.shortcut.ShortcutsActivity"
android:enabled="true"
android:exported="false"
android:exported="true"
android:noHistory="true"
android:excludeFromRecents="true"
android:finishOnTaskLaunch="true"
@@ -116,7 +111,7 @@
<service
android:name=".core.service.tile.TunnelControlTile"
android:exported="true"
android:icon="@drawable/ic_notification"
android:icon="@drawable/ic_launcher"
android:label="@string/tunnel_control"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<meta-data
@@ -133,7 +128,7 @@
<service
android:name=".core.service.tile.AutoTunnelControlTile"
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
@@ -151,39 +146,16 @@
android:name=".core.service.autotunnel.AutoTunnelService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="specialUse"
android:foregroundServiceType="systemExempted"
android:persistent="true"
android:stopWithTask="false"
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>
tools:node="merge" />
<service
android:name=".core.service.VpnForegroundService"
android:name=".core.service.TunnelForegroundService"
android:exported="false"
android:persistent="true"
android:foregroundServiceType="systemExempted"
android:foregroundServiceType="systemExempted"
android:permission="android.permission.BIND_VPN_SERVICE">
<intent-filter>
<action android:name="android.net.VpnService" />
@@ -193,8 +165,9 @@
<receiver
android:name=".core.broadcast.RestartReceiver"
android:enabled="true"
android:exported="false">
android:exported="true">
<intent-filter>
<category android:name="android.intent.category.DEFAULT" />
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
@@ -209,20 +182,6 @@
<action android:name="com.wireguard.android.action.REFRESH_TUNNEL_STATES" />
</intent-filter>
</receiver>
<!--custom security solution for easier user integration-->
<receiver
android:name=".core.broadcast.RemoteControlReceiver"
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
android:name=".core.broadcast.NotificationActionReceiver"
android:exported="false"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 16 KiB

@@ -1,589 +1,280 @@
package com.zaneschepke.wireguardautotunnel
import ProxySettingsScreen
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Color
import android.net.VpnService
import android.os.Build
import android.os.Bundle
import androidx.activity.SystemBarStyle
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
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.Scaffold
import androidx.compose.material3.SnackbarData
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextLinkStyles
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withLink
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.core.content.ContextCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
import androidx.navigation3.ui.NavDisplay
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.toRoute
import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
import com.zaneschepke.wireguardautotunnel.data.DataStoreManager.Companion.shouldShowDonationSnackbar
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm
import com.zaneschepke.wireguardautotunnel.ui.common.banner.AppAlertBanner
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.VpnDeniedDialog
import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.CustomSnackBar
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarInfo
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarType
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.rememberCustomSnackbarState
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
import com.zaneschepke.wireguardautotunnel.ui.navigation.Tab
import com.zaneschepke.wireguardautotunnel.ui.navigation.components.BottomNavbar
import com.zaneschepke.wireguardautotunnel.ui.navigation.components.DynamicTopAppBar
import com.zaneschepke.wireguardautotunnel.ui.navigation.components.currentRouteAsNavbarState
import com.zaneschepke.wireguardautotunnel.ui.navigation.functions.rememberNavController
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.AutoTunnelScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.advanced.AutoTunnelAdvancedScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.detection.WifiDetectionMethodScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.disclosure.LocationDisclosureScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.preferred.PreferredTunnelScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.wifi.WifiSettingsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.pin.PinLockScreen
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarControllerProvider
import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.ConfigScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.main.OptionsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.main.PinLockScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ScannerScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.main.splittunnel.SplitTunnelScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.main.TunnelAutoTunnelScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.AdvancedScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.AppearanceScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.DisplayScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.KillSwitchScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.LanguageScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.LocationDisclosureScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.AppearanceScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.display.DisplayScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.language.LanguageScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.dns.DnsSettingsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.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.settings.autotunnel.AutoTunnelScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.LogsScreen
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.util.LocaleUtil
import com.zaneschepke.wireguardautotunnel.util.extensions.*
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 com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
import dagger.hilt.android.AndroidEntryPoint
import de.raphaelebner.roomdatabasebackup.core.RoomBackup
import java.util.*
import timber.log.Timber
import javax.inject.Inject
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import xyz.teamgravity.pin_lock_compose.PinManager
import kotlin.system.exitProcess
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit var appStateRepository: AppStateRepository
@Inject lateinit var tunnelRepository: TunnelRepository
@Inject lateinit var appDatabase: AppDatabase
@Inject lateinit var networkMonitor: NetworkMonitor
@Inject
lateinit var appStateRepository: AppStateRepository
private lateinit var roomBackup: RoomBackup
@Inject
lateinit var tunnelManager: TunnelManager
@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
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)
@Inject
lateinit var shortcutManager: ShortcutManager
roomBackup = RoomBackup(this)
@Inject
lateinit var networkMonitor: NetworkMonitor
val viewModel by viewModels<SharedAppViewModel>()
private var lastLocationPermissionState: Boolean? = null
installSplashScreen().apply {
setKeepOnScreenCondition { !viewModel.container.stateFlow.value.isAppLoaded }
}
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)
setContent {
val context = LocalContext.current
val isTv = isRunningOnTv()
val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
val scope = rememberCoroutineScope()
val viewModel by viewModels<AppViewModel>()
LaunchedEffect(uiState.isAppLoaded) {
if (uiState.isAppLoaded) {
uiState.locale.let { LocaleUtil.changeLocale(it) }
}
}
installSplashScreen().apply {
setKeepOnScreenCondition {
!viewModel.isAppReady.value
}
}
val snackbarState = rememberCustomSnackbarState()
var showVpnPermissionDialog by remember { mutableStateOf(false) }
var vpnPermissionDenied by remember { mutableStateOf(false) }
var requestingAppMode by remember {
mutableStateOf<Pair<AppMode?, TunnelConfig?>>(Pair(null, null))
}
setContent {
val appUiState by viewModel.uiState.collectAsStateWithLifecycle()
val configurationChange by viewModel.configurationChange.collectAsStateWithLifecycle()
val navController = rememberNavController()
val startingStack = buildList {
add(Route.Tunnels)
if (intent?.action == Intent.ACTION_APPLICATION_PREFERENCES) add(Route.Settings)
if (uiState.pinLockEnabled) add(Route.Lock)
}
LaunchedEffect(configurationChange) {
if (configurationChange) {
Intent(this@MainActivity, MainActivity::class.java).also {
startActivity(it)
exitProcess(0)
}
}
}
val backStack = rememberNavBackStack(*startingStack.toTypedArray())
var previousRoute by remember { mutableStateOf<Route?>(null) }
with(appUiState.appSettings) {
LaunchedEffect(isShortcutsEnabled) {
if (!isShortcutsEnabled) return@LaunchedEffect shortcutManager.removeShortcuts()
shortcutManager.addShortcuts()
}
}
val navController =
rememberNavController<NavKey>(backStack, uiState.isLocationDisclosureShown) {
previousKey ->
previousRoute = previousKey as? Route
}
CompositionLocalProvider(LocalNavController provides navController) {
SnackbarControllerProvider { host ->
WireguardAutoTunnelTheme(theme = appUiState.generalState.theme) {
Scaffold(
contentWindowInsets = WindowInsets(0),
snackbarHost = {
SnackbarHost(host) { snackbarData: SnackbarData ->
CustomSnackBar(
snackbarData.visuals.message,
isRtl = false,
containerColor =
MaterialTheme.colorScheme.surfaceColorAtElevation(
2.dp,
),
)
}
},
bottomBar = {
BottomNavBar(
navController,
listOf(
BottomNavItem(
name = stringResource(R.string.tunnels),
route = Route.Main,
icon = Icons.Rounded.Home,
),
BottomNavItem(
name = stringResource(R.string.settings),
route = Route.Settings,
icon = Icons.Rounded.Settings,
),
BottomNavItem(
name = stringResource(R.string.support),
route = Route.Support,
icon = Icons.Rounded.QuestionMark,
),
),
)
},
) { padding ->
Box(modifier = Modifier.Companion.fillMaxSize().padding(padding)) {
NavHost(
navController,
enterTransition = { fadeIn(tween(Constants.TRANSITION_ANIMATION_TIME)) },
exitTransition = { fadeOut(tween(Constants.TRANSITION_ANIMATION_TIME)) },
startDestination = (if (appUiState.generalState.isPinLockEnabled) Route.Lock else Route.Main),
) {
composable<Route.Main> {
MainScreen(
uiState = appUiState,
)
}
composable<Route.Settings> {
SettingsScreen(
appViewModel = viewModel,
uiState = appUiState,
)
}
composable<Route.LocationDisclosure> {
LocationDisclosureScreen(viewModel, appUiState)
}
composable<Route.AutoTunnel> {
AutoTunnelScreen(
appUiState.appSettings,
)
}
composable<Route.Appearance> {
AppearanceScreen()
}
composable<Route.Language> {
LanguageScreen(appUiState, viewModel)
}
composable<Route.Display> {
DisplayScreen(appUiState)
}
composable<Route.Support> {
SupportScreen(appUiState, viewModel)
}
composable<Route.AutoTunnelAdvanced> {
AdvancedScreen(appUiState.appSettings, viewModel)
}
composable<Route.Logs> {
LogsScreen()
}
composable<Route.Config> { backStack ->
val args = backStack.toRoute<Route.Config>()
val config =
appUiState.tunnels.firstOrNull { it.id == args.id }
ConfigScreen(config)
}
composable<Route.TunnelOptions> { backStack ->
val args = backStack.toRoute<Route.TunnelOptions>()
appUiState.tunnels.firstOrNull { it.id == args.id }?.let { config ->
OptionsScreen(config, appUiState)
}
}
composable<Route.Lock> {
PinLockScreen(viewModel)
}
composable<Route.Scanner> {
ScannerScreen()
}
composable<Route.KillSwitch> {
KillSwitchScreen(appUiState, viewModel)
}
composable<Route.SplitTunnel> {
SplitTunnelScreen()
}
composable<Route.TunnelAutoTunnel> { backStack ->
val args = backStack.toRoute<Route.TunnelOptions>()
appUiState.tunnels.firstOrNull { it.id == args.id }?.let {
TunnelAutoTunnelScreen(it, appUiState.appSettings)
}
}
}
BackHandler {
if (navController.previousBackStackEntry == null || !navController.popBackStack()) {
this@MainActivity.finish()
}
}
}
}
}
}
}
}
}
override fun onResume() {
super.onResume()
checkPermissionAndNotify()
}
val vpnActivity =
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) {
viewModel.globalSideEffect.collectLatest { sideEffect ->
when (sideEffect) {
GlobalSideEffect.ConfigChanged -> restartApp()
GlobalSideEffect.PopBackStack -> navController.pop()
is GlobalSideEffect.RequestVpnPermission -> {
requestingAppMode = Pair(sideEffect.requestingMode, sideEffect.config)
vpnActivity.launch(VpnService.prepare(this@MainActivity))
}
is GlobalSideEffect.Snackbar -> {
scope.launch {
snackbarState.showSnackbar(
SnackbarInfo(
message =
buildAnnotatedString {
append(sideEffect.message.asString(context))
},
type = sideEffect.type ?: SnackbarType.INFO,
durationMs = sideEffect.durationMs ?: 4000L,
)
)
}
}
is GlobalSideEffect.Toast ->
scope.launch { context.showToast(sideEffect.message.asString(context)) }
is GlobalSideEffect.LaunchUrl -> context.openWebUrl(sideEffect.url)
is GlobalSideEffect.InstallApk -> context.installApk(sideEffect.apk)
}
}
}
if (!uiState.isAppLoaded) return@setContent
var showLock by remember {
mutableStateOf(uiState.pinLockEnabled && !uiState.isPinVerified)
}
LaunchedEffect(uiState.isPinVerified) { if (uiState.isPinVerified) showLock = false }
CompositionLocalProvider(
LocalIsAndroidTV provides isTv,
LocalSharedVm provides viewModel,
LocalNavController provides navController,
) {
WireguardAutoTunnelTheme(theme = uiState.theme) {
VpnDeniedDialog(
showVpnPermissionDialog,
onDismiss = {
showVpnPermissionDialog = false
vpnPermissionDenied = false
},
)
val annotatedMessage = buildAnnotatedString {
append(context.getString(R.string.donation_prompt_prefix))
append(" ")
withLink(
LinkAnnotation.Clickable(
tag = context.getString(R.string.support),
styles =
TextLinkStyles(
style =
SpanStyle(
textDecoration = TextDecoration.Underline,
color = MaterialTheme.colorScheme.primary,
),
focusedStyle =
SpanStyle(
textDecoration = TextDecoration.Underline,
color = MaterialTheme.colorScheme.primary,
background =
MaterialTheme.colorScheme.primary.copy(
alpha = 0.2f
),
),
),
) {
snackbarState.dismissCurrent()
navController.push(Route.Donate)
}
) {
append(context.getString(R.string.donation_prompt_link))
}
append(" ")
append(context.getString(R.string.donation_prompt_suffix))
}
LaunchedEffect(uiState.shouldShowDonationSnackbar) {
if (
uiState.shouldShowDonationSnackbar && !uiState.settings.alreadyDonated
) {
viewModel.setShouldShowDonationSnackbar(false)
snackbarState.showSnackbar(
SnackbarInfo(
message = annotatedMessage,
type = SnackbarType.THANK_YOU,
durationMs = 30_000L,
)
)
}
}
if (showLock) {
PinManager.initialize(context = this@MainActivity)
PinLockScreen()
} else {
val currentRoute by remember {
derivedStateOf { backStack.lastOrNull() as? Route }
}
val currentTab by remember {
derivedStateOf { Tab.fromRoute(currentRoute ?: Route.Tunnels) }
}
val navState by
currentRouteAsNavbarState(
uiState,
viewModel,
currentRoute,
navController,
)
Box(modifier = Modifier.fillMaxSize()) {
if (uiState.settings.appMode == AppMode.LOCK_DOWN) {
AppAlertBanner(
stringResource(R.string.locked_down)
.uppercase(Locale.getDefault()),
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 =
hiltViewModel<
TunnelViewModel,
TunnelViewModel.Factory,
>(
creationCallback = { factory ->
factory.create(key.id)
}
)
TunnelSettingsScreen(viewModel)
}
entry<Route.SplitTunnel> { key ->
val viewModel =
hiltViewModel<
SplitTunnelViewModel,
SplitTunnelViewModel.Factory,
>(
creationCallback = { factory ->
factory.create(key.id)
}
)
SplitTunnelScreen(viewModel)
}
entry<Route.Config> { key ->
val viewModel =
hiltViewModel<
ConfigViewModel,
ConfigViewModel.Factory,
>(
creationCallback = { factory ->
factory.create(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 =
hiltViewModel<
ConfigViewModel,
ConfigViewModel.Factory,
>(
creationCallback = { factory ->
factory.create(key.id)
}
)
ConfigScreen(viewModel)
}
entry<Route.SplitTunnelGlobal> { key ->
val viewModel =
hiltViewModel<
SplitTunnelViewModel,
SplitTunnelViewModel.Factory,
>(
creationCallback = { factory ->
factory.create(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()
}
private fun checkPermissionAndNotify() {
val hasLocation = ContextCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_FINE_LOCATION,
) == PackageManager.PERMISSION_GRANTED
if (lastLocationPermissionState != hasLocation) {
Timber.d("Location permission changed to: $hasLocation")
if (hasLocation) {
networkMonitor.sendLocationPermissionsGrantedBroadcast()
}
lastLocationPermissionState = hasLocation
}
}
}
@@ -4,94 +4,138 @@ import android.app.Application
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.wireguardautotunnel.core.notification.NotificationMonitor
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.core.worker.ServiceWorker
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository
import com.zaneschepke.wireguardautotunnel.di.MainDispatcher
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import dagger.hilt.android.HiltAndroidApp
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject
@HiltAndroidApp
class WireGuardAutoTunnel : Application(), Configuration.Provider {
@Inject lateinit var workerFactory: HiltWorkerFactory
@Inject
lateinit var workerFactory: HiltWorkerFactory
override val workManagerConfiguration: Configuration
get() = Configuration.Builder().setWorkerFactory(workerFactory).build()
override val workManagerConfiguration: Configuration
get() = Configuration.Builder()
.setWorkerFactory(workerFactory)
.build()
@Inject @ApplicationScope lateinit var applicationScope: CoroutineScope
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
@Inject lateinit var logReader: LogReader
@Inject
lateinit var logReader: LogReader
@Inject @IoDispatcher lateinit var ioDispatcher: CoroutineDispatcher
@Inject
lateinit var appDataRepository: AppDataRepository
@Inject lateinit var monitoringRepository: MonitoringSettingsRepository
@Inject
@IoDispatcher
lateinit var ioDispatcher: CoroutineDispatcher
@Inject lateinit var notificationMonitor: NotificationMonitor
@Inject
@MainDispatcher
lateinit var mainDispatcher: CoroutineDispatcher
override fun onCreate() {
super.onCreate()
instance = this
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
StrictMode.setThreadPolicy(
ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()
.penaltyLog()
.build()
)
} else {
Timber.plant(ReleaseTree())
}
@Inject
lateinit var tunnelManager: TunnelManager
applicationScope.launch(ioDispatcher) {
launch {
val monitoringSettings = monitoringRepository.getMonitoringSettings()
if (monitoringSettings.isLocalLogsEnabled) logReader.start()
}
launch { notificationMonitor.handleApplicationNotifications() }
}
override fun onCreate() {
super.onCreate()
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())
}
ServiceWorker.start(this)
}
GoBackend.setAlwaysOnCallback {
applicationScope.launch {
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")
}
}
}
companion object {
ServiceWorker.start(this)
private val _uiActive = MutableStateFlow(false)
applicationScope.launch {
withContext(mainDispatcher) {
if (appDataRepository.appState.isLocalLogsEnabled() && !isRunningOnTv()) logReader.start()
}
if (!appDataRepository.settings.get().isKernelEnabled) {
tunnelManager.setBackendState(BackendState.SERVICE_ACTIVE, emptyList())
}
appDataRepository.appState.getLocale()?.let {
withContext(mainDispatcher) {
LocaleUtil.changeLocale(it)
}
}
}
}
val uiActive: StateFlow<Boolean>
get() = _uiActive
override fun onTerminate() {
applicationScope.launch {
tunnelManager.setBackendState(BackendState.SERVICE_ACTIVE, emptyList())
}
super.onTerminate()
}
fun setUiActive(active: Boolean) {
_uiActive.update { active }
}
class AppLifecycleObserver : DefaultLifecycleObserver {
@Volatile private var lastActiveTunnels: List<Int> = emptyList()
override fun onStart(owner: LifecycleOwner) {
Timber.d("Application entered foreground")
foreground = true
}
override fun onPause(owner: LifecycleOwner) {
Timber.d("Application entered background")
foreground = false
}
}
@Synchronized
fun getLastActiveTunnels(): List<Int> {
return lastActiveTunnels
}
companion object {
private var foreground = false
@Synchronized
fun setLastActiveTunnels(newTunnels: List<Int>) {
lastActiveTunnels = newTunnels
}
fun isForeground(): Boolean {
return foreground
}
lateinit var instance: WireGuardAutoTunnel
private set
}
lateinit var instance: WireGuardAutoTunnel
private set
}
}
@@ -3,36 +3,47 @@ package com.zaneschepke.wireguardautotunnel.core.broadcast
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import 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 javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint
class KernelReceiver : BroadcastReceiver() {
@Inject @ApplicationScope lateinit var applicationScope: CoroutineScope
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
@Inject lateinit var tunnelRepository: TunnelRepository
@Inject
lateinit var tunnelRepository: TunnelRepository
@Inject lateinit var tunnelManager: TunnelManager
@Inject
lateinit var serviceManager: ServiceManager
override fun onReceive(context: Context, intent: Intent) {
val action = intent.action ?: return
applicationScope.launch {
if (action == REFRESH_TUNNELS_ACTION) {
tunnelManager.runningTunnelNames().forEach { name ->
val tunnel = tunnelRepository.findByTunnelName(name)
tunnel?.let { tunnelRepository.save(it.copy(isActive = true)) }
}
}
}
}
@Inject
lateinit var tunnelManager: TunnelManager
companion object {
const val REFRESH_TUNNELS_ACTION = "com.wireguard.android.action.REFRESH_TUNNEL_STATES"
}
override fun onReceive(context: Context, intent: Intent) {
val action = intent.action ?: return
applicationScope.launch {
if (action == REFRESH_TUNNELS_ACTION) {
tunnelManager.runningTunnelNames().forEach { name ->
val tunnel = tunnelRepository.findByTunnelName(name)
tunnel?.let {
tunnelRepository.save(it.copy(isActive = true))
}
}
serviceManager.updateTunnelTile()
}
}
}
companion object {
const val REFRESH_TUNNELS_ACTION = "com.wireguard.android.action.REFRESH_TUNNEL_STATES"
}
}
@@ -4,43 +4,43 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint
class NotificationActionReceiver : BroadcastReceiver() {
@Inject lateinit var tunnelManager: TunnelManager
@Inject
lateinit var serviceManager: ServiceManager
@Inject lateinit var tunnelRepository: TunnelRepository
@Inject
lateinit var tunnelManager: TunnelManager
@Inject lateinit var autoTunnelRepository: AutoTunnelSettingsRepository
@Inject
lateinit var tunnelRepository: TunnelRepository
@Inject @ApplicationScope lateinit var applicationScope: CoroutineScope
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
override fun onReceive(context: Context, intent: Intent) {
applicationScope.launch {
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 {
const val STOP_ALL_TUNNELS_ID = 0
}
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,95 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.broadcast
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.util.Constants
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
@AndroidEntryPoint
class RemoteControlReceiver : BroadcastReceiver() {
@Inject @ApplicationScope lateinit var applicationScope: CoroutineScope
@Inject lateinit var settingsRepository: GeneralSettingRepository
@Inject lateinit var tunnelsRepository: TunnelRepository
@Inject lateinit var autoTunnelSettingsRepository: AutoTunnelSettingsRepository
@Inject lateinit var tunnelManager: TunnelManager
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"
}
}
@@ -3,42 +3,62 @@ 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.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class RestartReceiver : BroadcastReceiver() {
@Inject
lateinit var appDataRepository: AppDataRepository
@Inject @ApplicationScope lateinit var applicationScope: CoroutineScope
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
@Inject lateinit var tunnelManager: TunnelManager
@Inject
lateinit var serviceManager: ServiceManager
@Inject lateinit var appStateRepository: AppStateRepository
@Inject
lateinit var tunnelManager: TunnelManager
@Inject lateinit var logReader: LogReader
@Inject
@IoDispatcher
lateinit var ioDispatcher: CoroutineDispatcher
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 -> {
tunnelManager.handleRestore()
logReader.deleteAndClearLogs()
appStateRepository.setShouldShowDonationSnackbar(true)
}
}
}
}
override fun onReceive(context: Context, intent: Intent) {
val action = intent.action ?: return
if (action != Intent.ACTION_BOOT_COMPLETED &&
action != Intent.ACTION_MY_PACKAGE_REPLACED &&
action != "com.htc.intent.action.QUICKBOOT_POWERON"
) {
return
}
Timber.d("RestartReceiver triggered with action: ${intent.action}")
applicationScope.launch(ioDispatcher) {
serviceManager.updateTunnelTile()
serviceManager.updateAutoTunnelTile()
val settings = appDataRepository.settings.get()
if (settings.isRestoreOnBootEnabled) {
if (settings.isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) {
Timber.d("Starting auto-tunnel on boot/update")
serviceManager.startAutoTunnel(true)
} else {
Timber.d("Restoring previous tunnel state")
tunnelManager.restorePreviousState()
}
} else {
Timber.d("Restore on boot disabled, skipping")
}
}
}
}
@@ -4,59 +4,44 @@ import android.app.Notification
import android.app.NotificationManager
import android.content.Context
import androidx.core.app.NotificationCompat
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification.NotificationChannels
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification.NotificationChannels
import com.zaneschepke.wireguardautotunnel.util.StringValue
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(
channel: NotificationChannels,
title: String = "",
actions: Collection<NotificationCompat.Action> = emptyList(),
description: String = "",
showTimestamp: Boolean = true,
importance: Int = NotificationManager.IMPORTANCE_LOW,
onGoing: Boolean = false,
onlyAlertOnce: Boolean = true,
groupKey: String? = null,
isGroupSummary: Boolean = false,
): Notification
fun createNotification(
channel: NotificationChannels,
title: StringValue,
actions: Collection<NotificationCompat.Action> = emptyList(),
description: StringValue,
showTimestamp: Boolean = false,
importance: Int = NotificationManager.IMPORTANCE_HIGH,
onGoing: Boolean = true,
onlyAlertOnce: Boolean = true,
): Notification
fun createNotification(
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(notificationAction: NotificationAction, extraId: Int? = null): NotificationCompat.Action
fun createNotificationAction(
notificationAction: NotificationAction,
extraId: Int? = null,
): NotificationCompat.Action
fun remove(notificationId: Int)
fun remove(notificationId: Int)
fun show(notificationId: Int, notification: Notification)
fun show(notificationId: Int, notification: Notification)
companion object {
const val VPN_GROUP_KEY = "VPN_GROUP"
const val AUTO_TUNNEL_GROUP_KEY = "AUTO_TUNNEL_GROUP"
const val AUTO_TUNNEL_LOCATION_PERMISSION_ID = 123
const val AUTO_TUNNEL_LOCATION_SERVICES_ID = 124
// 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"
}
companion object {
const val KERNEL_SERVICE_NOTIFICATION_ID = 123
const val AUTO_TUNNEL_NOTIFICATION_ID = 122
const val VPN_NOTIFICATION_ID = 100
const val EXTRA_ID = "id"
}
}
@@ -1,64 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.notification
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.util.StringValue
import jakarta.inject.Inject
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
class NotificationMonitor
@Inject
constructor(
private val tunnelManager: TunnelManager,
private val notificationManager: NotificationManager,
) {
suspend fun handleApplicationNotifications() = coroutineScope {
launch { handleTunnelErrors() }
launch { handleTunnelMessages() }
}
private suspend fun handleTunnelErrors() =
tunnelManager.errorEvents.collectLatest { (tunName, error) ->
if (!WireGuardAutoTunnel.uiActive.value) {
val notification =
notificationManager.createNotification(
WireGuardNotification.NotificationChannels.VPN,
title =
tunName?.let { StringValue.DynamicString(it) }
?: StringValue.StringResource(R.string.tunnel),
description =
StringValue.StringResource(
R.string.tunnel_error_template,
error.toStringValue(),
),
groupKey = NotificationManager.VPN_GROUP_KEY,
)
notificationManager.show(
NotificationManager.TUNNEL_ERROR_NOTIFICATION_ID,
notification,
)
}
}
private suspend fun handleTunnelMessages() =
tunnelManager.messageEvents.collectLatest { (tunName, message) ->
if (!WireGuardAutoTunnel.uiActive.value) {
val notification =
notificationManager.createNotification(
WireGuardNotification.NotificationChannels.VPN,
title =
tunName?.let { StringValue.DynamicString(it) }
?: StringValue.StringResource(R.string.tunnel),
description = message.toStringValue(),
groupKey = NotificationManager.VPN_GROUP_KEY,
)
notificationManager.show(
NotificationManager.TUNNEL_MESSAGES_NOTIFICATION_ID,
notification,
)
}
}
}
@@ -3,173 +3,167 @@ package com.zaneschepke.wireguardautotunnel.core.notification
import android.Manifest
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Color
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.zaneschepke.wireguardautotunnel.MainActivity
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.core.broadcast.NotificationActionReceiver
import com.zaneschepke.wireguardautotunnel.MainActivity
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager.Companion.EXTRA_ID
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
import com.zaneschepke.wireguardautotunnel.util.StringValue
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
class WireGuardNotification @Inject constructor(@ApplicationContext override val context: Context) :
NotificationManager {
class WireGuardNotification
@Inject
constructor(
@ApplicationContext override val context: Context,
) : com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager {
enum class NotificationChannels {
VPN,
AUTO_TUNNEL,
}
enum class NotificationChannels {
VPN,
AUTO_TUNNEL,
}
private val notificationManager = NotificationManagerCompat.from(context)
private val notificationManager = NotificationManagerCompat.from(context)
override fun createNotification(
channel: NotificationChannels,
title: String,
actions: Collection<NotificationCompat.Action>,
description: String,
showTimestamp: Boolean,
importance: Int,
onGoing: Boolean,
onlyAlertOnce: Boolean,
groupKey: String?,
isGroupSummary: Boolean,
): Notification {
notificationManager.createNotificationChannel(channel.asChannel(importance))
return channel
.asBuilder()
.apply {
actions.forEach { addAction(it) }
setContentTitle(title)
setContentIntent(
PendingIntent.getActivity(
context,
0,
Intent(context, MainActivity::class.java)
.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP),
PendingIntent.FLAG_IMMUTABLE,
)
)
setContentText(description)
setOnlyAlertOnce(onlyAlertOnce)
setOngoing(onGoing)
setPriority(NotificationCompat.PRIORITY_LOW)
setShowWhen(showTimestamp)
setSmallIcon(R.drawable.ic_notification)
if (groupKey != null) {
setGroup(groupKey)
if (isGroupSummary) {
setGroupSummary(true)
}
}
}
.build()
}
override fun createNotification(
channel: NotificationChannels,
title: String,
actions: Collection<NotificationCompat.Action>,
description: String,
showTimestamp: Boolean,
importance: Int,
onGoing: Boolean,
onlyAlertOnce: Boolean,
): Notification {
notificationManager.createNotificationChannel(channel.asChannel())
return channel.asBuilder().apply {
actions.forEach {
addAction(it)
}
setContentTitle(title)
setContentIntent(
PendingIntent.getActivity(
context,
0,
Intent(context, MainActivity::class.java),
PendingIntent.FLAG_IMMUTABLE,
),
)
setContentText(description)
setOnlyAlertOnce(onlyAlertOnce)
setOngoing(onGoing)
setPriority(NotificationCompat.PRIORITY_HIGH)
setShowWhen(showTimestamp)
setSmallIcon(R.drawable.ic_launcher)
}.build()
}
override fun createNotification(
channel: NotificationChannels,
title: StringValue,
actions: Collection<NotificationCompat.Action>,
description: StringValue,
showTimestamp: Boolean,
importance: Int,
onGoing: Boolean,
onlyAlertOnce: Boolean,
groupKey: String?,
isGroupSummary: Boolean,
): Notification {
return createNotification(
channel,
title.asString(context),
actions,
description.asString(context),
showTimestamp,
importance,
onGoing,
onlyAlertOnce,
)
}
override fun createNotification(
channel: NotificationChannels,
title: StringValue,
actions: Collection<NotificationCompat.Action>,
description: StringValue,
showTimestamp: Boolean,
importance: Int,
onGoing: Boolean,
onlyAlertOnce: Boolean,
): Notification {
return createNotification(
channel,
title.asString(context),
actions,
description.asString(context),
showTimestamp,
importance,
onGoing,
onlyAlertOnce,
)
}
override fun createNotificationAction(
notificationAction: NotificationAction,
extraId: Int?,
): NotificationCompat.Action {
val pendingIntent =
PendingIntent.getBroadcast(
context,
extraId ?: 0,
Intent(context, NotificationActionReceiver::class.java).apply {
action = notificationAction.name
if (extraId != null) putExtra(EXTRA_ID, extraId)
},
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
)
return NotificationCompat.Action.Builder(
R.drawable.ic_notification,
notificationAction.title(context).uppercase(),
pendingIntent,
)
.build()
}
override fun createNotificationAction(notificationAction: NotificationAction, extraId: Int?): NotificationCompat.Action {
val pendingIntent = PendingIntent.getBroadcast(
context,
0,
Intent(context, NotificationActionReceiver::class.java).apply {
action = notificationAction.name
if (extraId != null) putExtra(EXTRA_ID, extraId)
},
PendingIntent.FLAG_IMMUTABLE,
)
return NotificationCompat.Action.Builder(
R.drawable.ic_launcher,
notificationAction.title(context).uppercase(),
pendingIntent,
).build()
}
override fun remove(notificationId: Int) {
notificationManager.cancel(notificationId)
}
override fun remove(notificationId: Int) {
notificationManager.cancel(notificationId)
}
override fun show(notificationId: Int, notification: Notification) {
with(notificationManager) {
if (
ActivityCompat.checkSelfPermission(
context,
Manifest.permission.POST_NOTIFICATIONS,
) != PackageManager.PERMISSION_GRANTED
) {
return
}
notify(notificationId, notification)
}
}
override fun show(notificationId: Int, notification: Notification) {
with(notificationManager) {
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
return
}
notify(notificationId, notification)
}
}
private fun NotificationChannels.asBuilder(): NotificationCompat.Builder {
return when (this) {
NotificationChannels.AUTO_TUNNEL -> {
NotificationCompat.Builder(
context,
context.getString(R.string.auto_tunnel_channel_id),
)
}
NotificationChannels.VPN -> {
NotificationCompat.Builder(context, context.getString(R.string.vpn_channel_id))
}
}
}
private fun NotificationChannels.asBuilder(): NotificationCompat.Builder {
return when (this) {
NotificationChannels.AUTO_TUNNEL -> {
NotificationCompat.Builder(
context,
context.getString(R.string.auto_tunnel_channel_id),
)
}
NotificationChannels.VPN -> {
NotificationCompat.Builder(
context,
context.getString(R.string.vpn_channel_id),
)
}
}
}
private fun NotificationChannels.asChannel(importance: Int): NotificationChannel {
return when (this) {
NotificationChannels.VPN -> {
NotificationChannel(
context.getString(R.string.vpn_channel_id),
context.getString(R.string.vpn_channel_name),
importance,
)
.apply { description = context.getString(R.string.vpn_channel_description) }
}
NotificationChannels.AUTO_TUNNEL -> {
NotificationChannel(
context.getString(R.string.auto_tunnel_channel_id),
context.getString(R.string.auto_tunnel_channel_name),
importance,
)
.apply {
description = context.getString(R.string.auto_tunnel_channel_description)
}
}
}
}
private fun NotificationChannels.asChannel(): NotificationChannel {
return when (this) {
NotificationChannels.VPN -> {
NotificationChannel(
context.getString(R.string.vpn_channel_id),
context.getString(R.string.vpn_channel_name),
NotificationManager.IMPORTANCE_HIGH,
).apply {
description = context.getString(R.string.vpn_channel_description)
enableLights(true)
lightColor = Color.WHITE
enableVibration(false)
vibrationPattern = longArrayOf(100, 200, 300)
}
}
NotificationChannels.AUTO_TUNNEL -> {
NotificationChannel(
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,167 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.service
import android.app.Notification
import android.content.Intent
import android.os.IBinder
import androidx.core.app.ServiceCompat
import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelMonitor
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.util.extensions.distinctByKeys
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.launch
import timber.log.Timber
@AndroidEntryPoint
abstract class BaseTunnelForegroundService : LifecycleService(), TunnelService {
@Inject lateinit var notificationManager: NotificationManager
@Inject lateinit var serviceManager: ServiceManager
@Inject lateinit var tunnelManager: TunnelManager
@Inject lateinit var tunnelMonitor: TunnelMonitor
@Inject @IoDispatcher lateinit var ioDispatcher: CoroutineDispatcher
@Inject lateinit var settingsRepository: GeneralSettingRepository
@Inject lateinit var tunnelsRepository: TunnelRepository
protected abstract val fgsType: Int
override fun onBind(intent: Intent): IBinder {
super.onBind(intent)
return LocalBinder(this)
}
override fun onCreate() {
super.onCreate()
ServiceCompat.startForeground(
this,
NotificationManager.VPN_NOTIFICATION_ID,
onCreateNotification(),
fgsType,
)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
ServiceCompat.startForeground(
this,
NotificationManager.VPN_NOTIFICATION_ID,
onCreateNotification(),
fgsType,
)
if (
intent == null ||
intent.component == null ||
(intent.component?.packageName != this.packageName)
) {
Timber.d("Service started by Always-on VPN feature")
lifecycleScope.launch {
val settings = settingsRepository.getGeneralSettings()
if (settings.isAlwaysOnVpnEnabled) {
val tunnel = tunnelsRepository.getDefaultTunnel()
tunnel?.let { tunnelManager.startTunnel(it) }
} else {
Timber.w("Always-on VPN is not enabled in app settings")
}
}
} else {
start()
}
return START_STICKY
}
override fun start() {
lifecycleScope.launch(ioDispatcher) {
tunnelManager.activeTunnels.distinctByKeys().collect { activeTunnels ->
val activeTunConfigs = activeTunnels.keys
val tunnels = tunnelsRepository.getAll()
val activeConfigs = tunnels.filter { activeTunConfigs.contains(it.id) }
updateServiceNotification(activeConfigs)
}
}
}
// TODO Would be cool to have this include kill switch
private fun updateServiceNotification(activeConfigs: List<TunnelConfig>) {
val notification =
when (activeConfigs.size) {
0 -> onCreateNotification()
1 -> createTunnelNotification(activeConfigs.first())
else -> createTunnelsNotification()
}
ServiceCompat.startForeground(
this,
NotificationManager.VPN_NOTIFICATION_ID,
notification,
fgsType,
)
}
override fun stop() {
Timber.d("Stop called")
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
stopSelf()
}
override fun onDestroy() {
serviceManager.handleTunnelServiceDestroy()
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
Timber.d("onDestroy")
super.onDestroy()
}
private fun createTunnelNotification(tunnelConfig: TunnelConfig): Notification {
return notificationManager.createNotification(
WireGuardNotification.NotificationChannels.VPN,
title = "${getString(R.string.tunnel_running)} - ${tunnelConfig.name}",
actions =
listOf(
notificationManager.createNotificationAction(
NotificationAction.TUNNEL_OFF,
tunnelConfig.id,
)
),
onGoing = true,
groupKey = NotificationManager.VPN_GROUP_KEY,
isGroupSummary = true,
)
}
private fun createTunnelsNotification(): Notification {
return notificationManager.createNotification(
WireGuardNotification.NotificationChannels.VPN,
title = "${getString(R.string.tunnel_running)} - ${getString(R.string.multiple)}",
actions =
listOf(
notificationManager.createNotificationAction(NotificationAction.TUNNEL_OFF, 0)
),
groupKey = NotificationManager.VPN_GROUP_KEY,
isGroupSummary = true,
)
}
private fun onCreateNotification(): Notification {
return notificationManager.createNotification(
WireGuardNotification.NotificationChannels.VPN,
title = getString(R.string.tunnel_starting),
groupKey = NotificationManager.VPN_GROUP_KEY,
isGroupSummary = true,
)
}
}
@@ -1,11 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.service
import android.os.Binder
import java.lang.ref.WeakReference
class LocalBinder(service: TunnelService) : Binder() {
private val serviceRef = WeakReference(service)
val service: TunnelService?
get() = serviceRef.get()
}
@@ -1,189 +1,142 @@
package com.zaneschepke.wireguardautotunnel.core.service
import android.content.ComponentName
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.net.VpnService
import android.os.IBinder
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelService
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
import jakarta.inject.Inject
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import timber.log.Timber
class ServiceManager
@Inject
constructor(
private val context: Context,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
@ApplicationScope applicationScope: CoroutineScope,
private val mainDispatcher: CoroutineDispatcher,
private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
class ServiceManager @Inject constructor(
private val context: Context,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
@ApplicationScope private val applicationScope: CoroutineScope,
private val appDataRepository: AppDataRepository,
) {
private val autoTunnelMutex = Mutex()
private val tunnelMutex = Mutex()
private val _autoTunnelActive = MutableStateFlow(false)
val autoTunnelActive = _autoTunnelActive.asStateFlow()
private val _tunnelService = MutableStateFlow<TunnelService?>(null)
private val _autoTunnelService = MutableStateFlow<AutoTunnelService?>(null)
val autoTunnelService = _autoTunnelService.asStateFlow()
val tunnelService = _tunnelService.asStateFlow()
var autoTunnelService = CompletableDeferred<AutoTunnelService>()
var backgroundService = CompletableDeferred<TunnelForegroundService>()
init {
applicationScope.launch(ioDispatcher) {
_autoTunnelService
.onEach { _ -> withContext(mainDispatcher) { updateAutoTunnelTile() } }
.launchIn(this)
}
applicationScope.launch(ioDispatcher) {
combine(
autoTunnelSettingsRepository.flow
.map { it.isAutoTunnelEnabled }
.distinctUntilChanged(),
_autoTunnelService,
) { enabled, service ->
enabled to (service != null)
}
.collect { (enabled, isRunning) ->
when {
enabled && !isRunning -> {
autoTunnelMutex.withLock { startServiceInternal() }
}
!enabled && isRunning -> {
autoTunnelMutex.withLock { stopServiceInternal() }
}
}
}
}
}
private fun <T : Service> startService(cls: Class<T>, background: Boolean) {
runCatching {
val intent = Intent(context, cls)
if (background) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
}.onFailure { Timber.e(it) }
}
private val tunnelServiceConnection =
object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
val binder = service as? LocalBinder
_tunnelService.update { binder?.service }
val serviceClass =
when {
name.className.contains("VpnForegroundService") -> "VpnForegroundService"
name.className.contains("TunnelForegroundService") ->
"TunnelForegroundService"
else -> "Unknown"
}
Timber.d("$serviceClass connected")
}
fun startAutoTunnel(background: Boolean) {
applicationScope.launch(ioDispatcher) {
val settings = appDataRepository.settings.get()
appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = true))
if (autoTunnelService.isCompleted) {
_autoTunnelActive.update { true }
return@launch
}
runCatching {
autoTunnelService = CompletableDeferred()
startService(AutoTunnelService::class.java, background)
_autoTunnelActive.update { true }
}.onFailure {
Timber.e(it)
_autoTunnelActive.update { false }
}
}
updateAutoTunnelTile()
}
override fun onServiceDisconnected(name: ComponentName) {
_tunnelService.update { null }
val serviceClass =
when {
name.className.contains("VpnForegroundService") -> "VpnForegroundService"
name.className.contains("TunnelForegroundService") ->
"TunnelForegroundService"
else -> "Unknown"
}
Timber.d("$serviceClass disconnected")
}
}
fun startTunnelForegroundService(tunnelConf: TunnelConf) {
applicationScope.launch(ioDispatcher) {
if (backgroundService.isCompleted) return@launch
runCatching {
backgroundService = CompletableDeferred()
startService(TunnelForegroundService::class.java, true)
val service = withTimeoutOrNull(SERVICE_START_TIMEOUT) { backgroundService.await() }
?: throw IllegalStateException("Background service start timed out")
service.start(tunnelConf)
}.onFailure {
Timber.e(it)
}
}
}
private val autoTunnelServiceConnection =
object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
val binder = service as? AutoTunnelService.LocalBinder
_autoTunnelService.update { binder?.service }
Timber.d("AutoTunnelService connected")
}
fun updateTunnelForegroundServiceNotification(tunnelConf: TunnelConf) {
applicationScope.launch(ioDispatcher) {
if (!backgroundService.isCompleted) return@launch
runCatching {
val service = backgroundService.await()
service.start(tunnelConf)
}.onFailure {
Timber.e(it)
}
}
}
override fun onServiceDisconnected(name: ComponentName) {
_autoTunnelService.update { null }
Timber.d("AutoTunnelService disconnected")
}
}
fun stopTunnelForegroundService() {
applicationScope.launch(ioDispatcher) {
if (!backgroundService.isCompleted) return@launch
runCatching {
val service = backgroundService.await()
service.stop()
backgroundService = CompletableDeferred()
}.onFailure {
Timber.e(it)
}
}
}
fun hasVpnPermission(): Boolean {
return VpnService.prepare(context) == null
}
fun toggleAutoTunnel(background: Boolean) {
applicationScope.launch(ioDispatcher) {
if (_autoTunnelActive.value) stopAutoTunnel() else startAutoTunnel(background)
}
}
private fun startServiceInternal() {
if (autoTunnelService.value == null) {
val intent = Intent(context, AutoTunnelService::class.java)
context.startForegroundService(intent)
context.bindService(intent, autoTunnelServiceConnection, Context.BIND_AUTO_CREATE)
}
}
fun updateAutoTunnelTile() {
context.requestAutoTunnelTileServiceUpdate()
}
suspend fun startAutoTunnelService() = autoTunnelMutex.withLock { startServiceInternal() }
fun updateTunnelTile() {
context.requestTunnelTileServiceStateUpdate()
}
private fun stopServiceInternal() {
_autoTunnelService.value?.stop()
try {
context.unbindService(autoTunnelServiceConnection)
} catch (e: Exception) {
Timber.e(e, "Failed to unbind AutoTunnelService")
}
_autoTunnelService.update { null }
}
fun stopAutoTunnel() {
applicationScope.launch(ioDispatcher) {
val settings = appDataRepository.settings.get()
appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = false))
if (!autoTunnelService.isCompleted) return@launch
runCatching {
val service = autoTunnelService.await()
service.stop()
_autoTunnelActive.update { false }
autoTunnelService = CompletableDeferred()
updateAutoTunnelTile()
}.onFailure {
Timber.e(it)
}
}
}
suspend fun startTunnelService(appMode: AppMode) =
tunnelMutex.withLock {
if (_tunnelService.value != null) {
Timber.d("Service already exists, waiting for disconnect")
withTimeoutOrNull(2000L) { _tunnelService.first { it == null } }
?: Timber.w("Timeout waiting for existing service to disconnect")
}
if (_tunnelService.value == null) {
val serviceClass =
when (appMode) {
AppMode.VPN,
AppMode.LOCK_DOWN -> VpnForegroundService::class.java
AppMode.KERNEL,
AppMode.PROXY -> TunnelForegroundService::class.java
}
val intent = Intent(context, serviceClass)
context.startForegroundService(intent)
context.bindService(intent, tunnelServiceConnection, Context.BIND_AUTO_CREATE)
} else {
Timber.e("Service still not null after timeout")
}
}
suspend fun stopTunnelService() =
tunnelMutex.withLock {
_tunnelService.value?.let { service ->
service.stop()
try {
context.unbindService(tunnelServiceConnection)
} catch (e: Exception) {
Timber.e(e, "Failed to unbind Tunnel Service")
}
}
}
fun updateAutoTunnelTile() {
context.requestAutoTunnelTileServiceUpdate()
}
fun updateTunnelTile() {
context.requestTunnelTileServiceStateUpdate()
}
fun handleTunnelServiceDestroy() {
_tunnelService.update { null }
}
fun handleAutoTunnelServiceDestroy() {
_autoTunnelService.update { null }
}
companion object {
const val SERVICE_START_TIMEOUT = 5_000L
}
}
@@ -1,8 +1,176 @@
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.networkmonitor.NetworkMonitor
import com.zaneschepke.networkmonitor.NetworkStatus
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.util.Constants
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class TunnelForegroundService(override val fgsType: Int = Constants.SPECIAL_USE_SERVICE_TYPE_ID) :
BaseTunnelForegroundService()
class TunnelForegroundService : LifecycleService() {
@Inject
lateinit var notificationManager: NotificationManager
@Inject
lateinit var serviceManager: ServiceManager
@Inject
lateinit var networkMonitor: NetworkMonitor
@Inject
@IoDispatcher
lateinit var ioDispatcher: CoroutineDispatcher
@Inject
lateinit var tunnelRepo: TunnelRepository
private val isNetworkConnected = MutableStateFlow(true)
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 {
serviceManager.backgroundService.complete(this)
return super.onStartCommand(intent, flags, startId)
}
fun start(tunnelConf: TunnelConf) {
Timber.d("Service starting with TunnelConf instance: ${tunnelConf.hashCode()}")
ServiceCompat.startForeground(
this@TunnelForegroundService,
NotificationManager.KERNEL_SERVICE_NOTIFICATION_ID,
createNotification(tunnelConf),
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
)
// monitor if we have internet connectivity
startNetworkMonitorJob()
// job to trigger stats emit on interval
startTunnelStatsJob(tunnelConf)
// monitor changes to the tunnel config
startTunnelConfChangesJob(tunnelConf)
startPingJob(tunnelConf)
}
private fun startTunnelConfChangesJob(tunnelConf: TunnelConf) = lifecycleScope.launch(ioDispatcher) {
tunnelRepo.flow
.flowOn(ioDispatcher)
.map { storedTunnels ->
storedTunnels.firstOrNull { it.id == tunnelConf.id }
}
.filterNotNull()
// only emit when one of these 3 values change
.distinctUntilChanged { old, new ->
old.tunName == new.tunName && old.wgQuick == new.wgQuick && old.amQuick == new.amQuick
}
.collect { storedTunnel ->
if (tunnelConf.isTunnelConfigChanged(storedTunnel)) {
Timber.d("Config changed for ${storedTunnel.tunName}, bouncing")
tunnelConf.bounceTunnel(storedTunnel)
}
}
}
private fun startNetworkMonitorJob() = lifecycleScope.launch(ioDispatcher) {
networkMonitor.networkStatusFlow
.flowOn(ioDispatcher)
.collectLatest { status ->
val isAvailable = status !is NetworkStatus.Disconnected
isNetworkConnected.value = isAvailable
Timber.d("Network available: $status")
}
}
private fun startTunnelStatsJob(tunnel: TunnelConf) = lifecycleScope.launch(ioDispatcher) {
while (isActive) {
tunnel.onUpdateStatistics()
delay(STATS_DELAY)
}
}
private fun startPingJob(tunnel: TunnelConf) = lifecycleScope.launch(ioDispatcher) {
delay(PING_START_DELAY)
while (isActive) {
val shouldBounce = shouldBounceTunnel(tunnel)
val delayMs = if (shouldBounce) {
tunnel.bounceTunnel(tunnel)
tunnel.pingCooldown ?: Constants.PING_COOLDOWN
} else {
tunnel.pingInterval ?: Constants.PING_INTERVAL
}
delay(delayMs)
}
}
private suspend fun shouldBounceTunnel(tunnel: TunnelConf): Boolean {
if (!isNetworkConnected.value) {
Timber.d("Network disconnected, skipping ping for ${tunnel.tunName}")
return false
}
return runCatching {
!tunnel.isTunnelPingable(ioDispatcher)
}.onFailure { e ->
Timber.e(e, "Ping check failed for ${tunnel.tunName}")
}.getOrDefault(true)
}
fun stop() {
stopSelf()
}
override fun onDestroy() {
serviceManager.backgroundService = CompletableDeferred()
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
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),
),
)
}
companion object {
const val STATS_DELAY = 1_000L
const val PING_START_DELAY = 30_000L
}
}
@@ -1,7 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.service
interface TunnelService {
fun start()
fun stop()
}
@@ -1,8 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.service
import com.zaneschepke.wireguardautotunnel.util.Constants
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class VpnForegroundService(override val fgsType: Int = Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID) :
BaseTunnelForegroundService()
@@ -1,376 +1,255 @@
package com.zaneschepke.wireguardautotunnel.core.service.autotunnel
import android.content.Intent
import android.os.Binder
import android.os.IBinder
import android.os.PowerManager
import androidx.core.app.ServiceCompat
import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
import com.zaneschepke.networkmonitor.ConnectivityState
import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.networkmonitor.NetworkStatus
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.di.MainImmediateDispatcher
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
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.model.AutoTunnelSettings
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.events.KillSwitchEvent
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.state.AutoTunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.toDomain
import com.zaneschepke.wireguardautotunnel.domain.state.NetworkState
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.to
import com.zaneschepke.wireguardautotunnel.util.extensions.toMillis
import com.zaneschepke.wireguardautotunnel.util.extensions.Tunnels
import dagger.hilt.android.AndroidEntryPoint
import java.lang.ref.WeakReference
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Provider
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import timber.log.Timber
@AndroidEntryPoint
class AutoTunnelService : LifecycleService() {
@Inject lateinit var networkMonitor: NetworkMonitor
@Inject
lateinit var networkMonitor: NetworkMonitor
@Inject lateinit var notificationManager: NotificationManager
@Inject
lateinit var appDataRepository: Provider<AppDataRepository>
@Inject @IoDispatcher lateinit var ioDispatcher: CoroutineDispatcher
@Inject
lateinit var notificationManager: NotificationManager
@Inject lateinit var serviceManager: ServiceManager
@Inject
@IoDispatcher
lateinit var ioDispatcher: CoroutineDispatcher
@Inject lateinit var tunnelManager: TunnelManager
@Inject
lateinit var serviceManager: ServiceManager
@Inject lateinit var autoTunnelRepository: Provider<AutoTunnelSettingsRepository>
@Inject lateinit var settingsRepository: GeneralSettingRepository
@Inject lateinit var tunnelsRepository: TunnelRepository
@Inject
@MainImmediateDispatcher
lateinit var mainImmediateDispatcher: CoroutineDispatcher
private val defaultState = AutoTunnelState()
@Inject
lateinit var tunnelManager: TunnelManager
private val autoTunMutex = Mutex()
private val defaultState = AutoTunnelState()
private val autoTunnelStateFlow = MutableStateFlow(defaultState)
private val autoTunnelStateFlow = MutableStateFlow(defaultState)
private var autoTunnelJob: Job? = null
private var permissionsJob: Job? = null
private var wakeLock: PowerManager.WakeLock? = null
class LocalBinder(service: AutoTunnelService) : Binder() {
private val serviceRef = WeakReference(service)
override fun onCreate() {
super.onCreate()
serviceManager.autoTunnelService.complete(this)
lifecycleScope.launch(mainImmediateDispatcher) {
runCatching {
launchWatcherNotification()
}.onFailure {
Timber.e(it)
}
}
}
val service: AutoTunnelService?
get() = serviceRef.get()
}
override fun onBind(intent: Intent): IBinder? {
super.onBind(intent)
return null
}
private val binder = LocalBinder(this)
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
Timber.d("onStartCommand executed with startId: $startId")
serviceManager.autoTunnelService.complete(this)
start()
return START_STICKY
}
override fun onCreate() {
super.onCreate()
launchWatcherNotification()
}
fun start() {
kotlin.runCatching {
lifecycleScope.launch(mainImmediateDispatcher) {
launchWatcherNotification()
initWakeLock()
}
startAutoTunnelJob()
startAutoTunnelStateJob()
startKillSwitchJob()
}.onFailure {
Timber.e(it)
}
}
override fun onBind(intent: Intent): IBinder {
super.onBind(intent)
return binder
}
fun stop() {
wakeLock?.let { if (it.isHeld) it.release() }
stopSelf()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
Timber.d("onStartCommand executed with startId: $startId")
start()
return START_STICKY
}
override fun onDestroy() {
serviceManager.autoTunnelService = CompletableDeferred()
super.onDestroy()
}
fun start() {
launchWatcherNotification()
autoTunnelJob?.cancel()
autoTunnelJob = startAutoTunnelStateJob()
permissionsJob?.cancel()
permissionsJob = startLocationPermissionsNotificationJob()
}
private fun launchWatcherNotification(description: String = getString(R.string.monitoring_state_changes)) {
val notification =
notificationManager.createNotification(
WireGuardNotification.NotificationChannels.AUTO_TUNNEL,
title = getString(R.string.auto_tunnel_title),
description = description,
actions = listOf(
notificationManager.createNotificationAction(NotificationAction.AUTO_TUNNEL_OFF),
),
)
ServiceCompat.startForeground(
this,
NotificationManager.AUTO_TUNNEL_NOTIFICATION_ID,
notification,
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
)
}
fun stop() {
stopSelf()
}
private fun initWakeLock() {
wakeLock = (getSystemService(POWER_SERVICE) as PowerManager).run {
val tag = this.javaClass.name
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()
}
}
}
}
override fun onDestroy() {
serviceManager.handleAutoTunnelServiceDestroy()
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
super.onDestroy()
}
private fun buildNetworkState(networkStatus: NetworkStatus): NetworkState {
return with(autoTunnelStateFlow.value.networkState) {
val wifiName = when (networkStatus) {
is NetworkStatus.Connected -> {
networkStatus.wifiSsid
}
else -> null
}
copy(
isWifiConnected = networkStatus.wifiConnected,
isMobileDataConnected = networkStatus.cellularConnected,
isEthernetConnected = networkStatus.ethernetConnected,
wifiName = wifiName,
)
}
}
private fun launchWatcherNotification(
description: String = getString(R.string.monitoring_state_changes)
) {
val notification =
notificationManager.createNotification(
WireGuardNotification.NotificationChannels.AUTO_TUNNEL,
title = getString(R.string.auto_tunnel_title),
description = description,
actions =
listOf(
notificationManager.createNotificationAction(
NotificationAction.AUTO_TUNNEL_OFF
)
),
onGoing = true,
groupKey = NotificationManager.AUTO_TUNNEL_GROUP_KEY,
isGroupSummary = true,
)
ServiceCompat.startForeground(
this,
NotificationManager.AUTO_TUNNEL_NOTIFICATION_ID,
notification,
Constants.SPECIAL_USE_SERVICE_TYPE_ID,
)
}
@OptIn(ExperimentalCoroutinesApi::class)
private fun startAutoTunnelStateJob() = lifecycleScope.launch(ioDispatcher) {
combine(
combineSettings(),
appDataRepository.get().settings.flow
.distinctUntilChanged { old, new -> old.isKernelEnabled == new.isKernelEnabled } // Only emit when isKernelEnabled changes
.flatMapLatest {
networkMonitor.networkStatusFlow
.flowOn(ioDispatcher)
.map { buildNetworkState(it) }
}
.distinctUntilChanged(),
) { double, networkState ->
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,
)
}
}
}
private fun startAutoTunnelStateJob(): Job =
lifecycleScope.launch(ioDispatcher) {
val networkFlow =
debouncedConnectivityStateFlow
.flowOn(ioDispatcher)
.map { it.toDomain() }
.map(::NetworkChange)
.distinctUntilChanged()
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()
}
val settingsFlow =
combineSettings().map { (appMode, settings, tunnels) ->
SettingsChange(appMode, settings, tunnels)
}
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())
}
}
}
}
val tunnelsFlow = tunnelManager.activeTunnels.map(::ActiveTunnelsChange)
var reevaluationJob: Job? = null
// get everything in sync before we use merge
combine(networkFlow, settingsFlow, tunnelsFlow) { network, settings, tunnels ->
autoTunnelStateFlow.update {
it.copy(
activeTunnels = tunnels.activeTunnels,
networkState = network.networkState,
settings = settings.settings,
tunnels = settings.tunnels,
)
}
}
.first()
val initialState = autoTunnelStateFlow.value
if (initialState != defaultState) {
handleAutoTunnelEvent(
initialState.determineAutoTunnelEvent(NetworkChange(initialState.networkState))
)
}
// use merge to limit the noise of a combine and also increase the scalability of auto
// tunnel handling new states
merge(networkFlow, settingsFlow, tunnelsFlow).collect { change ->
if (change !is ActiveTunnelsChange) {
Timber.d("New state changed to ${change.javaClass.simpleName}")
}
val previousState = autoTunnelStateFlow.value
when (change) {
is NetworkChange -> {
Timber.d("Network change: ${change.networkState}")
reevaluationJob?.cancel()
autoTunnelStateFlow.update { it.copy(networkState = change.networkState) }
if (previousState.networkState == change.networkState) {
Timber.d("Duplicate network state change detected, ignoring")
return@collect
}
}
is SettingsChange -> {
reevaluationJob?.cancel()
autoTunnelStateFlow.update {
it.copy(settings = change.settings, tunnels = change.tunnels)
}
if (
previousState.settings == change.settings &&
previousState.tunnels == change.tunnels
) {
Timber.d("Duplicate settings change detected, ignoring")
return@collect
}
}
is ActiveTunnelsChange -> {
autoTunnelStateFlow.update { it.copy(activeTunnels = change.activeTunnels) }
return@collect
}
}
handleAutoTunnelEvent(autoTunnelStateFlow.value.determineAutoTunnelEvent(change))
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.get().flow,
tunnelsRepository.flow.map { tunnels ->
// isActive is ignored for equality checks so user can manually toggle off
// tunnel with auto-tunnel
tunnels.map { it.copy(isActive = false) }
},
) { 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)
}
is AutoTunnelEvent.Stop -> tunnelManager.stopActiveTunnels()
AutoTunnelEvent.DoNothing -> Timber.i("Auto-tunneling: nothing to do")
}
}
}
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
private val debouncedConnectivityStateFlow: Flow<ConnectivityState> by lazy {
autoTunnelRepository
.get()
.flow
.map { it.debounceDelaySeconds.toMillis() }
.distinctUntilChanged()
.flatMapLatest { debounceMillis ->
networkMonitor.connectivityStateFlow.debounce(debounceMillis)
}
}
companion object {
const val REEVALUATE_CHECK_DELAY = 3_000L
}
@OptIn(FlowPreview::class)
private fun startAutoTunnelJob() = lifecycleScope.launch(ioDispatcher) {
Timber.i("Starting auto-tunnel network event watcher")
val settings = appDataRepository.get().settings.get()
Timber.d("Starting with debounce delay of: ${settings.debounceDelaySeconds} seconds")
autoTunnelStateFlow.debounce(settings.debounceDelayMillis()).collect { watcherState ->
if (watcherState == defaultState) return@collect
Timber.d("New auto tunnel state emitted ${watcherState.networkState}")
when (val event = watcherState.asAutoTunnelEvent()) {
is AutoTunnelEvent.Start -> (event.tunnelConf ?: appDataRepository.get().getPrimaryOrFirstTunnel())?.let {
tunnelManager.startTunnel(it)
}
// TODO improve this to target specific tunnels to better support multi-tunnel
is AutoTunnelEvent.Stop -> tunnelManager.stopTunnel()
AutoTunnelEvent.DoNothing -> Timber.i("Auto-tunneling: no condition met")
}
}
}
}
@@ -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
@@ -8,100 +8,100 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.lifecycleScope
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class AutoTunnelControlTile : TileService(), LifecycleOwner {
@Inject
lateinit var appDataRepository: AppDataRepository
@Inject lateinit var autoTunnelSettingsRepository: AutoTunnelSettingsRepository
@Inject lateinit var tunnelsRepository: TunnelRepository
@Inject
lateinit var serviceManager: ServiceManager
@Inject lateinit var serviceManager: ServiceManager
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
override fun onCreate() {
super.onCreate()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
}
override fun onCreate() {
super.onCreate()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
}
override fun onDestroy() {
super.onDestroy()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
}
override fun onDestroy() {
super.onDestroy()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
}
override fun onStartListening() {
super.onStartListening()
Timber.d("Start listening called for auto tunnel tile")
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
lifecycleScope.launch {
serviceManager.autoTunnelActive.collect {
if (it) setActive() else setInactive()
}
}
lifecycleScope.launch {
appDataRepository.tunnels.flow.collect {
if (it.isEmpty()) {
setUnavailable()
} else {
if (qsTile.state == Tile.STATE_ACTIVE) setInactive()
}
}
}
}
override fun onStartListening() {
super.onStartListening()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
Timber.d("Start listening called for auto tunnel tile")
lifecycleScope.launch {
serviceManager.autoTunnelService.collect {
if (it != null) return@collect setActive()
setInactive()
}
}
lifecycleScope.launch {
tunnelsRepository.flow.collect {
if (it.isEmpty()) {
setUnavailable()
}
}
}
}
override fun onClick() {
super.onClick()
unlockAndRun {
lifecycleScope.launch {
if (serviceManager.autoTunnelActive.value) {
serviceManager.stopAutoTunnel()
setInactive()
} else {
serviceManager.startAutoTunnel(true)
setActive()
}
}
}
}
override fun onClick() {
super.onClick()
unlockAndRun {
lifecycleScope.launch {
if (serviceManager.autoTunnelService.value != null) {
autoTunnelSettingsRepository.updateAutoTunnelEnabled(false)
setInactive()
} else {
autoTunnelSettingsRepository.updateAutoTunnelEnabled(true)
setActive()
}
}
}
}
private fun setActive() {
runCatching {
qsTile.state = Tile.STATE_ACTIVE
qsTile.updateTile()
}
}
private fun setActive() {
runCatching {
qsTile.state = Tile.STATE_ACTIVE
qsTile.updateTile()
}
}
private fun setInactive() {
runCatching {
qsTile.state = Tile.STATE_INACTIVE
qsTile.updateTile()
}
}
private fun setInactive() {
runCatching {
qsTile.state = Tile.STATE_INACTIVE
qsTile.updateTile()
}
}
/* This works around an annoying unsolved frameworks bug some people are hitting. */
override fun onBind(intent: Intent): IBinder? {
var ret: IBinder? = null
try {
ret = super.onBind(intent)
} catch (_: Throwable) {
Timber.e("Failed to bind to TunnelControlTile")
}
return ret
}
private fun setUnavailable() {
runCatching {
qsTile.state = Tile.STATE_UNAVAILABLE
qsTile.updateTile()
}
}
private fun setUnavailable() {
runCatching {
qsTile.state = Tile.STATE_UNAVAILABLE
qsTile.updateTile()
}
}
/* This works around an annoying unsolved frameworks bug some people are hitting. */
override fun onBind(intent: Intent): IBinder? {
var ret: IBinder? = null
try {
ret = super.onBind(intent)
} catch (_: Throwable) {
Timber.e("Failed to bind to AutoTunnelControlTile")
}
return ret
}
override val lifecycle: Lifecycle
get() = lifecycleRegistry
override val lifecycle: Lifecycle
get() = lifecycleRegistry
}
@@ -10,175 +10,133 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.lifecycleScope
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class TunnelControlTile : TileService(), LifecycleOwner {
@Inject
lateinit var appDataRepository: AppDataRepository
@Inject lateinit var tunnelsRepository: TunnelRepository
@Inject
lateinit var serviceManager: ServiceManager
@Inject lateinit var serviceManager: ServiceManager
@Inject
lateinit var tunnelManager: TunnelManager
@Inject lateinit var tunnelManager: TunnelManager
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
private var isCollecting = false
private var isCollecting = false
override fun onCreate() {
super.onCreate()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
}
override fun onCreate() {
super.onCreate()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
}
override fun onDestroy() {
super.onDestroy()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
}
override fun onDestroy() {
super.onDestroy()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
}
override fun onStartListening() {
super.onStartListening()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
Timber.d("Start listening called for tunnel tile")
if (isCollecting) return
isCollecting = true
lifecycleScope.launch {
tunnelManager.activeTunnels.collect {
updateTileState()
}
}
}
override fun onStartListening() {
super.onStartListening()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
Timber.d("Start listening called for tunnel tile")
if (isCollecting) return
isCollecting = true
lifecycleScope.launch { tunnelManager.activeTunnels.collect { updateTileState() } }
}
private fun updateTileState() = lifecycleScope.launch {
val tunnels = appDataRepository.tunnels.getAll()
if (tunnels.isEmpty()) return@launch setUnavailable()
with(tunnelManager.activeTunnels.value) {
if (isNotEmpty()) if (size == 1) {
tunnels.firstOrNull { it.id == keys.first().id }?.let { return@launch updateTile(it.tunName, true) }
} else {
return@launch updateTile(getString(R.string.multiple), true)
}
}
appDataRepository.getStartTunnelConfig()?.let {
updateTile(it.tunName, false)
}
}
private suspend fun updateTileState() {
try {
val tunnels = tunnelsRepository.getAll()
if (tunnels.isEmpty()) {
setUnavailable()
return
}
override fun onClick() {
super.onClick()
unlockAndRun {
lifecycleScope.launch {
if (tunnelManager.activeTunnels.value.isNotEmpty()) return@launch tunnelManager.stopTunnel()
appDataRepository.getStartTunnelConfig()?.let {
tunnelManager.startTunnel(it)
}
}
}
}
val activeTunnels =
tunnelManager.activeTunnels.value.filter { it.value.status.isUpOrStarting() }
private fun setActive() {
runCatching {
qsTile.state = Tile.STATE_ACTIVE
qsTile.updateTile()
}
}
when {
activeTunnels.isNotEmpty() -> {
val activeIds = activeTunnels.map { it.key }
// TODO improvements would be needed to make this work well with toggling
// multiple tunnels
// this would be better managed elsewhere
WireGuardAutoTunnel.setLastActiveTunnels(activeIds)
val activeTunNames =
tunnels.filter { activeTunnels.keys.contains(it.id) }.map { it.name }
updateTileForActiveTunnels(activeTunNames)
}
else -> updateTileForLastActiveTunnels()
}
} catch (e: Exception) {
setUnavailable()
}
}
private fun setInactive() {
runCatching {
qsTile.state = Tile.STATE_INACTIVE
qsTile.updateTile()
}
}
private fun updateTileForActiveTunnels(activeTunnelNames: List<String>) {
val tileName =
when (activeTunnelNames.size) {
1 -> activeTunnelNames[0]
else -> getString(R.string.multiple)
}
updateTile(tileName, true)
}
private fun setUnavailable() {
runCatching {
qsTile.state = Tile.STATE_UNAVAILABLE
setTileDescription("")
qsTile.updateTile()
}
}
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()
}
}
}
private fun setTileDescription(description: String) {
runCatching {
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()
}
}
override fun onClick() {
super.onClick()
unlockAndRun {
lifecycleScope.launch {
if (tunnelManager.activeTunnels.value.isNotEmpty())
return@launch tunnelManager.stopActiveTunnels()
val lastActive = WireGuardAutoTunnel.getLastActiveTunnels()
if (lastActive.isEmpty()) {
tunnelsRepository.getStartTunnel()?.let { tunnelManager.startTunnel(it) }
} else {
lastActive.forEach { id ->
tunnelsRepository.getById(id)?.let { tunnelManager.startTunnel(it) }
}
}
}
}
}
/* 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
}
private fun setActive() {
runCatching {
qsTile.state = Tile.STATE_ACTIVE
qsTile.updateTile()
}
}
private fun setInactive() {
runCatching {
qsTile.state = Tile.STATE_INACTIVE
qsTile.updateTile()
}
}
private fun setUnavailable() {
runCatching {
qsTile.state = Tile.STATE_UNAVAILABLE
setTileDescription("")
qsTile.updateTile()
}
}
private fun setTileDescription(description: String) {
runCatching {
if (qsTile == null) return@runCatching
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
qsTile.subtitle = description
qsTile.stateDescription = description
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
qsTile.subtitle = description
}
qsTile.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
private fun updateTile(name: String, active: Boolean) {
runCatching {
setTileDescription(name)
if (active) return setActive()
setInactive()
}.onFailure {
Timber.e(it)
}
}
override val lifecycle: Lifecycle
get() = lifecycleRegistry
}
@@ -10,83 +10,70 @@ import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
class DynamicShortcutManager(
private val context: Context,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : ShortcutManager {
override suspend fun addShortcuts() {
withContext(ioDispatcher) {
ShortcutManagerCompat.setDynamicShortcuts(context, createShortcuts())
}
}
class DynamicShortcutManager(private val context: Context, @IoDispatcher private val ioDispatcher: CoroutineDispatcher) : ShortcutManager {
override suspend fun addShortcuts() {
withContext(ioDispatcher) {
ShortcutManagerCompat.setDynamicShortcuts(context, createShortcuts())
}
}
override suspend fun removeShortcuts() {
withContext(ioDispatcher) {
ShortcutManagerCompat.removeDynamicShortcuts(context, createShortcuts().map { it.id })
}
}
override suspend fun removeShortcuts() {
withContext(ioDispatcher) {
ShortcutManagerCompat.removeDynamicShortcuts(context, createShortcuts().map { it.id })
}
}
private fun createShortcuts(): List<ShortcutInfoCompat> {
return listOf(
buildShortcut(
context.getString(R.string.vpn_off),
context.getString(R.string.vpn_off),
context.getString(R.string.vpn_off),
intent =
Intent(context, ShortcutsActivity::class.java).apply {
putExtra("className", "WireGuardTunnelService")
action = ShortcutsActivity.Action.STOP.name
},
shortcutIcon = R.drawable.vpn_off,
),
buildShortcut(
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 {
putExtra("className", "WireGuardTunnelService")
action = ShortcutsActivity.Action.START.name
},
shortcutIcon = R.drawable.vpn_on,
),
buildShortcut(
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 {
putExtra("className", "WireGuardConnectivityWatcherService")
action = ShortcutsActivity.Action.START.name
},
shortcutIcon = R.drawable.auto_play,
),
buildShortcut(
context.getString(R.string.stop_auto),
context.getString(R.string.stop_auto),
context.getString(R.string.stop_auto),
intent =
Intent(context, ShortcutsActivity::class.java).apply {
putExtra("className", "WireGuardConnectivityWatcherService")
action = ShortcutsActivity.Action.STOP.name
},
shortcutIcon = R.drawable.auto_pause,
),
)
}
private fun createShortcuts(): List<ShortcutInfoCompat> {
return listOf(
buildShortcut(
context.getString(R.string.vpn_off),
context.getString(R.string.vpn_off),
context.getString(R.string.vpn_off),
intent = Intent(context, ShortcutsActivity::class.java).apply {
putExtra("className", "WireGuardTunnelService")
action = ShortcutsActivity.Action.STOP.name
},
shortcutIcon = R.drawable.vpn_off,
),
buildShortcut(
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 {
putExtra("className", "WireGuardTunnelService")
action = ShortcutsActivity.Action.START.name
},
shortcutIcon = R.drawable.vpn_on,
),
buildShortcut(
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 {
putExtra("className", "WireGuardConnectivityWatcherService")
action = ShortcutsActivity.Action.START.name
},
shortcutIcon = R.drawable.auto_play,
),
buildShortcut(
context.getString(R.string.stop_auto),
context.getString(R.string.stop_auto),
context.getString(R.string.stop_auto),
intent = Intent(context, ShortcutsActivity::class.java).apply {
putExtra("className", "WireGuardConnectivityWatcherService")
action = ShortcutsActivity.Action.STOP.name
},
shortcutIcon = R.drawable.auto_pause,
),
)
}
private fun buildShortcut(
id: String,
shortLabel: String,
longLabel: String,
intent: Intent,
shortcutIcon: Int,
): ShortcutInfoCompat {
return ShortcutInfoCompat.Builder(context, id)
.setShortLabel(shortLabel)
.setLongLabel(longLabel)
.setIntent(intent)
.setIcon(IconCompat.createWithResource(context, shortcutIcon))
.build()
}
private fun buildShortcut(id: String, shortLabel: String, longLabel: String, intent: Intent, shortcutIcon: Int): ShortcutInfoCompat {
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
interface ShortcutManager {
suspend fun addShortcuts()
suspend fun removeShortcuts()
suspend fun addShortcuts()
suspend fun removeShortcuts()
}
@@ -2,76 +2,76 @@ package com.zaneschepke.wireguardautotunnel.core.shortcut
import android.os.Bundle
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.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class ShortcutsActivity : ComponentActivity() {
@Inject
lateinit var appDataRepository: AppDataRepository
@Inject lateinit var settingsRepository: GeneralSettingRepository
@Inject lateinit var autoTunnelSettingsRepository: AutoTunnelSettingsRepository
@Inject lateinit var tunnelsRepository: TunnelRepository
@Inject
lateinit var serviceManager: ServiceManager
@Inject lateinit var tunnelManager: TunnelManager
@Inject
lateinit var tunnelManager: TunnelManager
@Inject @ApplicationScope lateinit var applicationScope: CoroutineScope
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
applicationScope.launch {
val settings = settingsRepository.getGeneralSettings()
if (settings.isShortcutsEnabled) {
when (intent.getStringExtra(CLASS_NAME_EXTRA_KEY)) {
LEGACY_TUNNEL_SERVICE_NAME,
TunnelProvider::class.java.simpleName -> {
val tunnelName = intent.getStringExtra(TUNNEL_NAME_EXTRA_KEY)
Timber.d("Tunnel name extra: $tunnelName")
val tunnelConfig =
tunnelName?.let { tunnelsRepository.findByTunnelName(it) }
?: tunnelsRepository.getDefaultTunnel()
Timber.d("Shortcut action on name: ${tunnelConfig?.name}")
tunnelConfig?.let {
when (intent.action) {
Action.START.name -> tunnelManager.startTunnel(it)
Action.STOP.name -> tunnelManager.stopActiveTunnels()
else -> Unit
}
}
}
AutoTunnelService::class.java.simpleName,
LEGACY_AUTO_TUNNEL_SERVICE_NAME -> {
when (intent.action) {
Action.START.name ->
autoTunnelSettingsRepository.updateAutoTunnelEnabled(true)
Action.STOP.name ->
autoTunnelSettingsRepository.updateAutoTunnelEnabled(false)
}
}
}
}
}
finish()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
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 -> tunnelManager.startTunnel(it)
Action.STOP.name -> tunnelManager.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()
}
enum class Action {
START,
STOP,
}
enum class Action {
START,
STOP,
}
companion object {
const val LEGACY_TUNNEL_SERVICE_NAME = "WireGuardTunnelService"
const val LEGACY_AUTO_TUNNEL_SERVICE_NAME = "WireGuardConnectivityWatcherService"
const val TUNNEL_NAME_EXTRA_KEY = "tunnelName"
const val CLASS_NAME_EXTRA_KEY = "className"
}
companion object {
const val LEGACY_TUNNEL_SERVICE_NAME = "WireGuardTunnelService"
const val LEGACY_AUTO_TUNNEL_SERVICE_NAME = "WireGuardConnectivityWatcherService"
const val TUNNEL_NAME_EXTRA_KEY = "tunnelName"
const val CLASS_NAME_EXTRA_KEY = "className"
}
}
@@ -1,155 +1,213 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.wireguard.android.backend.BackendException
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
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.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendError
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException
import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.state.LogHealthState
import com.zaneschepke.wireguardautotunnel.domain.state.PingState
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import java.util.concurrent.ConcurrentHashMap
import kotlin.coroutines.cancellation.CancellationException
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.util.StringValue
import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendError
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean
abstract class BaseTunnel(
@ApplicationScope protected val applicationScope: CoroutineScope,
@IoDispatcher protected val ioDispatcher: CoroutineDispatcher,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
@ApplicationScope private val applicationScope: CoroutineScope,
private val appDataRepository: AppDataRepository,
private val serviceManager: ServiceManager,
private val notificationManager: NotificationManager,
) : TunnelProvider {
protected val errors = MutableSharedFlow<Pair<String, BackendCoreException>>()
override val errorEvents = errors.asSharedFlow()
private val activeTuns = MutableStateFlow<Map<TunnelConf, TunnelState>>(emptyMap())
override val activeTunnels = activeTuns.asStateFlow()
private val _messageEvents = MutableSharedFlow<Pair<String, BackendMessage>>()
override val messageEvents = _messageEvents.asSharedFlow()
private val isBounce = AtomicBoolean(false)
protected val activeTuns = MutableStateFlow<Map<Int, TunnelState>>(emptyMap())
override val activeTunnels = activeTuns.asStateFlow()
private val mutex = Mutex()
protected val tunJobs = ConcurrentHashMap<Int, Job>()
private val tunMutex = Mutex()
private val tunStatusMutex = Mutex()
abstract suspend fun startBackend(tunnel: TunnelConf)
abstract fun tunnelStateFlow(tunnelConfig: TunnelConfig): Flow<TunnelStatus>
abstract fun stopBackend(tunnel: TunnelConf)
abstract override fun setBackendMode(backendMode: BackendMode)
private fun findActiveTunnel(id: Int): TunnelConf? = activeTuns.value.keys.find { it.id == id }
abstract override fun getBackendMode(): BackendMode
private fun isTunnelActive(id: Int) = (activeTuns.value.any { it.key.id == id })
abstract override suspend fun forceStopTunnel(tunnelId: Int)
private fun handleBackendThrowable(throwable: Throwable) {
val backendError = when (throwable) {
is BackendException -> throwable.toBackendError()
is org.amnezia.awg.backend.BackendException -> throwable.toBackendError()
else -> BackendError.Unknown
}
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)
BackendError.Unknown -> StringValue.StringResource(R.string.unknown_error)
}
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,
),
)
}
}
abstract override fun handleDnsReresolve(tunnelConfig: TunnelConfig): Boolean
private fun updateTunnelState(tunnelConf: TunnelConf, state: TunnelStatus? = null, stats: TunnelStatistics? = null) {
applicationScope.launch(ioDispatcher) {
mutex.withLock {
activeTuns.update { current ->
val originalConf = current.getKeyById(tunnelConf.id) ?: tunnelConf
val existingState = current.getValueById(tunnelConf.id) ?: TunnelState()
val newState = state ?: existingState.state
if (newState == TunnelStatus.DOWN) {
Timber.d("Removing tunnel ${tunnelConf.id} from activeTunnels as state is DOWN")
current - originalConf
} else if (existingState.state == newState && stats == null) {
Timber.d("Skipping redundant state update for ${tunnelConf.id}: $newState")
current
} else {
val updated = existingState.copy(
state = newState,
statistics = stats ?: existingState.statistics,
)
current + (originalConf to updated)
}
}
}
}
}
abstract override fun getStatistics(tunnelId: Int): TunnelStatistics?
protected suspend fun stopActiveTunnels() {
activeTunnels.value.forEach { (config, state) ->
if (state.state.isUp()) {
stopTunnel(config)
delay(300)
}
}
}
override suspend fun updateTunnelStatus(
tunnelId: Int,
status: TunnelStatus?,
stats: TunnelStatistics?,
pingStates: Map<String, PingState>?,
logHealthState: LogHealthState?,
) {
tunStatusMutex.withLock {
activeTuns.update { currentTuns ->
if (!currentTuns.containsKey(tunnelId) && status != TunnelStatus.Starting) {
Timber.d("Ignoring update for inactive tunnel $tunnelId")
return@update currentTuns
}
val existingState = currentTuns[tunnelId] ?: TunnelState()
val newStatus = status ?: existingState.status
if (newStatus == TunnelStatus.Down) {
Timber.d("Removing tunnel $tunnelId from activeTunnels as state is DOWN")
cleanUpTunJob(tunnelId)
currentTuns - tunnelId
} else if (
existingState.status == newStatus &&
stats == null &&
pingStates == null &&
logHealthState == null
) {
Timber.d("Skipping redundant state update for ${tunnelId}: $newStatus")
currentTuns
} else {
val updated =
existingState.copy(
status = newStatus,
statistics = stats ?: existingState.statistics,
pingStates = pingStates ?: existingState.pingStates,
logHealthState = logHealthState ?: existingState.logHealthState,
)
currentTuns + (tunnelId to updated)
}
}
}
}
private fun configureTunnelCallbacks(tunnelConf: TunnelConf) {
Timber.d("Configuring TunnelConf instance: ${tunnelConf.hashCode()}")
override suspend fun stopActiveTunnels() {
activeTunnels.value.forEach { (config, state) ->
if (state.status.isUpOrStarting()) {
stopTunnel(config)
}
}
}
tunnelConf.setStateChangeCallback { state ->
Timber.d("State change callback triggered for tunnel ${tunnelConf.id}: ${tunnelConf.tunName} with state $state at ${System.currentTimeMillis()}")
when (state) {
is Tunnel.State -> updateTunnelState(tunnelConf, state.asTunnelState())
is org.amnezia.awg.backend.Tunnel.State -> updateTunnelState(tunnelConf, state.asTunnelState())
}
serviceManager.updateTunnelTile()
}
tunnelConf.setTunnelStatsCallback {
val stats = getStatistics(tunnelConf)
updateTunnelState(tunnelConf, null, stats)
}
tunnelConf.setBounceTunnelCallback(::bounceTunnel)
}
override suspend fun startTunnel(tunnelConfig: TunnelConfig) {
tunMutex.withLock {
if (
activeTuns.value.containsKey(tunnelConfig.id) ||
tunJobs.containsKey(tunnelConfig.id)
) {
return Timber.w("Tunnel is already running: ${tunnelConfig.name}")
}
val job =
applicationScope.launch(ioDispatcher) {
try {
tunnelStateFlow(tunnelConfig).collect { status ->
updateTunnelStatus(tunnelConfig.id, status)
}
} catch (e: BackendCoreException) {
errors.emit(tunnelConfig.name to e)
updateTunnelStatus(tunnelConfig.id, TunnelStatus.Down)
} catch (_: CancellationException) {}
}
tunJobs[tunnelConfig.id] = job
job.invokeOnCompletion {
tunJobs.remove(tunnelConfig.id)
activeTuns.update { it - tunnelConfig.id }
}
}
}
override fun startTunnel(tunnelConf: TunnelConf) {
applicationScope.launch(ioDispatcher) {
runCatching {
if (isTunnelActive(tunnelConf.id)) return@launch
startTunnelInner(tunnelConf)
}.onFailure { exception ->
Timber.e(exception, "Failed to start tunnel ${tunnelConf.id} userspace")
stopTunnel(tunnelConf)
handleBackendThrowable(exception)
}.onSuccess {
Timber.i("Tunnel ${tunnelConf.id} started successfully")
}
}
}
override suspend fun stopTunnel(tunnelId: Int) {
tunMutex.withLock {
val currentState = activeTuns.value[tunnelId]?.status ?: return@withLock
updateTunnelStatus(tunnelId, TunnelStatus.Stopping)
tunJobs[tunnelId]?.cancel()
private suspend fun startTunnelInner(tunnelConf: TunnelConf) {
mutex.withLock {
configureTunnelCallbacks(tunnelConf)
startBackend(tunnelConf)
saveTunnelActiveState(tunnelConf, true)
if (!isBounce.get()) serviceManager.startTunnelForegroundService(tunnelConf)
}
}
withTimeoutOrNull(STOP_TIMEOUT_MS) {
activeTunnels.first {
!it.containsKey(tunnelId) || it[tunnelId]!!.status == TunnelStatus.Down
}
}
?: run {
Timber.w("Stop timeout for $tunnelId (was $currentState); forcing kill")
forceStopTunnel(tunnelId)
}
}
}
private suspend fun saveTunnelActiveState(tunnelConf: TunnelConf, active: Boolean) {
val tunnelCopy = tunnelConf.copyWithCallback(isActive = active)
appDataRepository.tunnels.save(tunnelCopy)
}
private fun cleanUpTunJob(tunnelId: Int) {
Timber.d("Removing job for $tunnelId")
tunJobs -= tunnelId
}
override fun stopTunnel(tunnelConf: TunnelConf?) {
applicationScope.launch(ioDispatcher) {
runCatching {
if (tunnelConf == null) return@launch stopActiveTunnels()
stopTunnelInner(tunnelConf)
}.onFailure { e ->
Timber.e(e, "Failed to stop tunnel ${tunnelConf?.id}")
}
}
}
companion object {
const val STARTUP_TIMEOUT_MS: Long = 15_000L
const val STOP_TIMEOUT_MS: Long = 5_000L
}
private suspend fun stopTunnelInner(tunnelConf: TunnelConf) {
mutex.withLock {
val tunnel = findActiveTunnel(tunnelConf.id) ?: return
stopBackend(tunnel)
removeActiveTunnel(tunnel)
// use latest tunnel
saveTunnelActiveState(tunnelConf, false)
handleServiceChangesOnStop()
}
}
private fun handleServiceChangesOnStop() {
if (activeTuns.value.isEmpty() && !isBounce.get()) return serviceManager.stopTunnelForegroundService()
val nextActive = activeTuns.value.keys.firstOrNull()
if (nextActive != null) {
Timber.d("Next active tunnel: ${nextActive.id}")
serviceManager.updateTunnelForegroundServiceNotification(nextActive)
}
}
private fun removeActiveTunnel(tunnelConf: TunnelConf) {
activeTuns.update { current ->
current.toMutableMap().apply { remove(tunnelConf) }
}
}
override fun bounceTunnel(tunnelConf: TunnelConf) {
applicationScope.launch(ioDispatcher) {
Timber.i("Bounce tunnel ${tunnelConf.name}")
isBounce.set(true)
stopTunnel(tunnelConf)
delay(300)
startTunnel(tunnelConf)
isBounce.set(false)
}
}
override suspend fun runningTunnelNames(): Set<String> = activeTuns.value.keys.map { it.tunName }.toSet()
}
@@ -1,50 +1,25 @@
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.entity.TunnelConf
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<TunnelConf, TunnelState>.allDown(): Boolean {
return this.all { it.value.state.isDown() }
}
fun Map<TunnelConfig, TunnelState>.hasActive(): Boolean {
return this.any { it.value.status.isUp() }
fun Map<TunnelConf, TunnelState>.hasActive(): Boolean {
return this.any { it.value.state.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<TunnelConf, 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<TunnelConf, TunnelState>.getKeyById(id: Int): TunnelConf? {
return this.keys.find { it.id == id }
}
fun Map<TunnelConfig, TunnelState>.isUp(tunnelConfig: TunnelConfig): Boolean {
return this.getValueById(tunnelConfig.id)?.status?.isUp() ?: false
}
fun MutableStateFlow<Map<TunnelConfig, TunnelState>>.exists(id: Int): Boolean {
return this.value.any { it.key.id == id }
}
fun MutableStateFlow<Map<TunnelConfig, TunnelState>>.isUp(id: Int): Boolean {
return this.value.any { it.key.id == id && it.value.status == TunnelStatus.Up }
}
fun MutableStateFlow<Map<TunnelConfig, TunnelState>>.isStarting(id: Int): Boolean {
return this.value.any { it.key.id == id && it.value.status == TunnelStatus.Starting }
}
fun MutableStateFlow<Map<TunnelConfig, TunnelState>>.findTunnel(id: Int): TunnelConfig? {
return this.value.keys.find { it.id == id }
}
private val URL_PATTERN =
Regex("""^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}:[0-9]{1,5}$""")
fun String.isUrl(): Boolean {
return URL_PATTERN.matches(this)
fun Map<TunnelConf, TunnelState>.isUp(tunnelConf: TunnelConf): Boolean {
return this.getValueById(tunnelConf.id)?.state?.isUp() ?: false
}
@@ -1,145 +1,53 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.wireguard.android.backend.Backend
import com.wireguard.android.backend.BackendException
import com.wireguard.android.backend.Tunnel as WgTunnel
import com.zaneschepke.wireguardautotunnel.R
import com.wireguard.android.backend.Tunnel
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.di.Kernel
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.events.DnsFailure
import com.zaneschepke.wireguardautotunnel.domain.events.InvalidConfig
import com.zaneschepke.wireguardautotunnel.domain.events.KernelTunnelName
import com.zaneschepke.wireguardautotunnel.domain.events.UnknownError
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
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.asTunnelState
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendCoreException
import java.util.concurrent.ConcurrentHashMap
import java.util.regex.Pattern
import javax.inject.Inject
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import timber.log.Timber
import javax.inject.Inject
class KernelTunnel
@Inject
constructor(
@ApplicationScope applicationScope: CoroutineScope,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
private val runConfigHelper: RunConfigHelper,
@Kernel private val backend: Backend,
) : BaseTunnel(applicationScope, ioDispatcher) {
class KernelTunnel @Inject constructor(
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
@ApplicationScope private val applicationScope: CoroutineScope,
serviceManager: ServiceManager,
appDataRepository: AppDataRepository,
notificationManager: NotificationManager,
private val backend: Backend,
) : BaseTunnel(ioDispatcher, applicationScope, appDataRepository, serviceManager, notificationManager) {
private val runtimeTunnels = ConcurrentHashMap<Int, WgTunnel>()
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics? {
return try {
WireGuardStatistics(backend.getStatistics(tunnelConf))
} catch (e: Exception) {
Timber.e(e)
null
}
}
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 suspend fun startBackend(tunnel: TunnelConf) {
backend.setState(tunnel, Tunnel.State.UP, tunnel.toWgConfig())
}
override fun tunnelStateFlow(tunnelConfig: TunnelConfig): Flow<TunnelStatus> = callbackFlow {
validateWireGuardInterfaceName(tunnelConfig.name).onFailure { close(it) }
override fun stopBackend(tunnel: TunnelConf) {
Timber.i("Stopping tunnel ${tunnel.id} kernel")
backend.setState(tunnel, Tunnel.State.DOWN, tunnel.toWgConfig())
}
val stateChannel = Channel<WgTunnel.State>()
override suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
Timber.w("Not yet implemented for kernel")
}
val runtimeTunnel = RuntimeWgTunnel(tunnelConfig, stateChannel)
runtimeTunnels[tunnelConfig.id] = runtimeTunnel
val consumerJob = launch {
stateChannel.consumeAsFlow().collect { state -> trySend(state.asTunnelState()) }
}
try {
withTimeout(STARTUP_TIMEOUT_MS) {
updateTunnelStatus(tunnelConfig.id, TunnelStatus.Starting)
val runConfig = runConfigHelper.buildWgRunConfig(tunnelConfig)
backend.setState(runtimeTunnel, WgTunnel.State.UP, runConfig)
}
} catch (e: TimeoutCancellationException) {
Timber.e("Startup timed out for ${tunnelConfig.name}")
errors.emit(tunnelConfig.name to DnsFailure())
forceStopTunnel(tunnelConfig.id)
close()
} catch (e: BackendException) {
close(e.toBackendCoreException())
} catch (e: IllegalArgumentException) {
Timber.e(e, "Invalid backend arguments")
close(InvalidConfig())
} catch (e: Exception) {
Timber.e(e, "Error while setting tunnel state")
close(UnknownError())
}
awaitClose {
try {
backend.setState(runtimeTunnel, WgTunnel.State.DOWN, null)
} catch (e: BackendException) {
errors.tryEmit(tunnelConfig.name to e.toBackendCoreException())
} finally {
consumerJob.cancel()
stateChannel.close()
runtimeTunnels.remove(tunnelConfig.id)
trySend(TunnelStatus.Down)
close()
}
}
}
override fun getStatistics(tunnelId: Int): TunnelStatistics? {
return try {
val runtimeTunnel = runtimeTunnels[tunnelId] ?: return null
WireGuardStatistics(backend.getStatistics(runtimeTunnel))
} catch (e: Exception) {
Timber.e(e, "Failed to get stats for $tunnelId")
null
}
}
override fun setBackendMode(backendMode: BackendMode) {
Timber.w("Not yet implemented for kernel")
}
override fun getBackendMode(): BackendMode {
return BackendMode.Inactive
}
override fun handleDnsReresolve(tunnelConfig: TunnelConfig): Boolean {
throw NotImplementedError()
}
override suspend fun runningTunnelNames(): Set<String> {
return backend.runningTunnelNames
}
override suspend fun forceStopTunnel(tunnelId: Int) {
val runtimeTunnel = runtimeTunnels[tunnelId] ?: return
try {
backend.setState(runtimeTunnel, WgTunnel.State.DOWN, null)
} catch (e: BackendException) {
Timber.e(e, "Force stop failed for $tunnelId")
} finally {
tunJobs[tunnelId]?.cancel()
runtimeTunnels.remove(tunnelId)
tunJobs.remove(tunnelId)
activeTuns.update { it - tunnelId }
updateTunnelStatus(tunnelId, TunnelStatus.Down)
}
}
override suspend fun runningTunnelNames(): Set<String> {
return backend.runningTunnelNames
}
}
@@ -1,104 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.data.model.DnsProtocol
import com.zaneschepke.wireguardautotunnel.domain.events.InvalidConfig
import com.zaneschepke.wireguardautotunnel.domain.model.DnsSettings
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings
import com.zaneschepke.wireguardautotunnel.domain.model.ProxySettings
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.repository.DnsSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.ProxySettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import java.util.*
import javax.inject.Inject
import kotlinx.coroutines.flow.firstOrNull
import org.amnezia.awg.config.Config
import org.amnezia.awg.config.proxy.HttpProxy
import org.amnezia.awg.config.proxy.Socks5Proxy
class RunConfigHelper
@Inject
constructor(
private val settingsRepository: GeneralSettingRepository,
private val proxySettingsRepository: ProxySettingsRepository,
private val dnsSettingsRepository: DnsSettingsRepository,
private val tunnelsRepository: TunnelRepository,
) {
private data class PrepResult(
val effectiveConfig: TunnelConfig,
val generalSettings: GeneralSettings,
val dnsSettings: DnsSettings,
)
private suspend fun prepare(tunnelConfig: TunnelConfig): PrepResult {
val generalSettings = settingsRepository.getGeneralSettings()
val dnsSettings = dnsSettingsRepository.getDnsSettings()
val effectiveConfig =
if (
generalSettings.isGlobalSplitTunnelEnabled || dnsSettings.isGlobalTunnelDnsEnabled
) {
val globalConfig =
tunnelsRepository.globalTunnelFlow.firstOrNull() ?: throw InvalidConfig()
tunnelConfig.copyWithGlobalValues(
globalConfig,
dnsSettings.isGlobalTunnelDnsEnabled,
generalSettings.isGlobalSplitTunnelEnabled,
)
} else {
tunnelConfig
}
return PrepResult(effectiveConfig, generalSettings, dnsSettings)
}
suspend fun buildAmRunConfig(tunnelConfig: TunnelConfig): Config {
val prep = prepare(tunnelConfig)
val proxies =
if (prep.generalSettings.appMode == AppMode.PROXY) {
val proxySettings = proxySettingsRepository.getProxySettings()
buildList {
if (proxySettings.socks5ProxyEnabled) {
add(
Socks5Proxy(
proxySettings.socks5ProxyBindAddress
?: ProxySettings.DEFAULT_SOCKS_BIND_ADDRESS,
proxySettings.proxyUsername,
proxySettings.proxyPassword,
)
)
}
if (proxySettings.httpProxyEnabled) {
add(
HttpProxy(
proxySettings.httpProxyBindAddress
?: ProxySettings.DEFAULT_HTTP_BIND_ADDRESS,
proxySettings.proxyUsername,
proxySettings.proxyPassword,
)
)
}
}
} else {
emptyList()
}
val amConfig = prep.effectiveConfig.toAmConfig()
return Config.Builder()
.setInterface(amConfig.`interface`)
.addPeers(amConfig.peers)
.addProxies(proxies)
.setDnsSettings(
org.amnezia.awg.config.DnsSettings(
prep.dnsSettings.dnsProtocol == DnsProtocol.DOH,
Optional.ofNullable(prep.dnsSettings.dnsEndpoint),
)
)
.build()
}
suspend fun buildWgRunConfig(tunnelConfig: TunnelConfig): com.wireguard.config.Config {
val prep = prepare(tunnelConfig)
return prep.effectiveConfig.toWgConfig()
}
}
@@ -1,21 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import kotlinx.coroutines.channels.Channel
import org.amnezia.awg.backend.Tunnel
class RuntimeAwgTunnel(
private val tunnelConfig: TunnelConfig,
private val stateChannel: Channel<Tunnel.State>,
) : Tunnel {
override fun getName() = tunnelConfig.name
override fun onStateChange(newState: Tunnel.State) {
stateChannel.trySend(newState)
}
override fun isIpv4ResolutionPreferred() = tunnelConfig.isIpv4Preferred
override fun isMetered() = tunnelConfig.isMetered
}
@@ -1,19 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import kotlinx.coroutines.channels.Channel
class RuntimeWgTunnel(
private val config: TunnelConfig,
private val stateChannel: Channel<Tunnel.State>,
) : Tunnel {
override fun getName() = config.name
override fun onStateChange(newState: Tunnel.State) {
stateChannel.trySend(newState)
}
override fun isIpv4ResolutionPreferred() = config.isIpv4Preferred
}
@@ -1,558 +1,97 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.di.*
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException
import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage
import com.zaneschepke.wireguardautotunnel.domain.events.NotAuthorized
import com.zaneschepke.wireguardautotunnel.domain.model.AutoTunnelSettings
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.LockdownSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.state.LogHealthState
import com.zaneschepke.wireguardautotunnel.domain.state.PingState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.di.Kernel
import com.zaneschepke.wireguardautotunnel.di.Userspace
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import java.util.concurrent.ConcurrentHashMap
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import javax.inject.Inject
import kotlin.concurrent.atomics.AtomicBoolean
import kotlin.concurrent.atomics.AtomicReference
import kotlin.concurrent.atomics.ExperimentalAtomicApi
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import timber.log.Timber
@OptIn(ExperimentalCoroutinesApi::class, ExperimentalAtomicApi::class)
class TunnelManager
@Inject
constructor(
@Kernel private val kernelTunnel: TunnelProvider,
@Userspace private val userspaceTunnel: TunnelProvider,
@ProxyUserspace private val proxyUserspaceTunnel: TunnelProvider,
private val serviceManager: ServiceManager,
private val settingsRepository: GeneralSettingRepository,
private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
private val lockdownSettingsRepository: LockdownSettingsRepository,
private val tunnelsRepository: TunnelRepository,
private val tunnelMonitor: TunnelMonitor,
@ApplicationScope private val applicationScope: CoroutineScope,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
class TunnelManager @Inject constructor(
@Kernel private val kernelTunnel: TunnelProvider,
@Userspace private val userspaceTunnel: TunnelProvider,
private val appDataRepository: AppDataRepository,
@ApplicationScope private val applicationScope: CoroutineScope,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : TunnelProvider {
@OptIn(ExperimentalCoroutinesApi::class)
private val localErrorEvents = MutableSharedFlow<Pair<String?, BackendCoreException>>()
@OptIn(ExperimentalCoroutinesApi::class)
private val tunnelProviderFlow = appDataRepository.settings.flow
.filterNotNull()
.flatMapLatest { settings ->
MutableStateFlow(if (settings.isKernelEnabled) kernelTunnel else userspaceTunnel)
}
.stateIn(
scope = applicationScope.plus(ioDispatcher),
started = SharingStarted.Eagerly,
initialValue = userspaceTunnel,
)
@OptIn(ExperimentalCoroutinesApi::class)
private val localMessageEvents = MutableSharedFlow<Pair<String?, BackendMessage>>()
@OptIn(ExperimentalCoroutinesApi::class)
override val activeTunnels = appDataRepository.settings.flow
.filterNotNull()
.flatMapLatest { settings ->
if (settings.isKernelEnabled) {
kernelTunnel.activeTunnels
} else {
userspaceTunnel.activeTunnels
}
}
.stateIn(
scope = applicationScope.plus(ioDispatcher),
started = SharingStarted.Eagerly,
initialValue = emptyMap(),
)
private val monitoringMutex = Mutex()
private val monitoringJobs = ConcurrentHashMap<Int, Job>()
override fun startTunnel(tunnelConf: TunnelConf) {
tunnelProviderFlow.value.startTunnel(tunnelConf)
}
private val ddnsMutex = Mutex()
private val ddnsJobs = ConcurrentHashMap<Int, Job>()
override fun stopTunnel(tunnelConf: TunnelConf?) {
tunnelProviderFlow.value.stopTunnel(tunnelConf)
}
private data class SideEffectState(
val activeTuns: Map<Int, TunnelState>,
val tuns: List<TunnelConfig>,
val settings: GeneralSettings,
val previouslyActive: Map<Int, TunnelState>,
)
override fun bounceTunnel(tunnelConf: TunnelConf) {
tunnelProviderFlow.value.bounceTunnel(tunnelConf)
}
private data class SideEffectWithCondition(
val effect: suspend (SideEffectState) -> Unit,
val condition: (SideEffectState) -> Boolean,
)
override suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
tunnelProviderFlow.value.setBackendState(backendState, allowedIps)
}
private val tunnelProviderFlow: StateFlow<TunnelProvider> = run {
val currentBackend = AtomicReference(userspaceTunnel)
val currentSettings = AtomicReference(GeneralSettings())
val initialEmit = AtomicBoolean(true)
override suspend fun runningTunnelNames(): Set<String> {
return tunnelProviderFlow.value.runningTunnelNames()
}
settingsRepository.flow
.filterNotNull()
// ignore default state
.filterNot { it == GeneralSettings() }
.distinctUntilChangedBy { it.appMode }
.map { settings ->
Timber.d("App mode changes with ${settings.appMode}")
val backend =
when (settings.appMode) {
AppMode.VPN -> userspaceTunnel
AppMode.PROXY -> proxyUserspaceTunnel
AppMode.LOCK_DOWN -> proxyUserspaceTunnel
AppMode.KERNEL -> kernelTunnel
}
settings to backend
}
.onEach { (settings, newBackend) ->
val isInitialEmit = initialEmit.exchange(false)
val previousBackend = currentBackend.exchange(newBackend)
val previousSettings = currentSettings.exchange(settings)
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics? {
return tunnelProviderFlow.value.getStatistics(tunnelConf)
}
if ((previousSettings.appMode != settings.appMode) && !isInitialEmit) {
handleModeChangeCleanup(previousBackend, previousSettings.appMode)
}
if (settings.appMode == AppMode.LOCK_DOWN) {
handleLockDownModeInit()
}
}
.map { (_, backend) -> backend }
.stateIn(
scope = applicationScope.plus(ioDispatcher),
started = SharingStarted.Eagerly,
initialValue = userspaceTunnel,
)
}
override val activeTunnels: StateFlow<Map<Int, TunnelState>> = run {
val activeTunsReference: AtomicReference<Map<Int, TunnelState>> =
AtomicReference(emptyMap())
tunnelProviderFlow
.flatMapLatest { backend ->
combine(
backend.activeTunnels,
tunnelsRepository.flow,
settingsRepository.flow.filterNotNull(),
) { activeTuns, tuns, settings ->
Triple(activeTuns, tuns, settings)
}
}
.onStart { handleRestore() }
.onEach { (activeTuns, tuns, settings) ->
val previouslyActive = activeTunsReference.exchange(activeTuns)
val state = SideEffectState(activeTuns, tuns, settings, previouslyActive)
applicationScope.launch(ioDispatcher) {
supervisorScope {
val sideEffects =
listOf(
SideEffectWithCondition(
effect = { s ->
handleTunnelServiceChange(s.settings.appMode, s.activeTuns)
},
condition = { s ->
s.activeTuns.size != s.previouslyActive.size
},
),
SideEffectWithCondition(
effect = { s ->
handleTunnelsActiveChange(
s.previouslyActive,
s.activeTuns,
s.tuns,
)
},
condition = { s ->
s.activeTuns.size != s.previouslyActive.size
},
),
SideEffectWithCondition(
effect = { s ->
handleDynamicDnsMonitoring(s.activeTuns, s.tuns, s.settings)
},
condition = { s ->
s.activeTuns.keys != s.previouslyActive.keys
},
),
SideEffectWithCondition(
effect = { s ->
handleFullTunnelMonitoring(s.activeTuns, s.tuns, s.settings)
},
condition = { s ->
s.activeTuns.keys != s.previouslyActive.keys
},
),
)
sideEffects
.filter { it.condition(state) }
.forEach { sideEffect ->
launch {
try {
sideEffect.effect(state)
} catch (e: Exception) {
Timber.e(e, "Side effect failed")
}
}
}
}
}
}
.map { (activeTuns, _, _) -> activeTuns }
.stateIn(
scope = applicationScope,
started = SharingStarted.Eagerly,
initialValue = emptyMap(),
)
}
@OptIn(ExperimentalCoroutinesApi::class)
override val errorEvents: SharedFlow<Pair<String?, BackendCoreException>> =
merge(localErrorEvents, tunnelProviderFlow.flatMapLatest { it.errorEvents })
.shareIn(
scope = applicationScope + ioDispatcher,
started = SharingStarted.Eagerly,
replay = 0,
)
@OptIn(ExperimentalCoroutinesApi::class)
override val messageEvents: SharedFlow<Pair<String?, BackendMessage>> =
merge(localMessageEvents, tunnelProviderFlow.flatMapLatest { it.messageEvents })
.shareIn(
scope = applicationScope.plus(ioDispatcher),
started = SharingStarted.Eagerly,
replay = 0,
)
override fun getStatistics(tunnelId: Int): TunnelStatistics? {
return tunnelProviderFlow.value.getStatistics(tunnelId)
}
override suspend fun startTunnel(tunnelConfig: TunnelConfig) {
if (activeTunnels.value.containsKey(tunnelConfig.id)) return
val provider = tunnelProviderFlow.value
val isKernel = provider is KernelTunnel
if (!isKernel && activeTunnels.value.isNotEmpty()) {
stopActiveTunnels()
withTimeoutOrNull(BaseTunnel.STARTUP_TIMEOUT_MS) {
activeTunnels.first { it.isEmpty() }
} ?: run { activeTunnels.value.keys.forEach { id -> provider.forceStopTunnel(id) } }
}
tunnelProviderFlow.value.startTunnel(tunnelConfig)
}
override suspend fun stopTunnel(tunnelId: Int) {
tunnelProviderFlow.value.stopTunnel(tunnelId)
}
override suspend fun forceStopTunnel(tunnelId: Int) {
tunnelProviderFlow.value.forceStopTunnel(tunnelId)
}
override suspend fun stopActiveTunnels() {
tunnelProviderFlow.value.stopActiveTunnels()
}
override fun setBackendMode(backendMode: BackendMode) {
tunnelProviderFlow.value.setBackendMode(backendMode)
}
override fun getBackendMode(): BackendMode {
return tunnelProviderFlow.value.getBackendMode()
}
override suspend fun runningTunnelNames(): Set<String> {
return tunnelProviderFlow.value.runningTunnelNames()
}
override fun handleDnsReresolve(tunnelConfig: TunnelConfig): Boolean {
return tunnelProviderFlow.value.handleDnsReresolve(tunnelConfig)
}
override suspend fun updateTunnelStatus(
tunnelId: Int,
status: TunnelStatus?,
stats: TunnelStatistics?,
pingStates: Map<String, PingState>?,
logHealthState: LogHealthState?,
) {
tunnelProviderFlow.value.updateTunnelStatus(
tunnelId,
status,
stats,
pingStates,
logHealthState,
)
}
private suspend fun handleTunnelServiceChange(
appMode: AppMode,
activeTuns: Map<Int, TunnelState>,
) {
if (activeTuns.isEmpty()) serviceManager.stopTunnelService()
if (activeTuns.isNotEmpty() && serviceManager.tunnelService.value == null)
serviceManager.startTunnelService(appMode)
serviceManager.updateTunnelTile()
}
// TODO this can crash if we haven't started foreground service yet, especially for
// workerManager
private suspend fun handleLockDownModeInit() {
val lockdownSettings = lockdownSettingsRepository.getLockdownSettings()
val allowedIps =
if (lockdownSettings.bypassLan) TunnelConfig.IPV4_PUBLIC_NETWORKS else emptySet()
try {
if (serviceManager.hasVpnPermission()) {
proxyUserspaceTunnel.setBackendMode(
BackendMode.KillSwitch(
allowedIps,
lockdownSettings.metered,
lockdownSettings.dualStack,
)
)
} else {
throw NotAuthorized()
}
} catch (e: BackendCoreException) {
localErrorEvents.tryEmit(null to e)
}
}
private suspend fun handleModeChangeCleanup(
previousBackend: TunnelProvider,
previousAppMode: AppMode,
) {
previousBackend.stopActiveTunnels()
// stop lockdown if we switch from that mode
if (previousAppMode == AppMode.LOCK_DOWN)
proxyUserspaceTunnel.setBackendMode(BackendMode.Inactive)
}
suspend fun handleRestore() =
withContext(ioDispatcher) {
val settings = settingsRepository.getGeneralSettings()
val autoTunnelSettings = autoTunnelSettingsRepository.getAutoTunnelSettings()
val tunnels = tunnelsRepository.getAll()
if (autoTunnelSettings.isAutoTunnelEnabled)
return@withContext restoreAutoTunnel(autoTunnelSettings)
if (settings.appMode == AppMode.LOCK_DOWN) handleLockDownModeInit()
if (tunnels.any { it.isActive }) {
if (settings.appMode == AppMode.VPN && !serviceManager.hasVpnPermission())
return@withContext localErrorEvents.emit(null to NotAuthorized())
when (settings.appMode) {
AppMode.VPN,
AppMode.PROXY,
AppMode.LOCK_DOWN -> {
tunnels.firstOrNull { it.isActive }?.let { startTunnel(it) }
}
AppMode.KERNEL ->
tunnels.filter { it.isActive }.forEach { conf -> startTunnel(conf) }
}
}
}
private suspend fun restoreAutoTunnel(autoTunnelSettings: AutoTunnelSettings) {
autoTunnelSettingsRepository.upsert(autoTunnelSettings.copy(isAutoTunnelEnabled = true))
serviceManager.startAutoTunnelService()
}
suspend fun handleReboot() =
withContext(ioDispatcher) {
val settings = settingsRepository.getGeneralSettings()
val autoTunnelSettings = autoTunnelSettingsRepository.getAutoTunnelSettings()
val defaultTunnel = tunnelsRepository.getStartTunnel()
if (autoTunnelSettings.startOnBoot)
return@withContext restoreAutoTunnel(autoTunnelSettings)
if (settings.isRestoreOnBootEnabled) {
tunnelsRepository.resetActiveTunnels()
when (settings.appMode) {
AppMode.LOCK_DOWN -> handleLockDownModeInit()
AppMode.VPN ->
if (!serviceManager.hasVpnPermission())
return@withContext localErrorEvents.emit(null to NotAuthorized())
AppMode.KERNEL,
AppMode.PROXY -> Unit
}
defaultTunnel?.let { startTunnel(it) }
}
}
private suspend fun handleTunnelsActiveChange(
previousActiveTuns: Map<Int, TunnelState>,
activeTuns: Map<Int, TunnelState>,
tuns: List<TunnelConfig>,
) {
val relevantTunnels = previousActiveTuns.keys + activeTuns.keys
relevantTunnels.forEach { tunnelId ->
val wasActive = previousActiveTuns.containsKey(tunnelId)
val isActiveNow = activeTuns.containsKey(tunnelId)
when {
!wasActive && isActiveNow -> {
tuns
.find { it.id == tunnelId }
?.let { dbTunnelConf ->
tunnelsRepository.save(dbTunnelConf.copy(isActive = true))
}
}
wasActive && !isActiveNow -> {
tuns
.find { it.id == tunnelId }
?.let { dbTunnelConf ->
tunnelsRepository.save(dbTunnelConf.copy(isActive = false))
}
}
}
}
}
suspend fun restartActiveTunnel(id: Int) =
withContext(ioDispatcher) {
val activeIds = activeTunnels.value.keys.toList()
if (activeIds.isEmpty()) return@withContext
if (!activeIds.contains(id)) return@withContext
val tunnel = tunnelsRepository.getById(id) ?: return@withContext
restartTunnel(tunnel)
}
suspend fun restartActiveTunnels() =
withContext(ioDispatcher) {
val activeIds = activeTunnels.value.keys.toList()
if (activeIds.isEmpty()) return@withContext
val tunnels = tunnelsRepository.getAll()
if (tunnels.isEmpty()) return@withContext
supervisorScope {
activeIds.forEach { id ->
val tunnel =
tunnels.find { it.id == id }
?: run {
Timber.w("Tunnel config $id not found; skipping restart")
return@forEach
}
restartTunnel(tunnel)
}
}
}
private suspend fun restartTunnel(tunnel: TunnelConfig) {
runCatching { stopTunnel(tunnel.id) }
.onFailure { e -> Timber.e(e, "Failed to stop tunnel ${tunnel.id} during restart") }
delay(RESTART_TUNNEL_DELAY)
runCatching { startTunnel(tunnel) }
.onFailure { e -> Timber.e(e, "Failed to restart tunnel ${tunnel.id}") }
}
private suspend fun handleDynamicDnsMonitoring(
activeTuns: Map<Int, TunnelState>,
configs: List<TunnelConfig>,
settings: GeneralSettings,
) =
ddnsMutex.withLock {
val activeIds =
activeTuns.keys
.filter { id ->
configs.find { it.id == id }?.restartOnPingFailure == true &&
settings.appMode != AppMode.KERNEL
}
.toSet()
val currentJobs = ddnsJobs.keys.toSet()
val obsoleteIds = currentJobs - activeIds
Timber.d(
"DDNS Monitoring: Active IDs: $activeIds, Obsolete IDs: $obsoleteIds, Total jobs before: ${ddnsJobs.size}"
)
obsoleteIds.forEach { id ->
ddnsJobs[id]?.cancel()
ddnsJobs.remove(id)
}
activeIds.forEach { id ->
if (ddnsJobs.containsKey(id)) return@forEach // Skip if already monitored
val conf = configs.find { it.id == id } ?: return@forEach
val tunStateFlow =
activeTunnels.map { it[id] }.stateIn(applicationScope + ioDispatcher)
val newJob =
applicationScope.launch(ioDispatcher) {
var backoff = 30_000L
while (isActive) {
val state = tunStateFlow.value ?: break
if (state.health() != TunnelState.Health.UNHEALTHY) {
backoff = BASE_BACKOFF
tunStateFlow.first {
it?.health() == TunnelState.Health.UNHEALTHY || it == null
}
continue
}
runCatching {
val updated = handleDnsReresolve(conf)
if (updated) {
localMessageEvents.emit(
conf.name to BackendMessage.DynamicDnsSuccess
)
backoff = BASE_BACKOFF
} else {
Timber.i(
"Dynamic DNS check completed, current endpoint address is already up to date."
)
}
}
.onFailure {
Timber.e(
it,
"Failed to handle dns re-resolution for ${conf.name}",
)
}
delay(backoff)
backoff = (backoff * 1.5).toLong().coerceAtMost(MAX_BACKOFF_TIME)
}
}
ddnsJobs[id] = newJob
}
}
private suspend fun handleFullTunnelMonitoring(
activeTuns: Map<Int, TunnelState>,
configs: List<TunnelConfig>,
settings: GeneralSettings,
) =
monitoringMutex.withLock {
val activeIds = activeTuns.keys.toSet()
val currentJobs = monitoringJobs.keys.toSet()
val obsoleteIds = currentJobs - activeIds
Timber.d(
"Monitoring: Active IDs: $activeIds, Obsolete IDs: $obsoleteIds, Total jobs before: ${monitoringJobs.size}"
)
obsoleteIds.forEach { id ->
monitoringJobs[id]?.cancel()
monitoringJobs.remove(id)
}
activeIds.forEach { id ->
if (monitoringJobs.containsKey(id)) return@forEach // Skip if already monitored
configs.find { it.id == id } ?: return@forEach
val tunStateFlow =
activeTunnels.map { it[id] }.stateIn(applicationScope + ioDispatcher)
val newJob =
applicationScope.launch(ioDispatcher) {
tunnelMonitor.startMonitoring(
id,
withLogs = settings.appMode != AppMode.KERNEL,
tunStateFlow = tunStateFlow,
getStatistics = { tunnelId -> getStatistics(tunnelId) },
updateTunnelStatus = { tid, _, stats, pings, logHealth ->
updateTunnelStatus(tid, null, stats, pings, logHealth)
},
)
}
monitoringJobs[id] = newJob
}
}
companion object {
const val BASE_BACKOFF = 30_000L
const val MAX_BACKOFF_TIME = 300_000L
const val RESTART_TUNNEL_DELAY = 300L
}
fun restorePreviousState() = applicationScope.launch(ioDispatcher) {
val settings = appDataRepository.settings.get()
if (settings.isRestoreOnBootEnabled) {
val previouslyActiveTuns = appDataRepository.tunnels.getActive()
val tunsToStart = previouslyActiveTuns.filterNot { tun -> activeTunnels.value.any { tun.id == it.key.id } }
if (settings.isKernelEnabled) {
return@launch tunsToStart.forEach {
startTunnel(it)
}
} else {
tunsToStart.firstOrNull()?.let { startTunnel(it) }
}
}
}
}
@@ -1,321 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import android.os.PowerManager
import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.state.*
import com.zaneschepke.wireguardautotunnel.util.extensions.toMillis
import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils
import inet.ipaddr.AddressValueException
import inet.ipaddr.IPAddress
import inet.ipaddr.IPAddressString
import io.ktor.util.collections.*
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import timber.log.Timber
@Singleton
class TunnelMonitor
@Inject
constructor(
private val settingsRepository: GeneralSettingRepository,
private val tunnelsRepository: TunnelRepository,
private val monitoringSettingsRepository: MonitoringSettingsRepository,
private val networkMonitor: NetworkMonitor,
private val networkUtils: NetworkUtils,
private val logReader: LogReader,
private val powerManager: PowerManager,
) {
@OptIn(FlowPreview::class)
suspend fun startMonitoring(
tunnelId: Int,
withLogs: Boolean,
tunStateFlow: StateFlow<TunnelState?>,
getStatistics: suspend (Int) -> TunnelStatistics?,
updateTunnelStatus:
suspend (
Int, TunnelStatus?, TunnelStatistics?, Map<String, PingState>?, LogHealthState?,
) -> Unit,
): Job = coroutineScope {
launch {
val config = tunnelsRepository.getById(tunnelId) ?: return@launch
launch { startPingMonitor(config, tunStateFlow, updateTunnelStatus) }
launch { startWgStatsPoll(tunnelId, getStatistics, updateTunnelStatus) }
if (withLogs) launch { startLogsMonitor(config, updateTunnelStatus) }
}
}
private suspend fun startLogsMonitor(
tunnelConfig: TunnelConfig,
updateTunnelStatus:
suspend (
Int, TunnelStatus?, TunnelStatistics?, Map<String, PingState>?, LogHealthState?,
) -> Unit,
) {
logReader.liveLogs
.filter { log -> log.tag.contains(tunnelConfig.name) }
.mapNotNull { log ->
val now = System.currentTimeMillis()
when {
successLogRegex.containsMatchIn(log.message) ->
LogHealthState(isHealthy = true, timestamp = now)
failureLogRegex.containsMatchIn(log.message) ->
LogHealthState(isHealthy = false, timestamp = now)
else -> null
}
}
.distinctUntilChangedBy { it.isHealthy }
.collect { logHealthState ->
Timber.d("Tunnel log health updated for ${tunnelConfig.name}: $logHealthState")
updateTunnelStatus(tunnelConfig.id, null, null, null, logHealthState)
}
}
private suspend fun startPingMonitor(
tunnelConfig: TunnelConfig,
tunStateFlow: StateFlow<TunnelState?>,
updateTunnelStatus:
suspend (
Int, TunnelStatus?, TunnelStatistics?, Map<String, PingState>?, LogHealthState?,
) -> Unit,
) = coroutineScope {
val pingStatsFlow = MutableStateFlow<Map<String, PingState>>(emptyMap())
val connectivityStateFlow = networkMonitor.connectivityStateFlow.stateIn(this)
val isNetworkConnected = connectivityStateFlow.map { it.hasInternet() }.stateIn(this)
combine(
settingsRepository.flow.distinctUntilChangedBy { it.appMode },
monitoringSettingsRepository.flow,
) { settings, monitorSettings ->
Pair(settings.appMode, monitorSettings)
}
.collectLatest { (appMode, settings) ->
if (!settings.isPingEnabled) return@collectLatest
// TODO for now until we get monitoring for these modes
if (appMode == AppMode.LOCK_DOWN || appMode == AppMode.PROXY) return@collectLatest
Timber.d("Starting pinger for ${tunnelConfig.name} with settings")
val config = tunnelConfig.toAmConfig()
val pingablePeers = config.peers.filter { it.allowedIps.isNotEmpty() }
if (pingablePeers.isEmpty()) return@collectLatest
suspend fun performPing() {
val updates = ConcurrentMap<String, PingState>()
pingablePeers
.map { it.publicKey.toBase64() to it }
.forEach { (key, peer) ->
ensureActive()
val previousState = pingStatsFlow.value[key] ?: PingState()
val allowedIpStr = peer.allowedIps.firstOrNull()?.toString()
if (allowedIpStr == null) {
updates[key] =
previousState.copy(
isReachable = false,
failureReason = FailureReason.NoResolvedEndpoint,
lastPingAttemptMillis = System.currentTimeMillis(),
)
return@forEach
}
val host =
tunnelConfig.pingTarget
?: run {
val parts = allowedIpStr.split("/")
val internalIp =
if (parts.size == 2) parts[0] else allowedIpStr
val prefix =
if (parts.size == 2) parts[1].toIntOrNull() ?: 32
else 32
val cleanedIp = internalIp.removeSurrounding("[", "]")
val defaultCloudflare =
if (cleanedIp.contains(":")) CLOUDFLARE_IPV6_IP
else CLOUDFLARE_IPV4_IP
if (prefix <= 1) {
defaultCloudflare
} else {
try {
val addrStr = IPAddressString(cleanedIp)
val addr: IPAddress =
addrStr.address
?: throw AddressValueException(
"Invalid IP: $cleanedIp"
)
val isIpv6 = addr.isIPv6
val cloudflareIp =
if (isIpv6) CLOUDFLARE_IPV6_IP
else CLOUDFLARE_IPV4_IP
val max = if (isIpv6) 128 else 32
if (prefix == max) {
addr.toCanonicalString()
} else {
val nextAddr: IPAddress? = addr.increment(1)
nextAddr?.toCanonicalString() ?: cloudflareIp
}
} catch (e: AddressValueException) {
Timber.e(
e,
"Failed to parse or increment IP: $cleanedIp",
)
defaultCloudflare
}
}
}
val attemptTime = System.currentTimeMillis()
val timeout = settings.tunnelPingTimeoutSeconds?.toMillis() ?: 5000L
runCatching {
withTimeout(
settings.tunnelPingTimeoutSeconds?.toMillis() ?: 5000L
) {
val pingStats =
settings.tunnelPingTimeoutSeconds?.let {
networkUtils.pingWithStats(
host,
settings.tunnelPingAttempts,
it.toMillis(),
)
}
?: networkUtils.pingWithStats(
host,
settings.tunnelPingAttempts,
)
updates[key] =
previousState.copy(
transmitted = pingStats.transmitted,
received = pingStats.received,
packetLoss = pingStats.packetLoss,
rttMin = pingStats.rttMin,
rttMax = pingStats.rttMax,
rttAvg = pingStats.rttAvg,
rttStddev = pingStats.rttStddev,
isReachable = pingStats.isReachable,
failureReason =
if (pingStats.isReachable) null
else FailureReason.PingFailed,
lastSuccessfulPingMillis =
pingStats.lastSuccessfulPingMillis
?: previousState.lastSuccessfulPingMillis,
pingTarget = host,
lastPingAttemptMillis = attemptTime,
)
Timber.d(
"Ping completed for peer ${peer.publicKey.toBase64().substring(0, 5)}.. to host $host with stats: $pingStats"
)
}
}
.onFailure {
Timber.e(
it,
"Ping failed for peer ${peer.publicKey} in ${tunnelConfig.name} to host $host",
)
updates[key] =
previousState.copy(
isReachable = false,
failureReason = FailureReason.PingFailed,
pingTarget = host,
lastPingAttemptMillis = attemptTime,
)
}
}
if (updates.isNotEmpty()) {
ensureActive()
pingStatsFlow.update { updates }
updateTunnelStatus(tunnelConfig.id, null, null, updates, null)
}
}
// Wait for the tunnel to be fully active
tunStateFlow.filter { state -> state?.status == TunnelStatus.Up }.first()
// small delay to make sure tunnel is fully up before we actively monitor
delay(3_000L)
while (isActive) {
ensureActive()
if (!powerManager.isDeviceIdleMode) {
if (isNetworkConnected.value) {
performPing()
} else {
pingStatsFlow.update { current ->
current.mapValues { entry ->
entry.value.copy(
isReachable = false,
failureReason = FailureReason.NoConnectivity,
lastPingAttemptMillis = System.currentTimeMillis(),
)
}
}
ensureActive()
updateTunnelStatus(
tunnelConfig.id,
null,
null,
pingStatsFlow.value,
null,
)
}
}
delay(settings.tunnelPingIntervalSeconds.toMillis())
}
}
}
private suspend fun startWgStatsPoll(
tunnelId: Int,
getStatistics: suspend (Int) -> TunnelStatistics?,
updateTunnelStatus:
suspend (
Int, TunnelStatus?, TunnelStatistics?, Map<String, PingState>?, LogHealthState?,
) -> Unit,
) = coroutineScope {
while (isActive) {
ensureActive()
if (!powerManager.isDeviceIdleMode) {
val stats = getStatistics(tunnelId)
ensureActive()
updateTunnelStatus(tunnelId, null, stats, null, null)
}
delay(STATS_DELAY)
}
}
companion object {
private val successLogRegex =
Regex("Received handshake response|Receiving keepalive packet", RegexOption.IGNORE_CASE)
private val failureLogRegex =
Regex(
"Failed to send handshake initiation: write udp|" +
"Handshake did not complete after 5 seconds, retrying|" +
"Failed to send data packets",
RegexOption.IGNORE_CASE,
)
const val CLOUDFLARE_IPV6_IP = "2606:4700:4700::1111"
const val CLOUDFLARE_IPV4_IP = "1.1.1.1"
const val STATS_DELAY = 1_000L
}
}
@@ -1,54 +1,17 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException
import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.state.LogHealthState
import com.zaneschepke.wireguardautotunnel.domain.state.PingState
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
interface TunnelProvider {
/** Starts the specified tunnel configuration. */
suspend fun startTunnel(tunnelConfig: TunnelConfig)
/**
* Stops the specified tunnel.
*
* @param tunnelId The tunnelConf to stop.
*/
suspend fun stopTunnel(tunnelId: Int)
suspend fun forceStopTunnel(tunnelId: Int)
/** Stops all active tunnels. */
suspend fun stopActiveTunnels()
fun setBackendMode(backendMode: BackendMode)
fun getBackendMode(): BackendMode
suspend fun runningTunnelNames(): Set<String>
fun handleDnsReresolve(tunnelConfig: TunnelConfig): Boolean
fun getStatistics(tunnelId: Int): TunnelStatistics?
val activeTunnels: StateFlow<Map<Int, TunnelState>>
val errorEvents: SharedFlow<Pair<String?, BackendCoreException>>
val messageEvents: SharedFlow<Pair<String?, BackendMessage>>
suspend fun updateTunnelStatus(
tunnelId: Int,
status: TunnelStatus? = null,
stats: TunnelStatistics? = null,
pingStates: Map<String, PingState>? = null,
logHealthState: LogHealthState? = null,
)
fun startTunnel(tunnelConf: TunnelConf)
fun stopTunnel(tunnelConf: TunnelConf? = null)
fun bounceTunnel(tunnelConf: TunnelConf)
suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>)
suspend fun runningTunnelNames(): Set<String>
fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics?
val activeTunnels: StateFlow<Map<TunnelConf, TunnelState>>
}
@@ -1,135 +1,56 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
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.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.events.*
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
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.asAmBackendMode
import com.zaneschepke.wireguardautotunnel.util.extensions.asBackendMode
import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendCoreException
import java.io.IOException
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.update
import com.zaneschepke.wireguardautotunnel.util.extensions.asAmBackendState
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import org.amnezia.awg.backend.Backend
import org.amnezia.awg.backend.BackendException
import org.amnezia.awg.backend.Tunnel as AwgTunnel
import org.amnezia.awg.backend.Tunnel
import timber.log.Timber
import javax.inject.Inject
class UserspaceTunnel
@Inject
constructor(
@ApplicationScope applicationScope: CoroutineScope,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
private val backend: Backend,
private val runConfigHelper: RunConfigHelper,
) : BaseTunnel(applicationScope, ioDispatcher) {
class UserspaceTunnel @Inject constructor(
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
@ApplicationScope private val applicationScope: CoroutineScope,
serviceManager: ServiceManager,
appDataRepository: AppDataRepository,
notificationManager: NotificationManager,
private val backend: Backend,
) : BaseTunnel(ioDispatcher, applicationScope, appDataRepository, serviceManager, notificationManager) {
private val runtimeTunnels = ConcurrentHashMap<Int, AwgTunnel>()
override suspend fun startBackend(tunnel: TunnelConf) {
stopActiveTunnels()
backend.setState(tunnel, Tunnel.State.UP, tunnel.toAmConfig())
}
override fun tunnelStateFlow(tunnelConfig: TunnelConfig): Flow<TunnelStatus> = callbackFlow {
val stateChannel = Channel<AwgTunnel.State>()
override fun stopBackend(tunnel: TunnelConf) {
Timber.i("Stopping tunnel ${tunnel.id} userspace")
backend.setState(tunnel, Tunnel.State.DOWN, tunnel.toAmConfig())
}
val runtimeTunnel = RuntimeAwgTunnel(tunnelConfig, stateChannel)
runtimeTunnels[tunnelConfig.id] = runtimeTunnel
override suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
Timber.d("Setting backend state: $backendState with allowedIps: $allowedIps")
backend.setBackendState(backendState.asAmBackendState(), allowedIps)
}
val consumerJob = launch {
stateChannel.consumeAsFlow().collect { awgState -> trySend(awgState.asTunnelState()) }
}
override suspend fun runningTunnelNames(): Set<String> {
return backend.runningTunnelNames
}
try {
withTimeout(STARTUP_TIMEOUT_MS) {
updateTunnelStatus(tunnelConfig.id, TunnelStatus.Starting)
val runConfig = runConfigHelper.buildAmRunConfig(tunnelConfig)
backend.setState(runtimeTunnel, AwgTunnel.State.UP, runConfig)
}
} catch (_: TimeoutCancellationException) {
Timber.e("Startup timed out for ${tunnelConfig.name} (likely DNS hang)")
errors.emit(tunnelConfig.name to DnsFailure())
forceStopTunnel(tunnelConfig.id)
close()
} catch (e: BackendException) {
close(e.toBackendCoreException())
} catch (_: IllegalArgumentException) {
close(InvalidConfig())
} catch (e: Exception) {
Timber.e(e, "Error while setting tunnel state")
close(UnknownError())
}
awaitClose {
try {
backend.setState(runtimeTunnel, AwgTunnel.State.DOWN, null)
} catch (e: BackendException) {
errors.tryEmit(tunnelConfig.name to e.toBackendCoreException())
} finally {
consumerJob.cancel()
stateChannel.close()
runtimeTunnels.remove(tunnelConfig.id)
trySend(TunnelStatus.Down)
close()
}
}
}
override fun setBackendMode(backendMode: BackendMode) {
Timber.d("Setting backend mode: $backendMode")
try {
backend.backendMode = backendMode.asAmBackendMode()
} catch (e: BackendException) {
throw e.toBackendCoreException()
// TODO this should be mapped to BackendException in the lib
} catch (e: IOException) {
throw VpnUnauthorized()
}
}
override fun getBackendMode(): BackendMode {
return backend.backendMode.asBackendMode()
}
override fun handleDnsReresolve(tunnelConfig: TunnelConfig): Boolean {
val tunnel = runtimeTunnels[tunnelConfig.id] ?: throw ServiceNotRunning()
return backend.resolveDDNS(tunnelConfig.toAmConfig(), tunnel.isIpv4ResolutionPreferred)
}
override suspend fun runningTunnelNames(): Set<String> {
return backend.runningTunnelNames
}
override fun getStatistics(tunnelId: Int): TunnelStatistics? {
return try {
val runtimeTunnel = runtimeTunnels[tunnelId] ?: return null
AmneziaStatistics(backend.getStatistics(runtimeTunnel))
} catch (e: Exception) {
Timber.e(e, "Failed to get stats for $tunnelId")
null
}
}
override suspend fun forceStopTunnel(tunnelId: Int) {
val runtimeTunnel = runtimeTunnels[tunnelId] ?: return
try {
backend.setState(runtimeTunnel, AwgTunnel.State.DOWN, null)
} catch (e: BackendException) {
Timber.e(e, "Force stop failed for $tunnelId")
} finally {
tunJobs[tunnelId]?.cancel()
runtimeTunnels.remove(tunnelId)
tunJobs.remove(tunnelId)
activeTuns.update { it - tunnelId }
updateTunnelStatus(tunnelId, TunnelStatus.Down)
}
}
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics? {
return try {
AmneziaStatistics(backend.getStatistics(tunnelConf))
} catch (e: Exception) {
Timber.e(e, "Failed to get stats for ${tunnelConf.tunName}")
null
}
}
}
@@ -2,61 +2,59 @@ package com.zaneschepke.wireguardautotunnel.core.worker
import android.content.Context
import androidx.hilt.work.HiltWorker
import androidx.work.*
import androidx.work.CoroutineWorker
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.util.concurrent.TimeUnit
@HiltWorker
class ServiceWorker
@AssistedInject
constructor(
@Assisted private val context: Context,
@Assisted private val params: WorkerParameters,
private val serviceManager: ServiceManager,
private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
class ServiceWorker @AssistedInject constructor(
@Assisted private val context: Context,
@Assisted private val params: WorkerParameters,
private val serviceManager: ServiceManager,
private val appDataRepository: AppDataRepository,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
private val tunnelManager: TunnelManager,
) : CoroutineWorker(context, params) {
companion object {
private const val TAG = "service_worker"
companion object {
private const val TAG = "service_worker"
fun stop(context: Context) {
WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
}
fun stop(context: Context) {
WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
}
fun start(context: Context) {
val periodicWorkRequest =
PeriodicWorkRequestBuilder<ServiceWorker>(
repeatInterval = 15,
repeatIntervalTimeUnit = TimeUnit.MINUTES,
)
.build()
WorkManager.getInstance(context)
.enqueueUniquePeriodicWork(
TAG,
ExistingPeriodicWorkPolicy.KEEP,
periodicWorkRequest,
)
}
}
fun start(context: Context) {
val periodicWorkRequest = PeriodicWorkRequestBuilder<ServiceWorker>(
repeatInterval = 15,
repeatIntervalTimeUnit = TimeUnit.MINUTES,
).build()
WorkManager.getInstance(context)
.enqueueUniquePeriodicWork(
TAG,
ExistingPeriodicWorkPolicy.KEEP,
periodicWorkRequest,
)
}
}
override suspend fun doWork(): Result =
withContext(ioDispatcher) {
Timber.i("Service worker started")
with(autoTunnelSettingsRepository.getAutoTunnelSettings()) {
Timber.i("Checking to see if auto-tunnel has been killed by system")
if (isAutoTunnelEnabled && serviceManager.autoTunnelService.value == null) {
Timber.i("Service has been killed by system, restoring.")
serviceManager.startAutoTunnelService()
}
}
Result.success()
}
override suspend fun doWork(): Result = withContext(ioDispatcher) {
Timber.i("Service worker started")
with(appDataRepository.settings.get()) {
if (isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) return@with serviceManager.startAutoTunnel(true)
if (tunnelManager.activeTunnels.value.isEmpty()) tunnelManager.restorePreviousState()
}
Result.success()
}
}
@@ -1,131 +1,90 @@
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.sqlite.db.SupportSQLiteDatabase
import com.zaneschepke.wireguardautotunnel.data.dao.*
import com.zaneschepke.wireguardautotunnel.data.entity.*
import com.zaneschepke.wireguardautotunnel.data.dao.SettingsDao
import com.zaneschepke.wireguardautotunnel.data.dao.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.data.model.Settings
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
@Database(
entities =
[
TunnelConfig::class,
ProxySettings::class,
GeneralSettings::class,
AutoTunnelSettings::class,
MonitoringSettings::class,
DnsSettings::class,
LockdownSettings::class,
],
version = 29,
autoMigrations =
[
AutoMigration(from = 1, to = 2),
AutoMigration(from = 2, to = 3),
AutoMigration(from = 3, to = 4),
AutoMigration(from = 4, to = 5),
AutoMigration(from = 5, to = 6),
AutoMigration(from = 6, to = 7, spec = RemoveLegacySettingColumnsMigration::class),
AutoMigration(7, 8),
AutoMigration(8, 9),
AutoMigration(9, 10),
AutoMigration(from = 10, to = 11, spec = RemoveTunnelPauseMigration::class),
AutoMigration(from = 11, to = 12),
AutoMigration(from = 12, to = 13),
AutoMigration(from = 13, to = 14),
AutoMigration(from = 14, to = 15),
AutoMigration(from = 15, to = 16),
AutoMigration(from = 16, to = 17, spec = WifiDetectionMigration::class),
AutoMigration(from = 17, to = 18),
AutoMigration(from = 18, to = 19, spec = PingMigration::class),
AutoMigration(from = 19, to = 20, spec = ProxyMigration::class),
AutoMigration(from = 20, to = 21, spec = FixProxySettingsMigration::class),
AutoMigration(from = 21, to = 22),
AutoMigration(from = 22, to = 23),
AutoMigration(from = 24, to = 25),
AutoMigration(from = 26, to = 27, spec = GlobalsMigration::class),
AutoMigration(from = 27, to = 28, spec = DonationMigration::class),
],
exportSchema = true,
entities = [Settings::class, TunnelConfig::class],
version = 16,
autoMigrations =
[
AutoMigration(from = 1, to = 2),
AutoMigration(from = 2, to = 3),
AutoMigration(
from = 3,
to = 4,
),
AutoMigration(
from = 4,
to = 5,
),
AutoMigration(
from = 5,
to = 6,
),
AutoMigration(
from = 6,
to = 7,
spec = RemoveLegacySettingColumnsMigration::class,
),
AutoMigration(7, 8),
AutoMigration(8, 9),
AutoMigration(9, 10),
AutoMigration(
from = 10,
to = 11,
spec = RemoveTunnelPauseMigration::class,
),
AutoMigration(
from = 11,
to = 12,
),
AutoMigration(
from = 12,
to = 13,
),
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 fun tunnelConfigDoa(): TunnelConfigDao
abstract fun settingDao(): SettingsDao
abstract fun proxySettingsDoa(): ProxySettingsDao
abstract fun generalSettingsDao(): GeneralSettingsDao
abstract fun autoTunnelSettingsDao(): AutoTunnelSettingsDao
abstract fun monitoringSettingsDao(): MonitoringSettingsDao
abstract fun lockdownSettingsDao(): LockdownSettingsDao
abstract fun dnsSettingsDao(): DnsSettingsDao
abstract fun tunnelConfigDoa(): TunnelConfigDao
}
@DeleteColumn(tableName = "Settings", columnName = "default_tunnel")
@DeleteColumn(tableName = "Settings", columnName = "is_battery_saver_enabled")
@DeleteColumn(
tableName = "Settings",
columnName = "default_tunnel",
)
@DeleteColumn(
tableName = "Settings",
columnName = "is_battery_saver_enabled",
)
class RemoveLegacySettingColumnsMigration : AutoMigrationSpec
@DeleteColumn(tableName = "Settings", columnName = "is_auto_tunnel_paused")
@DeleteColumn(
tableName = "Settings",
columnName = "is_auto_tunnel_paused",
)
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,77 +4,91 @@ import android.content.Context
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import java.io.IOException
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.IOException
class DataStoreManager(
private val context: Context,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
private val context: Context,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) {
private val preferencesKey = "preferences"
val Context.dataStore by preferencesDataStore(name = preferencesKey)
val dataStore = context.dataStore
companion object {
val locationDisclosureShown = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN")
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 {
val locationDisclosureShown = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN")
val batteryDisableShown = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN")
val shouldShowDonationSnackbar = booleanPreferencesKey("SHOW_DONATION_SNACK")
}
// preferences
private val preferencesKey = "preferences"
private val Context.dataStore by
preferencesDataStore(
name = preferencesKey,
)
suspend fun init() {
withContext(ioDispatcher) {
try {
dataStore.data.first()
} catch (e: IOException) {
Timber.e(e)
}
}
}
suspend fun init() {
withContext(ioDispatcher) {
try {
context.dataStore.data.first()
} catch (e: IOException) {
Timber.Forest.e(e)
}
}
}
suspend fun <T> saveToDataStore(key: Preferences.Key<T>, value: T) {
withContext(ioDispatcher) {
try {
dataStore.edit { it[key] = value }
} catch (e: IOException) {
Timber.e(e)
} catch (e: Exception) {
Timber.e(e)
}
}
}
suspend fun <T> saveToDataStore(key: Preferences.Key<T>, value: T) {
withContext(ioDispatcher) {
try {
context.dataStore.edit { it[key] = value }
} catch (e: IOException) {
Timber.Forest.e(e)
} catch (e: Exception) {
Timber.Forest.e(e)
}
}
}
suspend fun <T> removeFromDataStore(key: Preferences.Key<T>) {
withContext(ioDispatcher) {
try {
dataStore.edit { it.remove(key) }
} catch (e: IOException) {
Timber.e(e)
} catch (e: Exception) {
Timber.e(e)
}
}
}
suspend fun <T> removeFromDataStore(key: Preferences.Key<T>) {
withContext(ioDispatcher) {
try {
context.dataStore.edit { it.remove(key) }
} catch (e: IOException) {
Timber.Forest.e(e)
} catch (e: Exception) {
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? {
return withContext(ioDispatcher) {
try {
dataStore.data.map { it[key] }.first()
} catch (e: IOException) {
Timber.e(e)
null
}
}
}
suspend fun <T> getFromStore(key: Preferences.Key<T>): T? {
return withContext(ioDispatcher) {
try {
context.dataStore.data.map { it[key] }.first()
} catch (e: IOException) {
Timber.Forest.e(e)
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)
}
@@ -2,19 +2,20 @@ package com.zaneschepke.wireguardautotunnel.data
import androidx.room.RoomDatabase
import androidx.sqlite.db.SupportSQLiteDatabase
import javax.inject.Inject
import javax.inject.Provider
import timber.log.Timber
class DatabaseCallback @Inject constructor(private val databaseProvider: Provider<AppDatabase>) :
RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
Timber.d("Database created, inserting default rows")
db.execSQL("INSERT INTO proxy_settings DEFAULT VALUES")
db.execSQL("INSERT INTO general_settings DEFAULT VALUES")
db.execSQL("INSERT INTO auto_tunnel_settings DEFAULT VALUES")
db.execSQL("INSERT INTO monitoring_settings DEFAULT VALUES")
db.execSQL("INSERT INTO dns_settings DEFAULT VALUES")
}
class DatabaseCallback : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) = db.run {
// Notice non-ui thread is here
beginTransaction()
try {
execSQL(Queries.createDefaultSettings())
Timber.i("Bootstrapping settings data")
setTransactionSuccessful()
} 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,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.GeneralSettings
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?>
}
@@ -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,83 +1,58 @@
package com.zaneschepke.wireguardautotunnel.data.dao
import androidx.room.*
import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
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.TunnelConfig
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
import kotlinx.coroutines.flow.Flow
@Dao
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")
suspend fun resetActiveTunnels()
@Query("SELECT * FROM TunnelConfig WHERE is_Active=1")
suspend fun getActive(): TunnelConfigs
@Query("SELECT * FROM tunnel_config WHERE name=:name")
suspend fun getByName(name: String): TunnelConfig?
@Query("SELECT * FROM TunnelConfig")
suspend fun getAll(): TunnelConfigs
@Query("SELECT * FROM tunnel_config WHERE is_Active=1")
suspend fun getActive(): List<TunnelConfig>
@Delete
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 || '%'")
suspend fun findByTunnelNetworkName(name: String): List<TunnelConfig>
@Query("UPDATE TunnelConfig SET is_ethernet_tunnel = 0 WHERE is_ethernet_tunnel =1")
suspend fun resetEthernetTunnel()
@Query("UPDATE tunnel_config SET is_primary_tunnel = 0 WHERE is_primary_tunnel =1")
suspend fun resetPrimaryTunnel()
@Query("SELECT * FROM TUNNELCONFIG WHERE is_primary_tunnel=1")
suspend fun findByPrimary(): TunnelConfigs
@Query("UPDATE tunnel_config SET is_mobile_data_tunnel = 0 WHERE is_mobile_data_tunnel =1")
suspend fun resetMobileDataTunnel()
@Query("SELECT * FROM TUNNELCONFIG WHERE is_mobile_data_tunnel=1")
suspend fun findByMobileDataTunnel(): TunnelConfigs
@Query("UPDATE tunnel_config SET is_ethernet_tunnel = 0 WHERE is_ethernet_tunnel =1")
suspend fun resetEthernetTunnel()
@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
ORDER BY
CASE WHEN is_primary_tunnel = 1 THEN 0 ELSE 1 END,
position ASC
LIMIT 1"""
)
suspend fun getDefaultTunnel(): TunnelConfig?
@Query(
"""
SELECT * FROM tunnel_config
ORDER BY
CASE WHEN is_Active = 1 THEN 0
WHEN is_primary_tunnel = 1 THEN 1
ELSE 2 END,
position ASC
LIMIT 1"""
)
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?>
@Query("SELECT * FROM tunnelconfig")
fun getAllFlow(): Flow<MutableList<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,
)
@@ -1,16 +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.DnsProtocol
@Entity(tableName = "dns_settings")
data class DnsSettings(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
@ColumnInfo(name = "dns_protocol", defaultValue = "0")
val dnsProtocol: DnsProtocol = DnsProtocol.fromValue(0),
@ColumnInfo(name = "dns_endpoint") val dnsEndpoint: String? = null,
@ColumnInfo(name = "global_tunnel_dns_enabled", defaultValue = "0")
val isGlobalTunnelDnsEnabled: Boolean = false,
)
@@ -1,30 +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.AppMode
@Entity(tableName = "general_settings")
data class GeneralSettings(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
@ColumnInfo(name = "is_shortcuts_enabled", defaultValue = "0")
val isShortcutsEnabled: Boolean = false,
@ColumnInfo(name = "is_restore_on_boot_enabled", defaultValue = "0")
val isRestoreOnBootEnabled: Boolean = false,
@ColumnInfo(name = "is_multi_tunnel_enabled", defaultValue = "0")
val isMultiTunnelEnabled: Boolean = false,
@ColumnInfo(name = "global_split_tunnel_enabled", defaultValue = "0")
val isGlobalSplitTunnelEnabled: Boolean = false,
@ColumnInfo(name = "app_mode", defaultValue = "0") val appMode: AppMode = AppMode.fromValue(0),
@ColumnInfo(name = "theme", defaultValue = "AUTOMATIC") val theme: String = "AUTOMATIC",
@ColumnInfo(name = "locale") val locale: String? = null,
@ColumnInfo(name = "remote_key") val remoteKey: String? = null,
@ColumnInfo(name = "is_remote_control_enabled", defaultValue = "0")
val isRemoteControlEnabled: Boolean = false,
@ColumnInfo(name = "is_pin_lock_enabled", defaultValue = "0")
val isPinLockEnabled: Boolean = false,
@ColumnInfo(name = "is_always_on_vpn_enabled", defaultValue = "0")
val isAlwaysOnVpnEnabled: Boolean = false,
@ColumnInfo(name = "already_donated", defaultValue = "0") val alreadyDonated: Boolean = false,
)
@@ -1,12 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.entity
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class GitHubRelease(
@SerialName("tag_name") val tagName: String,
val name: String?,
val body: String?,
val assets: List<Asset>,
)
@@ -1,13 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "lockdown_settings")
data class LockdownSettings(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
@ColumnInfo(name = "bypass_lan", defaultValue = "0") val bypassLan: Boolean = false,
@ColumnInfo(name = "metered", defaultValue = "0") val metered: Boolean = false,
@ColumnInfo(name = "dual_stack", defaultValue = "0") val dualStack: Boolean = false,
)
@@ -1,21 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "monitoring_settings")
data class MonitoringSettings(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
@ColumnInfo(name = "is_ping_enabled", defaultValue = "0") val isPingEnabled: Boolean = false,
@ColumnInfo(name = "is_ping_monitoring_enabled", defaultValue = "1")
val isPingMonitoringEnabled: Boolean = true,
@ColumnInfo(name = "tunnel_ping_interval_sec", defaultValue = "30")
val tunnelPingIntervalSeconds: Int = 30,
@ColumnInfo(name = "tunnel_ping_attempts", defaultValue = "3") val tunnelPingAttempts: Int = 3,
@ColumnInfo(name = "tunnel_ping_timeout_sec") val tunnelPingTimeoutSeconds: Int? = null,
@ColumnInfo(name = "show_detailed_ping_stats", defaultValue = "0")
val showDetailedPingStats: Boolean = false,
@ColumnInfo(name = "is_local_logs_enabled", defaultValue = "0")
val isLocalLogsEnabled: Boolean = false,
)
@@ -1,18 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "proxy_settings")
data class ProxySettings(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
@ColumnInfo(name = "socks5_proxy_enabled", defaultValue = "0")
val socks5ProxyEnabled: Boolean = false,
@ColumnInfo(name = "socks5_proxy_bind_address") val socks5ProxyBindAddress: String? = null,
@ColumnInfo(name = "http_proxy_enable", defaultValue = "0")
val httpProxyEnabled: Boolean = false,
@ColumnInfo(name = "http_proxy_bind_address") val httpProxyBindAddress: String? = null,
@ColumnInfo(name = "proxy_username") val proxyUsername: String? = null,
@ColumnInfo(name = "proxy_password") val proxyPassword: String? = null,
)
@@ -1,36 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(tableName = "tunnel_config", indices = [Index(value = ["name"], unique = true)])
data class TunnelConfig(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
@ColumnInfo(name = "name") val name: String,
@ColumnInfo(name = "wg_quick") val wgQuick: String,
@ColumnInfo(name = "tunnel_networks", defaultValue = "")
val tunnelNetworks: Set<String> = setOf(),
@ColumnInfo(name = "is_mobile_data_tunnel", defaultValue = "false")
val isMobileDataTunnel: Boolean = false,
@ColumnInfo(name = "is_primary_tunnel", defaultValue = "false")
val isPrimaryTunnel: Boolean = false,
@ColumnInfo(name = "am_quick", defaultValue = "") val amQuick: String = "",
@ColumnInfo(name = "is_Active", defaultValue = "false") val isActive: Boolean = false,
@ColumnInfo(name = "restart_on_ping_failure", defaultValue = "false")
val restartOnPingFailure: Boolean = false,
@ColumnInfo(name = "ping_target", defaultValue = "null") var pingTarget: String? = null,
@ColumnInfo(name = "is_ethernet_tunnel", defaultValue = "false")
val isEthernetTunnel: Boolean = false,
@ColumnInfo(name = "is_ipv4_preferred", defaultValue = "true")
val isIpv4Preferred: Boolean = true,
@ColumnInfo(name = "position", defaultValue = "0") val position: Int = 0,
@ColumnInfo(name = "auto_tunnel_apps", defaultValue = "[]")
val autoTunnelApps: Set<String> = emptySet(),
@ColumnInfo(name = "is_metered", defaultValue = "false") val isMetered: Boolean = false,
) {
companion object {
const val GLOBAL_CONFIG_NAME = "4675ab06-903a-438b-8485-6ea4187a9512"
}
}
@@ -1,36 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.mapper
import com.zaneschepke.wireguardautotunnel.data.entity.AutoTunnelSettings as Entity
import com.zaneschepke.wireguardautotunnel.domain.model.AutoTunnelSettings as Domain
fun Entity.toDomain(): Domain =
Domain(
id = id,
isAutoTunnelEnabled = isAutoTunnelEnabled,
isTunnelOnMobileDataEnabled = isTunnelOnMobileDataEnabled,
trustedNetworkSSIDs = trustedNetworkSSIDs,
isTunnelOnEthernetEnabled = isTunnelOnEthernetEnabled,
isTunnelOnWifiEnabled = isTunnelOnWifiEnabled,
isWildcardsEnabled = isWildcardsEnabled,
isStopOnNoInternetEnabled = isStopOnNoInternetEnabled,
debounceDelaySeconds = debounceDelaySeconds,
isTunnelOnUnsecureEnabled = isTunnelOnUnsecureEnabled,
wifiDetectionMethod = wifiDetectionMethod,
startOnBoot = startOnBoot,
)
fun Domain.toEntity(): Entity =
Entity(
id = id,
isAutoTunnelEnabled = isAutoTunnelEnabled,
isTunnelOnMobileDataEnabled = isTunnelOnMobileDataEnabled,
trustedNetworkSSIDs = trustedNetworkSSIDs,
isTunnelOnEthernetEnabled = isTunnelOnEthernetEnabled,
isTunnelOnWifiEnabled = isTunnelOnWifiEnabled,
isWildcardsEnabled = isWildcardsEnabled,
isStopOnNoInternetEnabled = isStopOnNoInternetEnabled,
debounceDelaySeconds = debounceDelaySeconds,
isTunnelOnUnsecureEnabled = isTunnelOnUnsecureEnabled,
wifiDetectionMethod = wifiDetectionMethod,
startOnBoot = startOnBoot,
)
@@ -1,20 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.mapper
import com.zaneschepke.wireguardautotunnel.data.entity.DnsSettings as Entity
import com.zaneschepke.wireguardautotunnel.domain.model.DnsSettings as Domain
fun Entity.toDomain(): Domain =
Domain(
id = id,
dnsProtocol = dnsProtocol,
dnsEndpoint = dnsEndpoint,
isGlobalTunnelDnsEnabled = isGlobalTunnelDnsEnabled,
)
fun Domain.toEntity(): Entity =
Entity(
id = id,
dnsProtocol = dnsProtocol,
dnsEndpoint = dnsEndpoint,
isGlobalTunnelDnsEnabled = isGlobalTunnelDnsEnabled,
)
@@ -1,11 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.mapper
import com.zaneschepke.wireguardautotunnel.data.entity.AppState as Entity
import com.zaneschepke.wireguardautotunnel.domain.model.AppState as Domain
fun Entity.toDomain(): Domain =
Domain(
isLocationDisclosureShown = isLocationDisclosureShown,
isBatteryOptimizationDisableShown = isBatteryOptimizationDisableShown,
shouldShowDonationSnackbar = shouldShowDonationSnackbar,
)
@@ -1,19 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.mapper
import com.zaneschepke.wireguardautotunnel.data.entity.GitHubRelease
import com.zaneschepke.wireguardautotunnel.domain.model.AppUpdate
object GitHubReleaseMapper {
fun toAppUpdate(gitHubRelease: GitHubRelease, newVersion: String): AppUpdate {
with(gitHubRelease) {
val apkAsset = assets.firstOrNull { it.name.endsWith(".apk") }
return AppUpdate(
version = newVersion,
title = name ?: "Update $tagName",
releaseNotes = body ?: "No release notes provided",
apkUrl = apkAsset?.browserDownloadUrl,
apkFileName = apkAsset?.name,
)
}
}
}
@@ -1,10 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.mapper
import com.zaneschepke.wireguardautotunnel.data.entity.LockdownSettings as Entity
import com.zaneschepke.wireguardautotunnel.domain.model.LockdownSettings as Domain
fun Entity.toDomain(): Domain =
Domain(id = id, bypassLan = bypassLan, metered = metered, dualStack = dualStack)
fun Domain.toEntity(): Entity =
Entity(id = id, bypassLan = bypassLan, metered = metered, dualStack = dualStack)
@@ -1,28 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.mapper
import com.zaneschepke.wireguardautotunnel.data.entity.MonitoringSettings as Entity
import com.zaneschepke.wireguardautotunnel.domain.model.MonitoringSettings as Domain
fun Entity.toDomain(): Domain =
Domain(
id = id,
isPingEnabled = isPingEnabled,
isPingMonitoringEnabled = isPingMonitoringEnabled,
tunnelPingIntervalSeconds = tunnelPingIntervalSeconds,
tunnelPingAttempts = tunnelPingAttempts,
tunnelPingTimeoutSeconds = tunnelPingTimeoutSeconds,
showDetailedPingStats = showDetailedPingStats,
isLocalLogsEnabled = isLocalLogsEnabled,
)
fun Domain.toEntity(): Entity =
Entity(
id = id,
isPingEnabled = isPingEnabled,
isPingMonitoringEnabled = isPingMonitoringEnabled,
tunnelPingIntervalSeconds = tunnelPingIntervalSeconds,
tunnelPingAttempts = tunnelPingAttempts,
tunnelPingTimeoutSeconds = tunnelPingTimeoutSeconds,
showDetailedPingStats = showDetailedPingStats,
isLocalLogsEnabled = isLocalLogsEnabled,
)
@@ -1,26 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.mapper
import com.zaneschepke.wireguardautotunnel.data.entity.ProxySettings as Entity
import com.zaneschepke.wireguardautotunnel.domain.model.ProxySettings as Domain
fun Entity.toDomain(): Domain =
Domain(
id = id,
socks5ProxyEnabled = socks5ProxyEnabled,
socks5ProxyBindAddress = socks5ProxyBindAddress,
httpProxyEnabled = httpProxyEnabled,
httpProxyBindAddress = httpProxyBindAddress,
proxyUsername = proxyUsername,
proxyPassword = proxyPassword,
)
fun Domain.toEntity(): Entity =
Entity(
id = id,
socks5ProxyEnabled = socks5ProxyEnabled,
socks5ProxyBindAddress = socks5ProxyBindAddress,
httpProxyEnabled = httpProxyEnabled,
httpProxyBindAddress = httpProxyBindAddress,
proxyUsername = proxyUsername,
proxyPassword = proxyPassword,
)

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