mirror of
https://github.com/wgtunnel/android.git
synced 2026-06-02 08:33:40 +02:00
Compare commits
223 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c15943a81 | |||
| 05adf7539f | |||
| a9a49e3421 | |||
| 4dd8241fa1 | |||
| 431b2f9061 | |||
| f822292584 | |||
| 4ffc5d4069 | |||
| 680fbed28c | |||
| 737524831b | |||
| b8c36ac192 | |||
| 547686069f | |||
| ac18ac8274 | |||
| cb983da990 | |||
| cfe64dcb61 | |||
| 2db521d510 | |||
| ff6c763b7b | |||
| ebf7521fa1 | |||
| 7a2d96fcd7 | |||
| c6c8047982 | |||
| 9cfb7250de | |||
| 79b5b039b0 | |||
| 29616f8325 | |||
| 8bbe81d294 | |||
| 571fb1b12c | |||
| 02f6f97aa1 | |||
| 1d74d0984e | |||
| 6448386f76 | |||
| d09e85ba45 | |||
| a9bc1cc7f0 | |||
| 54d9653f04 | |||
| efc66821a6 | |||
| 3af7adc45b | |||
| 5754f2183c | |||
| f7f7f1bd9d | |||
| 57bb3f5e74 | |||
| 49196e7c7b | |||
| 894b63e668 | |||
| e16d44ff20 | |||
| b8b3f3001b | |||
| d142ecea6e | |||
| b793984ede | |||
| ae2532afe5 | |||
| 2720a3b35e | |||
| 2350364543 | |||
| f4172cb1fc | |||
| 90c482ae4f | |||
| 1eb8ad62e0 | |||
| d44baa84a8 | |||
| cb1b8ee7d6 | |||
| 4153351fc4 | |||
| 48e6f341cb | |||
| d531adede5 | |||
| 2df1bb07ab | |||
| a5e60c3fbe | |||
| e4af481402 | |||
| 77b3fc8360 | |||
| 4fd908f271 | |||
| 632da245ab | |||
| 04f22cb92d | |||
| 31194d8b88 | |||
| 421bf418d1 | |||
| e84d7e354c | |||
| 681b066d99 | |||
| ad53fca928 | |||
| f7e4b7e8ef | |||
| b04e8e7f60 | |||
| cbee5cfd1b | |||
| 440fe6ceda | |||
| 27def018bd | |||
| 16979dbb2b | |||
| 5d8190628d | |||
| 6d597a235d | |||
| 61fd2f01b9 | |||
| 914a641977 | |||
| c97a3afbc4 | |||
| fadc2d1562 | |||
| 106ab76b82 | |||
| 48b3f60b37 | |||
| e7f5cee6dd | |||
| 5e01fd6c85 | |||
| dcbd72c6f6 | |||
| f27aa1452a | |||
| 82c3521c58 | |||
| c6535ffe7e | |||
| be5fe94ce6 | |||
| 1e905c1cfb | |||
| a549c111aa | |||
| 5ab344044e | |||
| 7030f68548 | |||
| ecd51b6fe5 | |||
| 3e4f8e0791 | |||
| e940a0dbc5 | |||
| 8e233475df | |||
| 24789826ad | |||
| 787e0d1a71 | |||
| 914ef53ef0 | |||
| a569974beb | |||
| 87bc89b6f1 | |||
| c343220e96 | |||
| 5447ec73f7 | |||
| a2b8eb5b0b | |||
| e37777e662 | |||
| ee3fcabcf1 | |||
| 2a8895ffbc | |||
| b1fdb5b9b2 | |||
| 1d644748e5 | |||
| 61989b596f | |||
| 5946d7c10d | |||
| 4fc8ffbcbb | |||
| c0cff297b2 | |||
| 7ca0db3a40 | |||
| ee8db0a859 | |||
| c8205c4c59 | |||
| 3247e94358 | |||
| 2690ce29e1 | |||
| 500b85f687 | |||
| 84b2b75271 | |||
| 0197198f7b | |||
| 0b271778c9 | |||
| 6427b2f832 | |||
| 097097f620 | |||
| 20dfaed8de | |||
| 07aa37fc2a | |||
| 091cd2e028 | |||
| 7efa6d0bf4 | |||
| e31fb01410 | |||
| 76674323e7 | |||
| f1fc9ca6f7 | |||
| cb301e74eb | |||
| 8141fe19be | |||
| 0fdb3d0b31 | |||
| d9f3a21cc3 | |||
| fec84bc6ac | |||
| d6ee36edc0 | |||
| e3fcf712d5 | |||
| 5a15776bb3 | |||
| 3339448424 | |||
| 7ec294b789 | |||
| e59c72788d | |||
| 62435d549c | |||
| 96800037d1 | |||
| f5b3bb1cb7 | |||
| 654b4a4719 | |||
| a19b5ce22a | |||
| 86f592255c | |||
| 4a5dd76b5b | |||
| 1139d17f13 | |||
| 36855319a2 | |||
| 61e3751321 | |||
| dd16bd977f | |||
| aeb4a13389 | |||
| f0ec661223 | |||
| ffa7a207fb | |||
| 515e91d191 | |||
| 16e65aec9f | |||
| ff87bee3b4 | |||
| 9739d35eda | |||
| 11ad494fbb | |||
| 90b006acc5 | |||
| eb7b39c379 | |||
| 0a17593310 | |||
| c0e58125dd | |||
| 3791261f91 | |||
| d1e61be3ae | |||
| afd4fb127f | |||
| e0cce8fba4 | |||
| b70ecbdfff | |||
| 513d08998b | |||
| 79583e0e61 | |||
| 75790ec6d5 | |||
| a1941b7229 | |||
| 37bae82700 | |||
| 77cd328a71 | |||
| 5a1430706b | |||
| 321730536d | |||
| 2912238f27 | |||
| bc7daacd13 | |||
| c8f2cfd758 | |||
| ca3f3fd439 | |||
| 235170508b | |||
| 11aea3f1c4 | |||
| 7fbc51af4c | |||
| 1714618f0c | |||
| 7cb798a111 | |||
| f8bc264f30 | |||
| 2174c3f48c | |||
| 413b9a37df | |||
| 6c30c6bae6 | |||
| 3c5aff31aa | |||
| 991d1224ab | |||
| 69b07eec6f | |||
| e81066f508 | |||
| 64bb9f3b82 | |||
| c1b560e822 | |||
| 14fe5821cc | |||
| 9d9b7bebca | |||
| 12d1ccc084 | |||
| 20cc2c09b0 | |||
| eeccc71469 | |||
| 0e64bbb4e1 | |||
| f513297ba0 | |||
| 135f8c0459 | |||
| 7a811f4152 | |||
| 2abf681d17 | |||
| 689c97f452 | |||
| 08d11a53b4 | |||
| 9952e97e1c | |||
| 4cdc974778 | |||
| e31a4c03cd | |||
| 5b94f22359 | |||
| c673a8dc91 | |||
| f6612abe28 | |||
| 7ca5de1836 | |||
| 509d22a98c | |||
| 68b0902398 | |||
| 0c45558293 | |||
| 89212fe191 | |||
| a98a47f54d | |||
| cb3a7d024b | |||
| 259be21b04 | |||
| 8b81831910 | |||
| 49bf7fa8b9 | |||
| 2f53a8f3b4 |
@@ -0,0 +1,85 @@
|
||||
[{*.kt,*.kts}]
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
max_line_length = 100
|
||||
indent_size = 4
|
||||
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_allow_trailing_comma = true
|
||||
ij_kotlin_allow_trailing_comma_on_call_site = true
|
||||
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_call_parameters_wrap = on_every_item
|
||||
ij_kotlin_catch_on_new_line = false
|
||||
ij_kotlin_class_annotation_wrap = split_into_lines
|
||||
ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL
|
||||
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_method_parameters_wrap = on_every_item
|
||||
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
|
||||
@@ -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>.
|
||||
@@ -0,0 +1,2 @@
|
||||
ko_fi: zaneschepke
|
||||
liberapay: zaneschepke
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: "[BUG] - Problem with app"
|
||||
labels: bug
|
||||
assignees: zaneschepke
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
|
||||
- Device: [e.g. Pixel 4a]
|
||||
- Android Version: [e.g. Android 13]
|
||||
- App Version [e.g. 3.3.3]
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots (Only if necessary)**
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: "[FEATURE] - New feature request"
|
||||
labels: enhancement
|
||||
assignees: zaneschepke
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -0,0 +1,17 @@
|
||||
# Support
|
||||
|
||||
If you are experiencing issues with the app, the following resources are available to help you.
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
See the app docs site <a href="https://zaneschepke.com/wgtunnel-docs/overview.html">here</a> (work in progress).
|
||||
</li>
|
||||
<li>
|
||||
Chat with me and our community on Discord <a href="https://discord.gg/rbRRNh6H7V">here</a>, or open an issue on GitHub <a href="https://github.com/zaneschepke/wgtunnel/issues/new/choose">here</a>.
|
||||
</li>
|
||||
<li>
|
||||
Send me an email <a href="mailto:zanecschepke@gmail.com">here</a>.
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
Thank you for using WG Tunnel.
|
||||
@@ -0,0 +1,10 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: daily
|
||||
- package-ecosystem: gradle
|
||||
directory: /
|
||||
schedule:
|
||||
interval: daily
|
||||
@@ -0,0 +1,20 @@
|
||||
name: Issue Updates Workflow
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [ opened, closed, reopened ]
|
||||
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
name: Build
|
||||
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 }}"
|
||||
@@ -0,0 +1,21 @@
|
||||
name: Release Updates Workflow
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [ published ]
|
||||
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
name: Build
|
||||
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 }}"
|
||||
@@ -0,0 +1,211 @@
|
||||
name: release-android
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "4 3 * * *"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
track:
|
||||
type: choice
|
||||
description: "Google play release track"
|
||||
options:
|
||||
- none
|
||||
- internal
|
||||
- alpha
|
||||
- beta
|
||||
- production
|
||||
default: alpha
|
||||
required: true
|
||||
release_type:
|
||||
type: choice
|
||||
description: "GitHub release type"
|
||||
options:
|
||||
- none
|
||||
- prerelease
|
||||
- nightly
|
||||
- release
|
||||
default: release
|
||||
required: true
|
||||
tag_name:
|
||||
description: "Tag name for release"
|
||||
required: false
|
||||
default: nightly
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Signed APK
|
||||
if: ${{ inputs.release_type != 'none' }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
|
||||
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
|
||||
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
|
||||
KEY_STORE_FILE: 'android_keystore.jks'
|
||||
KEY_STORE_LOCATION: ${{ github.workspace }}/app/keystore/
|
||||
GH_USER: ${{ secrets.GH_USER }}
|
||||
# GH needed for gh cli
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
cache: gradle
|
||||
- name: Grant execute permission for gradlew
|
||||
run: chmod +x gradlew
|
||||
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt update && sudo apt install -y gh apksigner
|
||||
|
||||
# Here we need to decode keystore.jks from base64 string and place it
|
||||
# in the folder specified in the release signing configuration
|
||||
- name: Decode Keystore
|
||||
id: decode_keystore
|
||||
uses: timheuer/base64-to-file@v1.2
|
||||
with:
|
||||
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
|
||||
run: |
|
||||
store_path=${{ env.KEY_STORE_LOCATION }}${{ env.KEY_STORE_FILE }}
|
||||
echo "KEY_STORE_PATH=$store_path" >> $GITHUB_ENV
|
||||
|
||||
- name: Create service_account.json
|
||||
id: createServiceAccount
|
||||
run: echo '${{ secrets.SERVICE_ACCOUNT_JSON }}' > service_account.json
|
||||
|
||||
# Build and sign APK ("-x test" argument is used to skip tests)
|
||||
# add fdroid flavor for apk upload
|
||||
- name: Build Fdroid Release APK
|
||||
if: ${{ inputs.release_type != '' && inputs.release_type != 'nightly' }}
|
||||
run: ./gradlew :app:assembleFdroidRelease -x test
|
||||
|
||||
|
||||
- name: Build Fdroid Nightly APK
|
||||
if: ${{ inputs.release_type == '' || inputs.release_type == 'nightly' }}
|
||||
run: ./gradlew :app:assembleFdroidNightly -x test
|
||||
|
||||
- if: ${{ inputs.release_type == '' || inputs.release_type == 'nightly' }}
|
||||
run: echo "APK_PATH=$(find . -regex '^.*/build/outputs/apk/fdroid/nightly/.*\.apk$' -type f | head -1)" >> $GITHUB_ENV
|
||||
- if: ${{ inputs.release_type != '' && inputs.release_type != 'nightly' }}
|
||||
run: echo "APK_PATH=$(find . -regex '^.*/build/outputs/apk/fdroid/release/.*\.apk$' -type f | head -1)" >> $GITHUB_ENV
|
||||
|
||||
- name: Get version code
|
||||
if: ${{ inputs.release_type == 'release' || inputs.release_type == 'prerelease' }}
|
||||
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
|
||||
|
||||
# Save the APK after the Build job is complete to publish it as a Github release in the next job
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@v4.3.4
|
||||
with:
|
||||
name: wgtunnel
|
||||
path: ${{ env.APK_PATH }}
|
||||
|
||||
- name: Download APK from build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: wgtunnel
|
||||
|
||||
- name: Repository Dispatch for my F-Droid repo
|
||||
uses: peter-evans/repository-dispatch@v3
|
||||
if: ${{ inputs.release_type == 'release' }}
|
||||
with:
|
||||
token: ${{ secrets.PAT }}
|
||||
repository: zaneschepke/fdroid
|
||||
event-type: fdroid-update
|
||||
|
||||
- name: Set version release notes
|
||||
if: ${{ inputs.release_type == 'release' || inputs.release_type == 'prerelease' }}
|
||||
run: |
|
||||
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
|
||||
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
|
||||
|
||||
# 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: On nightly release
|
||||
if: ${{ contains(env.TAG_NAME, 'nightly') }}
|
||||
run: |
|
||||
echo "RELEASE_NOTES=Nightly build of the latest development version of the android client." >> $GITHUB_ENV
|
||||
gh release delete nightly --yes || true
|
||||
|
||||
- name: Get checksum
|
||||
id: checksum
|
||||
run: echo "checksum=$(apksigner verify -print-certs ${{ env.APK_PATH }} | grep -Po "(?<=SHA-256 digest:) .*" | tr -d "[:blank:]")" >> $GITHUB_OUTPUT
|
||||
|
||||
|
||||
- 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 }}
|
||||
|
||||
SHA256 fingerprint:
|
||||
```${{ steps.checksum.outputs.checksum }}```
|
||||
tag_name: ${{ env.TAG_NAME }}
|
||||
name: ${{ env.TAG_NAME }}
|
||||
draft: false
|
||||
prerelease: ${{ inputs.release_type == 'prerelease' || inputs.release_type == '' || inputs.release_type == 'nightly' }}
|
||||
make_latest: ${{ inputs.release_type == 'release' }}
|
||||
files: ${{ github.workspace }}/${{ env.APK_PATH }}
|
||||
|
||||
publish-play:
|
||||
if: ${{ inputs.track != 'none' && inputs.track != '' }}
|
||||
name: Publish to Google Play
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
|
||||
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
|
||||
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
|
||||
KEY_STORE_FILE: 'android_keystore.jks'
|
||||
KEY_STORE_LOCATION: ${{ github.workspace }}/app/keystore/
|
||||
GH_USER: ${{ secrets.GH_USER }}
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
cache: gradle
|
||||
|
||||
- name: Grant execute permission for gradlew
|
||||
run: chmod +x gradlew
|
||||
|
||||
- name: Deploy with fastlane
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: '3.2' # Not needed with a .ruby-version file
|
||||
bundler-cache: true
|
||||
|
||||
- name: Distribute app to Prod track 🚀
|
||||
run: (cd ${{ github.workspace }} && bundle install && bundle exec fastlane ${{ inputs.track }})
|
||||
|
||||
@@ -69,3 +69,5 @@ lint/tmp/
|
||||
# App Specific cases
|
||||
app/release/output.json
|
||||
.idea/codeStyles/
|
||||
# where we keep our signing secrets locally
|
||||
app/signing.properties
|
||||
|
||||
@@ -2,58 +2,109 @@
|
||||
WG Tunnel
|
||||
</h1>
|
||||
|
||||
<span align="center">
|
||||
<div align="center">
|
||||
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://discord.gg/Ad5fuEts)
|
||||
[](https://discord.gg/rbRRNh6H7V)
|
||||
[](https://twitter.com/i/communities/1780655267685736818)
|
||||
[](https://t.me/wgtunnel)
|
||||
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span align="center">
|
||||
<div align="center">
|
||||
|
||||
|
||||
[](https://play.google.com/store/apps/details?id=com.zaneschepke.wireguardautotunnel)
|
||||
[](https://f-droid.org/packages/com.zaneschepke.wireguardautotunnel/)
|
||||
|
||||
</span>
|
||||
|
||||
<span align="left">
|
||||
</div>
|
||||
|
||||
This is an alternative Android Application for [WireGuard](https://www.wireguard.com/) with added features. Built using the [wireguard-android](https://github.com/WireGuard/wireguard-android) library and [Jetpack Compose](https://developer.android.com/jetpack/compose), this application was inspired by the official [WireGuard Android](https://github.com/WireGuard/wireguard-android) app.
|
||||
|
||||
</span>
|
||||
<div align="left">
|
||||
|
||||
<span align="center">
|
||||
This is an alternative Android Application for [WireGuard](https://www.wireguard.com/)
|
||||
and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) with added
|
||||
features. Built using the [wireguard-android](https://github.com/WireGuard/wireguard-android)
|
||||
library and [Jetpack Compose](https://developer.android.com/jetpack/compose), this application was
|
||||
inspired by the official [WireGuard Android](https://github.com/WireGuard/wireguard-android) app.
|
||||
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
|
||||
## Screenshots
|
||||
|
||||
<p float="center">
|
||||
<img label="Main" style="padding-right:25px" src="./asset/main_screen.png" width="200" />
|
||||
<img label="Settings" style="padding-left:25px" src="./asset/settings_screen.png" width="200" />
|
||||
<img label="Support" style="padding-left:25px" src="./asset/support_screen.png" width="200" />
|
||||
<img label="Main" style="padding-right:25px" src="fastlane/metadata/android/en-US/images/phoneScreenshots/main_screen.png" width="200" />
|
||||
<img label="Config" style="padding-left:25px" src="fastlane/metadata/android/en-US/images/phoneScreenshots/config_screen.png" width="200" />
|
||||
<img label="Settings" style="padding-left:25px" src="fastlane/metadata/android/en-US/images/phoneScreenshots/settings_screen.png" width="200" />
|
||||
<img label="Support" style="padding-left:25px" src="fastlane/metadata/android/en-US/images/phoneScreenshots/support_screen.png" width="200" />
|
||||
</p>
|
||||
|
||||
<span align="left">
|
||||
<div align="left">
|
||||
|
||||
## Inspiration
|
||||
|
||||
The inspiration for this app came from the inconvenience of constantly having to turn VPN off and on while on different networks. With there being no free solution to this problem, this app was created to meet that need.
|
||||
The original inspiration for this app came from the inconvenience of having to manually turn VPN off
|
||||
and on while on different networks. This app was created to offer a free solution to this problem.
|
||||
|
||||
## Features
|
||||
|
||||
* Add tunnels via .conf file
|
||||
* Auto connect to VPN based on Wi-Fi SSID
|
||||
* Configurable Trusted Network list
|
||||
* Optional auto connect on mobile data
|
||||
* Automatic service restart after reboot
|
||||
* Service will stay running in background after app has been closed
|
||||
* Add tunnels via .conf file, zip, manual entry, or QR code
|
||||
* Auto connect to tunnels based on Wi-Fi SSID, ethernet, or mobile data
|
||||
* Split tunneling by application with search
|
||||
* WireGuard support for kernel and userspace modes
|
||||
* Amnezia support for userspace mode for DPI/censorship protection
|
||||
* Always-On VPN support
|
||||
* Export Amnezia and WireGuard tunnels to zip
|
||||
* Quick tile support for tunnel toggling, auto-tunneling
|
||||
* Static shortcuts support for tunnel toggling, auto-tunneling
|
||||
* Intent automation support for all tunnels
|
||||
* Automatic auto-tunneling service restart after reboot
|
||||
* Automatic tunnel restart after reboot
|
||||
* Battery preservation measures
|
||||
* Restart tunnel on ping failure (beta)
|
||||
|
||||
## Fdroid
|
||||
|
||||
Want updates faster?
|
||||
|
||||
Check out my personal [fdroid repository](https://github.com/zaneschepke/fdroid) to get updates the
|
||||
moment they are released.
|
||||
|
||||
## Docs
|
||||
|
||||
Information about features, behaviors, and answers to common questions can be found in the
|
||||
app [documentation](https://zaneschepke.com/wgtunnel-docs/overview.html).
|
||||
|
||||
The repository for these docs can be found [here](https://github.com/zaneschepke/wgtunnel-docs).
|
||||
|
||||
## Translation
|
||||
|
||||
This app is using [Weblate](https://weblate.org) to assist with translations.
|
||||
|
||||
Help translate WG Tunnel into your language
|
||||
at [Hosted Weblate](https://hosted.weblate.org/engage/wg-tunnel/).\
|
||||
[](https://hosted.weblate.org/engage/wg-tunnel/)
|
||||
|
||||
## Building
|
||||
|
||||
|
||||
```
|
||||
$ git clone https://github.com/zaneschepke/wgtunnel
|
||||
$ cd wgtunnel
|
||||
$ ./gradlew assembleRelease
|
||||
```
|
||||
|
||||
</span>
|
||||
And then build the app:
|
||||
|
||||
```
|
||||
$ ./gradlew assembleDebug
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Any contributions in the form of feedback, issues, code, or translations are welcome and much
|
||||
appreciated!
|
||||
|
||||
Please read
|
||||
the [code of conduct](https://github.com/zaneschepke/wgtunnel?tab=coc-ov-file#contributor-code-of-conduct)
|
||||
before contributing.
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please report security issues to `support@zaneschepke.com`
|
||||
+148
-80
@@ -1,126 +1,194 @@
|
||||
val rExtra = rootProject.extra
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
kotlin("kapt")
|
||||
id("com.google.dagger.hilt.android")
|
||||
id("io.objectbox")
|
||||
id("com.google.gms.google-services")
|
||||
id("com.google.firebase.crashlytics")
|
||||
id("org.jetbrains.kotlin.plugin.serialization")
|
||||
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)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.zaneschepke.wireguardautotunnel"
|
||||
compileSdk = 33
|
||||
namespace = Constants.APP_ID
|
||||
compileSdk = Constants.TARGET_SDK
|
||||
compileSdkPreview = "VanillaIceCream"
|
||||
|
||||
val versionMajor = 1
|
||||
val versionMinor = 1
|
||||
val versionPatch = 5
|
||||
val versionBuild = 0
|
||||
androidResources {
|
||||
generateLocaleConfig = true
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.zaneschepke.wireguardautotunnel"
|
||||
minSdk = 29
|
||||
targetSdk = 33
|
||||
versionCode = versionMajor * 10000 + versionMinor * 1000 + versionPatch * 100 + versionBuild
|
||||
versionName = "${versionMajor}.${versionMinor}.${versionPatch}"
|
||||
applicationId = Constants.APP_ID
|
||||
minSdk = Constants.MIN_SDK
|
||||
targetSdk = Constants.TARGET_SDK
|
||||
versionCode = Constants.VERSION_CODE
|
||||
versionName = Constants.VERSION_NAME
|
||||
|
||||
ksp { arg("room.schemaLocation", "$projectDir/schemas") }
|
||||
|
||||
sourceSets {
|
||||
getByName("debug").assets.srcDirs(files("$projectDir/schemas")) // Room
|
||||
}
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
useSupportLibrary = true
|
||||
vectorDrawables { useSupportLibrary = true }
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
create(Constants.RELEASE) {
|
||||
storeFile = getStoreFile()
|
||||
storePassword = getSigningProperty(Constants.STORE_PASS_VAR)
|
||||
keyAlias = getSigningProperty(Constants.KEY_ALIAS_VAR)
|
||||
keyPassword = getSigningProperty(Constants.KEY_PASS_VAR)
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
// don't strip
|
||||
packaging.jniLibs.keepDebugSymbols.addAll(
|
||||
listOf("libwg-go.so", "libwg-quick.so", "libwg.so"),
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
release {
|
||||
isDebuggable = false
|
||||
isMinifyEnabled = false
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
"proguard-rules.pro",
|
||||
)
|
||||
signingConfig = signingConfigs.getByName(Constants.RELEASE)
|
||||
}
|
||||
debug { isDebuggable = true }
|
||||
|
||||
create(Constants.NIGHTLY) {
|
||||
initWith(getByName("release"))
|
||||
defaultConfig.versionName = nightlyVersionName()
|
||||
defaultConfig.versionCode = nightlyVersionCode()
|
||||
}
|
||||
}
|
||||
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 = "17"
|
||||
}
|
||||
kotlinOptions { jvmTarget = Constants.JVM_TARGET }
|
||||
buildFeatures {
|
||||
compose = true
|
||||
buildConfig = true
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.4.7"
|
||||
}
|
||||
packaging {
|
||||
resources {
|
||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||
}
|
||||
}
|
||||
packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } }
|
||||
}
|
||||
|
||||
val generalImplementation by configurations
|
||||
|
||||
dependencies {
|
||||
implementation("androidx.core:core-ktx:1.10.1")
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1")
|
||||
implementation("androidx.activity:activity-compose:1.7.2")
|
||||
implementation(platform("androidx.compose:compose-bom:2023.03.00"))
|
||||
implementation("androidx.compose.ui:ui")
|
||||
implementation("androidx.compose.ui:ui-graphics")
|
||||
implementation("androidx.compose.ui:ui-tooling-preview")
|
||||
implementation("androidx.compose.material3:material3")
|
||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
||||
androidTestImplementation(platform("androidx.compose:compose-bom:2023.03.00"))
|
||||
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
|
||||
debugImplementation("androidx.compose.ui:ui-tooling")
|
||||
debugImplementation("androidx.compose.ui:ui-test-manifest")
|
||||
implementation(project(":logcatter"))
|
||||
|
||||
//wireguard tunnel
|
||||
implementation("com.wireguard.android:tunnel:1.0.20230405")
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
|
||||
// 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)
|
||||
|
||||
// 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)
|
||||
|
||||
// get tunnel lib from github packages or mavenLocal
|
||||
implementation(libs.tunnel)
|
||||
implementation(libs.amneziawg.android)
|
||||
coreLibraryDesugaring(libs.desugar.jdk.libs)
|
||||
|
||||
// logging
|
||||
implementation(libs.timber)
|
||||
|
||||
//logging
|
||||
implementation("com.jakewharton.timber:timber:5.0.1")
|
||||
|
||||
// compose navigation
|
||||
implementation("androidx.navigation:navigation-compose:2.5.3")
|
||||
implementation("androidx.hilt:hilt-navigation-compose:1.0.0")
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.androidx.hilt.navigation.compose)
|
||||
|
||||
implementation(libs.zaneschepke.multifab)
|
||||
|
||||
// hilt
|
||||
implementation("com.google.dagger:hilt-android:${rExtra.get("hiltVersion")}")
|
||||
kapt("com.google.dagger:hilt-android-compiler:${rExtra.get("hiltVersion")}")
|
||||
implementation(libs.hilt.android)
|
||||
ksp(libs.hilt.android.compiler)
|
||||
|
||||
//accompanist
|
||||
implementation("com.google.accompanist:accompanist-systemuicontroller:${rExtra.get("accompanistVersion")}")
|
||||
implementation("com.google.accompanist:accompanist-permissions:${rExtra.get("accompanistVersion")}")
|
||||
implementation("com.google.accompanist:accompanist-flowlayout:${rExtra.get("accompanistVersion")}")
|
||||
implementation("com.google.accompanist:accompanist-navigation-animation:${rExtra.get("accompanistVersion")}")
|
||||
// accompanist
|
||||
implementation(libs.accompanist.permissions)
|
||||
implementation(libs.accompanist.flowlayout)
|
||||
implementation(libs.accompanist.drawablepainter)
|
||||
|
||||
//db
|
||||
implementation("io.objectbox:objectbox-kotlin:${rExtra.get("objectBoxVersion")}")
|
||||
// storage
|
||||
implementation(libs.androidx.room.runtime)
|
||||
ksp(libs.androidx.room.compiler)
|
||||
implementation(libs.androidx.room.ktx)
|
||||
implementation(libs.androidx.datastore.preferences)
|
||||
|
||||
//lifecycle
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.1")
|
||||
// lifecycle
|
||||
implementation(libs.lifecycle.runtime.compose)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.lifecycle.process)
|
||||
|
||||
//icons
|
||||
implementation("androidx.compose.material:material-icons-extended:1.4.3")
|
||||
// icons
|
||||
implementation(libs.material.icons.extended)
|
||||
// serialization
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
|
||||
// barcode scanning
|
||||
implementation(libs.zxing.android.embedded)
|
||||
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
|
||||
// bio
|
||||
implementation(libs.androidx.biometric.ktx)
|
||||
implementation(libs.pin.lock.compose)
|
||||
|
||||
// shortcuts
|
||||
implementation(libs.androidx.core)
|
||||
implementation(libs.androidx.core.google.shortcuts)
|
||||
|
||||
//firebase crashlytics
|
||||
implementation(platform("com.google.firebase:firebase-bom:32.0.0"))
|
||||
implementation("com.google.firebase:firebase-crashlytics-ktx")
|
||||
implementation("com.google.firebase:firebase-analytics-ktx")
|
||||
|
||||
// splash
|
||||
implementation(libs.androidx.core.splashscreen)
|
||||
}
|
||||
|
||||
fun nightlyVersionCode() : Int {
|
||||
return Constants.VERSION_CODE + Constants.NIGHTLY_CODE
|
||||
}
|
||||
|
||||
fun nightlyVersionName() : String {
|
||||
return Constants.VERSION_NAME + "-${grgitService.service.get().grgit.head().abbreviatedId}"
|
||||
}
|
||||
kapt {
|
||||
correctErrorTypes = true
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
-dontwarn com.google.errorprone.annotations.**
|
||||
|
||||
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
|
||||
<fields>;
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
{
|
||||
"project_info": {
|
||||
"project_number": "328300975830",
|
||||
"project_id": "wireguard-auto-tunnel",
|
||||
"storage_bucket": "wireguard-auto-tunnel.appspot.com"
|
||||
},
|
||||
"client": [
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:328300975830:android:82cd774598ccb7234b1b77",
|
||||
"android_client_info": {
|
||||
"package_name": "com.zaneschepke.wireguardautotunnel"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "328300975830-m72lc3hr69ddhdqh9ngr27rvc8o0jb2d.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyBsSMY0LlckizXDnuYBy7nXWGSdl8zZedI"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": [
|
||||
{
|
||||
"client_id": "328300975830-m72lc3hr69ddhdqh9ngr27rvc8o0jb2d.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"configuration_version": "1"
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
{
|
||||
"_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.",
|
||||
"_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.",
|
||||
"_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.",
|
||||
"entities": [
|
||||
{
|
||||
"id": "1:2692736974585027589",
|
||||
"lastPropertyId": "15:5057486545428188436",
|
||||
"name": "TunnelConfig",
|
||||
"properties": [
|
||||
{
|
||||
"id": "1:1985347930017457084",
|
||||
"name": "id",
|
||||
"type": 6,
|
||||
"flags": 1
|
||||
},
|
||||
{
|
||||
"id": "12:2409068226744965585",
|
||||
"name": "name",
|
||||
"indexId": "1:4811206443952699137",
|
||||
"type": 9,
|
||||
"flags": 34848
|
||||
},
|
||||
{
|
||||
"id": "13:8987443291286312275",
|
||||
"name": "wgQuick",
|
||||
"type": 9
|
||||
}
|
||||
],
|
||||
"relations": []
|
||||
},
|
||||
{
|
||||
"id": "2:8887605597748372702",
|
||||
"lastPropertyId": "8:4981008812459251156",
|
||||
"name": "Settings",
|
||||
"properties": [
|
||||
{
|
||||
"id": "1:7485739868216068651",
|
||||
"name": "id",
|
||||
"type": 6,
|
||||
"flags": 1
|
||||
},
|
||||
{
|
||||
"id": "2:5814013113141456749",
|
||||
"name": "isAutoTunnelEnabled",
|
||||
"type": 1
|
||||
},
|
||||
{
|
||||
"id": "4:5645665441196906014",
|
||||
"name": "trustedNetworkSSIDs",
|
||||
"type": 30
|
||||
},
|
||||
{
|
||||
"id": "5:4989886999117763881",
|
||||
"name": "isTunnelOnMobileDataEnabled",
|
||||
"type": 1
|
||||
},
|
||||
{
|
||||
"id": "6:3370284381040192129",
|
||||
"name": "defaultTunnel",
|
||||
"type": 9
|
||||
}
|
||||
],
|
||||
"relations": []
|
||||
}
|
||||
],
|
||||
"lastEntityId": "2:8887605597748372702",
|
||||
"lastIndexId": "1:4811206443952699137",
|
||||
"lastRelationId": "0:0",
|
||||
"lastSequenceId": "0:0",
|
||||
"modelVersion": 5,
|
||||
"modelVersionParserMinimum": 5,
|
||||
"retiredEntityUids": [],
|
||||
"retiredIndexUids": [],
|
||||
"retiredPropertyUids": [
|
||||
1763475292291320186,
|
||||
6483820955437198310,
|
||||
8323071516033820771,
|
||||
5904440563612311217,
|
||||
1408037976996390989,
|
||||
7737847485212546994,
|
||||
8215616901775229364,
|
||||
8021610768066328637,
|
||||
6174306582797008721,
|
||||
2175939938544485767,
|
||||
7555225587864607050,
|
||||
969146862000617878,
|
||||
5057486545428188436,
|
||||
2814640993034665120,
|
||||
4981008812459251156
|
||||
],
|
||||
"retiredRelationUids": [],
|
||||
"version": 1
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
{
|
||||
"_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.",
|
||||
"_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.",
|
||||
"_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.",
|
||||
"entities": [
|
||||
{
|
||||
"id": "1:2692736974585027589",
|
||||
"lastPropertyId": "15:5057486545428188436",
|
||||
"name": "TunnelConfig",
|
||||
"properties": [
|
||||
{
|
||||
"id": "1:1985347930017457084",
|
||||
"name": "id",
|
||||
"type": 6,
|
||||
"flags": 1
|
||||
},
|
||||
{
|
||||
"id": "12:2409068226744965585",
|
||||
"name": "name",
|
||||
"indexId": "1:4811206443952699137",
|
||||
"type": 9,
|
||||
"flags": 34848
|
||||
},
|
||||
{
|
||||
"id": "13:8987443291286312275",
|
||||
"name": "wgQuick",
|
||||
"type": 9
|
||||
}
|
||||
],
|
||||
"relations": []
|
||||
},
|
||||
{
|
||||
"id": "2:8887605597748372702",
|
||||
"lastPropertyId": "8:4981008812459251156",
|
||||
"name": "Settings",
|
||||
"properties": [
|
||||
{
|
||||
"id": "1:7485739868216068651",
|
||||
"name": "id",
|
||||
"type": 6,
|
||||
"flags": 1
|
||||
},
|
||||
{
|
||||
"id": "2:5814013113141456749",
|
||||
"name": "isAutoTunnelEnabled",
|
||||
"type": 1
|
||||
},
|
||||
{
|
||||
"id": "4:5645665441196906014",
|
||||
"name": "trustedNetworkSSIDs",
|
||||
"type": 30
|
||||
},
|
||||
{
|
||||
"id": "5:4989886999117763881",
|
||||
"name": "isTunnelOnMobileDataEnabled",
|
||||
"type": 1
|
||||
},
|
||||
{
|
||||
"id": "6:3370284381040192129",
|
||||
"name": "defaultTunnel",
|
||||
"type": 9
|
||||
},
|
||||
{
|
||||
"id": "8:4981008812459251156",
|
||||
"name": "showProminentDisclosure",
|
||||
"type": 1
|
||||
}
|
||||
],
|
||||
"relations": []
|
||||
}
|
||||
],
|
||||
"lastEntityId": "2:8887605597748372702",
|
||||
"lastIndexId": "1:4811206443952699137",
|
||||
"lastRelationId": "0:0",
|
||||
"lastSequenceId": "0:0",
|
||||
"modelVersion": 5,
|
||||
"modelVersionParserMinimum": 5,
|
||||
"retiredEntityUids": [],
|
||||
"retiredIndexUids": [],
|
||||
"retiredPropertyUids": [
|
||||
1763475292291320186,
|
||||
6483820955437198310,
|
||||
8323071516033820771,
|
||||
5904440563612311217,
|
||||
1408037976996390989,
|
||||
7737847485212546994,
|
||||
8215616901775229364,
|
||||
8021610768066328637,
|
||||
6174306582797008721,
|
||||
2175939938544485767,
|
||||
7555225587864607050,
|
||||
969146862000617878,
|
||||
5057486545428188436,
|
||||
2814640993034665120
|
||||
],
|
||||
"retiredRelationUids": [],
|
||||
"version": 1
|
||||
}
|
||||
Vendored
+4
-1
@@ -18,4 +18,7 @@
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
#-renamesourcefileattribute SourceFile
|
||||
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
|
||||
<fields>;
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 1,
|
||||
"identityHash": "ba86153e6fb0b823197b987239b03e64",
|
||||
"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, `default_tunnel` TEXT, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL)",
|
||||
"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": "defaultTunnel",
|
||||
"columnName": "default_tunnel",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAlwaysOnVpnEnabled",
|
||||
"columnName": "is_always_on_vpn_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnEthernetEnabled",
|
||||
"columnName": "is_tunnel_on_ethernet_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"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)",
|
||||
"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
|
||||
}
|
||||
],
|
||||
"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`)"
|
||||
}
|
||||
],
|
||||
"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, 'ba86153e6fb0b823197b987239b03e64')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 2,
|
||||
"identityHash": "65b1c9efff61712231fa64d1f19f3915",
|
||||
"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, `default_tunnel` TEXT, `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_battery_saver_enabled` INTEGER NOT NULL DEFAULT false)",
|
||||
"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": "defaultTunnel",
|
||||
"columnName": "default_tunnel",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"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": "isBatterySaverEnabled",
|
||||
"columnName": "is_battery_saver_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"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)",
|
||||
"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
|
||||
}
|
||||
],
|
||||
"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`)"
|
||||
}
|
||||
],
|
||||
"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, '65b1c9efff61712231fa64d1f19f3915')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 3,
|
||||
"identityHash": "6b30daba29bb95f8ddc4d26206329d4f",
|
||||
"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, `default_tunnel` TEXT, `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_battery_saver_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false)",
|
||||
"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": "defaultTunnel",
|
||||
"columnName": "default_tunnel",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"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": "isBatterySaverEnabled",
|
||||
"columnName": "is_battery_saver_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnWifiEnabled",
|
||||
"columnName": "is_tunnel_on_wifi_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"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)",
|
||||
"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
|
||||
}
|
||||
],
|
||||
"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`)"
|
||||
}
|
||||
],
|
||||
"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, '6b30daba29bb95f8ddc4d26206329d4f')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 4,
|
||||
"identityHash": "aee55639422df8dadfe74c3bad204477",
|
||||
"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, `default_tunnel` TEXT, `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_battery_saver_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)",
|
||||
"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": "defaultTunnel",
|
||||
"columnName": "default_tunnel",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"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": "isBatterySaverEnabled",
|
||||
"columnName": "is_battery_saver_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"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"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)",
|
||||
"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
|
||||
}
|
||||
],
|
||||
"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`)"
|
||||
}
|
||||
],
|
||||
"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, 'aee55639422df8dadfe74c3bad204477')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 5,
|
||||
"identityHash": "bc15003a44746e18b9c260ec49737089",
|
||||
"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, `default_tunnel` TEXT, `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_battery_saver_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_auto_tunnel_paused` INTEGER NOT NULL DEFAULT false)",
|
||||
"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": "defaultTunnel",
|
||||
"columnName": "default_tunnel",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"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": "isBatterySaverEnabled",
|
||||
"columnName": "is_battery_saver_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": "isAutoTunnelPaused",
|
||||
"columnName": "is_auto_tunnel_paused",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"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)",
|
||||
"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
|
||||
}
|
||||
],
|
||||
"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`)"
|
||||
}
|
||||
],
|
||||
"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, 'bc15003a44746e18b9c260ec49737089')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 6,
|
||||
"identityHash": "625820076477aca948536f7bccccc7ca",
|
||||
"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, `default_tunnel` TEXT, `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_battery_saver_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_auto_tunnel_paused` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false)",
|
||||
"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": "defaultTunnel",
|
||||
"columnName": "default_tunnel",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"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": "isBatterySaverEnabled",
|
||||
"columnName": "is_battery_saver_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": "isAutoTunnelPaused",
|
||||
"columnName": "is_auto_tunnel_paused",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isPingEnabled",
|
||||
"columnName": "is_ping_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"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)",
|
||||
"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
|
||||
}
|
||||
],
|
||||
"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`)"
|
||||
}
|
||||
],
|
||||
"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, '625820076477aca948536f7bccccc7ca')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 7,
|
||||
"identityHash": "e65e4e7cf01f50fb03196d47b54288b1",
|
||||
"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_auto_tunnel_paused` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false)",
|
||||
"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": "isAutoTunnelPaused",
|
||||
"columnName": "is_auto_tunnel_paused",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isPingEnabled",
|
||||
"columnName": "is_ping_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"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)",
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"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`)"
|
||||
}
|
||||
],
|
||||
"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, 'e65e4e7cf01f50fb03196d47b54288b1')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 8,
|
||||
"identityHash": "b4d4a7c489f6b2f0d3aa4fa6f37b4935",
|
||||
"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_auto_tunnel_paused` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `is_amnezia_enabled` INTEGER NOT NULL DEFAULT false)",
|
||||
"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": "isAutoTunnelPaused",
|
||||
"columnName": "is_auto_tunnel_paused",
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"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 '')",
|
||||
"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": "''"
|
||||
}
|
||||
],
|
||||
"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`)"
|
||||
}
|
||||
],
|
||||
"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, 'b4d4a7c489f6b2f0d3aa4fa6f37b4935')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
SIGNING_STORE_PASSWORD=
|
||||
SIGNING_KEY_ALIAS=
|
||||
SIGNING_KEY_PASSWORD=
|
||||
KEY_STORE_PATH=/
|
||||
+3
-5
@@ -1,13 +1,11 @@
|
||||
package com.zaneschepke.wireguardautotunnel
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
@@ -21,4 +19,4 @@ class ExampleInstrumentedTest {
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("com.zaneschepke.wireguardautotunnel", appContext.packageName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.zaneschepke.wireguardautotunnel
|
||||
|
||||
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 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"
|
||||
|
||||
@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.
|
||||
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.
|
||||
}
|
||||
}
|
||||
@@ -1,63 +1,180 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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.READ_EXTERNAL_STORAGE"
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"
|
||||
android:maxSdkVersion="30" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32"
|
||||
tools:ignore="ScopedStorage" />
|
||||
<uses-permission
|
||||
android:name="android.permission.ACCESS_WIFI_STATE"
|
||||
android:maxSdkVersion="30"
|
||||
tools:ignore="LeanbackUsesWifi" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
||||
<!--foreground service exempt android 14-->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||
|
||||
<!--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"/>
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<!--android tv support-->
|
||||
<uses-feature
|
||||
android:name="android.software.leanback"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.location.gps"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.screen.portrait"
|
||||
android:required="false" />
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
</intent>
|
||||
</queries>
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:name=".WireGuardAutoTunnel"
|
||||
android:allowBackup="false"
|
||||
android:banner="@drawable/ic_banner"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.WireguardAutoTunnel"
|
||||
tools:targetApi="31">
|
||||
android:theme="@style/Theme.AppSplashScreen"
|
||||
tools:targetApi="tiramisu">
|
||||
<activity
|
||||
android:name=".ui.SplashActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.AppSplashScreen">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.MainActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.WireguardAutoTunnel">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<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=".service.shortcut.ShortcutsActivity"
|
||||
android:enabled="true"
|
||||
android:exported="true"
|
||||
android:finishOnTaskLaunch="true"
|
||||
android:theme="@android:style/Theme.NoDisplay" />
|
||||
|
||||
<service
|
||||
android:name=".service.foreground.ForegroundService"
|
||||
android:enabled="true"
|
||||
android:exported="false">
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="systemExempted"
|
||||
tools:node="merge" />
|
||||
<service
|
||||
android:name=".service.tile.TunnelControlTile"
|
||||
android:exported="true"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="Tunnel control"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
<meta-data
|
||||
android:name="android.service.quicksettings.ACTIVE_TILE"
|
||||
android:value="true" />
|
||||
<meta-data
|
||||
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
|
||||
android:value="true" />
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service
|
||||
android:name=".service.tile.AutoTunnelControlTile"
|
||||
android:exported="true"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="Auto-tunnel"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
<meta-data
|
||||
android:name="android.service.quicksettings.ACTIVE_TILE"
|
||||
android:value="true" />
|
||||
<meta-data
|
||||
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
|
||||
android:value="true" />
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service
|
||||
android:name=".service.foreground.WireGuardTunnelService"
|
||||
android:enabled="true"
|
||||
android:exported="false">
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="systemExempted"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||
android:persistent="true"
|
||||
tools:node="merge">
|
||||
<intent-filter>
|
||||
<action android:name="android.net.VpnService" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
|
||||
android:value="true" />
|
||||
</service>
|
||||
<service
|
||||
android:name=".service.foreground.WireGuardConnectivityWatcherService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="systemExempted"
|
||||
android:persistent="true"
|
||||
android:stopWithTask="false"
|
||||
tools:node="merge" />
|
||||
|
||||
<receiver
|
||||
android:name=".receiver.BootReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false">
|
||||
</service>
|
||||
<receiver android:enabled="true" android:name=".BootReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="android.intent.action.ACTION_BOOT_COMPLETED" />
|
||||
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
|
||||
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver
|
||||
android:name=".receiver.NotificationActionReceiver"
|
||||
android:exported="false" />
|
||||
</application>
|
||||
</manifest>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 16 KiB |
@@ -1,49 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.zaneschepke.wireguardautotunnel.repository.Repository
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceTracker
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class BootReceiver : BroadcastReceiver() {
|
||||
|
||||
@Inject
|
||||
lateinit var settingsRepo : Repository<Settings>
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
|
||||
GlobalScope.launch {
|
||||
try {
|
||||
val settings = settingsRepo.getAll()
|
||||
if (!settings.isNullOrEmpty()) {
|
||||
val setting = settings[0]
|
||||
if (setting.isAutoTunnelEnabled && setting.defaultTunnel != null) {
|
||||
val defaultTunnel = TunnelConfig.from(setting.defaultTunnel!!)
|
||||
ServiceTracker.actionOnService(
|
||||
Action.START, context,
|
||||
WireGuardConnectivityWatcherService::class.java,
|
||||
mapOf(context.resources.getString(R.string.tunnel_extras_key) to
|
||||
defaultTunnel.id.toString())
|
||||
)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,57 @@
|
||||
package com.zaneschepke.wireguardautotunnel
|
||||
|
||||
import android.app.Application
|
||||
import com.zaneschepke.wireguardautotunnel.repository.Repository
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
|
||||
import android.content.ComponentName
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.StrictMode
|
||||
import android.os.StrictMode.ThreadPolicy
|
||||
import android.service.quicksettings.TileService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelControlTile
|
||||
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
|
||||
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltAndroidApp
|
||||
class WireGuardAutoTunnel : Application() {
|
||||
|
||||
@Inject
|
||||
lateinit var settingsRepo : Repository<Settings>
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
if(BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
||||
settingsRepo.init()
|
||||
instance = this
|
||||
if (BuildConfig.DEBUG) {
|
||||
Timber.plant(Timber.DebugTree())
|
||||
StrictMode.setThreadPolicy(
|
||||
ThreadPolicy.Builder()
|
||||
.detectDiskReads()
|
||||
.detectDiskWrites()
|
||||
.detectNetwork()
|
||||
.penaltyLog()
|
||||
.build(),
|
||||
)
|
||||
} else Timber.plant(ReleaseTree())
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
lateinit var instance: WireGuardAutoTunnel
|
||||
private set
|
||||
|
||||
fun isRunningOnAndroidTv(): Boolean {
|
||||
return instance.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
||||
}
|
||||
|
||||
fun requestTunnelTileServiceStateUpdate() {
|
||||
TileService.requestListeningState(
|
||||
instance,
|
||||
ComponentName(instance, TunnelControlTile::class.java),
|
||||
)
|
||||
}
|
||||
|
||||
fun requestAutoTunnelTileServiceUpdate() {
|
||||
TileService.requestListeningState(
|
||||
instance,
|
||||
ComponentName(instance, AutoTunnelControlTile::class.java),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data
|
||||
|
||||
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 com.zaneschepke.wireguardautotunnel.data.domain.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
|
||||
@Database(
|
||||
entities = [Settings::class, TunnelConfig::class],
|
||||
version = 8,
|
||||
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),
|
||||
],
|
||||
exportSchema = true,
|
||||
)
|
||||
@TypeConverters(DatabaseListConverters::class)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun settingDao(): SettingsDao
|
||||
|
||||
abstract fun tunnelConfigDoa(): TunnelConfigDao
|
||||
}
|
||||
|
||||
@DeleteColumn(
|
||||
tableName = "Settings",
|
||||
columnName = "default_tunnel",
|
||||
)
|
||||
@DeleteColumn(
|
||||
tableName = "Settings",
|
||||
columnName = "is_battery_saver_enabled",
|
||||
)
|
||||
class RemoveLegacySettingColumnsMigration : AutoMigrationSpec
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data
|
||||
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import timber.log.Timber
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
'sampleSSID1,sampleSSID2',
|
||||
'false',
|
||||
'false',
|
||||
'false',
|
||||
'false',
|
||||
'false',
|
||||
'false',
|
||||
'false')
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
fun createTunnelConfig(): String {
|
||||
return """
|
||||
INSERT INTO TunnelConfig (name, wg_quick) VALUES ('test', 'test')
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data
|
||||
|
||||
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.domain.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
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data
|
||||
|
||||
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.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface TunnelConfigDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun save(t: TunnelConfig)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun saveAll(t: TunnelConfigs)
|
||||
|
||||
@Query("SELECT * FROM TunnelConfig WHERE id=:id")
|
||||
suspend fun getById(id: Long): TunnelConfig?
|
||||
|
||||
@Query("SELECT * FROM TunnelConfig WHERE name=:name")
|
||||
suspend fun getByName(name: String): TunnelConfig?
|
||||
|
||||
@Query("SELECT * FROM TunnelConfig")
|
||||
suspend fun getAll(): TunnelConfigs
|
||||
|
||||
@Delete
|
||||
suspend fun delete(t: TunnelConfig)
|
||||
|
||||
@Query("SELECT COUNT('id') FROM TunnelConfig")
|
||||
suspend fun count(): Long
|
||||
|
||||
@Query("SELECT * FROM TunnelConfig WHERE tunnel_networks LIKE '%' || :name || '%'")
|
||||
suspend fun findByTunnelNetworkName(name: String): TunnelConfigs
|
||||
|
||||
@Query("UPDATE TunnelConfig SET is_primary_tunnel = 0 WHERE is_primary_tunnel =1")
|
||||
suspend fun resetPrimaryTunnel()
|
||||
|
||||
@Query("UPDATE TunnelConfig SET is_mobile_data_tunnel = 0 WHERE is_mobile_data_tunnel =1")
|
||||
suspend fun resetMobileDataTunnel()
|
||||
|
||||
@Query("SELECT * FROM TUNNELCONFIG WHERE is_primary_tunnel=1")
|
||||
suspend fun findByPrimary(): TunnelConfigs
|
||||
|
||||
@Query("SELECT * FROM TUNNELCONFIG WHERE is_mobile_data_tunnel=1")
|
||||
suspend fun findByMobileDataTunnel(): TunnelConfigs
|
||||
|
||||
@Query("SELECT * FROM tunnelconfig")
|
||||
fun getAllFlow(): Flow<MutableList<TunnelConfig>>
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.datastore
|
||||
|
||||
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.intPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
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
|
||||
) {
|
||||
companion object {
|
||||
val LOCATION_DISCLOSURE_SHOWN = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN")
|
||||
val BATTERY_OPTIMIZE_DISABLE_SHOWN = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN")
|
||||
val TUNNEL_RUNNING_FROM_MANUAL_START =
|
||||
booleanPreferencesKey("TUNNEL_RUNNING_FROM_MANUAL_START")
|
||||
val ACTIVE_TUNNEL = intPreferencesKey("ACTIVE_TUNNEL")
|
||||
val CURRENT_SSID = stringPreferencesKey("CURRENT_SSID")
|
||||
val IS_PIN_LOCK_ENABLED = booleanPreferencesKey("PIN_LOCK_ENABLED")
|
||||
}
|
||||
|
||||
// preferences
|
||||
private val preferencesKey = "preferences"
|
||||
private val Context.dataStore by
|
||||
preferencesDataStore(
|
||||
name = preferencesKey,
|
||||
)
|
||||
|
||||
suspend fun init() {
|
||||
withContext(ioDispatcher) {
|
||||
try {
|
||||
context.dataStore.data.first()
|
||||
} catch (e: IOException) {
|
||||
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.e(e)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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 {
|
||||
context.dataStore.data.map { it[key] }.first()
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun <T> getFromStoreBlocking(key: Preferences.Key<T>) = runBlocking {
|
||||
context.dataStore.data.map { it[key] }.first()
|
||||
}
|
||||
|
||||
val preferencesFlow: Flow<Preferences?> = context.dataStore.data
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.domain
|
||||
|
||||
data class GeneralState(
|
||||
val isLocationDisclosureShown: Boolean = LOCATION_DISCLOSURE_SHOWN_DEFAULT,
|
||||
val isBatteryOptimizationDisableShown: Boolean = BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT,
|
||||
val isTunnelRunningFromManualStart: Boolean = TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT,
|
||||
val isPinLockEnabled: Boolean = PIN_LOCK_ENABLED_DEFAULT,
|
||||
val activeTunnelId: Int? = null
|
||||
) {
|
||||
companion object {
|
||||
const val LOCATION_DISCLOSURE_SHOWN_DEFAULT = false
|
||||
const val BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT = false
|
||||
const val TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT = false
|
||||
const val PIN_LOCK_ENABLED_DEFAULT = false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.domain
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity
|
||||
data class Settings(
|
||||
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
||||
@ColumnInfo(name = "is_tunnel_enabled") val isAutoTunnelEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "is_tunnel_on_mobile_data_enabled")
|
||||
val isTunnelOnMobileDataEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "trusted_network_ssids")
|
||||
val trustedNetworkSSIDs: MutableList<String> = mutableListOf(),
|
||||
@ColumnInfo(name = "is_always_on_vpn_enabled") val isAlwaysOnVpnEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "is_tunnel_on_ethernet_enabled")
|
||||
val isTunnelOnEthernetEnabled: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "is_shortcuts_enabled",
|
||||
defaultValue = "false",
|
||||
)
|
||||
val isShortcutsEnabled: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "is_tunnel_on_wifi_enabled",
|
||||
defaultValue = "false",
|
||||
)
|
||||
val isTunnelOnWifiEnabled: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "is_kernel_enabled",
|
||||
defaultValue = "false",
|
||||
)
|
||||
val isKernelEnabled: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "is_restore_on_boot_enabled",
|
||||
defaultValue = "false",
|
||||
)
|
||||
val isRestoreOnBootEnabled: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "is_multi_tunnel_enabled",
|
||||
defaultValue = "false",
|
||||
)
|
||||
val isMultiTunnelEnabled: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "is_auto_tunnel_paused",
|
||||
defaultValue = "false",
|
||||
)
|
||||
val isAutoTunnelPaused: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "is_ping_enabled",
|
||||
defaultValue = "false",
|
||||
)
|
||||
val isPingEnabled: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "is_amnezia_enabled",
|
||||
defaultValue = "false",
|
||||
)
|
||||
val isAmneziaEnabled: Boolean = false,
|
||||
)
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.domain
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import com.wireguard.config.Config
|
||||
import java.io.InputStream
|
||||
|
||||
@Entity(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: MutableList<String> = mutableListOf(),
|
||||
@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 = AM_QUICK_DEFAULT,
|
||||
) {
|
||||
companion object {
|
||||
fun configFromWgQuick(wgQuick: String): Config {
|
||||
val inputStream: InputStream = wgQuick.byteInputStream()
|
||||
return inputStream.bufferedReader(Charsets.UTF_8).use {
|
||||
Config.parse(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun configFromAmQuick(amQuick: String): org.amnezia.awg.config.Config {
|
||||
val inputStream: InputStream = amQuick.byteInputStream()
|
||||
return inputStream.bufferedReader(Charsets.UTF_8).use {
|
||||
org.amnezia.awg.config.Config.parse(it)
|
||||
}
|
||||
}
|
||||
|
||||
const val AM_QUICK_DEFAULT = ""
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
|
||||
interface AppDataRepository {
|
||||
suspend fun getPrimaryOrFirstTunnel(): TunnelConfig?
|
||||
suspend fun getStartTunnelConfig(): TunnelConfig?
|
||||
|
||||
suspend fun toggleWatcherServicePause()
|
||||
|
||||
val settings: SettingsRepository
|
||||
val tunnels: TunnelConfigRepository
|
||||
val appState: AppStateRepository
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import javax.inject.Inject
|
||||
|
||||
class AppDataRoomRepository @Inject constructor(
|
||||
override val settings: SettingsRepository,
|
||||
override val tunnels: TunnelConfigRepository,
|
||||
override val appState: AppStateRepository
|
||||
) : AppDataRepository {
|
||||
override suspend fun getPrimaryOrFirstTunnel(): TunnelConfig? {
|
||||
return tunnels.findPrimary().firstOrNull() ?: tunnels.getAll().firstOrNull()
|
||||
}
|
||||
|
||||
override suspend fun getStartTunnelConfig(): TunnelConfig? {
|
||||
return if (appState.isTunnelRunningFromManualStart()) {
|
||||
appState.getActiveTunnelId()?.let {
|
||||
tunnels.getById(it)
|
||||
}
|
||||
} else null
|
||||
}
|
||||
|
||||
override suspend fun toggleWatcherServicePause() {
|
||||
val settings = settings.getSettings()
|
||||
if (settings.isAutoTunnelEnabled) {
|
||||
val pauseAutoTunnel = !settings.isAutoTunnelPaused
|
||||
this.settings.save(
|
||||
settings.copy(
|
||||
isAutoTunnelPaused = pauseAutoTunnel,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.GeneralState
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface AppStateRepository {
|
||||
suspend fun isLocationDisclosureShown(): Boolean
|
||||
suspend fun setLocationDisclosureShown(shown: Boolean)
|
||||
|
||||
suspend fun isPinLockEnabled(): Boolean
|
||||
suspend fun setPinLockEnabled(enabled: Boolean)
|
||||
|
||||
suspend fun isBatteryOptimizationDisableShown(): Boolean
|
||||
suspend fun setBatteryOptimizationDisableShown(shown: Boolean)
|
||||
|
||||
suspend fun isTunnelRunningFromManualStart(): Boolean
|
||||
suspend fun setTunnelRunningFromManualStart(id: Int)
|
||||
|
||||
suspend fun setManualStop()
|
||||
|
||||
suspend fun getActiveTunnelId(): Int?
|
||||
|
||||
suspend fun getCurrentSsid(): String?
|
||||
|
||||
suspend fun setCurrentSsid(ssid: String)
|
||||
|
||||
val generalStateFlow: Flow<GeneralState>
|
||||
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.GeneralState
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import timber.log.Timber
|
||||
|
||||
class DataStoreAppStateRepository(private val dataStoreManager: DataStoreManager) :
|
||||
AppStateRepository {
|
||||
override suspend fun isLocationDisclosureShown(): Boolean {
|
||||
return dataStoreManager.getFromStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN)
|
||||
?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT
|
||||
}
|
||||
|
||||
override suspend fun setLocationDisclosureShown(shown: Boolean) {
|
||||
dataStoreManager.saveToDataStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN, shown)
|
||||
}
|
||||
|
||||
override suspend fun isPinLockEnabled(): Boolean {
|
||||
return dataStoreManager.getFromStore(DataStoreManager.IS_PIN_LOCK_ENABLED)
|
||||
?: GeneralState.PIN_LOCK_ENABLED_DEFAULT
|
||||
}
|
||||
|
||||
override suspend fun setPinLockEnabled(enabled: Boolean) {
|
||||
dataStoreManager.saveToDataStore(DataStoreManager.IS_PIN_LOCK_ENABLED, enabled)
|
||||
}
|
||||
|
||||
override suspend fun isBatteryOptimizationDisableShown(): Boolean {
|
||||
return dataStoreManager.getFromStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN)
|
||||
?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT
|
||||
}
|
||||
|
||||
override suspend fun setBatteryOptimizationDisableShown(shown: Boolean) {
|
||||
dataStoreManager.saveToDataStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN, shown)
|
||||
}
|
||||
|
||||
override suspend fun isTunnelRunningFromManualStart(): Boolean {
|
||||
return dataStoreManager.getFromStore(DataStoreManager.TUNNEL_RUNNING_FROM_MANUAL_START)
|
||||
?: GeneralState.TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT
|
||||
}
|
||||
|
||||
override suspend fun setTunnelRunningFromManualStart(id: Int) {
|
||||
setTunnelRunningFromManualStart(true)
|
||||
setActiveTunnelId(id)
|
||||
}
|
||||
|
||||
override suspend fun setManualStop() {
|
||||
setTunnelRunningFromManualStart(false)
|
||||
}
|
||||
|
||||
private suspend fun setTunnelRunningFromManualStart(running: Boolean) {
|
||||
dataStoreManager.saveToDataStore(DataStoreManager.TUNNEL_RUNNING_FROM_MANUAL_START, running)
|
||||
}
|
||||
|
||||
override suspend fun getActiveTunnelId(): Int? {
|
||||
return dataStoreManager.getFromStore(DataStoreManager.ACTIVE_TUNNEL)
|
||||
}
|
||||
|
||||
private suspend fun setActiveTunnelId(id: Int) {
|
||||
dataStoreManager.saveToDataStore(DataStoreManager.ACTIVE_TUNNEL, id)
|
||||
}
|
||||
|
||||
override suspend fun getCurrentSsid(): String? {
|
||||
return dataStoreManager.getFromStore(DataStoreManager.CURRENT_SSID)
|
||||
}
|
||||
|
||||
override suspend fun setCurrentSsid(ssid: String) {
|
||||
dataStoreManager.saveToDataStore(DataStoreManager.CURRENT_SSID, ssid)
|
||||
}
|
||||
|
||||
override val generalStateFlow: Flow<GeneralState> =
|
||||
dataStoreManager.preferencesFlow.map { prefs ->
|
||||
prefs?.let { pref ->
|
||||
try {
|
||||
GeneralState(
|
||||
isLocationDisclosureShown = pref[DataStoreManager.LOCATION_DISCLOSURE_SHOWN]
|
||||
?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT,
|
||||
isBatteryOptimizationDisableShown = pref[DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN]
|
||||
?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT,
|
||||
isTunnelRunningFromManualStart = pref[DataStoreManager.TUNNEL_RUNNING_FROM_MANUAL_START]
|
||||
?: GeneralState.TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT,
|
||||
isPinLockEnabled = pref[DataStoreManager.IS_PIN_LOCK_ENABLED]
|
||||
?: GeneralState.TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT,
|
||||
)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Timber.e(e)
|
||||
GeneralState()
|
||||
}
|
||||
} ?: GeneralState()
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.SettingsDao
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class RoomSettingsRepository(private val settingsDoa: SettingsDao) : SettingsRepository {
|
||||
|
||||
override suspend fun save(settings: Settings) {
|
||||
settingsDoa.save(settings)
|
||||
}
|
||||
|
||||
override fun getSettingsFlow(): Flow<Settings> {
|
||||
return settingsDoa.getSettingsFlow()
|
||||
}
|
||||
|
||||
override suspend fun getSettings(): Settings {
|
||||
return settingsDoa.getAll().firstOrNull() ?: Settings()
|
||||
}
|
||||
|
||||
override suspend fun getAll(): List<Settings> {
|
||||
return settingsDoa.getAll()
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class RoomTunnelConfigRepository(private val tunnelConfigDao: TunnelConfigDao) :
|
||||
TunnelConfigRepository {
|
||||
override fun getTunnelConfigsFlow(): Flow<TunnelConfigs> {
|
||||
return tunnelConfigDao.getAllFlow()
|
||||
}
|
||||
|
||||
override suspend fun getAll(): TunnelConfigs {
|
||||
return tunnelConfigDao.getAll()
|
||||
}
|
||||
|
||||
override suspend fun save(tunnelConfig: TunnelConfig) {
|
||||
tunnelConfigDao.save(tunnelConfig)
|
||||
}
|
||||
|
||||
override suspend fun updatePrimaryTunnel(tunnelConfig: TunnelConfig?) {
|
||||
tunnelConfigDao.resetPrimaryTunnel()
|
||||
tunnelConfig?.let {
|
||||
save(
|
||||
it.copy(
|
||||
isPrimaryTunnel = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override suspend fun updateMobileDataTunnel(tunnelConfig: TunnelConfig?) {
|
||||
tunnelConfigDao.resetMobileDataTunnel()
|
||||
tunnelConfig?.let {
|
||||
save(
|
||||
it.copy(
|
||||
isMobileDataTunnel = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun delete(tunnelConfig: TunnelConfig) {
|
||||
tunnelConfigDao.delete(tunnelConfig)
|
||||
}
|
||||
|
||||
override suspend fun getById(id: Int): TunnelConfig? {
|
||||
return tunnelConfigDao.getById(id.toLong())
|
||||
}
|
||||
|
||||
override suspend fun count(): Int {
|
||||
return tunnelConfigDao.count().toInt()
|
||||
}
|
||||
|
||||
override suspend fun findByTunnelName(name: String): TunnelConfig? {
|
||||
return tunnelConfigDao.getByName(name)
|
||||
}
|
||||
|
||||
override suspend fun findByTunnelNetworksName(name: String): TunnelConfigs {
|
||||
return tunnelConfigDao.findByTunnelNetworkName(name)
|
||||
}
|
||||
|
||||
override suspend fun findByMobileDataTunnel(): TunnelConfigs {
|
||||
return tunnelConfigDao.findByMobileDataTunnel()
|
||||
}
|
||||
|
||||
override suspend fun findPrimary(): TunnelConfigs {
|
||||
return tunnelConfigDao.findByPrimary()
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface SettingsRepository {
|
||||
suspend fun save(settings: Settings)
|
||||
|
||||
fun getSettingsFlow(): Flow<Settings>
|
||||
|
||||
suspend fun getSettings(): Settings
|
||||
|
||||
suspend fun getAll(): List<Settings>
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface TunnelConfigRepository {
|
||||
|
||||
fun getTunnelConfigsFlow(): Flow<TunnelConfigs>
|
||||
|
||||
suspend fun getAll(): TunnelConfigs
|
||||
|
||||
suspend fun save(tunnelConfig: TunnelConfig)
|
||||
|
||||
suspend fun updatePrimaryTunnel(tunnelConfig: TunnelConfig?)
|
||||
|
||||
suspend fun updateMobileDataTunnel(tunnelConfig: TunnelConfig?)
|
||||
|
||||
suspend fun delete(tunnelConfig: TunnelConfig)
|
||||
|
||||
suspend fun getById(id: Int): TunnelConfig?
|
||||
|
||||
suspend fun count(): Int
|
||||
|
||||
suspend fun findByTunnelName(name: String): TunnelConfig?
|
||||
|
||||
suspend fun findByTunnelNetworksName(name: String): TunnelConfigs
|
||||
|
||||
suspend fun findByMobileDataTunnel(): TunnelConfigs
|
||||
|
||||
suspend fun findPrimary(): TunnelConfigs
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.zaneschepke.wireguardautotunnel.module
|
||||
|
||||
import android.content.Context
|
||||
import com.zaneschepke.logcatter.LocalLogCollector
|
||||
import com.zaneschepke.logcatter.LogcatHelper
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
class AppModule {
|
||||
@Singleton
|
||||
@ApplicationScope
|
||||
@Provides
|
||||
fun providesApplicationScope(@DefaultDispatcher defaultDispatcher: CoroutineDispatcher): CoroutineScope =
|
||||
CoroutineScope(SupervisorJob() + defaultDispatcher)
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideLogCollect(@ApplicationContext context: Context): LocalLogCollector {
|
||||
return LogcatHelper.init(context = context)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.zaneschepke.wireguardautotunnel.module
|
||||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Qualifier
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
annotation class Kernel
|
||||
|
||||
@Qualifier
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
annotation class Userspace
|
||||
@@ -1,40 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.module
|
||||
|
||||
import android.content.Context
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.MyObjectBox
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import io.objectbox.Box
|
||||
import io.objectbox.BoxStore
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
class BoxModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideBoxStore(@ApplicationContext context : Context) : BoxStore {
|
||||
return MyObjectBox.builder()
|
||||
.androidContext(context.applicationContext)
|
||||
.build()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideBoxForSettings(store : BoxStore) : Box<Settings> {
|
||||
return store.boxFor(Settings::class.java)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideBoxForTunnels(store : BoxStore) : Box<TunnelConfig> {
|
||||
return store.boxFor(TunnelConfig::class.java)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.zaneschepke.wireguardautotunnel.module
|
||||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@Qualifier
|
||||
annotation class DefaultDispatcher
|
||||
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@Qualifier
|
||||
annotation class IoDispatcher
|
||||
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@Qualifier
|
||||
annotation class MainDispatcher
|
||||
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
@Qualifier
|
||||
annotation class MainImmediateDispatcher
|
||||
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@Qualifier
|
||||
annotation class ApplicationScope
|
||||
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@Qualifier
|
||||
annotation class ServiceScope
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package com.zaneschepke.wireguardautotunnel.module
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object CoroutinesDispatchersModule {
|
||||
@DefaultDispatcher
|
||||
@Provides
|
||||
fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
|
||||
|
||||
@IoDispatcher
|
||||
@Provides
|
||||
fun providesIoDispatcher(): CoroutineDispatcher = Dispatchers.IO
|
||||
|
||||
@MainDispatcher
|
||||
@Provides
|
||||
fun providesMainDispatcher(): CoroutineDispatcher = Dispatchers.Main
|
||||
|
||||
@MainImmediateDispatcher
|
||||
@Provides
|
||||
fun providesMainImmediateDispatcher(): CoroutineDispatcher = Dispatchers.Main.immediate
|
||||
}
|
||||
@@ -1,25 +1,93 @@
|
||||
package com.zaneschepke.wireguardautotunnel.module
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.repository.Repository
|
||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsBox
|
||||
import com.zaneschepke.wireguardautotunnel.repository.TunnelBox
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
|
||||
import dagger.Binds
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
||||
import com.zaneschepke.wireguardautotunnel.data.DatabaseCallback
|
||||
import com.zaneschepke.wireguardautotunnel.data.SettingsDao
|
||||
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
|
||||
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRoomRepository
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.DataStoreAppStateRepository
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.RoomSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.RoomTunnelConfigRepository
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
abstract class RepositoryModule {
|
||||
|
||||
@Binds
|
||||
class RepositoryModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
abstract fun provideSettingsRepository(settingsBox: SettingsBox) : Repository<Settings>
|
||||
fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
|
||||
return Room.databaseBuilder(
|
||||
context,
|
||||
AppDatabase::class.java,
|
||||
context.getString(R.string.db_name),
|
||||
)
|
||||
.fallbackToDestructiveMigration()
|
||||
.addCallback(DatabaseCallback())
|
||||
.build()
|
||||
}
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun provideTunnelRepository(tunnelBox: TunnelBox) : Repository<TunnelConfig>
|
||||
}
|
||||
@Provides
|
||||
fun provideSettingsDoa(appDatabase: AppDatabase): SettingsDao {
|
||||
return appDatabase.settingDao()
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideTunnelConfigDoa(appDatabase: AppDatabase): TunnelConfigDao {
|
||||
return appDatabase.tunnelConfigDoa()
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideTunnelConfigRepository(tunnelConfigDao: TunnelConfigDao): TunnelConfigRepository {
|
||||
return RoomTunnelConfigRepository(tunnelConfigDao)
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideSettingsRepository(settingsDao: SettingsDao): SettingsRepository {
|
||||
return RoomSettingsRepository(settingsDao)
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun providePreferencesDataStore(
|
||||
@ApplicationContext context: Context,
|
||||
@IoDispatcher ioDispatcher: CoroutineDispatcher
|
||||
): DataStoreManager {
|
||||
return DataStoreManager(context, ioDispatcher)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGeneralStateRepository(dataStoreManager: DataStoreManager): AppStateRepository {
|
||||
return DataStoreAppStateRepository(dataStoreManager)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAppDataRepository(
|
||||
settingsRepository: SettingsRepository,
|
||||
tunnelConfigRepository: TunnelConfigRepository,
|
||||
appStateRepository: AppStateRepository
|
||||
): AppDataRepository {
|
||||
return AppDataRoomRepository(settingsRepository, tunnelConfigRepository, appStateRepository)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.zaneschepke.wireguardautotunnel.module
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
|
||||
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
|
||||
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
|
||||
import com.zaneschepke.wireguardautotunnel.service.network.WifiService
|
||||
@@ -14,16 +15,25 @@ import dagger.hilt.android.scopes.ServiceScoped
|
||||
@Module
|
||||
@InstallIn(ServiceComponent::class)
|
||||
abstract class ServiceModule {
|
||||
@Binds
|
||||
@ServiceScoped
|
||||
abstract fun provideNotificationService(
|
||||
wireGuardNotification: WireGuardNotification
|
||||
): NotificationService
|
||||
|
||||
@Binds
|
||||
@ServiceScoped
|
||||
abstract fun provideNotificationService(wireGuardNotification: WireGuardNotification) : NotificationService
|
||||
abstract fun provideWifiService(wifiService: WifiService): NetworkService<WifiService>
|
||||
|
||||
@Binds
|
||||
@ServiceScoped
|
||||
abstract fun provideWifiService(wifiService: WifiService) : NetworkService<WifiService>
|
||||
abstract fun provideMobileDataService(
|
||||
mobileDataService: MobileDataService
|
||||
): NetworkService<MobileDataService>
|
||||
|
||||
@Binds
|
||||
@ServiceScoped
|
||||
abstract fun provideMobileDataService(mobileDataService : MobileDataService) : NetworkService<MobileDataService>
|
||||
}
|
||||
abstract fun provideEthernetService(
|
||||
ethernetService: EthernetService
|
||||
): NetworkService<EthernetService>
|
||||
}
|
||||
|
||||
@@ -3,6 +3,11 @@ package com.zaneschepke.wireguardautotunnel.module
|
||||
import android.content.Context
|
||||
import com.wireguard.android.backend.Backend
|
||||
import com.wireguard.android.backend.GoBackend
|
||||
import com.wireguard.android.backend.WgQuickBackend
|
||||
import com.wireguard.android.util.RootShell
|
||||
import com.wireguard.android.util.ToolsInstaller
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.WireGuardTunnel
|
||||
import dagger.Module
|
||||
@@ -10,22 +15,66 @@ import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import javax.inject.Provider
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
class TunnelModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideRootShell(@ApplicationContext context: Context): RootShell {
|
||||
return RootShell(context)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideBackend(@ApplicationContext context : Context) : Backend {
|
||||
@Userspace
|
||||
fun provideUserspaceBackend(@ApplicationContext context: Context): Backend {
|
||||
return GoBackend(context)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideVpnService(backend: Backend) : VpnService {
|
||||
return WireGuardTunnel(backend)
|
||||
@Kernel
|
||||
fun provideKernelBackend(@ApplicationContext context: Context, rootShell: RootShell): Backend {
|
||||
return WgQuickBackend(context, rootShell, ToolsInstaller(context, rootShell))
|
||||
}
|
||||
|
||||
}
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAmneziaBackend(@ApplicationContext context: Context): org.amnezia.awg.backend.Backend {
|
||||
return org.amnezia.awg.backend.GoBackend(context)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideVpnService(
|
||||
amneziaBackend: Provider<org.amnezia.awg.backend.Backend>,
|
||||
@Userspace userspaceBackend: Provider<Backend>,
|
||||
@Kernel kernelBackend: Provider<Backend>,
|
||||
appDataRepository: AppDataRepository,
|
||||
@ApplicationScope applicationScope: CoroutineScope,
|
||||
@IoDispatcher ioDispatcher: CoroutineDispatcher
|
||||
): VpnService {
|
||||
return WireGuardTunnel(
|
||||
amneziaBackend,
|
||||
userspaceBackend,
|
||||
kernelBackend,
|
||||
appDataRepository,
|
||||
applicationScope,
|
||||
ioDispatcher,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideServiceManager(
|
||||
appDataRepository: AppDataRepository,
|
||||
@IoDispatcher ioDispatcher: CoroutineDispatcher
|
||||
): ServiceManager {
|
||||
return ServiceManager(appDataRepository, ioDispatcher)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.zaneschepke.wireguardautotunnel.module
|
||||
|
||||
import android.content.Context
|
||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.components.ViewModelComponent
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.android.scopes.ViewModelScoped
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
|
||||
@Module
|
||||
@InstallIn(ViewModelComponent::class)
|
||||
class ViewModelModule {
|
||||
|
||||
@ViewModelScoped
|
||||
@Provides
|
||||
fun provideFileUtils(
|
||||
@ApplicationContext context: Context,
|
||||
@IoDispatcher ioDispatcher: CoroutineDispatcher
|
||||
): FileUtils {
|
||||
return FileUtils(context, ioDispatcher)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.zaneschepke.wireguardautotunnel.receiver
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class BootReceiver : BroadcastReceiver() {
|
||||
|
||||
@Inject
|
||||
lateinit var appDataRepository: AppDataRepository
|
||||
|
||||
@Inject
|
||||
lateinit var serviceManager: ServiceManager
|
||||
|
||||
@Inject
|
||||
@ApplicationScope
|
||||
lateinit var applicationScope: CoroutineScope
|
||||
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
if (Intent.ACTION_BOOT_COMPLETED != intent?.action) return
|
||||
context?.run {
|
||||
applicationScope.launch {
|
||||
val settings = appDataRepository.settings.getSettings()
|
||||
if(settings.isRestoreOnBootEnabled) {
|
||||
if (settings.isAutoTunnelEnabled) {
|
||||
Timber.i("Starting watcher service from boot")
|
||||
serviceManager.startWatcherServiceForeground(context)
|
||||
}
|
||||
if (appDataRepository.appState.isTunnelRunningFromManualStart()) {
|
||||
appDataRepository.appState.getActiveTunnelId()?.let {
|
||||
Timber.i("Starting tunnel that was active before reboot")
|
||||
serviceManager.startVpnServiceForeground(
|
||||
context,
|
||||
appDataRepository.tunnels.getById(it)?.id,
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
if (settings.isAlwaysOnVpnEnabled) {
|
||||
Timber.i("Starting vpn service from boot AOVPN")
|
||||
serviceManager.startVpnServiceForeground(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package com.zaneschepke.wireguardautotunnel.receiver
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class NotificationActionReceiver : BroadcastReceiver() {
|
||||
@Inject
|
||||
lateinit var settingsRepository: SettingsRepository
|
||||
|
||||
@Inject
|
||||
lateinit var serviceManager: ServiceManager
|
||||
|
||||
@Inject
|
||||
@ApplicationScope
|
||||
lateinit var applicationScope: CoroutineScope
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
applicationScope.launch {
|
||||
try {
|
||||
//TODO fix for manual start changes when enabled
|
||||
serviceManager.stopVpnServiceForeground(context)
|
||||
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
||||
serviceManager.startVpnServiceForeground(context)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
} finally {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.repository
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface Repository<T> {
|
||||
suspend fun save(t : T)
|
||||
suspend fun saveAll(t : List<T>)
|
||||
suspend fun getById(id : Long) : T?
|
||||
suspend fun getAll() : List<T>?
|
||||
suspend fun delete(t : T) : Boolean?
|
||||
suspend fun count() : Long?
|
||||
|
||||
val itemFlow : Flow<MutableList<T>>
|
||||
|
||||
fun init()
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.repository
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
|
||||
import io.objectbox.Box
|
||||
import io.objectbox.BoxStore
|
||||
import io.objectbox.kotlin.awaitCallInTx
|
||||
import io.objectbox.kotlin.toFlow
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
class SettingsBox @Inject constructor(private val box : Box<Settings>, private val boxStore : BoxStore) : Repository<Settings> {
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override val itemFlow = box.query().build().subscribe().toFlow()
|
||||
|
||||
override fun init() {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
if(getAll().isNullOrEmpty()) {
|
||||
save(Settings())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun save(t : Settings) {
|
||||
boxStore.awaitCallInTx {
|
||||
box.put(t)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun saveAll(t : List<Settings>) {
|
||||
boxStore.awaitCallInTx {
|
||||
box.put(t)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getById(id: Long): Settings? {
|
||||
return boxStore.awaitCallInTx {
|
||||
box[id]
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getAll(): List<Settings>? {
|
||||
return boxStore.awaitCallInTx {
|
||||
box.all
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun delete(t : Settings): Boolean? {
|
||||
return boxStore.awaitCallInTx {
|
||||
box.remove(t)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun count() : Long? {
|
||||
return boxStore.awaitCallInTx {
|
||||
box.count()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.repository
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
|
||||
import io.objectbox.Box
|
||||
import io.objectbox.BoxStore
|
||||
import io.objectbox.kotlin.awaitCallInTx
|
||||
import io.objectbox.kotlin.toFlow
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class TunnelBox @Inject constructor(private val box : Box<TunnelConfig>,private val boxStore : BoxStore) : Repository<TunnelConfig> {
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override val itemFlow = box.query().build().subscribe().toFlow()
|
||||
override fun init() {
|
||||
|
||||
}
|
||||
|
||||
override suspend fun save(t : TunnelConfig) {
|
||||
Timber.d("Saving tunnel config")
|
||||
boxStore.awaitCallInTx {
|
||||
box.put(t)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override suspend fun saveAll(t : List<TunnelConfig>) {
|
||||
boxStore.awaitCallInTx {
|
||||
box.put(t)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getById(id: Long): TunnelConfig? {
|
||||
return boxStore.awaitCallInTx {
|
||||
box[id]
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getAll(): List<TunnelConfig>? {
|
||||
return boxStore.awaitCallInTx {
|
||||
box.all
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun delete(t : TunnelConfig): Boolean? {
|
||||
return boxStore.awaitCallInTx {
|
||||
box.remove(t)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun count() : Long? {
|
||||
return boxStore.awaitCallInTx {
|
||||
box.count()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,5 +2,7 @@ package com.zaneschepke.wireguardautotunnel.service.foreground
|
||||
|
||||
enum class Action {
|
||||
START,
|
||||
STOP
|
||||
}
|
||||
START_FOREGROUND,
|
||||
STOP,
|
||||
STOP_FOREGROUND
|
||||
}
|
||||
|
||||
+20
-24
@@ -1,60 +1,56 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.foreground
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import androidx.lifecycle.LifecycleService
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import timber.log.Timber
|
||||
|
||||
|
||||
open class ForegroundService : Service() {
|
||||
|
||||
open class ForegroundService : LifecycleService() {
|
||||
private var isServiceStarted = false
|
||||
|
||||
override fun onBind(intent: Intent): IBinder? {
|
||||
super.onBind(intent)
|
||||
// We don't provide binding, so return null
|
||||
return null
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
super.onStartCommand(intent, flags, startId)
|
||||
Timber.d("onStartCommand executed with startId: $startId")
|
||||
if (intent != null) {
|
||||
val action = intent.action
|
||||
Timber.d("using an intent with action $action")
|
||||
when (action) {
|
||||
Action.START.name -> startService(intent.extras)
|
||||
Action.STOP.name -> stopService(intent.extras)
|
||||
Action.START.name,
|
||||
Action.START_FOREGROUND.name -> startService(intent.extras)
|
||||
|
||||
Action.STOP.name, Action.STOP_FOREGROUND.name -> stopService()
|
||||
Constants.ALWAYS_ON_VPN_ACTION -> {
|
||||
Timber.i("Always-on VPN starting service")
|
||||
startService(intent.extras)
|
||||
}
|
||||
|
||||
else -> Timber.d("This should never happen. No action in the received intent")
|
||||
}
|
||||
} else {
|
||||
Timber.d(
|
||||
"with a null intent. It has been probably restarted by the system."
|
||||
"with a null intent. It has been probably restarted by the system.",
|
||||
)
|
||||
}
|
||||
// by returning this we make sure the service is restarted if the system kills the service
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
Timber.d("The service has been destroyed")
|
||||
}
|
||||
|
||||
protected open fun startService(extras : Bundle?) {
|
||||
protected open fun startService(extras: Bundle?) {
|
||||
if (isServiceStarted) return
|
||||
Timber.d("Starting ${this.javaClass.simpleName}")
|
||||
isServiceStarted = true
|
||||
}
|
||||
|
||||
protected open fun stopService(extras : Bundle?) {
|
||||
protected open fun stopService() {
|
||||
Timber.d("Stopping ${this.javaClass.simpleName}")
|
||||
try {
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
stopSelf()
|
||||
} catch (e: Exception) {
|
||||
Timber.d("Service stopped without being started: ${e.message}")
|
||||
}
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
stopSelf()
|
||||
isServiceStarted = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.foreground
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
class ServiceManager(
|
||||
private val appDataRepository: AppDataRepository,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
|
||||
) {
|
||||
|
||||
private fun <T : Service> actionOnService(
|
||||
action: Action,
|
||||
context: Context,
|
||||
cls: Class<T>,
|
||||
extras: Map<String, Int>? = null
|
||||
) {
|
||||
val intent =
|
||||
Intent(context, cls).also {
|
||||
it.action = action.name
|
||||
extras?.forEach { (k, v) -> it.putExtra(k, v) }
|
||||
}
|
||||
intent.component?.javaClass
|
||||
try {
|
||||
when (action) {
|
||||
Action.START_FOREGROUND, Action.STOP_FOREGROUND -> context.startForegroundService(
|
||||
intent,
|
||||
)
|
||||
|
||||
Action.START, Action.STOP -> context.startService(intent)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun startVpnService(
|
||||
context: Context,
|
||||
tunnelId: Int? = null,
|
||||
isManualStart: Boolean = false
|
||||
) {
|
||||
if (isManualStart) onManualStart(tunnelId)
|
||||
actionOnService(
|
||||
Action.START,
|
||||
context,
|
||||
WireGuardTunnelService::class.java,
|
||||
tunnelId?.let { mapOf(Constants.TUNNEL_EXTRA_KEY to it) },
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun stopVpnServiceForeground(context: Context, isManualStop: Boolean = false) {
|
||||
withContext(ioDispatcher) {
|
||||
if (isManualStop) onManualStop()
|
||||
Timber.i("Stopping vpn service")
|
||||
actionOnService(
|
||||
Action.STOP_FOREGROUND,
|
||||
context,
|
||||
WireGuardTunnelService::class.java,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun stopVpnService(context: Context, isManualStop: Boolean = false) {
|
||||
withContext(ioDispatcher) {
|
||||
if (isManualStop) onManualStop()
|
||||
Timber.i("Stopping vpn service")
|
||||
actionOnService(
|
||||
Action.STOP,
|
||||
context,
|
||||
WireGuardTunnelService::class.java,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun onManualStop() {
|
||||
appDataRepository.appState.setManualStop()
|
||||
}
|
||||
|
||||
private suspend fun onManualStart(tunnelId: Int?) {
|
||||
tunnelId?.let {
|
||||
appDataRepository.appState.setTunnelRunningFromManualStart(it)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun startVpnServiceForeground(
|
||||
context: Context,
|
||||
tunnelId: Int? = null,
|
||||
isManualStart: Boolean = false
|
||||
) {
|
||||
withContext(ioDispatcher) {
|
||||
if (isManualStart) onManualStart(tunnelId)
|
||||
actionOnService(
|
||||
Action.START_FOREGROUND,
|
||||
context,
|
||||
WireGuardTunnelService::class.java,
|
||||
tunnelId?.let { mapOf(Constants.TUNNEL_EXTRA_KEY to it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun startWatcherServiceForeground(
|
||||
context: Context,
|
||||
) {
|
||||
actionOnService(
|
||||
Action.START_FOREGROUND,
|
||||
context,
|
||||
WireGuardConnectivityWatcherService::class.java,
|
||||
)
|
||||
}
|
||||
|
||||
fun startWatcherService(context: Context) {
|
||||
actionOnService(
|
||||
Action.START,
|
||||
context,
|
||||
WireGuardConnectivityWatcherService::class.java,
|
||||
)
|
||||
}
|
||||
|
||||
fun stopWatcherService(context: Context) {
|
||||
actionOnService(
|
||||
Action.STOP,
|
||||
context,
|
||||
WireGuardConnectivityWatcherService::class.java,
|
||||
)
|
||||
}
|
||||
}
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.foreground
|
||||
|
||||
enum class ServiceState {
|
||||
STARTED,
|
||||
STOPPED,
|
||||
}
|
||||
-48
@@ -1,48 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.foreground
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.app.Application
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Context.ACTIVITY_SERVICE
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
|
||||
object ServiceTracker {
|
||||
@Suppress("DEPRECATION")
|
||||
private // Deprecated for third party Services.
|
||||
fun <T> Context.isServiceRunning(service: Class<T>) =
|
||||
(getSystemService(ACTIVITY_SERVICE) as ActivityManager)
|
||||
.getRunningServices(Integer.MAX_VALUE)
|
||||
.any { it.service.className == service.name }
|
||||
|
||||
fun <T : Service> getServiceState(context: Context, cls : Class<T>): ServiceState {
|
||||
val isServiceRunning = context.isServiceRunning(cls)
|
||||
return if(isServiceRunning) ServiceState.STARTED else ServiceState.STOPPED
|
||||
}
|
||||
|
||||
fun <T : Service> actionOnService(action: Action, application: Application, cls : Class<T>, extras : Map<String,String>? = null) {
|
||||
if (getServiceState(application, cls) == ServiceState.STOPPED && action == Action.STOP) return
|
||||
val intent = Intent(application, cls).also {
|
||||
it.action = action.name
|
||||
extras?.forEach {(k, v) ->
|
||||
it.putExtra(k, v)
|
||||
}
|
||||
}
|
||||
intent.component?.javaClass
|
||||
application.startService(intent)
|
||||
}
|
||||
|
||||
fun <T : Service> actionOnService(action: Action, context: Context, cls : Class<T>, extras : Map<String,String>? = null) {
|
||||
if (getServiceState(context, cls) == ServiceState.STOPPED && action == Action.STOP) return
|
||||
val intent = Intent(context, cls).also {
|
||||
it.action = action.name
|
||||
extras?.forEach {(k, v) ->
|
||||
it.putExtra(k, v)
|
||||
}
|
||||
}
|
||||
intent.component?.javaClass
|
||||
context.startService(intent)
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.foreground
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
|
||||
|
||||
data class WatcherState(
|
||||
val isWifiConnected: Boolean = false,
|
||||
val isEthernetConnected: Boolean = false,
|
||||
val isMobileDataConnected: Boolean = false,
|
||||
val currentNetworkSSID: String = "",
|
||||
val settings: Settings = Settings()
|
||||
) {
|
||||
fun isEthernetConditionMet(): Boolean {
|
||||
return (isEthernetConnected &&
|
||||
settings.isTunnelOnEthernetEnabled)
|
||||
}
|
||||
|
||||
fun isMobileDataConditionMet(): Boolean {
|
||||
return (!isEthernetConnected &&
|
||||
settings.isTunnelOnMobileDataEnabled &&
|
||||
!isWifiConnected &&
|
||||
isMobileDataConnected)
|
||||
}
|
||||
|
||||
fun isTunnelOffOnMobileDataConditionMet(): Boolean {
|
||||
return (!isEthernetConnected &&
|
||||
!settings.isTunnelOnMobileDataEnabled &&
|
||||
isMobileDataConnected &&
|
||||
!isWifiConnected)
|
||||
}
|
||||
|
||||
fun isUntrustedWifiConditionMet(): Boolean {
|
||||
return (!isEthernetConnected &&
|
||||
isWifiConnected &&
|
||||
!settings.trustedNetworkSSIDs.contains(currentNetworkSSID) &&
|
||||
settings.isTunnelOnWifiEnabled)
|
||||
}
|
||||
|
||||
fun isTrustedWifiConditionMet(): Boolean {
|
||||
return (!isEthernetConnected &&
|
||||
(isWifiConnected &&
|
||||
settings.trustedNetworkSSIDs.contains(currentNetworkSSID)))
|
||||
}
|
||||
|
||||
fun isTunnelOffOnWifiConditionMet(): Boolean {
|
||||
return (!isEthernetConnected &&
|
||||
(isWifiConnected &&
|
||||
!settings.isTunnelOnWifiEnabled))
|
||||
}
|
||||
|
||||
fun isTunnelOffOnNoConnectivityMet(): Boolean {
|
||||
return (!isEthernetConnected &&
|
||||
!isWifiConnected &&
|
||||
!isMobileDataConnected)
|
||||
}
|
||||
}
|
||||
|
||||
+369
-141
@@ -1,228 +1,456 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.foreground
|
||||
|
||||
import android.app.AlarmManager
|
||||
import android.app.Application
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.PowerManager
|
||||
import android.os.SystemClock
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.repository.Repository
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.module.MainImmediateDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
|
||||
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
|
||||
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
|
||||
import com.zaneschepke.wireguardautotunnel.service.network.NetworkStatus
|
||||
import com.zaneschepke.wireguardautotunnel.service.network.WifiService
|
||||
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.net.InetAddress
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
@AndroidEntryPoint
|
||||
class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
|
||||
private val foregroundId = 122;
|
||||
private val foregroundId = 122
|
||||
|
||||
@Inject
|
||||
lateinit var wifiService : NetworkService<WifiService>
|
||||
lateinit var wifiService: NetworkService<WifiService>
|
||||
|
||||
@Inject
|
||||
lateinit var mobileDataService : NetworkService<MobileDataService>
|
||||
lateinit var mobileDataService: NetworkService<MobileDataService>
|
||||
|
||||
@Inject
|
||||
lateinit var settingsRepo: Repository<Settings>
|
||||
lateinit var ethernetService: NetworkService<EthernetService>
|
||||
|
||||
@Inject
|
||||
lateinit var notificationService : NotificationService
|
||||
lateinit var appDataRepository: AppDataRepository
|
||||
|
||||
@Inject
|
||||
lateinit var vpnService : VpnService
|
||||
lateinit var notificationService: NotificationService
|
||||
|
||||
private lateinit var watcherJob : Job;
|
||||
private lateinit var setting : Settings
|
||||
private lateinit var tunnelId: String
|
||||
@Inject
|
||||
lateinit var vpnService: VpnService
|
||||
|
||||
private var connecting = false
|
||||
private var disconnecting = false
|
||||
private var isWifiConnected = false
|
||||
private var isMobileDataConnected = false
|
||||
@Inject
|
||||
lateinit var serviceManager: ServiceManager
|
||||
|
||||
@Inject
|
||||
@IoDispatcher
|
||||
lateinit var ioDispatcher: CoroutineDispatcher
|
||||
|
||||
@Inject
|
||||
@MainImmediateDispatcher
|
||||
lateinit var mainImmediateDispatcher: CoroutineDispatcher
|
||||
|
||||
private val networkEventsFlow = MutableStateFlow(WatcherState())
|
||||
|
||||
private var watcherJob: Job? = null
|
||||
|
||||
private var wakeLock: PowerManager.WakeLock? = null
|
||||
private val tag = this.javaClass.name;
|
||||
private val tag = this.javaClass.name
|
||||
|
||||
|
||||
override fun startService(extras: Bundle?) {
|
||||
super.startService(extras)
|
||||
val tunnelId = extras?.getString(getString(R.string.tunnel_extras_key))
|
||||
if (tunnelId != null) {
|
||||
this.tunnelId = tunnelId
|
||||
}
|
||||
// we need this lock so our service gets not affected by Doze Mode
|
||||
initWakeLock()
|
||||
cancelWatcherJob()
|
||||
launchWatcherNotification()
|
||||
if(this::tunnelId.isInitialized) {
|
||||
startWatcherJob()
|
||||
} else {
|
||||
stopService(extras)
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
lifecycleScope.launch(mainImmediateDispatcher) {
|
||||
try {
|
||||
if (appDataRepository.settings.getSettings().isAutoTunnelPaused) {
|
||||
launchWatcherPausedNotification()
|
||||
} else launchWatcherNotification()
|
||||
} catch (e: Exception) {
|
||||
Timber.e("Failed to start watcher service, not enough permissions")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun stopService(extras: Bundle?) {
|
||||
super.stopService(extras)
|
||||
override fun startService(extras: Bundle?) {
|
||||
super.startService(extras)
|
||||
try {
|
||||
// we need this lock so our service gets not affected by Doze Mode
|
||||
lifecycleScope.launch { initWakeLock() }
|
||||
cancelWatcherJob()
|
||||
startWatcherJob()
|
||||
} catch (e: Exception) {
|
||||
Timber.e("Failed to launch watcher service, no permissions")
|
||||
}
|
||||
}
|
||||
|
||||
override fun stopService() {
|
||||
super.stopService()
|
||||
wakeLock?.let {
|
||||
if (it.isHeld) {
|
||||
it.release()
|
||||
}
|
||||
}
|
||||
cancelWatcherJob()
|
||||
stopVPN()
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
private fun launchWatcherNotification() {
|
||||
val notification = notificationService.createNotification(
|
||||
channelId = getString(R.string.watcher_channel_id),
|
||||
channelName = getString(R.string.watcher_channel_name),
|
||||
description = getString(R.string.watcher_notification_text))
|
||||
super.startForeground(foregroundId, notification)
|
||||
private fun launchWatcherNotification(
|
||||
description: String = getString(R.string.watcher_notification_text_active)
|
||||
) {
|
||||
val notification =
|
||||
notificationService.createNotification(
|
||||
channelId = getString(R.string.watcher_channel_id),
|
||||
channelName = getString(R.string.watcher_channel_name),
|
||||
title = getString(R.string.auto_tunnel_title),
|
||||
description = description,
|
||||
)
|
||||
ServiceCompat.startForeground(
|
||||
this,
|
||||
foregroundId,
|
||||
notification,
|
||||
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
|
||||
)
|
||||
}
|
||||
|
||||
//try to start task again if killed
|
||||
override fun onTaskRemoved(rootIntent: Intent) {
|
||||
Timber.d("Task Removed called")
|
||||
val restartServiceIntent = Intent(applicationContext, this::class.java).also {
|
||||
it.setPackage(packageName)
|
||||
};
|
||||
val restartServicePendingIntent: PendingIntent = PendingIntent.getService(this, 1, restartServiceIntent,
|
||||
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE);
|
||||
applicationContext.getSystemService(Context.ALARM_SERVICE);
|
||||
val alarmService: AlarmManager = applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager;
|
||||
alarmService.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 1000, restartServicePendingIntent);
|
||||
private fun launchWatcherPausedNotification() {
|
||||
launchWatcherNotification(getString(R.string.watcher_notification_text_paused))
|
||||
}
|
||||
|
||||
private fun initWakeLock() {
|
||||
wakeLock =
|
||||
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
|
||||
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
|
||||
acquire()
|
||||
try {
|
||||
Timber.i("Initiating wakelock with 10 min timeout")
|
||||
acquire(Constants.BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT)
|
||||
} finally {
|
||||
release()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun cancelWatcherJob() {
|
||||
if(this::watcherJob.isInitialized) {
|
||||
watcherJob.cancel()
|
||||
try {
|
||||
watcherJob?.cancel()
|
||||
} catch (e: CancellationException) {
|
||||
Timber.i("Watcher job cancelled")
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
private fun startWatcherJob() {
|
||||
watcherJob = GlobalScope.launch {
|
||||
val settings = settingsRepo.getAll();
|
||||
if(!settings.isNullOrEmpty()) {
|
||||
setting = settings[0]
|
||||
}
|
||||
GlobalScope.launch {
|
||||
watchForWifiConnectivityChanges()
|
||||
}
|
||||
if(setting.isTunnelOnMobileDataEnabled) {
|
||||
GlobalScope.launch {
|
||||
watchForMobileDataConnectivityChanges()
|
||||
watcherJob =
|
||||
lifecycleScope.launch {
|
||||
val setting = appDataRepository.settings.getSettings()
|
||||
launch {
|
||||
Timber.i("Starting wifi watcher")
|
||||
watchForWifiConnectivityChanges()
|
||||
}
|
||||
if (setting.isTunnelOnMobileDataEnabled) {
|
||||
launch {
|
||||
Timber.i("Starting mobile data watcher")
|
||||
watchForMobileDataConnectivityChanges()
|
||||
}
|
||||
}
|
||||
if (setting.isTunnelOnEthernetEnabled) {
|
||||
launch {
|
||||
Timber.i("Starting ethernet data watcher")
|
||||
watchForEthernetConnectivityChanges()
|
||||
}
|
||||
}
|
||||
launch {
|
||||
Timber.i("Starting settings watcher")
|
||||
watchForSettingsChanges()
|
||||
}
|
||||
if (setting.isPingEnabled) {
|
||||
launch {
|
||||
Timber.i("Starting ping watcher")
|
||||
watchForPingFailure()
|
||||
}
|
||||
}
|
||||
launch {
|
||||
Timber.i("Starting management watcher")
|
||||
manageVpn()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun watchForMobileDataConnectivityChanges() {
|
||||
mobileDataService.networkStatus.collect {
|
||||
when(it) {
|
||||
is NetworkStatus.Available -> {
|
||||
Timber.d("Gained Mobile data connection")
|
||||
isMobileDataConnected = true
|
||||
withContext(ioDispatcher) {
|
||||
mobileDataService.networkStatus.collect { status ->
|
||||
when (status) {
|
||||
is NetworkStatus.Available -> {
|
||||
Timber.i("Gained Mobile data connection")
|
||||
networkEventsFlow.update {
|
||||
it.copy(
|
||||
isMobileDataConnected = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is NetworkStatus.CapabilitiesChanged -> {
|
||||
networkEventsFlow.update {
|
||||
it.copy(
|
||||
isMobileDataConnected = true,
|
||||
)
|
||||
}
|
||||
Timber.i("Mobile data capabilities changed")
|
||||
}
|
||||
|
||||
is NetworkStatus.Unavailable -> {
|
||||
networkEventsFlow.update {
|
||||
it.copy(
|
||||
isMobileDataConnected = false,
|
||||
)
|
||||
}
|
||||
Timber.i("Lost mobile data connection")
|
||||
}
|
||||
}
|
||||
is NetworkStatus.CapabilitiesChanged -> {
|
||||
isMobileDataConnected = true
|
||||
Timber.d("Mobile data capabilities changed")
|
||||
if(!isWifiConnected && setting.isTunnelOnMobileDataEnabled
|
||||
&& vpnService.getState() == Tunnel.State.DOWN)
|
||||
startVPN()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun watchForPingFailure() {
|
||||
val context = this
|
||||
withContext(ioDispatcher) {
|
||||
try {
|
||||
do {
|
||||
if (vpnService.vpnState.value.status == TunnelState.UP) {
|
||||
val tunnelConfig = vpnService.vpnState.value.tunnelConfig
|
||||
tunnelConfig?.let {
|
||||
val config = TunnelConfig.configFromWgQuick(it.wgQuick)
|
||||
val results = config.peers.map { peer ->
|
||||
val host = if (peer.endpoint.isPresent &&
|
||||
peer.endpoint.get().resolved.isPresent)
|
||||
peer.endpoint.get().resolved.get().host
|
||||
else Constants.DEFAULT_PING_IP
|
||||
Timber.i("Checking reachability of: $host")
|
||||
val reachable = InetAddress.getByName(host)
|
||||
.isReachable(Constants.PING_TIMEOUT.toInt())
|
||||
Timber.i("Result: reachable - $reachable")
|
||||
reachable
|
||||
}
|
||||
if (results.contains(false)) {
|
||||
Timber.i("Restarting VPN for ping failure")
|
||||
serviceManager.stopVpnServiceForeground(context)
|
||||
delay(Constants.VPN_RESTART_DELAY)
|
||||
serviceManager.startVpnServiceForeground(context, it.id)
|
||||
delay(Constants.PING_COOLDOWN)
|
||||
}
|
||||
}
|
||||
}
|
||||
delay(Constants.PING_INTERVAL)
|
||||
} while (true)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun watchForSettingsChanges() {
|
||||
appDataRepository.settings.getSettingsFlow().collect { settings ->
|
||||
if (networkEventsFlow.value.settings.isAutoTunnelPaused != settings.isAutoTunnelPaused) {
|
||||
when (settings.isAutoTunnelPaused) {
|
||||
true -> launchWatcherPausedNotification()
|
||||
false -> launchWatcherNotification()
|
||||
}
|
||||
is NetworkStatus.Unavailable -> {
|
||||
isMobileDataConnected = false
|
||||
if(!isWifiConnected && vpnService.getState() == Tunnel.State.UP) stopVPN()
|
||||
Timber.d("Lost mobile data connection")
|
||||
}
|
||||
networkEventsFlow.update {
|
||||
it.copy(
|
||||
settings = settings,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun watchForEthernetConnectivityChanges() {
|
||||
withContext(ioDispatcher) {
|
||||
ethernetService.networkStatus.collect { status ->
|
||||
when (status) {
|
||||
is NetworkStatus.Available -> {
|
||||
Timber.i("Gained Ethernet connection")
|
||||
networkEventsFlow.update {
|
||||
it.copy(
|
||||
isEthernetConnected = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is NetworkStatus.CapabilitiesChanged -> {
|
||||
Timber.i("Ethernet capabilities changed")
|
||||
networkEventsFlow.update {
|
||||
it.copy(
|
||||
isEthernetConnected = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is NetworkStatus.Unavailable -> {
|
||||
networkEventsFlow.update {
|
||||
it.copy(
|
||||
isEthernetConnected = false,
|
||||
)
|
||||
}
|
||||
Timber.i("Lost Ethernet connection")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun watchForWifiConnectivityChanges() {
|
||||
wifiService.networkStatus.collect {
|
||||
when (it) {
|
||||
withContext(ioDispatcher) {
|
||||
wifiService.networkStatus.collect { status ->
|
||||
when (status) {
|
||||
is NetworkStatus.Available -> {
|
||||
Timber.d("Gained Wi-Fi connection")
|
||||
isWifiConnected = true
|
||||
}
|
||||
is NetworkStatus.CapabilitiesChanged -> {
|
||||
Timber.d("Wifi capabilities changed")
|
||||
isWifiConnected = true
|
||||
if (!connecting && !disconnecting) {
|
||||
Timber.d("Not connect and not disconnecting")
|
||||
val ssid = wifiService.getNetworkName(it.networkCapabilities);
|
||||
Timber.d("SSID: $ssid")
|
||||
if ((setting.trustedNetworkSSIDs?.contains(ssid) == false) && vpnService.getState() == Tunnel.State.DOWN) {
|
||||
Timber.d("Starting VPN Tunnel for untrusted network: $ssid")
|
||||
startVPN()
|
||||
} else if (!disconnecting && vpnService.getState() == Tunnel.State.UP && setting.trustedNetworkSSIDs.contains(
|
||||
ssid
|
||||
)
|
||||
) {
|
||||
Timber.d("Stopping VPN Tunnel for trusted network with ssid: $ssid")
|
||||
stopVPN()
|
||||
}
|
||||
Timber.i("Gained Wi-Fi connection")
|
||||
networkEventsFlow.update {
|
||||
it.copy(
|
||||
isWifiConnected = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is NetworkStatus.CapabilitiesChanged -> {
|
||||
Timber.i("Wifi capabilities changed")
|
||||
networkEventsFlow.update {
|
||||
it.copy(
|
||||
isWifiConnected = true,
|
||||
)
|
||||
}
|
||||
val ssid = wifiService.getNetworkName(status.networkCapabilities)
|
||||
ssid?.let { name ->
|
||||
if (name.contains(Constants.UNREADABLE_SSID)) {
|
||||
Timber.w("SSID unreadable: missing permissions")
|
||||
} else Timber.i("Detected valid SSID")
|
||||
appDataRepository.appState.setCurrentSsid(name)
|
||||
networkEventsFlow.update {
|
||||
it.copy(
|
||||
currentNetworkSSID = name,
|
||||
)
|
||||
}
|
||||
} ?: Timber.w("Failed to read ssid")
|
||||
}
|
||||
|
||||
is NetworkStatus.Unavailable -> {
|
||||
isWifiConnected = false
|
||||
Timber.d("Lost Wi-Fi connection")
|
||||
if(setting.isTunnelOnMobileDataEnabled && vpnService.getState() == Tunnel.State.DOWN
|
||||
&& isMobileDataConnected){
|
||||
Timber.d("Wifi not available so starting vpn for mobile data")
|
||||
startVPN()
|
||||
networkEventsFlow.update {
|
||||
it.copy(
|
||||
isWifiConnected = false,
|
||||
)
|
||||
}
|
||||
Timber.i("Lost Wi-Fi connection")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getMobileDataTunnel(): TunnelConfig? {
|
||||
return appDataRepository.tunnels.findByMobileDataTunnel().firstOrNull()
|
||||
}
|
||||
|
||||
private suspend fun getSsidTunnel(ssid: String): TunnelConfig? {
|
||||
return appDataRepository.tunnels.findByTunnelNetworksName(ssid).firstOrNull()
|
||||
}
|
||||
|
||||
private fun isTunnelDown(): Boolean {
|
||||
return vpnService.vpnState.value.status == TunnelState.DOWN
|
||||
}
|
||||
|
||||
private suspend fun manageVpn() {
|
||||
val context = this
|
||||
withContext(ioDispatcher) {
|
||||
networkEventsFlow.collectLatest { watcherState ->
|
||||
val autoTunnel = "Auto-tunnel watcher"
|
||||
if (!watcherState.settings.isAutoTunnelPaused) {
|
||||
//delay for rapid network state changes and then collect latest
|
||||
delay(Constants.WATCHER_COLLECTION_DELAY)
|
||||
val tunnelConfig = vpnService.vpnState.value.tunnelConfig
|
||||
when {
|
||||
watcherState.isEthernetConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel on on ethernet condition met")
|
||||
if (isTunnelDown()) serviceManager.startVpnServiceForeground(context)
|
||||
}
|
||||
|
||||
watcherState.isMobileDataConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel on mobile data condition met")
|
||||
val mobileDataTunnel = getMobileDataTunnel()
|
||||
val tunnel =
|
||||
mobileDataTunnel ?: appDataRepository.getPrimaryOrFirstTunnel()
|
||||
if (isTunnelDown() || tunnelConfig?.isMobileDataTunnel == false) {
|
||||
serviceManager.startVpnServiceForeground(
|
||||
context,
|
||||
tunnel?.id,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
watcherState.isTunnelOffOnMobileDataConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel off on mobile data met, turning vpn off")
|
||||
if (!isTunnelDown()) serviceManager.stopVpnServiceForeground(context)
|
||||
}
|
||||
|
||||
watcherState.isUntrustedWifiConditionMet() -> {
|
||||
if (tunnelConfig?.tunnelNetworks?.contains(watcherState.currentNetworkSSID) == false ||
|
||||
tunnelConfig == null) {
|
||||
Timber.i("$autoTunnel - tunnel on ssid not associated with current tunnel condition met")
|
||||
getSsidTunnel(watcherState.currentNetworkSSID)?.let {
|
||||
Timber.i("Found tunnel associated with this SSID, bringing tunnel up: ${it.name}")
|
||||
if (isTunnelDown() || tunnelConfig?.id != it.id) serviceManager.startVpnServiceForeground(
|
||||
context,
|
||||
it.id,
|
||||
)
|
||||
} ?: suspend {
|
||||
Timber.i("No tunnel associated with this SSID, using defaults")
|
||||
val default = appDataRepository.getPrimaryOrFirstTunnel()
|
||||
if (default?.name != vpnService.name) {
|
||||
default?.let {
|
||||
serviceManager.startVpnServiceForeground(context, it.id)
|
||||
}
|
||||
|
||||
}
|
||||
}.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
watcherState.isTrustedWifiConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel off on trusted wifi condition met, turning vpn off")
|
||||
if (!isTunnelDown()) serviceManager.stopVpnServiceForeground(context)
|
||||
}
|
||||
|
||||
watcherState.isTunnelOffOnWifiConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel off on wifi condition met, turning vpn off")
|
||||
if (!isTunnelDown()) serviceManager.stopVpnServiceForeground(context)
|
||||
}
|
||||
|
||||
watcherState.isTunnelOffOnNoConnectivityMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel off on no connectivity met, turning vpn off")
|
||||
if (!isTunnelDown()) serviceManager.stopVpnServiceForeground(context)
|
||||
}
|
||||
|
||||
else -> {
|
||||
Timber.i("$autoTunnel - no condition met")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private fun startVPN() {
|
||||
if(!connecting) {
|
||||
connecting = true
|
||||
ServiceTracker.actionOnService(
|
||||
Action.START,
|
||||
this.applicationContext as Application,
|
||||
WireGuardTunnelService::class.java,
|
||||
mapOf(getString(R.string.tunnel_extras_key) to tunnelId))
|
||||
connecting = false
|
||||
}
|
||||
}
|
||||
private fun stopVPN() {
|
||||
if(!disconnecting) {
|
||||
disconnecting = true
|
||||
ServiceTracker.actionOnService(
|
||||
Action.STOP,
|
||||
this.applicationContext as Application,
|
||||
WireGuardTunnelService::class.java
|
||||
)
|
||||
disconnecting = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+166
-45
@@ -1,80 +1,201 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.foreground
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.module.MainImmediateDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver
|
||||
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.handshakeStatus
|
||||
import com.zaneschepke.wireguardautotunnel.util.mapPeerStats
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class WireGuardTunnelService : ForegroundService() {
|
||||
|
||||
private val foregroundId = 123;
|
||||
private val foregroundId = 123
|
||||
|
||||
@Inject
|
||||
lateinit var vpnService : VpnService
|
||||
lateinit var vpnService: VpnService
|
||||
|
||||
@Inject
|
||||
lateinit var notificationService : NotificationService
|
||||
lateinit var appDataRepository: AppDataRepository
|
||||
|
||||
private lateinit var job : Job
|
||||
@Inject
|
||||
lateinit var notificationService: NotificationService
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun startService(extras : Bundle?) {
|
||||
super.startService(extras)
|
||||
val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key))
|
||||
cancelJob()
|
||||
job = GlobalScope.launch {
|
||||
if(tunnelConfigString != null) {
|
||||
try {
|
||||
val tunnelConfig = TunnelConfig.from(tunnelConfigString)
|
||||
val state = vpnService.startTunnel(tunnelConfig)
|
||||
if (state == Tunnel.State.UP) {
|
||||
launchVpnConnectedNotification(tunnelConfig.name)
|
||||
}
|
||||
} catch (e : Exception) {
|
||||
Timber.e("Problem starting tunnel: ${e.message}")
|
||||
stopService(extras)
|
||||
}
|
||||
} else {
|
||||
Timber.e("Tunnel config null")
|
||||
@Inject
|
||||
@MainImmediateDispatcher
|
||||
lateinit var mainImmediateDispatcher: CoroutineDispatcher
|
||||
|
||||
@Inject
|
||||
@IoDispatcher
|
||||
lateinit var ioDispatcher: CoroutineDispatcher
|
||||
|
||||
private var job: Job? = null
|
||||
|
||||
private var didShowConnected = false
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
lifecycleScope.launch(mainImmediateDispatcher) {
|
||||
//TODO fix this to not launch if AOVPN
|
||||
if (appDataRepository.tunnels.count() != 0) {
|
||||
launchVpnNotification()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun stopService(extras : Bundle?) {
|
||||
super.stopService(extras)
|
||||
CoroutineScope(Dispatchers.IO).launch() {
|
||||
override fun startService(extras: Bundle?) {
|
||||
super.startService(extras)
|
||||
cancelJob()
|
||||
job =
|
||||
lifecycleScope.launch {
|
||||
launch {
|
||||
val tunnelId = extras?.getInt(Constants.TUNNEL_EXTRA_KEY)
|
||||
if (vpnService.getState() == TunnelState.UP) {
|
||||
vpnService.stopTunnel()
|
||||
}
|
||||
vpnService.startTunnel(
|
||||
tunnelId?.let {
|
||||
appDataRepository.tunnels.getById(it)
|
||||
},
|
||||
)
|
||||
}
|
||||
launch {
|
||||
handshakeNotifications()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//TODO improve tunnel notifications
|
||||
private suspend fun handshakeNotifications() {
|
||||
withContext(ioDispatcher) {
|
||||
var tunnelName: String? = null
|
||||
vpnService.vpnState.collect { state ->
|
||||
state.statistics
|
||||
?.mapPeerStats()
|
||||
?.map { it.value?.handshakeStatus() }
|
||||
.let { statuses ->
|
||||
when {
|
||||
statuses?.all { it == HandshakeStatus.HEALTHY } == true -> {
|
||||
if (!didShowConnected) {
|
||||
delay(Constants.VPN_CONNECTED_NOTIFICATION_DELAY)
|
||||
tunnelName = state.tunnelConfig?.name
|
||||
launchVpnNotification(
|
||||
getString(R.string.tunnel_start_title),
|
||||
"${getString(R.string.tunnel_start_text)} - $tunnelName",
|
||||
)
|
||||
didShowConnected = true
|
||||
}
|
||||
}
|
||||
|
||||
statuses?.any { it == HandshakeStatus.STALE } == true -> {}
|
||||
statuses?.all { it == HandshakeStatus.NOT_STARTED } ==
|
||||
true -> {
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
if (state.status == TunnelState.UP && state.tunnelConfig?.name != tunnelName) {
|
||||
tunnelName = state.tunnelConfig?.name
|
||||
launchVpnNotification(
|
||||
getString(R.string.tunnel_start_title),
|
||||
"${getString(R.string.tunnel_start_text)} - $tunnelName",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchAlwaysOnDisabledNotification() {
|
||||
launchVpnNotification(
|
||||
title = this.getString(R.string.vpn_connection_failed),
|
||||
description = this.getString(R.string.always_on_disabled),
|
||||
)
|
||||
}
|
||||
|
||||
override fun stopService() {
|
||||
super.stopService()
|
||||
lifecycleScope.launch {
|
||||
vpnService.stopTunnel()
|
||||
didShowConnected = false
|
||||
}
|
||||
cancelJob()
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
private fun launchVpnConnectedNotification(tunnelName : String) {
|
||||
val notification = notificationService.createNotification(
|
||||
channelId = getString(R.string.vpn_channel_id),
|
||||
channelName = getString(R.string.vpn_channel_name),
|
||||
title = getString(R.string.tunnel_start_title),
|
||||
onGoing = false,
|
||||
showTimestamp = true,
|
||||
description = "${getString(R.string.tunnel_start_text)} $tunnelName"
|
||||
private fun launchVpnNotification(
|
||||
title: String = getString(R.string.vpn_starting),
|
||||
description: String = getString(R.string.attempt_connection)
|
||||
) {
|
||||
val notification =
|
||||
notificationService.createNotification(
|
||||
channelId = getString(R.string.vpn_channel_id),
|
||||
channelName = getString(R.string.vpn_channel_name),
|
||||
title = title,
|
||||
onGoing = false,
|
||||
vibration = false,
|
||||
showTimestamp = true,
|
||||
description = description,
|
||||
)
|
||||
ServiceCompat.startForeground(
|
||||
this,
|
||||
foregroundId,
|
||||
notification,
|
||||
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
|
||||
)
|
||||
super.startForeground(foregroundId, notification)
|
||||
}
|
||||
|
||||
private fun launchVpnConnectionFailedNotification(message: String) {
|
||||
val notification =
|
||||
notificationService.createNotification(
|
||||
channelId = getString(R.string.vpn_channel_id),
|
||||
channelName = getString(R.string.vpn_channel_name),
|
||||
action =
|
||||
PendingIntent.getBroadcast(
|
||||
this,
|
||||
0,
|
||||
Intent(this, NotificationActionReceiver::class.java),
|
||||
PendingIntent.FLAG_IMMUTABLE,
|
||||
),
|
||||
actionText = getString(R.string.restart),
|
||||
title = getString(R.string.vpn_connection_failed),
|
||||
onGoing = false,
|
||||
vibration = true,
|
||||
showTimestamp = true,
|
||||
description = message,
|
||||
)
|
||||
ServiceCompat.startForeground(
|
||||
this,
|
||||
foregroundId,
|
||||
notification,
|
||||
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
|
||||
)
|
||||
}
|
||||
|
||||
private fun cancelJob() {
|
||||
if(this::job.isInitialized) {
|
||||
job.cancel()
|
||||
try {
|
||||
job?.cancel()
|
||||
} catch (e: CancellationException) {
|
||||
Timber.i("Tunnel job cancelled")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+77
-62
@@ -1,6 +1,5 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.network
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
@@ -10,15 +9,15 @@ import android.net.wifi.SupplicantState
|
||||
import android.net.wifi.WifiInfo
|
||||
import android.net.wifi.WifiManager
|
||||
import android.os.Build
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
abstract class BaseNetworkService<T : BaseNetworkService<T>>(val context: Context, networkCapability : Int) : NetworkService<T> {
|
||||
abstract class BaseNetworkService<T : BaseNetworkService<T>>(
|
||||
val context: Context,
|
||||
networkCapability: Int
|
||||
) : NetworkService<T> {
|
||||
private val connectivityManager =
|
||||
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
|
||||
@@ -26,62 +25,73 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(val context: Contex
|
||||
context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
|
||||
|
||||
override val networkStatus = callbackFlow {
|
||||
val networkStatusCallback = when (Build.VERSION.SDK_INT) {
|
||||
in Build.VERSION_CODES.S..Int.MAX_VALUE -> {
|
||||
object : ConnectivityManager.NetworkCallback(
|
||||
FLAG_INCLUDE_LOCATION_INFO
|
||||
) {
|
||||
val networkStatusCallback =
|
||||
when (Build.VERSION.SDK_INT) {
|
||||
in Build.VERSION_CODES.S..Int.MAX_VALUE -> {
|
||||
object :
|
||||
ConnectivityManager.NetworkCallback(
|
||||
FLAG_INCLUDE_LOCATION_INFO,
|
||||
) {
|
||||
override fun onAvailable(network: Network) {
|
||||
trySend(NetworkStatus.Available(network))
|
||||
}
|
||||
|
||||
override fun onAvailable(network: Network) {
|
||||
trySend(NetworkStatus.Available(network))
|
||||
override fun onLost(network: Network) {
|
||||
trySend(NetworkStatus.Unavailable(network))
|
||||
}
|
||||
|
||||
override fun onCapabilitiesChanged(
|
||||
network: Network,
|
||||
networkCapabilities: NetworkCapabilities
|
||||
) {
|
||||
trySend(
|
||||
NetworkStatus.CapabilitiesChanged(
|
||||
network,
|
||||
networkCapabilities,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
trySend(NetworkStatus.Unavailable(network))
|
||||
}
|
||||
else -> {
|
||||
object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
trySend(NetworkStatus.Available(network))
|
||||
}
|
||||
|
||||
override fun onCapabilitiesChanged(
|
||||
network: Network,
|
||||
networkCapabilities: NetworkCapabilities
|
||||
) {
|
||||
trySend(NetworkStatus.CapabilitiesChanged(network, networkCapabilities))
|
||||
override fun onLost(network: Network) {
|
||||
trySend(NetworkStatus.Unavailable(network))
|
||||
}
|
||||
|
||||
override fun onCapabilitiesChanged(
|
||||
network: Network,
|
||||
networkCapabilities: NetworkCapabilities
|
||||
) {
|
||||
trySend(
|
||||
NetworkStatus.CapabilitiesChanged(
|
||||
network,
|
||||
networkCapabilities,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
object : ConnectivityManager.NetworkCallback() {
|
||||
|
||||
override fun onAvailable(network: Network) {
|
||||
trySend(NetworkStatus.Available(network))
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
trySend(NetworkStatus.Unavailable(network))
|
||||
}
|
||||
|
||||
override fun onCapabilitiesChanged(
|
||||
network: Network,
|
||||
networkCapabilities: NetworkCapabilities
|
||||
) {
|
||||
trySend(NetworkStatus.CapabilitiesChanged(network, networkCapabilities))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val request = NetworkRequest.Builder()
|
||||
.addTransportType(networkCapability)
|
||||
.build()
|
||||
val request =
|
||||
NetworkRequest.Builder()
|
||||
.addTransportType(networkCapability)
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||
.build()
|
||||
connectivityManager.registerNetworkCallback(request, networkStatusCallback)
|
||||
|
||||
awaitClose {
|
||||
connectivityManager.unregisterNetworkCallback(networkStatusCallback)
|
||||
}
|
||||
awaitClose { connectivityManager.unregisterNetworkCallback(networkStatusCallback) }
|
||||
}
|
||||
|
||||
|
||||
override fun getNetworkName(networkCapabilities: NetworkCapabilities): String? {
|
||||
var ssid : String? = getWifiNameFromCapabilities(networkCapabilities)
|
||||
if((Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) || (Build.VERSION.SDK_INT == Build.VERSION_CODES.R)) {
|
||||
var ssid: String? = getWifiNameFromCapabilities(networkCapabilities)
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
|
||||
val info = wifiManager.connectionInfo
|
||||
if (info.supplicantState === SupplicantState.COMPLETED) {
|
||||
ssid = info.ssid
|
||||
@@ -90,28 +100,33 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(val context: Contex
|
||||
return ssid?.trim('"')
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
private fun getWifiNameFromCapabilities(networkCapabilities: NetworkCapabilities) : String? {
|
||||
val info : WifiInfo
|
||||
if(networkCapabilities.transportInfo is WifiInfo) {
|
||||
info = networkCapabilities.transportInfo as WifiInfo
|
||||
} else {
|
||||
return null
|
||||
private fun getWifiNameFromCapabilities(networkCapabilities: NetworkCapabilities): String? {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val info: WifiInfo
|
||||
if (networkCapabilities.transportInfo is WifiInfo) {
|
||||
info = networkCapabilities.transportInfo as WifiInfo
|
||||
return info.ssid
|
||||
}
|
||||
}
|
||||
return info.ssid
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <Result> Flow<NetworkStatus>.map(
|
||||
crossinline onUnavailable: suspend (network : Network) -> Result,
|
||||
crossinline onAvailable: suspend (network : Network) -> Result,
|
||||
crossinline onCapabilitiesChanged: suspend (network : Network, networkCapabilities : NetworkCapabilities) -> Result,
|
||||
crossinline onUnavailable: suspend (network: Network) -> Result,
|
||||
crossinline onAvailable: suspend (network: Network) -> Result,
|
||||
crossinline onCapabilitiesChanged:
|
||||
suspend (network: Network, networkCapabilities: NetworkCapabilities) -> Result
|
||||
): Flow<Result> = map { status ->
|
||||
when (status) {
|
||||
is NetworkStatus.Unavailable -> onUnavailable(status.network)
|
||||
is NetworkStatus.Available -> onAvailable(status.network)
|
||||
is NetworkStatus.CapabilitiesChanged -> onCapabilitiesChanged(status.network, status.networkCapabilities)
|
||||
is NetworkStatus.CapabilitiesChanged ->
|
||||
onCapabilitiesChanged(
|
||||
status.network,
|
||||
status.networkCapabilities,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.network
|
||||
|
||||
import android.content.Context
|
||||
import android.net.NetworkCapabilities
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class EthernetService @Inject constructor(@ApplicationContext context: Context) :
|
||||
BaseNetworkService<EthernetService>(context, NetworkCapabilities.TRANSPORT_ETHERNET)
|
||||
+1
-2
@@ -6,5 +6,4 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class MobileDataService @Inject constructor(@ApplicationContext context: Context) :
|
||||
BaseNetworkService<MobileDataService>(context, NetworkCapabilities.TRANSPORT_CELLULAR) {
|
||||
}
|
||||
BaseNetworkService<MobileDataService>(context, NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||
|
||||
+3
-3
@@ -4,7 +4,7 @@ import android.net.NetworkCapabilities
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface NetworkService<T> {
|
||||
fun getNetworkName(networkCapabilities: NetworkCapabilities) : String?
|
||||
val networkStatus : Flow<NetworkStatus>
|
||||
fun getNetworkName(networkCapabilities: NetworkCapabilities): String?
|
||||
|
||||
}
|
||||
val networkStatus: Flow<NetworkStatus>
|
||||
}
|
||||
|
||||
+6
-3
@@ -4,7 +4,10 @@ import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
|
||||
sealed class NetworkStatus {
|
||||
class Available(val network : Network) : NetworkStatus()
|
||||
class Unavailable(val network : Network) : NetworkStatus()
|
||||
class CapabilitiesChanged(val network : Network, val networkCapabilities : NetworkCapabilities) : NetworkStatus()
|
||||
class Available(val network: Network) : NetworkStatus()
|
||||
|
||||
class Unavailable(val network: Network) : NetworkStatus()
|
||||
|
||||
class CapabilitiesChanged(val network: Network, val networkCapabilities: NetworkCapabilities) :
|
||||
NetworkStatus()
|
||||
}
|
||||
|
||||
+1
-2
@@ -6,5 +6,4 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class WifiService @Inject constructor(@ApplicationContext context: Context) :
|
||||
BaseNetworkService<WifiService>(context, NetworkCapabilities.TRANSPORT_WIFI) {
|
||||
}
|
||||
BaseNetworkService<WifiService>(context, NetworkCapabilities.TRANSPORT_WIFI)
|
||||
|
||||
+8
-4
@@ -2,17 +2,21 @@ package com.zaneschepke.wireguardautotunnel.service.notification
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
|
||||
interface NotificationService {
|
||||
fun createNotification(
|
||||
channelId: String,
|
||||
channelName: String,
|
||||
title: String = "",
|
||||
action: PendingIntent? = null,
|
||||
actionText: String? = null,
|
||||
description: String,
|
||||
showTimestamp : Boolean = false,
|
||||
showTimestamp: Boolean = false,
|
||||
importance: Int = NotificationManager.IMPORTANCE_HIGH,
|
||||
vibration: Boolean = true,
|
||||
vibration: Boolean = false,
|
||||
onGoing: Boolean = true,
|
||||
lights: Boolean = true
|
||||
lights: Boolean = true,
|
||||
onlyAlertOnce: Boolean = true,
|
||||
): Notification
|
||||
}
|
||||
}
|
||||
|
||||
+71
-32
@@ -7,57 +7,96 @@ import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.MainActivity
|
||||
import com.zaneschepke.wireguardautotunnel.ui.SplashActivity
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class WireGuardNotification @Inject constructor(@ApplicationContext private val context: Context) : NotificationService {
|
||||
class WireGuardNotification @Inject constructor(@ApplicationContext private val context: Context) :
|
||||
NotificationService {
|
||||
private val notificationManager =
|
||||
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
|
||||
private val watcherBuilder: NotificationCompat.Builder =
|
||||
NotificationCompat.Builder(
|
||||
context,
|
||||
context.getString(R.string.watcher_channel_id),
|
||||
)
|
||||
private val tunnelBuilder: NotificationCompat.Builder =
|
||||
NotificationCompat.Builder(
|
||||
context,
|
||||
context.getString(R.string.vpn_channel_id),
|
||||
)
|
||||
|
||||
override fun createNotification(
|
||||
channelId: String,
|
||||
channelName: String,
|
||||
title: String,
|
||||
action: PendingIntent?,
|
||||
actionText: String?,
|
||||
description: String,
|
||||
showTimestamp: Boolean,
|
||||
importance: Int,
|
||||
vibration: Boolean,
|
||||
onGoing: Boolean,
|
||||
lights: Boolean
|
||||
) : Notification {
|
||||
val channel = NotificationChannel(
|
||||
channelId,
|
||||
channelName,
|
||||
importance
|
||||
).let {
|
||||
it.description = title
|
||||
it.enableLights(lights)
|
||||
it.lightColor = Color.RED
|
||||
it.enableVibration(vibration)
|
||||
it.vibrationPattern = longArrayOf(100, 200, 300, 400, 500, 400, 300, 200, 400)
|
||||
it
|
||||
}
|
||||
lights: Boolean,
|
||||
onlyAlertOnce: Boolean,
|
||||
): Notification {
|
||||
val channel =
|
||||
NotificationChannel(
|
||||
channelId,
|
||||
channelName,
|
||||
importance,
|
||||
)
|
||||
.let {
|
||||
it.description = title
|
||||
it.enableLights(lights)
|
||||
it.lightColor = Color.RED
|
||||
it.enableVibration(vibration)
|
||||
it.vibrationPattern = longArrayOf(100, 200, 300)
|
||||
it
|
||||
}
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
val pendingIntent: PendingIntent =
|
||||
Intent(context, MainActivity::class.java).let { notificationIntent ->
|
||||
PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE)
|
||||
Intent(context, SplashActivity::class.java).let { notificationIntent ->
|
||||
PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
notificationIntent,
|
||||
PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
}
|
||||
|
||||
val builder: Notification.Builder =
|
||||
Notification.Builder(
|
||||
context,
|
||||
channelId
|
||||
)
|
||||
val builder =
|
||||
when (channelId) {
|
||||
context.getString(R.string.watcher_channel_id) -> watcherBuilder
|
||||
context.getString(R.string.vpn_channel_id) -> tunnelBuilder
|
||||
else -> {
|
||||
NotificationCompat.Builder(
|
||||
context,
|
||||
channelId,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return builder
|
||||
.setContentTitle(title)
|
||||
.setContentText(description)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setOngoing(onGoing)
|
||||
.setShowWhen(showTimestamp)
|
||||
.setSmallIcon(R.mipmap.ic_launcher_foreground)
|
||||
.build()
|
||||
return builder.let {
|
||||
if (action != null && actionText != null) {
|
||||
it.addAction(
|
||||
NotificationCompat.Action.Builder(0, actionText, action).build(),
|
||||
)
|
||||
it.setAutoCancel(true)
|
||||
}
|
||||
it.setContentTitle(title)
|
||||
.setContentText(description)
|
||||
.setOnlyAlertOnce(onlyAlertOnce)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setOngoing(onGoing)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setShowWhen(showTimestamp)
|
||||
.setSmallIcon(R.drawable.ic_launcher)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.shortcut
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ShortcutsActivity : ComponentActivity() {
|
||||
|
||||
@Inject
|
||||
lateinit var appDataRepository: AppDataRepository
|
||||
|
||||
@Inject
|
||||
lateinit var serviceManager: ServiceManager
|
||||
|
||||
@Inject
|
||||
@ApplicationScope
|
||||
lateinit var applicationScope: CoroutineScope
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
applicationScope.launch {
|
||||
val settings = appDataRepository.settings.getSettings()
|
||||
if (settings.isShortcutsEnabled) {
|
||||
when (intent.getStringExtra(CLASS_NAME_EXTRA_KEY)) {
|
||||
WireGuardTunnelService::class.java.simpleName -> {
|
||||
val tunnelName = intent.getStringExtra(TUNNEL_NAME_EXTRA_KEY)
|
||||
val tunnelConfig = tunnelName?.let {
|
||||
appDataRepository.tunnels.getAll().firstOrNull {
|
||||
it.name == tunnelName
|
||||
}
|
||||
}
|
||||
when (intent.action) {
|
||||
Action.START.name -> serviceManager.startVpnServiceForeground(
|
||||
this@ShortcutsActivity, tunnelConfig?.id, isManualStart = true,
|
||||
)
|
||||
|
||||
Action.STOP.name -> serviceManager.stopVpnServiceForeground(
|
||||
this@ShortcutsActivity,
|
||||
isManualStop = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
WireGuardConnectivityWatcherService::class.java.simpleName -> {
|
||||
when (intent.action) {
|
||||
Action.START.name -> appDataRepository.settings.save(
|
||||
settings.copy(
|
||||
isAutoTunnelPaused = false,
|
||||
),
|
||||
)
|
||||
|
||||
Action.STOP.name -> appDataRepository.settings.save(
|
||||
settings.copy(
|
||||
isAutoTunnelPaused = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finish()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TUNNEL_NAME_EXTRA_KEY = "tunnelName"
|
||||
const val CLASS_NAME_EXTRA_KEY = "className"
|
||||
}
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.tile
|
||||
|
||||
import android.os.Build
|
||||
import android.service.quicksettings.Tile
|
||||
import android.service.quicksettings.TileService
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ServiceLifecycleDispatcher
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.module.ServiceScope
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.cancel
|
||||
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 serviceManager: ServiceManager
|
||||
|
||||
private val dispatcher = ServiceLifecycleDispatcher(this)
|
||||
|
||||
private var manualStartConfig: TunnelConfig? = null
|
||||
|
||||
override fun onStartListening() {
|
||||
super.onStartListening()
|
||||
lifecycleScope.launch {
|
||||
val settings = appDataRepository.settings.getSettings()
|
||||
when (settings.isAutoTunnelEnabled) {
|
||||
true -> {
|
||||
if (settings.isAutoTunnelPaused) {
|
||||
setInactive()
|
||||
setTileDescription(this@AutoTunnelControlTile.getString(R.string.paused))
|
||||
} else {
|
||||
setActive()
|
||||
setTileDescription(this@AutoTunnelControlTile.getString(R.string.active))
|
||||
}
|
||||
}
|
||||
false -> {
|
||||
setTileDescription(this@AutoTunnelControlTile.getString(R.string.disabled))
|
||||
setUnavailable()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTileAdded() {
|
||||
super.onTileAdded()
|
||||
onStartListening()
|
||||
}
|
||||
|
||||
override fun onClick() {
|
||||
super.onClick()
|
||||
unlockAndRun {
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
appDataRepository.toggleWatcherServicePause()
|
||||
onStartListening()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e.message)
|
||||
} finally {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setActive() {
|
||||
qsTile.state = Tile.STATE_ACTIVE
|
||||
qsTile.updateTile()
|
||||
}
|
||||
|
||||
private fun setInactive() {
|
||||
qsTile.state = Tile.STATE_INACTIVE
|
||||
qsTile.updateTile()
|
||||
}
|
||||
|
||||
private fun setUnavailable() {
|
||||
manualStartConfig = null
|
||||
qsTile.state = Tile.STATE_UNAVAILABLE
|
||||
qsTile.updateTile()
|
||||
}
|
||||
|
||||
private fun setTileDescription(description: String) {
|
||||
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 val lifecycle: Lifecycle
|
||||
get() = dispatcher.lifecycle
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.tile
|
||||
|
||||
import android.os.Build
|
||||
import android.service.quicksettings.Tile
|
||||
import android.service.quicksettings.TileService
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.LifecycleService
|
||||
import androidx.lifecycle.ServiceLifecycleDispatcher
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.cancel
|
||||
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 vpnService: VpnService
|
||||
|
||||
@Inject
|
||||
lateinit var serviceManager: ServiceManager
|
||||
|
||||
private val dispatcher = ServiceLifecycleDispatcher(this)
|
||||
|
||||
private var manualStartConfig: TunnelConfig? = null
|
||||
|
||||
override fun onStartListening() {
|
||||
super.onStartListening()
|
||||
Timber.d("On start listening called")
|
||||
lifecycleScope.launch {
|
||||
when (vpnService.getState()) {
|
||||
TunnelState.UP -> {
|
||||
setActive()
|
||||
setTileDescription(vpnService.name)
|
||||
}
|
||||
|
||||
TunnelState.DOWN -> {
|
||||
setInactive()
|
||||
val config = appDataRepository.getStartTunnelConfig()?.also { config ->
|
||||
manualStartConfig = config
|
||||
} ?: appDataRepository.getPrimaryOrFirstTunnel()
|
||||
config?.let {
|
||||
setTileDescription(it.name)
|
||||
} ?: setUnavailable()
|
||||
}
|
||||
|
||||
else -> setInactive()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTileAdded() {
|
||||
super.onTileAdded()
|
||||
onStartListening()
|
||||
}
|
||||
|
||||
override fun onClick() {
|
||||
super.onClick()
|
||||
unlockAndRun {
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
if (vpnService.getState() == TunnelState.UP) {
|
||||
serviceManager.stopVpnServiceForeground(
|
||||
this@TunnelControlTile,
|
||||
isManualStop = true,
|
||||
)
|
||||
} else {
|
||||
serviceManager.startVpnServiceForeground(
|
||||
this@TunnelControlTile, manualStartConfig?.id, isManualStart = true,
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e.message)
|
||||
} finally {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setActive() {
|
||||
qsTile.state = Tile.STATE_ACTIVE
|
||||
qsTile.updateTile()
|
||||
}
|
||||
|
||||
private fun setInactive() {
|
||||
qsTile.state = Tile.STATE_INACTIVE
|
||||
qsTile.updateTile()
|
||||
}
|
||||
|
||||
private fun setUnavailable() {
|
||||
manualStartConfig = null
|
||||
qsTile.state = Tile.STATE_UNAVAILABLE
|
||||
qsTile.updateTile()
|
||||
}
|
||||
|
||||
private fun setTileDescription(description: String) {
|
||||
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 val lifecycle: Lifecycle
|
||||
get() = dispatcher.lifecycle
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.tunnel
|
||||
|
||||
enum class HandshakeStatus {
|
||||
HEALTHY,
|
||||
STALE,
|
||||
UNKNOWN,
|
||||
NOT_STARTED;
|
||||
|
||||
companion object {
|
||||
private const val WG_TYPICAL_HANDSHAKE_INTERVAL_WHEN_HEALTHY_SEC = 180
|
||||
const val STATUS_CHANGE_TIME_BUFFER = 30
|
||||
const val STALE_TIME_LIMIT_SEC =
|
||||
WG_TYPICAL_HANDSHAKE_INTERVAL_WHEN_HEALTHY_SEC + STATUS_CHANGE_TIME_BUFFER
|
||||
const val NEVER_CONNECTED_TO_UNHEALTHY_TIME_LIMIT_SEC = 30
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.tunnel
|
||||
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
|
||||
enum class TunnelState {
|
||||
UP,
|
||||
DOWN,
|
||||
TOGGLE;
|
||||
|
||||
fun toWgState(): Tunnel.State {
|
||||
return when (this) {
|
||||
UP -> Tunnel.State.UP
|
||||
DOWN -> Tunnel.State.DOWN
|
||||
TOGGLE -> Tunnel.State.TOGGLE
|
||||
}
|
||||
}
|
||||
|
||||
fun toAmState(): org.amnezia.awg.backend.Tunnel.State {
|
||||
return when (this) {
|
||||
UP -> org.amnezia.awg.backend.Tunnel.State.UP
|
||||
DOWN -> org.amnezia.awg.backend.Tunnel.State.DOWN
|
||||
TOGGLE -> org.amnezia.awg.backend.Tunnel.State.TOGGLE
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun from(state: Tunnel.State): TunnelState {
|
||||
return when (state) {
|
||||
Tunnel.State.DOWN -> DOWN
|
||||
Tunnel.State.TOGGLE -> TOGGLE
|
||||
Tunnel.State.UP -> UP
|
||||
}
|
||||
}
|
||||
|
||||
fun from(state: org.amnezia.awg.backend.Tunnel.State): TunnelState {
|
||||
return when (state) {
|
||||
org.amnezia.awg.backend.Tunnel.State.DOWN -> DOWN
|
||||
org.amnezia.awg.backend.Tunnel.State.TOGGLE -> TOGGLE
|
||||
org.amnezia.awg.backend.Tunnel.State.UP -> UP
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,15 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.tunnel
|
||||
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
interface VpnService : Tunnel, org.amnezia.awg.backend.Tunnel {
|
||||
suspend fun startTunnel(tunnelConfig: TunnelConfig? = null): TunnelState
|
||||
|
||||
interface VpnService : Tunnel {
|
||||
suspend fun startTunnel(tunnelConfig : TunnelConfig) : Tunnel.State
|
||||
suspend fun stopTunnel()
|
||||
val state : SharedFlow<Tunnel.State>
|
||||
val tunnelName : SharedFlow<String>
|
||||
fun getState() : Tunnel.State
|
||||
}
|
||||
|
||||
val vpnState: StateFlow<VpnState>
|
||||
|
||||
fun getState(): TunnelState
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.tunnel
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
|
||||
|
||||
data class VpnState(
|
||||
val status: TunnelState = TunnelState.DOWN,
|
||||
val tunnelConfig: TunnelConfig? = null,
|
||||
val statistics: TunnelStatistics? = null
|
||||
)
|
||||
+196
-39
@@ -2,64 +2,221 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel
|
||||
|
||||
import com.wireguard.android.backend.Backend
|
||||
import com.wireguard.android.backend.BackendException
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import com.wireguard.android.backend.Tunnel.State
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.module.Kernel
|
||||
import com.zaneschepke.wireguardautotunnel.module.Userspace
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.AmneziaStatistics
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.WireGuardStatistics
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.amnezia.awg.backend.Tunnel
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
|
||||
class WireGuardTunnel
|
||||
@Inject
|
||||
constructor(
|
||||
private val userspaceAmneziaBackend: Provider<org.amnezia.awg.backend.Backend>,
|
||||
@Userspace private val userspaceBackend: Provider<Backend>,
|
||||
@Kernel private val kernelBackend: Provider<Backend>,
|
||||
private val appDataRepository: AppDataRepository,
|
||||
@ApplicationScope private val applicationScope: CoroutineScope,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
|
||||
) : VpnService {
|
||||
private val _vpnState = MutableStateFlow(VpnState())
|
||||
override val vpnState: StateFlow<VpnState> = _vpnState.asStateFlow()
|
||||
|
||||
|
||||
class WireGuardTunnel @Inject constructor(private val backend : Backend) : VpnService {
|
||||
private var statsJob: Job? = null
|
||||
|
||||
private val _tunnelName = MutableStateFlow("")
|
||||
override val tunnelName get() = _tunnelName.asStateFlow()
|
||||
private val _state = MutableSharedFlow<Tunnel.State>(
|
||||
replay = 1,
|
||||
onBufferOverflow = BufferOverflow.SUSPEND,
|
||||
extraBufferCapacity = 1)
|
||||
override val state get() = _state.asSharedFlow()
|
||||
private var backendIsWgUserspace = true
|
||||
|
||||
override suspend fun startTunnel(tunnelConfig: TunnelConfig) : Tunnel.State{
|
||||
try {
|
||||
if(getState() == Tunnel.State.UP && _tunnelName.value != tunnelConfig.name) {
|
||||
stopTunnel()
|
||||
private var backendIsAmneziaUserspace = false
|
||||
|
||||
init {
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
appDataRepository.settings.getSettingsFlow().collect {
|
||||
if (it.isKernelEnabled && (backendIsWgUserspace || backendIsAmneziaUserspace)) {
|
||||
Timber.i("Setting kernel backend")
|
||||
backendIsWgUserspace = false
|
||||
backendIsAmneziaUserspace = false
|
||||
} else if (!it.isKernelEnabled && !it.isAmneziaEnabled && !backendIsWgUserspace) {
|
||||
Timber.i("Setting WireGuard userspace backend")
|
||||
backendIsWgUserspace = true
|
||||
backendIsAmneziaUserspace = false
|
||||
} else if (it.isAmneziaEnabled && !backendIsAmneziaUserspace) {
|
||||
Timber.i("Setting Amnezia userspace backend")
|
||||
backendIsAmneziaUserspace = true
|
||||
backendIsWgUserspace = false
|
||||
}
|
||||
}
|
||||
_tunnelName.emit(tunnelConfig.name)
|
||||
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
||||
val state = backend.setState(
|
||||
this, Tunnel.State.UP, config)
|
||||
_state.emit(state)
|
||||
return state;
|
||||
} catch (e : Exception) {
|
||||
Timber.e("Failed to start tunnel with error: ${e.message}")
|
||||
return Tunnel.State.DOWN
|
||||
}
|
||||
}
|
||||
|
||||
private fun setState(tunnelConfig: TunnelConfig?, tunnelState: TunnelState): TunnelState {
|
||||
return if (backendIsAmneziaUserspace) {
|
||||
Timber.i("Using Amnezia backend")
|
||||
val config = tunnelConfig?.let {
|
||||
if (it.amQuick != "") TunnelConfig.configFromAmQuick(it.amQuick) else {
|
||||
Timber.w("Using backwards compatible wg config, amnezia specific config not found.")
|
||||
TunnelConfig.configFromAmQuick(it.wgQuick)
|
||||
}
|
||||
}
|
||||
val state =
|
||||
userspaceAmneziaBackend.get().setState(this, tunnelState.toAmState(), config)
|
||||
TunnelState.from(state)
|
||||
} else {
|
||||
Timber.i("Using Wg backend")
|
||||
val wgConfig = tunnelConfig?.let { TunnelConfig.configFromWgQuick(it.wgQuick) }
|
||||
val state = backend().setState(
|
||||
this,
|
||||
tunnelState.toWgState(),
|
||||
wgConfig,
|
||||
)
|
||||
TunnelState.from(state)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun startTunnel(tunnelConfig: TunnelConfig?): TunnelState {
|
||||
return withContext(ioDispatcher) {
|
||||
try {
|
||||
//TODO we need better error handling here
|
||||
// need to bubble up these errors to the UI
|
||||
val config = tunnelConfig ?: appDataRepository.getPrimaryOrFirstTunnel()
|
||||
if (config != null) {
|
||||
emitTunnelConfig(config)
|
||||
setState(config, TunnelState.UP)
|
||||
} else throw Exception("No tunnels")
|
||||
} catch (e: BackendException) {
|
||||
Timber.e("Failed to start tunnel with error: ${e.message}")
|
||||
TunnelState.from(State.DOWN)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun backend(): Backend {
|
||||
return when {
|
||||
backendIsWgUserspace -> {
|
||||
userspaceBackend.get()
|
||||
}
|
||||
|
||||
!backendIsWgUserspace && !backendIsAmneziaUserspace -> {
|
||||
kernelBackend.get()
|
||||
}
|
||||
|
||||
else -> {
|
||||
userspaceBackend.get()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun emitTunnelState(state: TunnelState) {
|
||||
_vpnState.tryEmit(
|
||||
_vpnState.value.copy(
|
||||
status = state,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun emitBackendStatistics(statistics: TunnelStatistics) {
|
||||
_vpnState.tryEmit(
|
||||
_vpnState.value.copy(
|
||||
statistics = statistics,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun emitTunnelConfig(tunnelConfig: TunnelConfig?) {
|
||||
_vpnState.emit(
|
||||
_vpnState.value.copy(
|
||||
tunnelConfig = tunnelConfig,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun resetVpnState() {
|
||||
_vpnState.tryEmit(VpnState())
|
||||
}
|
||||
|
||||
override suspend fun stopTunnel() {
|
||||
withContext(ioDispatcher) {
|
||||
try {
|
||||
if (getState() == TunnelState.UP) {
|
||||
val state = setState(null, TunnelState.DOWN)
|
||||
resetVpnState()
|
||||
emitTunnelState(state)
|
||||
}
|
||||
} catch (e: BackendException) {
|
||||
Timber.e("Failed to stop wireguard tunnel with error: ${e.message}")
|
||||
} catch (e: org.amnezia.awg.backend.BackendException) {
|
||||
Timber.e("Failed to stop amnezia tunnel with error: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getState(): TunnelState {
|
||||
return if (backendIsAmneziaUserspace) TunnelState.from(
|
||||
userspaceAmneziaBackend.get().getState(this),
|
||||
)
|
||||
else TunnelState.from(backend().getState(this))
|
||||
}
|
||||
|
||||
override fun getName(): String {
|
||||
return _tunnelName.value
|
||||
return _vpnState.value.tunnelConfig?.name ?: ""
|
||||
}
|
||||
|
||||
override suspend fun stopTunnel() {
|
||||
try {
|
||||
if(getState() == Tunnel.State.UP) {
|
||||
val state = backend.setState(this, Tunnel.State.DOWN, null)
|
||||
_state.emit(state)
|
||||
|
||||
override fun onStateChange(newState: Tunnel.State) {
|
||||
handleStateChange(TunnelState.from(newState))
|
||||
}
|
||||
|
||||
private fun handleStateChange(state: TunnelState) {
|
||||
emitTunnelState(state)
|
||||
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
|
||||
if (state == TunnelState.UP) {
|
||||
statsJob = startTunnelStatisticsJob()
|
||||
}
|
||||
if (state == TunnelState.DOWN) {
|
||||
try {
|
||||
statsJob?.cancel()
|
||||
} catch (e: CancellationException) {
|
||||
Timber.i("Stats job cancelled")
|
||||
}
|
||||
} catch (e : BackendException) {
|
||||
Timber.e("Failed to stop tunnel with error: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun getState(): Tunnel.State {
|
||||
return backend.getState(this)
|
||||
private fun startTunnelStatisticsJob() = applicationScope.launch(ioDispatcher) {
|
||||
while (true) {
|
||||
if (backendIsAmneziaUserspace) {
|
||||
emitBackendStatistics(
|
||||
AmneziaStatistics(
|
||||
userspaceAmneziaBackend.get().getStatistics(this@WireGuardTunnel),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
emitBackendStatistics(WireGuardStatistics(backend().getStatistics(this@WireGuardTunnel)))
|
||||
}
|
||||
delay(Constants.VPN_STATISTIC_CHECK_INTERVAL)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStateChange(state : Tunnel.State) {
|
||||
_state.tryEmit(state)
|
||||
override fun onStateChange(state: State) {
|
||||
handleStateChange(TunnelState.from(state))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.tunnel.model
|
||||
|
||||
import io.objectbox.annotation.Entity
|
||||
import io.objectbox.annotation.Id
|
||||
|
||||
@Entity
|
||||
data class Settings(
|
||||
@Id
|
||||
var id : Long = 0,
|
||||
var isAutoTunnelEnabled : Boolean = false,
|
||||
var isTunnelOnMobileDataEnabled : Boolean = false,
|
||||
var trustedNetworkSSIDs : MutableList<String> = mutableListOf(),
|
||||
var defaultTunnel : String? = null
|
||||
)
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.tunnel.model
|
||||
|
||||
import com.wireguard.config.Config
|
||||
import io.objectbox.annotation.ConflictStrategy
|
||||
import io.objectbox.annotation.Entity
|
||||
import io.objectbox.annotation.Id
|
||||
import io.objectbox.annotation.Unique
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.InputStream
|
||||
|
||||
|
||||
@Entity
|
||||
@Serializable
|
||||
data class TunnelConfig(
|
||||
@Id
|
||||
var id : Long = 0,
|
||||
@Unique(onConflict = ConflictStrategy.REPLACE)
|
||||
var name : String,
|
||||
var wgQuick : String
|
||||
) {
|
||||
override fun toString(): String {
|
||||
return Json.encodeToString(serializer(), this)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun from(string : String) : TunnelConfig {
|
||||
return Json.decodeFromString<TunnelConfig>(string)
|
||||
}
|
||||
fun configFromQuick(wgQuick: String): Config {
|
||||
val inputStream: InputStream = wgQuick.byteInputStream()
|
||||
val reader = inputStream.bufferedReader(Charsets.UTF_8)
|
||||
return Config.parse(reader)
|
||||
}
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.tunnel.statistics
|
||||
|
||||
import org.amnezia.awg.backend.Statistics
|
||||
import org.amnezia.awg.crypto.Key
|
||||
|
||||
class AmneziaStatistics(private val statistics: Statistics) : TunnelStatistics() {
|
||||
override fun peerStats(peer: Key): PeerStats? {
|
||||
val key = Key.fromBase64(peer.toBase64())
|
||||
val stats = statistics.peer(key)
|
||||
return stats?.let {
|
||||
PeerStats(
|
||||
rxBytes = stats.rxBytes,
|
||||
txBytes = stats.txBytes,
|
||||
latestHandshakeEpochMillis = stats.latestHandshakeEpochMillis,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun isTunnelStale(): Boolean {
|
||||
return statistics.isStale
|
||||
}
|
||||
|
||||
override fun getPeers(): Array<Key> {
|
||||
return statistics.peers()
|
||||
}
|
||||
|
||||
override fun rx(): Long {
|
||||
return statistics.totalRx()
|
||||
}
|
||||
|
||||
override fun tx(): Long {
|
||||
return statistics.totalTx()
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.tunnel.statistics
|
||||
|
||||
import org.amnezia.awg.crypto.Key
|
||||
|
||||
abstract class TunnelStatistics {
|
||||
@JvmRecord
|
||||
data class PeerStats(val rxBytes: Long, val txBytes: Long, val latestHandshakeEpochMillis: Long)
|
||||
|
||||
abstract fun peerStats(peer: Key): PeerStats?
|
||||
|
||||
abstract fun isTunnelStale(): Boolean
|
||||
|
||||
abstract fun getPeers(): Array<Key>
|
||||
|
||||
abstract fun rx(): Long
|
||||
|
||||
abstract fun tx(): Long
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.tunnel.statistics
|
||||
|
||||
import com.wireguard.android.backend.Statistics
|
||||
import org.amnezia.awg.crypto.Key
|
||||
|
||||
class WireGuardStatistics(private val statistics: Statistics) : TunnelStatistics() {
|
||||
override fun peerStats(peer: Key): PeerStats? {
|
||||
val key = com.wireguard.crypto.Key.fromBase64(peer.toBase64())
|
||||
val peerStats = statistics.peer(key)
|
||||
return peerStats?.let {
|
||||
PeerStats(
|
||||
txBytes = peerStats.txBytes,
|
||||
rxBytes = peerStats.rxBytes,
|
||||
latestHandshakeEpochMillis = peerStats.latestHandshakeEpochMillis,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun isTunnelStale(): Boolean {
|
||||
return statistics.isStale
|
||||
}
|
||||
|
||||
override fun getPeers(): Array<Key> {
|
||||
return statistics.peers().map {
|
||||
Key.fromBase64(it.toBase64())
|
||||
}.toTypedArray()
|
||||
}
|
||||
|
||||
override fun rx(): Long {
|
||||
return statistics.totalRx()
|
||||
}
|
||||
|
||||
override fun tx(): Long {
|
||||
return statistics.totalTx()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui
|
||||
|
||||
data class AppUiState(
|
||||
val snackbarMessage: String = "",
|
||||
val snackbarMessageConsumed: Boolean = true,
|
||||
val vpnPermissionAccepted: Boolean = false,
|
||||
val notificationPermissionAccepted: Boolean = false,
|
||||
val requestPermissions: Boolean = false
|
||||
)
|
||||
@@ -0,0 +1,123 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.wireguard.android.backend.GoBackend
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class AppViewModel
|
||||
@Inject
|
||||
constructor() : ViewModel() {
|
||||
|
||||
val vpnIntent: Intent? = GoBackend.VpnService.prepare(WireGuardAutoTunnel.instance)
|
||||
|
||||
private val _appUiState = MutableStateFlow(
|
||||
AppUiState(
|
||||
vpnPermissionAccepted = vpnIntent == null,
|
||||
),
|
||||
)
|
||||
val appUiState = _appUiState.asStateFlow()
|
||||
|
||||
|
||||
fun isRequiredPermissionGranted(): Boolean {
|
||||
val allAccepted =
|
||||
(_appUiState.value.vpnPermissionAccepted && _appUiState.value.vpnPermissionAccepted)
|
||||
if (!allAccepted) requestPermissions()
|
||||
return allAccepted
|
||||
}
|
||||
|
||||
private fun requestPermissions() {
|
||||
_appUiState.update {
|
||||
it.copy(
|
||||
requestPermissions = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun permissionsRequested() {
|
||||
_appUiState.update {
|
||||
it.copy(
|
||||
requestPermissions = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun openWebPage(url: String, context: Context) {
|
||||
try {
|
||||
val webpage: Uri = Uri.parse(url)
|
||||
val intent = Intent(Intent.ACTION_VIEW, webpage).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
context.startActivity(intent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Timber.e(e)
|
||||
showSnackbarMessage(context.getString(R.string.no_browser_detected))
|
||||
}
|
||||
}
|
||||
|
||||
fun onVpnPermissionAccepted() {
|
||||
_appUiState.update {
|
||||
it.copy(
|
||||
vpnPermissionAccepted = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun launchEmail(context: Context) {
|
||||
try {
|
||||
val intent =
|
||||
Intent(Intent.ACTION_SENDTO).apply {
|
||||
type = Constants.EMAIL_MIME_TYPE
|
||||
putExtra(Intent.EXTRA_EMAIL, arrayOf(context.getString(R.string.my_email)))
|
||||
putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.email_subject))
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
context.startActivity(
|
||||
Intent.createChooser(intent, context.getString(R.string.email_chooser)).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
},
|
||||
)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Timber.e(e)
|
||||
showSnackbarMessage(context.getString(R.string.no_email_detected))
|
||||
}
|
||||
}
|
||||
|
||||
fun showSnackbarMessage(message: String) {
|
||||
_appUiState.update {
|
||||
it.copy(
|
||||
snackbarMessage = message,
|
||||
snackbarMessageConsumed = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun snackbarMessageConsumed() {
|
||||
_appUiState.update {
|
||||
it.copy(
|
||||
snackbarMessage = "",
|
||||
snackbarMessageConsumed = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun setNotificationPermissionAccepted(accepted: Boolean) {
|
||||
_appUiState.update {
|
||||
it.copy(
|
||||
notificationPermissionAccepted = accepted,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,165 +1,286 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import androidx.activity.SystemBarStyle
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarData
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.SnackbarResult
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import com.google.accompanist.navigation.animation.AnimatedNavHost
|
||||
import com.google.accompanist.navigation.animation.composable
|
||||
import com.google.accompanist.navigation.animation.rememberAnimatedNavController
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusProperties
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.navArgument
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.wireguard.android.backend.GoBackend
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.PermissionRequestFailedScreen
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.options.OptionsScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.pinlock.PinLockScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.TransparentSystemBars
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.logs.LogsScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
@OptIn(ExperimentalAnimationApi::class, ExperimentalMaterial3Api::class,
|
||||
ExperimentalPermissionsApi::class
|
||||
@Inject
|
||||
lateinit var appStateRepository: AppStateRepository
|
||||
|
||||
@Inject
|
||||
lateinit var settingsRepository: SettingsRepository
|
||||
|
||||
@Inject
|
||||
lateinit var serviceManager: ServiceManager
|
||||
|
||||
@OptIn(
|
||||
ExperimentalPermissionsApi::class,
|
||||
)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val isPinLockEnabled = intent.extras?.getBoolean(SplashActivity.IS_PIN_LOCK_ENABLED_KEY)
|
||||
|
||||
enableEdgeToEdge(navigationBarStyle = SystemBarStyle.dark(Color.Transparent.toArgb()))
|
||||
|
||||
lifecycleScope.launch {
|
||||
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
|
||||
val settings = settingsRepository.getSettings()
|
||||
if (settings.isAutoTunnelEnabled) {
|
||||
serviceManager.startWatcherService(application.applicationContext)
|
||||
}
|
||||
}
|
||||
|
||||
setContent {
|
||||
val navController = rememberAnimatedNavController()
|
||||
WireguardAutoTunnelTheme {
|
||||
TransparentSystemBars()
|
||||
val appViewModel = hiltViewModel<AppViewModel>()
|
||||
val appUiState by appViewModel.appUiState.collectAsStateWithLifecycle()
|
||||
val navController = rememberNavController()
|
||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val notificationPermissionState =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
|
||||
rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS) else null
|
||||
|
||||
val notificationPermissionState =
|
||||
rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS)
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
fun requestNotificationPermission() {
|
||||
if (!notificationPermissionState.status.isGranted && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
notificationPermissionState.launchPermissionRequest()
|
||||
}
|
||||
}
|
||||
|
||||
var vpnIntent by remember { mutableStateOf(GoBackend.VpnService.prepare(this)) }
|
||||
val vpnActivityResultState = rememberLauncherForActivityResult(
|
||||
val vpnActivityResultState =
|
||||
rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult(),
|
||||
onResult = {
|
||||
val accepted = (it.resultCode == RESULT_OK)
|
||||
if (accepted) {
|
||||
vpnIntent = null
|
||||
appViewModel.onVpnPermissionAccepted()
|
||||
}
|
||||
})
|
||||
LaunchedEffect(vpnIntent) {
|
||||
if (vpnIntent != null) {
|
||||
vpnActivityResultState.launch(vpnIntent)
|
||||
} else requestNotificationPermission()
|
||||
}
|
||||
|
||||
Scaffold(snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
bottomBar = if (vpnIntent == null && notificationPermissionState.status.isGranted) {
|
||||
{ BottomNavBar(navController, Routes.navItems) }
|
||||
} else {
|
||||
{}
|
||||
},
|
||||
)
|
||||
{ padding ->
|
||||
if (vpnIntent != null) {
|
||||
PermissionRequestFailedScreen(
|
||||
padding = padding,
|
||||
onRequestAgain = { vpnActivityResultState.launch(vpnIntent) },
|
||||
message = getString(R.string.vpn_permission_required),
|
||||
getString(R.string.retry)
|
||||
)
|
||||
return@Scaffold
|
||||
}
|
||||
if (!notificationPermissionState.status.isGranted) {
|
||||
PermissionRequestFailedScreen(
|
||||
padding = padding,
|
||||
onRequestAgain = {
|
||||
val intentSettings =
|
||||
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||
intentSettings.data =
|
||||
Uri.fromParts("package", this.packageName, null)
|
||||
startActivity(intentSettings);
|
||||
},
|
||||
message = getString(R.string.notification_permission_required),
|
||||
getString(R.string.open_settings)
|
||||
)
|
||||
return@Scaffold
|
||||
}
|
||||
AnimatedNavHost(navController, startDestination = Routes.Main.name) {
|
||||
composable(Routes.Main.name, enterTransition = {
|
||||
when (initialState.destination.route) {
|
||||
Routes.Settings.name, Routes.Support.name ->
|
||||
slideInHorizontally(
|
||||
initialOffsetX = { -1000 },
|
||||
animationSpec = tween(500)
|
||||
)
|
||||
|
||||
else -> {
|
||||
fadeIn(animationSpec = tween(2000))
|
||||
}
|
||||
}
|
||||
}) {
|
||||
MainScreen(padding = padding, snackbarHostState = snackbarHostState)
|
||||
fun showSnackBarMessage(message: StringValue) {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
val result =
|
||||
snackbarHostState.showSnackbar(
|
||||
message = message.asString(this@MainActivity),
|
||||
duration = SnackbarDuration.Short,
|
||||
)
|
||||
when (result) {
|
||||
SnackbarResult.ActionPerformed,
|
||||
SnackbarResult.Dismissed -> {
|
||||
snackbarHostState.currentSnackbarData?.dismiss()
|
||||
}
|
||||
composable(Routes.Settings.name, enterTransition = {
|
||||
when (initialState.destination.route) {
|
||||
Routes.Main.name ->
|
||||
slideInHorizontally(
|
||||
initialOffsetX = { 1000 },
|
||||
animationSpec = tween(500)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Routes.Support.name -> {
|
||||
slideInHorizontally(
|
||||
initialOffsetX = { -1000 },
|
||||
animationSpec = tween(500)
|
||||
)
|
||||
}
|
||||
LaunchedEffect(appUiState.requestPermissions) {
|
||||
if (appUiState.requestPermissions) {
|
||||
appViewModel.permissionsRequested()
|
||||
if (notificationPermissionState != null && !notificationPermissionState.status.isGranted
|
||||
) {
|
||||
showSnackBarMessage(StringValue.StringResource(R.string.notification_permission_required))
|
||||
return@LaunchedEffect notificationPermissionState.launchPermissionRequest()
|
||||
}
|
||||
if (!appUiState.vpnPermissionAccepted) {
|
||||
return@LaunchedEffect appViewModel.vpnIntent?.let {
|
||||
vpnActivityResultState.launch(
|
||||
it,
|
||||
)
|
||||
}!!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
fadeIn(animationSpec = tween(2000))
|
||||
}
|
||||
WireguardAutoTunnelTheme {
|
||||
LaunchedEffect(Unit) {
|
||||
appViewModel.setNotificationPermissionAccepted(
|
||||
notificationPermissionState?.status?.isGranted ?: true,
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(appUiState.snackbarMessageConsumed) {
|
||||
if (!appUiState.snackbarMessageConsumed) {
|
||||
showSnackBarMessage(StringValue.DynamicString(appUiState.snackbarMessage))
|
||||
appViewModel.snackbarMessageConsumed()
|
||||
}
|
||||
}
|
||||
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
Scaffold(
|
||||
snackbarHost = {
|
||||
SnackbarHost(snackbarHostState) { snackbarData: SnackbarData ->
|
||||
CustomSnackBar(
|
||||
snackbarData.visuals.message,
|
||||
isRtl = false,
|
||||
containerColor =
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(
|
||||
2.dp,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
//TODO refactor
|
||||
modifier = Modifier
|
||||
.focusable()
|
||||
.focusProperties {
|
||||
when (navBackStackEntry?.destination?.route) {
|
||||
Screen.Lock.route -> Unit
|
||||
else -> up = focusRequester
|
||||
}
|
||||
}) { SettingsScreen(padding = padding, snackbarHostState = snackbarHostState, navController = navController) }
|
||||
composable(Routes.Support.name, enterTransition = {
|
||||
when (initialState.destination.route) {
|
||||
Routes.Settings.name, Routes.Main.name ->
|
||||
slideInHorizontally(
|
||||
initialOffsetX = { 1000 },
|
||||
animationSpec = tween(500)
|
||||
)
|
||||
|
||||
else -> {
|
||||
fadeIn(animationSpec = tween(2000))
|
||||
}
|
||||
},
|
||||
bottomBar = {
|
||||
BottomNavBar(
|
||||
navController,
|
||||
listOf(
|
||||
Screen.Main.navItem,
|
||||
Screen.Settings.navItem,
|
||||
Screen.Support.navItem,
|
||||
),
|
||||
)
|
||||
},
|
||||
) { padding ->
|
||||
NavHost(
|
||||
navController,
|
||||
startDestination = (if (isPinLockEnabled == true) Screen.Lock.route else Screen.Main.route),
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
composable(
|
||||
Screen.Main.route,
|
||||
) {
|
||||
MainScreen(
|
||||
focusRequester = focusRequester,
|
||||
appViewModel = appViewModel,
|
||||
navController = navController,
|
||||
)
|
||||
}
|
||||
composable(
|
||||
Screen.Settings.route,
|
||||
) {
|
||||
SettingsScreen(
|
||||
appViewModel = appViewModel,
|
||||
navController = navController,
|
||||
focusRequester = focusRequester,
|
||||
)
|
||||
}
|
||||
composable(
|
||||
Screen.Support.route,
|
||||
) {
|
||||
SupportScreen(
|
||||
focusRequester = focusRequester,
|
||||
appViewModel = appViewModel,
|
||||
navController = navController,
|
||||
)
|
||||
}
|
||||
composable(Screen.Support.Logs.route) {
|
||||
LogsScreen()
|
||||
}
|
||||
composable(
|
||||
"${Screen.Config.route}/{id}?configType={configType}",
|
||||
arguments =
|
||||
listOf(
|
||||
navArgument("id") {
|
||||
type = NavType.StringType
|
||||
defaultValue = "0"
|
||||
},
|
||||
navArgument("configType") {
|
||||
type = NavType.StringType
|
||||
defaultValue = ConfigType.WIREGUARD.name
|
||||
},
|
||||
),
|
||||
) {
|
||||
val id = it.arguments?.getString("id")
|
||||
val configType = ConfigType.valueOf(
|
||||
it.arguments?.getString("configType") ?: ConfigType.WIREGUARD.name,
|
||||
)
|
||||
if (!id.isNullOrBlank()) {
|
||||
ConfigScreen(
|
||||
navController = navController,
|
||||
tunnelId = id,
|
||||
appViewModel = appViewModel,
|
||||
focusRequester = focusRequester,
|
||||
configType = configType,
|
||||
)
|
||||
}
|
||||
}) { SupportScreen(padding = padding) }
|
||||
}
|
||||
composable("${Screen.Option.route}/{id}") {
|
||||
val id = it.arguments?.getString("id")
|
||||
if (!id.isNullOrBlank()) {
|
||||
OptionsScreen(
|
||||
navController = navController,
|
||||
tunnelId = id,
|
||||
appViewModel = appViewModel,
|
||||
focusRequester = focusRequester,
|
||||
)
|
||||
}
|
||||
}
|
||||
composable(Screen.Lock.route) {
|
||||
PinLockScreen(
|
||||
navController = navController,
|
||||
appViewModel = appViewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui
|
||||
|
||||
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 com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
|
||||
|
||||
enum class Routes {
|
||||
Main,
|
||||
Settings,
|
||||
Support;
|
||||
|
||||
|
||||
companion object {
|
||||
val navItems = listOf(
|
||||
BottomNavItem(
|
||||
name = "Tunnels",
|
||||
route = Main.name,
|
||||
icon = Icons.Rounded.Home,
|
||||
),
|
||||
BottomNavItem(
|
||||
name = "Settings",
|
||||
route = Settings.name,
|
||||
icon = Icons.Rounded.Settings,
|
||||
),
|
||||
BottomNavItem(
|
||||
name = "Support",
|
||||
route = Support.name,
|
||||
icon = Icons.Rounded.QuestionMark,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user