mirror of
https://github.com/wgtunnel/android.git
synced 2026-06-02 08:33:40 +02:00
Compare commits
88 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a569974beb | |||
| 87bc89b6f1 | |||
| c343220e96 | |||
| 5447ec73f7 | |||
| a2b8eb5b0b | |||
| e37777e662 | |||
| ee3fcabcf1 | |||
| 2a8895ffbc | |||
| b1fdb5b9b2 | |||
| 1d644748e5 | |||
| 61989b596f | |||
| 5946d7c10d | |||
| 4fc8ffbcbb | |||
| c0cff297b2 | |||
| 7ca0db3a40 | |||
| ee8db0a859 | |||
| c8205c4c59 | |||
| 3247e94358 | |||
| 2690ce29e1 | |||
| 500b85f687 | |||
| 84b2b75271 | |||
| 0197198f7b | |||
| 0b271778c9 | |||
| 6427b2f832 | |||
| 097097f620 | |||
| 20dfaed8de | |||
| 07aa37fc2a | |||
| 091cd2e028 | |||
| 7efa6d0bf4 | |||
| e31fb01410 | |||
| 76674323e7 | |||
| f1fc9ca6f7 | |||
| cb301e74eb | |||
| 8141fe19be | |||
| 0fdb3d0b31 | |||
| d9f3a21cc3 | |||
| fec84bc6ac | |||
| d6ee36edc0 | |||
| e3fcf712d5 | |||
| 5a15776bb3 | |||
| 3339448424 | |||
| 7ec294b789 | |||
| e59c72788d | |||
| 62435d549c | |||
| 96800037d1 | |||
| f5b3bb1cb7 | |||
| 654b4a4719 | |||
| a19b5ce22a | |||
| 86f592255c | |||
| 4a5dd76b5b | |||
| 1139d17f13 | |||
| 36855319a2 | |||
| 61e3751321 | |||
| dd16bd977f | |||
| aeb4a13389 | |||
| f0ec661223 | |||
| ffa7a207fb | |||
| 515e91d191 | |||
| 16e65aec9f | |||
| ff87bee3b4 | |||
| 9739d35eda | |||
| 11ad494fbb | |||
| 90b006acc5 | |||
| eb7b39c379 | |||
| 0a17593310 | |||
| c0e58125dd | |||
| 3791261f91 | |||
| d1e61be3ae | |||
| afd4fb127f | |||
| e0cce8fba4 | |||
| b70ecbdfff | |||
| 513d08998b | |||
| 79583e0e61 | |||
| 75790ec6d5 | |||
| a1941b7229 | |||
| 37bae82700 | |||
| 77cd328a71 | |||
| 5a1430706b | |||
| 321730536d | |||
| 2912238f27 | |||
| bc7daacd13 | |||
| c8f2cfd758 | |||
| ca3f3fd439 | |||
| 235170508b | |||
| 11aea3f1c4 | |||
| 7fbc51af4c | |||
| 1714618f0c | |||
| 7cb798a111 |
@@ -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,124 @@
|
||||
# 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@v2
|
||||
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: Install apksigner
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y apksigner
|
||||
|
||||
- name: Get checksum
|
||||
id: checksum
|
||||
run: echo "checksum=$(apksigner verify -print-certs ${{ steps.apk-path.outputs.path }} | grep -Po "(?<=SHA-256 digest:) .*" | tr -d "[:blank:]")" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Append checksum
|
||||
id: append_checksum
|
||||
uses: softprops/action-gh-release@v2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
body: |
|
||||
SHA256 fingerprint:
|
||||
```${{ steps.checksum.outputs.checksum }}```
|
||||
tag_name: ${{ github.ref_name }}
|
||||
name: ${{ github.ref_name }}
|
||||
draft: false
|
||||
prerelease: true
|
||||
append_body: true
|
||||
|
||||
- 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,130 @@
|
||||
# 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@v2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
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: Install apksigner
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y apksigner
|
||||
|
||||
- name: Get checksum
|
||||
id: checksum
|
||||
run: echo "checksum=$(apksigner verify -print-certs ${{ steps.apk-path.outputs.path }} | grep -Po "(?<=SHA-256 digest:) .*" | tr -d "[:blank:]")" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Append checksum
|
||||
id: append_checksum
|
||||
uses: softprops/action-gh-release@v2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
body: |
|
||||
SHA256 fingerprint:
|
||||
```${{ steps.checksum.outputs.checksum }}```
|
||||
tag_name: ${{ github.ref_name }}
|
||||
name: ${{ github.ref_name }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
append_body: true
|
||||
|
||||
- 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/release/output.json
|
||||
.idea/codeStyles/
|
||||
# where we keep our signing secrets locally
|
||||
app/signing.properties
|
||||
|
||||
@@ -2,71 +2,84 @@
|
||||
WG Tunnel
|
||||
</h1>
|
||||
|
||||
<span align="center">
|
||||
<div align="center">
|
||||
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://discord.gg/rbRRNh6H7V)
|
||||
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span align="center">
|
||||
<div align="center">
|
||||
|
||||
|
||||
[](https://play.google.com/store/apps/details?id=com.zaneschepke.wireguardautotunnel)
|
||||
[](https://www.amazon.com/gp/product/B0CFGGL7WK)
|
||||
|
||||
</span>
|
||||
|
||||
<span align="center">
|
||||
|
||||
[](https://ko-fi.com/N4N8NMJN2)
|
||||
|
||||
</span>
|
||||
[](https://f-droid.org/packages/com.zaneschepke.wireguardautotunnel/)
|
||||
|
||||
|
||||
<span align="left">
|
||||
</div>
|
||||
|
||||
This is an alternative Android Application for [WireGuard](https://www.wireguard.com/) with added features. Built using the [wireguard-android](https://github.com/WireGuard/wireguard-android) library and [Jetpack Compose](https://developer.android.com/jetpack/compose), this application was inspired by the official [WireGuard Android](https://github.com/WireGuard/wireguard-android) app.
|
||||
|
||||
</span>
|
||||
<div align="left">
|
||||
|
||||
<span align="center">
|
||||
This is an alternative Android Application for [WireGuard](https://www.wireguard.com/) with added
|
||||
features. Built using the [wireguard-android](https://github.com/WireGuard/wireguard-android)
|
||||
library and [Jetpack Compose](https://developer.android.com/jetpack/compose), this application was
|
||||
inspired by the official [WireGuard Android](https://github.com/WireGuard/wireguard-android) app.
|
||||
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
|
||||
## Screenshots
|
||||
|
||||
<p float="center">
|
||||
<img label="Main" style="padding-right:25px" src="asset/main_screen.png" width="200" />
|
||||
<img label="Config" style="padding-left:25px" src="./asset/config_screen.png" width="200" />
|
||||
<img label="Settings" style="padding-left:25px" src="asset/settings_screen.png" width="200" />
|
||||
<img label="Support" style="padding-left:25px" src="asset/support_screen.png" width="200" />
|
||||
<img label="Main" style="padding-right:25px" src="fastlane/metadata/android/en-US/images/phoneScreenshots/main_screen.png" width="200" />
|
||||
<img label="Config" style="padding-left:25px" src="fastlane/metadata/android/en-US/images/phoneScreenshots/config_screen.png" width="200" />
|
||||
<img label="Settings" style="padding-left:25px" src="fastlane/metadata/android/en-US/images/phoneScreenshots/settings_screen.png" width="200" />
|
||||
<img label="Support" style="padding-left:25px" src="fastlane/metadata/android/en-US/images/phoneScreenshots/support_screen.png" width="200" />
|
||||
</p>
|
||||
|
||||
<span align="left">
|
||||
<div align="left">
|
||||
|
||||
## Inspiration
|
||||
|
||||
The inspiration for this app came from the inconvenience of constantly having to turn VPN off and on while on different networks. With there being no free solution to this problem, this app was created to meet that need.
|
||||
The original inspiration for this app came from the inconvenience of having to manually turn VPN off
|
||||
and on while on different networks. This app was created to offer a free solution to this problem.
|
||||
|
||||
## Features
|
||||
|
||||
* Add tunnels via .conf file
|
||||
* Auto connect to VPN based on Wi-Fi SSID
|
||||
* Add tunnels via .conf file, zip, manual entry, or QR code
|
||||
* Auto connect to tunnels based on Wi-Fi SSID, ethernet, or mobile data
|
||||
* Split tunneling by application with search
|
||||
* Always-on VPN for Android support
|
||||
* Quick tile support for vpn toggling
|
||||
* Dynamic shortcuts support for automation integration
|
||||
* Configurable Trusted Network list
|
||||
* Optional auto connect on mobile data
|
||||
* Automatic service restart after reboot
|
||||
* Service will stay running in background after app has been closed
|
||||
* WireGuard support for kernel and userspace modes
|
||||
* Always-On VPN support
|
||||
* Export tunnels to zip
|
||||
* Quick tile support for tunnel toggling, auto-tunneling
|
||||
* Static shortcuts support for tunnel toggling, auto-tunneling
|
||||
* Intent automation support for all tunnels
|
||||
* Automatic auto-tunneling service restart after reboot
|
||||
* Automatic tunnel restart after reboot
|
||||
* Battery preservation measures
|
||||
* Restart tunnel on ping failure (beta)
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
```
|
||||
$ git clone https://github.com/zaneschepke/wgtunnel
|
||||
$ cd wgtunnel
|
||||
$ ./gradlew assembleRelease
|
||||
```
|
||||
|
||||
And then build the app:
|
||||
|
||||
```
|
||||
$ ./gradlew assembleDebug
|
||||
```
|
||||
|
||||
</span>
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please report security issues to `support@zaneschepke.com`
|
||||
+119
-56
@@ -1,91 +1,146 @@
|
||||
import java.util.Properties
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.hilt.android)
|
||||
id("org.jetbrains.kotlin.plugin.serialization")
|
||||
alias(libs.plugins.kotlinxSerialization)
|
||||
alias(libs.plugins.ksp)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.zaneschepke.wireguardautotunnel"
|
||||
compileSdk = 34
|
||||
namespace = Constants.APP_ID
|
||||
compileSdk = Constants.TARGET_SDK
|
||||
|
||||
val versionMajor = 2
|
||||
val versionMinor = 5
|
||||
val versionPatch = 0
|
||||
val versionBuild = 0
|
||||
androidResources {
|
||||
generateLocaleConfig = true
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.zaneschepke.wireguardautotunnel"
|
||||
minSdk = 26
|
||||
targetSdk = 34
|
||||
versionCode = versionMajor * 10000 + versionMinor * 1000 + versionPatch * 100 + versionBuild
|
||||
versionName = "${versionMajor}.${versionMinor}.${versionPatch}"
|
||||
applicationId = Constants.APP_ID
|
||||
minSdk = Constants.MIN_SDK
|
||||
targetSdk = Constants.TARGET_SDK
|
||||
versionCode = Constants.VERSION_CODE
|
||||
versionName = Constants.VERSION_NAME
|
||||
|
||||
multiDexEnabled = true
|
||||
ksp { arg("room.schemaLocation", "$projectDir/schemas") }
|
||||
|
||||
resourceConfigurations.addAll(listOf("en"))
|
||||
sourceSets {
|
||||
getByName("debug").assets.srcDirs(files("$projectDir/schemas")) // Room
|
||||
}
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
useSupportLibrary = true
|
||||
vectorDrawables { 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 {
|
||||
// don't strip
|
||||
packaging.jniLibs.keepDebugSymbols.addAll(
|
||||
listOf("libwg-go.so", "libwg-quick.so", "libwg.so"),
|
||||
)
|
||||
|
||||
applicationVariants.all {
|
||||
val variant = this
|
||||
variant.outputs
|
||||
.map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl }
|
||||
.forEach { output ->
|
||||
val outputFileName =
|
||||
"${Constants.APP_NAME}-${variant.flavorName}-${variant.buildType.name}-${variant.versionName}.apk"
|
||||
output.outputFileName = outputFileName
|
||||
}
|
||||
}
|
||||
release {
|
||||
isDebuggable = false
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
"proguard-rules.pro",
|
||||
)
|
||||
signingConfig = signingConfigs.getByName(Constants.RELEASE)
|
||||
}
|
||||
debug {
|
||||
isDebuggable = true
|
||||
}
|
||||
debug { isDebuggable = true }
|
||||
}
|
||||
flavorDimensions.add("type")
|
||||
flavorDimensions.add(Constants.TYPE)
|
||||
productFlavors {
|
||||
create("fdroid") {
|
||||
dimension = "type"
|
||||
dimension = Constants.TYPE
|
||||
proguardFile("fdroid-rules.pro")
|
||||
}
|
||||
create("general") {
|
||||
dimension = "type"
|
||||
if (BuildHelper.isReleaseBuild(gradle) && BuildHelper.isGeneralFlavor(gradle))
|
||||
{
|
||||
apply(plugin = "com.google.gms.google-services")
|
||||
apply(plugin = "com.google.firebase.crashlytics")
|
||||
dimension = Constants.TYPE
|
||||
if (BuildHelper.isReleaseBuild(gradle) && BuildHelper.isGeneralFlavor(gradle)) {
|
||||
//any plugins general specific
|
||||
}
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
kotlinOptions { jvmTarget = Constants.JVM_TARGET }
|
||||
buildFeatures {
|
||||
compose = true
|
||||
buildConfig = true
|
||||
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = 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
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation(project(":logcatter"))
|
||||
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
// helpers for implementing LifecycleOwner in a Service
|
||||
implementation(libs.androidx.lifecycle.service)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.compose.ui)
|
||||
@@ -94,21 +149,25 @@ dependencies {
|
||||
implementation(libs.androidx.material3)
|
||||
implementation(libs.androidx.appcompat)
|
||||
|
||||
//test
|
||||
// test
|
||||
testImplementation(libs.junit)
|
||||
testImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||
androidTestImplementation(libs.androidx.compose.ui.test)
|
||||
androidTestImplementation(libs.androidx.room.testing)
|
||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||
debugImplementation(libs.androidx.compose.manifest)
|
||||
|
||||
//wg
|
||||
// get tunnel lib from github packages or mavenLocal
|
||||
implementation(libs.tunnel)
|
||||
coreLibraryDesugaring(libs.desugar.jdk.libs)
|
||||
|
||||
//logging
|
||||
// logging
|
||||
implementation(libs.timber)
|
||||
|
||||
|
||||
// compose navigation
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.androidx.hilt.navigation.compose)
|
||||
@@ -117,32 +176,36 @@ dependencies {
|
||||
implementation(libs.hilt.android)
|
||||
ksp(libs.hilt.android.compiler)
|
||||
|
||||
//accompanist
|
||||
implementation(libs.accompanist.systemuicontroller)
|
||||
// accompanist
|
||||
implementation(libs.accompanist.permissions)
|
||||
implementation(libs.accompanist.flowlayout)
|
||||
implementation(libs.accompanist.navigation.animation)
|
||||
implementation(libs.accompanist.drawablepainter)
|
||||
|
||||
//room
|
||||
// storage
|
||||
implementation(libs.androidx.room.runtime)
|
||||
ksp(libs.androidx.room.compiler)
|
||||
implementation(libs.androidx.room.ktx)
|
||||
implementation(libs.androidx.datastore.preferences)
|
||||
|
||||
//lifecycle
|
||||
// lifecycle
|
||||
implementation(libs.lifecycle.runtime.compose)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.lifecycle.process)
|
||||
|
||||
//icons
|
||||
// icons
|
||||
implementation(libs.material.icons.extended)
|
||||
//serialization
|
||||
// serialization
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
|
||||
//firebase crashlytics
|
||||
generalImplementation(platform(libs.firebase.bom))
|
||||
generalImplementation(libs.google.firebase.crashlytics.ktx)
|
||||
generalImplementation(libs.google.firebase.analytics.ktx)
|
||||
|
||||
//barcode scanning
|
||||
// barcode scanning
|
||||
implementation(libs.zxing.android.embedded)
|
||||
implementation(libs.zxing.core)
|
||||
}
|
||||
|
||||
// bio
|
||||
implementation(libs.androidx.biometric.ktx)
|
||||
implementation(libs.pin.lock.compose)
|
||||
|
||||
// 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>;
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
{
|
||||
"project_info": {
|
||||
"project_number": "328300975830",
|
||||
"project_id": "wireguard-auto-tunnel",
|
||||
"storage_bucket": "wireguard-auto-tunnel.appspot.com"
|
||||
},
|
||||
"client": [
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:328300975830:android:82cd774598ccb7234b1b77",
|
||||
"android_client_info": {
|
||||
"package_name": "com.zaneschepke.wireguardautotunnel"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "328300975830-m72lc3hr69ddhdqh9ngr27rvc8o0jb2d.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyBsSMY0LlckizXDnuYBy7nXWGSdl8zZedI"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": [
|
||||
{
|
||||
"client_id": "328300975830-m72lc3hr69ddhdqh9ngr27rvc8o0jb2d.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"configuration_version": "1"
|
||||
}
|
||||
Vendored
+6
-1
@@ -18,4 +18,9 @@
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
#-renamesourcefileattribute SourceFile
|
||||
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
|
||||
<fields>;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 1,
|
||||
"identityHash": "ba86153e6fb0b823197b987239b03e64",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "Settings",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `default_tunnel` TEXT, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAutoTunnelEnabled",
|
||||
"columnName": "is_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnMobileDataEnabled",
|
||||
"columnName": "is_tunnel_on_mobile_data_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "trustedNetworkSSIDs",
|
||||
"columnName": "trusted_network_ssids",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "defaultTunnel",
|
||||
"columnName": "default_tunnel",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAlwaysOnVpnEnabled",
|
||||
"columnName": "is_always_on_vpn_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnEthernetEnabled",
|
||||
"columnName": "is_tunnel_on_ethernet_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "TunnelConfig",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "wgQuick",
|
||||
"columnName": "wg_quick",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_TunnelConfig_name",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"name"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ba86153e6fb0b823197b987239b03e64')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 2,
|
||||
"identityHash": "65b1c9efff61712231fa64d1f19f3915",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "Settings",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `default_tunnel` TEXT, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_battery_saver_enabled` INTEGER NOT NULL DEFAULT false)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAutoTunnelEnabled",
|
||||
"columnName": "is_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnMobileDataEnabled",
|
||||
"columnName": "is_tunnel_on_mobile_data_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "trustedNetworkSSIDs",
|
||||
"columnName": "trusted_network_ssids",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "defaultTunnel",
|
||||
"columnName": "default_tunnel",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAlwaysOnVpnEnabled",
|
||||
"columnName": "is_always_on_vpn_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnEthernetEnabled",
|
||||
"columnName": "is_tunnel_on_ethernet_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isShortcutsEnabled",
|
||||
"columnName": "is_shortcuts_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isBatterySaverEnabled",
|
||||
"columnName": "is_battery_saver_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "TunnelConfig",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "wgQuick",
|
||||
"columnName": "wg_quick",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_TunnelConfig_name",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"name"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '65b1c9efff61712231fa64d1f19f3915')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 3,
|
||||
"identityHash": "6b30daba29bb95f8ddc4d26206329d4f",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "Settings",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `default_tunnel` TEXT, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_battery_saver_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAutoTunnelEnabled",
|
||||
"columnName": "is_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnMobileDataEnabled",
|
||||
"columnName": "is_tunnel_on_mobile_data_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "trustedNetworkSSIDs",
|
||||
"columnName": "trusted_network_ssids",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "defaultTunnel",
|
||||
"columnName": "default_tunnel",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAlwaysOnVpnEnabled",
|
||||
"columnName": "is_always_on_vpn_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnEthernetEnabled",
|
||||
"columnName": "is_tunnel_on_ethernet_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isShortcutsEnabled",
|
||||
"columnName": "is_shortcuts_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isBatterySaverEnabled",
|
||||
"columnName": "is_battery_saver_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnWifiEnabled",
|
||||
"columnName": "is_tunnel_on_wifi_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "TunnelConfig",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "wgQuick",
|
||||
"columnName": "wg_quick",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_TunnelConfig_name",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"name"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6b30daba29bb95f8ddc4d26206329d4f')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 4,
|
||||
"identityHash": "aee55639422df8dadfe74c3bad204477",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "Settings",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `default_tunnel` TEXT, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_battery_saver_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAutoTunnelEnabled",
|
||||
"columnName": "is_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnMobileDataEnabled",
|
||||
"columnName": "is_tunnel_on_mobile_data_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "trustedNetworkSSIDs",
|
||||
"columnName": "trusted_network_ssids",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "defaultTunnel",
|
||||
"columnName": "default_tunnel",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAlwaysOnVpnEnabled",
|
||||
"columnName": "is_always_on_vpn_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnEthernetEnabled",
|
||||
"columnName": "is_tunnel_on_ethernet_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isShortcutsEnabled",
|
||||
"columnName": "is_shortcuts_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isBatterySaverEnabled",
|
||||
"columnName": "is_battery_saver_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnWifiEnabled",
|
||||
"columnName": "is_tunnel_on_wifi_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isKernelEnabled",
|
||||
"columnName": "is_kernel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isRestoreOnBootEnabled",
|
||||
"columnName": "is_restore_on_boot_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isMultiTunnelEnabled",
|
||||
"columnName": "is_multi_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "TunnelConfig",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "wgQuick",
|
||||
"columnName": "wg_quick",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_TunnelConfig_name",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"name"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'aee55639422df8dadfe74c3bad204477')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 5,
|
||||
"identityHash": "bc15003a44746e18b9c260ec49737089",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "Settings",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `default_tunnel` TEXT, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_battery_saver_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_auto_tunnel_paused` INTEGER NOT NULL DEFAULT false)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAutoTunnelEnabled",
|
||||
"columnName": "is_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnMobileDataEnabled",
|
||||
"columnName": "is_tunnel_on_mobile_data_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "trustedNetworkSSIDs",
|
||||
"columnName": "trusted_network_ssids",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "defaultTunnel",
|
||||
"columnName": "default_tunnel",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAlwaysOnVpnEnabled",
|
||||
"columnName": "is_always_on_vpn_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnEthernetEnabled",
|
||||
"columnName": "is_tunnel_on_ethernet_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isShortcutsEnabled",
|
||||
"columnName": "is_shortcuts_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isBatterySaverEnabled",
|
||||
"columnName": "is_battery_saver_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnWifiEnabled",
|
||||
"columnName": "is_tunnel_on_wifi_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isKernelEnabled",
|
||||
"columnName": "is_kernel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isRestoreOnBootEnabled",
|
||||
"columnName": "is_restore_on_boot_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isMultiTunnelEnabled",
|
||||
"columnName": "is_multi_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAutoTunnelPaused",
|
||||
"columnName": "is_auto_tunnel_paused",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "TunnelConfig",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "wgQuick",
|
||||
"columnName": "wg_quick",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_TunnelConfig_name",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"name"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'bc15003a44746e18b9c260ec49737089')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 6,
|
||||
"identityHash": "625820076477aca948536f7bccccc7ca",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "Settings",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `default_tunnel` TEXT, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_battery_saver_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_auto_tunnel_paused` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAutoTunnelEnabled",
|
||||
"columnName": "is_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnMobileDataEnabled",
|
||||
"columnName": "is_tunnel_on_mobile_data_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "trustedNetworkSSIDs",
|
||||
"columnName": "trusted_network_ssids",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "defaultTunnel",
|
||||
"columnName": "default_tunnel",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAlwaysOnVpnEnabled",
|
||||
"columnName": "is_always_on_vpn_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnEthernetEnabled",
|
||||
"columnName": "is_tunnel_on_ethernet_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isShortcutsEnabled",
|
||||
"columnName": "is_shortcuts_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isBatterySaverEnabled",
|
||||
"columnName": "is_battery_saver_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnWifiEnabled",
|
||||
"columnName": "is_tunnel_on_wifi_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isKernelEnabled",
|
||||
"columnName": "is_kernel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isRestoreOnBootEnabled",
|
||||
"columnName": "is_restore_on_boot_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isMultiTunnelEnabled",
|
||||
"columnName": "is_multi_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAutoTunnelPaused",
|
||||
"columnName": "is_auto_tunnel_paused",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isPingEnabled",
|
||||
"columnName": "is_ping_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "TunnelConfig",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "wgQuick",
|
||||
"columnName": "wg_quick",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_TunnelConfig_name",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"name"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '625820076477aca948536f7bccccc7ca')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 7,
|
||||
"identityHash": "e65e4e7cf01f50fb03196d47b54288b1",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "Settings",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_auto_tunnel_paused` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAutoTunnelEnabled",
|
||||
"columnName": "is_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnMobileDataEnabled",
|
||||
"columnName": "is_tunnel_on_mobile_data_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "trustedNetworkSSIDs",
|
||||
"columnName": "trusted_network_ssids",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAlwaysOnVpnEnabled",
|
||||
"columnName": "is_always_on_vpn_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnEthernetEnabled",
|
||||
"columnName": "is_tunnel_on_ethernet_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isShortcutsEnabled",
|
||||
"columnName": "is_shortcuts_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnWifiEnabled",
|
||||
"columnName": "is_tunnel_on_wifi_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isKernelEnabled",
|
||||
"columnName": "is_kernel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isRestoreOnBootEnabled",
|
||||
"columnName": "is_restore_on_boot_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isMultiTunnelEnabled",
|
||||
"columnName": "is_multi_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAutoTunnelPaused",
|
||||
"columnName": "is_auto_tunnel_paused",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isPingEnabled",
|
||||
"columnName": "is_ping_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "TunnelConfig",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false, `is_primary_tunnel` INTEGER NOT NULL DEFAULT false)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "wgQuick",
|
||||
"columnName": "wg_quick",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "tunnelNetworks",
|
||||
"columnName": "tunnel_networks",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "''"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isMobileDataTunnel",
|
||||
"columnName": "is_mobile_data_tunnel",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isPrimaryTunnel",
|
||||
"columnName": "is_primary_tunnel",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_TunnelConfig_name",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"name"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e65e4e7cf01f50fb03196d47b54288b1')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
SIGNING_STORE_PASSWORD=
|
||||
SIGNING_KEY_ALIAS=
|
||||
SIGNING_KEY_PASSWORD=
|
||||
KEY_STORE_PATH=/
|
||||
+3
-5
@@ -1,13 +1,11 @@
|
||||
package com.zaneschepke.wireguardautotunnel
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
@@ -21,4 +19,4 @@ class ExampleInstrumentedTest {
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("com.zaneschepke.wireguardautotunnel", appContext.packageName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.zaneschepke.wireguardautotunnel
|
||||
|
||||
import androidx.room.testing.MigrationTestHelper
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
||||
import com.zaneschepke.wireguardautotunnel.data.Queries
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import java.io.IOException
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class MigrationTest {
|
||||
private val dbName = "migration-test"
|
||||
|
||||
@get:Rule
|
||||
val helper: MigrationTestHelper =
|
||||
MigrationTestHelper(
|
||||
InstrumentationRegistry.getInstrumentation(),
|
||||
AppDatabase::class.java,
|
||||
)
|
||||
|
||||
@Test
|
||||
@Throws(IOException::class)
|
||||
fun migrate6To7() {
|
||||
helper.createDatabase(dbName, 6).apply {
|
||||
// Database has schema version 1. Insert some data using SQL queries.
|
||||
// You can't use DAO classes because they expect the latest schema.
|
||||
execSQL(Queries.createDefaultSettings())
|
||||
execSQL(
|
||||
Queries.createTunnelConfig(),
|
||||
)
|
||||
// Prepare for the next version.
|
||||
close()
|
||||
}
|
||||
|
||||
// Re-open the database with version 2 and provide
|
||||
// MIGRATION_1_2 as the migration process.
|
||||
helper.runMigrationsAndValidate(dbName, 7, true)
|
||||
// MigrationTestHelper automatically verifies the schema changes,
|
||||
// but you need to validate that the data was migrated properly.
|
||||
}
|
||||
}
|
||||
@@ -1,124 +1,176 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32"
|
||||
tools:ignore="ScopedStorage" />
|
||||
<uses-permission
|
||||
android:name="android.permission.ACCESS_WIFI_STATE"
|
||||
android:maxSdkVersion="30"
|
||||
tools:ignore="LeanbackUsesWifi" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
||||
<!--foreground service exempt android 14-->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||
|
||||
<!--foreground service permissions-->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
<!--start service on boot permission-->
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<!--android tv support-->
|
||||
<uses-feature android:name="android.software.leanback"
|
||||
<uses-feature
|
||||
android:name="android.software.leanback"
|
||||
android:required="false" />
|
||||
<uses-feature android:name="android.hardware.touchscreen"
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.location.gps"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.screen.portrait"
|
||||
android:required="false" />
|
||||
android:name="android.hardware.screen.portrait"
|
||||
android:required="false" />
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
</intent>
|
||||
</queries>
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:name=".WireGuardAutoTunnel"
|
||||
android:allowBackup="true"
|
||||
android:banner="@drawable/ic_banner"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:banner="@mipmap/ic_banner"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.WireguardAutoTunnel"
|
||||
tools:targetApi="31">
|
||||
tools:targetApi="tiramisu">
|
||||
<activity
|
||||
android:name=".ui.MainActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.WireguardAutoTunnel">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.CaptureActivityPortrait"
|
||||
android:screenOrientation="fullSensor"
|
||||
android:stateNotNeeded="true"
|
||||
android:theme="@style/zxing_CaptureTheme"
|
||||
android:windowSoftInputMode="stateAlwaysHidden" />
|
||||
android:windowSoftInputMode="stateAlwaysHidden"
|
||||
tools:ignore="DiscouragedApi" />
|
||||
<activity
|
||||
android:name=".service.shortcut.ShortcutsActivity"
|
||||
android:enabled="true"
|
||||
android:exported="true"
|
||||
android:finishOnTaskLaunch="true"
|
||||
android:theme="@android:style/Theme.NoDisplay"
|
||||
android:name=".service.shortcut.ShortcutsActivity"/>
|
||||
android:theme="@android:style/Theme.NoDisplay" />
|
||||
|
||||
<service
|
||||
android:name=".service.foreground.ForegroundService"
|
||||
android:enabled="true"
|
||||
android:foregroundServiceType="remoteMessaging"
|
||||
android:exported="false">
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="systemExempted"
|
||||
tools:node="merge" />
|
||||
<service
|
||||
android:name=".service.tile.TunnelControlTile"
|
||||
android:exported="true"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="Tunnel control"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
<meta-data
|
||||
android:name="android.service.quicksettings.ACTIVE_TILE"
|
||||
android:value="true" />
|
||||
<meta-data
|
||||
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
|
||||
android:value="true" />
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service
|
||||
android:name=".service.tile.AutoTunnelControlTile"
|
||||
android:exported="true"
|
||||
android:name=".service.tile.TunnelControlTile"
|
||||
android:icon="@drawable/shield"
|
||||
android:label="WG Tunnel"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="Auto-tunnel"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
<meta-data android:name="android.service.quicksettings.ACTIVE_TILE"
|
||||
<meta-data
|
||||
android:name="android.service.quicksettings.ACTIVE_TILE"
|
||||
android:value="true" />
|
||||
<meta-data android:name="android.service.quicksettings.TOGGLEABLE_TILE"
|
||||
<meta-data
|
||||
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
|
||||
android:value="true" />
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service
|
||||
android:name=".service.foreground.WireGuardTunnelService"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="systemExempted"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||
android:persistent="true"
|
||||
android:foregroundServiceType="remoteMessaging"
|
||||
android:exported="false">
|
||||
tools:node="merge">
|
||||
<intent-filter>
|
||||
<action android:name="android.net.VpnService"/>
|
||||
<action android:name="android.net.VpnService" />
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
|
||||
android:value="true"/>
|
||||
<meta-data
|
||||
android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
|
||||
android:value="true" />
|
||||
</service>
|
||||
<service
|
||||
android:name=".service.foreground.WireGuardConnectivityWatcherService"
|
||||
android:enabled="true"
|
||||
android:stopWithTask="false"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="systemExempted"
|
||||
android:persistent="true"
|
||||
android:foregroundServiceType="location"
|
||||
android:permission=""
|
||||
android:stopWithTask="false"
|
||||
tools:node="merge" />
|
||||
|
||||
<receiver
|
||||
android:name=".receiver.BootReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false">
|
||||
</service>
|
||||
<receiver android:enabled="true" android:name=".receiver.BootReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="android.intent.action.ACTION_BOOT_COMPLETED" />
|
||||
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
|
||||
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver android:exported="false" android:name=".receiver.NotificationActionReceiver"/>
|
||||
<meta-data
|
||||
android:name="com.google.mlkit.vision.DEPENDENCIES"
|
||||
android:value="barcode_ui"/>
|
||||
<receiver
|
||||
android:name=".receiver.NotificationActionReceiver"
|
||||
android:exported="false" />
|
||||
</application>
|
||||
</manifest>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 16 KiB |
@@ -1,11 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel
|
||||
|
||||
object Constants {
|
||||
const val VPN_CONNECTIVITY_CHECK_INTERVAL = 3000L
|
||||
const val VPN_STATISTIC_CHECK_INTERVAL = 10000L
|
||||
const val SNACKBAR_DELAY = 3000L
|
||||
const val TOGGLE_TUNNEL_DELAY = 1000L
|
||||
const val FADE_IN_ANIMATION_DURATION = 1000
|
||||
const val SLIDE_IN_ANIMATION_DURATION = 500
|
||||
const val SLIDE_IN_TRANSITION_OFFSET = 1000
|
||||
}
|
||||
@@ -1,24 +1,46 @@
|
||||
package com.zaneschepke.wireguardautotunnel
|
||||
|
||||
import android.app.Application
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.service.quicksettings.TileService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelControlTile
|
||||
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
|
||||
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import timber.log.Timber
|
||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||
|
||||
@HiltAndroidApp
|
||||
class WireGuardAutoTunnel : Application() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
if(BuildConfig.DEBUG) {
|
||||
Timber.plant(Timber.DebugTree())
|
||||
}
|
||||
instance = this
|
||||
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) else Timber.plant(ReleaseTree())
|
||||
PinManager.initialize(this)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun isRunningOnAndroidTv(context : Context) : Boolean {
|
||||
return context.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
||||
lateinit var instance: WireGuardAutoTunnel
|
||||
private set
|
||||
|
||||
fun isRunningOnAndroidTv(): Boolean {
|
||||
return instance.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
||||
}
|
||||
|
||||
fun requestTunnelTileServiceStateUpdate(context: Context) {
|
||||
TileService.requestListeningState(
|
||||
context,
|
||||
ComponentName(instance, TunnelControlTile::class.java),
|
||||
)
|
||||
}
|
||||
|
||||
fun requestAutoTunnelTileServiceUpdate(context: Context) {
|
||||
TileService.requestListeningState(
|
||||
context,
|
||||
ComponentName(instance, AutoTunnelControlTile::class.java),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data
|
||||
|
||||
import androidx.room.AutoMigration
|
||||
import androidx.room.Database
|
||||
import androidx.room.DeleteColumn
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import androidx.room.migration.AutoMigrationSpec
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
|
||||
@Database(
|
||||
entities = [Settings::class, TunnelConfig::class],
|
||||
version = 7,
|
||||
autoMigrations =
|
||||
[
|
||||
AutoMigration(from = 1, to = 2),
|
||||
AutoMigration(from = 2, to = 3),
|
||||
AutoMigration(
|
||||
from = 3,
|
||||
to = 4,
|
||||
),
|
||||
AutoMigration(
|
||||
from = 4,
|
||||
to = 5,
|
||||
),
|
||||
AutoMigration(
|
||||
from = 5,
|
||||
to = 6,
|
||||
),
|
||||
AutoMigration(
|
||||
from = 6,
|
||||
to = 7,
|
||||
spec = RemoveLegacySettingColumnsMigration::class,
|
||||
),
|
||||
],
|
||||
exportSchema = true,
|
||||
)
|
||||
@TypeConverters(DatabaseListConverters::class)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun settingDao(): SettingsDao
|
||||
|
||||
abstract fun tunnelConfigDoa(): TunnelConfigDao
|
||||
}
|
||||
|
||||
@DeleteColumn(
|
||||
tableName = "Settings",
|
||||
columnName = "default_tunnel",
|
||||
)
|
||||
@DeleteColumn(
|
||||
tableName = "Settings",
|
||||
columnName = "is_battery_saver_enabled",
|
||||
)
|
||||
class RemoveLegacySettingColumnsMigration : AutoMigrationSpec
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data
|
||||
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import timber.log.Timber
|
||||
|
||||
class DatabaseCallback : RoomDatabase.Callback() {
|
||||
override fun onCreate(db: SupportSQLiteDatabase) = db.run {
|
||||
// Notice non-ui thread is here
|
||||
beginTransaction()
|
||||
try {
|
||||
execSQL(Queries.createDefaultSettings())
|
||||
Timber.i("Bootstrapping settings data")
|
||||
setTransactionSuccessful()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
} finally {
|
||||
endTransaction()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data
|
||||
|
||||
import androidx.room.TypeConverter
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
class DatabaseListConverters {
|
||||
@TypeConverter
|
||||
fun listToString(value: MutableList<String>): String {
|
||||
return Json.encodeToString(value)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun stringToList(value: String): MutableList<String> {
|
||||
if (value.isBlank() || value.isEmpty()) return mutableListOf()
|
||||
return try {
|
||||
Json.decodeFromString<MutableList<String>>(value)
|
||||
} catch (e: Exception) {
|
||||
val list = value.split(",").toMutableList()
|
||||
val json = listToString(list)
|
||||
Json.decodeFromString<MutableList<String>>(json)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data
|
||||
|
||||
object Queries {
|
||||
fun createDefaultSettings(): String {
|
||||
return """
|
||||
INSERT INTO Settings (is_tunnel_enabled,
|
||||
is_tunnel_on_mobile_data_enabled,
|
||||
trusted_network_ssids,
|
||||
is_always_on_vpn_enabled,
|
||||
is_tunnel_on_ethernet_enabled,
|
||||
is_shortcuts_enabled,
|
||||
is_tunnel_on_wifi_enabled,
|
||||
is_kernel_enabled,
|
||||
is_restore_on_boot_enabled,
|
||||
is_multi_tunnel_enabled)
|
||||
VALUES
|
||||
('false',
|
||||
'false',
|
||||
'sampleSSID1,sampleSSID2',
|
||||
'false',
|
||||
'false',
|
||||
'false',
|
||||
'false',
|
||||
'false',
|
||||
'false',
|
||||
'false')
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
fun createTunnelConfig(): String {
|
||||
return """
|
||||
INSERT INTO TunnelConfig (name, wg_quick) VALUES ('test', 'test')
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
+7
-5
@@ -1,16 +1,15 @@
|
||||
package com.zaneschepke.wireguardautotunnel.repository
|
||||
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.repository.model.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface SettingsDoa {
|
||||
|
||||
interface SettingsDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun save(t: Settings)
|
||||
|
||||
@@ -23,6 +22,9 @@ interface SettingsDoa {
|
||||
@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>>
|
||||
|
||||
@@ -31,4 +33,4 @@ interface SettingsDoa {
|
||||
|
||||
@Query("SELECT COUNT('id') FROM settings")
|
||||
suspend fun count(): Long
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
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 com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface TunnelConfigDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun save(t: TunnelConfig)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun saveAll(t: TunnelConfigs)
|
||||
|
||||
@Query("SELECT * FROM TunnelConfig WHERE id=:id")
|
||||
suspend fun getById(id: Long): TunnelConfig?
|
||||
|
||||
@Query("SELECT * FROM TunnelConfig")
|
||||
suspend fun getAll(): TunnelConfigs
|
||||
|
||||
@Delete
|
||||
suspend fun delete(t: TunnelConfig)
|
||||
|
||||
@Query("SELECT COUNT('id') FROM TunnelConfig")
|
||||
suspend fun count(): Long
|
||||
|
||||
@Query("SELECT * FROM TunnelConfig WHERE tunnel_networks LIKE '%' || :name || '%'")
|
||||
suspend fun findByTunnelNetworkName(name: String): TunnelConfigs
|
||||
|
||||
@Query("UPDATE TunnelConfig SET is_primary_tunnel = 0 WHERE is_primary_tunnel =1")
|
||||
fun resetPrimaryTunnel()
|
||||
|
||||
@Query("UPDATE TunnelConfig SET is_mobile_data_tunnel = 0 WHERE is_mobile_data_tunnel =1")
|
||||
fun resetMobileDataTunnel()
|
||||
|
||||
@Query("SELECT * FROM TUNNELCONFIG WHERE is_primary_tunnel=1")
|
||||
suspend fun findByPrimary(): TunnelConfigs
|
||||
|
||||
@Query("SELECT * FROM TUNNELCONFIG WHERE is_mobile_data_tunnel=1")
|
||||
suspend fun findByMobileDataTunnel(): TunnelConfigs
|
||||
|
||||
@Query("SELECT * FROM tunnelconfig")
|
||||
fun getAllFlow(): Flow<MutableList<TunnelConfig>>
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.datastore
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.intPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
|
||||
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")
|
||||
val TUNNEL_RUNNING_FROM_MANUAL_START =
|
||||
booleanPreferencesKey("TUNNEL_RUNNING_FROM_MANUAL_START")
|
||||
val ACTIVE_TUNNEL = intPreferencesKey("ACTIVE_TUNNEL")
|
||||
val CURRENT_SSID = stringPreferencesKey("CURRENT_SSID")
|
||||
}
|
||||
|
||||
// preferences
|
||||
private val preferencesKey = "preferences"
|
||||
private val Context.dataStore by
|
||||
preferencesDataStore(
|
||||
name = preferencesKey,
|
||||
)
|
||||
|
||||
suspend fun init() {
|
||||
try {
|
||||
context.dataStore.data.first()
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun <T> saveToDataStore(key: Preferences.Key<T>, value: T) {
|
||||
try {
|
||||
context.dataStore.edit { it[key] = value }
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun <T> getFromStoreFlow(key: Preferences.Key<T>) = context.dataStore.data.map { it[key] }
|
||||
|
||||
suspend fun <T> getFromStore(key: Preferences.Key<T>): T? {
|
||||
return try {
|
||||
context.dataStore.data.map { it[key] }.first()
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun <T> getFromStoreBlocking(key: Preferences.Key<T>) = runBlocking {
|
||||
context.dataStore.data.map { it[key] }.first()
|
||||
}
|
||||
|
||||
val preferencesFlow: Flow<Preferences?> = context.dataStore.data
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.model
|
||||
|
||||
data class GeneralState(
|
||||
val locationDisclosureShown: Boolean = LOCATION_DISCLOSURE_SHOWN_DEFAULT,
|
||||
val batteryOptimizationDisableShown: Boolean = BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT,
|
||||
val tunnelRunningFromManualStart: Boolean = TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT,
|
||||
val activeTunnelId: Int? = null
|
||||
) {
|
||||
companion object {
|
||||
const val LOCATION_DISCLOSURE_SHOWN_DEFAULT = false
|
||||
const val BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT = false
|
||||
const val TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT = false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
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") val isAutoTunnelEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "is_tunnel_on_mobile_data_enabled")
|
||||
val isTunnelOnMobileDataEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "trusted_network_ssids")
|
||||
val trustedNetworkSSIDs: MutableList<String> = mutableListOf(),
|
||||
@ColumnInfo(name = "is_always_on_vpn_enabled") val isAlwaysOnVpnEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "is_tunnel_on_ethernet_enabled")
|
||||
val isTunnelOnEthernetEnabled: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "is_shortcuts_enabled",
|
||||
defaultValue = "false",
|
||||
)
|
||||
val isShortcutsEnabled: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "is_tunnel_on_wifi_enabled",
|
||||
defaultValue = "false",
|
||||
)
|
||||
val isTunnelOnWifiEnabled: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "is_kernel_enabled",
|
||||
defaultValue = "false",
|
||||
)
|
||||
val isKernelEnabled: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "is_restore_on_boot_enabled",
|
||||
defaultValue = "false",
|
||||
)
|
||||
val isRestoreOnBootEnabled: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "is_multi_tunnel_enabled",
|
||||
defaultValue = "false",
|
||||
)
|
||||
val isMultiTunnelEnabled: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "is_auto_tunnel_paused",
|
||||
defaultValue = "false",
|
||||
)
|
||||
val isAutoTunnelPaused: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "is_ping_enabled",
|
||||
defaultValue = "false",
|
||||
)
|
||||
val isPingEnabled: Boolean = false,
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.model
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import com.wireguard.config.Config
|
||||
import java.io.InputStream
|
||||
|
||||
@Entity(indices = [Index(value = ["name"], unique = true)])
|
||||
data class TunnelConfig(
|
||||
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
||||
@ColumnInfo(name = "name") val name: String,
|
||||
@ColumnInfo(name = "wg_quick") val wgQuick: String,
|
||||
@ColumnInfo(
|
||||
name = "tunnel_networks",
|
||||
defaultValue = "",
|
||||
)
|
||||
val tunnelNetworks: MutableList<String> = mutableListOf(),
|
||||
@ColumnInfo(
|
||||
name = "is_mobile_data_tunnel",
|
||||
defaultValue = "false",
|
||||
)
|
||||
val isMobileDataTunnel: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "is_primary_tunnel",
|
||||
defaultValue = "false",
|
||||
)
|
||||
val isPrimaryTunnel: Boolean = false,
|
||||
) {
|
||||
companion object {
|
||||
fun configFromQuick(wgQuick: String): Config {
|
||||
val inputStream: InputStream = wgQuick.byteInputStream()
|
||||
val reader = inputStream.bufferedReader(Charsets.UTF_8)
|
||||
return Config.parse(reader)
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
|
||||
interface AppDataRepository {
|
||||
suspend fun getPrimaryOrFirstTunnel(): TunnelConfig?
|
||||
suspend fun getStartTunnelConfig(): TunnelConfig?
|
||||
|
||||
suspend fun toggleWatcherServicePause()
|
||||
|
||||
val settings: SettingsRepository
|
||||
val tunnels: TunnelConfigRepository
|
||||
val appState: AppStateRepository
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import javax.inject.Inject
|
||||
|
||||
class AppDataRoomRepository @Inject constructor(
|
||||
override val settings: SettingsRepository,
|
||||
override val tunnels: TunnelConfigRepository,
|
||||
override val appState: AppStateRepository
|
||||
) : AppDataRepository {
|
||||
override suspend fun getPrimaryOrFirstTunnel(): TunnelConfig? {
|
||||
return tunnels.findPrimary().firstOrNull() ?: tunnels.getAll().firstOrNull()
|
||||
}
|
||||
|
||||
override suspend fun getStartTunnelConfig(): TunnelConfig? {
|
||||
return if (appState.isTunnelRunningFromManualStart()) {
|
||||
appState.getActiveTunnelId()?.let {
|
||||
tunnels.getById(it)
|
||||
}
|
||||
} else null
|
||||
}
|
||||
|
||||
override suspend fun toggleWatcherServicePause() {
|
||||
val settings = settings.getSettings()
|
||||
if (settings.isAutoTunnelEnabled) {
|
||||
val pauseAutoTunnel = !settings.isAutoTunnelPaused
|
||||
this.settings.save(
|
||||
settings.copy(
|
||||
isAutoTunnelPaused = pauseAutoTunnel,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.GeneralState
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface AppStateRepository {
|
||||
suspend fun isLocationDisclosureShown(): Boolean
|
||||
suspend fun setLocationDisclosureShown(shown: Boolean)
|
||||
|
||||
suspend fun isBatteryOptimizationDisableShown(): Boolean
|
||||
suspend fun setBatteryOptimizationDisableShown(shown: Boolean)
|
||||
|
||||
suspend fun isTunnelRunningFromManualStart(): Boolean
|
||||
suspend fun setTunnelRunningFromManualStart(id: Int)
|
||||
|
||||
suspend fun setManualStop()
|
||||
|
||||
suspend fun getActiveTunnelId(): Int?
|
||||
|
||||
suspend fun getCurrentSsid(): String?
|
||||
|
||||
suspend fun setCurrentSsid(ssid: String)
|
||||
|
||||
val generalStateFlow: Flow<GeneralState>
|
||||
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.GeneralState
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import timber.log.Timber
|
||||
|
||||
class DataStoreAppStateRepository(private val dataStoreManager: DataStoreManager) :
|
||||
AppStateRepository {
|
||||
override suspend fun isLocationDisclosureShown(): Boolean {
|
||||
return dataStoreManager.getFromStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN)
|
||||
?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT
|
||||
}
|
||||
|
||||
override suspend fun setLocationDisclosureShown(shown: Boolean) {
|
||||
dataStoreManager.saveToDataStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN, shown)
|
||||
}
|
||||
|
||||
override suspend fun isBatteryOptimizationDisableShown(): Boolean {
|
||||
return dataStoreManager.getFromStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN)
|
||||
?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT
|
||||
}
|
||||
|
||||
override suspend fun setBatteryOptimizationDisableShown(shown: Boolean) {
|
||||
dataStoreManager.saveToDataStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN, shown)
|
||||
}
|
||||
|
||||
override suspend fun isTunnelRunningFromManualStart(): Boolean {
|
||||
return dataStoreManager.getFromStore(DataStoreManager.TUNNEL_RUNNING_FROM_MANUAL_START)
|
||||
?: GeneralState.TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT
|
||||
}
|
||||
|
||||
override suspend fun setTunnelRunningFromManualStart(id: Int) {
|
||||
setTunnelRunningFromManualStart(true)
|
||||
setActiveTunnelId(id)
|
||||
}
|
||||
|
||||
override suspend fun setManualStop() {
|
||||
setTunnelRunningFromManualStart(false)
|
||||
}
|
||||
|
||||
private suspend fun setTunnelRunningFromManualStart(running: Boolean) {
|
||||
dataStoreManager.saveToDataStore(DataStoreManager.TUNNEL_RUNNING_FROM_MANUAL_START, running)
|
||||
}
|
||||
|
||||
override suspend fun getActiveTunnelId(): Int? {
|
||||
return dataStoreManager.getFromStore(DataStoreManager.ACTIVE_TUNNEL)
|
||||
}
|
||||
|
||||
private suspend fun setActiveTunnelId(id: Int) {
|
||||
dataStoreManager.saveToDataStore(DataStoreManager.ACTIVE_TUNNEL, id)
|
||||
}
|
||||
|
||||
override suspend fun getCurrentSsid(): String? {
|
||||
return dataStoreManager.getFromStore(DataStoreManager.CURRENT_SSID)
|
||||
}
|
||||
|
||||
override suspend fun setCurrentSsid(ssid: String) {
|
||||
dataStoreManager.saveToDataStore(DataStoreManager.CURRENT_SSID, ssid)
|
||||
}
|
||||
|
||||
override val generalStateFlow: Flow<GeneralState> =
|
||||
dataStoreManager.preferencesFlow.map { prefs ->
|
||||
prefs?.let { pref ->
|
||||
try {
|
||||
GeneralState(
|
||||
locationDisclosureShown = pref[DataStoreManager.LOCATION_DISCLOSURE_SHOWN]
|
||||
?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT,
|
||||
batteryOptimizationDisableShown = pref[DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN]
|
||||
?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT,
|
||||
tunnelRunningFromManualStart = pref[DataStoreManager.TUNNEL_RUNNING_FROM_MANUAL_START]
|
||||
?: GeneralState.TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT,
|
||||
)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Timber.e(e)
|
||||
GeneralState()
|
||||
}
|
||||
} ?: GeneralState()
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.SettingsDao
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class RoomSettingsRepository(private val settingsDoa: SettingsDao) : SettingsRepository {
|
||||
|
||||
override suspend fun save(settings: Settings) {
|
||||
settingsDoa.save(settings)
|
||||
}
|
||||
|
||||
override fun getSettingsFlow(): Flow<Settings> {
|
||||
return settingsDoa.getSettingsFlow()
|
||||
}
|
||||
|
||||
override suspend fun getSettings(): Settings {
|
||||
return settingsDoa.getAll().firstOrNull() ?: Settings()
|
||||
}
|
||||
|
||||
override suspend fun getAll(): List<Settings> {
|
||||
return settingsDoa.getAll()
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
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 RoomTunnelConfigRepository(private val tunnelConfigDao: TunnelConfigDao) :
|
||||
TunnelConfigRepository {
|
||||
override fun getTunnelConfigsFlow(): Flow<TunnelConfigs> {
|
||||
return tunnelConfigDao.getAllFlow()
|
||||
}
|
||||
|
||||
override suspend fun getAll(): TunnelConfigs {
|
||||
return tunnelConfigDao.getAll()
|
||||
}
|
||||
|
||||
override suspend fun save(tunnelConfig: TunnelConfig) {
|
||||
tunnelConfigDao.save(tunnelConfig)
|
||||
}
|
||||
|
||||
override suspend fun updatePrimaryTunnel(tunnelConfig: TunnelConfig?) {
|
||||
tunnelConfigDao.resetPrimaryTunnel()
|
||||
tunnelConfig?.let {
|
||||
save(
|
||||
it.copy(
|
||||
isPrimaryTunnel = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override suspend fun updateMobileDataTunnel(tunnelConfig: TunnelConfig?) {
|
||||
tunnelConfigDao.resetMobileDataTunnel()
|
||||
tunnelConfig?.let {
|
||||
save(
|
||||
it.copy(
|
||||
isMobileDataTunnel = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun delete(tunnelConfig: TunnelConfig) {
|
||||
tunnelConfigDao.delete(tunnelConfig)
|
||||
}
|
||||
|
||||
override suspend fun getById(id: Int): TunnelConfig? {
|
||||
return tunnelConfigDao.getById(id.toLong())
|
||||
}
|
||||
|
||||
override suspend fun count(): Int {
|
||||
return tunnelConfigDao.count().toInt()
|
||||
}
|
||||
|
||||
override suspend fun findByTunnelNetworksName(name: String): TunnelConfigs {
|
||||
return tunnelConfigDao.findByTunnelNetworkName(name)
|
||||
}
|
||||
|
||||
override suspend fun findByMobileDataTunnel(): TunnelConfigs {
|
||||
return tunnelConfigDao.findByMobileDataTunnel()
|
||||
}
|
||||
|
||||
override suspend fun findPrimary(): TunnelConfigs {
|
||||
return tunnelConfigDao.findByPrimary()
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.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>
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
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 updatePrimaryTunnel(tunnelConfig: TunnelConfig?)
|
||||
|
||||
suspend fun updateMobileDataTunnel(tunnelConfig: TunnelConfig?)
|
||||
|
||||
suspend fun delete(tunnelConfig: TunnelConfig)
|
||||
|
||||
suspend fun getById(id: Int): TunnelConfig?
|
||||
|
||||
suspend fun count(): Int
|
||||
|
||||
suspend fun findByTunnelNetworksName(name: String): TunnelConfigs
|
||||
|
||||
suspend fun findByMobileDataTunnel(): TunnelConfigs
|
||||
|
||||
suspend fun findPrimary(): TunnelConfigs
|
||||
}
|
||||
@@ -3,7 +3,8 @@ package com.zaneschepke.wireguardautotunnel.module
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
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.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
@@ -16,10 +17,14 @@ import javax.inject.Singleton
|
||||
class DatabaseModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDatabase(@ApplicationContext context : Context) : AppDatabase {
|
||||
fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
|
||||
return Room.databaseBuilder(
|
||||
context,
|
||||
AppDatabase::class.java, context.getString(R.string.db_name)
|
||||
).build()
|
||||
AppDatabase::class.java,
|
||||
context.getString(R.string.db_name),
|
||||
)
|
||||
.fallbackToDestructiveMigration()
|
||||
.addCallback(DatabaseCallback())
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.zaneschepke.wireguardautotunnel.module
|
||||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Qualifier
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
annotation class Kernel
|
||||
@@ -1,27 +1,73 @@
|
||||
package com.zaneschepke.wireguardautotunnel.module
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.repository.AppDatabase
|
||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
|
||||
import android.content.Context
|
||||
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
||||
import com.zaneschepke.wireguardautotunnel.data.SettingsDao
|
||||
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
|
||||
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRoomRepository
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.DataStoreAppStateRepository
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.RoomSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.RoomTunnelConfigRepository
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
class RepositoryModule {
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideSettingsRepository(appDatabase: AppDatabase) : SettingsDoa {
|
||||
fun provideSettingsDoa(appDatabase: AppDatabase): SettingsDao {
|
||||
return appDatabase.settingDao()
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideTunnelConfigRepository(appDatabase: AppDatabase) : TunnelConfigDao {
|
||||
fun provideTunnelConfigDoa(appDatabase: AppDatabase): TunnelConfigDao {
|
||||
return appDatabase.tunnelConfigDoa()
|
||||
}
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideTunnelConfigRepository(tunnelConfigDao: TunnelConfigDao): TunnelConfigRepository {
|
||||
return RoomTunnelConfigRepository(tunnelConfigDao)
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideSettingsRepository(settingsDao: SettingsDao): SettingsRepository {
|
||||
return RoomSettingsRepository(settingsDao)
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun providePreferencesDataStore(@ApplicationContext context: Context): DataStoreManager {
|
||||
return DataStoreManager(context)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGeneralStateRepository(dataStoreManager: DataStoreManager): AppStateRepository {
|
||||
return DataStoreAppStateRepository(dataStoreManager)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAppDataRepository(
|
||||
settingsRepository: SettingsRepository,
|
||||
tunnelConfigRepository: TunnelConfigRepository,
|
||||
appStateRepository: AppStateRepository
|
||||
): AppDataRepository {
|
||||
return AppDataRoomRepository(settingsRepository, tunnelConfigRepository, appStateRepository)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.zaneschepke.wireguardautotunnel.module
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
|
||||
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
|
||||
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
|
||||
import com.zaneschepke.wireguardautotunnel.service.network.WifiService
|
||||
@@ -14,16 +15,25 @@ import dagger.hilt.android.scopes.ServiceScoped
|
||||
@Module
|
||||
@InstallIn(ServiceComponent::class)
|
||||
abstract class ServiceModule {
|
||||
@Binds
|
||||
@ServiceScoped
|
||||
abstract fun provideNotificationService(
|
||||
wireGuardNotification: WireGuardNotification
|
||||
): NotificationService
|
||||
|
||||
@Binds
|
||||
@ServiceScoped
|
||||
abstract fun provideNotificationService(wireGuardNotification: WireGuardNotification) : NotificationService
|
||||
abstract fun provideWifiService(wifiService: WifiService): NetworkService<WifiService>
|
||||
|
||||
@Binds
|
||||
@ServiceScoped
|
||||
abstract fun provideWifiService(wifiService: WifiService) : NetworkService<WifiService>
|
||||
abstract fun provideMobileDataService(
|
||||
mobileDataService: MobileDataService
|
||||
): NetworkService<MobileDataService>
|
||||
|
||||
@Binds
|
||||
@ServiceScoped
|
||||
abstract fun provideMobileDataService(mobileDataService : MobileDataService) : NetworkService<MobileDataService>
|
||||
}
|
||||
abstract fun provideEthernetService(
|
||||
ethernetService: EthernetService
|
||||
): NetworkService<EthernetService>
|
||||
}
|
||||
|
||||
@@ -3,6 +3,11 @@ package com.zaneschepke.wireguardautotunnel.module
|
||||
import android.content.Context
|
||||
import com.wireguard.android.backend.Backend
|
||||
import com.wireguard.android.backend.GoBackend
|
||||
import com.wireguard.android.backend.WgQuickBackend
|
||||
import com.wireguard.android.util.RootShell
|
||||
import com.wireguard.android.util.ToolsInstaller
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.WireGuardTunnel
|
||||
import dagger.Module
|
||||
@@ -15,17 +20,39 @@ import javax.inject.Singleton
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
class TunnelModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideRootShell(@ApplicationContext context: Context): RootShell {
|
||||
return RootShell(context)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideBackend(@ApplicationContext context : Context) : Backend {
|
||||
@Userspace
|
||||
fun provideUserspaceBackend(@ApplicationContext context: Context): Backend {
|
||||
return GoBackend(context)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideVpnService(backend: Backend) : VpnService {
|
||||
return WireGuardTunnel(backend)
|
||||
@Kernel
|
||||
fun provideKernelBackend(@ApplicationContext context: Context, rootShell: RootShell): Backend {
|
||||
return WgQuickBackend(context, rootShell, ToolsInstaller(context, rootShell))
|
||||
}
|
||||
|
||||
}
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideVpnService(
|
||||
@Userspace userspaceBackend: Backend,
|
||||
@Kernel kernelBackend: Backend,
|
||||
appDataRepository: AppDataRepository
|
||||
): VpnService {
|
||||
return WireGuardTunnel(userspaceBackend, kernelBackend, appDataRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideServiceManager(appDataRepository: AppDataRepository): ServiceManager {
|
||||
return ServiceManager(appDataRepository)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.zaneschepke.wireguardautotunnel.module
|
||||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Qualifier
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
annotation class Userspace
|
||||
@@ -3,36 +3,43 @@ package com.zaneschepke.wireguardautotunnel.receiver
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.util.goAsync
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class BootReceiver : BroadcastReceiver() {
|
||||
|
||||
@Inject
|
||||
lateinit var settingsRepo : SettingsDoa
|
||||
lateinit var appDataRepository: AppDataRepository
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
val settings = settingsRepo.getAll()
|
||||
if (!settings.isNullOrEmpty()) {
|
||||
val setting = settings.first()
|
||||
if (setting.isAutoTunnelEnabled && setting.defaultTunnel != null) {
|
||||
ServiceManager.startWatcherService(context, setting.defaultTunnel!!)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
cancel()
|
||||
@Inject
|
||||
lateinit var serviceManager: ServiceManager
|
||||
|
||||
override fun onReceive(context: Context?, intent: Intent?) = goAsync {
|
||||
if (Intent.ACTION_BOOT_COMPLETED != intent?.action) return@goAsync
|
||||
context?.run {
|
||||
val settings = appDataRepository.settings.getSettings()
|
||||
if (settings.isAutoTunnelEnabled) {
|
||||
Timber.i("Starting watcher service from boot")
|
||||
serviceManager.startWatcherServiceForeground(context)
|
||||
}
|
||||
if (appDataRepository.appState.isTunnelRunningFromManualStart()) {
|
||||
appDataRepository.appState.getActiveTunnelId()?.let {
|
||||
Timber.i("Starting tunnel that was active before reboot")
|
||||
serviceManager.startVpnServiceForeground(
|
||||
context,
|
||||
appDataRepository.tunnels.getById(it)?.id,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (settings.isAlwaysOnVpnEnabled) {
|
||||
Timber.i("Starting vpn service from boot AOVPN")
|
||||
serviceManager.startVpnServiceForeground(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+19
-22
@@ -3,37 +3,34 @@ package com.zaneschepke.wireguardautotunnel.receiver
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.zaneschepke.wireguardautotunnel.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||
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 kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class NotificationActionReceiver : BroadcastReceiver() {
|
||||
@Inject
|
||||
lateinit var settingsRepository: SettingsRepository
|
||||
|
||||
@Inject
|
||||
lateinit var settingsRepo : SettingsDoa
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
val settings = settingsRepo.getAll()
|
||||
if (settings.isNotEmpty()) {
|
||||
val setting = settings.first()
|
||||
if (setting.defaultTunnel != null) {
|
||||
ServiceManager.stopVpnService(context)
|
||||
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
||||
ServiceManager.startVpnService(context, setting.defaultTunnel.toString())
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
cancel()
|
||||
}
|
||||
lateinit var serviceManager: ServiceManager
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent?) = goAsync {
|
||||
try {
|
||||
//TODO fix for manual start changes when enabled
|
||||
serviceManager.stopVpnServiceForeground(context)
|
||||
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
||||
serviceManager.startVpnServiceForeground(context)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
} finally {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,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.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,15 +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,
|
||||
)
|
||||
-85
@@ -1,85 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.repository.model
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import com.wireguard.config.Config
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.InputStream
|
||||
|
||||
@Entity(indices = [Index(value = ["name"], unique = true)])
|
||||
@Serializable
|
||||
data class TunnelConfig(
|
||||
@PrimaryKey(autoGenerate = true) val id : Int = 0,
|
||||
@ColumnInfo(name = "name") var name : String,
|
||||
@ColumnInfo(name = "wg_quick") var wgQuick : String,
|
||||
){
|
||||
|
||||
override fun toString(): String {
|
||||
return Json.encodeToString(serializer(), this)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val INCLUDED_APPLICATIONS = "IncludedApplications = "
|
||||
private const val EXCLUDED_APPLICATIONS = "ExcludedApplications = "
|
||||
private const val INTERFACE = "[Interface]"
|
||||
private const val NEWLINE_CHAR = "\n"
|
||||
private const val APP_CONFIG_SEPARATOR = ", "
|
||||
|
||||
private fun addApplicationsToConfig(appConfig : String, wgQuick : String) : String {
|
||||
val configList = wgQuick.split(NEWLINE_CHAR).toMutableList()
|
||||
val interfaceIndex = configList.indexOf(INTERFACE)
|
||||
configList.add(interfaceIndex + 1, appConfig)
|
||||
return configList.joinToString(NEWLINE_CHAR)
|
||||
}
|
||||
|
||||
fun clearAllApplicationsFromConfig(wgQuick : String) : String {
|
||||
val configList = wgQuick.split(NEWLINE_CHAR).toMutableList()
|
||||
val itr = configList.iterator()
|
||||
while (itr.hasNext()) {
|
||||
val next = itr.next()
|
||||
if(next.contains(INCLUDED_APPLICATIONS) || next.contains(EXCLUDED_APPLICATIONS)) {
|
||||
itr.remove()
|
||||
}
|
||||
}
|
||||
return configList.joinToString(NEWLINE_CHAR)
|
||||
}
|
||||
|
||||
|
||||
fun setExcludedApplicationsOnQuick(packages : List<String>, wgQuick: String) : String {
|
||||
if(packages.isEmpty()) {
|
||||
return wgQuick
|
||||
}
|
||||
val clearedWgQuick = clearAllApplicationsFromConfig(wgQuick)
|
||||
val excludeConfig = buildExcludedApplicationsString(packages)
|
||||
return addApplicationsToConfig(excludeConfig, clearedWgQuick)
|
||||
}
|
||||
|
||||
fun setIncludedApplicationsOnQuick(packages : List<String>, wgQuick: String) : String {
|
||||
if(packages.isEmpty()) {
|
||||
return wgQuick
|
||||
}
|
||||
val clearedWgQuick = clearAllApplicationsFromConfig(wgQuick)
|
||||
val includeConfig = buildIncludedApplicationsString(packages)
|
||||
return addApplicationsToConfig(includeConfig, clearedWgQuick)
|
||||
}
|
||||
|
||||
private fun buildExcludedApplicationsString(packages : List<String>) : String {
|
||||
return EXCLUDED_APPLICATIONS + packages.joinToString(APP_CONFIG_SEPARATOR)
|
||||
}
|
||||
|
||||
private fun buildIncludedApplicationsString(packages : List<String>) : String {
|
||||
return INCLUDED_APPLICATIONS + packages.joinToString(APP_CONFIG_SEPARATOR)
|
||||
}
|
||||
fun from(string : String) : TunnelConfig {
|
||||
return Json.decodeFromString<TunnelConfig>(string)
|
||||
}
|
||||
fun configFromQuick(wgQuick: String): Config {
|
||||
val inputStream: InputStream = wgQuick.byteInputStream()
|
||||
val reader = inputStream.bufferedReader(Charsets.UTF_8)
|
||||
return Config.parse(reader)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,5 +3,6 @@ package com.zaneschepke.wireguardautotunnel.service.foreground
|
||||
enum class Action {
|
||||
START,
|
||||
START_FOREGROUND,
|
||||
STOP
|
||||
}
|
||||
STOP,
|
||||
STOP_FOREGROUND
|
||||
}
|
||||
|
||||
+17
-26
@@ -1,64 +1,55 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.foreground
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import androidx.lifecycle.LifecycleService
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import timber.log.Timber
|
||||
|
||||
|
||||
open class ForegroundService : Service() {
|
||||
|
||||
open class ForegroundService : LifecycleService() {
|
||||
private var isServiceStarted = false
|
||||
|
||||
override fun onBind(intent: Intent): IBinder? {
|
||||
super.onBind(intent)
|
||||
// We don't provide binding, so return null
|
||||
return null
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
super.onStartCommand(intent, flags, startId)
|
||||
Timber.d("onStartCommand executed with startId: $startId")
|
||||
if (intent != null) {
|
||||
val action = intent.action
|
||||
Timber.d("using an intent with action $action")
|
||||
when (action) {
|
||||
Action.START.name, Action.START_FOREGROUND.name -> startService(intent.extras)
|
||||
Action.STOP.name -> stopService(intent.extras)
|
||||
"android.net.VpnService" -> {
|
||||
Timber.d("Always-on VPN starting service")
|
||||
Action.START.name,
|
||||
Action.START_FOREGROUND.name -> startService(intent.extras)
|
||||
Action.STOP.name, Action.STOP_FOREGROUND.name -> stopService()
|
||||
Constants.ALWAYS_ON_VPN_ACTION -> {
|
||||
Timber.i("Always-on VPN starting service")
|
||||
startService(intent.extras)
|
||||
}
|
||||
|
||||
else -> Timber.d("This should never happen. No action in the received intent")
|
||||
}
|
||||
} else {
|
||||
Timber.d(
|
||||
"with a null intent. It has been probably restarted by the system."
|
||||
"with a null intent. It has been probably restarted by the system.",
|
||||
)
|
||||
}
|
||||
// by returning this we make sure the service is restarted if the system kills the service
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
Timber.d("The service has been destroyed")
|
||||
}
|
||||
|
||||
protected open fun startService(extras : Bundle?) {
|
||||
protected open fun startService(extras: Bundle?) {
|
||||
if (isServiceStarted) return
|
||||
Timber.d("Starting ${this.javaClass.simpleName}")
|
||||
isServiceStarted = true
|
||||
}
|
||||
|
||||
protected open fun stopService(extras : Bundle?) {
|
||||
protected open fun stopService() {
|
||||
Timber.d("Stopping ${this.javaClass.simpleName}")
|
||||
try {
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
stopSelf()
|
||||
} catch (e: Exception) {
|
||||
Timber.d("Service stopped without being started: ${e.message}")
|
||||
}
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
stopSelf()
|
||||
isServiceStarted = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+80
-72
@@ -1,109 +1,117 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.foreground
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Context.ACTIVITY_SERVICE
|
||||
import android.content.Intent
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import timber.log.Timber
|
||||
|
||||
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 }
|
||||
class ServiceManager(private val appDataRepository: AppDataRepository) {
|
||||
|
||||
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) {
|
||||
if (getServiceState(context, cls) == ServiceState.STOPPED && action == Action.STOP) return
|
||||
if (getServiceState(context, cls) == ServiceState.STARTED && action == Action.START) return
|
||||
val intent = Intent(context, cls).also {
|
||||
it.action = action.name
|
||||
extras?.forEach {(k, v) ->
|
||||
it.putExtra(k, v)
|
||||
private fun <T : Service> actionOnService(
|
||||
action: Action,
|
||||
context: Context,
|
||||
cls: Class<T>,
|
||||
extras: Map<String, Int>? = null
|
||||
) {
|
||||
val intent =
|
||||
Intent(context, cls).also {
|
||||
it.action = action.name
|
||||
extras?.forEach { (k, v) -> it.putExtra(k, v) }
|
||||
}
|
||||
}
|
||||
intent.component?.javaClass
|
||||
try {
|
||||
when(action) {
|
||||
Action.START_FOREGROUND -> {
|
||||
context.startForegroundService(intent)
|
||||
}
|
||||
Action.START -> {
|
||||
context.startService(intent)
|
||||
}
|
||||
Action.STOP -> context.startService(intent)
|
||||
when (action) {
|
||||
Action.START_FOREGROUND, Action.STOP_FOREGROUND -> context.startForegroundService(intent)
|
||||
Action.START, Action.STOP -> context.startService(intent)
|
||||
}
|
||||
} catch (e : Exception) {
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
fun startVpnService(context : Context, tunnelConfig : String) {
|
||||
suspend fun startVpnService(
|
||||
context: Context,
|
||||
tunnelId: Int? = null,
|
||||
isManualStart: Boolean = false
|
||||
) {
|
||||
if (isManualStart) onManualStart(tunnelId)
|
||||
actionOnService(
|
||||
Action.START,
|
||||
context,
|
||||
WireGuardTunnelService::class.java,
|
||||
mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig))
|
||||
}
|
||||
fun stopVpnService(context : Context) {
|
||||
actionOnService(
|
||||
Action.STOP,
|
||||
context,
|
||||
WireGuardTunnelService::class.java
|
||||
tunnelId?.let { mapOf(Constants.TUNNEL_EXTRA_KEY to it) },
|
||||
)
|
||||
}
|
||||
|
||||
fun startVpnServiceForeground(context : Context, tunnelConfig : String) {
|
||||
suspend fun stopVpnServiceForeground(context: Context, isManualStop: Boolean = false) {
|
||||
if (isManualStop) onManualStop()
|
||||
Timber.i("Stopping vpn service")
|
||||
actionOnService(
|
||||
Action.STOP_FOREGROUND,
|
||||
context,
|
||||
WireGuardTunnelService::class.java,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun stopVpnService(context: Context, isManualStop: Boolean = false) {
|
||||
if (isManualStop) onManualStop()
|
||||
Timber.i("Stopping vpn service")
|
||||
actionOnService(
|
||||
Action.STOP,
|
||||
context,
|
||||
WireGuardTunnelService::class.java,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun onManualStop() {
|
||||
appDataRepository.appState.setManualStop()
|
||||
}
|
||||
|
||||
private suspend fun onManualStart(tunnelId: Int?) {
|
||||
tunnelId?.let {
|
||||
appDataRepository.appState.setTunnelRunningFromManualStart(it)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun startVpnServiceForeground(
|
||||
context: Context,
|
||||
tunnelId: Int? = null,
|
||||
isManualStart: Boolean = false
|
||||
) {
|
||||
if (isManualStart) onManualStart(tunnelId)
|
||||
actionOnService(
|
||||
Action.START_FOREGROUND,
|
||||
context,
|
||||
WireGuardTunnelService::class.java,
|
||||
mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig))
|
||||
tunnelId?.let { mapOf(Constants.TUNNEL_EXTRA_KEY to it) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun startWatcherServiceForeground(context : Context, tunnelConfig : String) {
|
||||
fun startWatcherServiceForeground(
|
||||
context: Context,
|
||||
) {
|
||||
actionOnService(
|
||||
Action.START, context,
|
||||
WireGuardConnectivityWatcherService::class.java, mapOf(context.
|
||||
getString(R.string.tunnel_extras_key) to
|
||||
tunnelConfig))
|
||||
Action.START_FOREGROUND,
|
||||
context,
|
||||
WireGuardConnectivityWatcherService::class.java,
|
||||
)
|
||||
}
|
||||
|
||||
fun startWatcherService(context : Context, tunnelConfig : String) {
|
||||
fun startWatcherService(context: Context) {
|
||||
actionOnService(
|
||||
Action.START, context,
|
||||
WireGuardConnectivityWatcherService::class.java, mapOf(context.
|
||||
getString(R.string.tunnel_extras_key) to
|
||||
tunnelConfig))
|
||||
Action.START,
|
||||
context,
|
||||
WireGuardConnectivityWatcherService::class.java,
|
||||
)
|
||||
}
|
||||
|
||||
fun stopWatcherService(context : Context) {
|
||||
fun stopWatcherService(context: Context) {
|
||||
actionOnService(
|
||||
Action.STOP, context,
|
||||
WireGuardConnectivityWatcherService::class.java)
|
||||
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) {
|
||||
when(getServiceState( context,
|
||||
WireGuardConnectivityWatcherService::class.java,)) {
|
||||
ServiceState.STARTED -> stopWatcherService(context)
|
||||
ServiceState.STOPPED -> startWatcherServiceForeground(context, tunnelConfig)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.foreground
|
||||
|
||||
enum class ServiceState {
|
||||
STARTED,
|
||||
STOPPED,
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.foreground
|
||||
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
|
||||
data class WatcherState(
|
||||
val isWifiConnected: Boolean = false,
|
||||
val config: TunnelConfig? = null,
|
||||
val vpnStatus: Tunnel.State = Tunnel.State.DOWN,
|
||||
val isEthernetConnected: Boolean = false,
|
||||
val isMobileDataConnected: Boolean = false,
|
||||
val currentNetworkSSID: String = "",
|
||||
val settings: Settings = Settings()
|
||||
) {
|
||||
|
||||
private fun isVpnConnected() = vpnStatus == Tunnel.State.UP
|
||||
fun isEthernetConditionMet(): Boolean {
|
||||
return (isEthernetConnected &&
|
||||
settings.isTunnelOnEthernetEnabled &&
|
||||
!isVpnConnected())
|
||||
}
|
||||
|
||||
fun isMobileDataConditionMet(): Boolean {
|
||||
return (!isEthernetConnected &&
|
||||
settings.isTunnelOnMobileDataEnabled &&
|
||||
!isWifiConnected &&
|
||||
isMobileDataConnected &&
|
||||
!isVpnConnected())
|
||||
}
|
||||
|
||||
fun isTunnelNotMobileDataPreferredConditionMet(): Boolean {
|
||||
return (!isEthernetConnected &&
|
||||
settings.isTunnelOnMobileDataEnabled &&
|
||||
!isWifiConnected &&
|
||||
isMobileDataConnected &&
|
||||
config?.isMobileDataTunnel == false && isVpnConnected())
|
||||
}
|
||||
|
||||
fun isTunnelOffOnMobileDataConditionMet(): Boolean {
|
||||
return (!isEthernetConnected &&
|
||||
!settings.isTunnelOnMobileDataEnabled &&
|
||||
isMobileDataConnected &&
|
||||
!isWifiConnected &&
|
||||
isVpnConnected())
|
||||
}
|
||||
|
||||
fun isUntrustedWifiConditionMet(): Boolean {
|
||||
return (!isEthernetConnected &&
|
||||
isWifiConnected &&
|
||||
!settings.trustedNetworkSSIDs.contains(currentNetworkSSID) &&
|
||||
settings.isTunnelOnWifiEnabled
|
||||
&& !isVpnConnected())
|
||||
}
|
||||
|
||||
fun isTunnelNotWifiNamePreferredMet(ssid: String): Boolean {
|
||||
return (!isEthernetConnected &&
|
||||
isWifiConnected &&
|
||||
!settings.trustedNetworkSSIDs.contains(currentNetworkSSID) &&
|
||||
settings.isTunnelOnWifiEnabled && config?.tunnelNetworks?.contains(ssid) == false && isVpnConnected())
|
||||
}
|
||||
|
||||
fun isTrustedWifiConditionMet(): Boolean {
|
||||
return (!isEthernetConnected &&
|
||||
(isWifiConnected &&
|
||||
settings.trustedNetworkSSIDs.contains(currentNetworkSSID)) &&
|
||||
(isVpnConnected()))
|
||||
}
|
||||
|
||||
fun isTunnelOffOnWifiConditionMet(): Boolean {
|
||||
return (!isEthernetConnected &&
|
||||
(isWifiConnected &&
|
||||
!settings.isTunnelOnWifiEnabled &&
|
||||
(isVpnConnected())))
|
||||
}
|
||||
|
||||
fun isTunnelOffOnNoConnectivityMet(): Boolean {
|
||||
return (!isEthernetConnected &&
|
||||
!isWifiConnected &&
|
||||
!isMobileDataConnected &&
|
||||
(isVpnConnected()))
|
||||
}
|
||||
}
|
||||
|
||||
+329
-112
@@ -1,90 +1,94 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.foreground
|
||||
|
||||
import android.app.AlarmManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.PowerManager
|
||||
import android.os.SystemClock
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.zaneschepke.wireguardautotunnel.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
|
||||
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
|
||||
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
|
||||
import com.zaneschepke.wireguardautotunnel.service.network.NetworkStatus
|
||||
import com.zaneschepke.wireguardautotunnel.service.network.WifiService
|
||||
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.net.InetAddress
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
@AndroidEntryPoint
|
||||
class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
|
||||
private val foregroundId = 122;
|
||||
private val foregroundId = 122
|
||||
|
||||
@Inject
|
||||
lateinit var wifiService : NetworkService<WifiService>
|
||||
lateinit var wifiService: NetworkService<WifiService>
|
||||
|
||||
@Inject
|
||||
lateinit var mobileDataService : NetworkService<MobileDataService>
|
||||
lateinit var mobileDataService: NetworkService<MobileDataService>
|
||||
|
||||
@Inject
|
||||
lateinit var settingsRepo: SettingsDoa
|
||||
lateinit var ethernetService: NetworkService<EthernetService>
|
||||
|
||||
@Inject
|
||||
lateinit var notificationService : NotificationService
|
||||
lateinit var appDataRepository: AppDataRepository
|
||||
|
||||
@Inject
|
||||
lateinit var vpnService : VpnService
|
||||
lateinit var notificationService: NotificationService
|
||||
|
||||
private var isWifiConnected = false;
|
||||
private var isMobileDataConnected = false;
|
||||
private var currentNetworkSSID = "";
|
||||
@Inject
|
||||
lateinit var vpnService: VpnService
|
||||
|
||||
private lateinit var watcherJob : Job;
|
||||
private lateinit var setting : Settings
|
||||
private lateinit var tunnelConfig: String
|
||||
@Inject
|
||||
lateinit var serviceManager: ServiceManager
|
||||
|
||||
private val networkEventsFlow = MutableStateFlow(WatcherState())
|
||||
|
||||
private var watcherJob: Job? = null
|
||||
|
||||
private var wakeLock: PowerManager.WakeLock? = null
|
||||
private val tag = this.javaClass.name;
|
||||
|
||||
private val tag = this.javaClass.name
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
launchWatcherNotification()
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
if (appDataRepository.settings.getSettings().isAutoTunnelPaused) {
|
||||
launchWatcherPausedNotification()
|
||||
} else launchWatcherNotification()
|
||||
} catch (e: Exception) {
|
||||
Timber.e("Failed to start watcher service, not enough permissions")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun startService(extras: Bundle?) {
|
||||
super.startService(extras)
|
||||
launchWatcherNotification()
|
||||
val tunnelId = extras?.getString(getString(R.string.tunnel_extras_key))
|
||||
if (tunnelId != null) {
|
||||
this.tunnelConfig = tunnelId
|
||||
}
|
||||
// we need this lock so our service gets not affected by Doze Mode
|
||||
initWakeLock()
|
||||
cancelWatcherJob()
|
||||
if(this::tunnelConfig.isInitialized) {
|
||||
try {
|
||||
// we need this lock so our service gets not affected by Doze Mode
|
||||
lifecycleScope.launch { initWakeLock() }
|
||||
cancelWatcherJob()
|
||||
startWatcherJob()
|
||||
} else {
|
||||
stopService(extras)
|
||||
} catch (e: Exception) {
|
||||
Timber.e("Failed to launch watcher service, no permissions")
|
||||
}
|
||||
}
|
||||
|
||||
override fun stopService(extras: Bundle?) {
|
||||
super.stopService(extras)
|
||||
override fun stopService() {
|
||||
super.stopService()
|
||||
wakeLock?.let {
|
||||
if (it.isHeld) {
|
||||
it.release()
|
||||
@@ -94,74 +98,206 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
private fun launchWatcherNotification() {
|
||||
val notification = notificationService.createNotification(
|
||||
channelId = getString(R.string.watcher_channel_id),
|
||||
channelName = getString(R.string.watcher_channel_name),
|
||||
description = getString(R.string.watcher_notification_text))
|
||||
super.startForeground(foregroundId, notification)
|
||||
private fun launchWatcherNotification(
|
||||
description: String = getString(R.string.watcher_notification_text_active)
|
||||
) {
|
||||
val notification =
|
||||
notificationService.createNotification(
|
||||
channelId = getString(R.string.watcher_channel_id),
|
||||
channelName = getString(R.string.watcher_channel_name),
|
||||
title = getString(R.string.auto_tunnel_title),
|
||||
description = description,
|
||||
)
|
||||
ServiceCompat.startForeground(
|
||||
this,
|
||||
foregroundId,
|
||||
notification,
|
||||
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
|
||||
)
|
||||
}
|
||||
|
||||
//try to start task again if killed
|
||||
override fun onTaskRemoved(rootIntent: Intent) {
|
||||
Timber.d("Task Removed called")
|
||||
val restartServiceIntent = Intent(rootIntent)
|
||||
val restartServicePendingIntent: PendingIntent = PendingIntent.getService(this, 1, restartServiceIntent,
|
||||
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE);
|
||||
applicationContext.getSystemService(Context.ALARM_SERVICE);
|
||||
val alarmService: AlarmManager = applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager;
|
||||
alarmService.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 1000, restartServicePendingIntent);
|
||||
private fun launchWatcherPausedNotification() {
|
||||
launchWatcherNotification(getString(R.string.watcher_notification_text_paused))
|
||||
}
|
||||
|
||||
private fun initWakeLock() {
|
||||
wakeLock =
|
||||
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
|
||||
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
|
||||
acquire()
|
||||
try {
|
||||
Timber.i("Initiating wakelock with 10 min timeout")
|
||||
acquire(Constants.BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT)
|
||||
} finally {
|
||||
release()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun cancelWatcherJob() {
|
||||
if(this::watcherJob.isInitialized) {
|
||||
watcherJob.cancel()
|
||||
try {
|
||||
watcherJob?.cancel()
|
||||
} catch (e : CancellationException) {
|
||||
Timber.i("Watcher job cancelled")
|
||||
}
|
||||
}
|
||||
|
||||
private fun startWatcherJob() {
|
||||
watcherJob = CoroutineScope(Dispatchers.IO).launch {
|
||||
val settings = settingsRepo.getAll();
|
||||
if(settings.isNotEmpty()) {
|
||||
setting = settings[0]
|
||||
}
|
||||
launch {
|
||||
watchForWifiConnectivityChanges()
|
||||
}
|
||||
if(setting.isTunnelOnMobileDataEnabled) {
|
||||
watcherJob =
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val setting = appDataRepository.settings.getSettings()
|
||||
launch {
|
||||
watchForMobileDataConnectivityChanges()
|
||||
Timber.i("Starting wifi watcher")
|
||||
watchForWifiConnectivityChanges()
|
||||
}
|
||||
if (setting.isTunnelOnMobileDataEnabled) {
|
||||
launch {
|
||||
Timber.i("Starting mobile data watcher")
|
||||
watchForMobileDataConnectivityChanges()
|
||||
}
|
||||
}
|
||||
if (setting.isTunnelOnEthernetEnabled) {
|
||||
launch {
|
||||
Timber.i("Starting ethernet data watcher")
|
||||
watchForEthernetConnectivityChanges()
|
||||
}
|
||||
}
|
||||
launch {
|
||||
Timber.i("Starting vpn state watcher")
|
||||
watchForVpnConnectivityChanges()
|
||||
}
|
||||
launch {
|
||||
Timber.i("Starting settings watcher")
|
||||
watchForSettingsChanges()
|
||||
}
|
||||
if (setting.isPingEnabled) {
|
||||
launch {
|
||||
Timber.i("Starting ping watcher")
|
||||
watchForPingFailure()
|
||||
}
|
||||
}
|
||||
launch {
|
||||
Timber.i("Starting management watcher")
|
||||
manageVpn()
|
||||
}
|
||||
|
||||
}
|
||||
launch {
|
||||
manageVpn()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun watchForMobileDataConnectivityChanges() {
|
||||
mobileDataService.networkStatus.collect {
|
||||
when(it) {
|
||||
when (it) {
|
||||
is NetworkStatus.Available -> {
|
||||
Timber.d("Gained Mobile data connection")
|
||||
isMobileDataConnected = true
|
||||
Timber.i("Gained Mobile data connection")
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isMobileDataConnected = true,
|
||||
)
|
||||
}
|
||||
|
||||
is NetworkStatus.CapabilitiesChanged -> {
|
||||
isMobileDataConnected = true
|
||||
Timber.d("Mobile data capabilities changed")
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isMobileDataConnected = true,
|
||||
)
|
||||
Timber.i("Mobile data capabilities changed")
|
||||
}
|
||||
|
||||
is NetworkStatus.Unavailable -> {
|
||||
isMobileDataConnected = false
|
||||
Timber.d("Lost mobile data connection")
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isMobileDataConnected = false,
|
||||
)
|
||||
Timber.i("Lost mobile data connection")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun watchForPingFailure() {
|
||||
try {
|
||||
do {
|
||||
if (vpnService.vpnState.value.status == Tunnel.State.UP) {
|
||||
val tunnelConfig = vpnService.vpnState.value.tunnelConfig
|
||||
tunnelConfig?.let {
|
||||
val config = TunnelConfig.configFromQuick(it.wgQuick)
|
||||
val results = config.peers.map { peer ->
|
||||
val host = if (peer.endpoint.isPresent &&
|
||||
peer.endpoint.get().resolved.isPresent)
|
||||
peer.endpoint.get().resolved.get().host
|
||||
else Constants.BACKUP_PING_HOST
|
||||
Timber.i("Checking reachability of: $host")
|
||||
val reachable = InetAddress.getByName(host)
|
||||
.isReachable(Constants.PING_TIMEOUT.toInt())
|
||||
Timber.i("Result: reachable - $reachable")
|
||||
reachable
|
||||
}
|
||||
if (results.contains(false)) {
|
||||
Timber.i("Restarting VPN for ping failure")
|
||||
serviceManager.stopVpnServiceForeground(this)
|
||||
delay(Constants.VPN_RESTART_DELAY)
|
||||
serviceManager.startVpnServiceForeground(this)
|
||||
delay(Constants.PING_COOLDOWN)
|
||||
}
|
||||
}
|
||||
}
|
||||
delay(Constants.PING_INTERVAL)
|
||||
} while (true)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun watchForSettingsChanges() {
|
||||
appDataRepository.settings.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 {
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
vpnStatus = it.status,
|
||||
config = it.tunnelConfig,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun watchForEthernetConnectivityChanges() {
|
||||
ethernetService.networkStatus.collect {
|
||||
when (it) {
|
||||
is NetworkStatus.Available -> {
|
||||
Timber.i("Gained Ethernet connection")
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isEthernetConnected = true,
|
||||
)
|
||||
}
|
||||
|
||||
is NetworkStatus.CapabilitiesChanged -> {
|
||||
Timber.i("Ethernet capabilities changed")
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isEthernetConnected = true,
|
||||
)
|
||||
}
|
||||
|
||||
is NetworkStatus.Unavailable -> {
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isEthernetConnected = false,
|
||||
)
|
||||
Timber.i("Lost Ethernet connection")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -169,45 +305,126 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
|
||||
private suspend fun watchForWifiConnectivityChanges() {
|
||||
wifiService.networkStatus.collect {
|
||||
when (it) {
|
||||
is NetworkStatus.Available -> {
|
||||
Timber.d("Gained Wi-Fi connection")
|
||||
isWifiConnected = true
|
||||
when (it) {
|
||||
is NetworkStatus.Available -> {
|
||||
Timber.i("Gained Wi-Fi connection")
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isWifiConnected = true,
|
||||
)
|
||||
}
|
||||
|
||||
is NetworkStatus.CapabilitiesChanged -> {
|
||||
Timber.i("Wifi capabilities changed")
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isWifiConnected = true,
|
||||
)
|
||||
val ssid = wifiService.getNetworkName(it.networkCapabilities)
|
||||
ssid?.let {
|
||||
if(it.contains(Constants.UNREADABLE_SSID)) {
|
||||
Timber.w("SSID unreadable: missing permissions")
|
||||
} else Timber.i("Detected valid SSID")
|
||||
appDataRepository.appState.setCurrentSsid(ssid)
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
currentNetworkSSID = ssid,
|
||||
)
|
||||
} ?: Timber.w("Failed to read ssid")
|
||||
}
|
||||
|
||||
is NetworkStatus.Unavailable -> {
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isWifiConnected = false,
|
||||
)
|
||||
Timber.i("Lost Wi-Fi connection")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getMobileDataTunnel(): TunnelConfig? {
|
||||
return appDataRepository.tunnels.findByMobileDataTunnel().firstOrNull()
|
||||
}
|
||||
|
||||
private suspend fun getSsidTunnel(ssid: String): TunnelConfig? {
|
||||
return appDataRepository.tunnels.findByTunnelNetworksName(ssid).firstOrNull()
|
||||
}
|
||||
|
||||
private suspend fun manageVpn() {
|
||||
networkEventsFlow.collectLatest { watcherState ->
|
||||
val autoTunnel = "Auto-tunnel watcher"
|
||||
if (!watcherState.settings.isAutoTunnelPaused) {
|
||||
//delay for rapid network state changes and then collect latest
|
||||
delay(Constants.WATCHER_COLLECTION_DELAY)
|
||||
when {
|
||||
watcherState.isEthernetConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel on on ethernet condition met")
|
||||
serviceManager.startVpnServiceForeground(this)
|
||||
}
|
||||
is NetworkStatus.CapabilitiesChanged -> {
|
||||
Timber.d("Wifi capabilities changed")
|
||||
isWifiConnected = true
|
||||
currentNetworkSSID = wifiService.getNetworkName(it.networkCapabilities) ?: "";
|
||||
|
||||
watcherState.isMobileDataConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel on on mobile data condition met")
|
||||
serviceManager.startVpnServiceForeground(this, getMobileDataTunnel()?.id)
|
||||
}
|
||||
is NetworkStatus.Unavailable -> {
|
||||
isWifiConnected = false
|
||||
Timber.d("Lost Wi-Fi connection")
|
||||
|
||||
watcherState.isTunnelNotMobileDataPreferredConditionMet() -> {
|
||||
getMobileDataTunnel()?.let {
|
||||
Timber.i("$autoTunnel - tunnel connected on mobile data is not preferred condition met, switching to preferred")
|
||||
serviceManager.startVpnServiceForeground(
|
||||
this,
|
||||
getMobileDataTunnel()?.id,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
watcherState.isTunnelOffOnMobileDataConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel off on mobile data met, turning vpn off")
|
||||
serviceManager.stopVpnServiceForeground(this)
|
||||
}
|
||||
|
||||
watcherState.isTunnelNotWifiNamePreferredMet(watcherState.currentNetworkSSID) -> {
|
||||
Timber.i("$autoTunnel - tunnel on ssid not associated with current tunnel condition met")
|
||||
getSsidTunnel(watcherState.currentNetworkSSID)?.let {
|
||||
Timber.i("Found tunnel associated with this SSID, bringing tunnel up")
|
||||
serviceManager.startVpnServiceForeground(this, it.id)
|
||||
} ?: suspend {
|
||||
Timber.i("No tunnel associated with this SSID, using defaults")
|
||||
if (appDataRepository.getPrimaryOrFirstTunnel()?.name != vpnService.name) {
|
||||
serviceManager.startVpnServiceForeground(this)
|
||||
}
|
||||
}.invoke()
|
||||
}
|
||||
|
||||
watcherState.isUntrustedWifiConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel on untrusted wifi condition met")
|
||||
serviceManager.startVpnServiceForeground(
|
||||
this,
|
||||
getSsidTunnel(watcherState.currentNetworkSSID)?.id,
|
||||
)
|
||||
}
|
||||
|
||||
watcherState.isTrustedWifiConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel off on trusted wifi condition met, turning vpn off")
|
||||
serviceManager.stopVpnServiceForeground(this)
|
||||
}
|
||||
|
||||
watcherState.isTunnelOffOnWifiConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel off on wifi condition met, turning vpn off")
|
||||
serviceManager.stopVpnServiceForeground(this)
|
||||
}
|
||||
|
||||
watcherState.isTunnelOffOnNoConnectivityMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel off on no connectivity met, turning vpn off")
|
||||
serviceManager.stopVpnServiceForeground(this)
|
||||
}
|
||||
|
||||
else -> {
|
||||
Timber.i("$autoTunnel - no condition met")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun manageVpn() {
|
||||
while(true) {
|
||||
if(setting.isTunnelOnMobileDataEnabled &&
|
||||
!isWifiConnected &&
|
||||
isMobileDataConnected
|
||||
&& vpnService.getState() == Tunnel.State.DOWN) {
|
||||
ServiceManager.startVpnService(this, tunnelConfig)
|
||||
} else if(!setting.isTunnelOnMobileDataEnabled &&
|
||||
!isWifiConnected &&
|
||||
vpnService.getState() == Tunnel.State.UP) {
|
||||
ServiceManager.stopVpnService(this)
|
||||
} else if(isWifiConnected &&
|
||||
!setting.trustedNetworkSSIDs.contains(currentNetworkSSID) &&
|
||||
(vpnService.getState() != Tunnel.State.UP)) {
|
||||
ServiceManager.startVpnService(this, tunnelConfig)
|
||||
} else if((isWifiConnected &&
|
||||
setting.trustedNetworkSSIDs.contains(currentNetworkSSID)) &&
|
||||
(vpnService.getState() == Tunnel.State.UP)) {
|
||||
ServiceManager.stopVpnService(this)
|
||||
}
|
||||
delay(Constants.VPN_CONNECTIVITY_CHECK_INTERVAL)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+127
-98
@@ -3,157 +3,186 @@ package com.zaneschepke.wireguardautotunnel.service.foreground
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver
|
||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
||||
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 kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class WireGuardTunnelService : ForegroundService() {
|
||||
|
||||
private val foregroundId = 123;
|
||||
private val foregroundId = 123
|
||||
|
||||
@Inject
|
||||
lateinit var vpnService : VpnService
|
||||
lateinit var vpnService: VpnService
|
||||
|
||||
@Inject
|
||||
lateinit var settingsRepo: SettingsDoa
|
||||
lateinit var appDataRepository: AppDataRepository
|
||||
|
||||
@Inject
|
||||
lateinit var notificationService : NotificationService
|
||||
lateinit var notificationService: NotificationService
|
||||
|
||||
private lateinit var job : Job
|
||||
private var job: Job? = null
|
||||
|
||||
private var tunnelName : String = ""
|
||||
private var didShowConnected = false
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
launchVpnStartingNotification()
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
//TODO fix this to not launch if AOVPN
|
||||
if (appDataRepository.tunnels.count() != 0) {
|
||||
launchVpnNotification()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun startService(extras : Bundle?) {
|
||||
override fun startService(extras: Bundle?) {
|
||||
super.startService(extras)
|
||||
launchVpnStartingNotification()
|
||||
val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key))
|
||||
cancelJob()
|
||||
job = CoroutineScope(Dispatchers.IO).launch {
|
||||
if(tunnelConfigString != null) {
|
||||
try {
|
||||
val tunnelConfig = TunnelConfig.from(tunnelConfigString)
|
||||
tunnelName = tunnelConfig.name
|
||||
vpnService.startTunnel(tunnelConfig)
|
||||
} catch (e : Exception) {
|
||||
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
|
||||
vpnService.startTunnel(tunnelConfig)
|
||||
job =
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
launch {
|
||||
val tunnelId = extras?.getInt(Constants.TUNNEL_EXTRA_KEY)
|
||||
if (vpnService.getState() == Tunnel.State.UP) {
|
||||
vpnService.stopTunnel()
|
||||
}
|
||||
vpnService.startTunnel(
|
||||
tunnelId?.let {
|
||||
appDataRepository.tunnels.getById(it)
|
||||
},
|
||||
)
|
||||
}
|
||||
launch {
|
||||
handshakeNotifications()
|
||||
}
|
||||
}
|
||||
}
|
||||
CoroutineScope(job).launch {
|
||||
var didShowConnected = false
|
||||
var didShowFailedHandshakeNotification = false
|
||||
vpnService.handshakeStatus.collect {
|
||||
when(it) {
|
||||
HandshakeStatus.NOT_STARTED -> {
|
||||
}
|
||||
HandshakeStatus.NEVER_CONNECTED -> {
|
||||
if(!didShowFailedHandshakeNotification) {
|
||||
launchVpnConnectionFailedNotification(getString(R.string.initial_connection_failure_message))
|
||||
didShowFailedHandshakeNotification = true
|
||||
didShowConnected = false
|
||||
}
|
||||
|
||||
//TODO improve tunnel notifications
|
||||
private suspend fun handshakeNotifications() {
|
||||
var tunnelName: String? = null
|
||||
vpnService.vpnState.collect { state ->
|
||||
state.statistics
|
||||
?.mapPeerStats()
|
||||
?.map { it.value?.handshakeStatus() }
|
||||
.let { statuses ->
|
||||
when {
|
||||
statuses?.all { it == HandshakeStatus.HEALTHY } == true -> {
|
||||
if (!didShowConnected) {
|
||||
delay(Constants.VPN_CONNECTED_NOTIFICATION_DELAY)
|
||||
tunnelName = state.tunnelConfig?.name
|
||||
launchVpnNotification(
|
||||
getString(R.string.tunnel_start_title),
|
||||
"${getString(R.string.tunnel_start_text)} - $tunnelName",
|
||||
)
|
||||
didShowConnected = true
|
||||
}
|
||||
}
|
||||
}
|
||||
HandshakeStatus.HEALTHY -> {
|
||||
if(!didShowConnected) {
|
||||
launchVpnConnectedNotification()
|
||||
didShowConnected = true
|
||||
}
|
||||
}
|
||||
HandshakeStatus.UNHEALTHY -> {
|
||||
if(!didShowFailedHandshakeNotification) {
|
||||
launchVpnConnectionFailedNotification(getString(R.string.lost_connection_failure_message))
|
||||
didShowFailedHandshakeNotification = true
|
||||
didShowConnected = false
|
||||
|
||||
statuses?.any { it == HandshakeStatus.STALE } == true -> {}
|
||||
statuses?.all { it == HandshakeStatus.NOT_STARTED } ==
|
||||
true -> {
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
if (state.status == Tunnel.State.UP && state.tunnelConfig?.name != tunnelName) {
|
||||
tunnelName = state.tunnelConfig?.name
|
||||
launchVpnNotification(
|
||||
getString(R.string.tunnel_start_title),
|
||||
"${getString(R.string.tunnel_start_text)} - $tunnelName",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun stopService(extras : Bundle?) {
|
||||
super.stopService(extras)
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
private fun launchAlwaysOnDisabledNotification() {
|
||||
launchVpnNotification(
|
||||
title = this.getString(R.string.vpn_connection_failed),
|
||||
description = this.getString(R.string.always_on_disabled),
|
||||
)
|
||||
}
|
||||
|
||||
override fun stopService() {
|
||||
super.stopService()
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
vpnService.stopTunnel()
|
||||
didShowConnected = false
|
||||
}
|
||||
cancelJob()
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
private fun launchVpnConnectedNotification() {
|
||||
val notification = notificationService.createNotification(
|
||||
channelId = getString(R.string.vpn_channel_id),
|
||||
channelName = getString(R.string.vpn_channel_name),
|
||||
title = getString(R.string.tunnel_start_title),
|
||||
onGoing = false,
|
||||
showTimestamp = true,
|
||||
description = "${getString(R.string.tunnel_start_text)} $tunnelName"
|
||||
private fun launchVpnNotification(
|
||||
title: String = getString(R.string.vpn_starting),
|
||||
description: String = getString(R.string.attempt_connection)
|
||||
) {
|
||||
val notification =
|
||||
notificationService.createNotification(
|
||||
channelId = getString(R.string.vpn_channel_id),
|
||||
channelName = getString(R.string.vpn_channel_name),
|
||||
title = title,
|
||||
onGoing = false,
|
||||
vibration = false,
|
||||
showTimestamp = true,
|
||||
description = description,
|
||||
)
|
||||
ServiceCompat.startForeground(
|
||||
this,
|
||||
foregroundId,
|
||||
notification,
|
||||
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
|
||||
)
|
||||
super.startForeground(foregroundId, notification)
|
||||
}
|
||||
|
||||
private fun launchVpnStartingNotification() {
|
||||
val notification = notificationService.createNotification(
|
||||
channelId = getString(R.string.vpn_channel_id),
|
||||
channelName = getString(R.string.vpn_channel_name),
|
||||
title = getString(R.string.vpn_starting),
|
||||
onGoing = false,
|
||||
showTimestamp = true,
|
||||
description = getString(R.string.attempt_connection)
|
||||
private fun launchVpnConnectionFailedNotification(message: String) {
|
||||
val notification =
|
||||
notificationService.createNotification(
|
||||
channelId = getString(R.string.vpn_channel_id),
|
||||
channelName = getString(R.string.vpn_channel_name),
|
||||
action =
|
||||
PendingIntent.getBroadcast(
|
||||
this,
|
||||
0,
|
||||
Intent(this, NotificationActionReceiver::class.java),
|
||||
PendingIntent.FLAG_IMMUTABLE,
|
||||
),
|
||||
actionText = getString(R.string.restart),
|
||||
title = getString(R.string.vpn_connection_failed),
|
||||
onGoing = false,
|
||||
vibration = true,
|
||||
showTimestamp = true,
|
||||
description = message,
|
||||
)
|
||||
ServiceCompat.startForeground(
|
||||
this,
|
||||
foregroundId,
|
||||
notification,
|
||||
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
|
||||
)
|
||||
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() {
|
||||
if(this::job.isInitialized) {
|
||||
job.cancel()
|
||||
try {
|
||||
job?.cancel()
|
||||
} catch (e : CancellationException) {
|
||||
Timber.i("Tunnel job cancelled")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+69
-54
@@ -14,8 +14,10 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
|
||||
abstract class BaseNetworkService<T : BaseNetworkService<T>>(val context: Context, networkCapability : Int) : NetworkService<T> {
|
||||
abstract class BaseNetworkService<T : BaseNetworkService<T>>(
|
||||
val context: Context,
|
||||
networkCapability: Int
|
||||
) : NetworkService<T> {
|
||||
private val connectivityManager =
|
||||
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
|
||||
@@ -23,61 +25,70 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(val context: Contex
|
||||
context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
|
||||
|
||||
override val networkStatus = callbackFlow {
|
||||
val networkStatusCallback = when (Build.VERSION.SDK_INT) {
|
||||
in Build.VERSION_CODES.S..Int.MAX_VALUE -> {
|
||||
object : ConnectivityManager.NetworkCallback(
|
||||
FLAG_INCLUDE_LOCATION_INFO
|
||||
) {
|
||||
override fun onAvailable(network: Network) {
|
||||
trySend(NetworkStatus.Available(network))
|
||||
}
|
||||
val networkStatusCallback =
|
||||
when (Build.VERSION.SDK_INT) {
|
||||
in Build.VERSION_CODES.S..Int.MAX_VALUE -> {
|
||||
object :
|
||||
ConnectivityManager.NetworkCallback(
|
||||
FLAG_INCLUDE_LOCATION_INFO,
|
||||
) {
|
||||
override fun onAvailable(network: Network) {
|
||||
trySend(NetworkStatus.Available(network))
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
trySend(NetworkStatus.Unavailable(network))
|
||||
}
|
||||
override fun onLost(network: Network) {
|
||||
trySend(NetworkStatus.Unavailable(network))
|
||||
}
|
||||
|
||||
override fun onCapabilitiesChanged(
|
||||
network: Network,
|
||||
networkCapabilities: NetworkCapabilities
|
||||
) {
|
||||
trySend(NetworkStatus.CapabilitiesChanged(network, networkCapabilities))
|
||||
override fun onCapabilitiesChanged(
|
||||
network: Network,
|
||||
networkCapabilities: NetworkCapabilities
|
||||
) {
|
||||
trySend(
|
||||
NetworkStatus.CapabilitiesChanged(
|
||||
network,
|
||||
networkCapabilities,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
trySend(NetworkStatus.Available(network))
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
trySend(NetworkStatus.Unavailable(network))
|
||||
}
|
||||
|
||||
override fun onCapabilitiesChanged(
|
||||
network: Network,
|
||||
networkCapabilities: NetworkCapabilities
|
||||
) {
|
||||
trySend(
|
||||
NetworkStatus.CapabilitiesChanged(
|
||||
network,
|
||||
networkCapabilities,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
object : ConnectivityManager.NetworkCallback() {
|
||||
|
||||
override fun onAvailable(network: Network) {
|
||||
trySend(NetworkStatus.Available(network))
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
trySend(NetworkStatus.Unavailable(network))
|
||||
}
|
||||
|
||||
override fun onCapabilitiesChanged(
|
||||
network: Network,
|
||||
networkCapabilities: NetworkCapabilities
|
||||
) {
|
||||
trySend(NetworkStatus.CapabilitiesChanged(network, networkCapabilities))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val request = NetworkRequest.Builder()
|
||||
.addTransportType(networkCapability)
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||
.build()
|
||||
val request =
|
||||
NetworkRequest.Builder()
|
||||
.addTransportType(networkCapability)
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||
.build()
|
||||
connectivityManager.registerNetworkCallback(request, networkStatusCallback)
|
||||
|
||||
awaitClose {
|
||||
connectivityManager.unregisterNetworkCallback(networkStatusCallback)
|
||||
}
|
||||
awaitClose { connectivityManager.unregisterNetworkCallback(networkStatusCallback) }
|
||||
}
|
||||
|
||||
|
||||
override fun getNetworkName(networkCapabilities: NetworkCapabilities): String? {
|
||||
var ssid: String? = getWifiNameFromCapabilities(networkCapabilities)
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
|
||||
@@ -89,7 +100,6 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(val context: Contex
|
||||
return ssid?.trim('"')
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
private fun getWifiNameFromCapabilities(networkCapabilities: NetworkCapabilities): String? {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
@@ -105,13 +115,18 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(val context: Contex
|
||||
}
|
||||
|
||||
inline fun <Result> Flow<NetworkStatus>.map(
|
||||
crossinline onUnavailable: suspend (network : Network) -> Result,
|
||||
crossinline onAvailable: suspend (network : Network) -> Result,
|
||||
crossinline onCapabilitiesChanged: suspend (network : Network, networkCapabilities : NetworkCapabilities) -> Result,
|
||||
crossinline onUnavailable: suspend (network: Network) -> Result,
|
||||
crossinline onAvailable: suspend (network: Network) -> Result,
|
||||
crossinline onCapabilitiesChanged:
|
||||
suspend (network: Network, networkCapabilities: NetworkCapabilities) -> Result
|
||||
): Flow<Result> = map { status ->
|
||||
when (status) {
|
||||
is NetworkStatus.Unavailable -> onUnavailable(status.network)
|
||||
is NetworkStatus.Available -> onAvailable(status.network)
|
||||
is NetworkStatus.CapabilitiesChanged -> onCapabilitiesChanged(status.network, status.networkCapabilities)
|
||||
is NetworkStatus.CapabilitiesChanged ->
|
||||
onCapabilitiesChanged(
|
||||
status.network,
|
||||
status.networkCapabilities,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.network
|
||||
|
||||
import android.content.Context
|
||||
import android.net.NetworkCapabilities
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class EthernetService @Inject constructor(@ApplicationContext context: Context) :
|
||||
BaseNetworkService<EthernetService>(context, NetworkCapabilities.TRANSPORT_ETHERNET)
|
||||
+1
-2
@@ -6,5 +6,4 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class MobileDataService @Inject constructor(@ApplicationContext context: Context) :
|
||||
BaseNetworkService<MobileDataService>(context, NetworkCapabilities.TRANSPORT_CELLULAR) {
|
||||
}
|
||||
BaseNetworkService<MobileDataService>(context, NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||
|
||||
+3
-3
@@ -4,7 +4,7 @@ import android.net.NetworkCapabilities
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface NetworkService<T> {
|
||||
fun getNetworkName(networkCapabilities: NetworkCapabilities) : String?
|
||||
val networkStatus : Flow<NetworkStatus>
|
||||
fun getNetworkName(networkCapabilities: NetworkCapabilities): String?
|
||||
|
||||
}
|
||||
val networkStatus: Flow<NetworkStatus>
|
||||
}
|
||||
|
||||
+6
-3
@@ -4,7 +4,10 @@ import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
|
||||
sealed class NetworkStatus {
|
||||
class Available(val network : Network) : NetworkStatus()
|
||||
class Unavailable(val network : Network) : NetworkStatus()
|
||||
class CapabilitiesChanged(val network : Network, val networkCapabilities : NetworkCapabilities) : NetworkStatus()
|
||||
class Available(val network: Network) : NetworkStatus()
|
||||
|
||||
class Unavailable(val network: Network) : NetworkStatus()
|
||||
|
||||
class CapabilitiesChanged(val network: Network, val networkCapabilities: NetworkCapabilities) :
|
||||
NetworkStatus()
|
||||
}
|
||||
|
||||
+1
-2
@@ -6,5 +6,4 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class WifiService @Inject constructor(@ApplicationContext context: Context) :
|
||||
BaseNetworkService<WifiService>(context, NetworkCapabilities.TRANSPORT_WIFI) {
|
||||
}
|
||||
BaseNetworkService<WifiService>(context, NetworkCapabilities.TRANSPORT_WIFI)
|
||||
|
||||
+5
-4
@@ -12,10 +12,11 @@ interface NotificationService {
|
||||
action: PendingIntent? = null,
|
||||
actionText: String? = null,
|
||||
description: String,
|
||||
showTimestamp : Boolean = false,
|
||||
showTimestamp: Boolean = false,
|
||||
importance: Int = NotificationManager.IMPORTANCE_HIGH,
|
||||
vibration: Boolean = true,
|
||||
vibration: Boolean = false,
|
||||
onGoing: Boolean = true,
|
||||
lights: Boolean = true
|
||||
lights: Boolean = true,
|
||||
onlyAlertOnce: Boolean = true,
|
||||
): Notification
|
||||
}
|
||||
}
|
||||
|
||||
+55
-31
@@ -7,14 +7,27 @@ import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.MainActivity
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class WireGuardNotification @Inject constructor(@ApplicationContext private val context: Context) : NotificationService {
|
||||
class WireGuardNotification @Inject constructor(@ApplicationContext private val context: Context) :
|
||||
NotificationService {
|
||||
private val notificationManager =
|
||||
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
|
||||
private val watcherBuilder: NotificationCompat.Builder =
|
||||
NotificationCompat.Builder(
|
||||
context,
|
||||
context.getString(R.string.watcher_channel_id),
|
||||
)
|
||||
private val tunnelBuilder: NotificationCompat.Builder =
|
||||
NotificationCompat.Builder(
|
||||
context,
|
||||
context.getString(R.string.vpn_channel_id),
|
||||
)
|
||||
|
||||
override fun createNotification(
|
||||
channelId: String,
|
||||
@@ -27,20 +40,23 @@ class WireGuardNotification @Inject constructor(@ApplicationContext private val
|
||||
importance: Int,
|
||||
vibration: Boolean,
|
||||
onGoing: Boolean,
|
||||
lights: Boolean
|
||||
lights: Boolean,
|
||||
onlyAlertOnce: Boolean,
|
||||
): Notification {
|
||||
val channel = NotificationChannel(
|
||||
channelId,
|
||||
channelName,
|
||||
importance
|
||||
).let {
|
||||
it.description = title
|
||||
it.enableLights(lights)
|
||||
it.lightColor = Color.RED
|
||||
it.enableVibration(vibration)
|
||||
it.vibrationPattern = longArrayOf(100, 200, 300, 400, 500, 400, 300, 200, 400)
|
||||
it
|
||||
}
|
||||
val channel =
|
||||
NotificationChannel(
|
||||
channelId,
|
||||
channelName,
|
||||
importance,
|
||||
)
|
||||
.let {
|
||||
it.description = title
|
||||
it.enableLights(lights)
|
||||
it.lightColor = Color.RED
|
||||
it.enableVibration(vibration)
|
||||
it.vibrationPattern = longArrayOf(100, 200, 300)
|
||||
it
|
||||
}
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
val pendingIntent: PendingIntent =
|
||||
Intent(context, MainActivity::class.java).let { notificationIntent ->
|
||||
@@ -48,30 +64,38 @@ class WireGuardNotification @Inject constructor(@ApplicationContext private val
|
||||
context,
|
||||
0,
|
||||
notificationIntent,
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
}
|
||||
|
||||
val builder: Notification.Builder =
|
||||
Notification.Builder(
|
||||
context,
|
||||
channelId
|
||||
)
|
||||
return builder.let {
|
||||
if(action != null && actionText != null) {
|
||||
//TODO find a not deprecated way to do this
|
||||
it.addAction(
|
||||
Notification.Action.Builder(0, actionText, action)
|
||||
.build())
|
||||
it.setAutoCancel(true)
|
||||
val builder =
|
||||
when (channelId) {
|
||||
context.getString(R.string.watcher_channel_id) -> watcherBuilder
|
||||
context.getString(R.string.vpn_channel_id) -> tunnelBuilder
|
||||
else -> {
|
||||
NotificationCompat.Builder(
|
||||
context,
|
||||
channelId,
|
||||
)
|
||||
}
|
||||
}
|
||||
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)
|
||||
.setOnlyAlertOnce(onlyAlertOnce)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setOngoing(onGoing)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setShowWhen(showTimestamp)
|
||||
.setSmallIcon(R.mipmap.ic_launcher_foreground)
|
||||
.setSmallIcon(R.drawable.ic_launcher)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+52
-29
@@ -1,52 +1,75 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.shortcut
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ShortcutsActivity : AppCompatActivity() {
|
||||
class ShortcutsActivity : ComponentActivity() {
|
||||
|
||||
@Inject
|
||||
lateinit var settingsRepo : SettingsDoa
|
||||
lateinit var appDataRepository: AppDataRepository
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.Main);
|
||||
|
||||
private fun attemptWatcherServiceToggle(tunnelConfig : String) {
|
||||
scope.launch {
|
||||
val settings = settingsRepo.getAll()
|
||||
if (settings.isNotEmpty()) {
|
||||
val setting = settings.first()
|
||||
if(setting.isAutoTunnelEnabled) {
|
||||
ServiceManager.toggleWatcherServiceForeground(this@ShortcutsActivity, tunnelConfig)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@Inject
|
||||
lateinit var serviceManager: ServiceManager
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if(intent.getStringExtra(ShortcutsManager.CLASS_NAME_EXTRA_KEY)
|
||||
.equals(WireGuardTunnelService::class.java.name)) {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
val settings = appDataRepository.settings.getSettings()
|
||||
if (settings.isShortcutsEnabled) {
|
||||
when (intent.getStringExtra(CLASS_NAME_EXTRA_KEY)) {
|
||||
WireGuardTunnelService::class.java.simpleName -> {
|
||||
val tunnelName = intent.getStringExtra(TUNNEL_NAME_EXTRA_KEY)
|
||||
val tunnelConfig = tunnelName?.let {
|
||||
appDataRepository.tunnels.getAll().firstOrNull {
|
||||
it.name == tunnelName
|
||||
}
|
||||
}
|
||||
when (intent.action) {
|
||||
Action.START.name -> serviceManager.startVpnServiceForeground(
|
||||
this@ShortcutsActivity, tunnelConfig?.id, isManualStart = true,
|
||||
)
|
||||
|
||||
intent.getStringExtra(getString(R.string.tunnel_extras_key))?.let {
|
||||
attemptWatcherServiceToggle(it)
|
||||
}
|
||||
when(intent.action){
|
||||
Action.STOP.name -> ServiceManager.stopVpnService(this)
|
||||
Action.START.name -> intent.getStringExtra(getString(R.string.tunnel_extras_key))
|
||||
?.let { ServiceManager.startVpnService(this, it) }
|
||||
Action.STOP.name -> serviceManager.stopVpnServiceForeground(
|
||||
this@ShortcutsActivity,
|
||||
isManualStop = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
WireGuardConnectivityWatcherService::class.java.simpleName -> {
|
||||
when (intent.action) {
|
||||
Action.START.name -> appDataRepository.settings.save(
|
||||
settings.copy(
|
||||
isAutoTunnelPaused = false,
|
||||
),
|
||||
)
|
||||
|
||||
Action.STOP.name -> appDataRepository.settings.save(
|
||||
settings.copy(
|
||||
isAutoTunnelPaused = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TUNNEL_NAME_EXTRA_KEY = "tunnelName"
|
||||
const val CLASS_NAME_EXTRA_KEY = "className"
|
||||
}
|
||||
}
|
||||
|
||||
-73
@@ -1,73 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.shortcut
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.content.pm.ShortcutInfoCompat
|
||||
import androidx.core.content.pm.ShortcutManagerCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
|
||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
||||
|
||||
object ShortcutsManager {
|
||||
|
||||
private const val SHORT_LABEL_MAX_SIZE = 10;
|
||||
private const val LONG_LABEL_MAX_SIZE = 25;
|
||||
private const val APPEND_ON = " On";
|
||||
private const val APPEND_OFF = " Off"
|
||||
const val CLASS_NAME_EXTRA_KEY = "className"
|
||||
|
||||
private fun createAndPushShortcut(context : Context, intent : Intent, id : String, shortLabel : String,
|
||||
longLabel : String, drawable : Int ) {
|
||||
val shortcut = ShortcutInfoCompat.Builder(context, id)
|
||||
.setShortLabel(shortLabel)
|
||||
.setLongLabel(longLabel)
|
||||
.setIcon(IconCompat.createWithResource(context, drawable))
|
||||
.setIntent(intent)
|
||||
.build()
|
||||
ShortcutManagerCompat.pushDynamicShortcut(context, shortcut)
|
||||
}
|
||||
|
||||
fun createTunnelShortcuts(context : Context, tunnelConfig : TunnelConfig) {
|
||||
createAndPushShortcut(context,
|
||||
createTunnelOnIntent(context, mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig.toString())),
|
||||
tunnelConfig.id.toString() + APPEND_ON,
|
||||
tunnelConfig.name.take((SHORT_LABEL_MAX_SIZE - APPEND_ON.length)) + APPEND_ON,
|
||||
tunnelConfig.name.take((LONG_LABEL_MAX_SIZE - APPEND_ON.length)) + APPEND_ON,
|
||||
R.drawable.vpn_on
|
||||
)
|
||||
createAndPushShortcut(context,
|
||||
createTunnelOffIntent(context, mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig.toString())),
|
||||
tunnelConfig.id.toString() + APPEND_OFF,
|
||||
tunnelConfig.name.take((SHORT_LABEL_MAX_SIZE - APPEND_OFF.length)) + APPEND_OFF,
|
||||
tunnelConfig.name.take((LONG_LABEL_MAX_SIZE - APPEND_OFF.length)) + APPEND_OFF,
|
||||
R.drawable.vpn_off
|
||||
)
|
||||
}
|
||||
|
||||
fun removeTunnelShortcuts(context : Context, tunnelConfig : TunnelConfig) {
|
||||
ShortcutManagerCompat.removeDynamicShortcuts(context, listOf(tunnelConfig.id.toString() + APPEND_ON,
|
||||
tunnelConfig.id.toString() + APPEND_OFF ))
|
||||
}
|
||||
|
||||
private fun createTunnelOnIntent(context: Context, extras : Map<String,String>) : Intent {
|
||||
return Intent(context, ShortcutsActivity::class.java).also {
|
||||
it.action = Action.START.name
|
||||
it.putExtra(CLASS_NAME_EXTRA_KEY, WireGuardTunnelService::class.java.name)
|
||||
extras.forEach {(k, v) ->
|
||||
it.putExtra(k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createTunnelOffIntent(context : Context, extras : Map<String,String>) : Intent {
|
||||
return Intent(context, ShortcutsActivity::class.java).also {
|
||||
it.action = Action.STOP.name
|
||||
it.putExtra(CLASS_NAME_EXTRA_KEY, WireGuardTunnelService::class.java.name)
|
||||
extras.forEach {(k, v) ->
|
||||
it.putExtra(k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.tile
|
||||
|
||||
import android.os.Build
|
||||
import android.service.quicksettings.Tile
|
||||
import android.service.quicksettings.TileService
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class AutoTunnelControlTile : TileService() {
|
||||
|
||||
@Inject
|
||||
lateinit var appDataRepository: AppDataRepository
|
||||
|
||||
@Inject
|
||||
lateinit var serviceManager: ServiceManager
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
private var manualStartConfig: TunnelConfig? = null
|
||||
|
||||
override fun onStartListening() {
|
||||
super.onStartListening()
|
||||
scope.launch {
|
||||
appDataRepository.settings.getSettingsFlow().collectLatest {
|
||||
when (it.isAutoTunnelEnabled) {
|
||||
true -> {
|
||||
if (it.isAutoTunnelPaused) {
|
||||
setInactive()
|
||||
setTileDescription(this@AutoTunnelControlTile.getString(R.string.paused))
|
||||
} else {
|
||||
setActive()
|
||||
setTileDescription(this@AutoTunnelControlTile.getString(R.string.active))
|
||||
}
|
||||
}
|
||||
|
||||
false -> {
|
||||
setTileDescription(this@AutoTunnelControlTile.getString(R.string.disabled))
|
||||
setUnavailable()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTileAdded() {
|
||||
super.onTileAdded()
|
||||
onStartListening()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
override fun onTileRemoved() {
|
||||
super.onTileRemoved()
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
override fun onClick() {
|
||||
super.onClick()
|
||||
unlockAndRun {
|
||||
scope.launch {
|
||||
try {
|
||||
appDataRepository.toggleWatcherServicePause()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e.message)
|
||||
} finally {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setActive() {
|
||||
qsTile.state = Tile.STATE_ACTIVE
|
||||
qsTile.updateTile()
|
||||
}
|
||||
|
||||
private fun setInactive() {
|
||||
qsTile.state = Tile.STATE_INACTIVE
|
||||
qsTile.updateTile()
|
||||
}
|
||||
|
||||
private fun setUnavailable() {
|
||||
manualStartConfig = null
|
||||
qsTile.state = Tile.STATE_UNAVAILABLE
|
||||
qsTile.updateTile()
|
||||
}
|
||||
|
||||
private fun setTileDescription(description: String) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
qsTile.subtitle = description
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
qsTile.stateDescription = description
|
||||
}
|
||||
qsTile.updateTile()
|
||||
}
|
||||
}
|
||||
+63
-84
@@ -4,16 +4,13 @@ import android.os.Build
|
||||
import android.service.quicksettings.Tile
|
||||
import android.service.quicksettings.TileService
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
|
||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
@@ -23,36 +20,58 @@ import javax.inject.Inject
|
||||
class TunnelControlTile : TileService() {
|
||||
|
||||
@Inject
|
||||
lateinit var settingsRepo : SettingsDoa
|
||||
lateinit var appDataRepository: AppDataRepository
|
||||
|
||||
@Inject
|
||||
lateinit var configRepo : TunnelConfigDao
|
||||
lateinit var vpnService: VpnService
|
||||
|
||||
@Inject
|
||||
lateinit var vpnService : VpnService
|
||||
lateinit var serviceManager: ServiceManager
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.Main);
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
private lateinit var job : Job
|
||||
private var manualStartConfig: TunnelConfig? = null
|
||||
|
||||
override fun onStartListening() {
|
||||
job = scope.launch {
|
||||
updateTileState()
|
||||
}
|
||||
super.onStartListening()
|
||||
Timber.d("On start listening called")
|
||||
scope.launch {
|
||||
vpnService.vpnState.collect { it ->
|
||||
when (it.status) {
|
||||
Tunnel.State.UP -> {
|
||||
setActive()
|
||||
it.tunnelConfig?.name?.let { name -> setTileDescription(name) }
|
||||
}
|
||||
|
||||
Tunnel.State.DOWN -> {
|
||||
setInactive()
|
||||
val config = appDataRepository.getStartTunnelConfig()?.also { config ->
|
||||
manualStartConfig = config
|
||||
} ?: appDataRepository.getPrimaryOrFirstTunnel()
|
||||
config?.let {
|
||||
setTileDescription(it.name)
|
||||
} ?: setUnavailable()
|
||||
}
|
||||
|
||||
else -> setInactive()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTileAdded() {
|
||||
super.onTileAdded()
|
||||
qsTile.contentDescription = this.resources.getString(R.string.toggle_vpn)
|
||||
scope.launch {
|
||||
updateTileState();
|
||||
}
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
override fun onTileRemoved() {
|
||||
super.onTileRemoved()
|
||||
cancelJob()
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
override fun onTileAdded() {
|
||||
super.onTileAdded()
|
||||
onStartListening()
|
||||
}
|
||||
|
||||
override fun onClick() {
|
||||
@@ -60,16 +79,17 @@ class TunnelControlTile : TileService() {
|
||||
unlockAndRun {
|
||||
scope.launch {
|
||||
try {
|
||||
val tunnel = determineTileTunnel();
|
||||
if(tunnel != null) {
|
||||
attemptWatcherServiceToggle(tunnel.toString())
|
||||
if(vpnService.getState() == Tunnel.State.UP) {
|
||||
ServiceManager.stopVpnService(this@TunnelControlTile)
|
||||
} else {
|
||||
ServiceManager.startVpnServiceForeground(this@TunnelControlTile, tunnel.toString())
|
||||
}
|
||||
if (vpnService.getState() == Tunnel.State.UP) {
|
||||
serviceManager.stopVpnServiceForeground(
|
||||
this@TunnelControlTile,
|
||||
isManualStop = true,
|
||||
)
|
||||
} else {
|
||||
serviceManager.startVpnServiceForeground(
|
||||
this@TunnelControlTile, manualStartConfig?.id, isManualStart = true,
|
||||
)
|
||||
}
|
||||
} catch (e : Exception) {
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e.message)
|
||||
} finally {
|
||||
cancel()
|
||||
@@ -78,70 +98,29 @@ class TunnelControlTile : TileService() {
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun determineTileTunnel() : TunnelConfig? {
|
||||
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 setActive() {
|
||||
qsTile.state = Tile.STATE_ACTIVE
|
||||
qsTile.updateTile()
|
||||
}
|
||||
|
||||
|
||||
private fun attemptWatcherServiceToggle(tunnelConfig : String) {
|
||||
scope.launch {
|
||||
val settings = settingsRepo.getAll()
|
||||
if (settings.isNotEmpty()) {
|
||||
val setting = settings.first()
|
||||
if(setting.isAutoTunnelEnabled) {
|
||||
ServiceManager.toggleWatcherServiceForeground(this@TunnelControlTile, tunnelConfig)
|
||||
}
|
||||
}
|
||||
}
|
||||
private fun setInactive() {
|
||||
qsTile.state = Tile.STATE_INACTIVE
|
||||
qsTile.updateTile()
|
||||
}
|
||||
|
||||
private suspend fun updateTileState() {
|
||||
vpnService.state.collect {
|
||||
when(it) {
|
||||
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 setUnavailable() {
|
||||
manualStartConfig = null
|
||||
qsTile.state = Tile.STATE_UNAVAILABLE
|
||||
qsTile.updateTile()
|
||||
}
|
||||
|
||||
private fun setTileDescription(description : String) {
|
||||
private fun setTileDescription(description: String) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
qsTile.subtitle = description
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
qsTile.stateDescription = description;
|
||||
qsTile.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 {
|
||||
HEALTHY,
|
||||
UNHEALTHY,
|
||||
NEVER_CONNECTED,
|
||||
STALE,
|
||||
UNKNOWN,
|
||||
NOT_STARTED;
|
||||
|
||||
companion object {
|
||||
private const val WG_TYPICAL_HANDSHAKE_INTERVAL_WHEN_HEALTHY_SEC = 120
|
||||
const val UNHEALTHY_TIME_LIMIT_SEC = WG_TYPICAL_HANDSHAKE_INTERVAL_WHEN_HEALTHY_SEC + 60
|
||||
private const val WG_TYPICAL_HANDSHAKE_INTERVAL_WHEN_HEALTHY_SEC = 180
|
||||
const val STATUS_CHANGE_TIME_BUFFER = 30
|
||||
const val STALE_TIME_LIMIT_SEC =
|
||||
WG_TYPICAL_HANDSHAKE_INTERVAL_WHEN_HEALTHY_SEC + STATUS_CHANGE_TIME_BUFFER
|
||||
const val NEVER_CONNECTED_TO_UNHEALTHY_TIME_LIMIT_SEC = 30
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.tunnel
|
||||
|
||||
import com.wireguard.android.backend.Statistics
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.wireguard.crypto.Key
|
||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
interface VpnService : Tunnel {
|
||||
suspend fun startTunnel(tunnelConfig : TunnelConfig) : Tunnel.State
|
||||
suspend fun startTunnel(tunnelConfig: TunnelConfig? = null): Tunnel.State
|
||||
|
||||
suspend fun stopTunnel()
|
||||
val state : SharedFlow<Tunnel.State>
|
||||
val tunnelName : SharedFlow<String>
|
||||
val statistics : SharedFlow<Statistics>
|
||||
val lastHandshake : SharedFlow<Map<Key,Long>>
|
||||
val handshakeStatus : SharedFlow<HandshakeStatus>
|
||||
fun getState() : Tunnel.State
|
||||
}
|
||||
|
||||
val vpnState: StateFlow<VpnState>
|
||||
|
||||
fun getState(): Tunnel.State
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.tunnel
|
||||
|
||||
import com.wireguard.android.backend.Statistics
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
|
||||
data class VpnState(
|
||||
val status: Tunnel.State = Tunnel.State.DOWN,
|
||||
val tunnelConfig: TunnelConfig? = null,
|
||||
val statistics: Statistics? = null
|
||||
)
|
||||
+103
-89
@@ -3,129 +3,143 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel
|
||||
import com.wireguard.android.backend.Backend
|
||||
import com.wireguard.android.backend.BackendException
|
||||
import com.wireguard.android.backend.Statistics
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.wireguard.crypto.Key
|
||||
import com.zaneschepke.wireguardautotunnel.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||
import com.wireguard.android.backend.Tunnel.State
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.Kernel
|
||||
import com.zaneschepke.wireguardautotunnel.module.Userspace
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
class WireGuardTunnel @Inject constructor(private val backend : Backend,
|
||||
class WireGuardTunnel
|
||||
@Inject
|
||||
constructor(
|
||||
@Userspace private val userspaceBackend: Backend,
|
||||
@Kernel private val kernelBackend: Backend,
|
||||
private val appDataRepository: AppDataRepository,
|
||||
) : VpnService {
|
||||
private val _vpnState = MutableStateFlow(VpnState())
|
||||
override val vpnState: StateFlow<VpnState> = _vpnState.asStateFlow()
|
||||
|
||||
private val _tunnelName = MutableStateFlow("")
|
||||
override val tunnelName get() = _tunnelName.asStateFlow()
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
private val _state = MutableSharedFlow<Tunnel.State>(
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||
replay = 1)
|
||||
private var statsJob: Job? = null
|
||||
|
||||
private val _handshakeStatus = MutableSharedFlow<HandshakeStatus>(replay = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
override val state get() = _state.asSharedFlow()
|
||||
private var backend: Backend = userspaceBackend
|
||||
|
||||
private val _statistics = MutableSharedFlow<Statistics>(replay = 1)
|
||||
override val statistics get() = _statistics.asSharedFlow()
|
||||
private var backendIsUserspace = true
|
||||
|
||||
private val _lastHandshake = MutableSharedFlow<Map<Key, Long>>(replay = 1)
|
||||
override val lastHandshake get() = _lastHandshake.asSharedFlow()
|
||||
|
||||
override val handshakeStatus: SharedFlow<HandshakeStatus>
|
||||
get() = _handshakeStatus.asSharedFlow()
|
||||
|
||||
private lateinit var statsJob : Job
|
||||
|
||||
|
||||
override suspend fun startTunnel(tunnelConfig: TunnelConfig) : Tunnel.State{
|
||||
return try {
|
||||
if(getState() == Tunnel.State.UP && _tunnelName.value != tunnelConfig.name) {
|
||||
stopTunnel()
|
||||
init {
|
||||
scope.launch {
|
||||
appDataRepository.settings.getSettingsFlow().collect {
|
||||
if (it.isKernelEnabled && backendIsUserspace) {
|
||||
Timber.d("Setting kernel backend")
|
||||
backend = kernelBackend
|
||||
backendIsUserspace = false
|
||||
} else if (!it.isKernelEnabled && !backendIsUserspace) {
|
||||
Timber.d("Setting userspace backend")
|
||||
backend = userspaceBackend
|
||||
backendIsUserspace = true
|
||||
}
|
||||
}
|
||||
_tunnelName.emit(tunnelConfig.name)
|
||||
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
||||
val state = backend.setState(
|
||||
this, Tunnel.State.UP, config)
|
||||
_state.emit(state)
|
||||
state;
|
||||
} catch (e : Exception) {
|
||||
Timber.e("Failed to start tunnel with error: ${e.message}")
|
||||
Tunnel.State.DOWN
|
||||
}
|
||||
}
|
||||
|
||||
override fun getName(): String {
|
||||
return _tunnelName.value
|
||||
override suspend fun startTunnel(tunnelConfig: TunnelConfig?): State {
|
||||
return try {
|
||||
//TODO we need better error handling here
|
||||
val config = tunnelConfig ?: appDataRepository.getPrimaryOrFirstTunnel()
|
||||
if (config != null) {
|
||||
emitTunnelConfig(config)
|
||||
val wgConfig = TunnelConfig.configFromQuick(config.wgQuick)
|
||||
val state =
|
||||
backend.setState(
|
||||
this,
|
||||
State.UP,
|
||||
wgConfig,
|
||||
)
|
||||
state
|
||||
} else throw Exception("No tunnels")
|
||||
} catch (e: BackendException) {
|
||||
Timber.e("Failed to start tunnel with error: ${e.message}")
|
||||
State.DOWN
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun stopTunnel() {
|
||||
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 emitTunnelConfig(tunnelConfig: TunnelConfig?) {
|
||||
_vpnState.emit(
|
||||
_vpnState.value.copy(
|
||||
tunnelConfig = tunnelConfig,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun stopTunnel() {
|
||||
try {
|
||||
if(getState() == Tunnel.State.UP) {
|
||||
val state = backend.setState(this, Tunnel.State.DOWN, null)
|
||||
_state.emit(state)
|
||||
if (getState() == State.UP) {
|
||||
val state = backend.setState(this, State.DOWN, null)
|
||||
emitTunnelState(state)
|
||||
}
|
||||
} catch (e : BackendException) {
|
||||
} catch (e: BackendException) {
|
||||
Timber.e("Failed to stop tunnel with error: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun getState(): Tunnel.State {
|
||||
override fun getState(): State {
|
||||
return backend.getState(this)
|
||||
}
|
||||
|
||||
override fun onStateChange(state : Tunnel.State) {
|
||||
val tunnel = this;
|
||||
_state.tryEmit(state)
|
||||
if(state == Tunnel.State.UP) {
|
||||
statsJob = CoroutineScope(Dispatchers.IO).launch {
|
||||
val handshakeMap = HashMap<Key, Long>()
|
||||
var neverHadHandshakeCounter = 0
|
||||
while (true) {
|
||||
val statistics = backend.getStatistics(tunnel)
|
||||
_statistics.emit(statistics)
|
||||
statistics.peers().forEach {
|
||||
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)
|
||||
}
|
||||
override fun getName(): String {
|
||||
return _vpnState.value.tunnelConfig?.name ?: ""
|
||||
}
|
||||
|
||||
override fun onStateChange(state: State) {
|
||||
val tunnel = this
|
||||
emitTunnelState(state)
|
||||
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(WireGuardAutoTunnel.instance)
|
||||
if (state == State.UP) {
|
||||
statsJob =
|
||||
scope.launch {
|
||||
while (true) {
|
||||
val statistics = backend.getStatistics(tunnel)
|
||||
emitBackendStatistics(statistics)
|
||||
delay(Constants.VPN_STATISTIC_CHECK_INTERVAL)
|
||||
}
|
||||
_lastHandshake.emit(handshakeMap)
|
||||
delay(Constants.VPN_STATISTIC_CHECK_INTERVAL)
|
||||
}
|
||||
}
|
||||
}
|
||||
if(state == Tunnel.State.DOWN) {
|
||||
if(this::statsJob.isInitialized) {
|
||||
statsJob.cancel()
|
||||
if (state == State.DOWN) {
|
||||
try {
|
||||
statsJob?.cancel()
|
||||
} catch (e : CancellationException) {
|
||||
Timber.i("Stats job cancelled")
|
||||
}
|
||||
_handshakeStatus.tryEmit(HandshakeStatus.NOT_STARTED)
|
||||
_lastHandshake.tryEmit(emptyMap())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui
|
||||
|
||||
data class AppUiState(
|
||||
val snackbarMessage: String = "",
|
||||
val snackbarMessageConsumed: Boolean = true,
|
||||
val vpnPermissionAccepted: Boolean = false,
|
||||
val notificationPermissionAccepted: Boolean = false,
|
||||
val requestPermissions: Boolean = false
|
||||
)
|
||||
@@ -0,0 +1,148 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui
|
||||
|
||||
import android.app.Application
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.wireguard.android.backend.GoBackend
|
||||
import com.zaneschepke.logcatter.Logcatter
|
||||
import com.zaneschepke.logcatter.model.LogMessage
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||
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 java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class AppViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val application: Application,
|
||||
) : ViewModel() {
|
||||
|
||||
val vpnIntent: Intent? = GoBackend.VpnService.prepare(WireGuardAutoTunnel.instance)
|
||||
|
||||
private val _appUiState = MutableStateFlow(
|
||||
AppUiState(
|
||||
vpnPermissionAccepted = vpnIntent == null,
|
||||
),
|
||||
)
|
||||
val appUiState = _appUiState.asStateFlow()
|
||||
|
||||
|
||||
fun isRequiredPermissionGranted(): Boolean {
|
||||
val allAccepted =
|
||||
(_appUiState.value.vpnPermissionAccepted && _appUiState.value.vpnPermissionAccepted)
|
||||
if (!allAccepted) requestPermissions()
|
||||
return allAccepted
|
||||
}
|
||||
|
||||
private fun requestPermissions() {
|
||||
_appUiState.value = _appUiState.value.copy(
|
||||
requestPermissions = true,
|
||||
)
|
||||
}
|
||||
|
||||
fun permissionsRequested() {
|
||||
_appUiState.value = _appUiState.value.copy(
|
||||
requestPermissions = false,
|
||||
)
|
||||
}
|
||||
|
||||
fun openWebPage(url: String) {
|
||||
try {
|
||||
val webpage: Uri = Uri.parse(url)
|
||||
val intent = Intent(Intent.ACTION_VIEW, webpage).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
application.startActivity(intent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Timber.e(e)
|
||||
showSnackbarMessage(application.getString(R.string.no_browser_detected))
|
||||
}
|
||||
}
|
||||
|
||||
fun onVpnPermissionAccepted() {
|
||||
_appUiState.value = _appUiState.value.copy(
|
||||
vpnPermissionAccepted = true,
|
||||
)
|
||||
}
|
||||
|
||||
fun launchEmail() {
|
||||
try {
|
||||
val intent =
|
||||
Intent(Intent.ACTION_SENDTO).apply {
|
||||
type = Constants.EMAIL_MIME_TYPE
|
||||
putExtra(Intent.EXTRA_EMAIL, arrayOf(application.getString(R.string.my_email)))
|
||||
putExtra(Intent.EXTRA_SUBJECT, application.getString(R.string.email_subject))
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
application.startActivity(
|
||||
Intent.createChooser(intent, application.getString(R.string.email_chooser)).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
},
|
||||
)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Timber.e(e)
|
||||
showSnackbarMessage(application.getString(R.string.no_email_detected))
|
||||
}
|
||||
}
|
||||
|
||||
fun showSnackbarMessage(message: String) {
|
||||
_appUiState.value = _appUiState.value.copy(
|
||||
snackbarMessage = message,
|
||||
snackbarMessageConsumed = false,
|
||||
)
|
||||
}
|
||||
|
||||
fun snackbarMessageConsumed() {
|
||||
_appUiState.value = _appUiState.value.copy(
|
||||
snackbarMessage = "",
|
||||
snackbarMessageConsumed = true,
|
||||
)
|
||||
}
|
||||
|
||||
val logs = mutableStateListOf<LogMessage>()
|
||||
|
||||
fun readLogCatOutput() =
|
||||
viewModelScope.launch(viewModelScope.coroutineContext + Dispatchers.IO) {
|
||||
launch {
|
||||
Logcatter.logs(callback = {
|
||||
logs.add(it)
|
||||
if (logs.size > Constants.LOG_BUFFER_SIZE) {
|
||||
logs.removeRange(0, (logs.size - Constants.LOG_BUFFER_SIZE).toInt())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fun clearLogs() {
|
||||
logs.clear()
|
||||
Logcatter.clear()
|
||||
}
|
||||
|
||||
fun saveLogsToFile() {
|
||||
val fileName = "${Constants.BASE_LOG_FILE_NAME}-${Instant.now().epochSecond}.txt"
|
||||
val content = logs.joinToString(separator = "\n")
|
||||
FileUtils.saveFileToDownloads(application.applicationContext, content, fileName)
|
||||
Toast.makeText(application, application.getString(R.string.logs_saved), Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
|
||||
fun setNotificationPermissionAccepted(accepted: Boolean) {
|
||||
_appUiState.value = _appUiState.value.copy(
|
||||
notificationPermissionAccepted = accepted,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui;
|
||||
|
||||
import com.journeyapps.barcodescanner.CaptureActivity;
|
||||
|
||||
public class CaptureActivityPortrait extends CaptureActivity {
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui
|
||||
|
||||
import com.journeyapps.barcodescanner.CaptureActivity
|
||||
|
||||
class CaptureActivityPortrait : CaptureActivity()
|
||||
@@ -1,199 +1,265 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.view.KeyEvent
|
||||
import androidx.activity.SystemBarStyle
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarData
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.SnackbarResult
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.input.key.onKeyEvent
|
||||
import com.google.accompanist.navigation.animation.AnimatedNavHost
|
||||
import com.google.accompanist.navigation.animation.composable
|
||||
import com.google.accompanist.navigation.animation.rememberAnimatedNavController
|
||||
import androidx.compose.ui.focus.focusProperties
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.wireguard.android.backend.GoBackend
|
||||
import com.zaneschepke.wireguardautotunnel.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.PermissionRequestFailedScreen
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.detail.DetailScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.options.OptionsScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.pinlock.PinLockScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.TransparentSystemBars
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.logs.LogsScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import timber.log.Timber
|
||||
import java.lang.IllegalStateException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
@OptIn(ExperimentalAnimationApi::class,
|
||||
ExperimentalPermissionsApi::class
|
||||
@Inject
|
||||
lateinit var dataStoreManager: DataStoreManager
|
||||
|
||||
@Inject
|
||||
lateinit var settingsRepository: SettingsRepository
|
||||
|
||||
@Inject
|
||||
lateinit var serviceManager: ServiceManager
|
||||
|
||||
@OptIn(
|
||||
ExperimentalPermissionsApi::class,
|
||||
)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
enableEdgeToEdge(navigationBarStyle = SystemBarStyle.dark(Color.Transparent.toArgb()))
|
||||
|
||||
// load preferences into memory and init data
|
||||
lifecycleScope.launch {
|
||||
dataStoreManager.init()
|
||||
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(this@MainActivity)
|
||||
val settings = settingsRepository.getSettings()
|
||||
if (settings.isAutoTunnelEnabled) {
|
||||
serviceManager.startWatcherService(application.applicationContext)
|
||||
}
|
||||
}
|
||||
setContent {
|
||||
val navController = rememberAnimatedNavController()
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val appViewModel = hiltViewModel<AppViewModel>()
|
||||
val appUiState by appViewModel.appUiState.collectAsStateWithLifecycle()
|
||||
val navController = rememberNavController()
|
||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||
|
||||
WireguardAutoTunnelTheme {
|
||||
TransparentSystemBars()
|
||||
val notificationPermissionState =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
|
||||
rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS) else null
|
||||
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
val notificationPermissionState =
|
||||
rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS)
|
||||
|
||||
fun requestNotificationPermission() {
|
||||
if (!notificationPermissionState.status.isGranted && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
notificationPermissionState.launchPermissionRequest()
|
||||
}
|
||||
}
|
||||
|
||||
var vpnIntent by remember { mutableStateOf(GoBackend.VpnService.prepare(this)) }
|
||||
val vpnActivityResultState = rememberLauncherForActivityResult(
|
||||
val vpnActivityResultState =
|
||||
rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult(),
|
||||
onResult = {
|
||||
val accepted = (it.resultCode == RESULT_OK)
|
||||
if (accepted) {
|
||||
vpnIntent = null
|
||||
appViewModel.onVpnPermissionAccepted()
|
||||
}
|
||||
})
|
||||
LaunchedEffect(vpnIntent) {
|
||||
if (vpnIntent != null) {
|
||||
vpnActivityResultState.launch(vpnIntent)
|
||||
} else requestNotificationPermission()
|
||||
}
|
||||
|
||||
Scaffold(snackbarHost = { SnackbarHost(snackbarHostState)},
|
||||
modifier = Modifier.onKeyEvent {
|
||||
if (it.nativeKeyEvent.action == KeyEvent.ACTION_UP) {
|
||||
when (it.nativeKeyEvent.keyCode) {
|
||||
KeyEvent.KEYCODE_DPAD_UP -> {
|
||||
try {
|
||||
focusRequester.requestFocus()
|
||||
} catch(e : IllegalStateException) {
|
||||
Timber.e("No D-Pad focus request modifier added to element on screen")
|
||||
}
|
||||
false
|
||||
} else -> {
|
||||
false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
},
|
||||
bottomBar = if (vpnIntent == null && notificationPermissionState.status.isGranted) {
|
||||
{ BottomNavBar(navController, Routes.navItems) }
|
||||
} else {
|
||||
{}
|
||||
},
|
||||
)
|
||||
{ padding ->
|
||||
if (vpnIntent != null) {
|
||||
PermissionRequestFailedScreen(
|
||||
padding = padding,
|
||||
onRequestAgain = { vpnActivityResultState.launch(vpnIntent) },
|
||||
message = getString(R.string.vpn_permission_required),
|
||||
getString(R.string.retry)
|
||||
)
|
||||
return@Scaffold
|
||||
}
|
||||
if (!notificationPermissionState.status.isGranted) {
|
||||
PermissionRequestFailedScreen(
|
||||
padding = padding,
|
||||
onRequestAgain = {
|
||||
val intentSettings =
|
||||
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||
intentSettings.data =
|
||||
Uri.fromParts("package", this.packageName, null)
|
||||
startActivity(intentSettings);
|
||||
},
|
||||
message = getString(R.string.notification_permission_required),
|
||||
getString(R.string.open_settings)
|
||||
)
|
||||
return@Scaffold
|
||||
}
|
||||
AnimatedNavHost(navController, startDestination = Routes.Main.name) {
|
||||
composable(Routes.Main.name, enterTransition = {
|
||||
when (initialState.destination.route) {
|
||||
Routes.Settings.name, Routes.Support.name ->
|
||||
slideInHorizontally(
|
||||
initialOffsetX = { -Constants.SLIDE_IN_TRANSITION_OFFSET },
|
||||
animationSpec = tween(Constants.SLIDE_IN_ANIMATION_DURATION)
|
||||
)
|
||||
|
||||
else -> {
|
||||
fadeIn(animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION))
|
||||
}
|
||||
}
|
||||
}) {
|
||||
MainScreen(padding = padding, snackbarHostState = snackbarHostState, navController = navController)
|
||||
fun showSnackBarMessage(message: StringValue) {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
val result =
|
||||
snackbarHostState.showSnackbar(
|
||||
message = message.asString(this@MainActivity),
|
||||
duration = SnackbarDuration.Short,
|
||||
)
|
||||
when (result) {
|
||||
SnackbarResult.ActionPerformed,
|
||||
SnackbarResult.Dismissed -> {
|
||||
snackbarHostState.currentSnackbarData?.dismiss()
|
||||
}
|
||||
composable(Routes.Settings.name, enterTransition = {
|
||||
when (initialState.destination.route) {
|
||||
Routes.Main.name ->
|
||||
slideInHorizontally(
|
||||
initialOffsetX = { Constants.SLIDE_IN_TRANSITION_OFFSET },
|
||||
animationSpec = tween(Constants.SLIDE_IN_ANIMATION_DURATION)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Routes.Support.name -> {
|
||||
slideInHorizontally(
|
||||
initialOffsetX = { -Constants.SLIDE_IN_TRANSITION_OFFSET },
|
||||
animationSpec = tween(Constants.SLIDE_IN_ANIMATION_DURATION)
|
||||
)
|
||||
}
|
||||
LaunchedEffect(appUiState.requestPermissions) {
|
||||
if (appUiState.requestPermissions) {
|
||||
appViewModel.permissionsRequested()
|
||||
if (notificationPermissionState != null && !notificationPermissionState.status.isGranted
|
||||
) {
|
||||
showSnackBarMessage(StringValue.StringResource(R.string.notification_permission_required))
|
||||
return@LaunchedEffect notificationPermissionState.launchPermissionRequest()
|
||||
}
|
||||
if (!appUiState.vpnPermissionAccepted) {
|
||||
return@LaunchedEffect vpnActivityResultState.launch(appViewModel.vpnIntent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
fadeIn(animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION))
|
||||
}
|
||||
WireguardAutoTunnelTheme {
|
||||
LaunchedEffect(Unit) {
|
||||
appViewModel.setNotificationPermissionAccepted(
|
||||
notificationPermissionState?.status?.isGranted ?: true,
|
||||
)
|
||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) appViewModel.readLogCatOutput()
|
||||
}
|
||||
|
||||
LaunchedEffect(appUiState.snackbarMessageConsumed) {
|
||||
if (!appUiState.snackbarMessageConsumed) {
|
||||
showSnackBarMessage(StringValue.DynamicString(appUiState.snackbarMessage))
|
||||
appViewModel.snackbarMessageConsumed()
|
||||
}
|
||||
}
|
||||
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
Scaffold(
|
||||
snackbarHost = {
|
||||
SnackbarHost(snackbarHostState) { snackbarData: SnackbarData ->
|
||||
CustomSnackBar(
|
||||
snackbarData.visuals.message,
|
||||
isRtl = false,
|
||||
containerColor =
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(
|
||||
2.dp,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
//TODO refactor
|
||||
modifier = Modifier
|
||||
.focusable()
|
||||
.focusProperties {
|
||||
when (navBackStackEntry?.destination?.route) {
|
||||
Screen.Lock.route -> Unit
|
||||
else -> up = focusRequester
|
||||
}
|
||||
}) { SettingsScreen(padding = padding, snackbarHostState = snackbarHostState, navController = navController, 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))
|
||||
}
|
||||
},
|
||||
bottomBar = {
|
||||
BottomNavBar(
|
||||
navController,
|
||||
listOf(
|
||||
Screen.Main.navItem,
|
||||
Screen.Settings.navItem,
|
||||
Screen.Support.navItem,
|
||||
),
|
||||
)
|
||||
},
|
||||
) { padding ->
|
||||
NavHost(
|
||||
navController,
|
||||
startDestination =
|
||||
(if (PinManager.pinExists()) Screen.Lock.route else Screen.Main.route),
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(padding)
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
composable(
|
||||
Screen.Main.route,
|
||||
) {
|
||||
MainScreen(
|
||||
focusRequester = focusRequester,
|
||||
appViewModel = appViewModel,
|
||||
navController = navController,
|
||||
)
|
||||
}
|
||||
composable(
|
||||
Screen.Settings.route,
|
||||
) {
|
||||
SettingsScreen(
|
||||
appViewModel = appViewModel,
|
||||
navController = navController,
|
||||
focusRequester = focusRequester,
|
||||
)
|
||||
}
|
||||
composable(
|
||||
Screen.Support.route,
|
||||
) {
|
||||
SupportScreen(
|
||||
focusRequester = focusRequester,
|
||||
appViewModel = appViewModel,
|
||||
navController = navController,
|
||||
)
|
||||
}
|
||||
composable(Screen.Support.Logs.route) {
|
||||
LogsScreen(appViewModel)
|
||||
}
|
||||
composable("${Screen.Config.route}/{id}") {
|
||||
val id = it.arguments?.getString("id")
|
||||
if (!id.isNullOrBlank()) {
|
||||
ConfigScreen(
|
||||
navController = navController,
|
||||
tunnelId = id,
|
||||
appViewModel = appViewModel,
|
||||
focusRequester = focusRequester,
|
||||
)
|
||||
}
|
||||
}) { SupportScreen(padding = padding, focusRequester) }
|
||||
composable("${Routes.Config.name}/{id}", enterTransition = {
|
||||
fadeIn(animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION))
|
||||
}) { ConfigScreen(padding = padding, navController = navController, id = it.arguments?.getString("id"), focusRequester = focusRequester)}
|
||||
composable("${Routes.Detail.name}/{id}", enterTransition = {
|
||||
fadeIn(animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION))
|
||||
}) { DetailScreen(padding = padding, id = it.arguments?.getString("id")) }
|
||||
}
|
||||
composable("${Screen.Option.route}/{id}") {
|
||||
val id = it.arguments?.getString("id")
|
||||
if (!id.isNullOrBlank()) {
|
||||
OptionsScreen(
|
||||
navController = navController,
|
||||
tunnelId = id,
|
||||
appViewModel = appViewModel,
|
||||
focusRequester = focusRequester,
|
||||
)
|
||||
}
|
||||
}
|
||||
composable(Screen.Lock.route) {
|
||||
PinLockScreen(
|
||||
navController = navController,
|
||||
appViewModel = appViewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Home
|
||||
import androidx.compose.material.icons.rounded.QuestionMark
|
||||
import androidx.compose.material.icons.rounded.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
|
||||
|
||||
enum class Routes {
|
||||
Main,
|
||||
Settings,
|
||||
Support,
|
||||
Config,
|
||||
Detail;
|
||||
|
||||
|
||||
companion object {
|
||||
val navItems = listOf(
|
||||
BottomNavItem(
|
||||
name = "Tunnels",
|
||||
route = Main.name,
|
||||
icon = Icons.Rounded.Home,
|
||||
),
|
||||
BottomNavItem(
|
||||
name = "Settings",
|
||||
route = Settings.name,
|
||||
icon = Icons.Rounded.Settings,
|
||||
),
|
||||
BottomNavItem(
|
||||
name = "Support",
|
||||
route = Support.name,
|
||||
icon = Icons.Rounded.QuestionMark,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Home
|
||||
import androidx.compose.material.icons.rounded.QuestionMark
|
||||
import androidx.compose.material.icons.rounded.Settings
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
|
||||
sealed class Screen(val route: String) {
|
||||
data object Main : Screen("main") {
|
||||
val navItem =
|
||||
BottomNavItem(
|
||||
name = WireGuardAutoTunnel.instance.getString(R.string.tunnels),
|
||||
route = route,
|
||||
icon = Icons.Rounded.Home,
|
||||
)
|
||||
}
|
||||
|
||||
data object Settings : Screen("settings") {
|
||||
val navItem =
|
||||
BottomNavItem(
|
||||
name = WireGuardAutoTunnel.instance.getString(R.string.settings),
|
||||
route = route,
|
||||
icon = Icons.Rounded.Settings,
|
||||
)
|
||||
}
|
||||
|
||||
data object Support : Screen("support") {
|
||||
val navItem =
|
||||
BottomNavItem(
|
||||
name = WireGuardAutoTunnel.instance.getString(R.string.support),
|
||||
route = route,
|
||||
icon = Icons.Rounded.QuestionMark,
|
||||
)
|
||||
|
||||
data object Logs : Screen("support/logs")
|
||||
}
|
||||
|
||||
data object Config : Screen("config")
|
||||
data object Lock : Screen("lock")
|
||||
|
||||
data object Option : Screen("option")
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui
|
||||
|
||||
data class ViewState(
|
||||
val showSnackbarMessage : Boolean = false,
|
||||
val snackbarMessage : String = "",
|
||||
val snackbarActionText : String = "",
|
||||
val onSnackbarActionClick : () -> Unit = {},
|
||||
val isLoading : Boolean = false
|
||||
)
|
||||
+23
-12
@@ -3,29 +3,40 @@ package com.zaneschepke.wireguardautotunnel.ui.common
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
|
||||
@Composable
|
||||
fun ClickableIconButton(onIconClick : () -> Unit, text : String, icon : ImageVector, enabled : Boolean) {
|
||||
Button(onClick = {},
|
||||
enabled = enabled
|
||||
fun ClickableIconButton(
|
||||
onClick: () -> Unit,
|
||||
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))
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = "Delete",
|
||||
modifier = Modifier.size(ButtonDefaults.IconSize).clickable {
|
||||
if(enabled) {
|
||||
onIconClick()
|
||||
}
|
||||
}
|
||||
contentDescription = icon.name,
|
||||
modifier =
|
||||
Modifier
|
||||
.size(ButtonDefaults.IconSize)
|
||||
.weight(1f, false)
|
||||
.clickable {
|
||||
if (enabled) {
|
||||
onIconClick()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-35
@@ -1,35 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.common
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun PermissionRequestFailedScreen(padding : PaddingValues, onRequestAgain : () -> Unit, message : String, buttonText : String ) {
|
||||
val scope = rememberCoroutineScope()
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)) {
|
||||
Text(message, textAlign = TextAlign.Center, modifier = Modifier.padding(15.dp))
|
||||
Button(onClick = {
|
||||
scope.launch {
|
||||
onRequestAgain()
|
||||
}
|
||||
}) {
|
||||
Text(buttonText)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,55 +1,92 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.common
|
||||
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.draw.clip
|
||||
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)
|
||||
@Composable
|
||||
fun RowListItem(leadingIcon : ImageVector? = null, leadingIconColor : Color = Color.Gray, 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(
|
||||
modifier = Modifier
|
||||
modifier =
|
||||
Modifier
|
||||
.animateContentSize()
|
||||
.clip(RoundedCornerShape(30.dp))
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
onClick()
|
||||
},
|
||||
onLongClick = {
|
||||
onHold()
|
||||
}
|
||||
)
|
||||
onClick = { onClick() },
|
||||
onLongClick = { onHold() },
|
||||
),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(14.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically,) {
|
||||
if(leadingIcon != null) {
|
||||
Icon(
|
||||
leadingIcon, "status",
|
||||
tint = leadingIconColor,
|
||||
modifier = Modifier.padding(end = 10.dp).size(15.dp)
|
||||
)
|
||||
Column {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 15.dp, vertical = 5.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
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,
|
||||
) {
|
||||
//TODO change these to string resources
|
||||
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)
|
||||
}
|
||||
}
|
||||
Text(text)
|
||||
}
|
||||
|
||||
rowButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,9 +25,7 @@ import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
|
||||
@Composable
|
||||
fun SearchBar(
|
||||
onQuery : (queryString : String) -> Unit
|
||||
) {
|
||||
fun SearchBar(onQuery: (queryString: String) -> Unit) {
|
||||
// Immediately update and keep track of query from text field changes.
|
||||
var query: String by rememberSaveable { mutableStateOf("") }
|
||||
var showClearIcon by rememberSaveable { mutableStateOf(false) }
|
||||
@@ -49,7 +47,7 @@ fun SearchBar(
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Search,
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
contentDescription = stringResource(id = R.string.search_icon)
|
||||
contentDescription = stringResource(id = R.string.search_icon),
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
@@ -58,13 +56,14 @@ fun SearchBar(
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Clear,
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
contentDescription = stringResource(id = R.string.clear_icon)
|
||||
contentDescription = stringResource(id = R.string.clear_icon),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
maxLines = 1,
|
||||
colors = TextFieldDefaults.colors(
|
||||
colors =
|
||||
TextFieldDefaults.colors(
|
||||
focusedContainerColor = Color.Transparent,
|
||||
unfocusedContainerColor = Color.Transparent,
|
||||
disabledContainerColor = Color.Transparent,
|
||||
@@ -73,8 +72,9 @@ fun SearchBar(
|
||||
textStyle = MaterialTheme.typography.bodySmall,
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
|
||||
modifier = Modifier
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.background(color = MaterialTheme.colorScheme.background, shape = RectangleShape)
|
||||
.background(color = MaterialTheme.colorScheme.background, shape = RectangleShape),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.common.config
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
|
||||
@Composable
|
||||
fun ConfigurationTextBox(
|
||||
value: String,
|
||||
hint: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
keyboardActions: KeyboardActions,
|
||||
label: String,
|
||||
modifier: Modifier
|
||||
) {
|
||||
OutlinedTextField(
|
||||
modifier = modifier,
|
||||
value = value,
|
||||
singleLine = true,
|
||||
onValueChange = { onValueChange(it) },
|
||||
label = { Text(label) },
|
||||
maxLines = 1,
|
||||
placeholder = { Text(hint) },
|
||||
keyboardOptions =
|
||||
KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.None,
|
||||
imeAction = ImeAction.Done,
|
||||
),
|
||||
keyboardActions = keyboardActions,
|
||||
)
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.common.config
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.Dp
|
||||
|
||||
@Composable
|
||||
fun ConfigurationToggle(
|
||||
label: String,
|
||||
enabled: Boolean,
|
||||
checked: Boolean,
|
||||
padding: Dp,
|
||||
onCheckChanged: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(padding),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Text(label, textAlign = TextAlign.Start, modifier = Modifier
|
||||
.weight(
|
||||
weight = 1.0f,
|
||||
fill = false,
|
||||
),
|
||||
softWrap = true)
|
||||
Switch(
|
||||
modifier = modifier,
|
||||
enabled = enabled,
|
||||
checked = checked,
|
||||
onCheckedChange = { onCheckChanged() },
|
||||
)
|
||||
}
|
||||
}
|
||||
+20
-6
@@ -6,19 +6,33 @@ import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.Screen
|
||||
|
||||
@Composable
|
||||
fun BottomNavBar(navController : NavController, bottomNavItems : List<BottomNavItem>) {
|
||||
|
||||
fun BottomNavBar(navController: NavController, bottomNavItems: List<BottomNavItem>) {
|
||||
val backStackEntry = navController.currentBackStackEntryAsState()
|
||||
|
||||
var showBottomBar by rememberSaveable { mutableStateOf(true) }
|
||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||
|
||||
//TODO find a better way to hide nav bar
|
||||
showBottomBar = when (navBackStackEntry?.destination?.route) {
|
||||
Screen.Lock.route -> false
|
||||
else -> true
|
||||
}
|
||||
|
||||
NavigationBar(
|
||||
containerColor = MaterialTheme.colorScheme.background,
|
||||
containerColor = if (!showBottomBar) Color.Transparent else MaterialTheme.colorScheme.background,
|
||||
) {
|
||||
bottomNavItems.forEach { item ->
|
||||
if (showBottomBar) bottomNavItems.forEach { item ->
|
||||
val selected = item.route == backStackEntry.value?.destination?.route
|
||||
|
||||
NavigationBarItem(
|
||||
@@ -35,8 +49,8 @@ fun BottomNavBar(navController : NavController, bottomNavItems : List<BottomNavI
|
||||
imageVector = item.icon,
|
||||
contentDescription = "${item.name} Icon",
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.common.prompt
|
||||
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
|
||||
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
|
||||
@Composable
|
||||
fun AuthorizationPrompt(onSuccess: () -> Unit, onFailure: () -> Unit, onError: (String) -> Unit) {
|
||||
val context = LocalContext.current
|
||||
val biometricManager = BiometricManager.from(context)
|
||||
val bio = biometricManager.canAuthenticate(BIOMETRIC_WEAK or DEVICE_CREDENTIAL)
|
||||
val isBiometricAvailable = remember {
|
||||
when (bio) {
|
||||
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> {
|
||||
onError("Biometrics not available")
|
||||
false
|
||||
}
|
||||
|
||||
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
|
||||
onError("Biometrics not created")
|
||||
false
|
||||
}
|
||||
|
||||
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> {
|
||||
onError("Biometric hardware not found")
|
||||
false
|
||||
}
|
||||
|
||||
BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> {
|
||||
onError("Biometric security update required")
|
||||
false
|
||||
}
|
||||
|
||||
BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> {
|
||||
onError("Biometrics not supported")
|
||||
false
|
||||
}
|
||||
|
||||
BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> {
|
||||
onError("Biometrics status unknown")
|
||||
false
|
||||
}
|
||||
|
||||
BiometricManager.BIOMETRIC_SUCCESS -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
if (isBiometricAvailable) {
|
||||
val executor = remember { ContextCompat.getMainExecutor(context) }
|
||||
|
||||
val promptInfo =
|
||||
BiometricPrompt.PromptInfo.Builder()
|
||||
.setAllowedAuthenticators(BIOMETRIC_WEAK or DEVICE_CREDENTIAL)
|
||||
.setTitle("Biometric Authentication")
|
||||
.setSubtitle("Log in using your biometric credential")
|
||||
.build()
|
||||
|
||||
val biometricPrompt =
|
||||
BiometricPrompt(
|
||||
context as FragmentActivity,
|
||||
executor,
|
||||
object : BiometricPrompt.AuthenticationCallback() {
|
||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||
super.onAuthenticationError(errorCode, errString)
|
||||
onFailure()
|
||||
}
|
||||
|
||||
override fun onAuthenticationSucceeded(
|
||||
result: BiometricPrompt.AuthenticationResult
|
||||
) {
|
||||
super.onAuthenticationSucceeded(result)
|
||||
onSuccess()
|
||||
}
|
||||
|
||||
override fun onAuthenticationFailed() {
|
||||
super.onAuthenticationFailed()
|
||||
onFailure()
|
||||
}
|
||||
},
|
||||
)
|
||||
biometricPrompt.authenticate(promptInfo)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user