mirror of
https://github.com/wgtunnel/android.git
synced 2026-07-03 14:07:49 +02:00
Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -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,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,99 @@
|
|||||||
|
# name of the workflow
|
||||||
|
name: Android CI Tag Deployment (Pre-release)
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '*.*.*-**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build Signed APK
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# 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
|
||||||
|
run: ./gradlew :app:assembleFdroidRelease -x test
|
||||||
|
|
||||||
|
# get fdroid flavor release apk path
|
||||||
|
- name: Get apk path
|
||||||
|
id: apk-path
|
||||||
|
run: echo "path=$(find . -regex '^.*/build/outputs/apk/fdroid/release/.*\.apk$' -type f | head -1)" >> $GITHUB_OUTPUT
|
||||||
|
- name: Get version code
|
||||||
|
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.1
|
||||||
|
with:
|
||||||
|
name: wgtunnel
|
||||||
|
path: ${{ steps.apk-path.outputs.path }}
|
||||||
|
- name: Download APK from build
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: wgtunnel
|
||||||
|
- name: Create Release with Fastlane changelog notes
|
||||||
|
id: create_release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
# fix hardcode changelog file name
|
||||||
|
body_path: ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/${{ env.VERSION_CODE }}.txt
|
||||||
|
tag_name: ${{ github.ref_name }}
|
||||||
|
name: ${{ github.ref_name }}
|
||||||
|
draft: false
|
||||||
|
prerelease: true
|
||||||
|
files: ${{ github.workspace }}/${{ steps.apk-path.outputs.path }}
|
||||||
|
- 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 Beta track 🚀
|
||||||
|
run: (cd ${{ github.workspace }} && bundle install && bundle exec fastlane beta)
|
||||||
|
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
# name of the workflow
|
||||||
|
name: Android CI Tag Deployment (Release)
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '*.*.*'
|
||||||
|
- '!*.*.*-**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build Signed APK
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# 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
|
||||||
|
run: ./gradlew :app:assembleFdroidRelease -x test
|
||||||
|
|
||||||
|
# get fdroid flavor release apk path
|
||||||
|
- name: Get apk path
|
||||||
|
id: apk-path
|
||||||
|
run: echo "path=$(find . -regex '^.*/build/outputs/apk/fdroid/release/.*\.apk$' -type f | head -1)" >> $GITHUB_OUTPUT
|
||||||
|
- name: Get version code
|
||||||
|
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.1
|
||||||
|
with:
|
||||||
|
name: wgtunnel
|
||||||
|
path: ${{ steps.apk-path.outputs.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
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.PAT }}
|
||||||
|
repository: zaneschepke/fdroid
|
||||||
|
event-type: fdroid-update
|
||||||
|
- name: Create Release with Fastlane changelog notes
|
||||||
|
id: create_release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
# fix hardcode changelog file name
|
||||||
|
body_path: ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/${{ env.VERSION_CODE }}.txt
|
||||||
|
tag_name: ${{ github.ref_name }}
|
||||||
|
name: ${{ github.ref_name }}
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
files: ${{ github.workspace }}/${{ steps.apk-path.outputs.path }}
|
||||||
|
- 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 production)
|
||||||
|
|
||||||
@@ -69,3 +69,5 @@ lint/tmp/
|
|||||||
# App Specific cases
|
# App Specific cases
|
||||||
app/release/output.json
|
app/release/output.json
|
||||||
.idea/codeStyles/
|
.idea/codeStyles/
|
||||||
|
# where we keep our signing secrets locally
|
||||||
|
app/signing.properties
|
||||||
|
|||||||
@@ -13,22 +13,18 @@ WG Tunnel
|
|||||||
|
|
||||||
|
|
||||||
[](https://play.google.com/store/apps/details?id=com.zaneschepke.wireguardautotunnel)
|
[](https://play.google.com/store/apps/details?id=com.zaneschepke.wireguardautotunnel)
|
||||||
[](https://www.amazon.com/gp/product/B0CFGGL7WK)
|
|
||||||
[](https://f-droid.org/packages/com.zaneschepke.wireguardautotunnel/)
|
[](https://f-droid.org/packages/com.zaneschepke.wireguardautotunnel/)
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div align="center">
|
|
||||||
|
|
||||||
[](https://ko-fi.com/N4N8NMJN2)
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div align="left">
|
<div align="left">
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -37,38 +33,70 @@ This is an alternative Android Application for [WireGuard](https://www.wireguard
|
|||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
<p float="center">
|
<p float="center">
|
||||||
<img label="Main" style="padding-right:25px" src="asset/main_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="asset/config_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="asset/settings_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="asset/support_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>
|
</p>
|
||||||
|
|
||||||
<div align="left">
|
<div align="left">
|
||||||
|
|
||||||
## Inspiration
|
## 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
|
## Features
|
||||||
|
|
||||||
* Add tunnels via .conf file
|
* Add tunnels via .conf file, zip, manual entry, or QR code
|
||||||
* Auto connect to VPN based on Wi-Fi SSID
|
* Auto connect to VPN based on Wi-Fi SSID, ethernet, or mobile data
|
||||||
* Split tunneling by application with search
|
* Split tunneling by application with search
|
||||||
* Always-on VPN for Android support
|
* WireGuard support for kernel and userspace modes
|
||||||
* Quick tile support for vpn toggling
|
* Always-On VPN support
|
||||||
* Dynamic shortcuts support for automation integration
|
* Export tunnels to zip
|
||||||
* Configurable Trusted Network list
|
* Quick tile support for VPN toggling
|
||||||
* Optional auto connect on mobile data
|
* Static shortcuts support for primary tunnel for automation integration
|
||||||
|
* Intent automation support for all tunnels
|
||||||
* Automatic service restart after reboot
|
* Automatic service restart after reboot
|
||||||
* Service will stay running in background after app has been closed
|
* Battery preservation measures
|
||||||
|
|
||||||
|
## Docs (WIP)
|
||||||
|
|
||||||
|
Basic documentation of the feature and behaviors of this app can be found [here](https://zaneschepke.com/wgtunnel-docs/overview.html).
|
||||||
|
|
||||||
|
The repository for these docs can be found [here](https://github.com/zaneschepke/wgtunnel-docs).
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
```
|
```
|
||||||
$ git clone https://github.com/zaneschepke/wgtunnel
|
$ git clone https://github.com/zaneschepke/wgtunnel
|
||||||
$ cd wgtunnel
|
$ cd wgtunnel
|
||||||
$ ./gradlew assembleRelease
|
```
|
||||||
|
|
||||||
|
Create a personal access token (classic) in GitHuv to be able to pull the wireguard-android github dependencies from GitHub packages
|
||||||
|
as documented [here](https://docs.github.com/en/packages/learn-github-packages/introduction-to-github-packages#authenticating-to-github-packages).
|
||||||
|
|
||||||
|
Alternatively, you can clone [wireguard-android](https://github.com/zaneschepke/wireguard-android) and run the following command to publish the dependency to your local maven repository (requires you have maven installed). This is the ideal approach
|
||||||
|
if you intent to make changes to this lib.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ git clone https://github.com/zaneschepke/wireguard-android
|
||||||
|
$ cd wireguard-android
|
||||||
|
$ brew install maven
|
||||||
|
$ ./gradlew publishToMavenLocal
|
||||||
|
```
|
||||||
|
|
||||||
|
The [wireguard-android](https://github.com/zaneschepke/wireguard-android) dependency is a fork of the official [wireguard-android](https://github.com/WireGuard/wireguard-android) library.
|
||||||
|
|
||||||
|
Add the following lines to local.properties file:
|
||||||
|
```
|
||||||
|
GH_USER=<your github username>
|
||||||
|
GH_TOKEN=<the personal access token with read package permission you just created>
|
||||||
|
```
|
||||||
|
|
||||||
|
And then build the app:
|
||||||
|
```
|
||||||
|
$ ./gradlew assembleDebug
|
||||||
```
|
```
|
||||||
|
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
+105
-46
@@ -1,3 +1,5 @@
|
|||||||
|
import java.util.Properties
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
alias(libs.plugins.kotlin.android)
|
alias(libs.plugins.kotlin.android)
|
||||||
@@ -7,49 +9,108 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.zaneschepke.wireguardautotunnel"
|
namespace = Constants.APP_ID
|
||||||
compileSdk = 34
|
compileSdk = Constants.TARGET_SDK
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "com.zaneschepke.wireguardautotunnel"
|
applicationId = Constants.APP_ID
|
||||||
minSdk = 26
|
minSdk = Constants.MIN_SDK
|
||||||
targetSdk = 34
|
targetSdk = Constants.TARGET_SDK
|
||||||
versionCode = 31400
|
versionCode = Constants.VERSION_CODE
|
||||||
versionName = "3.1.4"
|
versionName = Constants.VERSION_NAME
|
||||||
|
|
||||||
multiDexEnabled = true
|
ksp { arg("room.schemaLocation", "$projectDir/schemas") }
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
getByName("debug").assets.srcDirs(files("$projectDir/schemas")) // Room
|
||||||
|
}
|
||||||
|
|
||||||
resourceConfigurations.addAll(listOf("en"))
|
resourceConfigurations.addAll(listOf("en"))
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables { useSupportLibrary = true }
|
||||||
useSupportLibrary = true
|
}
|
||||||
|
|
||||||
|
signingConfigs {
|
||||||
|
create(Constants.RELEASE) {
|
||||||
|
val properties =
|
||||||
|
Properties().apply {
|
||||||
|
// created local file for signing details
|
||||||
|
try {
|
||||||
|
load(file("signing.properties").reader())
|
||||||
|
} catch (_: Exception) {
|
||||||
|
load(file("signing_template.properties").reader())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// try to get secrets from env first for pipeline build, then properties file for local
|
||||||
|
// build
|
||||||
|
storeFile =
|
||||||
|
file(
|
||||||
|
System.getenv()
|
||||||
|
.getOrDefault(
|
||||||
|
Constants.KEY_STORE_PATH_VAR,
|
||||||
|
properties.getProperty(Constants.KEY_STORE_PATH_VAR),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
storePassword =
|
||||||
|
System.getenv()
|
||||||
|
.getOrDefault(
|
||||||
|
Constants.STORE_PASS_VAR,
|
||||||
|
properties.getProperty(Constants.STORE_PASS_VAR),
|
||||||
|
)
|
||||||
|
keyAlias =
|
||||||
|
System.getenv()
|
||||||
|
.getOrDefault(
|
||||||
|
Constants.KEY_ALIAS_VAR,
|
||||||
|
properties.getProperty(Constants.KEY_ALIAS_VAR),
|
||||||
|
)
|
||||||
|
keyPassword =
|
||||||
|
System.getenv()
|
||||||
|
.getOrDefault(
|
||||||
|
Constants.KEY_PASS_VAR,
|
||||||
|
properties.getProperty(Constants.KEY_PASS_VAR),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
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 {
|
release {
|
||||||
isDebuggable = false
|
isDebuggable = false
|
||||||
isMinifyEnabled = true
|
isMinifyEnabled = true
|
||||||
isShrinkResources = true
|
isShrinkResources = true
|
||||||
proguardFiles(
|
proguardFiles(
|
||||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
"proguard-rules.pro"
|
"proguard-rules.pro",
|
||||||
)
|
)
|
||||||
|
signingConfig = signingConfigs.getByName(Constants.RELEASE)
|
||||||
}
|
}
|
||||||
debug {
|
debug { isDebuggable = true }
|
||||||
isDebuggable = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
flavorDimensions.add("type")
|
flavorDimensions.add(Constants.TYPE)
|
||||||
productFlavors {
|
productFlavors {
|
||||||
create("fdroid") {
|
create("fdroid") {
|
||||||
dimension = "type"
|
dimension = Constants.TYPE
|
||||||
|
proguardFile("fdroid-rules.pro")
|
||||||
}
|
}
|
||||||
create("general") {
|
create("general") {
|
||||||
dimension = "type"
|
dimension = Constants.TYPE
|
||||||
if (BuildHelper.isReleaseBuild(gradle) && BuildHelper.isGeneralFlavor(gradle))
|
if (BuildHelper.isReleaseBuild(gradle) && BuildHelper.isGeneralFlavor(gradle)) {
|
||||||
{
|
|
||||||
apply(plugin = "com.google.gms.google-services")
|
apply(plugin = "com.google.gms.google-services")
|
||||||
apply(plugin = "com.google.firebase.crashlytics")
|
apply(plugin = "com.google.firebase.crashlytics")
|
||||||
}
|
}
|
||||||
@@ -58,26 +119,19 @@ android {
|
|||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
isCoreLibraryDesugaringEnabled = true
|
||||||
}
|
}
|
||||||
kotlinOptions {
|
kotlinOptions { jvmTarget = Constants.JVM_TARGET }
|
||||||
jvmTarget = "17"
|
|
||||||
}
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose = true
|
compose = true
|
||||||
buildConfig = true
|
buildConfig = true
|
||||||
|
|
||||||
}
|
|
||||||
composeOptions {
|
|
||||||
kotlinCompilerExtensionVersion = libs.versions.composeCompiler.get()
|
|
||||||
}
|
|
||||||
packaging {
|
|
||||||
resources {
|
|
||||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
composeOptions { kotlinCompilerExtensionVersion = Constants.COMPOSE_COMPILER_EXTENSION_VERSION }
|
||||||
|
packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } }
|
||||||
}
|
}
|
||||||
|
|
||||||
val generalImplementation by configurations
|
val generalImplementation by configurations
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
@@ -91,21 +145,25 @@ dependencies {
|
|||||||
implementation(libs.androidx.material3)
|
implementation(libs.androidx.material3)
|
||||||
implementation(libs.androidx.appcompat)
|
implementation(libs.androidx.appcompat)
|
||||||
|
|
||||||
//test
|
// test
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
|
testImplementation(libs.androidx.junit)
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
androidTestImplementation(libs.androidx.espresso.core)
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
androidTestImplementation(platform(libs.androidx.compose.bom))
|
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||||
androidTestImplementation(libs.androidx.compose.ui.test)
|
androidTestImplementation(libs.androidx.compose.ui.test)
|
||||||
|
androidTestImplementation(libs.androidx.room.testing)
|
||||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||||
debugImplementation(libs.androidx.compose.manifest)
|
debugImplementation(libs.androidx.compose.manifest)
|
||||||
|
|
||||||
//wg
|
// get tunnel lib from github packages or mavenLocal
|
||||||
implementation(libs.tunnel)
|
implementation(libs.tunnel)
|
||||||
|
coreLibraryDesugaring(libs.desugar.jdk.libs)
|
||||||
|
|
||||||
//logging
|
// logging
|
||||||
implementation(libs.timber)
|
implementation(libs.timber)
|
||||||
|
|
||||||
|
|
||||||
// compose navigation
|
// compose navigation
|
||||||
implementation(libs.androidx.navigation.compose)
|
implementation(libs.androidx.navigation.compose)
|
||||||
implementation(libs.androidx.hilt.navigation.compose)
|
implementation(libs.androidx.hilt.navigation.compose)
|
||||||
@@ -114,39 +172,40 @@ dependencies {
|
|||||||
implementation(libs.hilt.android)
|
implementation(libs.hilt.android)
|
||||||
ksp(libs.hilt.android.compiler)
|
ksp(libs.hilt.android.compiler)
|
||||||
|
|
||||||
//accompanist
|
// accompanist
|
||||||
implementation(libs.accompanist.systemuicontroller)
|
|
||||||
implementation(libs.accompanist.permissions)
|
implementation(libs.accompanist.permissions)
|
||||||
implementation(libs.accompanist.flowlayout)
|
implementation(libs.accompanist.flowlayout)
|
||||||
implementation(libs.accompanist.navigation.animation)
|
|
||||||
implementation(libs.accompanist.drawablepainter)
|
implementation(libs.accompanist.drawablepainter)
|
||||||
|
|
||||||
//room
|
// storage
|
||||||
implementation(libs.androidx.room.runtime)
|
implementation(libs.androidx.room.runtime)
|
||||||
ksp(libs.androidx.room.compiler)
|
ksp(libs.androidx.room.compiler)
|
||||||
implementation(libs.androidx.room.ktx)
|
implementation(libs.androidx.room.ktx)
|
||||||
|
implementation(libs.androidx.datastore.preferences)
|
||||||
|
|
||||||
//lifecycle
|
// lifecycle
|
||||||
implementation(libs.lifecycle.runtime.compose)
|
implementation(libs.lifecycle.runtime.compose)
|
||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
implementation(libs.androidx.lifecycle.process)
|
implementation(libs.androidx.lifecycle.process)
|
||||||
|
|
||||||
|
// icons
|
||||||
//icons
|
|
||||||
implementation(libs.material.icons.extended)
|
implementation(libs.material.icons.extended)
|
||||||
//serialization
|
// serialization
|
||||||
implementation(libs.kotlinx.serialization.json)
|
implementation(libs.kotlinx.serialization.json)
|
||||||
|
|
||||||
//firebase crashlytics
|
// firebase crashlytics
|
||||||
generalImplementation(platform(libs.firebase.bom))
|
generalImplementation(platform(libs.firebase.bom))
|
||||||
generalImplementation(libs.google.firebase.crashlytics.ktx)
|
generalImplementation(libs.google.firebase.crashlytics.ktx)
|
||||||
generalImplementation(libs.google.firebase.analytics.ktx)
|
generalImplementation(libs.google.firebase.analytics.ktx)
|
||||||
|
|
||||||
//barcode scanning
|
// barcode scanning
|
||||||
implementation(libs.zxing.android.embedded)
|
implementation(libs.zxing.android.embedded)
|
||||||
implementation(libs.zxing.core)
|
implementation(libs.zxing.core)
|
||||||
|
|
||||||
//bio
|
// bio
|
||||||
implementation(libs.androidx.biometric.ktx)
|
implementation(libs.androidx.biometric.ktx)
|
||||||
|
|
||||||
}
|
// shortcuts
|
||||||
|
implementation(libs.androidx.core)
|
||||||
|
implementation(libs.androidx.core.google.shortcuts)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-dontwarn com.google.errorprone.annotations.**
|
||||||
|
|
||||||
|
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
|
||||||
|
<fields>;
|
||||||
|
}
|
||||||
Vendored
+6
-1
@@ -18,4 +18,9 @@
|
|||||||
|
|
||||||
# If you keep the line number information, uncomment this to
|
# If you keep the line number information, uncomment this to
|
||||||
# hide the original source file name.
|
# 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,4 @@
|
|||||||
|
SIGNING_STORE_PASSWORD=
|
||||||
|
SIGNING_KEY_ALIAS=
|
||||||
|
SIGNING_KEY_PASSWORD=
|
||||||
|
KEY_STORE_PATH=/
|
||||||
+3
-5
@@ -1,13 +1,11 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel
|
package com.zaneschepke.wireguardautotunnel
|
||||||
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
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.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instrumented test, which will execute on an Android device.
|
* Instrumented test, which will execute on an Android device.
|
||||||
*
|
*
|
||||||
@@ -21,4 +19,4 @@ class ExampleInstrumentedTest {
|
|||||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
assertEquals("com.zaneschepke.wireguardautotunnel", appContext.packageName)
|
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 migrate4To5() {
|
||||||
|
helper.createDatabase(dbName, 4).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(
|
||||||
|
"INSERT INTO TunnelConfig (name, wg_quick)" + " VALUES ('hello', 'hello')",
|
||||||
|
)
|
||||||
|
// 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, 5, true)
|
||||||
|
// MigrationTestHelper automatically verifies the schema changes,
|
||||||
|
// but you need to validate that the data was migrated properly.
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,68 +1,82 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
<uses-permission
|
||||||
|
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||||
android:maxSdkVersion="32" />
|
android:maxSdkVersion="32" />
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
android:maxSdkVersion="32"
|
android:maxSdkVersion="32"
|
||||||
tools:ignore="ScopedStorage" />
|
tools:ignore="ScopedStorage" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"
|
<uses-permission
|
||||||
|
android:name="android.permission.ACCESS_WIFI_STATE"
|
||||||
android:maxSdkVersion="30"
|
android:maxSdkVersion="30"
|
||||||
tools:ignore="LeanbackUsesWifi" />
|
tools:ignore="LeanbackUsesWifi" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_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_FINE_LOCATION" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
|
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
|
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION"/>
|
<!--foreground service exempt android 14-->
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING"/>
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
|
||||||
|
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||||
|
|
||||||
<!--foreground service permissions-->
|
<!--foreground service permissions-->
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||||
<!--start service on boot permission-->
|
<!--start service on boot permission-->
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
<!--android tv support-->
|
<!--android tv support-->
|
||||||
<uses-feature android:name="android.software.leanback"
|
<uses-feature
|
||||||
|
android:name="android.software.leanback"
|
||||||
android:required="false" />
|
android:required="false" />
|
||||||
<uses-feature android:name="android.hardware.touchscreen"
|
<uses-feature
|
||||||
|
android:name="android.hardware.touchscreen"
|
||||||
android:required="false" />
|
android:required="false" />
|
||||||
<uses-feature
|
<uses-feature
|
||||||
android:name="android.hardware.location.gps"
|
android:name="android.hardware.location.gps"
|
||||||
android:required="false" />
|
android:required="false" />
|
||||||
<uses-feature
|
<uses-feature
|
||||||
android:name="android.hardware.screen.portrait"
|
android:name="android.hardware.screen.portrait"
|
||||||
android:required="false" />
|
android:required="false" />
|
||||||
|
|
||||||
<queries>
|
<queries>
|
||||||
<intent>
|
<intent>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
</intent>
|
</intent>
|
||||||
</queries>
|
</queries>
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
|
||||||
android:name=".WireGuardAutoTunnel"
|
android:name=".WireGuardAutoTunnel"
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:banner="@mipmap/ic_banner"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
|
android:enableOnBackInvokedCallback="true"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:banner="@mipmap/ic_banner"
|
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.WireguardAutoTunnel"
|
android:theme="@style/Theme.WireguardAutoTunnel"
|
||||||
tools:targetApi="31">
|
tools:targetApi="tiramisu">
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.MainActivity"
|
android:name=".ui.MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:theme="@style/Theme.WireguardAutoTunnel">
|
android:theme="@style/Theme.WireguardAutoTunnel">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||||
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES"/>
|
|
||||||
|
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<meta-data android:name="android.app.shortcuts"
|
<meta-data
|
||||||
|
android:name="android.app.shortcuts"
|
||||||
android:resource="@xml/shortcuts" />
|
android:resource="@xml/shortcuts" />
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
@@ -72,59 +86,74 @@
|
|||||||
android:theme="@style/zxing_CaptureTheme"
|
android:theme="@style/zxing_CaptureTheme"
|
||||||
android:windowSoftInputMode="stateAlwaysHidden" />
|
android:windowSoftInputMode="stateAlwaysHidden" />
|
||||||
<activity
|
<activity
|
||||||
android:finishOnTaskLaunch="true"
|
android:name=".service.shortcut.ShortcutsActivity"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:theme="@android:style/Theme.NoDisplay"
|
android:finishOnTaskLaunch="true"
|
||||||
android:name=".service.shortcut.ShortcutsActivity"/>
|
android:theme="@android:style/Theme.NoDisplay" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".service.foreground.ForegroundService"
|
android:name=".service.foreground.ForegroundService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:foregroundServiceType="remoteMessaging"
|
android:exported="false"
|
||||||
android:exported="false">
|
android:foregroundServiceType="systemExempted|specialUse"
|
||||||
</service>
|
tools:node="merge" />
|
||||||
<service
|
<service
|
||||||
android:exported="true"
|
|
||||||
android:name=".service.tile.TunnelControlTile"
|
android:name=".service.tile.TunnelControlTile"
|
||||||
android:icon="@drawable/shield"
|
android:exported="true"
|
||||||
|
android:icon="@drawable/ic_launcher"
|
||||||
android:label="WG Tunnel"
|
android:label="WG Tunnel"
|
||||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||||
<meta-data android:name="android.service.quicksettings.ACTIVE_TILE"
|
<meta-data
|
||||||
|
android:name="android.service.quicksettings.ACTIVE_TILE"
|
||||||
android:value="true" />
|
android:value="true" />
|
||||||
<meta-data android:name="android.service.quicksettings.TOGGLEABLE_TILE"
|
<meta-data
|
||||||
|
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
|
||||||
android:value="true" />
|
android:value="true" />
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
<service
|
<service
|
||||||
android:name=".service.foreground.WireGuardTunnelService"
|
android:name=".service.foreground.WireGuardTunnelService"
|
||||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="systemExempted|specialUse"
|
||||||
|
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||||
android:persistent="true"
|
android:persistent="true"
|
||||||
android:foregroundServiceType="remoteMessaging"
|
tools:node="merge">
|
||||||
android:exported="false">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.net.VpnService"/>
|
<action android:name="android.net.VpnService" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<meta-data android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
|
<meta-data
|
||||||
|
android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
|
||||||
android:value="true" />
|
android:value="true" />
|
||||||
</service>
|
</service>
|
||||||
<service
|
<service
|
||||||
android:name=".service.foreground.WireGuardConnectivityWatcherService"
|
android:name=".service.foreground.WireGuardConnectivityWatcherService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:stopWithTask="false"
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="systemExempted|specialUse"
|
||||||
android:persistent="true"
|
android:persistent="true"
|
||||||
android:foregroundServiceType="location"
|
android:stopWithTask="false"
|
||||||
android:permission=""
|
tools:node="merge" />
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".receiver.BootReceiver"
|
||||||
|
android:enabled="true"
|
||||||
android:exported="false">
|
android:exported="false">
|
||||||
</service>
|
|
||||||
<receiver android:enabled="true" android:name=".receiver.BootReceiver"
|
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
<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>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
<receiver android:exported="false" android:name=".receiver.NotificationActionReceiver"/>
|
<receiver
|
||||||
|
android:name=".receiver.NotificationActionReceiver"
|
||||||
|
android:exported="false" />
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 16 KiB |
@@ -1,18 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel
|
|
||||||
|
|
||||||
object Constants {
|
|
||||||
const val MANUAL_TUNNEL_CONFIG_ID = "0"
|
|
||||||
const val VPN_CONNECTIVITY_CHECK_INTERVAL = 3000L
|
|
||||||
const val VPN_STATISTIC_CHECK_INTERVAL = 10000L
|
|
||||||
const val TOGGLE_TUNNEL_DELAY = 500L
|
|
||||||
const val FADE_IN_ANIMATION_DURATION = 1000
|
|
||||||
const val SLIDE_IN_ANIMATION_DURATION = 500
|
|
||||||
const val SLIDE_IN_TRANSITION_OFFSET = 1000
|
|
||||||
const val CONF_FILE_EXTENSION = ".conf"
|
|
||||||
const val ZIP_FILE_EXTENSION = ".zip"
|
|
||||||
const val URI_CONTENT_SCHEME = "content"
|
|
||||||
const val URI_PACKAGE_SCHEME = "package"
|
|
||||||
const val ALLOWED_FILE_TYPES = "*/*"
|
|
||||||
const val GOOGLE_TV_EXPLORER_STUB = "com.google.android.tv.frameworkpackagestubs"
|
|
||||||
const val ANDROID_TV_EXPLORER_STUB = "com.android.tv.frameworkpackagestubs"
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel
|
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlin.coroutines.CoroutineContext
|
|
||||||
import kotlin.coroutines.EmptyCoroutineContext
|
|
||||||
|
|
||||||
fun BroadcastReceiver.goAsync(
|
|
||||||
context: CoroutineContext = EmptyCoroutineContext,
|
|
||||||
block: suspend CoroutineScope.() -> Unit
|
|
||||||
) {
|
|
||||||
val pendingResult = goAsync()
|
|
||||||
@OptIn(DelicateCoroutinesApi::class) // Must run globally; there's no teardown callback.
|
|
||||||
GlobalScope.launch(context) {
|
|
||||||
try {
|
|
||||||
block()
|
|
||||||
} finally {
|
|
||||||
pendingResult.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,48 +1,34 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel
|
package com.zaneschepke.wireguardautotunnel
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.ComponentName
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import androidx.lifecycle.ProcessLifecycleOwner
|
import android.service.quicksettings.TileService
|
||||||
import androidx.lifecycle.lifecycleScope
|
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltAndroidApp
|
@HiltAndroidApp
|
||||||
class WireGuardAutoTunnel : Application() {
|
class WireGuardAutoTunnel : Application() {
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var settingsRepo : SettingsDoa
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
if(BuildConfig.DEBUG) {
|
instance = this
|
||||||
Timber.plant(Timber.DebugTree())
|
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
||||||
}
|
|
||||||
initSettings()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initSettings() {
|
|
||||||
with(ProcessLifecycleOwner.get()) {
|
|
||||||
lifecycleScope.launch {
|
|
||||||
if(settingsRepo.getAll().isEmpty()) {
|
|
||||||
settingsRepo.save(Settings())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun isRunningOnAndroidTv(context : Context) : Boolean {
|
lateinit var instance: WireGuardAutoTunnel
|
||||||
return context.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
private set
|
||||||
|
|
||||||
|
fun isRunningOnAndroidTv(): Boolean {
|
||||||
|
return instance.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun requestTileServiceStateUpdate() {
|
||||||
|
TileService.requestListeningState(
|
||||||
|
instance,
|
||||||
|
ComponentName(instance, TunnelControlTile::class.java),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.data
|
||||||
|
|
||||||
|
import androidx.room.AutoMigration
|
||||||
|
import androidx.room.Database
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import androidx.room.TypeConverters
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
|
|
||||||
|
@Database(
|
||||||
|
entities = [Settings::class, TunnelConfig::class],
|
||||||
|
version = 5,
|
||||||
|
autoMigrations =
|
||||||
|
[
|
||||||
|
AutoMigration(from = 1, to = 2),
|
||||||
|
AutoMigration(from = 2, to = 3),
|
||||||
|
AutoMigration(
|
||||||
|
from = 3,
|
||||||
|
to = 4,
|
||||||
|
),
|
||||||
|
AutoMigration(
|
||||||
|
from = 4,
|
||||||
|
to = 5,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
exportSchema = true,
|
||||||
|
)
|
||||||
|
@TypeConverters(DatabaseListConverters::class)
|
||||||
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
|
abstract fun settingDao(): SettingsDao
|
||||||
|
|
||||||
|
abstract fun tunnelConfigDoa(): TunnelConfigDao
|
||||||
|
}
|
||||||
@@ -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.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,33 @@
|
|||||||
|
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,
|
||||||
|
default_tunnel,
|
||||||
|
is_always_on_vpn_enabled,
|
||||||
|
is_tunnel_on_ethernet_enabled,
|
||||||
|
is_shortcuts_enabled,
|
||||||
|
is_battery_saver_enabled,
|
||||||
|
is_tunnel_on_wifi_enabled,
|
||||||
|
is_kernel_enabled,
|
||||||
|
is_restore_on_boot_enabled,
|
||||||
|
is_multi_tunnel_enabled)
|
||||||
|
VALUES
|
||||||
|
('false',
|
||||||
|
'false',
|
||||||
|
'[trustedSSID1,trustedSSID2]',
|
||||||
|
NULL,
|
||||||
|
'false',
|
||||||
|
'false',
|
||||||
|
'false',
|
||||||
|
'false',
|
||||||
|
'false',
|
||||||
|
'false',
|
||||||
|
'false',
|
||||||
|
'false')
|
||||||
|
""".trimIndent()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
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.model.Settings
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface SettingsDao {
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun save(t: Settings)
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun saveAll(t: List<Settings>)
|
||||||
|
|
||||||
|
@Query("SELECT * FROM settings WHERE id=:id") suspend fun getById(id: Long): Settings?
|
||||||
|
|
||||||
|
@Query("SELECT * FROM settings") suspend fun getAll(): List<Settings>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM settings LIMIT 1") fun getSettingsFlow(): Flow<Settings>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM settings") fun getAllFlow(): Flow<MutableList<Settings>>
|
||||||
|
|
||||||
|
@Delete suspend fun delete(t: Settings)
|
||||||
|
|
||||||
|
@Query("SELECT COUNT('id') FROM settings") suspend fun count(): Long
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
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.model.TunnelConfig
|
||||||
|
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: List<TunnelConfig>)
|
||||||
|
|
||||||
|
@Query("SELECT * FROM TunnelConfig WHERE id=:id") suspend fun getById(id: Long): TunnelConfig?
|
||||||
|
|
||||||
|
@Query("SELECT * FROM TunnelConfig") suspend fun getAll(): List<TunnelConfig>
|
||||||
|
|
||||||
|
@Delete suspend fun delete(t: TunnelConfig)
|
||||||
|
|
||||||
|
@Query("SELECT COUNT('id') FROM TunnelConfig") suspend fun count(): Long
|
||||||
|
|
||||||
|
@Query("SELECT * FROM tunnelconfig") fun getAllFlow(): Flow<MutableList<TunnelConfig>>
|
||||||
|
}
|
||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
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.preferencesDataStore
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
|
class DataStoreManager(private val context: Context) {
|
||||||
|
companion object {
|
||||||
|
val LOCATION_DISCLOSURE_SHOWN = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN")
|
||||||
|
val BATTERY_OPTIMIZE_DISABLE_SHOWN = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN")
|
||||||
|
}
|
||||||
|
|
||||||
|
// preferences
|
||||||
|
private val preferencesKey = "preferences"
|
||||||
|
private val Context.dataStore by
|
||||||
|
preferencesDataStore(
|
||||||
|
name = preferencesKey,
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun init() {
|
||||||
|
context.dataStore.data.first()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun <T> saveToDataStore(key: Preferences.Key<T>, value: T) =
|
||||||
|
context.dataStore.edit { it[key] = value }
|
||||||
|
|
||||||
|
fun <T> getFromStoreFlow(key: Preferences.Key<T>) = context.dataStore.data.map { it[key] }
|
||||||
|
|
||||||
|
suspend fun <T> getFromStore(key: Preferences.Key<T>) =
|
||||||
|
context.dataStore.data.first { it.contains(key) }[key]
|
||||||
|
|
||||||
|
val preferencesFlow: Flow<Preferences?> = context.dataStore.data
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.data.model
|
||||||
|
|
||||||
|
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") var isAutoTunnelEnabled: Boolean = false,
|
||||||
|
@ColumnInfo(name = "is_tunnel_on_mobile_data_enabled")
|
||||||
|
var isTunnelOnMobileDataEnabled: Boolean = false,
|
||||||
|
@ColumnInfo(name = "trusted_network_ssids")
|
||||||
|
var trustedNetworkSSIDs: MutableList<String> = mutableListOf(),
|
||||||
|
@ColumnInfo(name = "default_tunnel") var defaultTunnel: String? = null,
|
||||||
|
@ColumnInfo(name = "is_always_on_vpn_enabled") var isAlwaysOnVpnEnabled: Boolean = false,
|
||||||
|
@ColumnInfo(name = "is_tunnel_on_ethernet_enabled")
|
||||||
|
var isTunnelOnEthernetEnabled: Boolean = false,
|
||||||
|
@ColumnInfo(
|
||||||
|
name = "is_shortcuts_enabled",
|
||||||
|
defaultValue = "false",
|
||||||
|
)
|
||||||
|
var isShortcutsEnabled: Boolean = false,
|
||||||
|
@ColumnInfo(
|
||||||
|
name = "is_battery_saver_enabled",
|
||||||
|
defaultValue = "false",
|
||||||
|
)
|
||||||
|
var isBatterySaverEnabled: Boolean = false,
|
||||||
|
@ColumnInfo(
|
||||||
|
name = "is_tunnel_on_wifi_enabled",
|
||||||
|
defaultValue = "false",
|
||||||
|
)
|
||||||
|
var isTunnelOnWifiEnabled: Boolean = false,
|
||||||
|
@ColumnInfo(
|
||||||
|
name = "is_kernel_enabled",
|
||||||
|
defaultValue = "false",
|
||||||
|
)
|
||||||
|
var isKernelEnabled: Boolean = false,
|
||||||
|
@ColumnInfo(
|
||||||
|
name = "is_restore_on_boot_enabled",
|
||||||
|
defaultValue = "false",
|
||||||
|
)
|
||||||
|
var isRestoreOnBootEnabled: Boolean = false,
|
||||||
|
@ColumnInfo(
|
||||||
|
name = "is_multi_tunnel_enabled",
|
||||||
|
defaultValue = "false",
|
||||||
|
)
|
||||||
|
var isMultiTunnelEnabled: Boolean = false,
|
||||||
|
@ColumnInfo(
|
||||||
|
name = "is_auto_tunnel_paused",
|
||||||
|
defaultValue = "false",
|
||||||
|
)
|
||||||
|
var isAutoTunnelPaused: Boolean = false,
|
||||||
|
) {
|
||||||
|
fun isTunnelConfigDefault(tunnelConfig: TunnelConfig): Boolean {
|
||||||
|
return if (defaultTunnel != null) {
|
||||||
|
val defaultConfig = TunnelConfig.from(defaultTunnel!!)
|
||||||
|
(tunnelConfig.id == defaultConfig.id)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
-9
@@ -1,4 +1,4 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.repository.model
|
package com.zaneschepke.wireguardautotunnel.data.model
|
||||||
|
|
||||||
import androidx.room.ColumnInfo
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
@@ -12,24 +12,23 @@ import java.io.InputStream
|
|||||||
@Entity(indices = [Index(value = ["name"], unique = true)])
|
@Entity(indices = [Index(value = ["name"], unique = true)])
|
||||||
@Serializable
|
@Serializable
|
||||||
data class TunnelConfig(
|
data class TunnelConfig(
|
||||||
@PrimaryKey(autoGenerate = true) val id : Int = 0,
|
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
||||||
@ColumnInfo(name = "name") var name : String,
|
@ColumnInfo(name = "name") var name: String,
|
||||||
@ColumnInfo(name = "wg_quick") var wgQuick : String,
|
@ColumnInfo(name = "wg_quick") var wgQuick: String
|
||||||
){
|
) {
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return Json.encodeToString(serializer(), this)
|
return Json.encodeToString(serializer(), this)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
fun from(string: String): TunnelConfig {
|
||||||
fun from(string : String) : TunnelConfig {
|
|
||||||
return Json.decodeFromString<TunnelConfig>(string)
|
return Json.decodeFromString<TunnelConfig>(string)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun configFromQuick(wgQuick: String): Config {
|
fun configFromQuick(wgQuick: String): Config {
|
||||||
val inputStream: InputStream = wgQuick.byteInputStream()
|
val inputStream: InputStream = wgQuick.byteInputStream()
|
||||||
val reader = inputStream.bufferedReader(Charsets.UTF_8)
|
val reader = inputStream.bufferedReader(Charsets.UTF_8)
|
||||||
return Config.parse(reader)
|
return Config.parse(reader)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||||
|
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.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>
|
||||||
|
}
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||||
|
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.SettingsDao
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
class SettingsRepositoryImpl(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()
|
||||||
|
}
|
||||||
|
}
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||||
|
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.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 delete(tunnelConfig: TunnelConfig)
|
||||||
|
|
||||||
|
suspend fun count(): Int
|
||||||
|
}
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||||
|
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
class TunnelConfigRepositoryImpl(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 delete(tunnelConfig: TunnelConfig) {
|
||||||
|
tunnelConfigDao.delete(tunnelConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun count(): Int {
|
||||||
|
return tunnelConfigDao.count().toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,8 @@ package com.zaneschepke.wireguardautotunnel.module
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.AppDatabase
|
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.DatabaseCallback
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
@@ -16,11 +17,14 @@ import javax.inject.Singleton
|
|||||||
class DatabaseModule {
|
class DatabaseModule {
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideDatabase(@ApplicationContext context : Context) : AppDatabase {
|
fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
|
||||||
return Room.databaseBuilder(
|
return Room.databaseBuilder(
|
||||||
context,
|
context,
|
||||||
AppDatabase::class.java, context.getString(R.string.db_name))
|
AppDatabase::class.java,
|
||||||
|
context.getString(R.string.db_name),
|
||||||
|
)
|
||||||
.fallbackToDestructiveMigration()
|
.fallbackToDestructiveMigration()
|
||||||
|
.addCallback(DatabaseCallback())
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.module
|
||||||
|
|
||||||
|
import javax.inject.Qualifier
|
||||||
|
|
||||||
|
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class Kernel
|
||||||
@@ -1,27 +1,51 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.module
|
package com.zaneschepke.wireguardautotunnel.module
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.AppDatabase
|
import android.content.Context
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
|
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.SettingsRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepositoryImpl
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepositoryImpl
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
class RepositoryModule {
|
class RepositoryModule {
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@Provides
|
@Provides
|
||||||
fun provideSettingsRepository(appDatabase: AppDatabase) : SettingsDoa {
|
fun provideSettingsDoa(appDatabase: AppDatabase): SettingsDao {
|
||||||
return appDatabase.settingDao()
|
return appDatabase.settingDao()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@Provides
|
@Provides
|
||||||
fun provideTunnelConfigRepository(appDatabase: AppDatabase) : TunnelConfigDao {
|
fun provideTunnelConfigDoa(appDatabase: AppDatabase): TunnelConfigDao {
|
||||||
return appDatabase.tunnelConfigDoa()
|
return appDatabase.tunnelConfigDoa()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
fun provideTunnelConfigRepository(tunnelConfigDao: TunnelConfigDao): TunnelConfigRepository {
|
||||||
|
return TunnelConfigRepositoryImpl(tunnelConfigDao)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
fun provideSettingsRepository(settingsDao: SettingsDao): SettingsRepository {
|
||||||
|
return SettingsRepositoryImpl(settingsDao)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
fun providePreferencesDataStore(@ApplicationContext context: Context): DataStoreManager {
|
||||||
|
return DataStoreManager(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,20 +15,25 @@ import dagger.hilt.android.scopes.ServiceScoped
|
|||||||
@Module
|
@Module
|
||||||
@InstallIn(ServiceComponent::class)
|
@InstallIn(ServiceComponent::class)
|
||||||
abstract class ServiceModule {
|
abstract class ServiceModule {
|
||||||
|
@Binds
|
||||||
|
@ServiceScoped
|
||||||
|
abstract fun provideNotificationService(
|
||||||
|
wireGuardNotification: WireGuardNotification
|
||||||
|
): NotificationService
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@ServiceScoped
|
@ServiceScoped
|
||||||
abstract fun provideNotificationService(wireGuardNotification: WireGuardNotification) : NotificationService
|
abstract fun provideWifiService(wifiService: WifiService): NetworkService<WifiService>
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@ServiceScoped
|
@ServiceScoped
|
||||||
abstract fun provideWifiService(wifiService: WifiService) : NetworkService<WifiService>
|
abstract fun provideMobileDataService(
|
||||||
|
mobileDataService: MobileDataService
|
||||||
|
): NetworkService<MobileDataService>
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@ServiceScoped
|
@ServiceScoped
|
||||||
abstract fun provideMobileDataService(mobileDataService : MobileDataService) : NetworkService<MobileDataService>
|
abstract fun provideEthernetService(
|
||||||
|
ethernetService: EthernetService
|
||||||
@Binds
|
): NetworkService<EthernetService>
|
||||||
@ServiceScoped
|
}
|
||||||
abstract fun provideEthernetService(ethernetService: EthernetService) : NetworkService<EthernetService>
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ package com.zaneschepke.wireguardautotunnel.module
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.wireguard.android.backend.Backend
|
import com.wireguard.android.backend.Backend
|
||||||
import com.wireguard.android.backend.GoBackend
|
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.SettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.WireGuardTunnel
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.WireGuardTunnel
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
@@ -15,16 +19,33 @@ import javax.inject.Singleton
|
|||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
class TunnelModule {
|
class TunnelModule {
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideRootShell(@ApplicationContext context: Context): RootShell {
|
||||||
|
return RootShell(context)
|
||||||
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideBackend(@ApplicationContext context : Context) : Backend {
|
@Userspace
|
||||||
|
fun provideUserspaceBackend(@ApplicationContext context: Context): Backend {
|
||||||
return GoBackend(context)
|
return GoBackend(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideVpnService(backend: Backend) : VpnService {
|
@Kernel
|
||||||
return WireGuardTunnel(backend)
|
fun provideKernelBackend(@ApplicationContext context: Context, rootShell: RootShell): Backend {
|
||||||
|
return WgQuickBackend(context, rootShell, ToolsInstaller(context, rootShell))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideVpnService(
|
||||||
|
@Userspace userspaceBackend: Backend,
|
||||||
|
@Kernel kernelBackend: Backend,
|
||||||
|
settingsRepository: SettingsRepository
|
||||||
|
): VpnService {
|
||||||
|
return WireGuardTunnel(userspaceBackend, kernelBackend, settingsRepository)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.module
|
||||||
|
|
||||||
|
import javax.inject.Qualifier
|
||||||
|
|
||||||
|
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class Userspace
|
||||||
@@ -3,32 +3,30 @@ package com.zaneschepke.wireguardautotunnel.receiver
|
|||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.zaneschepke.wireguardautotunnel.goAsync
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.goAsync
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.cancel
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class BootReceiver : BroadcastReceiver() {
|
class BootReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var settingsRepository: SettingsRepository
|
||||||
lateinit var settingsRepo : SettingsDoa
|
|
||||||
|
|
||||||
override fun onReceive(context: Context, intent: Intent) = goAsync {
|
@Inject lateinit var tunnelConfigRepository: TunnelConfigRepository
|
||||||
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
|
|
||||||
try {
|
override fun onReceive(context: Context?, intent: Intent?) = goAsync {
|
||||||
val settings = settingsRepo.getAll()
|
if (Intent.ACTION_BOOT_COMPLETED != intent?.action) return@goAsync
|
||||||
if (settings.isNotEmpty()) {
|
val settings = settingsRepository.getSettings()
|
||||||
val setting = settings.first()
|
if (settings.isAutoTunnelEnabled) {
|
||||||
if (setting.isAutoTunnelEnabled && setting.defaultTunnel != null) {
|
Timber.i("Starting watcher service from boot")
|
||||||
ServiceManager.startWatcherService(context, setting.defaultTunnel!!)
|
ServiceManager.startWatcherServiceForeground(context!!)
|
||||||
}
|
} else if(settings.isAlwaysOnVpnEnabled) {
|
||||||
}
|
Timber.i("Starting tunnel from boot")
|
||||||
} finally {
|
ServiceManager.startVpnServicePrimaryTunnel(context!!, settings, tunnelConfigRepository.getAll().firstOrNull())
|
||||||
cancel()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-14
@@ -3,10 +3,10 @@ package com.zaneschepke.wireguardautotunnel.receiver
|
|||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.zaneschepke.wireguardautotunnel.Constants
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.goAsync
|
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.goAsync
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
@@ -14,22 +14,18 @@ import javax.inject.Inject
|
|||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class NotificationActionReceiver : BroadcastReceiver() {
|
class NotificationActionReceiver : BroadcastReceiver() {
|
||||||
|
@Inject lateinit var settingsRepository: SettingsRepository
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var settingsRepo : SettingsDoa
|
|
||||||
override fun onReceive(context: Context, intent: Intent?) = goAsync {
|
override fun onReceive(context: Context, intent: Intent?) = goAsync {
|
||||||
try {
|
try {
|
||||||
val settings = settingsRepo.getAll()
|
val settings = settingsRepository.getSettings()
|
||||||
if (settings.isNotEmpty()) {
|
if (settings.defaultTunnel != null) {
|
||||||
val setting = settings.first()
|
ServiceManager.stopVpnService(context)
|
||||||
if (setting.defaultTunnel != null) {
|
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
||||||
ServiceManager.stopVpnService(context)
|
ServiceManager.startVpnServiceForeground(context, settings.defaultTunnel.toString())
|
||||||
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
|
||||||
ServiceManager.startVpnService(context, setting.defaultTunnel.toString())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.repository
|
|
||||||
|
|
||||||
import androidx.room.Database
|
|
||||||
import androidx.room.RoomDatabase
|
|
||||||
import androidx.room.TypeConverters
|
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
|
||||||
|
|
||||||
@Database(entities = [Settings::class, TunnelConfig::class], version = 1, exportSchema = false)
|
|
||||||
@TypeConverters(DatabaseListConverters::class)
|
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
|
||||||
abstract fun settingDao(): SettingsDoa
|
|
||||||
abstract fun tunnelConfigDoa() : TunnelConfigDao
|
|
||||||
}
|
|
||||||
-15
@@ -1,15 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.repository
|
|
||||||
|
|
||||||
import androidx.room.TypeConverter
|
|
||||||
|
|
||||||
class DatabaseListConverters {
|
|
||||||
@TypeConverter
|
|
||||||
fun listToString(value: MutableList<String>): String {
|
|
||||||
return value.joinToString(",")
|
|
||||||
}
|
|
||||||
@TypeConverter
|
|
||||||
fun <T> stringToList(value: String): MutableList<String> {
|
|
||||||
if(value.isEmpty()) return mutableListOf()
|
|
||||||
return value.split(",").toMutableList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.repository
|
|
||||||
|
|
||||||
import androidx.room.Dao
|
|
||||||
import androidx.room.Delete
|
|
||||||
import androidx.room.Insert
|
|
||||||
import androidx.room.OnConflictStrategy
|
|
||||||
import androidx.room.Query
|
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
|
|
||||||
@Dao
|
|
||||||
interface SettingsDoa {
|
|
||||||
|
|
||||||
@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")
|
|
||||||
fun getAllFlow(): Flow<MutableList<Settings>>
|
|
||||||
|
|
||||||
@Delete
|
|
||||||
suspend fun delete(t: Settings)
|
|
||||||
|
|
||||||
@Query("SELECT COUNT('id') FROM settings")
|
|
||||||
suspend fun count(): Long
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.repository
|
|
||||||
|
|
||||||
import androidx.room.Dao
|
|
||||||
import androidx.room.Delete
|
|
||||||
import androidx.room.Insert
|
|
||||||
import androidx.room.OnConflictStrategy
|
|
||||||
import androidx.room.Query
|
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
|
||||||
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: List<TunnelConfig>)
|
|
||||||
|
|
||||||
@Query("SELECT * FROM TunnelConfig WHERE id=:id")
|
|
||||||
suspend fun getById(id: Long): TunnelConfig?
|
|
||||||
|
|
||||||
@Query("SELECT * FROM TunnelConfig")
|
|
||||||
suspend fun getAll(): List<TunnelConfig>
|
|
||||||
|
|
||||||
@Delete
|
|
||||||
suspend fun delete(t: TunnelConfig)
|
|
||||||
|
|
||||||
@Query("SELECT COUNT('id') FROM TunnelConfig")
|
|
||||||
suspend fun count(): Long
|
|
||||||
|
|
||||||
@Query("SELECT * FROM tunnelconfig")
|
|
||||||
fun getAllFlow(): Flow<MutableList<TunnelConfig>>
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.repository.model
|
|
||||||
|
|
||||||
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") var isAutoTunnelEnabled : Boolean = false,
|
|
||||||
@ColumnInfo(name = "is_tunnel_on_mobile_data_enabled") var isTunnelOnMobileDataEnabled : Boolean = false,
|
|
||||||
@ColumnInfo(name = "trusted_network_ssids") var trustedNetworkSSIDs : MutableList<String> = mutableListOf(),
|
|
||||||
@ColumnInfo(name = "default_tunnel") var defaultTunnel : String? = null,
|
|
||||||
@ColumnInfo(name = "is_always_on_vpn_enabled") var isAlwaysOnVpnEnabled : Boolean = false,
|
|
||||||
@ColumnInfo(name = "is_tunnel_on_ethernet_enabled") var isTunnelOnEthernetEnabled : Boolean = false,
|
|
||||||
) {
|
|
||||||
fun isTunnelConfigDefault(tunnelConfig: TunnelConfig) : Boolean {
|
|
||||||
return if (defaultTunnel != null) {
|
|
||||||
val defaultConfig = TunnelConfig.from(defaultTunnel!!)
|
|
||||||
(tunnelConfig.id == defaultConfig.id)
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,4 +4,4 @@ enum class Action {
|
|||||||
START,
|
START,
|
||||||
START_FOREGROUND,
|
START_FOREGROUND,
|
||||||
STOP
|
STOP
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-9
@@ -6,9 +6,7 @@ import android.os.IBinder
|
|||||||
import androidx.lifecycle.LifecycleService
|
import androidx.lifecycle.LifecycleService
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
|
|
||||||
open class ForegroundService : LifecycleService() {
|
open class ForegroundService : LifecycleService() {
|
||||||
|
|
||||||
private var isServiceStarted = false
|
private var isServiceStarted = false
|
||||||
|
|
||||||
override fun onBind(intent: Intent): IBinder? {
|
override fun onBind(intent: Intent): IBinder? {
|
||||||
@@ -22,9 +20,9 @@ open class ForegroundService : LifecycleService() {
|
|||||||
Timber.d("onStartCommand executed with startId: $startId")
|
Timber.d("onStartCommand executed with startId: $startId")
|
||||||
if (intent != null) {
|
if (intent != null) {
|
||||||
val action = intent.action
|
val action = intent.action
|
||||||
Timber.d("using an intent with action $action")
|
|
||||||
when (action) {
|
when (action) {
|
||||||
Action.START.name, Action.START_FOREGROUND.name -> startService(intent.extras)
|
Action.START.name,
|
||||||
|
Action.START_FOREGROUND.name -> startService(intent.extras)
|
||||||
Action.STOP.name -> stopService(intent.extras)
|
Action.STOP.name -> stopService(intent.extras)
|
||||||
"android.net.VpnService" -> {
|
"android.net.VpnService" -> {
|
||||||
Timber.d("Always-on VPN starting service")
|
Timber.d("Always-on VPN starting service")
|
||||||
@@ -34,26 +32,25 @@ open class ForegroundService : LifecycleService() {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Timber.d(
|
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
|
// by returning this we make sure the service is restarted if the system kills the service
|
||||||
return START_STICKY
|
return START_STICKY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
Timber.d("The service has been destroyed")
|
Timber.d("The service has been destroyed")
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun startService(extras : Bundle?) {
|
protected open fun startService(extras: Bundle?) {
|
||||||
if (isServiceStarted) return
|
if (isServiceStarted) return
|
||||||
Timber.d("Starting ${this.javaClass.simpleName}")
|
Timber.d("Starting ${this.javaClass.simpleName}")
|
||||||
isServiceStarted = true
|
isServiceStarted = true
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun stopService(extras : Bundle?) {
|
protected open fun stopService(extras: Bundle?) {
|
||||||
Timber.d("Stopping ${this.javaClass.simpleName}")
|
Timber.d("Stopping ${this.javaClass.simpleName}")
|
||||||
try {
|
try {
|
||||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||||
@@ -63,4 +60,4 @@ open class ForegroundService : LifecycleService() {
|
|||||||
}
|
}
|
||||||
isServiceStarted = false
|
isServiceStarted = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+73
-66
@@ -1,38 +1,44 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.service.foreground
|
package com.zaneschepke.wireguardautotunnel.service.foreground
|
||||||
|
|
||||||
import android.app.ActivityManager
|
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Context.ACTIVITY_SERVICE
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
object ServiceManager {
|
object ServiceManager {
|
||||||
@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 {
|
// private
|
||||||
val isServiceRunning = context.isServiceRunning(cls)
|
// fun <T> Context.isServiceRunning(service: Class<T>) =
|
||||||
return if(isServiceRunning) ServiceState.STARTED else ServiceState.STOPPED
|
// (getSystemService(ACTIVITY_SERVICE) as ActivityManager)
|
||||||
}
|
// .runningAppProcesses.any {
|
||||||
|
// it.processName == 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
|
||||||
|
// }
|
||||||
|
|
||||||
private fun <T : Service> actionOnService(action: Action, context: Context, cls : Class<T>, extras : Map<String,String>? = null) {
|
private fun <T : Service> actionOnService(
|
||||||
if (getServiceState(context, cls) == ServiceState.STOPPED && action == Action.STOP) return
|
action: Action,
|
||||||
if (getServiceState(context, cls) == ServiceState.STARTED && action == Action.START) return
|
context: Context,
|
||||||
val intent = Intent(context, cls).also {
|
cls: Class<T>,
|
||||||
it.action = action.name
|
extras: Map<String, String>? = null
|
||||||
extras?.forEach {(k, v) ->
|
) {
|
||||||
it.putExtra(k, v)
|
val intent =
|
||||||
|
Intent(context, cls).also {
|
||||||
|
it.action = action.name
|
||||||
|
extras?.forEach { (k, v) -> it.putExtra(k, v) }
|
||||||
}
|
}
|
||||||
}
|
|
||||||
intent.component?.javaClass
|
intent.component?.javaClass
|
||||||
try {
|
try {
|
||||||
when(action) {
|
when (action) {
|
||||||
Action.START_FOREGROUND -> {
|
Action.START_FOREGROUND -> {
|
||||||
context.startForegroundService(intent)
|
context.startForegroundService(intent)
|
||||||
}
|
}
|
||||||
@@ -41,69 +47,70 @@ object ServiceManager {
|
|||||||
}
|
}
|
||||||
Action.STOP -> context.startService(intent)
|
Action.STOP -> context.startService(intent)
|
||||||
}
|
}
|
||||||
} catch (e : Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e.message)
|
Timber.e(e.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startVpnService(context : Context, tunnelConfig : String) {
|
fun startVpnService(context: Context, tunnelConfig: String) {
|
||||||
actionOnService(
|
actionOnService(
|
||||||
Action.START,
|
Action.START,
|
||||||
context,
|
context,
|
||||||
WireGuardTunnelService::class.java,
|
WireGuardTunnelService::class.java,
|
||||||
mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig))
|
mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig),
|
||||||
}
|
|
||||||
fun stopVpnService(context : Context) {
|
|
||||||
actionOnService(
|
|
||||||
Action.STOP,
|
|
||||||
context,
|
|
||||||
WireGuardTunnelService::class.java
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startVpnServiceForeground(context : Context, tunnelConfig : String) {
|
fun stopVpnService(context: Context) {
|
||||||
|
Timber.d("Stopping vpn service action")
|
||||||
|
actionOnService(
|
||||||
|
Action.STOP,
|
||||||
|
context,
|
||||||
|
WireGuardTunnelService::class.java,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startVpnServiceForeground(context: Context, tunnelConfig: String) {
|
||||||
actionOnService(
|
actionOnService(
|
||||||
Action.START_FOREGROUND,
|
Action.START_FOREGROUND,
|
||||||
context,
|
context,
|
||||||
WireGuardTunnelService::class.java,
|
WireGuardTunnelService::class.java,
|
||||||
mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig))
|
mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startWatcherServiceForeground(context : Context, tunnelConfig : String) {
|
fun startVpnServicePrimaryTunnel(context: Context, settings: Settings, fallbackTunnel: TunnelConfig? = null) {
|
||||||
actionOnService(
|
if(settings.defaultTunnel != null) {
|
||||||
Action.START, context,
|
return startVpnServiceForeground(context, settings.defaultTunnel!!)
|
||||||
WireGuardConnectivityWatcherService::class.java, mapOf(context.
|
}
|
||||||
getString(R.string.tunnel_extras_key) to
|
if(fallbackTunnel != null) {
|
||||||
tunnelConfig))
|
startVpnServiceForeground(context, fallbackTunnel.toString())
|
||||||
}
|
|
||||||
|
|
||||||
fun startWatcherService(context : Context, tunnelConfig : String) {
|
|
||||||
actionOnService(
|
|
||||||
Action.START, context,
|
|
||||||
WireGuardConnectivityWatcherService::class.java, mapOf(context.
|
|
||||||
getString(R.string.tunnel_extras_key) to
|
|
||||||
tunnelConfig))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun stopWatcherService(context : Context) {
|
|
||||||
actionOnService(
|
|
||||||
Action.STOP, context,
|
|
||||||
WireGuardConnectivityWatcherService::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toggleWatcherService(context: Context, tunnelConfig : String) {
|
|
||||||
when(getServiceState( context,
|
|
||||||
WireGuardConnectivityWatcherService::class.java,)) {
|
|
||||||
ServiceState.STARTED -> stopWatcherService(context)
|
|
||||||
ServiceState.STOPPED -> startWatcherService(context, tunnelConfig)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggleWatcherServiceForeground(context: Context, tunnelConfig : String) {
|
fun startWatcherServiceForeground(
|
||||||
when(getServiceState( context,
|
context: Context,
|
||||||
WireGuardConnectivityWatcherService::class.java,)) {
|
) {
|
||||||
ServiceState.STARTED -> stopWatcherService(context)
|
actionOnService(
|
||||||
ServiceState.STOPPED -> startWatcherServiceForeground(context, tunnelConfig)
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -3,4 +3,4 @@ package com.zaneschepke.wireguardautotunnel.service.foreground
|
|||||||
enum class ServiceState {
|
enum class ServiceState {
|
||||||
STARTED,
|
STARTED,
|
||||||
STOPPED,
|
STOPPED,
|
||||||
}
|
}
|
||||||
|
|||||||
+269
-120
@@ -7,12 +7,12 @@ import android.content.Intent
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
|
import androidx.core.app.ServiceCompat
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.wireguard.android.backend.Tunnel
|
import com.wireguard.android.backend.Tunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.Constants
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
|
import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
|
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
|
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
|
||||||
@@ -20,72 +20,72 @@ import com.zaneschepke.wireguardautotunnel.service.network.NetworkStatus
|
|||||||
import com.zaneschepke.wireguardautotunnel.service.network.WifiService
|
import com.zaneschepke.wireguardautotunnel.service.network.WifiService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
|
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class WireGuardConnectivityWatcherService : ForegroundService() {
|
class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||||
|
private val foregroundId = 122
|
||||||
|
|
||||||
private val foregroundId = 122;
|
@Inject lateinit var wifiService: NetworkService<WifiService>
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var mobileDataService: NetworkService<MobileDataService>
|
||||||
lateinit var wifiService : NetworkService<WifiService>
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var ethernetService: NetworkService<EthernetService>
|
||||||
lateinit var mobileDataService : NetworkService<MobileDataService>
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var settingsRepository: SettingsRepository
|
||||||
lateinit var ethernetService: NetworkService<EthernetService>
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var notificationService: NotificationService
|
||||||
lateinit var settingsRepo: SettingsDoa
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var vpnService: VpnService
|
||||||
lateinit var notificationService : NotificationService
|
|
||||||
|
|
||||||
@Inject
|
private val networkEventsFlow = MutableStateFlow(WatcherState())
|
||||||
lateinit var vpnService : VpnService
|
|
||||||
|
|
||||||
private var isWifiConnected = false;
|
data class WatcherState(
|
||||||
private var isEthernetConnected = false;
|
val isWifiConnected: Boolean = false,
|
||||||
private var isMobileDataConnected = false;
|
val isVpnConnected: Boolean = false,
|
||||||
private var currentNetworkSSID = "";
|
val isEthernetConnected: Boolean = false,
|
||||||
|
val isMobileDataConnected: Boolean = false,
|
||||||
|
val currentNetworkSSID: String = "",
|
||||||
|
val settings: Settings = Settings()
|
||||||
|
)
|
||||||
|
|
||||||
private lateinit var watcherJob : Job;
|
private lateinit var watcherJob: Job
|
||||||
private lateinit var setting : Settings
|
|
||||||
private lateinit var tunnelConfig: String
|
|
||||||
|
|
||||||
private var wakeLock: PowerManager.WakeLock? = null
|
private var wakeLock: PowerManager.WakeLock? = null
|
||||||
private val tag = this.javaClass.name;
|
private val tag = this.javaClass.name
|
||||||
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
launchWatcherNotification()
|
try {
|
||||||
|
if (settingsRepository.getSettings().isAutoTunnelPaused) {
|
||||||
|
launchWatcherPausedNotification()
|
||||||
|
} else launchWatcherNotification()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e("Failed to start watcher service, not enough permissions")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun startService(extras: Bundle?) {
|
override fun startService(extras: Bundle?) {
|
||||||
super.startService(extras)
|
super.startService(extras)
|
||||||
launchWatcherNotification()
|
try {
|
||||||
val tunnelId = extras?.getString(getString(R.string.tunnel_extras_key))
|
// we need this lock so our service gets not affected by Doze Mode
|
||||||
if (tunnelId != null) {
|
lifecycleScope.launch { initWakeLock() }
|
||||||
this.tunnelConfig = tunnelId
|
cancelWatcherJob()
|
||||||
}
|
|
||||||
// we need this lock so our service gets not affected by Doze Mode
|
|
||||||
initWakeLock()
|
|
||||||
cancelWatcherJob()
|
|
||||||
if(this::tunnelConfig.isInitialized) {
|
|
||||||
startWatcherJob()
|
startWatcherJob()
|
||||||
} else {
|
} catch (e: Exception) {
|
||||||
stopService(extras)
|
Timber.e("Failed to launch watcher service, no permissions")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,100 +100,197 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
stopSelf()
|
stopSelf()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchWatcherNotification() {
|
private fun launchWatcherNotification(
|
||||||
val notification = notificationService.createNotification(
|
description: String = getString(R.string.watcher_notification_text_active)
|
||||||
channelId = getString(R.string.watcher_channel_id),
|
) {
|
||||||
channelName = getString(R.string.watcher_channel_name),
|
val notification =
|
||||||
description = getString(R.string.watcher_notification_text))
|
notificationService.createNotification(
|
||||||
super.startForeground(foregroundId, notification)
|
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
|
private fun launchWatcherPausedNotification() {
|
||||||
|
launchWatcherNotification(getString(R.string.watcher_notification_text_paused))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO could this be restarting service in a bad state?
|
||||||
|
// try to start task again if killed
|
||||||
override fun onTaskRemoved(rootIntent: Intent) {
|
override fun onTaskRemoved(rootIntent: Intent) {
|
||||||
Timber.d("Task Removed called")
|
Timber.d("Task Removed called")
|
||||||
val restartServiceIntent = Intent(rootIntent)
|
val restartServiceIntent = Intent(rootIntent)
|
||||||
val restartServicePendingIntent: PendingIntent = PendingIntent.getService(this, 1, restartServiceIntent,
|
val restartServicePendingIntent: PendingIntent =
|
||||||
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE);
|
PendingIntent.getService(
|
||||||
applicationContext.getSystemService(Context.ALARM_SERVICE);
|
this,
|
||||||
val alarmService: AlarmManager = applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager;
|
1,
|
||||||
alarmService.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 1000, restartServicePendingIntent);
|
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 initWakeLock() {
|
private suspend fun initWakeLock() {
|
||||||
|
val isBatterySaverOn =
|
||||||
|
withContext(lifecycleScope.coroutineContext) {
|
||||||
|
settingsRepository.getSettings().isBatterySaverEnabled
|
||||||
|
}
|
||||||
wakeLock =
|
wakeLock =
|
||||||
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
|
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
|
||||||
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
|
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
|
||||||
//TODO decide what to do here with the wakelock
|
try {
|
||||||
//this is draining battery. Perhaps users only care for VPN to connect when their screen is on
|
if (isBatterySaverOn) {
|
||||||
//and they are actively using apps
|
Timber.d("Initiating wakelock with timeout")
|
||||||
acquire()
|
acquire(Constants.BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT)
|
||||||
|
} else {
|
||||||
|
Timber.d("Initiating wakelock with zero timeout")
|
||||||
|
acquire(Constants.DEFAULT_WATCHER_WAKE_LOCK_TIMEOUT)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
release()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cancelWatcherJob() {
|
private fun cancelWatcherJob() {
|
||||||
if(this::watcherJob.isInitialized) {
|
if (this::watcherJob.isInitialized) {
|
||||||
watcherJob.cancel()
|
watcherJob.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startWatcherJob() {
|
private fun startWatcherJob() {
|
||||||
watcherJob = lifecycleScope.launch(Dispatchers.IO) {
|
watcherJob =
|
||||||
val settings = settingsRepo.getAll();
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
if(settings.isNotEmpty()) {
|
val setting = settingsRepository.getSettings()
|
||||||
setting = settings[0]
|
|
||||||
}
|
|
||||||
launch {
|
|
||||||
watchForWifiConnectivityChanges()
|
|
||||||
}
|
|
||||||
if(setting.isTunnelOnMobileDataEnabled) {
|
|
||||||
launch {
|
launch {
|
||||||
watchForMobileDataConnectivityChanges()
|
Timber.d("Starting wifi watcher")
|
||||||
|
watchForWifiConnectivityChanges()
|
||||||
|
}
|
||||||
|
if (setting.isTunnelOnMobileDataEnabled) {
|
||||||
|
launch {
|
||||||
|
Timber.d("Starting mobile data watcher")
|
||||||
|
watchForMobileDataConnectivityChanges()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (setting.isTunnelOnEthernetEnabled) {
|
||||||
|
launch {
|
||||||
|
Timber.d("Starting ethernet data watcher")
|
||||||
|
watchForEthernetConnectivityChanges()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
Timber.d("Starting vpn state watcher")
|
||||||
|
watchForVpnConnectivityChanges()
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
Timber.d("Starting settings watcher")
|
||||||
|
watchForSettingsChanges()
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
Timber.d("Starting management watcher")
|
||||||
|
manageVpn()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(setting.isTunnelOnEthernetEnabled) {
|
|
||||||
launch {
|
|
||||||
watchForEthernetConnectivityChanges()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
launch {
|
|
||||||
manageVpn()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun watchForMobileDataConnectivityChanges() {
|
private suspend fun watchForMobileDataConnectivityChanges() {
|
||||||
mobileDataService.networkStatus.collect {
|
mobileDataService.networkStatus.collect {
|
||||||
when(it) {
|
when (it) {
|
||||||
is NetworkStatus.Available -> {
|
is NetworkStatus.Available -> {
|
||||||
Timber.d("Gained Mobile data connection")
|
Timber.d("Gained Mobile data connection")
|
||||||
isMobileDataConnected = true
|
networkEventsFlow.value =
|
||||||
|
networkEventsFlow.value.copy(
|
||||||
|
isMobileDataConnected = true,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
is NetworkStatus.CapabilitiesChanged -> {
|
is NetworkStatus.CapabilitiesChanged -> {
|
||||||
isMobileDataConnected = true
|
networkEventsFlow.value =
|
||||||
|
networkEventsFlow.value.copy(
|
||||||
|
isMobileDataConnected = true,
|
||||||
|
)
|
||||||
Timber.d("Mobile data capabilities changed")
|
Timber.d("Mobile data capabilities changed")
|
||||||
}
|
}
|
||||||
is NetworkStatus.Unavailable -> {
|
is NetworkStatus.Unavailable -> {
|
||||||
isMobileDataConnected = false
|
networkEventsFlow.value =
|
||||||
|
networkEventsFlow.value.copy(
|
||||||
|
isMobileDataConnected = false,
|
||||||
|
)
|
||||||
Timber.d("Lost mobile data connection")
|
Timber.d("Lost mobile data connection")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun watchForSettingsChanges() {
|
||||||
|
settingsRepository.getSettingsFlow().collect {
|
||||||
|
if (networkEventsFlow.value.settings.isAutoTunnelPaused != it.isAutoTunnelPaused) {
|
||||||
|
when (it.isAutoTunnelPaused) {
|
||||||
|
true -> launchWatcherPausedNotification()
|
||||||
|
false -> launchWatcherNotification()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
networkEventsFlow.value =
|
||||||
|
networkEventsFlow.value.copy(
|
||||||
|
settings = it,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun watchForVpnConnectivityChanges() {
|
||||||
|
vpnService.vpnState.collect {
|
||||||
|
when (it.status) {
|
||||||
|
Tunnel.State.DOWN ->
|
||||||
|
networkEventsFlow.value =
|
||||||
|
networkEventsFlow.value.copy(
|
||||||
|
isVpnConnected = false,
|
||||||
|
)
|
||||||
|
Tunnel.State.UP ->
|
||||||
|
networkEventsFlow.value =
|
||||||
|
networkEventsFlow.value.copy(
|
||||||
|
isVpnConnected = true,
|
||||||
|
)
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun watchForEthernetConnectivityChanges() {
|
private suspend fun watchForEthernetConnectivityChanges() {
|
||||||
ethernetService.networkStatus.collect {
|
ethernetService.networkStatus.collect {
|
||||||
when (it) {
|
when (it) {
|
||||||
is NetworkStatus.Available -> {
|
is NetworkStatus.Available -> {
|
||||||
Timber.d("Gained Ethernet connection")
|
Timber.d("Gained Ethernet connection")
|
||||||
isEthernetConnected = true
|
networkEventsFlow.value =
|
||||||
|
networkEventsFlow.value.copy(
|
||||||
|
isEthernetConnected = true,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
is NetworkStatus.CapabilitiesChanged -> {
|
is NetworkStatus.CapabilitiesChanged -> {
|
||||||
Timber.d("Ethernet capabilities changed")
|
Timber.d("Ethernet capabilities changed")
|
||||||
isEthernetConnected = true
|
networkEventsFlow.value =
|
||||||
|
networkEventsFlow.value.copy(
|
||||||
|
isEthernetConnected = true,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
is NetworkStatus.Unavailable -> {
|
is NetworkStatus.Unavailable -> {
|
||||||
isEthernetConnected = false
|
networkEventsFlow.value =
|
||||||
|
networkEventsFlow.value.copy(
|
||||||
|
isEthernetConnected = false,
|
||||||
|
)
|
||||||
Timber.d("Lost Ethernet connection")
|
Timber.d("Lost Ethernet connection")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -202,48 +299,100 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
|
|
||||||
private suspend fun watchForWifiConnectivityChanges() {
|
private suspend fun watchForWifiConnectivityChanges() {
|
||||||
wifiService.networkStatus.collect {
|
wifiService.networkStatus.collect {
|
||||||
when (it) {
|
when (it) {
|
||||||
is NetworkStatus.Available -> {
|
is NetworkStatus.Available -> {
|
||||||
Timber.d("Gained Wi-Fi connection")
|
Timber.d("Gained Wi-Fi connection")
|
||||||
isWifiConnected = true
|
networkEventsFlow.value =
|
||||||
|
networkEventsFlow.value.copy(
|
||||||
|
isWifiConnected = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is NetworkStatus.CapabilitiesChanged -> {
|
||||||
|
Timber.d("Wifi capabilities changed")
|
||||||
|
networkEventsFlow.value =
|
||||||
|
networkEventsFlow.value.copy(
|
||||||
|
isWifiConnected = true,
|
||||||
|
)
|
||||||
|
val ssid = wifiService.getNetworkName(it.networkCapabilities) ?: ""
|
||||||
|
Timber.d("Detected SSID: $ssid")
|
||||||
|
networkEventsFlow.value =
|
||||||
|
networkEventsFlow.value.copy(
|
||||||
|
currentNetworkSSID = ssid,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is NetworkStatus.Unavailable -> {
|
||||||
|
networkEventsFlow.value =
|
||||||
|
networkEventsFlow.value.copy(
|
||||||
|
isWifiConnected = false,
|
||||||
|
)
|
||||||
|
Timber.d("Lost Wi-Fi connection")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO clean this up
|
||||||
|
private suspend fun manageVpn() {
|
||||||
|
networkEventsFlow.collectLatest {
|
||||||
|
Timber.i("New watcher state: $it")
|
||||||
|
if (!it.settings.isAutoTunnelPaused && it.settings.defaultTunnel != null) {
|
||||||
|
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
||||||
|
when {
|
||||||
|
((it.isEthernetConnected &&
|
||||||
|
it.settings.isTunnelOnEthernetEnabled &&
|
||||||
|
!it.isVpnConnected)) -> {
|
||||||
|
ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!)
|
||||||
|
Timber.i("Condition 1 met")
|
||||||
}
|
}
|
||||||
is NetworkStatus.CapabilitiesChanged -> {
|
(!it.isEthernetConnected &&
|
||||||
Timber.d("Wifi capabilities changed")
|
it.settings.isTunnelOnMobileDataEnabled &&
|
||||||
isWifiConnected = true
|
!it.isWifiConnected &&
|
||||||
currentNetworkSSID = wifiService.getNetworkName(it.networkCapabilities) ?: "";
|
it.isMobileDataConnected &&
|
||||||
|
!it.isVpnConnected) -> {
|
||||||
|
ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!)
|
||||||
|
Timber.i("Condition 2 met")
|
||||||
}
|
}
|
||||||
is NetworkStatus.Unavailable -> {
|
(!it.isEthernetConnected &&
|
||||||
isWifiConnected = false
|
!it.settings.isTunnelOnMobileDataEnabled &&
|
||||||
Timber.d("Lost Wi-Fi connection")
|
!it.isWifiConnected &&
|
||||||
|
it.isVpnConnected) -> {
|
||||||
|
ServiceManager.stopVpnService(this)
|
||||||
|
Timber.i("Condition 3 met")
|
||||||
|
}
|
||||||
|
(!it.isEthernetConnected &&
|
||||||
|
it.isWifiConnected &&
|
||||||
|
!it.settings.trustedNetworkSSIDs.contains(it.currentNetworkSSID) &&
|
||||||
|
it.settings.isTunnelOnWifiEnabled &&
|
||||||
|
(!it.isVpnConnected)) -> {
|
||||||
|
ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!)
|
||||||
|
Timber.i("Condition 4 met")
|
||||||
|
}
|
||||||
|
(!it.isEthernetConnected &&
|
||||||
|
(it.isWifiConnected &&
|
||||||
|
it.settings.trustedNetworkSSIDs.contains(it.currentNetworkSSID)) &&
|
||||||
|
(it.isVpnConnected)) -> {
|
||||||
|
ServiceManager.stopVpnService(this)
|
||||||
|
Timber.i("Condition 5 met")
|
||||||
|
}
|
||||||
|
(!it.isEthernetConnected &&
|
||||||
|
(it.isWifiConnected &&
|
||||||
|
!it.settings.isTunnelOnWifiEnabled &&
|
||||||
|
(it.isVpnConnected))) -> {
|
||||||
|
ServiceManager.stopVpnService(this)
|
||||||
|
Timber.i("Condition 6 met")
|
||||||
|
}
|
||||||
|
(!it.isEthernetConnected &&
|
||||||
|
!it.isWifiConnected &&
|
||||||
|
!it.isMobileDataConnected &&
|
||||||
|
(it.isVpnConnected)) -> {
|
||||||
|
ServiceManager.stopVpnService(this)
|
||||||
|
Timber.i("Condition 7 met")
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Timber.i("No condition met")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun manageVpn() {
|
|
||||||
while(true) {
|
|
||||||
if(isEthernetConnected && setting.isTunnelOnEthernetEnabled && vpnService.getState() == Tunnel.State.DOWN) {
|
|
||||||
ServiceManager.startVpnService(this, tunnelConfig)
|
|
||||||
}
|
|
||||||
if(!isEthernetConnected && setting.isTunnelOnMobileDataEnabled &&
|
|
||||||
!isWifiConnected &&
|
|
||||||
isMobileDataConnected
|
|
||||||
&& vpnService.getState() == Tunnel.State.DOWN) {
|
|
||||||
ServiceManager.startVpnService(this, tunnelConfig)
|
|
||||||
} else if(!isEthernetConnected && !setting.isTunnelOnMobileDataEnabled &&
|
|
||||||
!isWifiConnected &&
|
|
||||||
vpnService.getState() == Tunnel.State.UP) {
|
|
||||||
ServiceManager.stopVpnService(this)
|
|
||||||
} else if(!isEthernetConnected && isWifiConnected &&
|
|
||||||
!setting.trustedNetworkSSIDs.contains(currentNetworkSSID) &&
|
|
||||||
(vpnService.getState() != Tunnel.State.UP)) {
|
|
||||||
ServiceManager.startVpnService(this, tunnelConfig)
|
|
||||||
} else if(!isEthernetConnected && (isWifiConnected &&
|
|
||||||
setting.trustedNetworkSSIDs.contains(currentNetworkSSID)) &&
|
|
||||||
(vpnService.getState() == Tunnel.State.UP)) {
|
|
||||||
ServiceManager.stopVpnService(this)
|
|
||||||
}
|
|
||||||
delay(Constants.VPN_CONNECTIVITY_CHECK_INTERVAL)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+120
-101
@@ -3,162 +3,181 @@ package com.zaneschepke.wireguardautotunnel.service.foreground
|
|||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.core.app.ServiceCompat
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver
|
import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
|
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.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 dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class WireGuardTunnelService : ForegroundService() {
|
class WireGuardTunnelService : ForegroundService() {
|
||||||
|
private val foregroundId = 123
|
||||||
|
|
||||||
private val foregroundId = 123;
|
@Inject lateinit var vpnService: VpnService
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var settingsRepository: SettingsRepository
|
||||||
lateinit var vpnService : VpnService
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var tunnelConfigRepository: TunnelConfigRepository
|
||||||
lateinit var settingsRepo: SettingsDoa
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var notificationService: NotificationService
|
||||||
lateinit var notificationService : NotificationService
|
|
||||||
|
|
||||||
private lateinit var job : Job
|
private lateinit var job: Job
|
||||||
|
|
||||||
private var tunnelName : String = ""
|
private var tunnelName: String = ""
|
||||||
|
private var didShowConnected = false
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
launchVpnStartingNotification()
|
if (tunnelConfigRepository.getAll().isNotEmpty()) {
|
||||||
|
launchVpnNotification()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun startService(extras : Bundle?) {
|
override fun startService(extras: Bundle?) {
|
||||||
super.startService(extras)
|
super.startService(extras)
|
||||||
launchVpnStartingNotification()
|
|
||||||
val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key))
|
|
||||||
cancelJob()
|
cancelJob()
|
||||||
job = lifecycleScope.launch(Dispatchers.IO) {
|
val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key))
|
||||||
launch {
|
val tunnelConfig = tunnelConfigString?.let { TunnelConfig.from(it) }
|
||||||
if(tunnelConfigString != null) {
|
tunnelName = tunnelConfig?.name ?: ""
|
||||||
try {
|
job =
|
||||||
val tunnelConfig = TunnelConfig.from(tunnelConfigString)
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
tunnelName = tunnelConfig.name
|
launch {
|
||||||
vpnService.startTunnel(tunnelConfig)
|
if (tunnelConfig != null) {
|
||||||
} catch (e : Exception) {
|
try {
|
||||||
Timber.e("Problem starting tunnel: ${e.message}")
|
|
||||||
stopService(extras)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Timber.d("Tunnel config null, starting default tunnel")
|
|
||||||
val settings = settingsRepo.getAll();
|
|
||||||
if(settings.isNotEmpty()) {
|
|
||||||
val setting = settings[0]
|
|
||||||
if(setting.defaultTunnel != null && setting.isAlwaysOnVpnEnabled) {
|
|
||||||
val tunnelConfig = TunnelConfig.from(setting.defaultTunnel!!)
|
|
||||||
tunnelName = tunnelConfig.name
|
tunnelName = tunnelConfig.name
|
||||||
vpnService.startTunnel(tunnelConfig)
|
vpnService.startTunnel(tunnelConfig)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e("Problem starting tunnel: ${e.message}")
|
||||||
|
stopService(extras)
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
}
|
Timber.d("Tunnel config null, starting default tunnel or first")
|
||||||
}
|
val settings = settingsRepository.getSettings()
|
||||||
launch {
|
val tunnels = tunnelConfigRepository.getAll()
|
||||||
var didShowConnected = false
|
if (settings.isAlwaysOnVpnEnabled) {
|
||||||
var didShowFailedHandshakeNotification = false
|
val tunnel =
|
||||||
vpnService.handshakeStatus.collect {
|
if (settings.defaultTunnel != null) {
|
||||||
when(it) {
|
TunnelConfig.from(settings.defaultTunnel!!)
|
||||||
HandshakeStatus.NOT_STARTED -> {
|
} else if (tunnels.isNotEmpty()) {
|
||||||
}
|
tunnels.first()
|
||||||
HandshakeStatus.NEVER_CONNECTED -> {
|
} else {
|
||||||
if(!didShowFailedHandshakeNotification) {
|
null
|
||||||
launchVpnConnectionFailedNotification(getString(R.string.initial_connection_failure_message))
|
}
|
||||||
didShowFailedHandshakeNotification = true
|
if (tunnel != null) {
|
||||||
didShowConnected = false
|
tunnelName = tunnel.name
|
||||||
}
|
vpnService.startTunnel(tunnel)
|
||||||
}
|
|
||||||
HandshakeStatus.HEALTHY -> {
|
|
||||||
if(!didShowConnected) {
|
|
||||||
launchVpnConnectedNotification()
|
|
||||||
didShowConnected = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
HandshakeStatus.UNHEALTHY -> {
|
|
||||||
if(!didShowFailedHandshakeNotification) {
|
|
||||||
launchVpnConnectionFailedNotification(getString(R.string.lost_connection_failure_message))
|
|
||||||
didShowFailedHandshakeNotification = true
|
|
||||||
didShowConnected = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// TODO add failed to connect notification
|
||||||
|
launch {
|
||||||
|
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)
|
||||||
|
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 -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun stopService(extras : Bundle?) {
|
override fun stopService(extras: Bundle?) {
|
||||||
super.stopService(extras)
|
super.stopService(extras)
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
vpnService.stopTunnel()
|
vpnService.stopTunnel()
|
||||||
|
didShowConnected = false
|
||||||
}
|
}
|
||||||
cancelJob()
|
cancelJob()
|
||||||
stopSelf()
|
stopSelf()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchVpnConnectedNotification() {
|
private fun launchVpnNotification(
|
||||||
val notification = notificationService.createNotification(
|
title: String = getString(R.string.vpn_starting),
|
||||||
channelId = getString(R.string.vpn_channel_id),
|
description: String = getString(R.string.attempt_connection)
|
||||||
channelName = getString(R.string.vpn_channel_name),
|
) {
|
||||||
title = getString(R.string.tunnel_start_title),
|
val notification =
|
||||||
onGoing = false,
|
notificationService.createNotification(
|
||||||
showTimestamp = true,
|
channelId = getString(R.string.vpn_channel_id),
|
||||||
description = "${getString(R.string.tunnel_start_text)} $tunnelName"
|
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 launchVpnStartingNotification() {
|
private fun launchVpnConnectionFailedNotification(message: String) {
|
||||||
val notification = notificationService.createNotification(
|
val notification =
|
||||||
channelId = getString(R.string.vpn_channel_id),
|
notificationService.createNotification(
|
||||||
channelName = getString(R.string.vpn_channel_name),
|
channelId = getString(R.string.vpn_channel_id),
|
||||||
title = getString(R.string.vpn_starting),
|
channelName = getString(R.string.vpn_channel_name),
|
||||||
onGoing = false,
|
action =
|
||||||
showTimestamp = true,
|
PendingIntent.getBroadcast(
|
||||||
description = getString(R.string.attempt_connection)
|
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,
|
||||||
)
|
)
|
||||||
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,
|
|
||||||
showTimestamp = true,
|
|
||||||
description = message
|
|
||||||
)
|
|
||||||
super.startForeground(foregroundId, notification)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun cancelJob() {
|
private fun cancelJob() {
|
||||||
if(this::job.isInitialized) {
|
if (this::job.isInitialized) {
|
||||||
job.cancel()
|
job.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+68
-54
@@ -14,8 +14,10 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
import kotlinx.coroutines.flow.callbackFlow
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
|
abstract class BaseNetworkService<T : BaseNetworkService<T>>(
|
||||||
abstract class BaseNetworkService<T : BaseNetworkService<T>>(val context: Context, networkCapability : Int) : NetworkService<T> {
|
val context: Context,
|
||||||
|
networkCapability: Int
|
||||||
|
) : NetworkService<T> {
|
||||||
private val connectivityManager =
|
private val connectivityManager =
|
||||||
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||||
|
|
||||||
@@ -23,61 +25,69 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(val context: Contex
|
|||||||
context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
|
context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
|
||||||
|
|
||||||
override val networkStatus = callbackFlow {
|
override val networkStatus = callbackFlow {
|
||||||
val networkStatusCallback = when (Build.VERSION.SDK_INT) {
|
val networkStatusCallback =
|
||||||
in Build.VERSION_CODES.S..Int.MAX_VALUE -> {
|
when (Build.VERSION.SDK_INT) {
|
||||||
object : ConnectivityManager.NetworkCallback(
|
in Build.VERSION_CODES.S..Int.MAX_VALUE -> {
|
||||||
FLAG_INCLUDE_LOCATION_INFO
|
object :
|
||||||
) {
|
ConnectivityManager.NetworkCallback(
|
||||||
override fun onAvailable(network: Network) {
|
FLAG_INCLUDE_LOCATION_INFO,
|
||||||
trySend(NetworkStatus.Available(network))
|
) {
|
||||||
}
|
override fun onAvailable(network: Network) {
|
||||||
|
trySend(NetworkStatus.Available(network))
|
||||||
|
}
|
||||||
|
|
||||||
override fun onLost(network: Network) {
|
override fun onLost(network: Network) {
|
||||||
trySend(NetworkStatus.Unavailable(network))
|
trySend(NetworkStatus.Unavailable(network))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCapabilitiesChanged(
|
override fun onCapabilitiesChanged(
|
||||||
network: Network,
|
network: Network,
|
||||||
networkCapabilities: NetworkCapabilities
|
networkCapabilities: NetworkCapabilities
|
||||||
) {
|
) {
|
||||||
trySend(NetworkStatus.CapabilitiesChanged(network, 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 =
|
||||||
else -> {
|
NetworkRequest.Builder()
|
||||||
object : ConnectivityManager.NetworkCallback() {
|
.addTransportType(networkCapability)
|
||||||
|
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||||
override fun onAvailable(network: Network) {
|
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||||
trySend(NetworkStatus.Available(network))
|
.build()
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
|
||||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
|
||||||
.build()
|
|
||||||
connectivityManager.registerNetworkCallback(request, networkStatusCallback)
|
connectivityManager.registerNetworkCallback(request, networkStatusCallback)
|
||||||
|
|
||||||
awaitClose {
|
awaitClose { connectivityManager.unregisterNetworkCallback(networkStatusCallback) }
|
||||||
connectivityManager.unregisterNetworkCallback(networkStatusCallback)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun getNetworkName(networkCapabilities: NetworkCapabilities): String? {
|
override fun getNetworkName(networkCapabilities: NetworkCapabilities): String? {
|
||||||
var ssid: String? = getWifiNameFromCapabilities(networkCapabilities)
|
var ssid: String? = getWifiNameFromCapabilities(networkCapabilities)
|
||||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
|
||||||
@@ -89,7 +99,6 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(val context: Contex
|
|||||||
return ssid?.trim('"')
|
return ssid?.trim('"')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private fun getWifiNameFromCapabilities(networkCapabilities: NetworkCapabilities): String? {
|
private fun getWifiNameFromCapabilities(networkCapabilities: NetworkCapabilities): String? {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
@@ -105,13 +114,18 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(val context: Contex
|
|||||||
}
|
}
|
||||||
|
|
||||||
inline fun <Result> Flow<NetworkStatus>.map(
|
inline fun <Result> Flow<NetworkStatus>.map(
|
||||||
crossinline onUnavailable: suspend (network : Network) -> Result,
|
crossinline onUnavailable: suspend (network: Network) -> Result,
|
||||||
crossinline onAvailable: suspend (network : Network) -> Result,
|
crossinline onAvailable: suspend (network: Network) -> Result,
|
||||||
crossinline onCapabilitiesChanged: suspend (network : Network, networkCapabilities : NetworkCapabilities) -> Result,
|
crossinline onCapabilitiesChanged:
|
||||||
|
suspend (network: Network, networkCapabilities: NetworkCapabilities) -> Result
|
||||||
): Flow<Result> = map { status ->
|
): Flow<Result> = map { status ->
|
||||||
when (status) {
|
when (status) {
|
||||||
is NetworkStatus.Unavailable -> onUnavailable(status.network)
|
is NetworkStatus.Unavailable -> onUnavailable(status.network)
|
||||||
is NetworkStatus.Available -> onAvailable(status.network)
|
is NetworkStatus.Available -> onAvailable(status.network)
|
||||||
is NetworkStatus.CapabilitiesChanged -> onCapabilitiesChanged(status.network, status.networkCapabilities)
|
is NetworkStatus.CapabilitiesChanged ->
|
||||||
|
onCapabilitiesChanged(
|
||||||
|
status.network,
|
||||||
|
status.networkCapabilities,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-2
@@ -6,5 +6,4 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class EthernetService @Inject constructor(@ApplicationContext context: Context) :
|
class EthernetService @Inject constructor(@ApplicationContext context: Context) :
|
||||||
BaseNetworkService<EthernetService>(context, NetworkCapabilities.TRANSPORT_ETHERNET) {
|
BaseNetworkService<EthernetService>(context, NetworkCapabilities.TRANSPORT_ETHERNET)
|
||||||
}
|
|
||||||
|
|||||||
+1
-2
@@ -6,5 +6,4 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class MobileDataService @Inject constructor(@ApplicationContext context: Context) :
|
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
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
interface NetworkService<T> {
|
interface NetworkService<T> {
|
||||||
fun getNetworkName(networkCapabilities: NetworkCapabilities) : String?
|
fun getNetworkName(networkCapabilities: NetworkCapabilities): String?
|
||||||
val networkStatus : Flow<NetworkStatus>
|
|
||||||
|
|
||||||
}
|
val networkStatus: Flow<NetworkStatus>
|
||||||
|
}
|
||||||
|
|||||||
+6
-3
@@ -4,7 +4,10 @@ import android.net.Network
|
|||||||
import android.net.NetworkCapabilities
|
import android.net.NetworkCapabilities
|
||||||
|
|
||||||
sealed class NetworkStatus {
|
sealed class NetworkStatus {
|
||||||
class Available(val network : Network) : NetworkStatus()
|
class Available(val network: Network) : NetworkStatus()
|
||||||
class Unavailable(val network : Network) : NetworkStatus()
|
|
||||||
class CapabilitiesChanged(val network : Network, val networkCapabilities : NetworkCapabilities) : 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
|
import javax.inject.Inject
|
||||||
|
|
||||||
class WifiService @Inject constructor(@ApplicationContext context: Context) :
|
class WifiService @Inject constructor(@ApplicationContext context: Context) :
|
||||||
BaseNetworkService<WifiService>(context, NetworkCapabilities.TRANSPORT_WIFI) {
|
BaseNetworkService<WifiService>(context, NetworkCapabilities.TRANSPORT_WIFI)
|
||||||
}
|
|
||||||
|
|||||||
+5
-4
@@ -12,10 +12,11 @@ interface NotificationService {
|
|||||||
action: PendingIntent? = null,
|
action: PendingIntent? = null,
|
||||||
actionText: String? = null,
|
actionText: String? = null,
|
||||||
description: String,
|
description: String,
|
||||||
showTimestamp : Boolean = false,
|
showTimestamp: Boolean = false,
|
||||||
importance: Int = NotificationManager.IMPORTANCE_HIGH,
|
importance: Int = NotificationManager.IMPORTANCE_HIGH,
|
||||||
vibration: Boolean = true,
|
vibration: Boolean = false,
|
||||||
onGoing: Boolean = true,
|
onGoing: Boolean = true,
|
||||||
lights: Boolean = true
|
lights: Boolean = true,
|
||||||
|
onlyAlertOnce: Boolean = true,
|
||||||
): Notification
|
): Notification
|
||||||
}
|
}
|
||||||
|
|||||||
+55
-31
@@ -7,14 +7,27 @@ import android.app.PendingIntent
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.MainActivity
|
import com.zaneschepke.wireguardautotunnel.ui.MainActivity
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import javax.inject.Inject
|
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(
|
override fun createNotification(
|
||||||
channelId: String,
|
channelId: String,
|
||||||
@@ -27,20 +40,23 @@ class WireGuardNotification @Inject constructor(@ApplicationContext private val
|
|||||||
importance: Int,
|
importance: Int,
|
||||||
vibration: Boolean,
|
vibration: Boolean,
|
||||||
onGoing: Boolean,
|
onGoing: Boolean,
|
||||||
lights: Boolean
|
lights: Boolean,
|
||||||
|
onlyAlertOnce: Boolean,
|
||||||
): Notification {
|
): Notification {
|
||||||
val channel = NotificationChannel(
|
val channel =
|
||||||
channelId,
|
NotificationChannel(
|
||||||
channelName,
|
channelId,
|
||||||
importance
|
channelName,
|
||||||
).let {
|
importance,
|
||||||
it.description = title
|
)
|
||||||
it.enableLights(lights)
|
.let {
|
||||||
it.lightColor = Color.RED
|
it.description = title
|
||||||
it.enableVibration(vibration)
|
it.enableLights(lights)
|
||||||
it.vibrationPattern = longArrayOf(100, 200, 300, 400, 500, 400, 300, 200, 400)
|
it.lightColor = Color.RED
|
||||||
it
|
it.enableVibration(vibration)
|
||||||
}
|
it.vibrationPattern = longArrayOf(100, 200, 300)
|
||||||
|
it
|
||||||
|
}
|
||||||
notificationManager.createNotificationChannel(channel)
|
notificationManager.createNotificationChannel(channel)
|
||||||
val pendingIntent: PendingIntent =
|
val pendingIntent: PendingIntent =
|
||||||
Intent(context, MainActivity::class.java).let { notificationIntent ->
|
Intent(context, MainActivity::class.java).let { notificationIntent ->
|
||||||
@@ -48,30 +64,38 @@ class WireGuardNotification @Inject constructor(@ApplicationContext private val
|
|||||||
context,
|
context,
|
||||||
0,
|
0,
|
||||||
notificationIntent,
|
notificationIntent,
|
||||||
PendingIntent.FLAG_IMMUTABLE
|
PendingIntent.FLAG_IMMUTABLE,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val builder: Notification.Builder =
|
val builder =
|
||||||
Notification.Builder(
|
when (channelId) {
|
||||||
context,
|
context.getString(R.string.watcher_channel_id) -> watcherBuilder
|
||||||
channelId
|
context.getString(R.string.vpn_channel_id) -> tunnelBuilder
|
||||||
)
|
else -> {
|
||||||
return builder.let {
|
NotificationCompat.Builder(
|
||||||
if(action != null && actionText != null) {
|
context,
|
||||||
//TODO find a not deprecated way to do this
|
channelId,
|
||||||
it.addAction(
|
)
|
||||||
Notification.Action.Builder(0, actionText, action)
|
}
|
||||||
.build())
|
|
||||||
it.setAutoCancel(true)
|
|
||||||
}
|
}
|
||||||
it.setContentTitle(title)
|
|
||||||
|
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)
|
.setContentText(description)
|
||||||
|
.setOnlyAlertOnce(onlyAlertOnce)
|
||||||
.setContentIntent(pendingIntent)
|
.setContentIntent(pendingIntent)
|
||||||
.setOngoing(onGoing)
|
.setOngoing(onGoing)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
.setShowWhen(showTimestamp)
|
.setShowWhen(showTimestamp)
|
||||||
.setSmallIcon(R.mipmap.ic_launcher_foreground)
|
.setSmallIcon(R.drawable.ic_launcher)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+55
-43
@@ -1,77 +1,89 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.service.shortcut
|
package com.zaneschepke.wireguardautotunnel.service.shortcut
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
|
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
|
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
|
||||||
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.cancel
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class ShortcutsActivity : ComponentActivity() {
|
class ShortcutsActivity : ComponentActivity() {
|
||||||
|
@Inject lateinit var settingsRepository: SettingsRepository
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var tunnelConfigRepository: TunnelConfigRepository
|
||||||
lateinit var settingsRepo : SettingsDoa
|
|
||||||
|
|
||||||
@Inject
|
private suspend fun toggleWatcherServicePause() {
|
||||||
lateinit var tunnelConfigRepo : TunnelConfigDao
|
val settings = settingsRepository.getSettings()
|
||||||
|
if (settings.isAutoTunnelEnabled) {
|
||||||
|
val pauseAutoTunnel = !settings.isAutoTunnelPaused
|
||||||
private fun attemptWatcherServiceToggle(tunnelConfig : String) {
|
settingsRepository.save(
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
settings.copy(
|
||||||
val settings = getSettings()
|
isAutoTunnelPaused = pauseAutoTunnel,
|
||||||
if(settings.isAutoTunnelEnabled) {
|
),
|
||||||
ServiceManager.toggleWatcherServiceForeground(this@ShortcutsActivity, tunnelConfig)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
if(intent.getStringExtra(CLASS_NAME_EXTRA_KEY)
|
setContentView(View(this))
|
||||||
.equals(WireGuardTunnelService::class.java.simpleName)) {
|
if (
|
||||||
|
intent
|
||||||
|
.getStringExtra(CLASS_NAME_EXTRA_KEY)
|
||||||
|
.equals(WireGuardTunnelService::class.java.simpleName)
|
||||||
|
) {
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
try {
|
val settings = settingsRepository.getSettings()
|
||||||
val settings = getSettings()
|
if (settings.isShortcutsEnabled) {
|
||||||
val tunnelConfig = if(settings.defaultTunnel == null) {
|
try {
|
||||||
tunnelConfigRepo.getAll().first()
|
val tunnelName = intent.getStringExtra(TUNNEL_NAME_EXTRA_KEY)
|
||||||
} else {
|
val tunnelConfig =
|
||||||
TunnelConfig.from(settings.defaultTunnel!!)
|
if (tunnelName != null) {
|
||||||
|
tunnelConfigRepository.getAll().firstOrNull {
|
||||||
|
it.name == tunnelName
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (settings.defaultTunnel == null) {
|
||||||
|
tunnelConfigRepository.getAll().first()
|
||||||
|
} else {
|
||||||
|
TunnelConfig.from(settings.defaultTunnel!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tunnelConfig ?: return@launch
|
||||||
|
toggleWatcherServicePause()
|
||||||
|
when (intent.action) {
|
||||||
|
Action.STOP.name ->
|
||||||
|
ServiceManager.stopVpnService(
|
||||||
|
this@ShortcutsActivity,
|
||||||
|
)
|
||||||
|
Action.START.name ->
|
||||||
|
ServiceManager.startVpnServiceForeground(
|
||||||
|
this@ShortcutsActivity,
|
||||||
|
tunnelConfig.toString(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e.message)
|
||||||
|
finish()
|
||||||
}
|
}
|
||||||
attemptWatcherServiceToggle(tunnelConfig.toString())
|
|
||||||
when(intent.action){
|
|
||||||
Action.STOP.name -> ServiceManager.stopVpnService(this@ShortcutsActivity)
|
|
||||||
Action.START.name -> ServiceManager.startVpnService(this@ShortcutsActivity, tunnelConfig.toString())
|
|
||||||
}
|
|
||||||
} catch (e : Exception) {
|
|
||||||
Timber.e(e.message)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getSettings() : Settings {
|
|
||||||
val settings = settingsRepo.getAll()
|
|
||||||
return if (settings.isNotEmpty()) {
|
|
||||||
settings.first()
|
|
||||||
} else {
|
|
||||||
throw WgTunnelException("Settings empty")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
companion object {
|
companion object {
|
||||||
|
const val TUNNEL_NAME_EXTRA_KEY = "tunnelName"
|
||||||
const val CLASS_NAME_EXTRA_KEY = "className"
|
const val CLASS_NAME_EXTRA_KEY = "className"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+72
-91
@@ -4,77 +4,86 @@ import android.os.Build
|
|||||||
import android.service.quicksettings.Tile
|
import android.service.quicksettings.Tile
|
||||||
import android.service.quicksettings.TileService
|
import android.service.quicksettings.TileService
|
||||||
import com.wireguard.android.backend.Tunnel
|
import com.wireguard.android.backend.Tunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
|
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class TunnelControlTile : TileService() {
|
class TunnelControlTile() : TileService() {
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var tunnelConfigRepository: TunnelConfigRepository
|
||||||
lateinit var settingsRepo : SettingsDoa
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var settingsRepository: SettingsRepository
|
||||||
lateinit var configRepo : TunnelConfigDao
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var vpnService: VpnService
|
||||||
lateinit var vpnService : VpnService
|
|
||||||
|
|
||||||
private val scope = CoroutineScope(Dispatchers.Main);
|
private val scope = CoroutineScope(Dispatchers.IO)
|
||||||
|
|
||||||
private lateinit var job : Job
|
private var tunnelName: String? = null
|
||||||
|
|
||||||
override fun onStartListening() {
|
override fun onStartListening() {
|
||||||
job = scope.launch {
|
|
||||||
updateTileState()
|
|
||||||
}
|
|
||||||
super.onStartListening()
|
super.onStartListening()
|
||||||
}
|
Timber.d("On start listening called")
|
||||||
|
|
||||||
override fun onTileAdded() {
|
|
||||||
super.onTileAdded()
|
|
||||||
qsTile.contentDescription = this.resources.getString(R.string.toggle_vpn)
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
updateTileState();
|
vpnService.vpnState.collect {
|
||||||
|
when (it.status) {
|
||||||
|
Tunnel.State.UP -> setActive()
|
||||||
|
Tunnel.State.DOWN -> setInactive()
|
||||||
|
else -> setInactive()
|
||||||
|
}
|
||||||
|
val tunnels = tunnelConfigRepository.getAll()
|
||||||
|
if (tunnels.isEmpty()) {
|
||||||
|
setUnavailable()
|
||||||
|
return@collect
|
||||||
|
}
|
||||||
|
tunnelName =
|
||||||
|
it.name.ifBlank {
|
||||||
|
val settings = settingsRepository.getSettings()
|
||||||
|
if (settings.defaultTunnel != null) {
|
||||||
|
TunnelConfig.from(settings.defaultTunnel!!).name
|
||||||
|
} else tunnels.firstOrNull()?.name
|
||||||
|
}
|
||||||
|
setTileDescription(tunnelName ?: "")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTileRemoved() {
|
|
||||||
super.onTileRemoved()
|
|
||||||
cancelJob()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
scope.cancel()
|
scope.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onTileRemoved() {
|
||||||
|
super.onTileRemoved()
|
||||||
|
scope.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onClick() {
|
override fun onClick() {
|
||||||
super.onClick()
|
super.onClick()
|
||||||
unlockAndRun {
|
unlockAndRun {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
val tunnel = determineTileTunnel();
|
val tunnelConfig =
|
||||||
if(tunnel != null) {
|
tunnelConfigRepository.getAll().first { it.name == tunnelName }
|
||||||
attemptWatcherServiceToggle(tunnel.toString())
|
toggleWatcherServicePause()
|
||||||
if(vpnService.getState() == Tunnel.State.UP) {
|
if (vpnService.getState() == Tunnel.State.UP) {
|
||||||
ServiceManager.stopVpnService(this@TunnelControlTile)
|
ServiceManager.stopVpnService(this@TunnelControlTile)
|
||||||
} else {
|
} else {
|
||||||
ServiceManager.startVpnServiceForeground(this@TunnelControlTile, tunnel.toString())
|
ServiceManager.startVpnServiceForeground(
|
||||||
}
|
this@TunnelControlTile,
|
||||||
|
tunnelConfig.toString(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} catch (e : Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e.message)
|
Timber.e(e.message)
|
||||||
} finally {
|
} finally {
|
||||||
cancel()
|
cancel()
|
||||||
@@ -83,70 +92,42 @@ class TunnelControlTile : TileService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun determineTileTunnel() : TunnelConfig? {
|
private fun toggleWatcherServicePause() {
|
||||||
var tunnelConfig : TunnelConfig? = null;
|
|
||||||
val settings = settingsRepo.getAll()
|
|
||||||
if (settings.isNotEmpty()) {
|
|
||||||
val setting = settings.first()
|
|
||||||
tunnelConfig = if (setting.defaultTunnel != null) {
|
|
||||||
TunnelConfig.from(setting.defaultTunnel!!);
|
|
||||||
} else {
|
|
||||||
val configs = configRepo.getAll();
|
|
||||||
val config = if(configs.isNotEmpty()) {
|
|
||||||
configs.first();
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
config
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tunnelConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun attemptWatcherServiceToggle(tunnelConfig : String) {
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val settings = settingsRepo.getAll()
|
val settings = settingsRepository.getSettings()
|
||||||
if (settings.isNotEmpty()) {
|
if (settings.isAutoTunnelEnabled) {
|
||||||
val setting = settings.first()
|
val pauseAutoTunnel = !settings.isAutoTunnelPaused
|
||||||
if(setting.isAutoTunnelEnabled) {
|
settingsRepository.save(
|
||||||
ServiceManager.toggleWatcherServiceForeground(this@TunnelControlTile, tunnelConfig)
|
settings.copy(
|
||||||
}
|
isAutoTunnelPaused = pauseAutoTunnel,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun updateTileState() {
|
private fun setActive() {
|
||||||
vpnService.state.collect {
|
qsTile.state = Tile.STATE_ACTIVE
|
||||||
when(it) {
|
qsTile.updateTile()
|
||||||
Tunnel.State.UP -> {
|
|
||||||
qsTile.state = Tile.STATE_ACTIVE
|
|
||||||
}
|
|
||||||
Tunnel.State.DOWN -> {
|
|
||||||
qsTile.state = Tile.STATE_INACTIVE;
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
qsTile.state = Tile.STATE_UNAVAILABLE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val config = determineTileTunnel();
|
|
||||||
setTileDescription(config?.name ?: this.resources.getString(R.string.no_tunnel_available))
|
|
||||||
qsTile.updateTile()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setTileDescription(description : String) {
|
private fun setInactive() {
|
||||||
|
qsTile.state = Tile.STATE_INACTIVE
|
||||||
|
qsTile.updateTile()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setUnavailable() {
|
||||||
|
qsTile.state = Tile.STATE_UNAVAILABLE
|
||||||
|
qsTile.updateTile()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setTileDescription(description: String) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
qsTile.subtitle = description
|
qsTile.subtitle = description
|
||||||
}
|
}
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
qsTile.stateDescription = description;
|
qsTile.stateDescription = description
|
||||||
}
|
}
|
||||||
|
qsTile.updateTile()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
private fun cancelJob() {
|
|
||||||
if(this::job.isInitialized) {
|
|
||||||
job.cancel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+7
-5
@@ -2,13 +2,15 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel
|
|||||||
|
|
||||||
enum class HandshakeStatus {
|
enum class HandshakeStatus {
|
||||||
HEALTHY,
|
HEALTHY,
|
||||||
UNHEALTHY,
|
STALE,
|
||||||
NEVER_CONNECTED,
|
UNKNOWN,
|
||||||
NOT_STARTED;
|
NOT_STARTED;
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val WG_TYPICAL_HANDSHAKE_INTERVAL_WHEN_HEALTHY_SEC = 120
|
private const val WG_TYPICAL_HANDSHAKE_INTERVAL_WHEN_HEALTHY_SEC = 180
|
||||||
const val UNHEALTHY_TIME_LIMIT_SEC = WG_TYPICAL_HANDSHAKE_INTERVAL_WHEN_HEALTHY_SEC + 60
|
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
|
const val NEVER_CONNECTED_TO_UNHEALTHY_TIME_LIMIT_SEC = 30
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,15 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.service.tunnel
|
package com.zaneschepke.wireguardautotunnel.service.tunnel
|
||||||
|
|
||||||
import com.wireguard.android.backend.Statistics
|
|
||||||
import com.wireguard.android.backend.Tunnel
|
import com.wireguard.android.backend.Tunnel
|
||||||
import com.wireguard.crypto.Key
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
|
||||||
|
|
||||||
interface VpnService : Tunnel {
|
interface VpnService : Tunnel {
|
||||||
suspend fun startTunnel(tunnelConfig : TunnelConfig) : Tunnel.State
|
suspend fun startTunnel(tunnelConfig: TunnelConfig): Tunnel.State
|
||||||
|
|
||||||
suspend fun stopTunnel()
|
suspend fun stopTunnel()
|
||||||
val state : SharedFlow<Tunnel.State>
|
|
||||||
val tunnelName : SharedFlow<String>
|
val vpnState: StateFlow<VpnState>
|
||||||
val statistics : SharedFlow<Statistics>
|
|
||||||
val lastHandshake : SharedFlow<Map<Key,Long>>
|
fun getState(): Tunnel.State
|
||||||
val handshakeStatus : SharedFlow<HandshakeStatus>
|
}
|
||||||
fun getState() : Tunnel.State
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.service.tunnel
|
||||||
|
|
||||||
|
import com.wireguard.android.backend.Statistics
|
||||||
|
import com.wireguard.android.backend.Tunnel
|
||||||
|
|
||||||
|
data class VpnState(
|
||||||
|
val status: Tunnel.State = Tunnel.State.DOWN,
|
||||||
|
val name: String = "",
|
||||||
|
val statistics: Statistics? = null
|
||||||
|
)
|
||||||
+102
-89
@@ -3,134 +3,147 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel
|
|||||||
import com.wireguard.android.backend.Backend
|
import com.wireguard.android.backend.Backend
|
||||||
import com.wireguard.android.backend.BackendException
|
import com.wireguard.android.backend.BackendException
|
||||||
import com.wireguard.android.backend.Statistics
|
import com.wireguard.android.backend.Statistics
|
||||||
import com.wireguard.android.backend.Tunnel
|
import com.wireguard.android.backend.Tunnel.State
|
||||||
import com.wireguard.crypto.Key
|
import com.wireguard.config.Config
|
||||||
import com.zaneschepke.wireguardautotunnel.Constants
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.module.Kernel
|
||||||
|
import com.zaneschepke.wireguardautotunnel.module.Userspace
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.cancel
|
|
||||||
import kotlinx.coroutines.channels.BufferOverflow
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class WireGuardTunnel
|
||||||
class WireGuardTunnel @Inject constructor(private val backend : Backend,
|
@Inject
|
||||||
|
constructor(
|
||||||
|
@Userspace private val userspaceBackend : Backend,
|
||||||
|
@Kernel private val kernelBackend: Backend,
|
||||||
|
private val settingsRepository: SettingsRepository
|
||||||
) : VpnService {
|
) : VpnService {
|
||||||
|
private val _vpnState = MutableStateFlow(VpnState())
|
||||||
|
override val vpnState: StateFlow<VpnState> = _vpnState.asStateFlow()
|
||||||
|
|
||||||
private val _tunnelName = MutableStateFlow("")
|
private val scope = CoroutineScope(Dispatchers.IO)
|
||||||
override val tunnelName get() = _tunnelName.asStateFlow()
|
|
||||||
|
|
||||||
private val _state = MutableSharedFlow<Tunnel.State>(
|
private lateinit var statsJob: Job
|
||||||
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
|
||||||
replay = 1)
|
|
||||||
|
|
||||||
private val _handshakeStatus = MutableSharedFlow<HandshakeStatus>(replay = 1,
|
private var config: Config? = null
|
||||||
onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
|
||||||
override val state get() = _state.asSharedFlow()
|
|
||||||
|
|
||||||
private val _statistics = MutableSharedFlow<Statistics>(replay = 1)
|
private var backend: Backend = userspaceBackend
|
||||||
override val statistics get() = _statistics.asSharedFlow()
|
|
||||||
|
|
||||||
private val _lastHandshake = MutableSharedFlow<Map<Key, Long>>(replay = 1)
|
private var backendIsUserspace = true
|
||||||
override val lastHandshake get() = _lastHandshake.asSharedFlow()
|
|
||||||
|
|
||||||
override val handshakeStatus: SharedFlow<HandshakeStatus>
|
init {
|
||||||
get() = _handshakeStatus.asSharedFlow()
|
scope.launch {
|
||||||
|
settingsRepository.getSettingsFlow().collect {
|
||||||
private val scope = CoroutineScope(Dispatchers.IO);
|
if (it.isKernelEnabled && backendIsUserspace) {
|
||||||
|
Timber.d("Setting kernel backend")
|
||||||
private lateinit var statsJob : Job
|
backend = kernelBackend
|
||||||
|
backendIsUserspace = false
|
||||||
|
} else if (!it.isKernelEnabled && !backendIsUserspace) {
|
||||||
override suspend fun startTunnel(tunnelConfig: TunnelConfig) : Tunnel.State{
|
Timber.d("Setting userspace backend")
|
||||||
return try {
|
backend = userspaceBackend
|
||||||
if(getState() == Tunnel.State.UP && _tunnelName.value != tunnelConfig.name) {
|
backendIsUserspace = true
|
||||||
stopTunnel()
|
}
|
||||||
}
|
}
|
||||||
_tunnelName.emit(tunnelConfig.name)
|
}
|
||||||
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
}
|
||||||
val state = backend.setState(
|
|
||||||
this, Tunnel.State.UP, config)
|
override suspend fun startTunnel(tunnelConfig: TunnelConfig): State {
|
||||||
_state.emit(state)
|
return try {
|
||||||
state;
|
stopTunnelOnConfigChange(tunnelConfig)
|
||||||
} catch (e : Exception) {
|
emitTunnelName(tunnelConfig.name)
|
||||||
|
config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
||||||
|
val state =
|
||||||
|
backend.setState(
|
||||||
|
this,
|
||||||
|
State.UP,
|
||||||
|
config,
|
||||||
|
)
|
||||||
|
emitTunnelState(state)
|
||||||
|
state
|
||||||
|
} catch (e: Exception) {
|
||||||
Timber.e("Failed to start tunnel with error: ${e.message}")
|
Timber.e("Failed to start tunnel with error: ${e.message}")
|
||||||
Tunnel.State.DOWN
|
State.DOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun emitTunnelState(state: State) {
|
||||||
|
_vpnState.tryEmit(
|
||||||
|
_vpnState.value.copy(
|
||||||
|
status = state,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun emitBackendStatistics(statistics: Statistics) {
|
||||||
|
_vpnState.tryEmit(
|
||||||
|
_vpnState.value.copy(
|
||||||
|
statistics = statistics,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun emitTunnelName(name: String) {
|
||||||
|
_vpnState.emit(
|
||||||
|
_vpnState.value.copy(
|
||||||
|
name = name,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun stopTunnelOnConfigChange(tunnelConfig: TunnelConfig) {
|
||||||
|
if (getState() == State.UP && _vpnState.value.name != tunnelConfig.name) {
|
||||||
|
stopTunnel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getName(): String {
|
override fun getName(): String {
|
||||||
return _tunnelName.value
|
return _vpnState.value.name
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun stopTunnel() {
|
override suspend fun stopTunnel() {
|
||||||
try {
|
try {
|
||||||
if(getState() == Tunnel.State.UP) {
|
if (getState() == State.UP) {
|
||||||
val state = backend.setState(this, Tunnel.State.DOWN, null)
|
val state = backend.setState(this, State.DOWN, null)
|
||||||
_state.emit(state)
|
emitTunnelState(state)
|
||||||
}
|
}
|
||||||
} catch (e : BackendException) {
|
} catch (e: BackendException) {
|
||||||
Timber.e("Failed to stop tunnel with error: ${e.message}")
|
Timber.e("Failed to stop tunnel with error: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getState(): Tunnel.State {
|
override fun getState(): State {
|
||||||
return backend.getState(this)
|
return backend.getState(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStateChange(state : Tunnel.State) {
|
override fun onStateChange(state: State) {
|
||||||
val tunnel = this;
|
val tunnel = this
|
||||||
_state.tryEmit(state)
|
emitTunnelState(state)
|
||||||
if(state == Tunnel.State.UP) {
|
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
||||||
statsJob = scope.launch {
|
if (state == State.UP) {
|
||||||
val handshakeMap = HashMap<Key, Long>()
|
statsJob =
|
||||||
var neverHadHandshakeCounter = 0
|
scope.launch {
|
||||||
while (true) {
|
while (true) {
|
||||||
val statistics = backend.getStatistics(tunnel)
|
val statistics = backend.getStatistics(tunnel)
|
||||||
_statistics.emit(statistics)
|
emitBackendStatistics(statistics)
|
||||||
statistics.peers().forEach {
|
delay(Constants.VPN_STATISTIC_CHECK_INTERVAL)
|
||||||
val handshakeEpoch = statistics.peer(it)?.latestHandshakeEpochMillis ?: 0L
|
|
||||||
handshakeMap[it] = handshakeEpoch
|
|
||||||
if(handshakeEpoch == 0L) {
|
|
||||||
if(neverHadHandshakeCounter >= HandshakeStatus.NEVER_CONNECTED_TO_UNHEALTHY_TIME_LIMIT_SEC) {
|
|
||||||
_handshakeStatus.emit(HandshakeStatus.NEVER_CONNECTED)
|
|
||||||
} else {
|
|
||||||
_handshakeStatus.emit(HandshakeStatus.NOT_STARTED)
|
|
||||||
}
|
|
||||||
if(neverHadHandshakeCounter <= HandshakeStatus.NEVER_CONNECTED_TO_UNHEALTHY_TIME_LIMIT_SEC) {
|
|
||||||
neverHadHandshakeCounter += 10
|
|
||||||
}
|
|
||||||
return@forEach
|
|
||||||
}
|
|
||||||
if(NumberUtils.getSecondsBetweenTimestampAndNow(handshakeEpoch) >= HandshakeStatus.UNHEALTHY_TIME_LIMIT_SEC) {
|
|
||||||
_handshakeStatus.emit(HandshakeStatus.UNHEALTHY)
|
|
||||||
} else {
|
|
||||||
_handshakeStatus.emit(HandshakeStatus.HEALTHY)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_lastHandshake.emit(handshakeMap)
|
|
||||||
delay(Constants.VPN_STATISTIC_CHECK_INTERVAL)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if(state == Tunnel.State.DOWN) {
|
if (state == State.DOWN) {
|
||||||
if(this::statsJob.isInitialized) {
|
if (this::statsJob.isInitialized) {
|
||||||
statsJob.cancel()
|
statsJob.cancel()
|
||||||
}
|
}
|
||||||
_handshakeStatus.tryEmit(HandshakeStatus.NOT_STARTED)
|
|
||||||
_lastHandshake.tryEmit(emptyMap())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class ActivityViewModel
|
||||||
|
@Inject
|
||||||
|
constructor() : ViewModel() {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -2,4 +2,4 @@ package com.zaneschepke.wireguardautotunnel.ui
|
|||||||
|
|
||||||
import com.journeyapps.barcodescanner.CaptureActivity
|
import com.journeyapps.barcodescanner.CaptureActivity
|
||||||
|
|
||||||
class CaptureActivityPortrait : CaptureActivity()
|
class CaptureActivityPortrait : CaptureActivity()
|
||||||
|
|||||||
@@ -6,16 +6,13 @@ import android.net.Uri
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.view.KeyEvent
|
import androidx.activity.SystemBarStyle
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.animation.ExitTransition
|
import androidx.compose.foundation.focusable
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.animation.fadeIn
|
|
||||||
import androidx.compose.animation.slideInHorizontally
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.SnackbarData
|
import androidx.compose.material3.SnackbarData
|
||||||
@@ -25,221 +22,201 @@ import androidx.compose.material3.SnackbarHostState
|
|||||||
import androidx.compose.material3.SnackbarResult
|
import androidx.compose.material3.SnackbarResult
|
||||||
import androidx.compose.material3.surfaceColorAtElevation
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.input.key.onKeyEvent
|
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.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.google.accompanist.navigation.animation.AnimatedNavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import com.google.accompanist.navigation.animation.composable
|
import androidx.navigation.compose.composable
|
||||||
import com.google.accompanist.navigation.animation.rememberAnimatedNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
import com.google.accompanist.permissions.isGranted
|
import com.google.accompanist.permissions.isGranted
|
||||||
import com.google.accompanist.permissions.rememberPermissionState
|
import com.google.accompanist.permissions.rememberPermissionState
|
||||||
import com.wireguard.android.backend.GoBackend
|
|
||||||
import com.zaneschepke.wireguardautotunnel.Constants
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.PermissionRequestFailedScreen
|
import com.zaneschepke.wireguardautotunnel.ui.common.PermissionRequestFailedScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar
|
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.detail.DetailScreen
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.TransparentSystemBars
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
|
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.io.IOException
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
@OptIn(ExperimentalAnimationApi::class,
|
@Inject
|
||||||
ExperimentalPermissionsApi::class
|
lateinit var dataStoreManager: DataStoreManager
|
||||||
|
|
||||||
|
@Inject lateinit var settingsRepository: SettingsRepository
|
||||||
|
@OptIn(
|
||||||
|
ExperimentalPermissionsApi::class,
|
||||||
)
|
)
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
enableEdgeToEdge(navigationBarStyle = SystemBarStyle.dark(Color.Transparent.toArgb()))
|
||||||
|
|
||||||
|
// load preferences into memory and init data
|
||||||
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
dataStoreManager.init()
|
||||||
|
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Timber.e("Failed to load preferences")
|
||||||
|
}
|
||||||
|
}
|
||||||
setContent {
|
setContent {
|
||||||
val navController = rememberAnimatedNavController()
|
//val activityViewModel = hiltViewModel<ActivityViewModel>()
|
||||||
|
|
||||||
|
val navController = rememberNavController()
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
WireguardAutoTunnelTheme {
|
WireguardAutoTunnelTheme {
|
||||||
TransparentSystemBars()
|
|
||||||
|
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
|
||||||
val notificationPermissionState =
|
val notificationPermissionState = if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
|
||||||
rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS)
|
rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS) else null
|
||||||
|
|
||||||
fun requestNotificationPermission() {
|
fun requestNotificationPermission() {
|
||||||
if (!notificationPermissionState.status.isGranted && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
if (notificationPermissionState != null && !notificationPermissionState.status.isGranted
|
||||||
|
) {
|
||||||
notificationPermissionState.launchPermissionRequest()
|
notificationPermissionState.launchPermissionRequest()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var vpnIntent by remember { mutableStateOf(GoBackend.VpnService.prepare(this)) }
|
LaunchedEffect(Unit) {
|
||||||
val vpnActivityResultState = rememberLauncherForActivityResult(
|
requestNotificationPermission()
|
||||||
ActivityResultContracts.StartActivityForResult(),
|
|
||||||
onResult = {
|
|
||||||
val accepted = (it.resultCode == RESULT_OK)
|
|
||||||
if (accepted) {
|
|
||||||
vpnIntent = null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
LaunchedEffect(vpnIntent) {
|
|
||||||
if (vpnIntent != null) {
|
|
||||||
vpnActivityResultState.launch(vpnIntent)
|
|
||||||
} else requestNotificationPermission()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showSnackBarMessage(message : String) {
|
fun showSnackBarMessage(message: String) {
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
val result = snackbarHostState.showSnackbar(
|
val result =
|
||||||
message = message,
|
snackbarHostState.showSnackbar(
|
||||||
actionLabel = applicationContext.getString(R.string.okay),
|
message = message,
|
||||||
duration = SnackbarDuration.Short,
|
actionLabel = applicationContext.getString(R.string.okay),
|
||||||
)
|
duration = SnackbarDuration.Short,
|
||||||
|
)
|
||||||
when (result) {
|
when (result) {
|
||||||
SnackbarResult.ActionPerformed -> { snackbarHostState.currentSnackbarData?.dismiss() }
|
SnackbarResult.ActionPerformed,
|
||||||
SnackbarResult.Dismissed -> { snackbarHostState.currentSnackbarData?.dismiss() }
|
SnackbarResult.Dismissed -> {
|
||||||
|
snackbarHostState.currentSnackbarData?.dismiss()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(snackbarHost = {
|
Scaffold(
|
||||||
|
snackbarHost = {
|
||||||
SnackbarHost(snackbarHostState) { snackbarData: SnackbarData ->
|
SnackbarHost(snackbarHostState) { snackbarData: SnackbarData ->
|
||||||
CustomSnackBar(
|
CustomSnackBar(
|
||||||
snackbarData.visuals.message,
|
snackbarData.visuals.message,
|
||||||
isRtl = false,
|
isRtl = false,
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
|
containerColor =
|
||||||
|
MaterialTheme.colorScheme.surfaceColorAtElevation(
|
||||||
|
2.dp,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier = Modifier.onKeyEvent {
|
modifier = Modifier
|
||||||
if (it.nativeKeyEvent.action == KeyEvent.ACTION_UP) {
|
.focusable()
|
||||||
when (it.nativeKeyEvent.keyCode) {
|
.focusProperties { up = focusRequester },
|
||||||
KeyEvent.KEYCODE_DPAD_UP -> {
|
bottomBar =
|
||||||
try {
|
if (notificationPermissionState == null || notificationPermissionState.status.isGranted) {
|
||||||
focusRequester.requestFocus()
|
{
|
||||||
} catch(e : IllegalStateException) {
|
BottomNavBar(
|
||||||
Timber.e("No D-Pad focus request modifier added to element on screen")
|
navController,
|
||||||
}
|
listOf(
|
||||||
false
|
Screen.Main.navItem,
|
||||||
} else -> {
|
Screen.Settings.navItem,
|
||||||
false
|
Screen.Support.navItem,
|
||||||
}
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
false
|
{}
|
||||||
}
|
},
|
||||||
},
|
) { padding ->
|
||||||
bottomBar = if (vpnIntent == null && notificationPermissionState.status.isGranted) {
|
if (notificationPermissionState != null && !notificationPermissionState.status.isGranted) {
|
||||||
{ BottomNavBar(navController, Routes.navItems) }
|
Column(modifier = Modifier.padding(padding)) {
|
||||||
} else {
|
PermissionRequestFailedScreen(
|
||||||
{}
|
onRequestAgain = {
|
||||||
},
|
val intentSettings =
|
||||||
)
|
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||||
{ padding ->
|
intentSettings.data =
|
||||||
if (vpnIntent != null) {
|
Uri.fromParts(
|
||||||
PermissionRequestFailedScreen(
|
Constants.URI_PACKAGE_SCHEME,
|
||||||
padding = padding,
|
this@MainActivity.packageName,
|
||||||
onRequestAgain = { vpnActivityResultState.launch(vpnIntent) },
|
null,
|
||||||
message = getString(R.string.vpn_permission_required),
|
)
|
||||||
getString(R.string.retry)
|
startActivity(intentSettings)
|
||||||
)
|
},
|
||||||
return@Scaffold
|
message = getString(R.string.notification_permission_required),
|
||||||
}
|
getString(R.string.open_settings),
|
||||||
if (!notificationPermissionState.status.isGranted) {
|
)
|
||||||
PermissionRequestFailedScreen(
|
return@Scaffold
|
||||||
padding = padding,
|
|
||||||
onRequestAgain = {
|
|
||||||
val intentSettings =
|
|
||||||
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
|
||||||
intentSettings.data =
|
|
||||||
Uri.fromParts(Constants.URI_PACKAGE_SCHEME, 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 = { -Constants.SLIDE_IN_TRANSITION_OFFSET },
|
|
||||||
animationSpec = tween(Constants.SLIDE_IN_ANIMATION_DURATION)
|
|
||||||
)
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
fadeIn(animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, exitTransition = {
|
|
||||||
ExitTransition.None
|
|
||||||
}
|
}
|
||||||
|
NavHost(navController, startDestination = Screen.Main.route) {
|
||||||
|
composable(
|
||||||
|
Screen.Main.route,
|
||||||
) {
|
) {
|
||||||
MainScreen(padding = padding, showSnackbarMessage = { message -> showSnackBarMessage(message) }, navController = navController)
|
MainScreen(
|
||||||
}
|
focusRequester = focusRequester,
|
||||||
composable(Routes.Settings.name, enterTransition = {
|
showSnackbarMessage = { message -> showSnackBarMessage(message) },
|
||||||
when (initialState.destination.route) {
|
navController = navController,
|
||||||
Routes.Main.name ->
|
)
|
||||||
slideInHorizontally(
|
}
|
||||||
initialOffsetX = { Constants.SLIDE_IN_TRANSITION_OFFSET },
|
composable(
|
||||||
animationSpec = tween(Constants.SLIDE_IN_ANIMATION_DURATION)
|
Screen.Settings.route,
|
||||||
)
|
) {
|
||||||
|
SettingsScreen(
|
||||||
Routes.Support.name -> {
|
padding = padding,
|
||||||
slideInHorizontally(
|
showSnackbarMessage = { message -> showSnackBarMessage(message) },
|
||||||
initialOffsetX = { -Constants.SLIDE_IN_TRANSITION_OFFSET },
|
focusRequester = focusRequester,
|
||||||
animationSpec = tween(Constants.SLIDE_IN_ANIMATION_DURATION)
|
)
|
||||||
|
//
|
||||||
|
}
|
||||||
|
composable(
|
||||||
|
Screen.Support.route,
|
||||||
|
) {
|
||||||
|
SupportScreen(
|
||||||
|
padding = padding,
|
||||||
|
focusRequester = focusRequester,
|
||||||
|
showSnackbarMessage = { message -> showSnackBarMessage(message) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composable("${Screen.Config.route}/{id}") {
|
||||||
|
val id = it.arguments?.getString("id")
|
||||||
|
if (!id.isNullOrBlank()) {
|
||||||
|
ConfigScreen(
|
||||||
|
padding = padding,
|
||||||
|
navController = navController,
|
||||||
|
id = id,
|
||||||
|
showSnackbarMessage = { message ->
|
||||||
|
showSnackBarMessage(message)
|
||||||
|
},
|
||||||
|
focusRequester = focusRequester,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
|
||||||
fadeIn(animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}) { SettingsScreen(padding = padding, showSnackbarMessage = { message -> showSnackBarMessage(message) }, focusRequester = focusRequester) }
|
|
||||||
composable(Routes.Support.name, enterTransition = {
|
|
||||||
when (initialState.destination.route) {
|
|
||||||
Routes.Settings.name, Routes.Main.name ->
|
|
||||||
slideInHorizontally(
|
|
||||||
initialOffsetX = { Constants.SLIDE_IN_ANIMATION_DURATION },
|
|
||||||
animationSpec = tween(Constants.SLIDE_IN_ANIMATION_DURATION)
|
|
||||||
)
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
fadeIn(animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}) { SupportScreen(padding = padding, focusRequester) }
|
|
||||||
composable("${Routes.Config.name}/{id}", enterTransition = {
|
|
||||||
fadeIn(animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION))
|
|
||||||
}) { it ->
|
|
||||||
val id = it.arguments?.getString("id")
|
|
||||||
if(!id.isNullOrBlank()) {
|
|
||||||
ConfigScreen(navController = navController, id = id, showSnackbarMessage = { message -> showSnackBarMessage(message) }, focusRequester = focusRequester)}
|
|
||||||
}
|
|
||||||
composable("${Routes.Detail.name}/{id}", enterTransition = {
|
|
||||||
fadeIn(animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION))
|
|
||||||
}) {
|
|
||||||
val id = it.arguments?.getString("id")
|
|
||||||
if(!id.isNullOrBlank()) {
|
|
||||||
DetailScreen(padding = padding, focusRequester = focusRequester, id = id)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-17
@@ -6,31 +6,33 @@ import androidx.compose.material.icons.rounded.QuestionMark
|
|||||||
import androidx.compose.material.icons.rounded.Settings
|
import androidx.compose.material.icons.rounded.Settings
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
|
||||||
|
|
||||||
enum class Routes {
|
sealed class Screen(val route: String) {
|
||||||
Main,
|
data object Main : Screen("main") {
|
||||||
Settings,
|
val navItem =
|
||||||
Support,
|
|
||||||
Config,
|
|
||||||
Detail;
|
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val navItems = listOf(
|
|
||||||
BottomNavItem(
|
BottomNavItem(
|
||||||
name = "Tunnels",
|
name = "Tunnels",
|
||||||
route = Main.name,
|
route = route,
|
||||||
icon = Icons.Rounded.Home,
|
icon = Icons.Rounded.Home,
|
||||||
),
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data object Settings : Screen("settings") {
|
||||||
|
val navItem =
|
||||||
BottomNavItem(
|
BottomNavItem(
|
||||||
name = "Settings",
|
name = "Settings",
|
||||||
route = Settings.name,
|
route = route,
|
||||||
icon = Icons.Rounded.Settings,
|
icon = Icons.Rounded.Settings,
|
||||||
),
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data object Support : Screen("support") {
|
||||||
|
val navItem =
|
||||||
BottomNavItem(
|
BottomNavItem(
|
||||||
name = "Support",
|
name = "Support",
|
||||||
route = Support.name,
|
route = route,
|
||||||
icon = Icons.Rounded.QuestionMark,
|
icon = Icons.Rounded.QuestionMark,
|
||||||
)
|
)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
data object Config : Screen("config")
|
||||||
|
}
|
||||||
+18
-10
@@ -14,20 +14,28 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ClickableIconButton(onIconClick : () -> Unit, text : String, icon : ImageVector, enabled : Boolean) {
|
fun ClickableIconButton(
|
||||||
TextButton(onClick = {},
|
onClick: () -> Unit,
|
||||||
enabled = enabled
|
onIconClick: () -> Unit,
|
||||||
|
text: String,
|
||||||
|
icon: ImageVector,
|
||||||
|
enabled: Boolean
|
||||||
|
) {
|
||||||
|
TextButton(
|
||||||
|
onClick = onClick,
|
||||||
|
enabled = enabled,
|
||||||
) {
|
) {
|
||||||
Text(text)
|
Text(text, Modifier.weight(1f, false))
|
||||||
Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing))
|
Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing))
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = icon,
|
imageVector = icon,
|
||||||
contentDescription = stringResource(R.string.delete),
|
contentDescription = stringResource(R.string.delete),
|
||||||
modifier = Modifier.size(ButtonDefaults.IconSize).clickable {
|
modifier =
|
||||||
if(enabled) {
|
Modifier.size(ButtonDefaults.IconSize).weight(1f, false).clickable {
|
||||||
onIconClick()
|
if (enabled) {
|
||||||
}
|
onIconClick()
|
||||||
}
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-12
@@ -2,7 +2,6 @@ package com.zaneschepke.wireguardautotunnel.ui.common
|
|||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
@@ -16,20 +15,22 @@ import androidx.compose.ui.unit.dp
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PermissionRequestFailedScreen(padding : PaddingValues, onRequestAgain : () -> Unit, message : String, buttonText : String ) {
|
fun PermissionRequestFailedScreen(
|
||||||
|
onRequestAgain: () -> Unit,
|
||||||
|
message: String,
|
||||||
|
buttonText: String
|
||||||
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally,
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxSize(),
|
||||||
.fillMaxSize()
|
) {
|
||||||
.padding(padding)) {
|
|
||||||
Text(message, textAlign = TextAlign.Center, modifier = Modifier.padding(15.dp))
|
Text(message, textAlign = TextAlign.Center, modifier = Modifier.padding(15.dp))
|
||||||
Button(onClick = {
|
Button(
|
||||||
scope.launch {
|
onClick = { scope.launch { onRequestAgain() } },
|
||||||
onRequestAgain()
|
) {
|
||||||
}
|
|
||||||
}) {
|
|
||||||
Text(buttonText)
|
Text(buttonText)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,49 +1,88 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.common
|
package com.zaneschepke.wireguardautotunnel.ui.common
|
||||||
|
|
||||||
|
import androidx.compose.animation.animateContentSize
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.wireguard.android.backend.Statistics
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.toThreeDecimalPlaceString
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun RowListItem(icon : @Composable() () -> Unit, text : String, onHold : () -> Unit, onClick: () -> Unit, rowButton : @Composable() () -> Unit ) {
|
fun RowListItem(
|
||||||
|
icon: @Composable () -> Unit,
|
||||||
|
text: String,
|
||||||
|
onHold: () -> Unit,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
rowButton: @Composable () -> Unit,
|
||||||
|
expanded: Boolean,
|
||||||
|
statistics: Statistics?
|
||||||
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier =
|
||||||
.combinedClickable(
|
Modifier.animateContentSize()
|
||||||
onClick = {
|
.clip(RoundedCornerShape(30.dp))
|
||||||
onClick()
|
.combinedClickable(
|
||||||
},
|
onClick = { onClick() },
|
||||||
onLongClick = {
|
onLongClick = { onHold() },
|
||||||
onHold()
|
),
|
||||||
}
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
Row(
|
Column {
|
||||||
modifier = Modifier
|
Row(
|
||||||
.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth().padding(horizontal = 15.dp, vertical = 5.dp),
|
||||||
.padding(14.dp),
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
) {
|
||||||
) {
|
Row(
|
||||||
Row(verticalAlignment = Alignment.CenterVertically,) {
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
icon()
|
modifier = Modifier.fillMaxWidth(.60f),
|
||||||
Text(text)
|
) {
|
||||||
|
icon()
|
||||||
|
Text(text)
|
||||||
|
}
|
||||||
|
rowButton()
|
||||||
|
}
|
||||||
|
if (expanded) {
|
||||||
|
statistics?.peers()?.forEach {
|
||||||
|
Row(
|
||||||
|
modifier =
|
||||||
|
Modifier.fillMaxWidth()
|
||||||
|
.padding(end = 10.dp, bottom = 10.dp, start = 10.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
|
) {
|
||||||
|
val handshakeEpoch = statistics.peer(it)!!.latestHandshakeEpochMillis
|
||||||
|
val peerTx = statistics.peer(it)!!.txBytes
|
||||||
|
val peerRx = statistics.peer(it)!!.rxBytes
|
||||||
|
val peerId = it.toBase64().subSequence(0, 3).toString() + "***"
|
||||||
|
val handshakeSec =
|
||||||
|
NumberUtils.getSecondsBetweenTimestampAndNow(handshakeEpoch)
|
||||||
|
val handshake =
|
||||||
|
if (handshakeSec == null) "never" else "$handshakeSec secs ago"
|
||||||
|
val peerTxMB = NumberUtils.bytesToMB(peerTx).toThreeDecimalPlaceString()
|
||||||
|
val peerRxMB = NumberUtils.bytesToMB(peerRx).toThreeDecimalPlaceString()
|
||||||
|
val fontSize = 9.sp
|
||||||
|
Text("peer: $peerId", fontSize = fontSize)
|
||||||
|
Text("handshake: $handshake", fontSize = fontSize)
|
||||||
|
Text("tx: $peerTxMB MB", fontSize = fontSize)
|
||||||
|
Text("rx: $peerRxMB MB", fontSize = fontSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rowButton()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,9 +25,7 @@ import androidx.compose.ui.text.input.KeyboardType
|
|||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SearchBar(
|
fun SearchBar(onQuery: (queryString: String) -> Unit) {
|
||||||
onQuery : (queryString : String) -> Unit
|
|
||||||
) {
|
|
||||||
// Immediately update and keep track of query from text field changes.
|
// Immediately update and keep track of query from text field changes.
|
||||||
var query: String by rememberSaveable { mutableStateOf("") }
|
var query: String by rememberSaveable { mutableStateOf("") }
|
||||||
var showClearIcon by rememberSaveable { mutableStateOf(false) }
|
var showClearIcon by rememberSaveable { mutableStateOf(false) }
|
||||||
@@ -49,7 +47,7 @@ fun SearchBar(
|
|||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Rounded.Search,
|
imageVector = Icons.Rounded.Search,
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
tint = MaterialTheme.colorScheme.onBackground,
|
||||||
contentDescription = stringResource(id = R.string.search_icon)
|
contentDescription = stringResource(id = R.string.search_icon),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
@@ -58,23 +56,24 @@ fun SearchBar(
|
|||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Rounded.Clear,
|
imageVector = Icons.Rounded.Clear,
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
tint = MaterialTheme.colorScheme.onBackground,
|
||||||
contentDescription = stringResource(id = R.string.clear_icon)
|
contentDescription = stringResource(id = R.string.clear_icon),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
colors = TextFieldDefaults.colors(
|
colors =
|
||||||
focusedContainerColor = Color.Transparent,
|
TextFieldDefaults.colors(
|
||||||
unfocusedContainerColor = Color.Transparent,
|
focusedContainerColor = Color.Transparent,
|
||||||
disabledContainerColor = Color.Transparent,
|
unfocusedContainerColor = Color.Transparent,
|
||||||
),
|
disabledContainerColor = Color.Transparent,
|
||||||
|
),
|
||||||
placeholder = { Text(text = stringResource(R.string.hint_search_packages)) },
|
placeholder = { Text(text = stringResource(R.string.hint_search_packages)) },
|
||||||
textStyle = MaterialTheme.typography.bodySmall,
|
textStyle = MaterialTheme.typography.bodySmall,
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
|
||||||
modifier = Modifier
|
modifier =
|
||||||
.fillMaxWidth()
|
Modifier.fillMaxWidth()
|
||||||
.background(color = MaterialTheme.colorScheme.background, shape = RectangleShape)
|
.background(color = MaterialTheme.colorScheme.background, shape = RectangleShape),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-15
@@ -1,6 +1,5 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.common.config
|
package com.zaneschepke.wireguardautotunnel.ui.common.config
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
@@ -11,24 +10,27 @@ import androidx.compose.ui.text.input.ImeAction
|
|||||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun
|
fun ConfigurationTextBox(
|
||||||
ConfigurationTextBox(value : String, hint : String, onValueChange : (String) -> Unit, keyboardActions : KeyboardActions, label : String, modifier: Modifier) {
|
value: String,
|
||||||
OutlinedTextField(
|
hint: String,
|
||||||
|
onValueChange: (String) -> Unit,
|
||||||
|
keyboardActions: KeyboardActions,
|
||||||
|
label: String,
|
||||||
|
modifier: Modifier
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
value = value,
|
value = value,
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
onValueChange = {
|
onValueChange = { onValueChange(it) },
|
||||||
onValueChange(it)
|
|
||||||
},
|
|
||||||
label = { Text(label) },
|
label = { Text(label) },
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
placeholder = {
|
placeholder = { Text(hint) },
|
||||||
Text(hint)
|
keyboardOptions =
|
||||||
},
|
KeyboardOptions(
|
||||||
keyboardOptions = KeyboardOptions(
|
capitalization = KeyboardCapitalization.None,
|
||||||
capitalization = KeyboardCapitalization.None,
|
imeAction = ImeAction.Done,
|
||||||
imeAction = ImeAction.Done
|
),
|
||||||
),
|
|
||||||
keyboardActions = keyboardActions,
|
keyboardActions = keyboardActions,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-10
@@ -12,23 +12,25 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ConfigurationToggle(label : String, enabled : Boolean, checked : Boolean, padding : Dp,
|
fun ConfigurationToggle(
|
||||||
onCheckChanged : () -> Unit, modifier : Modifier = Modifier) {
|
label: String,
|
||||||
|
enabled: Boolean,
|
||||||
|
checked: Boolean,
|
||||||
|
padding: Dp,
|
||||||
|
onCheckChanged: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth().padding(padding),
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(padding),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
) {
|
) {
|
||||||
Text(label)
|
Text(label)
|
||||||
Switch(
|
Switch(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
checked = checked,
|
checked = checked,
|
||||||
onCheckedChange = {
|
onCheckedChange = { onCheckChanged() },
|
||||||
onCheckChanged()
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-4
@@ -11,8 +11,7 @@ import androidx.navigation.NavController
|
|||||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BottomNavBar(navController : NavController, bottomNavItems : List<BottomNavItem>) {
|
fun BottomNavBar(navController: NavController, bottomNavItems: List<BottomNavItem>) {
|
||||||
|
|
||||||
val backStackEntry = navController.currentBackStackEntryAsState()
|
val backStackEntry = navController.currentBackStackEntryAsState()
|
||||||
|
|
||||||
NavigationBar(
|
NavigationBar(
|
||||||
@@ -35,8 +34,8 @@ fun BottomNavBar(navController : NavController, bottomNavItems : List<BottomNavI
|
|||||||
imageVector = item.icon,
|
imageVector = item.icon,
|
||||||
contentDescription = "${item.name} Icon",
|
contentDescription = "${item.name} Icon",
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-27
@@ -11,12 +11,12 @@ import androidx.core.content.ContextCompat
|
|||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AuthorizationPrompt(onSuccess : () -> Unit, onFailure : () -> Unit, onError : (String) -> Unit) {
|
fun AuthorizationPrompt(onSuccess: () -> Unit, onFailure: () -> Unit, onError: (String) -> Unit) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val biometricManager = BiometricManager.from(context)
|
val biometricManager = BiometricManager.from(context)
|
||||||
val bio = biometricManager.canAuthenticate(BIOMETRIC_WEAK or DEVICE_CREDENTIAL)
|
val bio = biometricManager.canAuthenticate(BIOMETRIC_WEAK or DEVICE_CREDENTIAL)
|
||||||
val isBiometricAvailable = remember {
|
val isBiometricAvailable = remember {
|
||||||
when(bio){
|
when (bio) {
|
||||||
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> {
|
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> {
|
||||||
onError("Biometrics not available")
|
onError("Biometrics not available")
|
||||||
false
|
false
|
||||||
@@ -45,35 +45,39 @@ fun AuthorizationPrompt(onSuccess : () -> Unit, onFailure : () -> Unit, onError
|
|||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(isBiometricAvailable) {
|
if (isBiometricAvailable) {
|
||||||
val executor = remember { ContextCompat.getMainExecutor(context) }
|
val executor = remember { ContextCompat.getMainExecutor(context) }
|
||||||
|
|
||||||
val promptInfo = BiometricPrompt.PromptInfo.Builder()
|
val promptInfo =
|
||||||
.setAllowedAuthenticators(BIOMETRIC_WEAK or DEVICE_CREDENTIAL)
|
BiometricPrompt.PromptInfo.Builder()
|
||||||
.setTitle("Biometric Authentication")
|
.setAllowedAuthenticators(BIOMETRIC_WEAK or DEVICE_CREDENTIAL)
|
||||||
.setSubtitle("Log in using your biometric credential")
|
.setTitle("Biometric Authentication")
|
||||||
.build()
|
.setSubtitle("Log in using your biometric credential")
|
||||||
|
.build()
|
||||||
|
|
||||||
val biometricPrompt = BiometricPrompt(
|
val biometricPrompt =
|
||||||
context as FragmentActivity,
|
BiometricPrompt(
|
||||||
executor,
|
context as FragmentActivity,
|
||||||
object : BiometricPrompt.AuthenticationCallback() {
|
executor,
|
||||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
object : BiometricPrompt.AuthenticationCallback() {
|
||||||
super.onAuthenticationError(errorCode, errString)
|
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||||
onFailure()
|
super.onAuthenticationError(errorCode, errString)
|
||||||
}
|
onFailure()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
override fun onAuthenticationSucceeded(
|
||||||
super.onAuthenticationSucceeded(result)
|
result: BiometricPrompt.AuthenticationResult
|
||||||
onSuccess()
|
) {
|
||||||
}
|
super.onAuthenticationSucceeded(result)
|
||||||
|
onSuccess()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onAuthenticationFailed() {
|
override fun onAuthenticationFailed() {
|
||||||
super.onAuthenticationFailed()
|
super.onAuthenticationFailed()
|
||||||
onFailure()
|
onFailure()
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
biometricPrompt.authenticate(promptInfo)
|
biometricPrompt.authenticate(promptInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-14
@@ -1,10 +1,12 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.common.prompt
|
package com.zaneschepke.wireguardautotunnel.ui.common.prompt
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.rounded.Info
|
import androidx.compose.material.icons.rounded.Info
|
||||||
@@ -17,7 +19,6 @@ import androidx.compose.runtime.CompositionLocalProvider
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.LayoutDirection
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
@@ -31,28 +32,31 @@ fun CustomSnackBar(
|
|||||||
isRtl: Boolean = true,
|
isRtl: Boolean = true,
|
||||||
containerColor: Color = MaterialTheme.colorScheme.surface
|
containerColor: Color = MaterialTheme.colorScheme.surface
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
Snackbar(
|
||||||
Snackbar(containerColor = containerColor,
|
containerColor = containerColor,
|
||||||
modifier = Modifier.fillMaxWidth(
|
modifier =
|
||||||
if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) 1/3f else 2/3f).padding(bottom = 100.dp),
|
Modifier.fillMaxWidth(
|
||||||
shape = RoundedCornerShape(16.dp)
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) 1 / 3f else 2 / 3f,
|
||||||
) {
|
)
|
||||||
|
.padding(bottom = 100.dp),
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
) {
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalLayoutDirection provides
|
LocalLayoutDirection provides if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr,
|
||||||
if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr
|
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.width(IntrinsicSize.Max).height(IntrinsicSize.Min),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly
|
horizontalArrangement = Arrangement.Start,
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Rounded.Info,
|
Icons.Rounded.Info,
|
||||||
contentDescription = stringResource(R.string.info),
|
contentDescription = stringResource(R.string.info),
|
||||||
tint = Color.White
|
tint = Color.White,
|
||||||
|
modifier = Modifier.padding(end = 10.dp),
|
||||||
)
|
)
|
||||||
Text(message, color = Color.White, modifier = Modifier.padding(end = 5.dp))
|
Text(message, color = Color.White, modifier = Modifier.padding(end = 5.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.common.screen
|
||||||
|
|
||||||
|
import androidx.compose.foundation.focusable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LoadingScreen() {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Top,
|
||||||
|
modifier = Modifier.fillMaxSize().focusable().padding(),
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(120.dp)) { CircularProgressIndicator() }
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-3
@@ -12,11 +12,11 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SectionTitle(title : String, padding : Dp) {
|
fun SectionTitle(title: String, padding: Dp) {
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
style = TextStyle(fontSize = 18.sp, fontWeight = FontWeight.ExtraBold),
|
style = TextStyle(fontSize = 18.sp, fontWeight = FontWeight.ExtraBold),
|
||||||
modifier = Modifier.padding(padding, bottom = 5.dp, top = 5.dp)
|
modifier = Modifier.padding(padding, bottom = 5.dp, top = 5.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,30 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.models
|
package com.zaneschepke.wireguardautotunnel.ui.models
|
||||||
|
|
||||||
import com.wireguard.config.Interface
|
import com.wireguard.config.Interface
|
||||||
import com.wireguard.config.Peer
|
|
||||||
|
|
||||||
data class InterfaceProxy(
|
data class InterfaceProxy(
|
||||||
var privateKey : String = "",
|
var privateKey: String = "",
|
||||||
var publicKey : String = "",
|
var publicKey: String = "",
|
||||||
var addresses : String = "",
|
var addresses: String = "",
|
||||||
var dnsServers : String = "",
|
var dnsServers: String = "",
|
||||||
var listenPort : String = "",
|
var listenPort: String = "",
|
||||||
var mtu : String = "",
|
var mtu: String = ""
|
||||||
){
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
fun from(i : Interface) : InterfaceProxy {
|
fun from(i: Interface): InterfaceProxy {
|
||||||
return InterfaceProxy(
|
return InterfaceProxy(
|
||||||
publicKey = i.keyPair.publicKey.toBase64().trim(),
|
publicKey = i.keyPair.publicKey.toBase64().trim(),
|
||||||
privateKey = i.keyPair.privateKey.toBase64().trim(),
|
privateKey = i.keyPair.privateKey.toBase64().trim(),
|
||||||
addresses = i.addresses.joinToString(", ").trim(),
|
addresses = i.addresses.joinToString(", ").trim(),
|
||||||
dnsServers = i.dnsServers.joinToString(", ").replace("/", "").trim(),
|
dnsServers = i.dnsServers.joinToString(", ").replace("/", "").trim(),
|
||||||
listenPort = if(i.listenPort.isPresent) i.listenPort.get().toString().trim() else "",
|
listenPort =
|
||||||
mtu = if(i.mtu.isPresent) i.mtu.get().toString().trim() else ""
|
if (i.listenPort.isPresent) {
|
||||||
|
i.listenPort.get().toString().trim()
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
},
|
||||||
|
mtu = if (i.mtu.isPresent) i.mtu.get().toString().trim() else "",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,30 +3,71 @@ package com.zaneschepke.wireguardautotunnel.ui.models
|
|||||||
import com.wireguard.config.Peer
|
import com.wireguard.config.Peer
|
||||||
|
|
||||||
data class PeerProxy(
|
data class PeerProxy(
|
||||||
var publicKey : String = "",
|
var publicKey: String = "",
|
||||||
var preSharedKey : String = "",
|
var preSharedKey: String = "",
|
||||||
var persistentKeepalive : String = "",
|
var persistentKeepalive: String = "",
|
||||||
var endpoint : String = "",
|
var endpoint: String = "",
|
||||||
var allowedIps: String = IPV4_WILDCARD.joinToString(", ").trim()
|
var allowedIps: String = IPV4_WILDCARD.joinToString(", ").trim()
|
||||||
){
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
fun from(peer : Peer) : PeerProxy {
|
fun from(peer: Peer): PeerProxy {
|
||||||
return PeerProxy(
|
return PeerProxy(
|
||||||
publicKey = peer.publicKey.toBase64(),
|
publicKey = peer.publicKey.toBase64(),
|
||||||
preSharedKey = if(peer.preSharedKey.isPresent) peer.preSharedKey.get().toBase64().trim() else "",
|
preSharedKey =
|
||||||
persistentKeepalive = if(peer.persistentKeepalive.isPresent) peer.persistentKeepalive.get().toString().trim() else "",
|
if (peer.preSharedKey.isPresent) {
|
||||||
endpoint = if(peer.endpoint.isPresent) peer.endpoint.get().toString().trim() else "",
|
peer.preSharedKey.get().toBase64().trim()
|
||||||
allowedIps = peer.allowedIps.joinToString(", ").trim()
|
} else {
|
||||||
|
""
|
||||||
|
},
|
||||||
|
persistentKeepalive =
|
||||||
|
if (peer.persistentKeepalive.isPresent) {
|
||||||
|
peer.persistentKeepalive.get().toString().trim()
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
},
|
||||||
|
endpoint =
|
||||||
|
if (peer.endpoint.isPresent) {
|
||||||
|
peer.endpoint.get().toString().trim()
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
},
|
||||||
|
allowedIps = peer.allowedIps.joinToString(", ").trim(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val IPV4_PUBLIC_NETWORKS = setOf(
|
|
||||||
"0.0.0.0/5", "8.0.0.0/7", "11.0.0.0/8", "12.0.0.0/6", "16.0.0.0/4", "32.0.0.0/3",
|
val IPV4_PUBLIC_NETWORKS =
|
||||||
"64.0.0.0/2", "128.0.0.0/3", "160.0.0.0/5", "168.0.0.0/6", "172.0.0.0/12",
|
setOf(
|
||||||
"172.32.0.0/11", "172.64.0.0/10", "172.128.0.0/9", "173.0.0.0/8", "174.0.0.0/7",
|
"0.0.0.0/5",
|
||||||
"176.0.0.0/4", "192.0.0.0/9", "192.128.0.0/11", "192.160.0.0/13", "192.169.0.0/16",
|
"8.0.0.0/7",
|
||||||
"192.170.0.0/15", "192.172.0.0/14", "192.176.0.0/12", "192.192.0.0/10",
|
"11.0.0.0/8",
|
||||||
"193.0.0.0/8", "194.0.0.0/7", "196.0.0.0/6", "200.0.0.0/5", "208.0.0.0/4"
|
"12.0.0.0/6",
|
||||||
)
|
"16.0.0.0/4",
|
||||||
|
"32.0.0.0/3",
|
||||||
|
"64.0.0.0/2",
|
||||||
|
"128.0.0.0/3",
|
||||||
|
"160.0.0.0/5",
|
||||||
|
"168.0.0.0/6",
|
||||||
|
"172.0.0.0/12",
|
||||||
|
"172.32.0.0/11",
|
||||||
|
"172.64.0.0/10",
|
||||||
|
"172.128.0.0/9",
|
||||||
|
"173.0.0.0/8",
|
||||||
|
"174.0.0.0/7",
|
||||||
|
"176.0.0.0/4",
|
||||||
|
"192.0.0.0/9",
|
||||||
|
"192.128.0.0/11",
|
||||||
|
"192.160.0.0/13",
|
||||||
|
"192.169.0.0/16",
|
||||||
|
"192.170.0.0/15",
|
||||||
|
"192.172.0.0/14",
|
||||||
|
"192.176.0.0/12",
|
||||||
|
"192.192.0.0/10",
|
||||||
|
"193.0.0.0/8",
|
||||||
|
"194.0.0.0/7",
|
||||||
|
"196.0.0.0/6",
|
||||||
|
"200.0.0.0/5",
|
||||||
|
"208.0.0.0/4",
|
||||||
|
)
|
||||||
val IPV4_WILDCARD = setOf("0.0.0.0/0")
|
val IPV4_WILDCARD = setOf("0.0.0.0/0")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+398
-427
File diff suppressed because it is too large
Load Diff
+18
@@ -0,0 +1,18 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.config
|
||||||
|
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.models.InterfaceProxy
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.models.PeerProxy
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Packages
|
||||||
|
|
||||||
|
data class ConfigUiState(
|
||||||
|
val proxyPeers: List<PeerProxy> = arrayListOf(PeerProxy()),
|
||||||
|
val interfaceProxy: InterfaceProxy = InterfaceProxy(),
|
||||||
|
val packages: Packages = emptyList(),
|
||||||
|
val checkedPackageNames: List<String> = emptyList(),
|
||||||
|
val include: Boolean = true,
|
||||||
|
val isAllApplicationsEnabled: Boolean = false,
|
||||||
|
val loading: Boolean = true,
|
||||||
|
val tunnel: TunnelConfig? = null,
|
||||||
|
val tunnelName: String = ""
|
||||||
|
)
|
||||||
+233
-279
@@ -5,8 +5,6 @@ import android.app.Application
|
|||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
|
||||||
import androidx.compose.runtime.toMutableStateList
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.wireguard.config.Config
|
import com.wireguard.config.Config
|
||||||
@@ -14,387 +12,343 @@ import com.wireguard.config.Interface
|
|||||||
import com.wireguard.config.Peer
|
import com.wireguard.config.Peer
|
||||||
import com.wireguard.crypto.Key
|
import com.wireguard.crypto.Key
|
||||||
import com.wireguard.crypto.KeyPair
|
import com.wireguard.crypto.KeyPair
|
||||||
import com.zaneschepke.wireguardautotunnel.Constants
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.models.InterfaceProxy
|
import com.zaneschepke.wireguardautotunnel.ui.models.InterfaceProxy
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.models.PeerProxy
|
import com.zaneschepke.wireguardautotunnel.ui.models.PeerProxy
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Event
|
||||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||||
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
|
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.removeAt
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.update
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class ConfigViewModel @Inject constructor(private val application : Application,
|
class ConfigViewModel
|
||||||
private val tunnelRepo : TunnelConfigDao,
|
@Inject
|
||||||
private val settingsRepo : SettingsDoa
|
constructor(
|
||||||
|
private val application: Application,
|
||||||
|
private val tunnelConfigRepository: TunnelConfigRepository,
|
||||||
|
private val settingsRepository: SettingsRepository,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _tunnel = MutableStateFlow<TunnelConfig?>(null)
|
|
||||||
private val _tunnelName = MutableStateFlow("")
|
|
||||||
val tunnelName get() = _tunnelName.asStateFlow()
|
|
||||||
val tunnel get() = _tunnel.asStateFlow()
|
|
||||||
|
|
||||||
private var _proxyPeers = MutableStateFlow(mutableStateListOf<PeerProxy>())
|
|
||||||
val proxyPeers get() = _proxyPeers.asStateFlow()
|
|
||||||
|
|
||||||
private var _interface = MutableStateFlow(InterfaceProxy())
|
|
||||||
val interfaceProxy = _interface.asStateFlow()
|
|
||||||
|
|
||||||
private val _packages = MutableStateFlow(emptyList<PackageInfo>())
|
|
||||||
val packages get() = _packages.asStateFlow()
|
|
||||||
private val packageManager = application.packageManager
|
private val packageManager = application.packageManager
|
||||||
|
|
||||||
private val _checkedPackages = MutableStateFlow(mutableStateListOf<String>())
|
private val _uiState = MutableStateFlow(ConfigUiState())
|
||||||
val checkedPackages get() = _checkedPackages.asStateFlow()
|
val uiState = _uiState.asStateFlow()
|
||||||
private val _include = MutableStateFlow(true)
|
|
||||||
val include get() = _include.asStateFlow()
|
|
||||||
|
|
||||||
private val _isAllApplicationsEnabled = MutableStateFlow(false)
|
fun init(tunnelId: String) =
|
||||||
val isAllApplicationsEnabled get() = _isAllApplicationsEnabled.asStateFlow()
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
private val _isDefaultTunnel = MutableStateFlow(false)
|
val packages = getQueriedPackages("")
|
||||||
val isDefaultTunnel = _isDefaultTunnel.asStateFlow()
|
val state =
|
||||||
|
if (tunnelId != Constants.MANUAL_TUNNEL_CONFIG_ID) {
|
||||||
private lateinit var tunnelConfig: TunnelConfig
|
val tunnelConfig =
|
||||||
|
tunnelConfigRepository.getAll().firstOrNull { it.id.toString() == tunnelId }
|
||||||
fun onScreenLoad(id : String) {
|
if (tunnelConfig != null) {
|
||||||
if(id != Constants.MANUAL_TUNNEL_CONFIG_ID) {
|
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
val proxyPeers = config.peers.map { PeerProxy.from(it) }
|
||||||
tunnelConfig = withContext(this.coroutineContext) {
|
val proxyInterface = InterfaceProxy.from(config.`interface`)
|
||||||
getTunnelConfigById(id) ?: throw WgTunnelException("Config not found")
|
var include = true
|
||||||
|
var isAllApplicationsEnabled = false
|
||||||
|
val checkedPackages =
|
||||||
|
if (config.`interface`.includedApplications.isNotEmpty()) {
|
||||||
|
config.`interface`.includedApplications
|
||||||
|
} else if (config.`interface`.excludedApplications.isNotEmpty()) {
|
||||||
|
include = false
|
||||||
|
config.`interface`.excludedApplications
|
||||||
|
} else {
|
||||||
|
isAllApplicationsEnabled = true
|
||||||
|
emptySet()
|
||||||
|
}
|
||||||
|
ConfigUiState(
|
||||||
|
proxyPeers,
|
||||||
|
proxyInterface,
|
||||||
|
packages,
|
||||||
|
checkedPackages.toList(),
|
||||||
|
include,
|
||||||
|
isAllApplicationsEnabled,
|
||||||
|
false,
|
||||||
|
tunnelConfig,
|
||||||
|
tunnelConfig.name,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ConfigUiState(loading = false, packages = packages)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ConfigUiState(loading = false, packages = packages)
|
||||||
}
|
}
|
||||||
emitScreenData()
|
_uiState.value = state
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
emitEmptyScreenData()
|
fun onTunnelNameChange(name: String) {
|
||||||
|
_uiState.value = _uiState.value.copy(tunnelName = name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onIncludeChange(include: Boolean) {
|
||||||
|
_uiState.value = _uiState.value.copy(include = include)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onAddCheckedPackage(packageName: String) {
|
||||||
|
_uiState.value =
|
||||||
|
_uiState.value.copy(
|
||||||
|
checkedPackageNames = _uiState.value.checkedPackageNames + packageName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onAllApplicationsChange(isAllApplicationsEnabled: Boolean) {
|
||||||
|
_uiState.value = _uiState.value.copy(isAllApplicationsEnabled = isAllApplicationsEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onRemoveCheckedPackage(packageName: String) {
|
||||||
|
_uiState.value =
|
||||||
|
_uiState.value.copy(
|
||||||
|
checkedPackageNames = _uiState.value.checkedPackageNames - packageName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getQueriedPackages(query: String): List<PackageInfo> {
|
||||||
|
return getAllInternetCapablePackages().filter {
|
||||||
|
getPackageLabel(it).lowercase().contains(query.lowercase())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun emitEmptyScreenData() {
|
fun getPackageLabel(packageInfo: PackageInfo): String {
|
||||||
tunnelConfig = TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = "")
|
|
||||||
viewModelScope.launch {
|
|
||||||
emitTunnelConfig()
|
|
||||||
emitPeerProxy(PeerProxy())
|
|
||||||
emitInterfaceProxy(InterfaceProxy())
|
|
||||||
emitTunnelConfigName()
|
|
||||||
emitDefaultTunnelStatus()
|
|
||||||
emitQueriedPackages("")
|
|
||||||
emitTunnelAllApplicationsEnabled()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private suspend fun emitScreenData() {
|
|
||||||
emitTunnelConfig()
|
|
||||||
emitPeersFromConfig()
|
|
||||||
emitInterfaceFromConfig()
|
|
||||||
emitTunnelConfigName()
|
|
||||||
emitDefaultTunnelStatus()
|
|
||||||
emitQueriedPackages("")
|
|
||||||
emitCurrentPackageConfigurations()
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun emitDefaultTunnelStatus() {
|
|
||||||
val settings = settingsRepo.getAll()
|
|
||||||
if(settings.isNotEmpty()) {
|
|
||||||
_isDefaultTunnel.value = settings.first().isTunnelConfigDefault(tunnelConfig)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun emitInterfaceFromConfig() {
|
|
||||||
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
|
||||||
_interface.value = InterfaceProxy.from(config.`interface`)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun emitPeersFromConfig() {
|
|
||||||
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
|
||||||
config.peers.forEach{
|
|
||||||
_proxyPeers.value.add(PeerProxy.from(it))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun emitPeerProxy(peerProxy: PeerProxy) {
|
|
||||||
_proxyPeers.value.add(peerProxy)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun emitInterfaceProxy(interfaceProxy: InterfaceProxy) {
|
|
||||||
_interface.value = interfaceProxy
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun getTunnelConfigById(id : String) : TunnelConfig? {
|
|
||||||
return try {
|
|
||||||
tunnelRepo.getById(id.toLong())
|
|
||||||
} catch (_ : Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun emitTunnelConfig() {
|
|
||||||
_tunnel.emit(tunnelConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun emitTunnelConfigName() {
|
|
||||||
_tunnelName.emit(tunnelConfig.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onTunnelNameChange(name : String) {
|
|
||||||
_tunnelName.value = name
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onIncludeChange(include : Boolean) {
|
|
||||||
_include.value = include
|
|
||||||
}
|
|
||||||
fun onAddCheckedPackage(packageName : String) {
|
|
||||||
_checkedPackages.value.add(packageName)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onAllApplicationsChange(isAllApplicationsEnabled : Boolean) {
|
|
||||||
_isAllApplicationsEnabled.value = isAllApplicationsEnabled
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onRemoveCheckedPackage(packageName : String) {
|
|
||||||
_checkedPackages.value.remove(packageName)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun emitSplitTunnelConfiguration(config : Config) {
|
|
||||||
val excludedApps = config.`interface`.excludedApplications
|
|
||||||
val includedApps = config.`interface`.includedApplications
|
|
||||||
if (excludedApps.isNotEmpty() || includedApps.isNotEmpty()) {
|
|
||||||
emitTunnelAllApplicationsDisabled()
|
|
||||||
determineAppInclusionState(excludedApps, includedApps)
|
|
||||||
} else {
|
|
||||||
emitTunnelAllApplicationsEnabled()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun determineAppInclusionState(excludedApps : Set<String>, includedApps : Set<String>) {
|
|
||||||
if (excludedApps.isEmpty()) {
|
|
||||||
emitIncludedAppsExist()
|
|
||||||
emitCheckedApps(includedApps)
|
|
||||||
} else {
|
|
||||||
emitExcludedAppsExist()
|
|
||||||
emitCheckedApps(excludedApps)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun emitIncludedAppsExist() {
|
|
||||||
_include.emit(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun emitExcludedAppsExist() {
|
|
||||||
_include.emit(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun emitCheckedApps(apps : Set<String>) {
|
|
||||||
_checkedPackages.emit(apps.toMutableStateList())
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun emitTunnelAllApplicationsEnabled() {
|
|
||||||
_isAllApplicationsEnabled.emit(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun emitTunnelAllApplicationsDisabled() {
|
|
||||||
_isAllApplicationsEnabled.emit(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun emitCurrentPackageConfigurations() {
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
|
||||||
emitSplitTunnelConfiguration(config)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun emitQueriedPackages(query : String) {
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
val packages = getAllInternetCapablePackages().filter {
|
|
||||||
getPackageLabel(it).lowercase().contains(query.lowercase())
|
|
||||||
}
|
|
||||||
_packages.emit(packages)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getPackageLabel(packageInfo : PackageInfo) : String {
|
|
||||||
return packageInfo.applicationInfo.loadLabel(application.packageManager).toString()
|
return packageInfo.applicationInfo.loadLabel(application.packageManager).toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getAllInternetCapablePackages(): List<PackageInfo> {
|
||||||
private fun getAllInternetCapablePackages() : List<PackageInfo> {
|
|
||||||
return getPackagesHoldingPermissions(arrayOf(Manifest.permission.INTERNET))
|
return getPackagesHoldingPermissions(arrayOf(Manifest.permission.INTERNET))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPackagesHoldingPermissions(permissions: Array<String>): List<PackageInfo> {
|
private fun getPackagesHoldingPermissions(permissions: Array<String>): List<PackageInfo> {
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
packageManager.getPackagesHoldingPermissions(permissions, PackageManager.PackageInfoFlags.of(0L))
|
packageManager.getPackagesHoldingPermissions(
|
||||||
|
permissions,
|
||||||
|
PackageManager.PackageInfoFlags.of(0L),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
packageManager.getPackagesHoldingPermissions(permissions, 0)
|
packageManager.getPackagesHoldingPermissions(permissions, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isAllApplicationsEnabled() : Boolean {
|
private fun isAllApplicationsEnabled(): Boolean {
|
||||||
return _isAllApplicationsEnabled.value
|
return _uiState.value.isAllApplicationsEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isIncludeApplicationsEnabled() : Boolean {
|
private fun saveConfig(tunnelConfig: TunnelConfig) =
|
||||||
return _include.value
|
viewModelScope.launch { tunnelConfigRepository.save(tunnelConfig) }
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun saveConfig(tunnelConfig: TunnelConfig) {
|
private fun updateTunnelConfig(tunnelConfig: TunnelConfig?) =
|
||||||
tunnelRepo.save(tunnelConfig)
|
viewModelScope.launch {
|
||||||
}
|
if (tunnelConfig != null) {
|
||||||
private suspend fun updateTunnelConfig(tunnelConfig: TunnelConfig?) {
|
saveConfig(tunnelConfig).join()
|
||||||
if(tunnelConfig != null) {
|
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
||||||
saveConfig(tunnelConfig)
|
updateSettingsDefaultTunnel(tunnelConfig)
|
||||||
updateSettingsDefaultTunnel(tunnelConfig)
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun updateSettingsDefaultTunnel(tunnelConfig: TunnelConfig) {
|
private suspend fun updateSettingsDefaultTunnel(tunnelConfig: TunnelConfig) {
|
||||||
val settings = settingsRepo.getAll()
|
val settings = settingsRepository.getSettingsFlow().first()
|
||||||
if(settings.isNotEmpty()) {
|
if (settings.defaultTunnel != null) {
|
||||||
val setting = settings[0]
|
if (tunnelConfig.id == TunnelConfig.from(settings.defaultTunnel!!).id) {
|
||||||
if(setting.defaultTunnel != null) {
|
settingsRepository.save(settings.copy(defaultTunnel = tunnelConfig.toString()))
|
||||||
if(tunnelConfig.id == TunnelConfig.from(setting.defaultTunnel!!).id) {
|
|
||||||
settingsRepo.save(setting.copy(
|
|
||||||
defaultTunnel = tunnelConfig.toString()
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun buildPeerListFromProxyPeers() : List<Peer> {
|
private fun buildPeerListFromProxyPeers(): List<Peer> {
|
||||||
return _proxyPeers.value.map {
|
return _uiState.value.proxyPeers.map {
|
||||||
val builder = Peer.Builder()
|
val builder = Peer.Builder()
|
||||||
if (it.allowedIps.isNotEmpty()) builder.parseAllowedIPs(it.allowedIps.trim())
|
if (it.allowedIps.isNotEmpty()) builder.parseAllowedIPs(it.allowedIps.trim())
|
||||||
if (it.publicKey.isNotEmpty()) builder.parsePublicKey(it.publicKey.trim())
|
if (it.publicKey.isNotEmpty()) builder.parsePublicKey(it.publicKey.trim())
|
||||||
if (it.preSharedKey.isNotEmpty()) builder.parsePreSharedKey(it.preSharedKey.trim())
|
if (it.preSharedKey.isNotEmpty()) builder.parsePreSharedKey(it.preSharedKey.trim())
|
||||||
if (it.endpoint.isNotEmpty()) builder.parseEndpoint(it.endpoint.trim())
|
if (it.endpoint.isNotEmpty()) builder.parseEndpoint(it.endpoint.trim())
|
||||||
if (it.persistentKeepalive.isNotEmpty()) builder.parsePersistentKeepalive(it.persistentKeepalive.trim())
|
if (it.persistentKeepalive.isNotEmpty()) {
|
||||||
|
builder.parsePersistentKeepalive(it.persistentKeepalive.trim())
|
||||||
|
}
|
||||||
builder.build()
|
builder.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun buildInterfaceListFromProxyInterface() : Interface {
|
private fun emptyCheckedPackagesList() {
|
||||||
|
_uiState.value = _uiState.value.copy(checkedPackageNames = emptyList())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildInterfaceListFromProxyInterface(): Interface {
|
||||||
val builder = Interface.Builder()
|
val builder = Interface.Builder()
|
||||||
builder.parsePrivateKey(_interface.value.privateKey.trim())
|
builder.parsePrivateKey(_uiState.value.interfaceProxy.privateKey.trim())
|
||||||
builder.parseAddresses(_interface.value.addresses.trim())
|
builder.parseAddresses(_uiState.value.interfaceProxy.addresses.trim())
|
||||||
builder.parseDnsServers(_interface.value.dnsServers.trim())
|
if (_uiState.value.interfaceProxy.dnsServers.isNotEmpty()) {
|
||||||
if(_interface.value.mtu.isNotEmpty()) builder.parseMtu(_interface.value.mtu.trim())
|
builder.parseDnsServers(_uiState.value.interfaceProxy.dnsServers.trim())
|
||||||
if(_interface.value.listenPort.isNotEmpty()) builder.parseListenPort(_interface.value.listenPort.trim())
|
}
|
||||||
if(isAllApplicationsEnabled()) _checkedPackages.value.clear()
|
if (_uiState.value.interfaceProxy.mtu.isNotEmpty())
|
||||||
if(_include.value) builder.includeApplications(_checkedPackages.value)
|
builder.parseMtu(_uiState.value.interfaceProxy.mtu.trim())
|
||||||
if(!_include.value) builder.excludeApplications(_checkedPackages.value)
|
if (_uiState.value.interfaceProxy.listenPort.isNotEmpty()) {
|
||||||
|
builder.parseListenPort(_uiState.value.interfaceProxy.listenPort.trim())
|
||||||
|
}
|
||||||
|
if (isAllApplicationsEnabled()) emptyCheckedPackagesList()
|
||||||
|
if (_uiState.value.include) builder.includeApplications(_uiState.value.checkedPackageNames)
|
||||||
|
if (!_uiState.value.include) builder.excludeApplications(_uiState.value.checkedPackageNames)
|
||||||
return builder.build()
|
return builder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onSaveAllChanges(): Result<Event> {
|
||||||
|
return try {
|
||||||
suspend fun onSaveAllChanges() {
|
|
||||||
try {
|
|
||||||
val peerList = buildPeerListFromProxyPeers()
|
val peerList = buildPeerListFromProxyPeers()
|
||||||
val wgInterface = buildInterfaceListFromProxyInterface()
|
val wgInterface = buildInterfaceListFromProxyInterface()
|
||||||
val config = Config.Builder().addPeers(peerList).setInterface(wgInterface).build()
|
val config = Config.Builder().addPeers(peerList).setInterface(wgInterface).build()
|
||||||
val tunnelConfig = _tunnel.value?.copy(
|
val tunnelConfig = when(uiState.value.tunnel) {
|
||||||
name = _tunnelName.value,
|
null -> TunnelConfig(name = _uiState.value.tunnelName, wgQuick = config.toWgQuickString())
|
||||||
wgQuick = config.toWgQuickString()
|
else -> uiState.value.tunnel!!.copy(
|
||||||
)
|
name = _uiState.value.tunnelName,
|
||||||
|
wgQuick = config.toWgQuickString(),
|
||||||
|
)
|
||||||
|
}
|
||||||
updateTunnelConfig(tunnelConfig)
|
updateTunnelConfig(tunnelConfig)
|
||||||
} catch (e : Exception) {
|
Result.Success(Event.Message.ConfigSaved)
|
||||||
throw WgTunnelException("Error: ${e.cause?.message?.lowercase() ?: "unknown error occurred"}")
|
} catch (e: Exception) {
|
||||||
|
Result.Error(Event.Error.Exception(e))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onPeerPublicKeyChange(index: Int, publicKey: String) {
|
fun onPeerPublicKeyChange(index: Int, value: String) {
|
||||||
_proxyPeers.value[index] = _proxyPeers.value[index].copy(
|
_uiState.value =
|
||||||
publicKey = publicKey
|
_uiState.value.copy(
|
||||||
)
|
proxyPeers =
|
||||||
|
_uiState.value.proxyPeers.update(
|
||||||
|
index,
|
||||||
|
_uiState.value.proxyPeers[index].copy(publicKey = value),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onPreSharedKeyChange(index: Int, value: String) {
|
fun onPreSharedKeyChange(index: Int, value: String) {
|
||||||
_proxyPeers.value[index] = _proxyPeers.value[index].copy(
|
_uiState.value =
|
||||||
preSharedKey = value
|
_uiState.value.copy(
|
||||||
)
|
proxyPeers =
|
||||||
|
_uiState.value.proxyPeers.update(
|
||||||
|
index,
|
||||||
|
_uiState.value.proxyPeers[index].copy(preSharedKey = value),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onEndpointChange(index: Int, value: String) {
|
fun onEndpointChange(index: Int, value: String) {
|
||||||
_proxyPeers.value[index] = _proxyPeers.value[index].copy(
|
_uiState.value =
|
||||||
endpoint = value
|
_uiState.value.copy(
|
||||||
)
|
proxyPeers =
|
||||||
|
_uiState.value.proxyPeers.update(
|
||||||
|
index,
|
||||||
|
_uiState.value.proxyPeers[index].copy(endpoint = value),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onAllowedIpsChange(index: Int, value: String) {
|
fun onAllowedIpsChange(index: Int, value: String) {
|
||||||
_proxyPeers.value[index] = _proxyPeers.value[index].copy(
|
_uiState.value =
|
||||||
allowedIps = value
|
_uiState.value.copy(
|
||||||
)
|
proxyPeers =
|
||||||
|
_uiState.value.proxyPeers.update(
|
||||||
|
index,
|
||||||
|
_uiState.value.proxyPeers[index].copy(allowedIps = value),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onPersistentKeepaliveChanged(index : Int, value : String) {
|
fun onPersistentKeepaliveChanged(index: Int, value: String) {
|
||||||
_proxyPeers.value[index] = _proxyPeers.value[index].copy(
|
_uiState.value =
|
||||||
persistentKeepalive = value
|
_uiState.value.copy(
|
||||||
)
|
proxyPeers =
|
||||||
|
_uiState.value.proxyPeers.update(
|
||||||
|
index,
|
||||||
|
_uiState.value.proxyPeers[index].copy(persistentKeepalive = value),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onDeletePeer(index: Int) {
|
fun onDeletePeer(index: Int) {
|
||||||
proxyPeers.value.removeAt(index)
|
_uiState.value =
|
||||||
|
_uiState.value.copy(
|
||||||
|
proxyPeers = _uiState.value.proxyPeers.removeAt(index),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addEmptyPeer() {
|
fun addEmptyPeer() {
|
||||||
_proxyPeers.value.add(PeerProxy())
|
_uiState.value = _uiState.value.copy(proxyPeers = _uiState.value.proxyPeers + PeerProxy())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun generateKeyPair() {
|
fun generateKeyPair() {
|
||||||
val keyPair = KeyPair()
|
val keyPair = KeyPair()
|
||||||
_interface.value = _interface.value.copy(
|
_uiState.value =
|
||||||
privateKey = keyPair.privateKey.toBase64(),
|
_uiState.value.copy(
|
||||||
publicKey = keyPair.publicKey.toBase64()
|
interfaceProxy =
|
||||||
)
|
_uiState.value.interfaceProxy.copy(
|
||||||
|
privateKey = keyPair.privateKey.toBase64(),
|
||||||
|
publicKey = keyPair.publicKey.toBase64(),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onAddressesChanged(value: String) {
|
fun onAddressesChanged(value: String) {
|
||||||
_interface.value = _interface.value.copy(
|
_uiState.value =
|
||||||
addresses = value
|
_uiState.value.copy(
|
||||||
)
|
interfaceProxy = _uiState.value.interfaceProxy.copy(addresses = value)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onListenPortChanged(value: String) {
|
fun onListenPortChanged(value: String) {
|
||||||
_interface.value = _interface.value.copy(
|
_uiState.value =
|
||||||
listenPort = value
|
_uiState.value.copy(
|
||||||
)
|
interfaceProxy = _uiState.value.interfaceProxy.copy(listenPort = value)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onDnsServersChanged(value: String) {
|
fun onDnsServersChanged(value: String) {
|
||||||
_interface.value = _interface.value.copy(
|
_uiState.value =
|
||||||
dnsServers = value
|
_uiState.value.copy(
|
||||||
)
|
interfaceProxy = _uiState.value.interfaceProxy.copy(dnsServers = value)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onMtuChanged(value: String) {
|
fun onMtuChanged(value: String) {
|
||||||
_interface.value = _interface.value.copy(
|
_uiState.value =
|
||||||
mtu = value
|
_uiState.value.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(mtu = value))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onInterfacePublicKeyChange(value : String) {
|
private fun onInterfacePublicKeyChange(value: String) {
|
||||||
_interface.value = _interface.value.copy(
|
_uiState.value =
|
||||||
publicKey = value
|
_uiState.value.copy(
|
||||||
)
|
interfaceProxy = _uiState.value.interfaceProxy.copy(publicKey = value)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onPrivateKeyChange(value: String) {
|
fun onPrivateKeyChange(value: String) {
|
||||||
_interface.value = _interface.value.copy(
|
_uiState.value =
|
||||||
privateKey = value
|
_uiState.value.copy(
|
||||||
)
|
interfaceProxy = _uiState.value.interfaceProxy.copy(privateKey = value)
|
||||||
if(NumberUtils.isValidKey(value)) {
|
)
|
||||||
|
if (NumberUtils.isValidKey(value)) {
|
||||||
val pair = KeyPair(Key.fromBase64(value))
|
val pair = KeyPair(Key.fromBase64(value))
|
||||||
onInterfacePublicKeyChange(pair.publicKey.toBase64())
|
onInterfacePublicKeyChange(pair.publicKey.toBase64())
|
||||||
} else {
|
} else {
|
||||||
onInterfacePublicKeyChange("")
|
onInterfacePublicKeyChange("")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
fun emitQueriedPackages(query: String) {
|
||||||
|
val packages =
|
||||||
|
getAllInternetCapablePackages().filter {
|
||||||
|
getPackageLabel(it).lowercase().contains(query.lowercase())
|
||||||
|
}
|
||||||
|
_uiState.value = _uiState.value.copy(packages = packages)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
-163
@@ -1,163 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.detail
|
|
||||||
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.focusGroup
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
|
||||||
import androidx.compose.ui.focus.focusRequester
|
|
||||||
import androidx.compose.ui.platform.ClipboardManager
|
|
||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
|
||||||
import java.time.Duration
|
|
||||||
import java.time.Instant
|
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
|
||||||
@Composable
|
|
||||||
fun DetailScreen(
|
|
||||||
viewModel: DetailViewModel = hiltViewModel(),
|
|
||||||
focusRequester: FocusRequester,
|
|
||||||
padding: PaddingValues,
|
|
||||||
id : String
|
|
||||||
) {
|
|
||||||
|
|
||||||
val context = LocalContext.current
|
|
||||||
val clipboardManager: ClipboardManager = LocalClipboardManager.current
|
|
||||||
val tunnelStats by viewModel.tunnelStats.collectAsStateWithLifecycle(null)
|
|
||||||
val tunnel by viewModel.tunnel.collectAsStateWithLifecycle(null)
|
|
||||||
val tunnelName by viewModel.tunnelName.collectAsStateWithLifecycle()
|
|
||||||
val lastHandshake by viewModel.lastHandshake.collectAsStateWithLifecycle(emptyMap())
|
|
||||||
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
viewModel.emitConfig(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
if(null != tunnel) {
|
|
||||||
val interfaceKey = tunnel?.`interface`?.keyPair?.publicKey?.toBase64().toString()
|
|
||||||
val addresses = tunnel?.`interface`?.addresses!!.joinToString()
|
|
||||||
val dnsServers = tunnel?.`interface`?.dnsServers!!.joinToString()
|
|
||||||
val optionalMtu = tunnel?.`interface`?.mtu
|
|
||||||
val mtu = if(optionalMtu?.isPresent == true) optionalMtu.get().toString() else stringResource(
|
|
||||||
id = R.string.none
|
|
||||||
)
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.Start,
|
|
||||||
verticalArrangement = Arrangement.Top,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.fillMaxHeight(if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) 4/5f else 1f)
|
|
||||||
.verticalScroll(rememberScrollState())
|
|
||||||
.focusRequester(focusRequester)
|
|
||||||
.padding(padding)
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 20.dp, vertical = 7.dp).focusGroup(),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
|
||||||
) {
|
|
||||||
Column(modifier = Modifier.weight(1f, true)) {
|
|
||||||
Text(stringResource(R.string.config_interface), fontWeight = FontWeight.Bold, fontSize = 20.sp)
|
|
||||||
Text(stringResource(R.string.name), fontStyle = FontStyle.Italic)
|
|
||||||
Text(text = tunnelName, modifier = Modifier.clickable {
|
|
||||||
clipboardManager.setText(AnnotatedString(tunnelName))
|
|
||||||
})
|
|
||||||
Text(stringResource(R.string.public_key), fontStyle = FontStyle.Italic)
|
|
||||||
Text(text = interfaceKey, modifier = Modifier.clickable {
|
|
||||||
clipboardManager.setText(AnnotatedString(interfaceKey))
|
|
||||||
})
|
|
||||||
Text(stringResource(R.string.addresses), fontStyle = FontStyle.Italic)
|
|
||||||
Text(text = addresses, modifier = Modifier.clickable {
|
|
||||||
clipboardManager.setText(AnnotatedString(addresses))
|
|
||||||
})
|
|
||||||
Text(stringResource(R.string.dns_servers), fontStyle = FontStyle.Italic)
|
|
||||||
Text(text = dnsServers, modifier = Modifier.clickable {
|
|
||||||
clipboardManager.setText(AnnotatedString(dnsServers))
|
|
||||||
})
|
|
||||||
Text(stringResource(R.string.mtu), fontStyle = FontStyle.Italic)
|
|
||||||
Text(text = mtu, modifier = Modifier.clickable {
|
|
||||||
clipboardManager.setText(AnnotatedString(mtu))
|
|
||||||
})
|
|
||||||
Box(modifier = Modifier.padding(10.dp))
|
|
||||||
tunnel?.peers?.forEach{
|
|
||||||
val peerKey = it.publicKey.toBase64().toString()
|
|
||||||
val allowedIps = it.allowedIps.joinToString()
|
|
||||||
val endpoint = if(it.endpoint.isPresent) it.endpoint.get().toString() else stringResource(
|
|
||||||
id = R.string.none
|
|
||||||
)
|
|
||||||
Text(stringResource(R.string.peer), fontWeight = FontWeight.Bold, fontSize = 20.sp)
|
|
||||||
Text(stringResource(R.string.public_key), fontStyle = FontStyle.Italic)
|
|
||||||
Text(text = peerKey, modifier = Modifier.clickable {
|
|
||||||
clipboardManager.setText(AnnotatedString(peerKey))
|
|
||||||
})
|
|
||||||
Text(stringResource(id = R.string.allowed_ips), fontStyle = FontStyle.Italic)
|
|
||||||
Text(text = allowedIps, modifier = Modifier.clickable {
|
|
||||||
clipboardManager.setText(AnnotatedString(allowedIps))
|
|
||||||
})
|
|
||||||
Text(stringResource(R.string.endpoint), fontStyle = FontStyle.Italic)
|
|
||||||
Text(text = endpoint, modifier = Modifier.clickable {
|
|
||||||
clipboardManager.setText(AnnotatedString(endpoint))
|
|
||||||
})
|
|
||||||
if (tunnelStats != null) {
|
|
||||||
val totalRx = tunnelStats?.totalRx() ?: 0
|
|
||||||
val totalTx = tunnelStats?.totalTx() ?: 0
|
|
||||||
if((totalRx + totalTx != 0L)) {
|
|
||||||
val rxKB = NumberUtils.bytesToKB(tunnelStats!!.totalRx())
|
|
||||||
val txKB = NumberUtils.bytesToKB(tunnelStats!!.totalTx())
|
|
||||||
Text(stringResource(R.string.transfer), fontStyle = FontStyle.Italic)
|
|
||||||
val transfer = "rx: ${NumberUtils.formatDecimalTwoPlaces(rxKB)} KB tx: ${NumberUtils.formatDecimalTwoPlaces(txKB)} KB"
|
|
||||||
Text(transfer, modifier = Modifier.clickable {
|
|
||||||
clipboardManager.setText(AnnotatedString(transfer))})
|
|
||||||
Text(stringResource(R.string.last_handshake), fontStyle = FontStyle.Italic)
|
|
||||||
val handshakeEpoch = lastHandshake[it.publicKey]
|
|
||||||
if(handshakeEpoch != null) {
|
|
||||||
if(handshakeEpoch == 0L) {
|
|
||||||
Text(stringResource(id = R.string.never), modifier = Modifier.clickable {
|
|
||||||
clipboardManager.setText(AnnotatedString(context.getString(R.string.never)))
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
val time = Instant.ofEpochMilli(handshakeEpoch)
|
|
||||||
val duration = "${Duration.between(time, Instant.now()).seconds} seconds ago"
|
|
||||||
Text(duration, modifier = Modifier.clickable {
|
|
||||||
clipboardManager.setText(AnnotatedString(duration))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-46
@@ -1,46 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.detail
|
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import com.wireguard.config.Config
|
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
|
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
|
||||||
class DetailViewModel @Inject constructor(private val tunnelRepo : TunnelConfigDao, private val vpnService : VpnService
|
|
||||||
) : ViewModel() {
|
|
||||||
|
|
||||||
private val _tunnel = MutableStateFlow<Config?>(null)
|
|
||||||
val tunnel get() = _tunnel.asStateFlow()
|
|
||||||
|
|
||||||
private val _tunnelName = MutableStateFlow("")
|
|
||||||
val tunnelName = _tunnelName.asStateFlow()
|
|
||||||
val tunnelStats get() = vpnService.statistics
|
|
||||||
val lastHandshake get() = vpnService.lastHandshake
|
|
||||||
|
|
||||||
private suspend fun getTunnelConfigById(id: String): TunnelConfig? {
|
|
||||||
return try {
|
|
||||||
tunnelRepo.getById(id.toLong())
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e.message)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fun emitConfig(id: String) {
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
val tunnelConfig = getTunnelConfigById(id)
|
|
||||||
if(tunnelConfig != null) {
|
|
||||||
_tunnelName.emit(tunnelConfig.name)
|
|
||||||
_tunnel.emit(TunnelConfig.configFromQuick(tunnelConfig.wgQuick))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+501
-275
@@ -1,55 +1,68 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.main
|
package com.zaneschepke.wireguardautotunnel.ui.screens.main
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.animation.slideInVertically
|
import androidx.compose.animation.slideInVertically
|
||||||
import androidx.compose.animation.slideOutVertically
|
import androidx.compose.animation.slideOutVertically
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.focusable
|
import androidx.compose.foundation.focusable
|
||||||
|
import androidx.compose.foundation.gestures.ScrollableDefaults
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.requiredWidth
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.overscroll
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Create
|
import androidx.compose.material.icons.filled.Create
|
||||||
import androidx.compose.material.icons.filled.FileOpen
|
import androidx.compose.material.icons.filled.FileOpen
|
||||||
import androidx.compose.material.icons.filled.QrCode
|
import androidx.compose.material.icons.filled.QrCode
|
||||||
import androidx.compose.material.icons.rounded.Add
|
import androidx.compose.material.icons.rounded.Add
|
||||||
|
import androidx.compose.material.icons.rounded.Bolt
|
||||||
import androidx.compose.material.icons.rounded.Circle
|
import androidx.compose.material.icons.rounded.Circle
|
||||||
import androidx.compose.material.icons.rounded.Delete
|
import androidx.compose.material.icons.rounded.Delete
|
||||||
import androidx.compose.material.icons.rounded.Edit
|
import androidx.compose.material.icons.rounded.Edit
|
||||||
import androidx.compose.material.icons.rounded.Info
|
import androidx.compose.material.icons.rounded.Info
|
||||||
import androidx.compose.material.icons.rounded.Star
|
import androidx.compose.material.icons.rounded.Star
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Divider
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.FabPosition
|
import androidx.compose.material3.FabPosition
|
||||||
import androidx.compose.material3.FloatingActionButton
|
import androidx.compose.material3.FloatingActionButton
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.MaterialTheme.typography
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.material3.surfaceColorAtElevation
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -61,16 +74,14 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
@@ -78,145 +89,250 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import com.journeyapps.barcodescanner.ScanContract
|
import com.journeyapps.barcodescanner.ScanContract
|
||||||
import com.journeyapps.barcodescanner.ScanOptions
|
import com.journeyapps.barcodescanner.ScanOptions
|
||||||
|
import com.wireguard.android.backend.GoBackend
|
||||||
import com.wireguard.android.backend.Tunnel
|
import com.wireguard.android.backend.Tunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.Constants
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.CaptureActivityPortrait
|
import com.zaneschepke.wireguardautotunnel.ui.CaptureActivityPortrait
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.Routes
|
import com.zaneschepke.wireguardautotunnel.ui.Screen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem
|
import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.brickRed
|
import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.theme.corn
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.mint
|
import com.zaneschepke.wireguardautotunnel.ui.theme.mint
|
||||||
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Event
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.handshakeStatus
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.mapPeerStats
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MainScreen(
|
fun MainScreen(
|
||||||
viewModel: MainViewModel = hiltViewModel(),
|
viewModel: MainViewModel = hiltViewModel(),
|
||||||
padding: PaddingValues,
|
focusRequester: FocusRequester,
|
||||||
showSnackbarMessage: (String) -> Unit,
|
showSnackbarMessage: (String) -> Unit,
|
||||||
navController: NavController
|
navController: NavController
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val haptic = LocalHapticFeedback.current
|
val haptic = LocalHapticFeedback.current
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val isVisible = rememberSaveable { mutableStateOf(true) }
|
val isVisible = rememberSaveable { mutableStateOf(true) }
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope { Dispatchers.IO }
|
||||||
|
|
||||||
val sheetState = rememberModalBottomSheetState()
|
val sheetState = rememberModalBottomSheetState()
|
||||||
var showBottomSheet by remember { mutableStateOf(false) }
|
var showBottomSheet by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
var showPrimaryChangeAlertDialog by remember { mutableStateOf(false) }
|
var showPrimaryChangeAlertDialog by remember { mutableStateOf(false) }
|
||||||
val tunnels by viewModel.tunnels.collectAsStateWithLifecycle(mutableListOf())
|
var showDeleteTunnelAlertDialog by remember { mutableStateOf(false) }
|
||||||
val handshakeStatus by viewModel.handshakeStatus.collectAsStateWithLifecycle(HandshakeStatus.NOT_STARTED)
|
|
||||||
var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) }
|
var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) }
|
||||||
val state by viewModel.state.collectAsStateWithLifecycle(Tunnel.State.DOWN)
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
val tunnelName by viewModel.tunnelName.collectAsStateWithLifecycle("")
|
|
||||||
val settings by viewModel.settings.collectAsStateWithLifecycle()
|
|
||||||
|
|
||||||
// Nested scroll for control FAB
|
var vpnIntent by remember { mutableStateOf(GoBackend.VpnService.prepare(WireGuardAutoTunnel.instance)) }
|
||||||
val nestedScrollConnection = remember {
|
val vpnActivityResultState =
|
||||||
object : NestedScrollConnection {
|
rememberLauncherForActivityResult(
|
||||||
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
ActivityResultContracts.StartActivityForResult(),
|
||||||
// Hide FAB
|
onResult = {
|
||||||
if (available.y < -1) {
|
val accepted = (it.resultCode == AppCompatActivity.RESULT_OK)
|
||||||
isVisible.value = false
|
if (accepted) {
|
||||||
|
vpnIntent = null
|
||||||
}
|
}
|
||||||
// Show FAB
|
|
||||||
if (available.y > 1) {
|
|
||||||
isVisible.value = true
|
|
||||||
}
|
|
||||||
return Offset.Zero
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val tunnelFileImportResultLauncher = rememberLauncherForActivityResult(object : ActivityResultContracts.GetContent() {
|
|
||||||
override fun createIntent(context: Context, input: String): Intent {
|
|
||||||
val intent = super.createIntent(context, input)
|
|
||||||
|
|
||||||
/* AndroidTV now comes with stubs that do nothing but display a Toast less helpful than
|
|
||||||
* what we can do, so detect this and throw an exception that we can catch later. */
|
|
||||||
val activitiesToResolveIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
context.packageManager.queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong()))
|
|
||||||
} else {
|
|
||||||
context.packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
|
|
||||||
}
|
|
||||||
if (activitiesToResolveIntent.all {
|
|
||||||
val name = it.activityInfo.packageName
|
|
||||||
name.startsWith(Constants.GOOGLE_TV_EXPLORER_STUB) || name.startsWith(Constants.ANDROID_TV_EXPLORER_STUB)
|
|
||||||
}) {
|
|
||||||
throw WgTunnelException("No file explorer installed")
|
|
||||||
}
|
|
||||||
return intent
|
|
||||||
}
|
|
||||||
}) { data ->
|
|
||||||
if (data == null) return@rememberLauncherForActivityResult
|
|
||||||
scope.launch(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
viewModel.onTunnelFileSelected(data)
|
|
||||||
} catch (e : Exception) {
|
|
||||||
showSnackbarMessage(e.message ?: "Unknown error occurred")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val scanLauncher = rememberLauncherForActivityResult(
|
|
||||||
contract = ScanContract(),
|
|
||||||
onResult = {
|
|
||||||
try {
|
|
||||||
viewModel.onTunnelQrResult(it.contents)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
showSnackbarMessage(context.getString(R.string.qr_result_failed))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if(showPrimaryChangeAlertDialog) {
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = {
|
|
||||||
showPrimaryChangeAlertDialog = false
|
|
||||||
},
|
},
|
||||||
confirmButton = {
|
)
|
||||||
TextButton(onClick = {
|
LaunchedEffect(uiState.loading) {
|
||||||
|
if (!uiState.loading && WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
|
delay(Constants.FOCUS_REQUEST_DELAY)
|
||||||
|
focusRequester.requestFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uiState.loading) {
|
||||||
|
LoadingScreen()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val tunnelFileImportResultLauncher =
|
||||||
|
rememberLauncherForActivityResult(
|
||||||
|
object : ActivityResultContracts.GetContent() {
|
||||||
|
override fun createIntent(context: Context, input: String): Intent {
|
||||||
|
val intent = super.createIntent(context, input)
|
||||||
|
|
||||||
|
/* AndroidTV now comes with stubs that do nothing but display a Toast less helpful than
|
||||||
|
* what we can do, so detect this and throw an exception that we can catch later. */
|
||||||
|
val activitiesToResolveIntent =
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
context.packageManager.queryIntentActivities(
|
||||||
|
intent,
|
||||||
|
PackageManager.ResolveInfoFlags.of(
|
||||||
|
PackageManager.MATCH_DEFAULT_ONLY.toLong(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
context.packageManager.queryIntentActivities(
|
||||||
|
intent,
|
||||||
|
PackageManager.MATCH_DEFAULT_ONLY,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
activitiesToResolveIntent.all {
|
||||||
|
val name = it.activityInfo.packageName
|
||||||
|
name.startsWith(Constants.GOOGLE_TV_EXPLORER_STUB) ||
|
||||||
|
name.startsWith(Constants.ANDROID_TV_EXPLORER_STUB)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
showSnackbarMessage(Event.Error.FileExplorerRequired.message)
|
||||||
|
}
|
||||||
|
return intent
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) { data ->
|
||||||
|
if (data == null) return@rememberLauncherForActivityResult
|
||||||
|
scope.launch {
|
||||||
|
viewModel.onTunnelFileSelected(data).let {
|
||||||
|
when (it) {
|
||||||
|
is Result.Error -> showSnackbarMessage(it.error.message)
|
||||||
|
is Result.Success -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val scanLauncher =
|
||||||
|
rememberLauncherForActivityResult(
|
||||||
|
contract = ScanContract(),
|
||||||
|
onResult = {
|
||||||
|
if (it.contents != null) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
viewModel.onTunnelQrResult(it.contents).let { result ->
|
||||||
|
when (result) {
|
||||||
|
is Result.Success -> {}
|
||||||
|
is Result.Error -> showSnackbarMessage(result.error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
AnimatedVisibility(showPrimaryChangeAlertDialog) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showPrimaryChangeAlertDialog = false },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
viewModel.onDefaultTunnelChange(selectedTunnel)
|
viewModel.onDefaultTunnelChange(selectedTunnel)
|
||||||
showPrimaryChangeAlertDialog = false
|
showPrimaryChangeAlertDialog = false
|
||||||
selectedTunnel = null
|
selectedTunnel = null
|
||||||
}
|
},
|
||||||
})
|
) {
|
||||||
{ Text(text = stringResource(R.string.okay)) }
|
Text(text = stringResource(R.string.okay))
|
||||||
|
}
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
TextButton(onClick = {
|
TextButton(onClick = { showPrimaryChangeAlertDialog = false }) {
|
||||||
showPrimaryChangeAlertDialog = false
|
Text(text = stringResource(R.string.cancel))
|
||||||
})
|
}
|
||||||
{ Text(text = stringResource(R.string.cancel)) }
|
|
||||||
},
|
},
|
||||||
title = { Text(text = stringResource(R.string.primary_tunnel_change)) },
|
title = { Text(text = stringResource(R.string.primary_tunnel_change)) },
|
||||||
text = { Text(text = stringResource(R.string.primary_tunnnel_change_question)) }
|
text = { Text(text = stringResource(R.string.primary_tunnel_change_question)) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onTunnelToggle(checked : Boolean , tunnel : TunnelConfig) {
|
AnimatedVisibility(showDeleteTunnelAlertDialog) {
|
||||||
try {
|
AlertDialog(
|
||||||
if (checked) viewModel.onTunnelStart(tunnel) else viewModel.onTunnelStop()
|
onDismissRequest = { showDeleteTunnelAlertDialog = false },
|
||||||
} catch (e : Exception) {
|
confirmButton = {
|
||||||
showSnackbarMessage(e.message!!)
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
selectedTunnel?.let { viewModel.onDelete(it) }
|
||||||
|
showDeleteTunnelAlertDialog = false
|
||||||
|
selectedTunnel = null
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.yes))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { showDeleteTunnelAlertDialog = false }) {
|
||||||
|
Text(text = stringResource(R.string.cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title = { Text(text = stringResource(R.string.delete_tunnel)) },
|
||||||
|
text = { Text(text = stringResource(R.string.delete_tunnel_message)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) {
|
||||||
|
if (vpnIntent != null) {
|
||||||
|
return vpnActivityResultState.launch(vpnIntent)
|
||||||
}
|
}
|
||||||
|
if (checked) viewModel.onTunnelStart(tunnel) else viewModel.onTunnelStop()
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier.pointerInput(Unit) {
|
modifier =
|
||||||
detectTapGestures(onTap = {
|
Modifier.pointerInput(Unit) {
|
||||||
selectedTunnel = null
|
detectTapGestures(
|
||||||
})
|
onTap = {
|
||||||
},
|
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) selectedTunnel = null
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
floatingActionButtonPosition = FabPosition.End,
|
floatingActionButtonPosition = FabPosition.End,
|
||||||
|
topBar = {
|
||||||
|
if (uiState.settings.isAutoTunnelEnabled)
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.requiredWidth(LocalConfiguration.current.screenWidthDp.dp)
|
||||||
|
.padding(end = 5.dp)
|
||||||
|
) {
|
||||||
|
Row {
|
||||||
|
Icon(
|
||||||
|
Icons.Rounded.Bolt,
|
||||||
|
stringResource(id = R.string.auto),
|
||||||
|
modifier = Modifier.size(25.dp),
|
||||||
|
tint =
|
||||||
|
if (uiState.settings.isAutoTunnelPaused) Color.Gray
|
||||||
|
else mint,
|
||||||
|
)
|
||||||
|
val autoTunnelingLabel = buildAnnotatedString {
|
||||||
|
append(stringResource(id = R.string.auto_tunneling))
|
||||||
|
append(": ")
|
||||||
|
if(uiState.settings.isAutoTunnelPaused) append(stringResource(id = R.string.paused)) else append(
|
||||||
|
stringResource(id = R.string.active),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
autoTunnelingLabel.text,
|
||||||
|
style = typography.bodyLarge,
|
||||||
|
modifier = Modifier.padding(start = 10.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (uiState.settings.isAutoTunnelPaused)
|
||||||
|
TextButton(
|
||||||
|
onClick = { viewModel.resumeAutoTunneling() },
|
||||||
|
modifier = Modifier.padding(end = 10.dp),
|
||||||
|
) {
|
||||||
|
Text(stringResource(id = R.string.resume))
|
||||||
|
}
|
||||||
|
else
|
||||||
|
TextButton(
|
||||||
|
onClick = { viewModel.pauseAutoTunneling() },
|
||||||
|
modifier = Modifier.padding(end = 10.dp),
|
||||||
|
) {
|
||||||
|
Text(stringResource(id = R.string.pause))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = isVisible.value,
|
visible = isVisible.value,
|
||||||
@@ -227,14 +343,20 @@ fun MainScreen(
|
|||||||
val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
|
val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
|
||||||
var fobColor by remember { mutableStateOf(secondaryColor) }
|
var fobColor by remember { mutableStateOf(secondaryColor) }
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
modifier = Modifier.padding(bottom = 90.dp).onFocusChanged {
|
modifier =
|
||||||
if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
(if (
|
||||||
fobColor = if (it.isFocused) hoverColor else secondaryColor }
|
WireGuardAutoTunnel.isRunningOnAndroidTv() &&
|
||||||
}
|
uiState.tunnels.isEmpty()
|
||||||
,
|
)
|
||||||
onClick = {
|
Modifier.focusRequester(focusRequester)
|
||||||
showBottomSheet = true
|
else Modifier)
|
||||||
},
|
.padding(bottom = 90.dp)
|
||||||
|
.onFocusChanged {
|
||||||
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
|
fobColor = if (it.isFocused) hoverColor else secondaryColor
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onClick = { showBottomSheet = true },
|
||||||
containerColor = fobColor,
|
containerColor = fobColor,
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
) {
|
) {
|
||||||
@@ -245,247 +367,351 @@ fun MainScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
) {
|
) { innerPadding ->
|
||||||
if (tunnels.isEmpty()) {
|
AnimatedVisibility(uiState.tunnels.isEmpty(), exit = fadeOut(), enter = fadeIn()) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(padding)
|
.padding(innerPadding),
|
||||||
) {
|
) {
|
||||||
Text(text = stringResource(R.string.no_tunnels), fontStyle = FontStyle.Italic)
|
Text(text = stringResource(R.string.no_tunnels), fontStyle = FontStyle.Italic)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (showBottomSheet) {
|
if (showBottomSheet) {
|
||||||
ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
onDismissRequest = {
|
onDismissRequest = { showBottomSheet = false },
|
||||||
showBottomSheet = false
|
sheetState = sheetState,
|
||||||
},
|
|
||||||
sheetState = sheetState
|
|
||||||
) {
|
) {
|
||||||
// Sheet content
|
// Sheet content
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier =
|
||||||
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable {
|
.clickable {
|
||||||
showBottomSheet = false
|
showBottomSheet = false
|
||||||
try {
|
tunnelFileImportResultLauncher.launch(Constants.ALLOWED_FILE_TYPES)
|
||||||
tunnelFileImportResultLauncher.launch(Constants.ALLOWED_FILE_TYPES)
|
|
||||||
} catch (e : Exception) {
|
|
||||||
showSnackbarMessage(e.message!!)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.padding(10.dp)
|
.padding(10.dp),
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Filled.FileOpen,
|
Icons.Filled.FileOpen,
|
||||||
contentDescription = stringResource(id = R.string.open_file),
|
contentDescription = stringResource(id = R.string.open_file),
|
||||||
modifier = Modifier.padding(10.dp)
|
modifier = Modifier.padding(10.dp),
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
stringResource(id = R.string.add_tunnels_text),
|
stringResource(id = R.string.add_tunnels_text),
|
||||||
modifier = Modifier.padding(10.dp)
|
modifier = Modifier.padding(10.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if(!WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
Divider()
|
HorizontalDivider()
|
||||||
Row(modifier = Modifier
|
Row(
|
||||||
.fillMaxWidth()
|
modifier =
|
||||||
.clickable {
|
Modifier
|
||||||
scope.launch {
|
.fillMaxWidth()
|
||||||
showBottomSheet = false
|
.clickable {
|
||||||
val scanOptions = ScanOptions()
|
scope.launch {
|
||||||
scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE)
|
showBottomSheet = false
|
||||||
scanOptions.setOrientationLocked(true)
|
val scanOptions = ScanOptions()
|
||||||
scanOptions.setPrompt(context.getString(R.string.scanning_qr))
|
scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE)
|
||||||
scanOptions.setBeepEnabled(false)
|
scanOptions.setOrientationLocked(true)
|
||||||
scanOptions.captureActivity = CaptureActivityPortrait::class.java
|
scanOptions.setPrompt(
|
||||||
scanLauncher.launch(scanOptions)
|
context.getString(R.string.scanning_qr)
|
||||||
|
)
|
||||||
|
scanOptions.setBeepEnabled(false)
|
||||||
|
scanOptions.captureActivity =
|
||||||
|
CaptureActivityPortrait::class.java
|
||||||
|
scanLauncher.launch(scanOptions)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
.padding(10.dp),
|
||||||
.padding(10.dp)
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Filled.QrCode,
|
Icons.Filled.QrCode,
|
||||||
contentDescription = stringResource(id = R.string.qr_scan),
|
contentDescription = stringResource(id = R.string.qr_scan),
|
||||||
modifier = Modifier.padding(10.dp)
|
modifier = Modifier.padding(10.dp),
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
stringResource(id = R.string.add_from_qr),
|
stringResource(id = R.string.add_from_qr),
|
||||||
modifier = Modifier.padding(10.dp)
|
modifier = Modifier.padding(10.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Divider()
|
HorizontalDivider()
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier =
|
||||||
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable {
|
.clickable {
|
||||||
showBottomSheet = false
|
showBottomSheet = false
|
||||||
navController.navigate("${Routes.Config.name}/${Constants.MANUAL_TUNNEL_CONFIG_ID}")
|
navController.navigate(
|
||||||
|
"${Screen.Config.route}/${Constants.MANUAL_TUNNEL_CONFIG_ID}",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.padding(10.dp)
|
.padding(10.dp),
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Filled.Create,
|
Icons.Filled.Create,
|
||||||
contentDescription = stringResource(id = R.string.create_import),
|
contentDescription = stringResource(id = R.string.create_import),
|
||||||
modifier = Modifier.padding(10.dp)
|
modifier = Modifier.padding(10.dp),
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
stringResource(id = R.string.create_import),
|
stringResource(id = R.string.create_import),
|
||||||
modifier = Modifier.padding(10.dp)
|
modifier = Modifier.padding(10.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Column(
|
|
||||||
|
LazyColumn(
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier = Modifier
|
modifier =
|
||||||
.fillMaxSize()
|
Modifier
|
||||||
.padding(padding)
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight(.90f)
|
||||||
|
.overscroll(ScrollableDefaults.overscrollEffect())
|
||||||
|
.padding(innerPadding),
|
||||||
|
state = rememberLazyListState(0, uiState.tunnels.count()),
|
||||||
|
userScrollEnabled = true,
|
||||||
|
reverseLayout = true,
|
||||||
|
flingBehavior = ScrollableDefaults.flingBehavior(),
|
||||||
) {
|
) {
|
||||||
LazyColumn(
|
items(
|
||||||
modifier = Modifier
|
uiState.tunnels,
|
||||||
.fillMaxSize()
|
key = { tunnel -> tunnel.id },
|
||||||
.nestedScroll(nestedScrollConnection),
|
) { tunnel ->
|
||||||
) {
|
val leadingIconColor =
|
||||||
items(tunnels, key = { tunnel -> tunnel.id }) { tunnel ->
|
(if (
|
||||||
val leadingIconColor = (if (tunnelName == tunnel.name) when (handshakeStatus) {
|
uiState.vpnState.name == tunnel.name &&
|
||||||
HandshakeStatus.HEALTHY -> mint
|
uiState.vpnState.status == Tunnel.State.UP
|
||||||
HandshakeStatus.UNHEALTHY -> brickRed
|
) {
|
||||||
HandshakeStatus.NOT_STARTED -> Color.Gray
|
uiState.vpnState.statistics
|
||||||
HandshakeStatus.NEVER_CONNECTED -> brickRed
|
?.mapPeerStats()
|
||||||
} else {Color.Gray})
|
?.map { it.value?.handshakeStatus() }
|
||||||
val focusRequester = remember { FocusRequester() }
|
.let { statuses ->
|
||||||
RowListItem(icon = {
|
when {
|
||||||
if (settings.isTunnelConfigDefault(tunnel))
|
statuses?.all { it == HandshakeStatus.HEALTHY } == true -> mint
|
||||||
|
statuses?.any { it == HandshakeStatus.STALE } == true -> corn
|
||||||
|
statuses?.all { it == HandshakeStatus.NOT_STARTED } == true ->
|
||||||
|
Color.Gray
|
||||||
|
else -> {
|
||||||
|
Color.Gray
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Color.Gray
|
||||||
|
})
|
||||||
|
val expanded = remember { mutableStateOf(false) }
|
||||||
|
RowListItem(
|
||||||
|
icon = {
|
||||||
|
if (uiState.settings.isTunnelConfigDefault(tunnel)) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Rounded.Star, "status",
|
Icons.Rounded.Star,
|
||||||
|
stringResource(R.string.status),
|
||||||
tint = leadingIconColor,
|
tint = leadingIconColor,
|
||||||
modifier = Modifier.padding(end = 10.dp).size(20.dp)
|
modifier = Modifier
|
||||||
|
.padding(end = 10.dp)
|
||||||
|
.size(20.dp),
|
||||||
)
|
)
|
||||||
else Icon(
|
} else {
|
||||||
Icons.Rounded.Circle, "status",
|
Icon(
|
||||||
tint = leadingIconColor,
|
Icons.Rounded.Circle,
|
||||||
modifier = Modifier.padding(end = 15.dp).size(15.dp)
|
stringResource(R.string.status),
|
||||||
)
|
tint = leadingIconColor,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = 15.dp)
|
||||||
|
.size(15.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
text = tunnel.name,
|
text = tunnel.name,
|
||||||
onHold = {
|
onHold = {
|
||||||
if (state == Tunnel.State.UP && tunnel.name == tunnelName) {
|
if (
|
||||||
showSnackbarMessage(context.resources.getString(R.string.turn_off_tunnel))
|
(uiState.vpnState.status == Tunnel.State.UP) &&
|
||||||
return@RowListItem
|
(tunnel.name == uiState.vpnState.name)
|
||||||
|
) {
|
||||||
|
showSnackbarMessage(Event.Message.TunnelOffAction.message)
|
||||||
|
return@RowListItem
|
||||||
|
}
|
||||||
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
|
selectedTunnel = tunnel
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
|
if (
|
||||||
|
uiState.vpnState.status == Tunnel.State.UP &&
|
||||||
|
(uiState.vpnState.name == tunnel.name)
|
||||||
|
) {
|
||||||
|
expanded.value = !expanded.value
|
||||||
}
|
}
|
||||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
} else {
|
||||||
selectedTunnel = tunnel
|
selectedTunnel = tunnel
|
||||||
},
|
focusRequester.requestFocus()
|
||||||
onClick = {
|
}
|
||||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
},
|
||||||
navController.navigate("${Routes.Detail.name}/${tunnel.id}")
|
statistics = uiState.vpnState.statistics,
|
||||||
} else {
|
expanded = expanded.value,
|
||||||
selectedTunnel = tunnel
|
rowButton = {
|
||||||
focusRequester.requestFocus()
|
if (
|
||||||
|
tunnel.id == selectedTunnel?.id &&
|
||||||
|
!WireGuardAutoTunnel.isRunningOnAndroidTv()
|
||||||
|
) {
|
||||||
|
Row {
|
||||||
|
if (!uiState.settings.isTunnelConfigDefault(tunnel)) {
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
if (
|
||||||
|
uiState.settings.isAutoTunnelEnabled &&
|
||||||
|
!uiState.settings.isAutoTunnelPaused
|
||||||
|
) {
|
||||||
|
showSnackbarMessage(
|
||||||
|
Event.Message.AutoTunnelOffAction.message,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
showPrimaryChangeAlertDialog = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Rounded.Star,
|
||||||
|
stringResource(id = R.string.set_primary),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
if (
|
||||||
|
uiState.settings.isAutoTunnelEnabled &&
|
||||||
|
uiState.settings.isTunnelConfigDefault(
|
||||||
|
tunnel,
|
||||||
|
) &&
|
||||||
|
!uiState.settings.isAutoTunnelPaused
|
||||||
|
) {
|
||||||
|
showSnackbarMessage(
|
||||||
|
Event.Message.AutoTunnelOffAction.message,
|
||||||
|
)
|
||||||
|
} else
|
||||||
|
navController.navigate(
|
||||||
|
"${Screen.Config.route}/${selectedTunnel?.id}",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Icon(Icons.Rounded.Edit, stringResource(id = R.string.edit))
|
||||||
|
}
|
||||||
|
IconButton(
|
||||||
|
modifier = Modifier.focusable(),
|
||||||
|
onClick = { showDeleteTunnelAlertDialog = true },
|
||||||
|
) {
|
||||||
|
Icon(Icons.Rounded.Delete, stringResource(id = R.string.delete))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
} else {
|
||||||
rowButton = {
|
val checked by remember {
|
||||||
if (tunnel.id == selectedTunnel?.id && !WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
derivedStateOf {
|
||||||
|
(uiState.vpnState.status == Tunnel.State.UP &&
|
||||||
|
tunnel.name == uiState.vpnState.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!checked) expanded.value = false
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TunnelSwitch() =
|
||||||
|
Switch(
|
||||||
|
modifier = Modifier.focusRequester(focusRequester),
|
||||||
|
checked = checked,
|
||||||
|
onCheckedChange = { checked ->
|
||||||
|
if (!checked) expanded.value = false
|
||||||
|
onTunnelToggle(checked, tunnel)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
Row {
|
Row {
|
||||||
if(!settings.isTunnelConfigDefault(tunnel)) {
|
if (!uiState.settings.isTunnelConfigDefault(tunnel)) {
|
||||||
IconButton(onClick = {
|
IconButton(
|
||||||
if(settings.isAutoTunnelEnabled) {
|
onClick = {
|
||||||
showSnackbarMessage(context.resources.getString(R.string.turn_off_auto))
|
if (uiState.settings.isAutoTunnelEnabled) {
|
||||||
} else showPrimaryChangeAlertDialog = true
|
showSnackbarMessage(
|
||||||
}) {
|
Event.Message.AutoTunnelOffAction.message,
|
||||||
Icon(Icons.Rounded.Star, stringResource(id = R.string.set_primary))
|
)
|
||||||
|
} else {
|
||||||
|
selectedTunnel = tunnel
|
||||||
|
showPrimaryChangeAlertDialog = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Rounded.Star,
|
||||||
|
stringResource(id = R.string.set_primary),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
IconButton(onClick = {
|
IconButton(
|
||||||
navController.navigate("${Routes.Config.name}/${selectedTunnel?.id}")
|
modifier = Modifier.focusRequester(focusRequester),
|
||||||
}) {
|
onClick = {
|
||||||
|
if (
|
||||||
|
uiState.vpnState.status == Tunnel.State.UP &&
|
||||||
|
(uiState.vpnState.name == tunnel.name)
|
||||||
|
) {
|
||||||
|
expanded.value = !expanded.value
|
||||||
|
} else {
|
||||||
|
showSnackbarMessage(
|
||||||
|
Event.Message.TunnelOnAction.message
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Icon(Icons.Rounded.Info, stringResource(R.string.info))
|
||||||
|
}
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
if (
|
||||||
|
uiState.vpnState.status == Tunnel.State.UP &&
|
||||||
|
tunnel.name == uiState.vpnState.name
|
||||||
|
) {
|
||||||
|
showSnackbarMessage(
|
||||||
|
Event.Message.TunnelOffAction.message
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
navController.navigate(
|
||||||
|
"${Screen.Config.route}/${tunnel.id}",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
Icon(Icons.Rounded.Edit, stringResource(id = R.string.edit))
|
Icon(Icons.Rounded.Edit, stringResource(id = R.string.edit))
|
||||||
}
|
}
|
||||||
IconButton(
|
IconButton(
|
||||||
modifier = Modifier.focusable(),
|
onClick = {
|
||||||
onClick = { viewModel.onDelete(tunnel) }) {
|
if (
|
||||||
|
uiState.vpnState.status == Tunnel.State.UP &&
|
||||||
|
tunnel.name == uiState.vpnState.name
|
||||||
|
) {
|
||||||
|
showSnackbarMessage(
|
||||||
|
Event.Message.TunnelOffAction.message
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
showDeleteTunnelAlertDialog = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Rounded.Delete,
|
Icons.Rounded.Delete,
|
||||||
stringResource(id = R.string.delete)
|
stringResource(id = R.string.delete),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
TunnelSwitch()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
TunnelSwitch()
|
||||||
Row {
|
|
||||||
if(!settings.isTunnelConfigDefault(tunnel)) {
|
|
||||||
IconButton(onClick = {
|
|
||||||
if(settings.isAutoTunnelEnabled) {
|
|
||||||
showSnackbarMessage(context.resources.getString(R.string.turn_off_auto))
|
|
||||||
} else showPrimaryChangeAlertDialog = true
|
|
||||||
}) {
|
|
||||||
Icon(Icons.Rounded.Star, stringResource(id = R.string.set_primary))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
IconButton(
|
|
||||||
modifier = Modifier.focusRequester(focusRequester),
|
|
||||||
onClick = {
|
|
||||||
navController.navigate("${Routes.Detail.name}/${tunnel.id}")
|
|
||||||
}) {
|
|
||||||
Icon(Icons.Rounded.Info, "Info")
|
|
||||||
}
|
|
||||||
IconButton(onClick = {
|
|
||||||
if (state == Tunnel.State.UP && tunnel.name == tunnelName)
|
|
||||||
showSnackbarMessage(
|
|
||||||
context.resources.getString(
|
|
||||||
R.string.turn_off_tunnel
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else {
|
|
||||||
navController.navigate("${Routes.Config.name}/${tunnel.id}")
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
Icon(
|
|
||||||
Icons.Rounded.Edit,
|
|
||||||
stringResource(id = R.string.edit)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
IconButton(onClick = {
|
|
||||||
if (state == Tunnel.State.UP && tunnel.name == tunnelName)
|
|
||||||
showSnackbarMessage(
|
|
||||||
context.resources.getString(
|
|
||||||
R.string.turn_off_tunnel
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else {
|
|
||||||
viewModel.onDelete(tunnel)
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
Icon(
|
|
||||||
Icons.Rounded.Delete,
|
|
||||||
stringResource(id = R.string.delete)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Switch(
|
|
||||||
modifier = Modifier.focusRequester(focusRequester),
|
|
||||||
checked = (state == Tunnel.State.UP && tunnel.name == tunnelName),
|
|
||||||
onCheckedChange = { checked ->
|
|
||||||
onTunnelToggle(checked, tunnel)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Switch(
|
|
||||||
checked = (state == Tunnel.State.UP && tunnel.name == tunnelName),
|
|
||||||
onCheckedChange = { checked ->
|
|
||||||
onTunnelToggle(checked, tunnel)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.main
|
||||||
|
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
||||||
|
|
||||||
|
data class MainUiState(
|
||||||
|
val settings: Settings = Settings(),
|
||||||
|
val tunnels: TunnelConfigs = emptyList(),
|
||||||
|
val vpnState: VpnState = VpnState(),
|
||||||
|
val loading: Boolean = true
|
||||||
|
)
|
||||||
+165
-162
@@ -8,235 +8,237 @@ import android.provider.OpenableColumns
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.wireguard.config.Config
|
import com.wireguard.config.Config
|
||||||
import com.zaneschepke.wireguardautotunnel.Constants
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceState
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Event
|
||||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||||
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
|
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import timber.log.Timber
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.util.zip.ZipInputStream
|
import java.util.zip.ZipInputStream
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class MainViewModel @Inject constructor(
|
class MainViewModel
|
||||||
|
@Inject
|
||||||
|
constructor(
|
||||||
private val application: Application,
|
private val application: Application,
|
||||||
private val tunnelRepo: TunnelConfigDao,
|
private val tunnelConfigRepository: TunnelConfigRepository,
|
||||||
private val settingsRepo: SettingsDoa,
|
private val settingsRepository: SettingsRepository,
|
||||||
private val vpnService: VpnService
|
private val vpnService: VpnService
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
val tunnels get() = tunnelRepo.getAllFlow()
|
val uiState =
|
||||||
val state get() = vpnService.state
|
combine(
|
||||||
|
settingsRepository.getSettingsFlow(),
|
||||||
val handshakeStatus get() = vpnService.handshakeStatus
|
tunnelConfigRepository.getTunnelConfigsFlow(),
|
||||||
val tunnelName get() = vpnService.tunnelName
|
vpnService.vpnState,
|
||||||
private val _settings = MutableStateFlow(Settings())
|
) { settings, tunnels, vpnState ->
|
||||||
val settings get() = _settings.asStateFlow()
|
|
||||||
|
|
||||||
init {
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
settingsRepo.getAllFlow().filter { it.isNotEmpty() }.collect {
|
|
||||||
val settings = it.first()
|
|
||||||
validateWatcherServiceState(settings)
|
validateWatcherServiceState(settings)
|
||||||
_settings.emit(settings)
|
MainUiState(settings, tunnels, vpnState, false)
|
||||||
|
}
|
||||||
|
.stateIn(
|
||||||
|
viewModelScope,
|
||||||
|
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
|
||||||
|
MainUiState(),
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun validateWatcherServiceState(settings: Settings) =
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
if (settings.isAutoTunnelEnabled) {
|
||||||
|
ServiceManager.startWatcherService(application.applicationContext)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun validateWatcherServiceState(settings: Settings) {
|
private fun stopWatcherService() =
|
||||||
val watcherState = ServiceManager.getServiceState(
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
application.applicationContext,
|
ServiceManager.stopWatcherService(application.applicationContext)
|
||||||
WireGuardConnectivityWatcherService::class.java
|
|
||||||
)
|
|
||||||
if (settings.isAutoTunnelEnabled && watcherState == ServiceState.STOPPED && settings.defaultTunnel != null) {
|
|
||||||
ServiceManager.startWatcherService(
|
|
||||||
application.applicationContext,
|
|
||||||
settings.defaultTunnel!!
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun onDelete(tunnel: TunnelConfig) {
|
fun onDelete(tunnel: TunnelConfig) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
if (tunnelRepo.count() == 1L) {
|
if (tunnelConfigRepository.count() == 1) {
|
||||||
ServiceManager.stopWatcherService(application.applicationContext)
|
stopWatcherService()
|
||||||
val settings = settingsRepo.getAll()
|
val settings = settingsRepository.getSettings()
|
||||||
if (settings.isNotEmpty()) {
|
settings.defaultTunnel = null
|
||||||
val setting = settings[0]
|
settings.isAutoTunnelEnabled = false
|
||||||
setting.defaultTunnel = null
|
settings.isAlwaysOnVpnEnabled = false
|
||||||
setting.isAutoTunnelEnabled = false
|
saveSettings(settings)
|
||||||
setting.isAlwaysOnVpnEnabled = false
|
|
||||||
settingsRepo.save(setting)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
tunnelRepo.delete(tunnel)
|
tunnelConfigRepository.delete(tunnel)
|
||||||
|
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onTunnelStart(tunnelConfig: TunnelConfig) {
|
fun onTunnelStart(tunnelConfig: TunnelConfig) =
|
||||||
viewModelScope.launch {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
stopActiveTunnel()
|
Timber.d("On start called!")
|
||||||
|
stopActiveTunnel().await()
|
||||||
startTunnel(tunnelConfig)
|
startTunnel(tunnelConfig)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun startTunnel(tunnelConfig: TunnelConfig) {
|
private fun startTunnel(tunnelConfig: TunnelConfig) =
|
||||||
ServiceManager.startVpnService(application.applicationContext, tunnelConfig.toString())
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
}
|
Timber.d("Start tunnel via manager")
|
||||||
|
ServiceManager.startVpnService(application.applicationContext, tunnelConfig.toString())
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun stopActiveTunnel() {
|
private fun stopActiveTunnel() =
|
||||||
if (ServiceManager.getServiceState(
|
viewModelScope.async(Dispatchers.IO) {
|
||||||
application.applicationContext,
|
|
||||||
WireGuardTunnelService::class.java,
|
|
||||||
) == ServiceState.STARTED
|
|
||||||
) {
|
|
||||||
onTunnelStop()
|
onTunnelStop()
|
||||||
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun onTunnelStop() {
|
fun onTunnelStop() =
|
||||||
ServiceManager.stopVpnService(application.applicationContext)
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
}
|
Timber.d("Stopping active tunnel")
|
||||||
|
ServiceManager.stopVpnService(application.applicationContext)
|
||||||
|
}
|
||||||
|
|
||||||
private fun validateConfigString(config: String) {
|
private fun validateConfigString(config: String) {
|
||||||
if (!config.contains(application.getString(R.string.config_validation))) {
|
TunnelConfig.configFromQuick(config)
|
||||||
throw WgTunnelException(application.getString(R.string.config_validation))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onTunnelQrResult(result: String) {
|
suspend fun onTunnelQrResult(result: String): Result<Unit> {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
return try {
|
||||||
try {
|
validateConfigString(result)
|
||||||
validateConfigString(result)
|
val tunnelConfig =
|
||||||
val tunnelConfig =
|
TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = result)
|
||||||
TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = result)
|
addTunnel(tunnelConfig)
|
||||||
addTunnel(tunnelConfig)
|
Result.Success(Unit)
|
||||||
} catch (e: WgTunnelException) {
|
|
||||||
throw WgTunnelException(
|
|
||||||
e.message ?: application.getString(R.string.unknown_error_message)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun saveTunnelConfigFromStream(stream: InputStream, fileName: String) {
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
val bufferReader = stream.bufferedReader(charset = Charsets.UTF_8)
|
|
||||||
val config = Config.parse(bufferReader)
|
|
||||||
val tunnelName = getNameFromFileName(fileName)
|
|
||||||
addTunnel(TunnelConfig(name = tunnelName, wgQuick = config.toWgQuickString()))
|
|
||||||
stream.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getInputStreamFromUri(uri: Uri): InputStream {
|
|
||||||
return application.applicationContext.contentResolver.openInputStream(uri)
|
|
||||||
?: throw WgTunnelException(application.getString(R.string.stream_failed))
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun onTunnelFileSelected(uri: Uri) {
|
|
||||||
try {
|
|
||||||
val fileName = getFileName(application.applicationContext, uri)
|
|
||||||
val fileExtension = getFileExtensionFromFileName(fileName)
|
|
||||||
when(fileExtension){
|
|
||||||
Constants.CONF_FILE_EXTENSION -> saveTunnelFromConfUri(fileName, uri)
|
|
||||||
Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri)
|
|
||||||
else -> throw WgTunnelException(application.getString(R.string.file_extension_message))
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw WgTunnelException(e.message ?: "Error importing file")
|
Result.Error(Event.Error.InvalidQrCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun saveTunnelConfigFromStream(stream: InputStream, fileName: String) {
|
||||||
|
val bufferReader = stream.bufferedReader(charset = Charsets.UTF_8)
|
||||||
|
val config = Config.parse(bufferReader)
|
||||||
|
val tunnelName = getNameFromFileName(fileName)
|
||||||
|
addTunnel(TunnelConfig(name = tunnelName, wgQuick = config.toWgQuickString()))
|
||||||
|
withContext(Dispatchers.IO) { stream.close() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getInputStreamFromUri(uri: Uri): InputStream? {
|
||||||
|
return application.applicationContext.contentResolver.openInputStream(uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun onTunnelFileSelected(uri: Uri): Result<Unit> {
|
||||||
|
try {
|
||||||
|
if (isValidUriContentScheme(uri)) {
|
||||||
|
val fileName = getFileName(application.applicationContext, uri)
|
||||||
|
when (getFileExtensionFromFileName(fileName)) {
|
||||||
|
Constants.CONF_FILE_EXTENSION ->
|
||||||
|
saveTunnelFromConfUri(fileName, uri).let {
|
||||||
|
when (it) {
|
||||||
|
is Result.Error -> return Result.Error(Event.Error.FileReadFailed)
|
||||||
|
is Result.Success -> return it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri)
|
||||||
|
else -> return Result.Error(Event.Error.InvalidFileExtension)
|
||||||
|
}
|
||||||
|
return Result.Success(Unit)
|
||||||
|
} else {
|
||||||
|
return Result.Error(Event.Error.InvalidFileExtension)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
return Result.Error(Event.Error.FileReadFailed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun saveTunnelsFromZipUri(uri: Uri) {
|
private suspend fun saveTunnelsFromZipUri(uri: Uri) {
|
||||||
ZipInputStream(getInputStreamFromUri(uri)).use { zip ->
|
ZipInputStream(getInputStreamFromUri(uri)).use { zip ->
|
||||||
generateSequence { zip.nextEntry }
|
generateSequence { zip.nextEntry }
|
||||||
.filterNot { it.isDirectory ||
|
.filterNot {
|
||||||
getFileExtensionFromFileName(it.name) != Constants.CONF_FILE_EXTENSION }
|
it.isDirectory ||
|
||||||
|
getFileExtensionFromFileName(it.name) != Constants.CONF_FILE_EXTENSION
|
||||||
|
}
|
||||||
.forEach {
|
.forEach {
|
||||||
val name = getNameFromFileName(it.name)
|
val name = getNameFromFileName(it.name)
|
||||||
val config = Config.parse(zip)
|
val config = Config.parse(zip)
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
addTunnel(TunnelConfig(name = name, wgQuick = config.toWgQuickString()))
|
addTunnel(TunnelConfig(name = name, wgQuick = config.toWgQuickString()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveTunnelFromConfUri(name : String, uri: Uri) {
|
private suspend fun saveTunnelFromConfUri(name: String, uri: Uri): Result<Unit> {
|
||||||
val stream = getInputStreamFromUri(uri)
|
val stream = getInputStreamFromUri(uri)
|
||||||
saveTunnelConfigFromStream(stream, name)
|
return if (stream != null) {
|
||||||
|
saveTunnelConfigFromStream(stream, name)
|
||||||
|
Result.Success(Unit)
|
||||||
|
} else {
|
||||||
|
Result.Error(Event.Error.FileReadFailed)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun addTunnel(tunnelConfig: TunnelConfig) {
|
private suspend fun addTunnel(tunnelConfig: TunnelConfig) {
|
||||||
saveTunnel(tunnelConfig)
|
saveTunnel(tunnelConfig)
|
||||||
|
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun pauseAutoTunneling() =
|
||||||
|
viewModelScope.launch {
|
||||||
|
settingsRepository.save(uiState.value.settings.copy(isAutoTunnelPaused = true))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resumeAutoTunneling() =
|
||||||
|
viewModelScope.launch {
|
||||||
|
settingsRepository.save(uiState.value.settings.copy(isAutoTunnelPaused = false))
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun saveTunnel(tunnelConfig: TunnelConfig) {
|
private suspend fun saveTunnel(tunnelConfig: TunnelConfig) {
|
||||||
tunnelRepo.save(tunnelConfig)
|
tunnelConfigRepository.save(tunnelConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getFileNameByCursor(context: Context, uri: Uri): String {
|
private fun getFileNameByCursor(context: Context, uri: Uri): String? {
|
||||||
val cursor = context.contentResolver.query(uri, null, null, null, null)
|
context.contentResolver.query(uri, null, null, null, null)?.use {
|
||||||
if (cursor != null) {
|
return getDisplayNameByCursor(it)
|
||||||
cursor.use {
|
|
||||||
return getDisplayNameByCursor(it)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw WgTunnelException("Failed to initialize cursor")
|
|
||||||
}
|
}
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getDisplayNameColumnIndex(cursor: Cursor): Int {
|
private fun getDisplayNameColumnIndex(cursor: Cursor): Int? {
|
||||||
val columnIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
val columnIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||||
if (columnIndex == -1) {
|
return if (columnIndex != -1) {
|
||||||
throw WgTunnelException("Cursor out of bounds")
|
return columnIndex
|
||||||
}
|
|
||||||
return columnIndex
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getDisplayNameByCursor(cursor: Cursor): String {
|
|
||||||
if (cursor.moveToFirst()) {
|
|
||||||
val index = getDisplayNameColumnIndex(cursor)
|
|
||||||
return cursor.getString(index)
|
|
||||||
} else {
|
} else {
|
||||||
throw WgTunnelException("Cursor failed to move to first")
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun validateUriContentScheme(uri: Uri) {
|
private fun getDisplayNameByCursor(cursor: Cursor): String? {
|
||||||
if (uri.scheme != Constants.URI_CONTENT_SCHEME) {
|
return if (cursor.moveToFirst()) {
|
||||||
throw WgTunnelException(application.getString(R.string.file_extension_message))
|
val index = getDisplayNameColumnIndex(cursor)
|
||||||
}
|
if (index != null) {
|
||||||
|
cursor.getString(index)
|
||||||
|
} else null
|
||||||
|
} else null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isValidUriContentScheme(uri: Uri): Boolean {
|
||||||
|
return uri.scheme == Constants.URI_CONTENT_SCHEME
|
||||||
|
}
|
||||||
|
|
||||||
private fun getFileName(context: Context, uri: Uri): String {
|
private fun getFileName(context: Context, uri: Uri): String {
|
||||||
validateUriContentScheme(uri)
|
return getFileNameByCursor(context, uri) ?: NumberUtils.generateRandomTunnelName()
|
||||||
return try {
|
|
||||||
getFileNameByCursor(context, uri)
|
|
||||||
} catch (_: Exception) {
|
|
||||||
NumberUtils.generateRandomTunnelName()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getNameFromFileName(fileName: String): String {
|
private fun getNameFromFileName(fileName: String): String {
|
||||||
@@ -251,14 +253,15 @@ class MainViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun onDefaultTunnelChange(selectedTunnel: TunnelConfig?) {
|
private fun saveSettings(settings: Settings) =
|
||||||
if (selectedTunnel != null) {
|
viewModelScope.launch(Dispatchers.IO) { settingsRepository.save(settings) }
|
||||||
_settings.emit(
|
|
||||||
_settings.value.copy(
|
fun onDefaultTunnelChange(selectedTunnel: TunnelConfig?) =
|
||||||
defaultTunnel = selectedTunnel.toString()
|
viewModelScope.launch {
|
||||||
)
|
if (selectedTunnel != null) {
|
||||||
)
|
saveSettings(uiState.value.settings.copy(defaultTunnel = selectedTunnel.toString()))
|
||||||
settingsRepo.save(_settings.value)
|
.join()
|
||||||
|
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
+466
-267
@@ -1,10 +1,18 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.settings
|
package com.zaneschepke.wireguardautotunnel.ui.screens.settings
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context.POWER_SERVICE
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.os.PowerManager
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
|
import android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.ActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@@ -15,7 +23,6 @@ import androidx.compose.foundation.layout.IntrinsicSize
|
|||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
@@ -30,6 +37,7 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.outlined.Add
|
import androidx.compose.material.icons.outlined.Add
|
||||||
import androidx.compose.material.icons.rounded.LocationOff
|
import androidx.compose.material.icons.rounded.LocationOff
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -38,21 +46,20 @@ import androidx.compose.material3.Surface
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.SideEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusProperties
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
@@ -65,361 +72,553 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
import com.google.accompanist.permissions.isGranted
|
import com.google.accompanist.permissions.isGranted
|
||||||
import com.google.accompanist.permissions.rememberPermissionState
|
import com.google.accompanist.permissions.rememberPermissionState
|
||||||
|
import com.wireguard.android.backend.Tunnel
|
||||||
|
import com.wireguard.android.backend.WgQuickBackend
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
|
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
|
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
|
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
|
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
|
||||||
import com.zaneschepke.wireguardautotunnel.util.StorageUtil
|
import com.zaneschepke.wireguardautotunnel.util.Event
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import kotlin.math.exp
|
|
||||||
|
|
||||||
@OptIn(
|
@OptIn(
|
||||||
ExperimentalPermissionsApi::class,
|
ExperimentalPermissionsApi::class,
|
||||||
ExperimentalLayoutApi::class, ExperimentalComposeUiApi::class
|
ExperimentalLayoutApi::class,
|
||||||
)
|
)
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsScreen(
|
fun SettingsScreen(
|
||||||
viewModel: SettingsViewModel = hiltViewModel(),
|
|
||||||
padding: PaddingValues,
|
padding: PaddingValues,
|
||||||
|
viewModel: SettingsViewModel = hiltViewModel(),
|
||||||
showSnackbarMessage: (String) -> Unit,
|
showSnackbarMessage: (String) -> Unit,
|
||||||
focusRequester: FocusRequester,
|
focusRequester: FocusRequester
|
||||||
) {
|
) {
|
||||||
|
val scope = rememberCoroutineScope { Dispatchers.IO }
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
val scrollState = rememberScrollState()
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
|
||||||
val settings by viewModel.settings.collectAsStateWithLifecycle()
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
val trustedSSIDs by viewModel.trustedSSIDs.collectAsStateWithLifecycle()
|
|
||||||
val tunnels by viewModel.tunnels.collectAsStateWithLifecycle(mutableListOf())
|
|
||||||
val fineLocationState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
|
val fineLocationState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||||
var currentText by remember { mutableStateOf("") }
|
var currentText by remember { mutableStateOf("") }
|
||||||
val scrollState = rememberScrollState()
|
|
||||||
var didShowLocationDisclaimer by remember { mutableStateOf(false) }
|
|
||||||
var isBackgroundLocationGranted by remember { mutableStateOf(true) }
|
var isBackgroundLocationGranted by remember { mutableStateOf(true) }
|
||||||
var showAuthPrompt by remember { mutableStateOf(false) }
|
var showLocationServicesAlertDialog by remember { mutableStateOf(false) }
|
||||||
var didExportFiles by remember { mutableStateOf(false) }
|
var didExportFiles by remember { mutableStateOf(false) }
|
||||||
|
var showAuthPrompt by remember { mutableStateOf(false) }
|
||||||
|
val focusRequester2 = remember { FocusRequester() }
|
||||||
|
|
||||||
val screenPadding = 5.dp
|
val screenPadding = 5.dp
|
||||||
val fillMaxWidth = .85f
|
val fillMaxWidth = .85f
|
||||||
|
|
||||||
|
if (uiState.loading) {
|
||||||
|
LoadingScreen()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val startForResult =
|
||||||
|
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
|
result: ActivityResult ->
|
||||||
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
|
result.data
|
||||||
|
// Handle the Intent
|
||||||
|
}
|
||||||
|
viewModel.setBatteryOptimizeDisableShown()
|
||||||
|
}
|
||||||
|
|
||||||
fun exportAllConfigs() {
|
fun exportAllConfigs() {
|
||||||
try {
|
try {
|
||||||
val files = tunnels.map { File(context.cacheDir, "${it.name}.conf") }
|
val files = uiState.tunnels.map { File(context.cacheDir, "${it.name}.conf") }
|
||||||
files.forEachIndexed() { index, file ->
|
files.forEachIndexed { index, file ->
|
||||||
file.outputStream().use {
|
file.outputStream().use { it.write(uiState.tunnels[index].wgQuick.toByteArray()) }
|
||||||
it.write(tunnels[index].wgQuick.toByteArray())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
StorageUtil.saveFilesToZip(context, files)
|
FileUtils.saveFilesToZip(context, files)
|
||||||
didExportFiles = true
|
didExportFiles = true
|
||||||
showSnackbarMessage("Exported configs to downloads")
|
showSnackbarMessage(Event.Message.ConfigsExported.message)
|
||||||
} catch (e : Exception) {
|
} catch (e: Exception) {
|
||||||
showSnackbarMessage(e.message!!)
|
showSnackbarMessage(Event.Error.Exception(e).message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isBatteryOptimizationsDisabled(): Boolean {
|
||||||
|
val pm = context.getSystemService(POWER_SERVICE) as PowerManager
|
||||||
|
return pm.isIgnoringBatteryOptimizations(context.packageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun requestBatteryOptimizationsDisabled() {
|
||||||
|
val intent =
|
||||||
|
Intent().apply {
|
||||||
|
this.action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
|
||||||
|
data = Uri.fromParts("package", context.packageName, null)
|
||||||
|
}
|
||||||
|
startForResult.launch(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun handleAutoTunnelToggle() {
|
||||||
|
if (uiState.isBatteryOptimizeDisableShown || isBatteryOptimizationsDisabled()) {
|
||||||
|
viewModel.toggleAutoTunnel()
|
||||||
|
} else {
|
||||||
|
requestBatteryOptimizationsDisabled()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun saveTrustedSSID() {
|
fun saveTrustedSSID() {
|
||||||
if (currentText.isNotEmpty()) {
|
if (currentText.isNotEmpty()) {
|
||||||
scope.launch {
|
viewModel.onSaveTrustedSSID(currentText).let {
|
||||||
try {
|
when (it) {
|
||||||
viewModel.onSaveTrustedSSID(currentText)
|
is Result.Success -> currentText = ""
|
||||||
currentText = ""
|
is Result.Error -> showSnackbarMessage(it.error.message)
|
||||||
} catch (e : Exception) {
|
|
||||||
showSnackbarMessage(e.message ?: "Unknown error")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isAllAutoTunnelPermissionsEnabled() : Boolean {
|
|
||||||
return(isBackgroundLocationGranted && fineLocationState.status.isGranted && !viewModel.isLocationServicesNeeded())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun openSettings() {
|
fun openSettings() {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val intentSettings =
|
val intentSettings = Intent(ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||||
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
intentSettings.data = Uri.fromParts("package", context.packageName, null)
|
||||||
intentSettings.data =
|
|
||||||
Uri.fromParts("package", context.packageName, null)
|
|
||||||
context.startActivity(intentSettings)
|
context.startActivity(intentSettings)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
fun checkFineLocationGranted() {
|
||||||
val backgroundLocationState =
|
isBackgroundLocationGranted =
|
||||||
rememberPermissionState(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
|
if (!fineLocationState.status.isGranted) {
|
||||||
if(!backgroundLocationState.status.isGranted) {
|
false
|
||||||
isBackgroundLocationGranted = false
|
} else {
|
||||||
if(!didShowLocationDisclaimer) {
|
viewModel.setLocationDisclosureShown()
|
||||||
Column(
|
true
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
verticalArrangement = Arrangement.Top,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.verticalScroll(scrollState)
|
|
||||||
.padding(padding)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Icons.Rounded.LocationOff,
|
|
||||||
contentDescription = stringResource(id = R.string.map),
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(30.dp)
|
|
||||||
.size(128.dp)
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.prominent_background_location_title),
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
modifier = Modifier.padding(30.dp),
|
|
||||||
fontSize = 20.sp
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.prominent_background_location_message),
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
modifier = Modifier.padding(30.dp),
|
|
||||||
fontSize = 15.sp
|
|
||||||
)
|
|
||||||
Row(
|
|
||||||
modifier = if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(10.dp) else Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(30.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly
|
|
||||||
) {
|
|
||||||
TextButton(onClick = {
|
|
||||||
didShowLocationDisclaimer = true
|
|
||||||
}) {
|
|
||||||
Text(stringResource(id = R.string.no_thanks))
|
|
||||||
}
|
|
||||||
TextButton(modifier = Modifier.focusRequester(focusRequester), onClick = {
|
|
||||||
openSettings()
|
|
||||||
}) {
|
|
||||||
Text(stringResource(id = R.string.turn_on))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
if (
|
||||||
|
WireGuardAutoTunnel.isRunningOnAndroidTv() &&
|
||||||
|
Build.VERSION.SDK_INT == Build.VERSION_CODES.Q
|
||||||
|
) {
|
||||||
|
checkFineLocationGranted()
|
||||||
} else {
|
} else {
|
||||||
isBackgroundLocationGranted = true
|
val backgroundLocationState =
|
||||||
|
rememberPermissionState(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
|
||||||
|
isBackgroundLocationGranted =
|
||||||
|
if (!backgroundLocationState.status.isGranted) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
SideEffect { viewModel.setLocationDisclosureShown() }
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(showAuthPrompt) {
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
|
||||||
AuthorizationPrompt(onSuccess = {
|
checkFineLocationGranted()
|
||||||
showAuthPrompt = false
|
|
||||||
exportAllConfigs() },
|
|
||||||
onError = { error ->
|
|
||||||
showSnackbarMessage(error)
|
|
||||||
showAuthPrompt = false
|
|
||||||
},
|
|
||||||
onFailure = {
|
|
||||||
showAuthPrompt = false
|
|
||||||
showSnackbarMessage("Authentication failed")
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tunnels.isEmpty()) {
|
AnimatedVisibility(showLocationServicesAlertDialog) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showLocationServicesAlertDialog = false },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
showLocationServicesAlertDialog = false
|
||||||
|
handleAutoTunnelToggle()
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.okay))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { showLocationServicesAlertDialog = false }) {
|
||||||
|
Text(text = stringResource(R.string.cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title = { Text(text = stringResource(R.string.location_services_not_detected)) },
|
||||||
|
text = { Text(text = stringResource(R.string.location_services_missing_message)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!uiState.isLocationDisclosureShown) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Top,
|
||||||
|
modifier = Modifier.fillMaxSize().verticalScroll(scrollState).padding(padding),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Rounded.LocationOff,
|
||||||
|
contentDescription = stringResource(id = R.string.map),
|
||||||
|
modifier = Modifier.padding(30.dp).size(128.dp),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.prominent_background_location_title),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(30.dp),
|
||||||
|
fontSize = 20.sp,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.prominent_background_location_message),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(30.dp),
|
||||||
|
fontSize = 15.sp,
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier =
|
||||||
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
|
Modifier.fillMaxWidth().padding(10.dp)
|
||||||
|
} else {
|
||||||
|
Modifier.fillMaxWidth().padding(30.dp)
|
||||||
|
},
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
|
) {
|
||||||
|
TextButton(onClick = { viewModel.setLocationDisclosureShown() }) {
|
||||||
|
Text(stringResource(id = R.string.no_thanks))
|
||||||
|
}
|
||||||
|
TextButton(
|
||||||
|
modifier = Modifier.focusRequester(focusRequester),
|
||||||
|
onClick = {
|
||||||
|
openSettings()
|
||||||
|
viewModel.setLocationDisclosureShown()
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Text(stringResource(id = R.string.turn_on))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showAuthPrompt) {
|
||||||
|
AuthorizationPrompt(
|
||||||
|
onSuccess = {
|
||||||
|
showAuthPrompt = false
|
||||||
|
exportAllConfigs()
|
||||||
|
},
|
||||||
|
onError = { _ ->
|
||||||
|
showAuthPrompt = false
|
||||||
|
showSnackbarMessage(Event.Error.AuthenticationFailed.message)
|
||||||
|
},
|
||||||
|
onFailure = {
|
||||||
|
showAuthPrompt = false
|
||||||
|
showSnackbarMessage(Event.Error.AuthorizationFailed.message)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uiState.tunnels.isEmpty() && uiState.isLocationDisclosureShown) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxSize().padding(padding),
|
||||||
.fillMaxSize()
|
|
||||||
.padding(padding)
|
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.one_tunnel_required),
|
stringResource(R.string.one_tunnel_required),
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
modifier = Modifier.padding(15.dp),
|
modifier = Modifier.padding(15.dp),
|
||||||
fontStyle = FontStyle.Italic
|
fontStyle = FontStyle.Italic,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
if (uiState.isLocationDisclosureShown && uiState.tunnels.isNotEmpty()) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier = Modifier
|
modifier =
|
||||||
.fillMaxSize()
|
Modifier.fillMaxSize().padding(padding).verticalScroll(scrollState).clickable(
|
||||||
.verticalScroll(scrollState)
|
indication = null,
|
||||||
.clickable(indication = null, interactionSource = interactionSource) {
|
interactionSource = interactionSource,
|
||||||
focusManager.clearFocus()
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Surface(
|
|
||||||
tonalElevation = 2.dp,
|
|
||||||
shadowElevation = 2.dp,
|
|
||||||
shape = RoundedCornerShape(12.dp),
|
|
||||||
color = MaterialTheme.colorScheme.surface,
|
|
||||||
modifier = (if (WireGuardAutoTunnel.isRunningOnAndroidTv(context))
|
|
||||||
Modifier
|
|
||||||
.height(IntrinsicSize.Min)
|
|
||||||
.fillMaxWidth(fillMaxWidth)
|
|
||||||
else Modifier.fillMaxWidth(fillMaxWidth)).padding(top = 60.dp, bottom = 25.dp)
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.Start,
|
|
||||||
verticalArrangement = Arrangement.Top,
|
|
||||||
modifier = Modifier.padding(15.dp)
|
|
||||||
) {
|
|
||||||
SectionTitle(title = stringResource(id = R.string.auto_tunneling), padding = screenPadding)
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.trusted_ssid),
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
modifier = Modifier.padding(screenPadding, bottom = 5.dp, top = 5.dp)
|
|
||||||
)
|
|
||||||
FlowRow(
|
|
||||||
modifier = Modifier.padding(screenPadding),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
verticalArrangement = Arrangement.SpaceEvenly
|
|
||||||
) {
|
) {
|
||||||
trustedSSIDs.forEach { ssid ->
|
focusManager.clearFocus()
|
||||||
ClickableIconButton(
|
},
|
||||||
onIconClick = {
|
) {
|
||||||
scope.launch {
|
|
||||||
viewModel.onDeleteTrustedSSID(ssid)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
text = ssid,
|
|
||||||
icon = Icons.Filled.Close,
|
|
||||||
enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if(trustedSSIDs.isEmpty()) {
|
|
||||||
Text(stringResource(R.string.none), fontStyle = FontStyle.Italic, color = Color.Gray)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
OutlinedTextField(
|
|
||||||
enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled),
|
|
||||||
value = currentText,
|
|
||||||
onValueChange = { currentText = it },
|
|
||||||
label = { Text(stringResource(R.string.add_trusted_ssid)) },
|
|
||||||
modifier = Modifier.padding(start = screenPadding, top = 5.dp).focusRequester(focusRequester).onFocusChanged {
|
|
||||||
if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
|
||||||
keyboardController?.hide()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
maxLines = 1,
|
|
||||||
keyboardOptions = KeyboardOptions(
|
|
||||||
capitalization = KeyboardCapitalization.None,
|
|
||||||
imeAction = ImeAction.Done
|
|
||||||
),
|
|
||||||
keyboardActions = KeyboardActions(
|
|
||||||
onDone = {
|
|
||||||
saveTrustedSSID()
|
|
||||||
}
|
|
||||||
),
|
|
||||||
trailingIcon = {
|
|
||||||
IconButton(onClick = { saveTrustedSSID() }) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Outlined.Add,
|
|
||||||
contentDescription = if (currentText == "") stringResource(id = R.string.trusted_ssid_empty_description) else stringResource(
|
|
||||||
id = R.string.trusted_ssid_value_description
|
|
||||||
),
|
|
||||||
tint = if (currentText == "") Color.Transparent else MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
ConfigurationToggle(stringResource(R.string.tunnel_mobile_data),
|
|
||||||
enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled),
|
|
||||||
checked = settings.isTunnelOnMobileDataEnabled,
|
|
||||||
padding = screenPadding,
|
|
||||||
onCheckChanged = {
|
|
||||||
scope.launch {
|
|
||||||
viewModel.onToggleTunnelOnMobileData()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
ConfigurationToggle(stringResource(id = R.string.tunnel_on_ethernet),
|
|
||||||
enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled),
|
|
||||||
checked = settings.isTunnelOnEthernetEnabled,
|
|
||||||
padding = screenPadding,
|
|
||||||
onCheckChanged = {
|
|
||||||
scope.launch {
|
|
||||||
viewModel.onToggleTunnelOnEthernet()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
ConfigurationToggle(stringResource(R.string.enable_auto_tunnel),
|
|
||||||
enabled = !settings.isAlwaysOnVpnEnabled,
|
|
||||||
checked = settings.isAutoTunnelEnabled,
|
|
||||||
padding = screenPadding,
|
|
||||||
onCheckChanged = {
|
|
||||||
if(!isAllAutoTunnelPermissionsEnabled()) {
|
|
||||||
val message = if(viewModel.isLocationServicesNeeded()){
|
|
||||||
"Location services required"
|
|
||||||
} else if(!isBackgroundLocationGranted){
|
|
||||||
"Background location required"
|
|
||||||
} else {
|
|
||||||
"Precise location required"
|
|
||||||
}
|
|
||||||
showSnackbarMessage(message)
|
|
||||||
} else scope.launch {
|
|
||||||
viewModel.toggleAutoTunnel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
if(!WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
|
||||||
Surface(
|
Surface(
|
||||||
tonalElevation = 2.dp,
|
tonalElevation = 2.dp,
|
||||||
shadowElevation = 2.dp,
|
shadowElevation = 2.dp,
|
||||||
shape = RoundedCornerShape(12.dp),
|
shape = RoundedCornerShape(12.dp),
|
||||||
color = MaterialTheme.colorScheme.surface,
|
color = MaterialTheme.colorScheme.surface,
|
||||||
modifier = Modifier.fillMaxWidth(fillMaxWidth)
|
modifier =
|
||||||
.height(IntrinsicSize.Min)
|
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
.padding(bottom = 180.dp)
|
Modifier.height(IntrinsicSize.Min)
|
||||||
|
.fillMaxWidth(fillMaxWidth)
|
||||||
|
.padding(top = 10.dp)
|
||||||
|
} else {
|
||||||
|
Modifier.fillMaxWidth(fillMaxWidth).padding(top = 20.dp)
|
||||||
|
})
|
||||||
|
.padding(bottom = 10.dp),
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier = Modifier.padding(15.dp)
|
modifier = Modifier.padding(15.dp),
|
||||||
) {
|
) {
|
||||||
SectionTitle(title = stringResource(id = R.string.other), padding = screenPadding)
|
SectionTitle(
|
||||||
ConfigurationToggle(stringResource(R.string.always_on_vpn_support),
|
title = stringResource(id = R.string.auto_tunneling),
|
||||||
enabled = !settings.isAutoTunnelEnabled,
|
|
||||||
checked = settings.isAlwaysOnVpnEnabled,
|
|
||||||
padding = screenPadding,
|
padding = screenPadding,
|
||||||
onCheckChanged = {
|
)
|
||||||
scope.launch {
|
ConfigurationToggle(
|
||||||
viewModel.onToggleAlwaysOnVPN()
|
stringResource(id = R.string.tunnel_on_wifi),
|
||||||
|
enabled =
|
||||||
|
!(uiState.settings.isAutoTunnelEnabled ||
|
||||||
|
uiState.settings.isAlwaysOnVpnEnabled),
|
||||||
|
checked = uiState.settings.isTunnelOnWifiEnabled,
|
||||||
|
padding = screenPadding,
|
||||||
|
onCheckChanged = { viewModel.onToggleTunnelOnWifi() },
|
||||||
|
modifier =
|
||||||
|
if (uiState.settings.isAutoTunnelEnabled) Modifier
|
||||||
|
else
|
||||||
|
Modifier.focusRequester(focusRequester).focusProperties {
|
||||||
|
down = focusRequester2
|
||||||
|
},
|
||||||
|
)
|
||||||
|
AnimatedVisibility(visible = uiState.settings.isTunnelOnWifiEnabled) {
|
||||||
|
Column {
|
||||||
|
FlowRow(
|
||||||
|
modifier = Modifier.padding(screenPadding).fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(5.dp),
|
||||||
|
) {
|
||||||
|
uiState.settings.trustedNetworkSSIDs.forEach { ssid ->
|
||||||
|
ClickableIconButton(
|
||||||
|
onClick = {
|
||||||
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
|
viewModel.onDeleteTrustedSSID(ssid)
|
||||||
|
focusRequester2.requestFocus()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onIconClick = { viewModel.onDeleteTrustedSSID(ssid) },
|
||||||
|
text = ssid,
|
||||||
|
icon = Icons.Filled.Close,
|
||||||
|
enabled =
|
||||||
|
!(uiState.settings.isAutoTunnelEnabled ||
|
||||||
|
uiState.settings.isAlwaysOnVpnEnabled),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (uiState.settings.trustedNetworkSSIDs.isEmpty()) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.none),
|
||||||
|
fontStyle = FontStyle.Italic,
|
||||||
|
color = Color.Gray,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
OutlinedTextField(
|
||||||
|
enabled =
|
||||||
|
!(uiState.settings.isAutoTunnelEnabled ||
|
||||||
|
uiState.settings.isAlwaysOnVpnEnabled),
|
||||||
|
value = currentText,
|
||||||
|
onValueChange = { currentText = it },
|
||||||
|
label = { Text(stringResource(R.string.add_trusted_ssid)) },
|
||||||
|
modifier =
|
||||||
|
Modifier.padding(
|
||||||
|
start = screenPadding,
|
||||||
|
top = 5.dp,
|
||||||
|
bottom = 10.dp,
|
||||||
|
)
|
||||||
|
.focusRequester(focusRequester2),
|
||||||
|
maxLines = 1,
|
||||||
|
keyboardOptions =
|
||||||
|
KeyboardOptions(
|
||||||
|
capitalization = KeyboardCapitalization.None,
|
||||||
|
imeAction = ImeAction.Done,
|
||||||
|
),
|
||||||
|
keyboardActions = KeyboardActions(onDone = { saveTrustedSSID() }),
|
||||||
|
trailingIcon = {
|
||||||
|
if (currentText != "") {
|
||||||
|
IconButton(onClick = { saveTrustedSSID() }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Add,
|
||||||
|
contentDescription =
|
||||||
|
if (currentText == "") {
|
||||||
|
stringResource(
|
||||||
|
id =
|
||||||
|
R.string
|
||||||
|
.trusted_ssid_empty_description,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
stringResource(
|
||||||
|
id =
|
||||||
|
R.string
|
||||||
|
.trusted_ssid_value_description,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
ConfigurationToggle(
|
||||||
|
stringResource(R.string.tunnel_mobile_data),
|
||||||
|
enabled =
|
||||||
|
!(uiState.settings.isAutoTunnelEnabled ||
|
||||||
|
uiState.settings.isAlwaysOnVpnEnabled),
|
||||||
|
checked = uiState.settings.isTunnelOnMobileDataEnabled,
|
||||||
|
padding = screenPadding,
|
||||||
|
onCheckChanged = { viewModel.onToggleTunnelOnMobileData() },
|
||||||
|
)
|
||||||
|
ConfigurationToggle(
|
||||||
|
stringResource(id = R.string.tunnel_on_ethernet),
|
||||||
|
enabled =
|
||||||
|
!(uiState.settings.isAutoTunnelEnabled ||
|
||||||
|
uiState.settings.isAlwaysOnVpnEnabled),
|
||||||
|
checked = uiState.settings.isTunnelOnEthernetEnabled,
|
||||||
|
padding = screenPadding,
|
||||||
|
onCheckChanged = { viewModel.onToggleTunnelOnEthernet() },
|
||||||
|
)
|
||||||
|
ConfigurationToggle(
|
||||||
|
stringResource(R.string.battery_saver),
|
||||||
|
enabled =
|
||||||
|
!(uiState.settings.isAutoTunnelEnabled ||
|
||||||
|
uiState.settings.isAlwaysOnVpnEnabled),
|
||||||
|
checked = uiState.settings.isBatterySaverEnabled,
|
||||||
|
padding = screenPadding,
|
||||||
|
onCheckChanged = { viewModel.onToggleBatterySaver() },
|
||||||
)
|
)
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier
|
modifier =
|
||||||
.fillMaxSize()
|
(if (!uiState.settings.isAutoTunnelEnabled) Modifier
|
||||||
.padding(top = 5.dp),
|
else
|
||||||
horizontalArrangement = Arrangement.Center
|
Modifier.focusRequester(
|
||||||
|
focusRequester,
|
||||||
|
))
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(top = 5.dp),
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
) {
|
) {
|
||||||
TextButton(
|
TextButton(
|
||||||
enabled = !didExportFiles,
|
enabled = !uiState.settings.isAlwaysOnVpnEnabled,
|
||||||
onClick = {
|
onClick = {
|
||||||
showAuthPrompt = true
|
if (
|
||||||
}) {
|
uiState.settings.isTunnelOnWifiEnabled &&
|
||||||
Text("Export configs")
|
!uiState.settings.isAutoTunnelEnabled
|
||||||
|
) {
|
||||||
|
when (false) {
|
||||||
|
isBackgroundLocationGranted ->
|
||||||
|
showSnackbarMessage(
|
||||||
|
Event.Error.BackgroundLocationRequired.message
|
||||||
|
)
|
||||||
|
fineLocationState.status.isGranted ->
|
||||||
|
showSnackbarMessage(
|
||||||
|
Event.Error.PreciseLocationRequired.message
|
||||||
|
)
|
||||||
|
viewModel.isLocationEnabled(context) ->
|
||||||
|
showLocationServicesAlertDialog = true
|
||||||
|
else -> {
|
||||||
|
handleAutoTunnelToggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
handleAutoTunnelToggle()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
val autoTunnelButtonText =
|
||||||
|
if (uiState.settings.isAutoTunnelEnabled) {
|
||||||
|
stringResource(R.string.disable_auto_tunnel)
|
||||||
|
} else {
|
||||||
|
stringResource(id = R.string.enable_auto_tunnel)
|
||||||
|
}
|
||||||
|
Text(autoTunnelButtonText)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if (WgQuickBackend.hasKernelSupport()) {
|
||||||
if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
Surface(
|
||||||
Spacer(modifier = Modifier.weight(.17f))
|
tonalElevation = 2.dp,
|
||||||
|
shadowElevation = 2.dp,
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
modifier = Modifier.fillMaxWidth(fillMaxWidth).padding(vertical = 10.dp),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.Start,
|
||||||
|
verticalArrangement = Arrangement.Top,
|
||||||
|
modifier = Modifier.padding(15.dp),
|
||||||
|
) {
|
||||||
|
SectionTitle(
|
||||||
|
title = stringResource(id = R.string.kernel),
|
||||||
|
padding = screenPadding,
|
||||||
|
)
|
||||||
|
ConfigurationToggle(
|
||||||
|
stringResource(R.string.use_kernel),
|
||||||
|
enabled =
|
||||||
|
!(uiState.settings.isAutoTunnelEnabled ||
|
||||||
|
uiState.settings.isAlwaysOnVpnEnabled ||
|
||||||
|
(uiState.vpnState.status == Tunnel.State.UP)),
|
||||||
|
checked = uiState.settings.isKernelEnabled,
|
||||||
|
padding = screenPadding,
|
||||||
|
onCheckChanged = {
|
||||||
|
viewModel.onToggleKernelMode().let {
|
||||||
|
when (it) {
|
||||||
|
is Result.Error -> showSnackbarMessage(it.error.message)
|
||||||
|
is Result.Success -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
|
Surface(
|
||||||
|
tonalElevation = 2.dp,
|
||||||
|
shadowElevation = 2.dp,
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
modifier =
|
||||||
|
Modifier.fillMaxWidth(fillMaxWidth)
|
||||||
|
.padding(vertical = 10.dp)
|
||||||
|
.padding(bottom = 140.dp),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.Start,
|
||||||
|
verticalArrangement = Arrangement.Top,
|
||||||
|
modifier = Modifier.padding(15.dp),
|
||||||
|
) {
|
||||||
|
SectionTitle(
|
||||||
|
title = stringResource(id = R.string.other),
|
||||||
|
padding = screenPadding,
|
||||||
|
)
|
||||||
|
ConfigurationToggle(
|
||||||
|
stringResource(R.string.always_on_vpn_support),
|
||||||
|
enabled = !uiState.settings.isAutoTunnelEnabled,
|
||||||
|
checked = uiState.settings.isAlwaysOnVpnEnabled,
|
||||||
|
padding = screenPadding,
|
||||||
|
onCheckChanged = { viewModel.onToggleAlwaysOnVPN() },
|
||||||
|
)
|
||||||
|
ConfigurationToggle(
|
||||||
|
stringResource(R.string.enabled_app_shortcuts),
|
||||||
|
enabled = true,
|
||||||
|
checked = uiState.settings.isShortcutsEnabled,
|
||||||
|
padding = screenPadding,
|
||||||
|
onCheckChanged = { viewModel.onToggleShortcutsEnabled() },
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxSize().padding(top = 5.dp),
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
TextButton(
|
||||||
|
enabled = !didExportFiles,
|
||||||
|
onClick = { showAuthPrompt = true },
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.export_configs))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
|
Spacer(modifier = Modifier.weight(.17f))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.settings
|
||||||
|
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
|
||||||
|
|
||||||
|
data class SettingsUiState(
|
||||||
|
val settings: Settings = Settings(),
|
||||||
|
val tunnels: List<TunnelConfig> = emptyList(),
|
||||||
|
val vpnState: VpnState = VpnState(),
|
||||||
|
val isLocationDisclosureShown: Boolean = true,
|
||||||
|
val isBatteryOptimizeDisableShown: Boolean = false,
|
||||||
|
val loading: Boolean = true
|
||||||
|
)
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user