mirror of
https://github.com/wgtunnel/android.git
synced 2026-07-03 14:07:49 +02:00
Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -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
|
||||||
@@ -11,12 +11,14 @@ assignees: zaneschepke
|
|||||||
A clear and concise description of what the bug is.
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
**Smartphone (please complete the following information):**
|
**Smartphone (please complete the following information):**
|
||||||
- Device: [e.g. iPhone6]
|
|
||||||
- Android Version: [e.g. iOS8.1]
|
- Device: [e.g. Pixel 4a]
|
||||||
- App Version [e.g. 22]
|
- Android Version: [e.g. Android 13]
|
||||||
|
- App Version [e.g. 3.3.3]
|
||||||
|
|
||||||
**To Reproduce**
|
**To Reproduce**
|
||||||
Steps to reproduce the behavior:
|
Steps to reproduce the behavior:
|
||||||
|
|
||||||
1. Go to '...'
|
1. Go to '...'
|
||||||
2. Click on '....'
|
2. Click on '....'
|
||||||
3. Scroll down to '....'
|
3. Scroll down to '....'
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: /
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
- package-ecosystem: gradle
|
||||||
|
directory: /
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
@@ -1,27 +1,31 @@
|
|||||||
# name of the workflow
|
# name of the workflow
|
||||||
name: Android CI Tag Deployment
|
name: Android CI Tag Deployment (Pre-release)
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- '*.*.*'
|
- '*.*.*-**'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build Signed APK
|
name: Build Signed APK
|
||||||
# change to macos because of hilt issues on ubuntu in gradle 8.3
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
env:
|
env:
|
||||||
KEY_STORE_PATH: ${{ secrets.KEY_STORE_PATH }}
|
|
||||||
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
|
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
|
||||||
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
|
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
|
||||||
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_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:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Set up JDK 17
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
@@ -35,10 +39,16 @@ jobs:
|
|||||||
id: decode_keystore
|
id: decode_keystore
|
||||||
uses: timheuer/base64-to-file@v1.2
|
uses: timheuer/base64-to-file@v1.2
|
||||||
with:
|
with:
|
||||||
fileName: 'android_keystore.jks'
|
fileName: ${{ env.KEY_STORE_FILE }}
|
||||||
fileDir: ${{ github.workspace }}/app/keystore/
|
fileDir: ${{ env.KEY_STORE_LOCATION }}
|
||||||
encodedString: ${{ secrets.KEYSTORE }}
|
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
|
- name: Create service_account.json
|
||||||
id: createServiceAccount
|
id: createServiceAccount
|
||||||
run: echo '${{ secrets.SERVICE_ACCOUNT_JSON }}' > service_account.json
|
run: echo '${{ secrets.SERVICE_ACCOUNT_JSON }}' > service_account.json
|
||||||
@@ -52,15 +62,18 @@ jobs:
|
|||||||
- name: Get apk path
|
- name: Get apk path
|
||||||
id: apk-path
|
id: apk-path
|
||||||
run: echo "path=$(find . -regex '^.*/build/outputs/apk/fdroid/release/.*\.apk$' -type f | head -1)" >> $GITHUB_OUTPUT
|
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
|
# Save the APK after the Build job is complete to publish it as a Github release in the next job
|
||||||
- name: Upload APK
|
- name: Upload APK
|
||||||
uses: actions/upload-artifact@v3.1.2
|
uses: actions/upload-artifact@v4.3.1
|
||||||
with:
|
with:
|
||||||
name: wgtunnel
|
name: wgtunnel
|
||||||
path: ${{ steps.apk-path.outputs.path }}
|
path: ${{ steps.apk-path.outputs.path }}
|
||||||
- name: Download APK from build
|
- name: Download APK from build
|
||||||
uses: actions/download-artifact@v1
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: wgtunnel
|
name: wgtunnel
|
||||||
- name: Create Release with Fastlane changelog notes
|
- name: Create Release with Fastlane changelog notes
|
||||||
@@ -70,18 +83,17 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
# fix hardcode changelog file name
|
# fix hardcode changelog file name
|
||||||
body_path: ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/32500.txt
|
body_path: ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/${{ env.VERSION_CODE }}.txt
|
||||||
tag_name: ${{ github.ref_name }}
|
tag_name: ${{ github.ref_name }}
|
||||||
name: Release ${{ github.ref_name }}
|
name: ${{ github.ref_name }}
|
||||||
draft: false
|
draft: false
|
||||||
prerelease: false
|
prerelease: true
|
||||||
files: ${{ github.workspace }}/${{ steps.apk-path.outputs.path }}
|
files: ${{ github.workspace }}/${{ steps.apk-path.outputs.path }}
|
||||||
- name: Deploy with fastlane
|
- name: Deploy with fastlane
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: '3.2' # Not needed with a .ruby-version file
|
ruby-version: '3.2' # Not needed with a .ruby-version file
|
||||||
bundler-cache: true
|
bundler-cache: true
|
||||||
|
|
||||||
- name: Distribute app to Beta track 🚀
|
- name: Distribute app to Beta track 🚀
|
||||||
run: (cd ${{ github.workspace }} && bundle install && bundle exec fastlane beta)
|
run: (cd ${{ github.workspace }} && bundle install && bundle exec fastlane beta)
|
||||||
|
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
# name of the workflow
|
||||||
|
name: Android CI Tag Deployment (Release)
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '*.*.*'
|
||||||
|
- '!*.*.*-**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build Signed APK
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
env:
|
||||||
|
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
|
||||||
|
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
|
||||||
|
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
|
||||||
|
KEY_STORE_FILE: 'android_keystore.jks'
|
||||||
|
KEY_STORE_LOCATION: ${{ github.workspace }}/app/keystore/
|
||||||
|
GH_USER: ${{ secrets.GH_USER }}
|
||||||
|
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Set up JDK 17
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'temurin'
|
||||||
|
java-version: '17'
|
||||||
|
cache: gradle
|
||||||
|
- name: Grant execute permission for gradlew
|
||||||
|
run: chmod +x gradlew
|
||||||
|
|
||||||
|
# Here we need to decode keystore.jks from base64 string and place it
|
||||||
|
# in the folder specified in the release signing configuration
|
||||||
|
- name: Decode Keystore
|
||||||
|
id: decode_keystore
|
||||||
|
uses: timheuer/base64-to-file@v1.2
|
||||||
|
with:
|
||||||
|
fileName: ${{ env.KEY_STORE_FILE }}
|
||||||
|
fileDir: ${{ env.KEY_STORE_LOCATION }}
|
||||||
|
encodedString: ${{ secrets.KEYSTORE }}
|
||||||
|
|
||||||
|
# create keystore path for gradle to read
|
||||||
|
- name: Create keystore path env var
|
||||||
|
run: |
|
||||||
|
store_path=${{ env.KEY_STORE_LOCATION }}${{ env.KEY_STORE_FILE }}
|
||||||
|
echo "KEY_STORE_PATH=$store_path" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Create service_account.json
|
||||||
|
id: createServiceAccount
|
||||||
|
run: echo '${{ secrets.SERVICE_ACCOUNT_JSON }}' > service_account.json
|
||||||
|
|
||||||
|
# Build and sign APK ("-x test" argument is used to skip tests)
|
||||||
|
# add fdroid flavor for apk upload
|
||||||
|
- name: Build Fdroid Release APK
|
||||||
|
run: ./gradlew :app:assembleFdroidRelease -x test
|
||||||
|
|
||||||
|
# get fdroid flavor release apk path
|
||||||
|
- name: Get apk path
|
||||||
|
id: apk-path
|
||||||
|
run: echo "path=$(find . -regex '^.*/build/outputs/apk/fdroid/release/.*\.apk$' -type f | head -1)" >> $GITHUB_OUTPUT
|
||||||
|
- name: Get version code
|
||||||
|
run: |
|
||||||
|
version_code=$(grep "VERSION_CODE" buildSrc/src/main/kotlin/Constants.kt | awk '{print $5}' | tr -d '\n')
|
||||||
|
echo "VERSION_CODE=$version_code" >> $GITHUB_ENV
|
||||||
|
# Save the APK after the Build job is complete to publish it as a Github release in the next job
|
||||||
|
- name: Upload APK
|
||||||
|
uses: actions/upload-artifact@v4.3.1
|
||||||
|
with:
|
||||||
|
name: wgtunnel
|
||||||
|
path: ${{ steps.apk-path.outputs.path }}
|
||||||
|
- name: Download APK from build
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: wgtunnel
|
||||||
|
- name: Repository Dispatch for my F-Droid repo
|
||||||
|
uses: peter-evans/repository-dispatch@v3
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.PAT }}
|
||||||
|
repository: zaneschepke/fdroid
|
||||||
|
event-type: fdroid-update
|
||||||
|
- name: Create Release with Fastlane changelog notes
|
||||||
|
id: create_release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
# fix hardcode changelog file name
|
||||||
|
body_path: ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/${{ env.VERSION_CODE }}.txt
|
||||||
|
tag_name: ${{ github.ref_name }}
|
||||||
|
name: ${{ github.ref_name }}
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
files: ${{ github.workspace }}/${{ steps.apk-path.outputs.path }}
|
||||||
|
- name: Deploy with fastlane
|
||||||
|
uses: ruby/setup-ruby@v1
|
||||||
|
with:
|
||||||
|
ruby-version: '3.2' # Not needed with a .ruby-version file
|
||||||
|
bundler-cache: true
|
||||||
|
- name: Distribute app to Prod track 🚀
|
||||||
|
run: (cd ${{ github.workspace }} && bundle install && bundle exec fastlane production)
|
||||||
|
|
||||||
@@ -13,22 +13,18 @@ WG Tunnel
|
|||||||
|
|
||||||
|
|
||||||
[](https://play.google.com/store/apps/details?id=com.zaneschepke.wireguardautotunnel)
|
[](https://play.google.com/store/apps/details?id=com.zaneschepke.wireguardautotunnel)
|
||||||
[](https://www.amazon.com/gp/product/B0CFGGL7WK)
|
|
||||||
[](https://f-droid.org/packages/com.zaneschepke.wireguardautotunnel/)
|
[](https://f-droid.org/packages/com.zaneschepke.wireguardautotunnel/)
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div align="center">
|
|
||||||
|
|
||||||
[](https://ko-fi.com/N4N8NMJN2)
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div align="left">
|
<div align="left">
|
||||||
|
|
||||||
This is an alternative Android Application for [WireGuard](https://www.wireguard.com/) with added features. Built using the [wireguard-android](https://github.com/WireGuard/wireguard-android) library and [Jetpack Compose](https://developer.android.com/jetpack/compose), this application was inspired by the official [WireGuard Android](https://github.com/WireGuard/wireguard-android) app.
|
This is an alternative Android Application for [WireGuard](https://www.wireguard.com/) with added
|
||||||
|
features. Built using the [wireguard-android](https://github.com/WireGuard/wireguard-android)
|
||||||
|
library and [Jetpack Compose](https://developer.android.com/jetpack/compose), this application was
|
||||||
|
inspired by the official [WireGuard Android](https://github.com/WireGuard/wireguard-android) app.
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -37,38 +33,69 @@ This is an alternative Android Application for [WireGuard](https://www.wireguard
|
|||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
<p float="center">
|
<p float="center">
|
||||||
<img label="Main" style="padding-right:25px" src="asset/main_screen.png" width="200" />
|
<img label="Main" style="padding-right:25px" src="fastlane/metadata/android/en-US/images/phoneScreenshots/main_screen.png" width="200" />
|
||||||
<img label="Config" style="padding-left:25px" src="asset/config_screen.png" width="200" />
|
<img label="Config" style="padding-left:25px" src="fastlane/metadata/android/en-US/images/phoneScreenshots/config_screen.png" width="200" />
|
||||||
<img label="Settings" style="padding-left:25px" src="asset/settings_screen.png" width="200" />
|
<img label="Settings" style="padding-left:25px" src="fastlane/metadata/android/en-US/images/phoneScreenshots/settings_screen.png" width="200" />
|
||||||
<img label="Support" style="padding-left:25px" src="asset/support_screen.png" width="200" />
|
<img label="Support" style="padding-left:25px" src="fastlane/metadata/android/en-US/images/phoneScreenshots/support_screen.png" width="200" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div align="left">
|
<div align="left">
|
||||||
|
|
||||||
## Inspiration
|
## Inspiration
|
||||||
|
|
||||||
The 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.
|
The original inspiration for this app came from the inconvenience of having to manually turn VPN off
|
||||||
|
and on while on different networks. This app was created to offer a free solution to this problem.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
* Add tunnels via .conf file, zip, manual entry, or QR code
|
* Add tunnels via .conf file, zip, manual entry, or QR code
|
||||||
* Auto connect to VPN based on Wi-Fi SSID
|
* Auto connect to VPN based on Wi-Fi SSID, ethernet, or mobile data
|
||||||
* Split tunneling by application with search
|
* Split tunneling by application with search
|
||||||
* Always-on VPN for Android support
|
* WireGuard support for kernel and userspace modes
|
||||||
|
* Always-On VPN support
|
||||||
* Export tunnels to zip
|
* Export tunnels to zip
|
||||||
* Quick tile support for VPN toggling
|
* Quick tile support for VPN toggling
|
||||||
* Static shortcuts support for primary tunnel for automation integration
|
* Static shortcuts support for primary tunnel for automation integration
|
||||||
* Intent automation support for all tunnels
|
* Intent automation support for all tunnels
|
||||||
* Optional auto connect on mobile data, ethernet
|
|
||||||
* Automatic service restart after reboot
|
* Automatic service restart after reboot
|
||||||
* Service will stay running in background after app has been closed
|
* Battery preservation measures
|
||||||
|
|
||||||
|
## Docs (WIP)
|
||||||
|
|
||||||
|
Basic documentation of the feature and behaviors of this app can be found [here](https://zaneschepke.com/wgtunnel-docs/overview.html).
|
||||||
|
|
||||||
|
The repository for these docs can be found [here](https://github.com/zaneschepke/wgtunnel-docs).
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
```
|
```
|
||||||
$ git clone https://github.com/zaneschepke/wgtunnel
|
$ git clone https://github.com/zaneschepke/wgtunnel
|
||||||
$ cd wgtunnel
|
$ cd wgtunnel
|
||||||
|
```
|
||||||
|
|
||||||
|
Create a personal access token (classic) in GitHuv to be able to pull the wireguard-android github dependencies from GitHub packages
|
||||||
|
as documented [here](https://docs.github.com/en/packages/learn-github-packages/introduction-to-github-packages#authenticating-to-github-packages).
|
||||||
|
|
||||||
|
Alternatively, you can clone [wireguard-android](https://github.com/zaneschepke/wireguard-android) and run the following command to publish the dependency to your local maven repository (requires you have maven installed). This is the ideal approach
|
||||||
|
if you intent to make changes to this lib.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ git clone https://github.com/zaneschepke/wireguard-android
|
||||||
|
$ cd wireguard-android
|
||||||
|
$ brew install maven
|
||||||
|
$ ./gradlew publishToMavenLocal
|
||||||
|
```
|
||||||
|
|
||||||
|
The [wireguard-android](https://github.com/zaneschepke/wireguard-android) dependency is a fork of the official [wireguard-android](https://github.com/WireGuard/wireguard-android) library.
|
||||||
|
|
||||||
|
Add the following lines to local.properties file:
|
||||||
|
```
|
||||||
|
GH_USER=<your github username>
|
||||||
|
GH_TOKEN=<the personal access token with read package permission you just created>
|
||||||
|
```
|
||||||
|
|
||||||
|
And then build the app:
|
||||||
|
```
|
||||||
$ ./gradlew assembleDebug
|
$ ./gradlew assembleDebug
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+38
-42
@@ -19,9 +19,7 @@ android {
|
|||||||
versionCode = Constants.VERSION_CODE
|
versionCode = Constants.VERSION_CODE
|
||||||
versionName = Constants.VERSION_NAME
|
versionName = Constants.VERSION_NAME
|
||||||
|
|
||||||
ksp {
|
ksp { arg("room.schemaLocation", "$projectDir/schemas") }
|
||||||
arg("room.schemaLocation", "$projectDir/schemas")
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
getByName("debug").assets.srcDirs(files("$projectDir/schemas")) // Room
|
getByName("debug").assets.srcDirs(files("$projectDir/schemas")) // Room
|
||||||
@@ -30,9 +28,7 @@ android {
|
|||||||
resourceConfigurations.addAll(listOf("en"))
|
resourceConfigurations.addAll(listOf("en"))
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables { useSupportLibrary = true }
|
||||||
useSupportLibrary = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
@@ -47,32 +43,41 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// try to get secrets from env first for pipeline build, then properties file for local build
|
// try to get secrets from env first for pipeline build, then properties file for local
|
||||||
storeFile = file(
|
// build
|
||||||
System.getenv().getOrDefault(
|
storeFile =
|
||||||
Constants.KEY_STORE_PATH_VAR,
|
file(
|
||||||
properties.getProperty(Constants.KEY_STORE_PATH_VAR)
|
System.getenv()
|
||||||
|
.getOrDefault(
|
||||||
|
Constants.KEY_STORE_PATH_VAR,
|
||||||
|
properties.getProperty(Constants.KEY_STORE_PATH_VAR),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
storePassword =
|
||||||
storePassword = System.getenv().getOrDefault(
|
System.getenv()
|
||||||
Constants.STORE_PASS_VAR,
|
.getOrDefault(
|
||||||
properties.getProperty(Constants.STORE_PASS_VAR)
|
Constants.STORE_PASS_VAR,
|
||||||
)
|
properties.getProperty(Constants.STORE_PASS_VAR),
|
||||||
keyAlias = System.getenv().getOrDefault(
|
)
|
||||||
Constants.KEY_ALIAS_VAR,
|
keyAlias =
|
||||||
properties.getProperty(Constants.KEY_ALIAS_VAR)
|
System.getenv()
|
||||||
)
|
.getOrDefault(
|
||||||
keyPassword = System.getenv().getOrDefault(
|
Constants.KEY_ALIAS_VAR,
|
||||||
Constants.KEY_PASS_VAR,
|
properties.getProperty(Constants.KEY_ALIAS_VAR),
|
||||||
properties.getProperty(Constants.KEY_PASS_VAR)
|
)
|
||||||
)
|
keyPassword =
|
||||||
|
System.getenv()
|
||||||
|
.getOrDefault(
|
||||||
|
Constants.KEY_PASS_VAR,
|
||||||
|
properties.getProperty(Constants.KEY_PASS_VAR),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
// don't strip
|
// don't strip
|
||||||
packaging.jniLibs.keepDebugSymbols.addAll(
|
packaging.jniLibs.keepDebugSymbols.addAll(
|
||||||
listOf("libwg-go.so", "libwg-quick.so", "libwg.so")
|
listOf("libwg-go.so", "libwg-quick.so", "libwg.so"),
|
||||||
)
|
)
|
||||||
|
|
||||||
applicationVariants.all {
|
applicationVariants.all {
|
||||||
@@ -91,13 +96,11 @@ android {
|
|||||||
isShrinkResources = true
|
isShrinkResources = true
|
||||||
proguardFiles(
|
proguardFiles(
|
||||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
"proguard-rules.pro"
|
"proguard-rules.pro",
|
||||||
)
|
)
|
||||||
signingConfig = signingConfigs.getByName(Constants.RELEASE)
|
signingConfig = signingConfigs.getByName(Constants.RELEASE)
|
||||||
}
|
}
|
||||||
debug {
|
debug { isDebuggable = true }
|
||||||
isDebuggable = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
flavorDimensions.add(Constants.TYPE)
|
flavorDimensions.add(Constants.TYPE)
|
||||||
productFlavors {
|
productFlavors {
|
||||||
@@ -118,24 +121,17 @@ android {
|
|||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
isCoreLibraryDesugaringEnabled = true
|
isCoreLibraryDesugaringEnabled = true
|
||||||
}
|
}
|
||||||
kotlinOptions {
|
kotlinOptions { jvmTarget = Constants.JVM_TARGET }
|
||||||
jvmTarget = Constants.JVM_TARGET
|
|
||||||
}
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose = true
|
compose = true
|
||||||
buildConfig = true
|
buildConfig = true
|
||||||
}
|
}
|
||||||
composeOptions {
|
composeOptions { kotlinCompilerExtensionVersion = Constants.COMPOSE_COMPILER_EXTENSION_VERSION }
|
||||||
kotlinCompilerExtensionVersion = libs.versions.composeCompiler.get()
|
packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } }
|
||||||
}
|
|
||||||
packaging {
|
|
||||||
resources {
|
|
||||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val generalImplementation by configurations
|
val generalImplementation by configurations
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
@@ -160,13 +156,14 @@ dependencies {
|
|||||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||||
debugImplementation(libs.androidx.compose.manifest)
|
debugImplementation(libs.androidx.compose.manifest)
|
||||||
|
|
||||||
// wg
|
// get tunnel lib from github packages or mavenLocal
|
||||||
implementation(libs.tunnel)
|
implementation(libs.tunnel)
|
||||||
coreLibraryDesugaring(libs.desugar.jdk.libs)
|
coreLibraryDesugaring(libs.desugar.jdk.libs)
|
||||||
|
|
||||||
// logging
|
// logging
|
||||||
implementation(libs.timber)
|
implementation(libs.timber)
|
||||||
|
|
||||||
|
|
||||||
// compose navigation
|
// compose navigation
|
||||||
implementation(libs.androidx.navigation.compose)
|
implementation(libs.androidx.navigation.compose)
|
||||||
implementation(libs.androidx.hilt.navigation.compose)
|
implementation(libs.androidx.hilt.navigation.compose)
|
||||||
@@ -176,7 +173,6 @@ dependencies {
|
|||||||
ksp(libs.hilt.android.compiler)
|
ksp(libs.hilt.android.compiler)
|
||||||
|
|
||||||
// accompanist
|
// accompanist
|
||||||
implementation(libs.accompanist.systemuicontroller)
|
|
||||||
implementation(libs.accompanist.permissions)
|
implementation(libs.accompanist.permissions)
|
||||||
implementation(libs.accompanist.flowlayout)
|
implementation(libs.accompanist.flowlayout)
|
||||||
implementation(libs.accompanist.drawablepainter)
|
implementation(libs.accompanist.drawablepainter)
|
||||||
|
|||||||
Vendored
+2
@@ -22,3 +22,5 @@
|
|||||||
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
|
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
|
||||||
<fields>;
|
<fields>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,52 +3,33 @@ package com.zaneschepke.wireguardautotunnel
|
|||||||
import androidx.room.testing.MigrationTestHelper
|
import androidx.room.testing.MigrationTestHelper
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.AppDatabase
|
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
||||||
import java.io.IOException
|
import com.zaneschepke.wireguardautotunnel.data.Queries
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class MigrationTest {
|
class MigrationTest {
|
||||||
private val dbName = "migration-test"
|
private val dbName = "migration-test"
|
||||||
|
|
||||||
@get:Rule
|
@get:Rule
|
||||||
val helper: MigrationTestHelper = MigrationTestHelper(
|
val helper: MigrationTestHelper =
|
||||||
InstrumentationRegistry.getInstrumentation(),
|
MigrationTestHelper(
|
||||||
AppDatabase::class.java
|
InstrumentationRegistry.getInstrumentation(),
|
||||||
)
|
AppDatabase::class.java,
|
||||||
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun migrate2To3() {
|
fun migrate4To5() {
|
||||||
helper.createDatabase(dbName, 3).apply {
|
helper.createDatabase(dbName, 4).apply {
|
||||||
// Database has schema version 1. Insert some data using SQL queries.
|
// Database has schema version 1. Insert some data using SQL queries.
|
||||||
// You can't use DAO classes because they expect the latest schema.
|
// You can't use DAO classes because they expect the latest schema.
|
||||||
|
execSQL(Queries.createDefaultSettings())
|
||||||
execSQL(
|
execSQL(
|
||||||
"INSERT INTO Settings (is_tunnel_enabled, " +
|
"INSERT INTO TunnelConfig (name, wg_quick)" + " VALUES ('hello', 'hello')",
|
||||||
"is_tunnel_on_mobile_data_enabled," +
|
|
||||||
"trusted_network_ssids," +
|
|
||||||
"default_tunnel, " +
|
|
||||||
"is_always_on_vpn_enabled," +
|
|
||||||
"is_tunnel_on_ethernet_enabled," +
|
|
||||||
"is_shortcuts_enabled," +
|
|
||||||
"is_battery_saver_enabled," +
|
|
||||||
"is_tunnel_on_wifi_enabled)" +
|
|
||||||
" VALUES (" +
|
|
||||||
"false," +
|
|
||||||
"false," +
|
|
||||||
"'[trustedSSID1,trustedSSID2]'," +
|
|
||||||
"'defaultTunnel'," +
|
|
||||||
"false," +
|
|
||||||
"false," +
|
|
||||||
"false," +
|
|
||||||
"false," +
|
|
||||||
"false)"
|
|
||||||
)
|
|
||||||
execSQL(
|
|
||||||
"INSERT INTO TunnelConfig (name, wg_quick)" +
|
|
||||||
" VALUES ('hello', 'hello')"
|
|
||||||
)
|
)
|
||||||
// Prepare for the next version.
|
// Prepare for the next version.
|
||||||
close()
|
close()
|
||||||
@@ -56,7 +37,7 @@ class MigrationTest {
|
|||||||
|
|
||||||
// Re-open the database with version 2 and provide
|
// Re-open the database with version 2 and provide
|
||||||
// MIGRATION_1_2 as the migration process.
|
// MIGRATION_1_2 as the migration process.
|
||||||
helper.runMigrationsAndValidate(dbName, 4, true)
|
helper.runMigrationsAndValidate(dbName, 5, true)
|
||||||
// MigrationTestHelper automatically verifies the schema changes,
|
// MigrationTestHelper automatically verifies the schema changes,
|
||||||
// but you need to validate that the data was migrated properly.
|
// but you need to validate that the data was migrated properly.
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,56 +1,63 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
<uses-permission
|
||||||
|
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||||
android:maxSdkVersion="32" />
|
android:maxSdkVersion="32" />
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
android:maxSdkVersion="32"
|
android:maxSdkVersion="32"
|
||||||
tools:ignore="ScopedStorage" />
|
tools:ignore="ScopedStorage" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"
|
<uses-permission
|
||||||
|
android:name="android.permission.ACCESS_WIFI_STATE"
|
||||||
android:maxSdkVersion="30"
|
android:maxSdkVersion="30"
|
||||||
tools:ignore="LeanbackUsesWifi" />
|
tools:ignore="LeanbackUsesWifi" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
|
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
|
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
||||||
<!--foreground service exempt android 14-->
|
<!--foreground service exempt android 14-->
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED"/>
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
|
||||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
|
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||||
|
|
||||||
<!--foreground service permissions-->
|
<!--foreground service permissions-->
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||||
<!--start service on boot permission-->
|
<!--start service on boot permission-->
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
<!--android tv support-->
|
<!--android tv support-->
|
||||||
<uses-feature android:name="android.software.leanback"
|
<uses-feature
|
||||||
|
android:name="android.software.leanback"
|
||||||
android:required="false" />
|
android:required="false" />
|
||||||
<uses-feature android:name="android.hardware.touchscreen"
|
<uses-feature
|
||||||
|
android:name="android.hardware.touchscreen"
|
||||||
android:required="false" />
|
android:required="false" />
|
||||||
<uses-feature
|
<uses-feature
|
||||||
android:name="android.hardware.location.gps"
|
android:name="android.hardware.location.gps"
|
||||||
android:required="false" />
|
android:required="false" />
|
||||||
<uses-feature
|
<uses-feature
|
||||||
android:name="android.hardware.screen.portrait"
|
android:name="android.hardware.screen.portrait"
|
||||||
android:required="false" />
|
android:required="false" />
|
||||||
|
|
||||||
<queries>
|
<queries>
|
||||||
<intent>
|
<intent>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
</intent>
|
</intent>
|
||||||
</queries>
|
</queries>
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
|
||||||
android:enableOnBackInvokedCallback="true"
|
|
||||||
android:name=".WireGuardAutoTunnel"
|
android:name=".WireGuardAutoTunnel"
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:banner="@mipmap/ic_banner"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
|
android:enableOnBackInvokedCallback="true"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:banner="@mipmap/ic_banner"
|
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
@@ -62,11 +69,14 @@
|
|||||||
android:theme="@style/Theme.WireguardAutoTunnel">
|
android:theme="@style/Theme.WireguardAutoTunnel">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||||
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES"/>
|
|
||||||
|
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<meta-data android:name="android.app.shortcuts"
|
<meta-data
|
||||||
|
android:name="android.app.shortcuts"
|
||||||
android:resource="@xml/shortcuts" />
|
android:resource="@xml/shortcuts" />
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
@@ -76,61 +86,74 @@
|
|||||||
android:theme="@style/zxing_CaptureTheme"
|
android:theme="@style/zxing_CaptureTheme"
|
||||||
android:windowSoftInputMode="stateAlwaysHidden" />
|
android:windowSoftInputMode="stateAlwaysHidden" />
|
||||||
<activity
|
<activity
|
||||||
android:finishOnTaskLaunch="true"
|
android:name=".service.shortcut.ShortcutsActivity"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:theme="@android:style/Theme.NoDisplay"
|
android:finishOnTaskLaunch="true"
|
||||||
android:name=".service.shortcut.ShortcutsActivity"/>
|
android:theme="@android:style/Theme.NoDisplay" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".service.foreground.ForegroundService"
|
android:name=".service.foreground.ForegroundService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
|
android:exported="false"
|
||||||
android:foregroundServiceType="systemExempted|specialUse"
|
android:foregroundServiceType="systemExempted|specialUse"
|
||||||
tools:node="merge"
|
tools:node="merge" />
|
||||||
android:exported="false">
|
|
||||||
</service>
|
|
||||||
<service
|
<service
|
||||||
android:exported="true"
|
|
||||||
android:name=".service.tile.TunnelControlTile"
|
android:name=".service.tile.TunnelControlTile"
|
||||||
android:icon="@drawable/shield"
|
android:exported="true"
|
||||||
|
android:icon="@drawable/ic_launcher"
|
||||||
android:label="WG Tunnel"
|
android:label="WG Tunnel"
|
||||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||||
<meta-data android:name="android.service.quicksettings.ACTIVE_TILE"
|
<meta-data
|
||||||
|
android:name="android.service.quicksettings.ACTIVE_TILE"
|
||||||
android:value="true" />
|
android:value="true" />
|
||||||
<meta-data android:name="android.service.quicksettings.TOGGLEABLE_TILE"
|
<meta-data
|
||||||
|
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
|
||||||
android:value="true" />
|
android:value="true" />
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
<service
|
<service
|
||||||
android:name=".service.foreground.WireGuardTunnelService"
|
android:name=".service.foreground.WireGuardTunnelService"
|
||||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:persistent="true"
|
android:exported="false"
|
||||||
android:foregroundServiceType="systemExempted|specialUse"
|
android:foregroundServiceType="systemExempted|specialUse"
|
||||||
tools:node="merge"
|
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||||
android:exported="false">
|
android:persistent="true"
|
||||||
|
tools:node="merge">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.net.VpnService"/>
|
<action android:name="android.net.VpnService" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<meta-data android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
|
<meta-data
|
||||||
|
android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
|
||||||
android:value="true" />
|
android:value="true" />
|
||||||
</service>
|
</service>
|
||||||
<service
|
<service
|
||||||
android:name=".service.foreground.WireGuardConnectivityWatcherService"
|
android:name=".service.foreground.WireGuardConnectivityWatcherService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:stopWithTask="false"
|
android:exported="false"
|
||||||
android:persistent="true"
|
|
||||||
android:foregroundServiceType="systemExempted|specialUse"
|
android:foregroundServiceType="systemExempted|specialUse"
|
||||||
tools:node="merge"
|
android:persistent="true"
|
||||||
|
android:stopWithTask="false"
|
||||||
|
tools:node="merge" />
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".receiver.BootReceiver"
|
||||||
|
android:enabled="true"
|
||||||
android:exported="false">
|
android:exported="false">
|
||||||
</service>
|
|
||||||
<receiver android:enabled="true" android:name=".receiver.BootReceiver"
|
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
|
||||||
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
|
<action android:name="android.intent.action.ACTION_BOOT_COMPLETED" />
|
||||||
|
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
|
||||||
|
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
<receiver android:exported="false" android:name=".receiver.NotificationActionReceiver"/>
|
<receiver
|
||||||
|
android:name=".receiver.NotificationActionReceiver"
|
||||||
|
android:exported="false" />
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 16 KiB |
@@ -1,31 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel
|
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import java.math.BigDecimal
|
|
||||||
import java.text.DecimalFormat
|
|
||||||
import kotlin.coroutines.CoroutineContext
|
|
||||||
import kotlin.coroutines.EmptyCoroutineContext
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
fun BroadcastReceiver.goAsync(
|
|
||||||
context: CoroutineContext = EmptyCoroutineContext,
|
|
||||||
block: suspend CoroutineScope.() -> Unit
|
|
||||||
) {
|
|
||||||
val pendingResult = goAsync()
|
|
||||||
@OptIn(DelicateCoroutinesApi::class) // Must run globally; there's no teardown callback.
|
|
||||||
GlobalScope.launch(context) {
|
|
||||||
try {
|
|
||||||
block()
|
|
||||||
} finally {
|
|
||||||
pendingResult.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun BigDecimal.toThreeDecimalPlaceString(): String {
|
|
||||||
val df = DecimalFormat("#.###")
|
|
||||||
return df.format(this)
|
|
||||||
}
|
|
||||||
@@ -1,56 +1,34 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel
|
package com.zaneschepke.wireguardautotunnel
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.ComponentName
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import androidx.lifecycle.ProcessLifecycleOwner
|
import android.service.quicksettings.TileService
|
||||||
import androidx.lifecycle.lifecycleScope
|
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.datastore.DataStoreManager
|
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
import java.io.IOException
|
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@HiltAndroidApp
|
@HiltAndroidApp
|
||||||
class WireGuardAutoTunnel : Application() {
|
class WireGuardAutoTunnel : Application() {
|
||||||
@Inject
|
|
||||||
lateinit var settingsRepo: SettingsDoa
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var dataStoreManager: DataStoreManager
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
instance = this
|
||||||
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
||||||
initSettings()
|
|
||||||
with(ProcessLifecycleOwner.get()) {
|
|
||||||
lifecycleScope.launch {
|
|
||||||
try {
|
|
||||||
// load preferences into memory
|
|
||||||
dataStoreManager.init()
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Timber.e("Failed to load preferences")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initSettings() {
|
|
||||||
with(ProcessLifecycleOwner.get()) {
|
|
||||||
lifecycleScope.launch {
|
|
||||||
if (settingsRepo.getAll().isEmpty()) {
|
|
||||||
settingsRepo.save(Settings())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun isRunningOnAndroidTv(context: Context): Boolean {
|
lateinit var instance: WireGuardAutoTunnel
|
||||||
return context.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
private set
|
||||||
|
|
||||||
|
fun isRunningOnAndroidTv(): Boolean {
|
||||||
|
return instance.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun requestTileServiceStateUpdate() {
|
||||||
|
TileService.requestListeningState(
|
||||||
|
instance,
|
||||||
|
ComponentName(instance, TunnelControlTile::class.java),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.data
|
||||||
|
|
||||||
|
import androidx.room.AutoMigration
|
||||||
|
import androidx.room.Database
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import androidx.room.TypeConverters
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
|
|
||||||
|
@Database(
|
||||||
|
entities = [Settings::class, TunnelConfig::class],
|
||||||
|
version = 5,
|
||||||
|
autoMigrations =
|
||||||
|
[
|
||||||
|
AutoMigration(from = 1, to = 2),
|
||||||
|
AutoMigration(from = 2, to = 3),
|
||||||
|
AutoMigration(
|
||||||
|
from = 3,
|
||||||
|
to = 4,
|
||||||
|
),
|
||||||
|
AutoMigration(
|
||||||
|
from = 4,
|
||||||
|
to = 5,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
exportSchema = true,
|
||||||
|
)
|
||||||
|
@TypeConverters(DatabaseListConverters::class)
|
||||||
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
|
abstract fun settingDao(): SettingsDao
|
||||||
|
|
||||||
|
abstract fun tunnelConfigDoa(): TunnelConfigDao
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.data
|
||||||
|
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
class DatabaseCallback : RoomDatabase.Callback() {
|
||||||
|
override fun onCreate(db: SupportSQLiteDatabase) = db.run {
|
||||||
|
// Notice non-ui thread is here
|
||||||
|
beginTransaction()
|
||||||
|
try {
|
||||||
|
execSQL(Queries.createDefaultSettings())
|
||||||
|
Timber.i("Bootstrapping settings data")
|
||||||
|
setTransactionSuccessful()
|
||||||
|
} catch (e : Exception) {
|
||||||
|
Timber.e(e)
|
||||||
|
} finally {
|
||||||
|
endTransaction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.repository
|
package com.zaneschepke.wireguardautotunnel.data
|
||||||
|
|
||||||
import androidx.room.TypeConverter
|
import androidx.room.TypeConverter
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.data
|
||||||
|
|
||||||
|
object Queries {
|
||||||
|
fun createDefaultSettings() : String {
|
||||||
|
return """
|
||||||
|
INSERT INTO Settings (is_tunnel_enabled,
|
||||||
|
is_tunnel_on_mobile_data_enabled,
|
||||||
|
trusted_network_ssids,
|
||||||
|
default_tunnel,
|
||||||
|
is_always_on_vpn_enabled,
|
||||||
|
is_tunnel_on_ethernet_enabled,
|
||||||
|
is_shortcuts_enabled,
|
||||||
|
is_battery_saver_enabled,
|
||||||
|
is_tunnel_on_wifi_enabled,
|
||||||
|
is_kernel_enabled,
|
||||||
|
is_restore_on_boot_enabled,
|
||||||
|
is_multi_tunnel_enabled)
|
||||||
|
VALUES
|
||||||
|
('false',
|
||||||
|
'false',
|
||||||
|
'sampleSSID1,sampleSSID2',
|
||||||
|
NULL,
|
||||||
|
'false',
|
||||||
|
'false',
|
||||||
|
'false',
|
||||||
|
'false',
|
||||||
|
'false',
|
||||||
|
'false',
|
||||||
|
'false',
|
||||||
|
'false')
|
||||||
|
""".trimIndent()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.data
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Delete
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface SettingsDao {
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun save(t: Settings)
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun saveAll(t: List<Settings>)
|
||||||
|
|
||||||
|
@Query("SELECT * FROM settings WHERE id=:id") suspend fun getById(id: Long): Settings?
|
||||||
|
|
||||||
|
@Query("SELECT * FROM settings") suspend fun getAll(): List<Settings>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM settings LIMIT 1") fun getSettingsFlow(): Flow<Settings>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM settings") fun getAllFlow(): Flow<MutableList<Settings>>
|
||||||
|
|
||||||
|
@Delete suspend fun delete(t: Settings)
|
||||||
|
|
||||||
|
@Query("SELECT COUNT('id') FROM settings") suspend fun count(): Long
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.data
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Delete
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface TunnelConfigDao {
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun save(t: TunnelConfig)
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun saveAll(t: List<TunnelConfig>)
|
||||||
|
|
||||||
|
@Query("SELECT * FROM TunnelConfig WHERE id=:id") suspend fun getById(id: Long): TunnelConfig?
|
||||||
|
|
||||||
|
@Query("SELECT * FROM TunnelConfig") suspend fun getAll(): List<TunnelConfig>
|
||||||
|
|
||||||
|
@Delete suspend fun delete(t: TunnelConfig)
|
||||||
|
|
||||||
|
@Query("SELECT COUNT('id') FROM TunnelConfig") suspend fun count(): Long
|
||||||
|
|
||||||
|
@Query("SELECT * FROM tunnelconfig") fun getAllFlow(): Flow<MutableList<TunnelConfig>>
|
||||||
|
}
|
||||||
+13
-14
@@ -1,4 +1,5 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.repository.datastore
|
package com.zaneschepke.wireguardautotunnel.data.datastore
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.datastore.preferences.core.Preferences
|
import androidx.datastore.preferences.core.Preferences
|
||||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||||
@@ -11,29 +12,27 @@ import kotlinx.coroutines.flow.map
|
|||||||
class DataStoreManager(private val context: Context) {
|
class DataStoreManager(private val context: Context) {
|
||||||
companion object {
|
companion object {
|
||||||
val LOCATION_DISCLOSURE_SHOWN = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN")
|
val LOCATION_DISCLOSURE_SHOWN = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN")
|
||||||
|
val BATTERY_OPTIMIZE_DISABLE_SHOWN = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN")
|
||||||
}
|
}
|
||||||
|
|
||||||
// preferences
|
// preferences
|
||||||
private val preferencesKey = "preferences"
|
private val preferencesKey = "preferences"
|
||||||
private val Context.dataStore by preferencesDataStore(
|
private val Context.dataStore by
|
||||||
name = preferencesKey
|
preferencesDataStore(
|
||||||
)
|
name = preferencesKey,
|
||||||
|
)
|
||||||
|
|
||||||
suspend fun init() {
|
suspend fun init() {
|
||||||
context.dataStore.data.first()
|
context.dataStore.data.first()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun <T> saveToDataStore(key: Preferences.Key<T>, value: T) =
|
suspend fun <T> saveToDataStore(key: Preferences.Key<T>, value: T) =
|
||||||
context.dataStore.edit {
|
context.dataStore.edit { it[key] = value }
|
||||||
it[key] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <T> getFromStore(key: Preferences.Key<T>) =
|
fun <T> getFromStoreFlow(key: Preferences.Key<T>) = context.dataStore.data.map { it[key] }
|
||||||
context.dataStore.data.map {
|
|
||||||
it[key]
|
|
||||||
}
|
|
||||||
|
|
||||||
val locationDisclosureFlow: Flow<Boolean?> = context.dataStore.data.map {
|
suspend fun <T> getFromStore(key: Preferences.Key<T>) =
|
||||||
it[LOCATION_DISCLOSURE_SHOWN]
|
context.dataStore.data.first { it.contains(key) }[key]
|
||||||
}
|
|
||||||
|
val preferencesFlow: Flow<Preferences?> = context.dataStore.data
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.data.model
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
data class Settings(
|
||||||
|
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
||||||
|
@ColumnInfo(name = "is_tunnel_enabled") var isAutoTunnelEnabled: Boolean = false,
|
||||||
|
@ColumnInfo(name = "is_tunnel_on_mobile_data_enabled")
|
||||||
|
var isTunnelOnMobileDataEnabled: Boolean = false,
|
||||||
|
@ColumnInfo(name = "trusted_network_ssids")
|
||||||
|
var trustedNetworkSSIDs: MutableList<String> = mutableListOf(),
|
||||||
|
@ColumnInfo(name = "default_tunnel") var defaultTunnel: String? = null,
|
||||||
|
@ColumnInfo(name = "is_always_on_vpn_enabled") var isAlwaysOnVpnEnabled: Boolean = false,
|
||||||
|
@ColumnInfo(name = "is_tunnel_on_ethernet_enabled")
|
||||||
|
var isTunnelOnEthernetEnabled: Boolean = false,
|
||||||
|
@ColumnInfo(
|
||||||
|
name = "is_shortcuts_enabled",
|
||||||
|
defaultValue = "false",
|
||||||
|
)
|
||||||
|
var isShortcutsEnabled: Boolean = false,
|
||||||
|
@ColumnInfo(
|
||||||
|
name = "is_battery_saver_enabled",
|
||||||
|
defaultValue = "false",
|
||||||
|
)
|
||||||
|
var isBatterySaverEnabled: Boolean = false,
|
||||||
|
@ColumnInfo(
|
||||||
|
name = "is_tunnel_on_wifi_enabled",
|
||||||
|
defaultValue = "false",
|
||||||
|
)
|
||||||
|
var isTunnelOnWifiEnabled: Boolean = false,
|
||||||
|
@ColumnInfo(
|
||||||
|
name = "is_kernel_enabled",
|
||||||
|
defaultValue = "false",
|
||||||
|
)
|
||||||
|
var isKernelEnabled: Boolean = false,
|
||||||
|
@ColumnInfo(
|
||||||
|
name = "is_restore_on_boot_enabled",
|
||||||
|
defaultValue = "false",
|
||||||
|
)
|
||||||
|
var isRestoreOnBootEnabled: Boolean = false,
|
||||||
|
@ColumnInfo(
|
||||||
|
name = "is_multi_tunnel_enabled",
|
||||||
|
defaultValue = "false",
|
||||||
|
)
|
||||||
|
var isMultiTunnelEnabled: Boolean = false,
|
||||||
|
@ColumnInfo(
|
||||||
|
name = "is_auto_tunnel_paused",
|
||||||
|
defaultValue = "false",
|
||||||
|
)
|
||||||
|
var isAutoTunnelPaused: Boolean = false,
|
||||||
|
) {
|
||||||
|
fun isTunnelConfigDefault(tunnelConfig: TunnelConfig): Boolean {
|
||||||
|
return if (defaultTunnel != null) {
|
||||||
|
val defaultConfig = TunnelConfig.from(defaultTunnel!!)
|
||||||
|
(tunnelConfig.id == defaultConfig.id)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -1,13 +1,13 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.repository.model
|
package com.zaneschepke.wireguardautotunnel.data.model
|
||||||
|
|
||||||
import androidx.room.ColumnInfo
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.Index
|
import androidx.room.Index
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import com.wireguard.config.Config
|
import com.wireguard.config.Config
|
||||||
import java.io.InputStream
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
@Entity(indices = [Index(value = ["name"], unique = true)])
|
@Entity(indices = [Index(value = ["name"], unique = true)])
|
||||||
@Serializable
|
@Serializable
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||||
|
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
interface SettingsRepository {
|
||||||
|
suspend fun save(settings: Settings)
|
||||||
|
|
||||||
|
fun getSettingsFlow(): Flow<Settings>
|
||||||
|
|
||||||
|
suspend fun getSettings(): Settings
|
||||||
|
|
||||||
|
suspend fun getAll(): List<Settings>
|
||||||
|
}
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||||
|
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.SettingsDao
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
class SettingsRepositoryImpl(private val settingsDoa: SettingsDao) : SettingsRepository {
|
||||||
|
|
||||||
|
override suspend fun save(settings: Settings) {
|
||||||
|
settingsDoa.save(settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSettingsFlow(): Flow<Settings> {
|
||||||
|
return settingsDoa.getSettingsFlow()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getSettings(): Settings {
|
||||||
|
return settingsDoa.getAll().firstOrNull() ?: Settings()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getAll(): List<Settings> {
|
||||||
|
return settingsDoa.getAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||||
|
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
interface TunnelConfigRepository {
|
||||||
|
|
||||||
|
fun getTunnelConfigsFlow(): Flow<TunnelConfigs>
|
||||||
|
|
||||||
|
suspend fun getAll(): TunnelConfigs
|
||||||
|
|
||||||
|
suspend fun save(tunnelConfig: TunnelConfig)
|
||||||
|
|
||||||
|
suspend fun delete(tunnelConfig: TunnelConfig)
|
||||||
|
|
||||||
|
suspend fun count(): Int
|
||||||
|
}
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||||
|
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
class TunnelConfigRepositoryImpl(private val tunnelConfigDao: TunnelConfigDao) :
|
||||||
|
TunnelConfigRepository {
|
||||||
|
override fun getTunnelConfigsFlow(): Flow<TunnelConfigs> {
|
||||||
|
return tunnelConfigDao.getAllFlow()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getAll(): TunnelConfigs {
|
||||||
|
return tunnelConfigDao.getAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun save(tunnelConfig: TunnelConfig) {
|
||||||
|
tunnelConfigDao.save(tunnelConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun delete(tunnelConfig: TunnelConfig) {
|
||||||
|
tunnelConfigDao.delete(tunnelConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun count(): Int {
|
||||||
|
return tunnelConfigDao.count().toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,8 @@ package com.zaneschepke.wireguardautotunnel.module
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.AppDatabase
|
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.DatabaseCallback
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
@@ -16,15 +17,14 @@ import javax.inject.Singleton
|
|||||||
class DatabaseModule {
|
class DatabaseModule {
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideDatabase(
|
fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
|
||||||
@ApplicationContext context: Context
|
|
||||||
): AppDatabase {
|
|
||||||
return Room.databaseBuilder(
|
return Room.databaseBuilder(
|
||||||
context,
|
context,
|
||||||
AppDatabase::class.java,
|
AppDatabase::class.java,
|
||||||
context.getString(R.string.db_name)
|
context.getString(R.string.db_name),
|
||||||
)
|
)
|
||||||
.fallbackToDestructiveMigration()
|
.fallbackToDestructiveMigration()
|
||||||
|
.addCallback(DatabaseCallback())
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,4 @@ package com.zaneschepke.wireguardautotunnel.module
|
|||||||
|
|
||||||
import javax.inject.Qualifier
|
import javax.inject.Qualifier
|
||||||
|
|
||||||
@Qualifier
|
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class Kernel
|
||||||
@Retention(AnnotationRetention.BINARY)
|
|
||||||
annotation class Kernel
|
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.module
|
package com.zaneschepke.wireguardautotunnel.module
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.AppDatabase
|
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
import com.zaneschepke.wireguardautotunnel.data.SettingsDao
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
|
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.datastore.DataStoreManager
|
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepositoryImpl
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepositoryImpl
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
@@ -17,16 +21,28 @@ import javax.inject.Singleton
|
|||||||
class RepositoryModule {
|
class RepositoryModule {
|
||||||
@Singleton
|
@Singleton
|
||||||
@Provides
|
@Provides
|
||||||
fun provideSettingsRepository(appDatabase: AppDatabase): SettingsDoa {
|
fun provideSettingsDoa(appDatabase: AppDatabase): SettingsDao {
|
||||||
return appDatabase.settingDao()
|
return appDatabase.settingDao()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@Provides
|
@Provides
|
||||||
fun provideTunnelConfigRepository(appDatabase: AppDatabase): TunnelConfigDao {
|
fun provideTunnelConfigDoa(appDatabase: AppDatabase): TunnelConfigDao {
|
||||||
return appDatabase.tunnelConfigDoa()
|
return appDatabase.tunnelConfigDoa()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
fun provideTunnelConfigRepository(tunnelConfigDao: TunnelConfigDao): TunnelConfigRepository {
|
||||||
|
return TunnelConfigRepositoryImpl(tunnelConfigDao)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
fun provideSettingsRepository(settingsDao: SettingsDao): SettingsRepository {
|
||||||
|
return SettingsRepositoryImpl(settingsDao)
|
||||||
|
}
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@Provides
|
@Provides
|
||||||
fun providePreferencesDataStore(@ApplicationContext context: Context): DataStoreManager {
|
fun providePreferencesDataStore(@ApplicationContext context: Context): DataStoreManager {
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ import dagger.hilt.android.scopes.ServiceScoped
|
|||||||
abstract class ServiceModule {
|
abstract class ServiceModule {
|
||||||
@Binds
|
@Binds
|
||||||
@ServiceScoped
|
@ServiceScoped
|
||||||
abstract fun provideNotificationService(wireGuardNotification: WireGuardNotification): NotificationService
|
abstract fun provideNotificationService(
|
||||||
|
wireGuardNotification: WireGuardNotification
|
||||||
|
): NotificationService
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@ServiceScoped
|
@ServiceScoped
|
||||||
@@ -25,9 +27,13 @@ abstract class ServiceModule {
|
|||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@ServiceScoped
|
@ServiceScoped
|
||||||
abstract fun provideMobileDataService(mobileDataService: MobileDataService): NetworkService<MobileDataService>
|
abstract fun provideMobileDataService(
|
||||||
|
mobileDataService: MobileDataService
|
||||||
|
): NetworkService<MobileDataService>
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@ServiceScoped
|
@ServiceScoped
|
||||||
abstract fun provideEthernetService(ethernetService: EthernetService): NetworkService<EthernetService>
|
abstract fun provideEthernetService(
|
||||||
|
ethernetService: EthernetService
|
||||||
|
): NetworkService<EthernetService>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import com.wireguard.android.backend.GoBackend
|
|||||||
import com.wireguard.android.backend.WgQuickBackend
|
import com.wireguard.android.backend.WgQuickBackend
|
||||||
import com.wireguard.android.util.RootShell
|
import com.wireguard.android.util.RootShell
|
||||||
import com.wireguard.android.util.ToolsInstaller
|
import com.wireguard.android.util.ToolsInstaller
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.WireGuardTunnel
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.WireGuardTunnel
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
@@ -21,28 +21,21 @@ import javax.inject.Singleton
|
|||||||
class TunnelModule {
|
class TunnelModule {
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideRootShell(
|
fun provideRootShell(@ApplicationContext context: Context): RootShell {
|
||||||
@ApplicationContext context: Context
|
|
||||||
): RootShell {
|
|
||||||
return RootShell(context)
|
return RootShell(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
@Userspace
|
@Userspace
|
||||||
fun provideUserspaceBackend(
|
fun provideUserspaceBackend(@ApplicationContext context: Context): Backend {
|
||||||
@ApplicationContext context: Context
|
|
||||||
): Backend {
|
|
||||||
return GoBackend(context)
|
return GoBackend(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
@Kernel
|
@Kernel
|
||||||
fun provideKernelBackend(
|
fun provideKernelBackend(@ApplicationContext context: Context, rootShell: RootShell): Backend {
|
||||||
@ApplicationContext context: Context,
|
|
||||||
rootShell: RootShell
|
|
||||||
): Backend {
|
|
||||||
return WgQuickBackend(context, rootShell, ToolsInstaller(context, rootShell))
|
return WgQuickBackend(context, rootShell, ToolsInstaller(context, rootShell))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,8 +44,8 @@ class TunnelModule {
|
|||||||
fun provideVpnService(
|
fun provideVpnService(
|
||||||
@Userspace userspaceBackend: Backend,
|
@Userspace userspaceBackend: Backend,
|
||||||
@Kernel kernelBackend: Backend,
|
@Kernel kernelBackend: Backend,
|
||||||
settingsDoa: SettingsDoa
|
settingsRepository: SettingsRepository
|
||||||
): VpnService {
|
): VpnService {
|
||||||
return WireGuardTunnel(userspaceBackend, kernelBackend, settingsDoa)
|
return WireGuardTunnel(userspaceBackend, kernelBackend, settingsRepository)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,4 @@ package com.zaneschepke.wireguardautotunnel.module
|
|||||||
|
|
||||||
import javax.inject.Qualifier
|
import javax.inject.Qualifier
|
||||||
|
|
||||||
@Qualifier
|
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class Userspace
|
||||||
@Retention(AnnotationRetention.BINARY)
|
|
||||||
annotation class Userspace
|
|
||||||
|
|||||||
@@ -3,34 +3,30 @@ package com.zaneschepke.wireguardautotunnel.receiver
|
|||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.zaneschepke.wireguardautotunnel.goAsync
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.goAsync
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.cancel
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class BootReceiver : BroadcastReceiver() {
|
class BootReceiver : BroadcastReceiver() {
|
||||||
@Inject
|
|
||||||
lateinit var settingsRepo: SettingsDoa
|
|
||||||
|
|
||||||
override fun onReceive(
|
@Inject lateinit var settingsRepository: SettingsRepository
|
||||||
context: Context,
|
|
||||||
intent: Intent
|
@Inject lateinit var tunnelConfigRepository: TunnelConfigRepository
|
||||||
) = goAsync {
|
|
||||||
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
|
override fun onReceive(context: Context?, intent: Intent?) = goAsync {
|
||||||
try {
|
if (Intent.ACTION_BOOT_COMPLETED != intent?.action) return@goAsync
|
||||||
val settings = settingsRepo.getAll()
|
val settings = settingsRepository.getSettings()
|
||||||
if (settings.isNotEmpty()) {
|
if (settings.isAutoTunnelEnabled) {
|
||||||
val setting = settings.first()
|
Timber.i("Starting watcher service from boot")
|
||||||
if (setting.isAutoTunnelEnabled && setting.defaultTunnel != null) {
|
ServiceManager.startWatcherServiceForeground(context!!)
|
||||||
ServiceManager.startWatcherService(context, setting.defaultTunnel!!)
|
} else if(settings.isAlwaysOnVpnEnabled) {
|
||||||
}
|
Timber.i("Starting tunnel from boot")
|
||||||
}
|
ServiceManager.startVpnServicePrimaryTunnel(context!!, settings, tunnelConfigRepository.getAll().firstOrNull())
|
||||||
} finally {
|
|
||||||
cancel()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-18
@@ -3,33 +3,26 @@ package com.zaneschepke.wireguardautotunnel.receiver
|
|||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.zaneschepke.wireguardautotunnel.Constants
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.goAsync
|
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.goAsync
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class NotificationActionReceiver : BroadcastReceiver() {
|
class NotificationActionReceiver : BroadcastReceiver() {
|
||||||
@Inject
|
@Inject lateinit var settingsRepository: SettingsRepository
|
||||||
lateinit var settingsRepo: SettingsDoa
|
|
||||||
|
|
||||||
override fun onReceive(
|
override fun onReceive(context: Context, intent: Intent?) = goAsync {
|
||||||
context: Context,
|
|
||||||
intent: Intent?
|
|
||||||
) = goAsync {
|
|
||||||
try {
|
try {
|
||||||
val settings = settingsRepo.getAll()
|
val settings = settingsRepository.getSettings()
|
||||||
if (settings.isNotEmpty()) {
|
if (settings.defaultTunnel != null) {
|
||||||
val setting = settings.first()
|
ServiceManager.stopVpnService(context)
|
||||||
if (setting.defaultTunnel != null) {
|
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
||||||
ServiceManager.stopVpnService(context)
|
ServiceManager.startVpnServiceForeground(context, settings.defaultTunnel.toString())
|
||||||
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
|
||||||
ServiceManager.startVpnService(context, setting.defaultTunnel.toString())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
cancel()
|
cancel()
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.repository
|
|
||||||
|
|
||||||
import androidx.room.AutoMigration
|
|
||||||
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 = 4,
|
|
||||||
autoMigrations = [
|
|
||||||
AutoMigration(from = 1, to = 2), AutoMigration(from = 2, to = 3), AutoMigration(
|
|
||||||
from = 3,
|
|
||||||
to = 4
|
|
||||||
)
|
|
||||||
],
|
|
||||||
exportSchema = true
|
|
||||||
)
|
|
||||||
@TypeConverters(DatabaseListConverters::class)
|
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
|
||||||
abstract fun settingDao(): SettingsDoa
|
|
||||||
|
|
||||||
abstract fun tunnelConfigDoa(): TunnelConfigDao
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.repository
|
|
||||||
|
|
||||||
import androidx.room.Dao
|
|
||||||
import androidx.room.Delete
|
|
||||||
import androidx.room.Insert
|
|
||||||
import androidx.room.OnConflictStrategy
|
|
||||||
import androidx.room.Query
|
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
|
|
||||||
@Dao
|
|
||||||
interface SettingsDoa {
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
|
||||||
suspend fun save(t: Settings)
|
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
|
||||||
suspend fun saveAll(t: List<Settings>)
|
|
||||||
|
|
||||||
@Query("SELECT * FROM settings WHERE id=:id")
|
|
||||||
suspend fun getById(id: Long): Settings?
|
|
||||||
|
|
||||||
@Query("SELECT * FROM settings")
|
|
||||||
suspend fun getAll(): List<Settings>
|
|
||||||
|
|
||||||
@Query("SELECT * FROM settings")
|
|
||||||
fun getAllFlow(): Flow<MutableList<Settings>>
|
|
||||||
|
|
||||||
@Delete
|
|
||||||
suspend fun delete(t: Settings)
|
|
||||||
|
|
||||||
@Query("SELECT COUNT('id') FROM settings")
|
|
||||||
suspend fun count(): Long
|
|
||||||
}
|
|
||||||
@@ -1,33 +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,49 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.repository.model
|
|
||||||
|
|
||||||
import androidx.room.ColumnInfo
|
|
||||||
import androidx.room.Entity
|
|
||||||
import androidx.room.PrimaryKey
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
data class Settings(
|
|
||||||
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
|
||||||
@ColumnInfo(name = "is_tunnel_enabled") var isAutoTunnelEnabled: Boolean = false,
|
|
||||||
@ColumnInfo(name = "is_tunnel_on_mobile_data_enabled") var isTunnelOnMobileDataEnabled: Boolean = false,
|
|
||||||
@ColumnInfo(name = "trusted_network_ssids") var trustedNetworkSSIDs: MutableList<String> = mutableListOf(),
|
|
||||||
@ColumnInfo(name = "default_tunnel") var defaultTunnel: String? = null,
|
|
||||||
@ColumnInfo(name = "is_always_on_vpn_enabled") var isAlwaysOnVpnEnabled: Boolean = false,
|
|
||||||
@ColumnInfo(name = "is_tunnel_on_ethernet_enabled") var isTunnelOnEthernetEnabled: Boolean = false,
|
|
||||||
@ColumnInfo(
|
|
||||||
name = "is_shortcuts_enabled",
|
|
||||||
defaultValue = "false"
|
|
||||||
) var isShortcutsEnabled: Boolean = false,
|
|
||||||
@ColumnInfo(
|
|
||||||
name = "is_battery_saver_enabled",
|
|
||||||
defaultValue = "false"
|
|
||||||
) var isBatterySaverEnabled: Boolean = false,
|
|
||||||
@ColumnInfo(
|
|
||||||
name = "is_tunnel_on_wifi_enabled",
|
|
||||||
defaultValue = "false"
|
|
||||||
) var isTunnelOnWifiEnabled: Boolean = false,
|
|
||||||
@ColumnInfo(
|
|
||||||
name = "is_kernel_enabled",
|
|
||||||
defaultValue = "false"
|
|
||||||
) var isKernelEnabled: Boolean = false,
|
|
||||||
@ColumnInfo(
|
|
||||||
name = "is_restore_on_boot_enabled",
|
|
||||||
defaultValue = "false"
|
|
||||||
) var isRestoreOnBootEnabled: Boolean = false,
|
|
||||||
@ColumnInfo(
|
|
||||||
name = "is_multi_tunnel_enabled",
|
|
||||||
defaultValue = "false"
|
|
||||||
) var isMultiTunnelEnabled: Boolean = false
|
|
||||||
) {
|
|
||||||
fun isTunnelConfigDefault(tunnelConfig: TunnelConfig): Boolean {
|
|
||||||
return if (defaultTunnel != null) {
|
|
||||||
val defaultConfig = TunnelConfig.from(defaultTunnel!!)
|
|
||||||
(tunnelConfig.id == defaultConfig.id)
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+4
-8
@@ -15,18 +15,14 @@ open class ForegroundService : LifecycleService() {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
intent: Intent?,
|
|
||||||
flags: Int,
|
|
||||||
startId: Int
|
|
||||||
): Int {
|
|
||||||
super.onStartCommand(intent, flags, startId)
|
super.onStartCommand(intent, flags, startId)
|
||||||
Timber.d("onStartCommand executed with startId: $startId")
|
Timber.d("onStartCommand executed with startId: $startId")
|
||||||
if (intent != null) {
|
if (intent != null) {
|
||||||
val action = intent.action
|
val action = intent.action
|
||||||
Timber.d("using an intent with action $action")
|
|
||||||
when (action) {
|
when (action) {
|
||||||
Action.START.name, Action.START_FOREGROUND.name -> startService(intent.extras)
|
Action.START.name,
|
||||||
|
Action.START_FOREGROUND.name -> startService(intent.extras)
|
||||||
Action.STOP.name -> stopService(intent.extras)
|
Action.STOP.name -> stopService(intent.extras)
|
||||||
"android.net.VpnService" -> {
|
"android.net.VpnService" -> {
|
||||||
Timber.d("Always-on VPN starting service")
|
Timber.d("Always-on VPN starting service")
|
||||||
@@ -36,7 +32,7 @@ open class ForegroundService : LifecycleService() {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Timber.d(
|
Timber.d(
|
||||||
"with a null intent. It has been probably restarted by the system."
|
"with a null intent. It has been probably restarted by the system.",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// by returning this we make sure the service is restarted if the system kills the service
|
// by returning this we make sure the service is restarted if the system kills the service
|
||||||
|
|||||||
+42
-72
@@ -1,28 +1,29 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.service.foreground
|
package com.zaneschepke.wireguardautotunnel.service.foreground
|
||||||
|
|
||||||
import android.app.ActivityManager
|
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Context.ACTIVITY_SERVICE
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
object ServiceManager {
|
object ServiceManager {
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
private // Deprecated for third party Services.
|
|
||||||
fun <T> Context.isServiceRunning(service: Class<T>) =
|
|
||||||
(getSystemService(ACTIVITY_SERVICE) as ActivityManager)
|
|
||||||
.getRunningServices(Integer.MAX_VALUE)
|
|
||||||
.any { it.service.className == service.name }
|
|
||||||
|
|
||||||
fun <T : Service> getServiceState(
|
// private
|
||||||
context: Context,
|
// fun <T> Context.isServiceRunning(service: Class<T>) =
|
||||||
cls: Class<T>
|
// (getSystemService(ACTIVITY_SERVICE) as ActivityManager)
|
||||||
): ServiceState {
|
// .runningAppProcesses.any {
|
||||||
val isServiceRunning = context.isServiceRunning(cls)
|
// it.processName == service.name
|
||||||
return if (isServiceRunning) ServiceState.STARTED else ServiceState.STOPPED
|
// }
|
||||||
}
|
//
|
||||||
|
// 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(
|
private fun <T : Service> actionOnService(
|
||||||
action: Action,
|
action: Action,
|
||||||
@@ -30,14 +31,10 @@ object ServiceManager {
|
|||||||
cls: Class<T>,
|
cls: Class<T>,
|
||||||
extras: Map<String, String>? = null
|
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 =
|
val intent =
|
||||||
Intent(context, cls).also {
|
Intent(context, cls).also {
|
||||||
it.action = action.name
|
it.action = action.name
|
||||||
extras?.forEach { (k, v) ->
|
extras?.forEach { (k, v) -> it.putExtra(k, v) }
|
||||||
it.putExtra(k, v)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
intent.component?.javaClass
|
intent.component?.javaClass
|
||||||
try {
|
try {
|
||||||
@@ -45,11 +42,9 @@ object ServiceManager {
|
|||||||
Action.START_FOREGROUND -> {
|
Action.START_FOREGROUND -> {
|
||||||
context.startForegroundService(intent)
|
context.startForegroundService(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
Action.START -> {
|
Action.START -> {
|
||||||
context.startService(intent)
|
context.startService(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
Action.STOP -> context.startService(intent)
|
Action.STOP -> context.startService(intent)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -57,67 +52,57 @@ object ServiceManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startVpnService(
|
fun startVpnService(context: Context, tunnelConfig: String) {
|
||||||
context: Context,
|
|
||||||
tunnelConfig: String
|
|
||||||
) {
|
|
||||||
actionOnService(
|
actionOnService(
|
||||||
Action.START,
|
Action.START,
|
||||||
context,
|
context,
|
||||||
WireGuardTunnelService::class.java,
|
WireGuardTunnelService::class.java,
|
||||||
mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig)
|
mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopVpnService(context: Context) {
|
fun stopVpnService(context: Context) {
|
||||||
|
Timber.d("Stopping vpn service action")
|
||||||
actionOnService(
|
actionOnService(
|
||||||
Action.STOP,
|
Action.STOP,
|
||||||
context,
|
context,
|
||||||
WireGuardTunnelService::class.java
|
WireGuardTunnelService::class.java,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startVpnServiceForeground(
|
fun startVpnServiceForeground(context: Context, tunnelConfig: String) {
|
||||||
context: Context,
|
|
||||||
tunnelConfig: String
|
|
||||||
) {
|
|
||||||
actionOnService(
|
actionOnService(
|
||||||
Action.START_FOREGROUND,
|
Action.START_FOREGROUND,
|
||||||
context,
|
context,
|
||||||
WireGuardTunnelService::class.java,
|
WireGuardTunnelService::class.java,
|
||||||
mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig)
|
mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startWatcherServiceForeground(
|
fun startVpnServicePrimaryTunnel(context: Context, settings: Settings, fallbackTunnel: TunnelConfig? = null) {
|
||||||
|
if(settings.defaultTunnel != null) {
|
||||||
|
return startVpnServiceForeground(context, settings.defaultTunnel!!)
|
||||||
|
}
|
||||||
|
if(fallbackTunnel != null) {
|
||||||
|
startVpnServiceForeground(context, fallbackTunnel.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startWatcherServiceForeground(
|
||||||
context: Context,
|
context: Context,
|
||||||
tunnelConfig: String
|
|
||||||
) {
|
) {
|
||||||
|
actionOnService(
|
||||||
|
Action.START_FOREGROUND,
|
||||||
|
context,
|
||||||
|
WireGuardConnectivityWatcherService::class.java,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startWatcherService(context: Context) {
|
||||||
actionOnService(
|
actionOnService(
|
||||||
Action.START,
|
Action.START,
|
||||||
context,
|
context,
|
||||||
WireGuardConnectivityWatcherService::class.java,
|
WireGuardConnectivityWatcherService::class.java,
|
||||||
mapOf(
|
|
||||||
context
|
|
||||||
.getString(R.string.tunnel_extras_key) to
|
|
||||||
tunnelConfig
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun startWatcherService(
|
|
||||||
context: Context,
|
|
||||||
tunnelConfig: String
|
|
||||||
) {
|
|
||||||
actionOnService(
|
|
||||||
Action.START,
|
|
||||||
context,
|
|
||||||
WireGuardConnectivityWatcherService::class.java,
|
|
||||||
mapOf(
|
|
||||||
context
|
|
||||||
.getString(R.string.tunnel_extras_key) to
|
|
||||||
tunnelConfig
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,22 +110,7 @@ object ServiceManager {
|
|||||||
actionOnService(
|
actionOnService(
|
||||||
Action.STOP,
|
Action.STOP,
|
||||||
context,
|
context,
|
||||||
WireGuardConnectivityWatcherService::class.java
|
WireGuardConnectivityWatcherService::class.java,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggleWatcherServiceForeground(
|
|
||||||
context: Context,
|
|
||||||
tunnelConfig: String
|
|
||||||
) {
|
|
||||||
when (
|
|
||||||
getServiceState(
|
|
||||||
context,
|
|
||||||
WireGuardConnectivityWatcherService::class.java
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
ServiceState.STARTED -> stopWatcherService(context)
|
|
||||||
ServiceState.STOPPED -> startWatcherServiceForeground(context, tunnelConfig)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+196
-133
@@ -10,10 +10,9 @@ import android.os.SystemClock
|
|||||||
import androidx.core.app.ServiceCompat
|
import androidx.core.app.ServiceCompat
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.wireguard.android.backend.Tunnel
|
import com.wireguard.android.backend.Tunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.Constants
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
|
import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
|
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
|
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
|
||||||
@@ -21,45 +20,46 @@ import com.zaneschepke.wireguardautotunnel.service.network.NetworkStatus
|
|||||||
import com.zaneschepke.wireguardautotunnel.service.network.WifiService
|
import com.zaneschepke.wireguardautotunnel.service.network.WifiService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
|
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class WireGuardConnectivityWatcherService : ForegroundService() {
|
class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||||
private val foregroundId = 122
|
private val foregroundId = 122
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var wifiService: NetworkService<WifiService>
|
||||||
lateinit var wifiService: NetworkService<WifiService>
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var mobileDataService: NetworkService<MobileDataService>
|
||||||
lateinit var mobileDataService: NetworkService<MobileDataService>
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var ethernetService: NetworkService<EthernetService>
|
||||||
lateinit var ethernetService: NetworkService<EthernetService>
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var settingsRepository: SettingsRepository
|
||||||
lateinit var settingsRepo: SettingsDoa
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var notificationService: NotificationService
|
||||||
lateinit var notificationService: NotificationService
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var vpnService: VpnService
|
||||||
lateinit var vpnService: VpnService
|
|
||||||
|
|
||||||
private var isWifiConnected = false
|
private val networkEventsFlow = MutableStateFlow(WatcherState())
|
||||||
private var isEthernetConnected = false
|
|
||||||
private var isMobileDataConnected = false
|
data class WatcherState(
|
||||||
private var currentNetworkSSID = ""
|
val isWifiConnected: Boolean = false,
|
||||||
|
val isVpnConnected: Boolean = false,
|
||||||
|
val isEthernetConnected: Boolean = false,
|
||||||
|
val isMobileDataConnected: Boolean = false,
|
||||||
|
val currentNetworkSSID: String = "",
|
||||||
|
val settings: Settings = Settings()
|
||||||
|
)
|
||||||
|
|
||||||
private lateinit var watcherJob: Job
|
private lateinit var watcherJob: Job
|
||||||
private lateinit var setting: Settings
|
|
||||||
private lateinit var tunnelConfig: String
|
|
||||||
|
|
||||||
private var wakeLock: PowerManager.WakeLock? = null
|
private var wakeLock: PowerManager.WakeLock? = null
|
||||||
private val tag = this.javaClass.name
|
private val tag = this.javaClass.name
|
||||||
@@ -68,7 +68,9 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
super.onCreate()
|
super.onCreate()
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
launchWatcherNotification()
|
if (settingsRepository.getSettings().isAutoTunnelPaused) {
|
||||||
|
launchWatcherPausedNotification()
|
||||||
|
} else launchWatcherNotification()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e("Failed to start watcher service, not enough permissions")
|
Timber.e("Failed to start watcher service, not enough permissions")
|
||||||
}
|
}
|
||||||
@@ -78,21 +80,10 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
override fun startService(extras: Bundle?) {
|
override fun startService(extras: Bundle?) {
|
||||||
super.startService(extras)
|
super.startService(extras)
|
||||||
try {
|
try {
|
||||||
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
|
// we need this lock so our service gets not affected by Doze Mode
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch { initWakeLock() }
|
||||||
initWakeLock()
|
|
||||||
}
|
|
||||||
cancelWatcherJob()
|
cancelWatcherJob()
|
||||||
if (this::tunnelConfig.isInitialized) {
|
startWatcherJob()
|
||||||
startWatcherJob()
|
|
||||||
} else {
|
|
||||||
stopService(extras)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e("Failed to launch watcher service, no permissions")
|
Timber.e("Failed to launch watcher service, no permissions")
|
||||||
}
|
}
|
||||||
@@ -109,22 +100,29 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
stopSelf()
|
stopSelf()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchWatcherNotification() {
|
private fun launchWatcherNotification(
|
||||||
|
description: String = getString(R.string.watcher_notification_text_active)
|
||||||
|
) {
|
||||||
val notification =
|
val notification =
|
||||||
notificationService.createNotification(
|
notificationService.createNotification(
|
||||||
channelId = getString(R.string.watcher_channel_id),
|
channelId = getString(R.string.watcher_channel_id),
|
||||||
channelName = getString(R.string.watcher_channel_name),
|
channelName = getString(R.string.watcher_channel_name),
|
||||||
description = getString(R.string.watcher_notification_text),
|
title = getString(R.string.auto_tunnel_title),
|
||||||
vibration = false
|
description = description,
|
||||||
)
|
)
|
||||||
ServiceCompat.startForeground(
|
ServiceCompat.startForeground(
|
||||||
this,
|
this,
|
||||||
foregroundId,
|
foregroundId,
|
||||||
notification,
|
notification,
|
||||||
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID
|
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun launchWatcherPausedNotification() {
|
||||||
|
launchWatcherNotification(getString(R.string.watcher_notification_text_paused))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO could this be restarting service in a bad state?
|
||||||
// try to start task again if killed
|
// try to start task again if killed
|
||||||
override fun onTaskRemoved(rootIntent: Intent) {
|
override fun onTaskRemoved(rootIntent: Intent) {
|
||||||
Timber.d("Task Removed called")
|
Timber.d("Task Removed called")
|
||||||
@@ -134,7 +132,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
this,
|
this,
|
||||||
1,
|
1,
|
||||||
restartServiceIntent,
|
restartServiceIntent,
|
||||||
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
|
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE,
|
||||||
)
|
)
|
||||||
applicationContext.getSystemService(Context.ALARM_SERVICE)
|
applicationContext.getSystemService(Context.ALARM_SERVICE)
|
||||||
val alarmService: AlarmManager =
|
val alarmService: AlarmManager =
|
||||||
@@ -142,24 +140,28 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
alarmService.set(
|
alarmService.set(
|
||||||
AlarmManager.ELAPSED_REALTIME,
|
AlarmManager.ELAPSED_REALTIME,
|
||||||
SystemClock.elapsedRealtime() + 1000,
|
SystemClock.elapsedRealtime() + 1000,
|
||||||
restartServicePendingIntent
|
restartServicePendingIntent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun initWakeLock() {
|
private suspend fun initWakeLock() {
|
||||||
val isBatterySaverOn =
|
val isBatterySaverOn =
|
||||||
withContext(lifecycleScope.coroutineContext) {
|
withContext(lifecycleScope.coroutineContext) {
|
||||||
settingsRepo.getAll().firstOrNull()?.isBatterySaverEnabled ?: false
|
settingsRepository.getSettings().isBatterySaverEnabled
|
||||||
}
|
}
|
||||||
wakeLock =
|
wakeLock =
|
||||||
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
|
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
|
||||||
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
|
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
|
||||||
if (isBatterySaverOn) {
|
try {
|
||||||
Timber.d("Initiating wakelock with timeout")
|
if (isBatterySaverOn) {
|
||||||
acquire(Constants.WATCHER_SERVICE_WAKE_LOCK_TIMEOUT)
|
Timber.d("Initiating wakelock with timeout")
|
||||||
} else {
|
acquire(Constants.BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT)
|
||||||
Timber.d("Initiating wakelock with zero timeout")
|
} else {
|
||||||
acquire()
|
Timber.d("Initiating wakelock with zero timeout")
|
||||||
|
acquire(Constants.DEFAULT_WATCHER_WAKE_LOCK_TIMEOUT)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
release()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -174,24 +176,33 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
private fun startWatcherJob() {
|
private fun startWatcherJob() {
|
||||||
watcherJob =
|
watcherJob =
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val settings = settingsRepo.getAll()
|
val setting = settingsRepository.getSettings()
|
||||||
if (settings.isNotEmpty()) {
|
|
||||||
setting = settings[0]
|
|
||||||
}
|
|
||||||
launch {
|
launch {
|
||||||
|
Timber.d("Starting wifi watcher")
|
||||||
watchForWifiConnectivityChanges()
|
watchForWifiConnectivityChanges()
|
||||||
}
|
}
|
||||||
if (setting.isTunnelOnMobileDataEnabled) {
|
if (setting.isTunnelOnMobileDataEnabled) {
|
||||||
launch {
|
launch {
|
||||||
|
Timber.d("Starting mobile data watcher")
|
||||||
watchForMobileDataConnectivityChanges()
|
watchForMobileDataConnectivityChanges()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (setting.isTunnelOnEthernetEnabled) {
|
if (setting.isTunnelOnEthernetEnabled) {
|
||||||
launch {
|
launch {
|
||||||
|
Timber.d("Starting ethernet data watcher")
|
||||||
watchForEthernetConnectivityChanges()
|
watchForEthernetConnectivityChanges()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
launch {
|
launch {
|
||||||
|
Timber.d("Starting vpn state watcher")
|
||||||
|
watchForVpnConnectivityChanges()
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
Timber.d("Starting settings watcher")
|
||||||
|
watchForSettingsChanges()
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
Timber.d("Starting management watcher")
|
||||||
manageVpn()
|
manageVpn()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -202,37 +213,84 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
when (it) {
|
when (it) {
|
||||||
is NetworkStatus.Available -> {
|
is NetworkStatus.Available -> {
|
||||||
Timber.d("Gained Mobile data connection")
|
Timber.d("Gained Mobile data connection")
|
||||||
isMobileDataConnected = true
|
networkEventsFlow.value =
|
||||||
|
networkEventsFlow.value.copy(
|
||||||
|
isMobileDataConnected = true,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkStatus.CapabilitiesChanged -> {
|
is NetworkStatus.CapabilitiesChanged -> {
|
||||||
isMobileDataConnected = true
|
networkEventsFlow.value =
|
||||||
|
networkEventsFlow.value.copy(
|
||||||
|
isMobileDataConnected = true,
|
||||||
|
)
|
||||||
Timber.d("Mobile data capabilities changed")
|
Timber.d("Mobile data capabilities changed")
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkStatus.Unavailable -> {
|
is NetworkStatus.Unavailable -> {
|
||||||
isMobileDataConnected = false
|
networkEventsFlow.value =
|
||||||
|
networkEventsFlow.value.copy(
|
||||||
|
isMobileDataConnected = false,
|
||||||
|
)
|
||||||
Timber.d("Lost mobile data connection")
|
Timber.d("Lost mobile data connection")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun watchForSettingsChanges() {
|
||||||
|
settingsRepository.getSettingsFlow().collect {
|
||||||
|
if (networkEventsFlow.value.settings.isAutoTunnelPaused != it.isAutoTunnelPaused) {
|
||||||
|
when (it.isAutoTunnelPaused) {
|
||||||
|
true -> launchWatcherPausedNotification()
|
||||||
|
false -> launchWatcherNotification()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
networkEventsFlow.value =
|
||||||
|
networkEventsFlow.value.copy(
|
||||||
|
settings = it,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun watchForVpnConnectivityChanges() {
|
||||||
|
vpnService.vpnState.collect {
|
||||||
|
when (it.status) {
|
||||||
|
Tunnel.State.DOWN ->
|
||||||
|
networkEventsFlow.value =
|
||||||
|
networkEventsFlow.value.copy(
|
||||||
|
isVpnConnected = false,
|
||||||
|
)
|
||||||
|
Tunnel.State.UP ->
|
||||||
|
networkEventsFlow.value =
|
||||||
|
networkEventsFlow.value.copy(
|
||||||
|
isVpnConnected = true,
|
||||||
|
)
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun watchForEthernetConnectivityChanges() {
|
private suspend fun watchForEthernetConnectivityChanges() {
|
||||||
ethernetService.networkStatus.collect {
|
ethernetService.networkStatus.collect {
|
||||||
when (it) {
|
when (it) {
|
||||||
is NetworkStatus.Available -> {
|
is NetworkStatus.Available -> {
|
||||||
Timber.d("Gained Ethernet connection")
|
Timber.d("Gained Ethernet connection")
|
||||||
isEthernetConnected = true
|
networkEventsFlow.value =
|
||||||
|
networkEventsFlow.value.copy(
|
||||||
|
isEthernetConnected = true,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkStatus.CapabilitiesChanged -> {
|
is NetworkStatus.CapabilitiesChanged -> {
|
||||||
Timber.d("Ethernet capabilities changed")
|
Timber.d("Ethernet capabilities changed")
|
||||||
isEthernetConnected = true
|
networkEventsFlow.value =
|
||||||
|
networkEventsFlow.value.copy(
|
||||||
|
isEthernetConnected = true,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkStatus.Unavailable -> {
|
is NetworkStatus.Unavailable -> {
|
||||||
isEthernetConnected = false
|
networkEventsFlow.value =
|
||||||
|
networkEventsFlow.value.copy(
|
||||||
|
isEthernetConnected = false,
|
||||||
|
)
|
||||||
Timber.d("Lost Ethernet connection")
|
Timber.d("Lost Ethernet connection")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -244,92 +302,97 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
when (it) {
|
when (it) {
|
||||||
is NetworkStatus.Available -> {
|
is NetworkStatus.Available -> {
|
||||||
Timber.d("Gained Wi-Fi connection")
|
Timber.d("Gained Wi-Fi connection")
|
||||||
isWifiConnected = true
|
networkEventsFlow.value =
|
||||||
|
networkEventsFlow.value.copy(
|
||||||
|
isWifiConnected = true,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkStatus.CapabilitiesChanged -> {
|
is NetworkStatus.CapabilitiesChanged -> {
|
||||||
Timber.d("Wifi capabilities changed")
|
Timber.d("Wifi capabilities changed")
|
||||||
isWifiConnected = true
|
networkEventsFlow.value =
|
||||||
|
networkEventsFlow.value.copy(
|
||||||
|
isWifiConnected = true,
|
||||||
|
)
|
||||||
val ssid = wifiService.getNetworkName(it.networkCapabilities) ?: ""
|
val ssid = wifiService.getNetworkName(it.networkCapabilities) ?: ""
|
||||||
Timber.d("Detected SSID: $ssid")
|
Timber.d("Detected SSID: $ssid")
|
||||||
currentNetworkSSID = ssid
|
networkEventsFlow.value =
|
||||||
|
networkEventsFlow.value.copy(
|
||||||
|
currentNetworkSSID = ssid,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkStatus.Unavailable -> {
|
is NetworkStatus.Unavailable -> {
|
||||||
isWifiConnected = false
|
networkEventsFlow.value =
|
||||||
|
networkEventsFlow.value.copy(
|
||||||
|
isWifiConnected = false,
|
||||||
|
)
|
||||||
Timber.d("Lost Wi-Fi connection")
|
Timber.d("Lost Wi-Fi connection")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO clean this up
|
||||||
private suspend fun manageVpn() {
|
private suspend fun manageVpn() {
|
||||||
while (true) {
|
networkEventsFlow.collectLatest {
|
||||||
when {
|
Timber.i("New watcher state: $it")
|
||||||
(
|
if (!it.settings.isAutoTunnelPaused && it.settings.defaultTunnel != null) {
|
||||||
(
|
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
||||||
isEthernetConnected &&
|
when {
|
||||||
setting.isTunnelOnEthernetEnabled &&
|
((it.isEthernetConnected &&
|
||||||
vpnService.getState() == Tunnel.State.DOWN
|
it.settings.isTunnelOnEthernetEnabled &&
|
||||||
)
|
!it.isVpnConnected)) -> {
|
||||||
) ->
|
ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!)
|
||||||
ServiceManager.startVpnService(this, tunnelConfig)
|
Timber.i("Condition 1 met")
|
||||||
|
}
|
||||||
(
|
(!it.isEthernetConnected &&
|
||||||
!isEthernetConnected &&
|
it.settings.isTunnelOnMobileDataEnabled &&
|
||||||
setting.isTunnelOnMobileDataEnabled &&
|
!it.isWifiConnected &&
|
||||||
!isWifiConnected &&
|
it.isMobileDataConnected &&
|
||||||
isMobileDataConnected &&
|
!it.isVpnConnected) -> {
|
||||||
vpnService.getState() == Tunnel.State.DOWN
|
ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!)
|
||||||
) ->
|
Timber.i("Condition 2 met")
|
||||||
ServiceManager.startVpnService(this, tunnelConfig)
|
}
|
||||||
|
(!it.isEthernetConnected &&
|
||||||
(
|
!it.settings.isTunnelOnMobileDataEnabled &&
|
||||||
!isEthernetConnected &&
|
!it.isWifiConnected &&
|
||||||
!setting.isTunnelOnMobileDataEnabled &&
|
it.isVpnConnected) -> {
|
||||||
!isWifiConnected &&
|
ServiceManager.stopVpnService(this)
|
||||||
vpnService.getState() == Tunnel.State.UP
|
Timber.i("Condition 3 met")
|
||||||
) ->
|
}
|
||||||
ServiceManager.stopVpnService(this)
|
(!it.isEthernetConnected &&
|
||||||
|
it.isWifiConnected &&
|
||||||
(
|
!it.settings.trustedNetworkSSIDs.contains(it.currentNetworkSSID) &&
|
||||||
!isEthernetConnected && isWifiConnected &&
|
it.settings.isTunnelOnWifiEnabled &&
|
||||||
!setting.trustedNetworkSSIDs.contains(currentNetworkSSID) &&
|
(!it.isVpnConnected)) -> {
|
||||||
setting.isTunnelOnWifiEnabled &&
|
ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!)
|
||||||
(vpnService.getState() != Tunnel.State.UP)
|
Timber.i("Condition 4 met")
|
||||||
) ->
|
}
|
||||||
ServiceManager.startVpnService(this, tunnelConfig)
|
(!it.isEthernetConnected &&
|
||||||
|
(it.isWifiConnected &&
|
||||||
(
|
it.settings.trustedNetworkSSIDs.contains(it.currentNetworkSSID)) &&
|
||||||
!isEthernetConnected && (
|
(it.isVpnConnected)) -> {
|
||||||
isWifiConnected &&
|
ServiceManager.stopVpnService(this)
|
||||||
setting.trustedNetworkSSIDs.contains(currentNetworkSSID)
|
Timber.i("Condition 5 met")
|
||||||
) &&
|
}
|
||||||
(vpnService.getState() == Tunnel.State.UP)
|
(!it.isEthernetConnected &&
|
||||||
) ->
|
(it.isWifiConnected &&
|
||||||
ServiceManager.stopVpnService(this)
|
!it.settings.isTunnelOnWifiEnabled &&
|
||||||
|
(it.isVpnConnected))) -> {
|
||||||
(
|
ServiceManager.stopVpnService(this)
|
||||||
!isEthernetConnected && (
|
Timber.i("Condition 6 met")
|
||||||
isWifiConnected &&
|
}
|
||||||
!setting.isTunnelOnWifiEnabled &&
|
(!it.isEthernetConnected &&
|
||||||
(vpnService.getState() == Tunnel.State.UP)
|
!it.isWifiConnected &&
|
||||||
)
|
!it.isMobileDataConnected &&
|
||||||
) ->
|
(it.isVpnConnected)) -> {
|
||||||
ServiceManager.stopVpnService(this)
|
ServiceManager.stopVpnService(this)
|
||||||
|
Timber.i("Condition 7 met")
|
||||||
(
|
}
|
||||||
!isEthernetConnected && !isWifiConnected &&
|
else -> {
|
||||||
!isMobileDataConnected &&
|
Timber.i("No condition met")
|
||||||
(vpnService.getState() == Tunnel.State.UP)
|
}
|
||||||
) ->
|
|
||||||
ServiceManager.stopVpnService(this)
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
// Do nothing
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
delay(Constants.VPN_CONNECTIVITY_CHECK_INTERVAL)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+80
-86
@@ -5,57 +5,62 @@ import android.content.Intent
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.core.app.ServiceCompat
|
import androidx.core.app.ServiceCompat
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.zaneschepke.wireguardautotunnel.Constants
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver
|
import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
|
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.handshakeStatus
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.mapPeerStats
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class WireGuardTunnelService : ForegroundService() {
|
class WireGuardTunnelService : ForegroundService() {
|
||||||
private val foregroundId = 123
|
private val foregroundId = 123
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var vpnService: VpnService
|
||||||
lateinit var vpnService: VpnService
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var settingsRepository: SettingsRepository
|
||||||
lateinit var settingsRepo: SettingsDoa
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var tunnelConfigRepository: TunnelConfigRepository
|
||||||
lateinit var notificationService: NotificationService
|
|
||||||
|
@Inject lateinit var notificationService: NotificationService
|
||||||
|
|
||||||
private lateinit var job: Job
|
private lateinit var job: Job
|
||||||
|
|
||||||
private var tunnelName: String = ""
|
private var tunnelName: String = ""
|
||||||
|
private var didShowConnected = false
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
launchVpnStartingNotification()
|
if (tunnelConfigRepository.getAll().isNotEmpty()) {
|
||||||
|
launchVpnNotification()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun startService(extras: Bundle?) {
|
override fun startService(extras: Bundle?) {
|
||||||
super.startService(extras)
|
super.startService(extras)
|
||||||
// TODO fix grapheneOS calls always-on on install
|
|
||||||
launchVpnStartingNotification()
|
|
||||||
val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key))
|
|
||||||
cancelJob()
|
cancelJob()
|
||||||
|
val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key))
|
||||||
|
val tunnelConfig = tunnelConfigString?.let { TunnelConfig.from(it) }
|
||||||
|
tunnelName = tunnelConfig?.name ?: ""
|
||||||
job =
|
job =
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
launch {
|
launch {
|
||||||
if (tunnelConfigString != null) {
|
if (tunnelConfig != null) {
|
||||||
try {
|
try {
|
||||||
val tunnelConfig = TunnelConfig.from(tunnelConfigString)
|
|
||||||
tunnelName = tunnelConfig.name
|
tunnelName = tunnelConfig.name
|
||||||
vpnService.startTunnel(tunnelConfig)
|
vpnService.startTunnel(tunnelConfig)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -63,101 +68,90 @@ class WireGuardTunnelService : ForegroundService() {
|
|||||||
stopService(extras)
|
stopService(extras)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Timber.d("Tunnel config null, starting default tunnel")
|
Timber.d("Tunnel config null, starting default tunnel or first")
|
||||||
val settings = settingsRepo.getAll()
|
val settings = settingsRepository.getSettings()
|
||||||
if (settings.isNotEmpty()) {
|
val tunnels = tunnelConfigRepository.getAll()
|
||||||
val setting = settings[0]
|
if (settings.isAlwaysOnVpnEnabled) {
|
||||||
if (setting.defaultTunnel != null && setting.isAlwaysOnVpnEnabled) {
|
val tunnel =
|
||||||
val tunnelConfig = TunnelConfig.from(setting.defaultTunnel!!)
|
if (settings.defaultTunnel != null) {
|
||||||
tunnelName = tunnelConfig.name
|
TunnelConfig.from(settings.defaultTunnel!!)
|
||||||
vpnService.startTunnel(tunnelConfig)
|
} else if (tunnels.isNotEmpty()) {
|
||||||
|
tunnels.first()
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
if (tunnel != null) {
|
||||||
|
tunnelName = tunnel.name
|
||||||
|
vpnService.startTunnel(tunnel)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
launchAlwaysOnDisabledNotification()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// TODO add failed to connect notification
|
||||||
launch {
|
launch {
|
||||||
var didShowConnected = false
|
vpnService.vpnState.collect { state ->
|
||||||
var didShowFailedHandshakeNotification = false
|
state.statistics
|
||||||
vpnService.handshakeStatus.collect {
|
?.mapPeerStats()
|
||||||
when (it) {
|
?.map { it.value?.handshakeStatus() }
|
||||||
HandshakeStatus.NOT_STARTED -> {
|
.let { statuses ->
|
||||||
}
|
when {
|
||||||
HandshakeStatus.NEVER_CONNECTED -> {
|
statuses?.all { it == HandshakeStatus.HEALTHY } == true -> {
|
||||||
if (!didShowFailedHandshakeNotification) {
|
if (!didShowConnected) {
|
||||||
launchVpnConnectionFailedNotification(
|
delay(Constants.VPN_CONNECTED_NOTIFICATION_DELAY)
|
||||||
getString(R.string.initial_connection_failure_message)
|
launchVpnNotification(
|
||||||
)
|
getString(R.string.tunnel_start_title),
|
||||||
didShowFailedHandshakeNotification = true
|
"${getString(R.string.tunnel_start_text)} $tunnelName",
|
||||||
didShowConnected = false
|
)
|
||||||
|
didShowConnected = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
statuses?.any { it == HandshakeStatus.STALE } == true -> {}
|
||||||
|
statuses?.all { it == HandshakeStatus.NOT_STARTED } ==
|
||||||
|
true -> {}
|
||||||
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
HandshakeStatus.HEALTHY -> {
|
|
||||||
if (!didShowConnected) {
|
|
||||||
launchVpnConnectedNotification()
|
|
||||||
didShowConnected = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
HandshakeStatus.STALE -> {}
|
|
||||||
HandshakeStatus.UNHEALTHY -> {
|
|
||||||
if (!didShowFailedHandshakeNotification) {
|
|
||||||
launchVpnConnectionFailedNotification(
|
|
||||||
getString(R.string.lost_connection_failure_message)
|
|
||||||
)
|
|
||||||
didShowFailedHandshakeNotification = true
|
|
||||||
didShowConnected = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun launchAlwaysOnDisabledNotification() {
|
||||||
|
launchVpnNotification(title = this.getString(R.string.vpn_connection_failed),
|
||||||
|
description = this.getString(R.string.always_on_disabled))
|
||||||
|
}
|
||||||
|
|
||||||
override fun stopService(extras: Bundle?) {
|
override fun stopService(extras: Bundle?) {
|
||||||
super.stopService(extras)
|
super.stopService(extras)
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
vpnService.stopTunnel()
|
vpnService.stopTunnel()
|
||||||
|
didShowConnected = false
|
||||||
}
|
}
|
||||||
cancelJob()
|
cancelJob()
|
||||||
stopSelf()
|
stopSelf()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchVpnConnectedNotification() {
|
private fun launchVpnNotification(
|
||||||
|
title: String = getString(R.string.vpn_starting),
|
||||||
|
description: String = getString(R.string.attempt_connection)
|
||||||
|
) {
|
||||||
val notification =
|
val notification =
|
||||||
notificationService.createNotification(
|
notificationService.createNotification(
|
||||||
channelId = getString(R.string.vpn_channel_id),
|
channelId = getString(R.string.vpn_channel_id),
|
||||||
channelName = getString(R.string.vpn_channel_name),
|
channelName = getString(R.string.vpn_channel_name),
|
||||||
title = getString(R.string.tunnel_start_title),
|
title = title,
|
||||||
onGoing = false,
|
onGoing = false,
|
||||||
vibration = false,
|
vibration = false,
|
||||||
showTimestamp = true,
|
showTimestamp = true,
|
||||||
description = "${getString(R.string.tunnel_start_text)} $tunnelName"
|
description = description,
|
||||||
)
|
)
|
||||||
ServiceCompat.startForeground(
|
ServiceCompat.startForeground(
|
||||||
this,
|
this,
|
||||||
foregroundId,
|
foregroundId,
|
||||||
notification,
|
notification,
|
||||||
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID
|
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
vibration = false,
|
|
||||||
showTimestamp = true,
|
|
||||||
description = getString(R.string.attempt_connection)
|
|
||||||
)
|
|
||||||
ServiceCompat.startForeground(
|
|
||||||
this,
|
|
||||||
foregroundId,
|
|
||||||
notification,
|
|
||||||
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,24 +161,24 @@ class WireGuardTunnelService : ForegroundService() {
|
|||||||
channelId = getString(R.string.vpn_channel_id),
|
channelId = getString(R.string.vpn_channel_id),
|
||||||
channelName = getString(R.string.vpn_channel_name),
|
channelName = getString(R.string.vpn_channel_name),
|
||||||
action =
|
action =
|
||||||
PendingIntent.getBroadcast(
|
PendingIntent.getBroadcast(
|
||||||
this,
|
this,
|
||||||
0,
|
0,
|
||||||
Intent(this, NotificationActionReceiver::class.java),
|
Intent(this, NotificationActionReceiver::class.java),
|
||||||
PendingIntent.FLAG_IMMUTABLE
|
PendingIntent.FLAG_IMMUTABLE,
|
||||||
),
|
),
|
||||||
actionText = getString(R.string.restart),
|
actionText = getString(R.string.restart),
|
||||||
title = getString(R.string.vpn_connection_failed),
|
title = getString(R.string.vpn_connection_failed),
|
||||||
onGoing = false,
|
onGoing = false,
|
||||||
vibration = true,
|
vibration = true,
|
||||||
showTimestamp = true,
|
showTimestamp = true,
|
||||||
description = message
|
description = message,
|
||||||
)
|
)
|
||||||
ServiceCompat.startForeground(
|
ServiceCompat.startForeground(
|
||||||
this,
|
this,
|
||||||
foregroundId,
|
foregroundId,
|
||||||
notification,
|
notification,
|
||||||
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID
|
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+64
-69
@@ -24,72 +24,69 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(
|
|||||||
private val wifiManager =
|
private val wifiManager =
|
||||||
context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
|
context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
|
||||||
|
|
||||||
override val networkStatus =
|
override val networkStatus = callbackFlow {
|
||||||
callbackFlow {
|
val networkStatusCallback =
|
||||||
val networkStatusCallback =
|
when (Build.VERSION.SDK_INT) {
|
||||||
when (Build.VERSION.SDK_INT) {
|
in Build.VERSION_CODES.S..Int.MAX_VALUE -> {
|
||||||
in Build.VERSION_CODES.S..Int.MAX_VALUE -> {
|
object :
|
||||||
object : ConnectivityManager.NetworkCallback(
|
ConnectivityManager.NetworkCallback(
|
||||||
FLAG_INCLUDE_LOCATION_INFO
|
FLAG_INCLUDE_LOCATION_INFO,
|
||||||
) {
|
) {
|
||||||
override fun onAvailable(network: Network) {
|
override fun onAvailable(network: Network) {
|
||||||
trySend(NetworkStatus.Available(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 -> {
|
override fun onLost(network: Network) {
|
||||||
object : ConnectivityManager.NetworkCallback() {
|
trySend(NetworkStatus.Unavailable(network))
|
||||||
override fun onAvailable(network: Network) {
|
}
|
||||||
trySend(NetworkStatus.Available(network))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLost(network: Network) {
|
override fun onCapabilitiesChanged(
|
||||||
trySend(NetworkStatus.Unavailable(network))
|
network: Network,
|
||||||
}
|
networkCapabilities: NetworkCapabilities
|
||||||
|
) {
|
||||||
override fun onCapabilitiesChanged(
|
trySend(
|
||||||
network: Network,
|
NetworkStatus.CapabilitiesChanged(
|
||||||
networkCapabilities: NetworkCapabilities
|
network,
|
||||||
) {
|
networkCapabilities,
|
||||||
trySend(
|
),
|
||||||
NetworkStatus.CapabilitiesChanged(
|
)
|
||||||
network,
|
|
||||||
networkCapabilities
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val request =
|
else -> {
|
||||||
NetworkRequest.Builder()
|
object : ConnectivityManager.NetworkCallback() {
|
||||||
.addTransportType(networkCapability)
|
override fun onAvailable(network: Network) {
|
||||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
trySend(NetworkStatus.Available(network))
|
||||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
}
|
||||||
.build()
|
|
||||||
connectivityManager.registerNetworkCallback(request, networkStatusCallback)
|
|
||||||
|
|
||||||
awaitClose {
|
override fun onLost(network: Network) {
|
||||||
connectivityManager.unregisterNetworkCallback(networkStatusCallback)
|
trySend(NetworkStatus.Unavailable(network))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCapabilitiesChanged(
|
||||||
|
network: Network,
|
||||||
|
networkCapabilities: NetworkCapabilities
|
||||||
|
) {
|
||||||
|
trySend(
|
||||||
|
NetworkStatus.CapabilitiesChanged(
|
||||||
|
network,
|
||||||
|
networkCapabilities,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
val request =
|
||||||
|
NetworkRequest.Builder()
|
||||||
|
.addTransportType(networkCapability)
|
||||||
|
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||||
|
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||||
|
.build()
|
||||||
|
connectivityManager.registerNetworkCallback(request, networkStatusCallback)
|
||||||
|
|
||||||
|
awaitClose { connectivityManager.unregisterNetworkCallback(networkStatusCallback) }
|
||||||
|
}
|
||||||
|
|
||||||
override fun getNetworkName(networkCapabilities: NetworkCapabilities): String? {
|
override fun getNetworkName(networkCapabilities: NetworkCapabilities): String? {
|
||||||
var ssid: String? = getWifiNameFromCapabilities(networkCapabilities)
|
var ssid: String? = getWifiNameFromCapabilities(networkCapabilities)
|
||||||
@@ -119,18 +116,16 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(
|
|||||||
inline fun <Result> Flow<NetworkStatus>.map(
|
inline fun <Result> Flow<NetworkStatus>.map(
|
||||||
crossinline onUnavailable: suspend (network: Network) -> Result,
|
crossinline onUnavailable: suspend (network: Network) -> Result,
|
||||||
crossinline onAvailable: suspend (network: Network) -> Result,
|
crossinline onAvailable: suspend (network: Network) -> Result,
|
||||||
crossinline onCapabilitiesChanged: suspend (
|
crossinline onCapabilitiesChanged:
|
||||||
network: Network,
|
suspend (network: Network, networkCapabilities: NetworkCapabilities) -> Result
|
||||||
networkCapabilities: NetworkCapabilities
|
): Flow<Result> = map { status ->
|
||||||
) -> Result
|
when (status) {
|
||||||
): Flow<Result> =
|
is NetworkStatus.Unavailable -> onUnavailable(status.network)
|
||||||
map { status ->
|
is NetworkStatus.Available -> onAvailable(status.network)
|
||||||
when (status) {
|
is NetworkStatus.CapabilitiesChanged ->
|
||||||
is NetworkStatus.Unavailable -> onUnavailable(status.network)
|
onCapabilitiesChanged(
|
||||||
is NetworkStatus.Available -> onAvailable(status.network)
|
|
||||||
is NetworkStatus.CapabilitiesChanged -> onCapabilitiesChanged(
|
|
||||||
status.network,
|
status.network,
|
||||||
status.networkCapabilities
|
status.networkCapabilities,
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
+1
-5
@@ -5,9 +5,5 @@ import android.net.NetworkCapabilities
|
|||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class EthernetService
|
class EthernetService @Inject constructor(@ApplicationContext context: Context) :
|
||||||
@Inject
|
|
||||||
constructor(
|
|
||||||
@ApplicationContext context: Context
|
|
||||||
) :
|
|
||||||
BaseNetworkService<EthernetService>(context, NetworkCapabilities.TRANSPORT_ETHERNET)
|
BaseNetworkService<EthernetService>(context, NetworkCapabilities.TRANSPORT_ETHERNET)
|
||||||
|
|||||||
+1
-5
@@ -5,9 +5,5 @@ import android.net.NetworkCapabilities
|
|||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class MobileDataService
|
class MobileDataService @Inject constructor(@ApplicationContext context: Context) :
|
||||||
@Inject
|
|
||||||
constructor(
|
|
||||||
@ApplicationContext context: Context
|
|
||||||
) :
|
|
||||||
BaseNetworkService<MobileDataService>(context, NetworkCapabilities.TRANSPORT_CELLULAR)
|
BaseNetworkService<MobileDataService>(context, NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||||
|
|||||||
+1
-5
@@ -5,9 +5,5 @@ import android.net.NetworkCapabilities
|
|||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class WifiService
|
class WifiService @Inject constructor(@ApplicationContext context: Context) :
|
||||||
@Inject
|
|
||||||
constructor(
|
|
||||||
@ApplicationContext context: Context
|
|
||||||
) :
|
|
||||||
BaseNetworkService<WifiService>(context, NetworkCapabilities.TRANSPORT_WIFI)
|
BaseNetworkService<WifiService>(context, NetworkCapabilities.TRANSPORT_WIFI)
|
||||||
|
|||||||
+2
-1
@@ -16,6 +16,7 @@ interface NotificationService {
|
|||||||
importance: Int = NotificationManager.IMPORTANCE_HIGH,
|
importance: Int = NotificationManager.IMPORTANCE_HIGH,
|
||||||
vibration: Boolean = false,
|
vibration: Boolean = false,
|
||||||
onGoing: Boolean = true,
|
onGoing: Boolean = true,
|
||||||
lights: Boolean = true
|
lights: Boolean = true,
|
||||||
|
onlyAlertOnce: Boolean = true,
|
||||||
): Notification
|
): Notification
|
||||||
}
|
}
|
||||||
|
|||||||
+45
-27
@@ -7,19 +7,28 @@ import android.app.PendingIntent
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.MainActivity
|
import com.zaneschepke.wireguardautotunnel.ui.MainActivity
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class WireGuardNotification
|
class WireGuardNotification @Inject constructor(@ApplicationContext private val context: Context) :
|
||||||
@Inject
|
NotificationService {
|
||||||
constructor(
|
|
||||||
@ApplicationContext private val context: Context
|
|
||||||
) : NotificationService {
|
|
||||||
private val notificationManager =
|
private val notificationManager =
|
||||||
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
|
||||||
|
private val watcherBuilder: NotificationCompat.Builder =
|
||||||
|
NotificationCompat.Builder(
|
||||||
|
context,
|
||||||
|
context.getString(R.string.watcher_channel_id),
|
||||||
|
)
|
||||||
|
private val tunnelBuilder: NotificationCompat.Builder =
|
||||||
|
NotificationCompat.Builder(
|
||||||
|
context,
|
||||||
|
context.getString(R.string.vpn_channel_id),
|
||||||
|
)
|
||||||
|
|
||||||
override fun createNotification(
|
override fun createNotification(
|
||||||
channelId: String,
|
channelId: String,
|
||||||
channelName: String,
|
channelName: String,
|
||||||
@@ -31,21 +40,23 @@ constructor(
|
|||||||
importance: Int,
|
importance: Int,
|
||||||
vibration: Boolean,
|
vibration: Boolean,
|
||||||
onGoing: Boolean,
|
onGoing: Boolean,
|
||||||
lights: Boolean
|
lights: Boolean,
|
||||||
|
onlyAlertOnce: Boolean,
|
||||||
): Notification {
|
): Notification {
|
||||||
val channel =
|
val channel =
|
||||||
NotificationChannel(
|
NotificationChannel(
|
||||||
channelId,
|
channelId,
|
||||||
channelName,
|
channelName,
|
||||||
importance
|
importance,
|
||||||
).let {
|
)
|
||||||
it.description = title
|
.let {
|
||||||
it.enableLights(lights)
|
it.description = title
|
||||||
it.lightColor = Color.RED
|
it.enableLights(lights)
|
||||||
it.enableVibration(vibration)
|
it.lightColor = Color.RED
|
||||||
it.vibrationPattern = longArrayOf(100, 200, 300, 400, 500, 400, 300, 200, 400)
|
it.enableVibration(vibration)
|
||||||
it
|
it.vibrationPattern = longArrayOf(100, 200, 300)
|
||||||
}
|
it
|
||||||
|
}
|
||||||
notificationManager.createNotificationChannel(channel)
|
notificationManager.createNotificationChannel(channel)
|
||||||
val pendingIntent: PendingIntent =
|
val pendingIntent: PendingIntent =
|
||||||
Intent(context, MainActivity::class.java).let { notificationIntent ->
|
Intent(context, MainActivity::class.java).let { notificationIntent ->
|
||||||
@@ -53,30 +64,37 @@ constructor(
|
|||||||
context,
|
context,
|
||||||
0,
|
0,
|
||||||
notificationIntent,
|
notificationIntent,
|
||||||
PendingIntent.FLAG_IMMUTABLE
|
PendingIntent.FLAG_IMMUTABLE,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val builder: Notification.Builder =
|
val builder =
|
||||||
Notification.Builder(
|
when (channelId) {
|
||||||
context,
|
context.getString(R.string.watcher_channel_id) -> watcherBuilder
|
||||||
channelId
|
context.getString(R.string.vpn_channel_id) -> tunnelBuilder
|
||||||
)
|
else -> {
|
||||||
|
NotificationCompat.Builder(
|
||||||
|
context,
|
||||||
|
channelId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return builder.let {
|
return builder.let {
|
||||||
if (action != null && actionText != null) {
|
if (action != null && actionText != null) {
|
||||||
// TODO find a not deprecated way to do this
|
|
||||||
it.addAction(
|
it.addAction(
|
||||||
Notification.Action.Builder(0, actionText, action)
|
NotificationCompat.Action.Builder(0, actionText, action).build(),
|
||||||
.build()
|
|
||||||
)
|
)
|
||||||
it.setAutoCancel(true)
|
it.setAutoCancel(true)
|
||||||
}
|
}
|
||||||
it.setContentTitle(title)
|
it.setContentTitle(title)
|
||||||
.setContentText(description)
|
.setContentText(description)
|
||||||
|
.setOnlyAlertOnce(onlyAlertOnce)
|
||||||
.setContentIntent(pendingIntent)
|
.setContentIntent(pendingIntent)
|
||||||
.setOngoing(onGoing)
|
.setOngoing(onGoing)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
.setShowWhen(showTimestamp)
|
.setShowWhen(showTimestamp)
|
||||||
.setSmallIcon(R.mipmap.ic_launcher_foreground)
|
.setSmallIcon(R.drawable.ic_launcher)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+36
-37
@@ -1,72 +1,80 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.service.shortcut
|
package com.zaneschepke.wireguardautotunnel.service.shortcut
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
|
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
|
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
|
||||||
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class ShortcutsActivity : ComponentActivity() {
|
class ShortcutsActivity : ComponentActivity() {
|
||||||
@Inject
|
@Inject lateinit var settingsRepository: SettingsRepository
|
||||||
lateinit var settingsRepo: SettingsDoa
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var tunnelConfigRepository: TunnelConfigRepository
|
||||||
lateinit var tunnelConfigRepo: TunnelConfigDao
|
|
||||||
|
|
||||||
private fun attemptWatcherServiceToggle(tunnelConfig: String) {
|
private suspend fun toggleWatcherServicePause() {
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
val settings = settingsRepository.getSettings()
|
||||||
val settings = getSettings()
|
if (settings.isAutoTunnelEnabled) {
|
||||||
if (settings.isAutoTunnelEnabled) {
|
val pauseAutoTunnel = !settings.isAutoTunnelPaused
|
||||||
ServiceManager.toggleWatcherServiceForeground(this@ShortcutsActivity, tunnelConfig)
|
settingsRepository.save(
|
||||||
}
|
settings.copy(
|
||||||
|
isAutoTunnelPaused = pauseAutoTunnel,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
if (intent.getStringExtra(CLASS_NAME_EXTRA_KEY)
|
setContentView(View(this))
|
||||||
|
if (
|
||||||
|
intent
|
||||||
|
.getStringExtra(CLASS_NAME_EXTRA_KEY)
|
||||||
.equals(WireGuardTunnelService::class.java.simpleName)
|
.equals(WireGuardTunnelService::class.java.simpleName)
|
||||||
) {
|
) {
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
val settings = getSettings()
|
val settings = settingsRepository.getSettings()
|
||||||
if (settings.isShortcutsEnabled) {
|
if (settings.isShortcutsEnabled) {
|
||||||
try {
|
try {
|
||||||
val tunnelName = intent.getStringExtra(TUNNEL_NAME_EXTRA_KEY)
|
val tunnelName = intent.getStringExtra(TUNNEL_NAME_EXTRA_KEY)
|
||||||
val tunnelConfig =
|
val tunnelConfig =
|
||||||
if (tunnelName != null) {
|
if (tunnelName != null) {
|
||||||
tunnelConfigRepo.getAll().firstOrNull { it.name == tunnelName }
|
tunnelConfigRepository.getAll().firstOrNull {
|
||||||
|
it.name == tunnelName
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (settings.defaultTunnel == null) {
|
if (settings.defaultTunnel == null) {
|
||||||
tunnelConfigRepo.getAll().first()
|
tunnelConfigRepository.getAll().first()
|
||||||
} else {
|
} else {
|
||||||
TunnelConfig.from(settings.defaultTunnel!!)
|
TunnelConfig.from(settings.defaultTunnel!!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tunnelConfig ?: return@launch
|
tunnelConfig ?: return@launch
|
||||||
attemptWatcherServiceToggle(tunnelConfig.toString())
|
toggleWatcherServicePause()
|
||||||
when (intent.action) {
|
when (intent.action) {
|
||||||
Action.STOP.name -> ServiceManager.stopVpnService(
|
Action.STOP.name ->
|
||||||
this@ShortcutsActivity
|
ServiceManager.stopVpnService(
|
||||||
)
|
this@ShortcutsActivity,
|
||||||
Action.START.name -> ServiceManager.startVpnService(
|
)
|
||||||
this@ShortcutsActivity,
|
Action.START.name ->
|
||||||
tunnelConfig.toString()
|
ServiceManager.startVpnServiceForeground(
|
||||||
)
|
this@ShortcutsActivity,
|
||||||
|
tunnelConfig.toString(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e.message)
|
Timber.e(e.message)
|
||||||
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,15 +82,6 @@ class ShortcutsActivity : ComponentActivity() {
|
|||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getSettings(): Settings {
|
|
||||||
val settings = settingsRepo.getAll()
|
|
||||||
return if (settings.isNotEmpty()) {
|
|
||||||
settings.first()
|
|
||||||
} else {
|
|
||||||
throw WgTunnelException("Settings empty")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val TUNNEL_NAME_EXTRA_KEY = "tunnelName"
|
const val TUNNEL_NAME_EXTRA_KEY = "tunnelName"
|
||||||
const val CLASS_NAME_EXTRA_KEY = "className"
|
const val CLASS_NAME_EXTRA_KEY = "className"
|
||||||
|
|||||||
+70
-96
@@ -4,47 +4,57 @@ import android.os.Build
|
|||||||
import android.service.quicksettings.Tile
|
import android.service.quicksettings.Tile
|
||||||
import android.service.quicksettings.TileService
|
import android.service.quicksettings.TileService
|
||||||
import com.wireguard.android.backend.Tunnel
|
import com.wireguard.android.backend.Tunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
|
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class TunnelControlTile : TileService() {
|
class TunnelControlTile() : TileService() {
|
||||||
@Inject
|
|
||||||
lateinit var settingsRepo: SettingsDoa
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var tunnelConfigRepository: TunnelConfigRepository
|
||||||
lateinit var configRepo: TunnelConfigDao
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var settingsRepository: SettingsRepository
|
||||||
lateinit var vpnService: VpnService
|
|
||||||
|
|
||||||
private val scope = CoroutineScope(Dispatchers.Main)
|
@Inject lateinit var vpnService: VpnService
|
||||||
|
|
||||||
private lateinit var job: Job
|
private val scope = CoroutineScope(Dispatchers.IO)
|
||||||
|
|
||||||
|
private var tunnelName: String? = null
|
||||||
|
|
||||||
override fun onStartListening() {
|
override fun onStartListening() {
|
||||||
job =
|
|
||||||
scope.launch {
|
|
||||||
updateTileState()
|
|
||||||
}
|
|
||||||
super.onStartListening()
|
super.onStartListening()
|
||||||
}
|
Timber.d("On start listening called")
|
||||||
|
scope.launch {
|
||||||
override fun onTileRemoved() {
|
vpnService.vpnState.collect {
|
||||||
super.onTileRemoved()
|
when (it.status) {
|
||||||
cancelJob()
|
Tunnel.State.UP -> setActive()
|
||||||
|
Tunnel.State.DOWN -> setInactive()
|
||||||
|
else -> setInactive()
|
||||||
|
}
|
||||||
|
val tunnels = tunnelConfigRepository.getAll()
|
||||||
|
if (tunnels.isEmpty()) {
|
||||||
|
setUnavailable()
|
||||||
|
return@collect
|
||||||
|
}
|
||||||
|
tunnelName =
|
||||||
|
it.name.ifBlank {
|
||||||
|
val settings = settingsRepository.getSettings()
|
||||||
|
if (settings.defaultTunnel != null) {
|
||||||
|
TunnelConfig.from(settings.defaultTunnel!!).name
|
||||||
|
} else tunnels.firstOrNull()?.name
|
||||||
|
}
|
||||||
|
setTileDescription(tunnelName ?: "")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
@@ -52,22 +62,26 @@ class TunnelControlTile : TileService() {
|
|||||||
scope.cancel()
|
scope.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onTileRemoved() {
|
||||||
|
super.onTileRemoved()
|
||||||
|
scope.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onClick() {
|
override fun onClick() {
|
||||||
super.onClick()
|
super.onClick()
|
||||||
unlockAndRun {
|
unlockAndRun {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
val tunnel = determineTileTunnel()
|
val tunnelConfig =
|
||||||
if (tunnel != null) {
|
tunnelConfigRepository.getAll().first { it.name == tunnelName }
|
||||||
attemptWatcherServiceToggle(tunnel.toString())
|
toggleWatcherServicePause()
|
||||||
if (vpnService.getState() == Tunnel.State.UP) {
|
if (vpnService.getState() == Tunnel.State.UP) {
|
||||||
ServiceManager.stopVpnService(this@TunnelControlTile)
|
ServiceManager.stopVpnService(this@TunnelControlTile)
|
||||||
} else {
|
} else {
|
||||||
ServiceManager.startVpnServiceForeground(
|
ServiceManager.startVpnServiceForeground(
|
||||||
this@TunnelControlTile,
|
this@TunnelControlTile,
|
||||||
tunnel.toString()
|
tunnelConfig.toString(),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e.message)
|
Timber.e(e.message)
|
||||||
@@ -78,68 +92,33 @@ class TunnelControlTile : TileService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun determineTileTunnel(): TunnelConfig? {
|
private fun toggleWatcherServicePause() {
|
||||||
var tunnelConfig: TunnelConfig? = null
|
|
||||||
val settings = settingsRepo.getAll()
|
|
||||||
if (settings.isNotEmpty()) {
|
|
||||||
val setting = settings.first()
|
|
||||||
tunnelConfig =
|
|
||||||
if (setting.defaultTunnel != null) {
|
|
||||||
TunnelConfig.from(setting.defaultTunnel!!)
|
|
||||||
} else {
|
|
||||||
val configs = configRepo.getAll()
|
|
||||||
val config =
|
|
||||||
if (configs.isNotEmpty()) {
|
|
||||||
configs.first()
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
config
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tunnelConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun attemptWatcherServiceToggle(tunnelConfig: String) {
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val settings = settingsRepo.getAll()
|
val settings = settingsRepository.getSettings()
|
||||||
if (settings.isNotEmpty()) {
|
if (settings.isAutoTunnelEnabled) {
|
||||||
val setting = settings.first()
|
val pauseAutoTunnel = !settings.isAutoTunnelPaused
|
||||||
if (setting.isAutoTunnelEnabled) {
|
settingsRepository.save(
|
||||||
ServiceManager.toggleWatcherServiceForeground(
|
settings.copy(
|
||||||
this@TunnelControlTile,
|
isAutoTunnelPaused = pauseAutoTunnel,
|
||||||
tunnelConfig
|
),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun updateTileState() {
|
private fun setActive() {
|
||||||
vpnService.state.collect {
|
qsTile.state = Tile.STATE_ACTIVE
|
||||||
try {
|
qsTile.updateTile()
|
||||||
when (it) {
|
}
|
||||||
Tunnel.State.UP -> {
|
|
||||||
qsTile.state = Tile.STATE_ACTIVE
|
|
||||||
}
|
|
||||||
|
|
||||||
Tunnel.State.DOWN -> {
|
private fun setInactive() {
|
||||||
qsTile.state = Tile.STATE_INACTIVE
|
qsTile.state = Tile.STATE_INACTIVE
|
||||||
}
|
qsTile.updateTile()
|
||||||
|
}
|
||||||
|
|
||||||
else -> {
|
private fun setUnavailable() {
|
||||||
qsTile.state = Tile.STATE_UNAVAILABLE
|
qsTile.state = Tile.STATE_UNAVAILABLE
|
||||||
}
|
qsTile.updateTile()
|
||||||
}
|
|
||||||
val config = determineTileTunnel()
|
|
||||||
setTileDescription(
|
|
||||||
config?.name ?: this.resources.getString(R.string.no_tunnel_available)
|
|
||||||
)
|
|
||||||
qsTile.updateTile()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e("Unable to update tile state")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setTileDescription(description: String) {
|
private fun setTileDescription(description: String) {
|
||||||
@@ -149,11 +128,6 @@ class TunnelControlTile : TileService() {
|
|||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
qsTile.stateDescription = description
|
qsTile.stateDescription = description
|
||||||
}
|
}
|
||||||
}
|
qsTile.updateTile()
|
||||||
|
|
||||||
private fun cancelJob() {
|
|
||||||
if (this::job.isInitialized) {
|
|
||||||
job.cancel()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-5
@@ -3,15 +3,14 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel
|
|||||||
enum class HandshakeStatus {
|
enum class HandshakeStatus {
|
||||||
HEALTHY,
|
HEALTHY,
|
||||||
STALE,
|
STALE,
|
||||||
UNHEALTHY,
|
UNKNOWN,
|
||||||
NEVER_CONNECTED,
|
NOT_STARTED;
|
||||||
NOT_STARTED
|
|
||||||
;
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val WG_TYPICAL_HANDSHAKE_INTERVAL_WHEN_HEALTHY_SEC = 180
|
private const val WG_TYPICAL_HANDSHAKE_INTERVAL_WHEN_HEALTHY_SEC = 180
|
||||||
const val STATUS_CHANGE_TIME_BUFFER = 30
|
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 STALE_TIME_LIMIT_SEC =
|
||||||
|
WG_TYPICAL_HANDSHAKE_INTERVAL_WHEN_HEALTHY_SEC + STATUS_CHANGE_TIME_BUFFER
|
||||||
const val NEVER_CONNECTED_TO_UNHEALTHY_TIME_LIMIT_SEC = 30
|
const val NEVER_CONNECTED_TO_UNHEALTHY_TIME_LIMIT_SEC = 30
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,15 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.service.tunnel
|
package com.zaneschepke.wireguardautotunnel.service.tunnel
|
||||||
|
|
||||||
import com.wireguard.android.backend.Statistics
|
|
||||||
import com.wireguard.android.backend.Tunnel
|
import com.wireguard.android.backend.Tunnel
|
||||||
import com.wireguard.crypto.Key
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
|
||||||
|
|
||||||
interface VpnService : Tunnel {
|
interface VpnService : Tunnel {
|
||||||
suspend fun startTunnel(tunnelConfig: TunnelConfig): Tunnel.State
|
suspend fun startTunnel(tunnelConfig: TunnelConfig): Tunnel.State
|
||||||
|
|
||||||
suspend fun stopTunnel()
|
suspend fun stopTunnel()
|
||||||
|
|
||||||
val state: SharedFlow<Tunnel.State>
|
val vpnState: StateFlow<VpnState>
|
||||||
val tunnelName: SharedFlow<String>
|
|
||||||
val statistics: SharedFlow<Statistics>
|
|
||||||
val lastHandshake: SharedFlow<Map<Key, Long>>
|
|
||||||
val handshakeStatus: SharedFlow<HandshakeStatus>
|
|
||||||
|
|
||||||
fun getState(): Tunnel.State
|
fun getState(): Tunnel.State
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.service.tunnel
|
||||||
|
|
||||||
|
import com.wireguard.android.backend.Statistics
|
||||||
|
import com.wireguard.android.backend.Tunnel
|
||||||
|
|
||||||
|
data class VpnState(
|
||||||
|
val status: Tunnel.State = Tunnel.State.DOWN,
|
||||||
|
val name: String = "",
|
||||||
|
val statistics: Statistics? = null
|
||||||
|
)
|
||||||
+52
-90
@@ -3,60 +3,34 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel
|
|||||||
import com.wireguard.android.backend.Backend
|
import com.wireguard.android.backend.Backend
|
||||||
import com.wireguard.android.backend.BackendException
|
import com.wireguard.android.backend.BackendException
|
||||||
import com.wireguard.android.backend.Statistics
|
import com.wireguard.android.backend.Statistics
|
||||||
import com.wireguard.android.backend.Tunnel
|
import com.wireguard.android.backend.Tunnel.State
|
||||||
import com.wireguard.config.Config
|
import com.wireguard.config.Config
|
||||||
import com.wireguard.crypto.Key
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.Constants
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.module.Kernel
|
import com.zaneschepke.wireguardautotunnel.module.Kernel
|
||||||
import com.zaneschepke.wireguardautotunnel.module.Userspace
|
import com.zaneschepke.wireguardautotunnel.module.Userspace
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.channels.BufferOverflow
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
class WireGuardTunnel
|
class WireGuardTunnel
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
@Userspace private val userspaceBackend: Backend,
|
@Userspace private val userspaceBackend : Backend,
|
||||||
@Kernel private val kernelBackend: Backend,
|
@Kernel private val kernelBackend: Backend,
|
||||||
private val settingsRepo: SettingsDoa
|
private val settingsRepository: SettingsRepository
|
||||||
) : VpnService {
|
) : VpnService {
|
||||||
private val _tunnelName = MutableStateFlow("")
|
private val _vpnState = MutableStateFlow(VpnState())
|
||||||
override val tunnelName get() = _tunnelName.asStateFlow()
|
override val vpnState: StateFlow<VpnState> = _vpnState.asStateFlow()
|
||||||
|
|
||||||
private val _state =
|
|
||||||
MutableSharedFlow<Tunnel.State>(
|
|
||||||
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
|
||||||
replay = 1
|
|
||||||
)
|
|
||||||
|
|
||||||
private val _handshakeStatus =
|
|
||||||
MutableSharedFlow<HandshakeStatus>(
|
|
||||||
replay = 1,
|
|
||||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
|
||||||
)
|
|
||||||
override val state get() = _state.asSharedFlow()
|
|
||||||
|
|
||||||
private val _statistics = MutableSharedFlow<Statistics>(replay = 1)
|
|
||||||
override val statistics get() = _statistics.asSharedFlow()
|
|
||||||
|
|
||||||
private val _lastHandshake = MutableSharedFlow<Map<Key, Long>>(replay = 1)
|
|
||||||
override val lastHandshake get() = _lastHandshake.asSharedFlow()
|
|
||||||
|
|
||||||
override val handshakeStatus: SharedFlow<HandshakeStatus>
|
|
||||||
get() = _handshakeStatus.asSharedFlow()
|
|
||||||
|
|
||||||
private val scope = CoroutineScope(Dispatchers.IO)
|
private val scope = CoroutineScope(Dispatchers.IO)
|
||||||
|
|
||||||
@@ -70,13 +44,12 @@ constructor(
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
settingsRepo.getAllFlow().collect {
|
settingsRepository.getSettingsFlow().collect {
|
||||||
val settings = it.first()
|
if (it.isKernelEnabled && backendIsUserspace) {
|
||||||
if (settings.isKernelEnabled && backendIsUserspace) {
|
|
||||||
Timber.d("Setting kernel backend")
|
Timber.d("Setting kernel backend")
|
||||||
backend = kernelBackend
|
backend = kernelBackend
|
||||||
backendIsUserspace = false
|
backendIsUserspace = false
|
||||||
} else if (!settings.isKernelEnabled && !backendIsUserspace) {
|
} else if (!it.isKernelEnabled && !backendIsUserspace) {
|
||||||
Timber.d("Setting userspace backend")
|
Timber.d("Setting userspace backend")
|
||||||
backend = userspaceBackend
|
backend = userspaceBackend
|
||||||
backendIsUserspace = true
|
backendIsUserspace = true
|
||||||
@@ -85,7 +58,7 @@ constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun startTunnel(tunnelConfig: TunnelConfig): Tunnel.State {
|
override suspend fun startTunnel(tunnelConfig: TunnelConfig): State {
|
||||||
return try {
|
return try {
|
||||||
stopTunnelOnConfigChange(tunnelConfig)
|
stopTunnelOnConfigChange(tunnelConfig)
|
||||||
emitTunnelName(tunnelConfig.name)
|
emitTunnelName(tunnelConfig.name)
|
||||||
@@ -93,95 +66,84 @@ constructor(
|
|||||||
val state =
|
val state =
|
||||||
backend.setState(
|
backend.setState(
|
||||||
this,
|
this,
|
||||||
Tunnel.State.UP,
|
State.UP,
|
||||||
config
|
config,
|
||||||
)
|
)
|
||||||
_state.emit(state)
|
emitTunnelState(state)
|
||||||
state
|
state
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e("Failed to start tunnel with error: ${e.message}")
|
Timber.e("Failed to start tunnel with error: ${e.message}")
|
||||||
Tunnel.State.DOWN
|
State.DOWN
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun emitTunnelState(state: State) {
|
||||||
|
_vpnState.tryEmit(
|
||||||
|
_vpnState.value.copy(
|
||||||
|
status = state,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun emitBackendStatistics(statistics: Statistics) {
|
||||||
|
_vpnState.tryEmit(
|
||||||
|
_vpnState.value.copy(
|
||||||
|
statistics = statistics,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun emitTunnelName(name: String) {
|
private suspend fun emitTunnelName(name: String) {
|
||||||
_tunnelName.emit(name)
|
_vpnState.emit(
|
||||||
|
_vpnState.value.copy(
|
||||||
|
name = name,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun stopTunnelOnConfigChange(tunnelConfig: TunnelConfig) {
|
private suspend fun stopTunnelOnConfigChange(tunnelConfig: TunnelConfig) {
|
||||||
if (getState() == Tunnel.State.UP && _tunnelName.value != tunnelConfig.name) {
|
if (getState() == State.UP && _vpnState.value.name != tunnelConfig.name) {
|
||||||
stopTunnel()
|
stopTunnel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getName(): String {
|
override fun getName(): String {
|
||||||
return _tunnelName.value
|
return _vpnState.value.name
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun stopTunnel() {
|
override suspend fun stopTunnel() {
|
||||||
try {
|
try {
|
||||||
if (getState() == Tunnel.State.UP) {
|
if (getState() == State.UP) {
|
||||||
val state = backend.setState(this, Tunnel.State.DOWN, null)
|
val state = backend.setState(this, State.DOWN, null)
|
||||||
_state.emit(state)
|
emitTunnelState(state)
|
||||||
}
|
}
|
||||||
} catch (e: BackendException) {
|
} catch (e: BackendException) {
|
||||||
Timber.e("Failed to stop tunnel with error: ${e.message}")
|
Timber.e("Failed to stop tunnel with error: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getState(): Tunnel.State {
|
override fun getState(): State {
|
||||||
return backend.getState(this)
|
return backend.getState(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStateChange(state: Tunnel.State) {
|
override fun onStateChange(state: State) {
|
||||||
val tunnel = this
|
val tunnel = this
|
||||||
_state.tryEmit(state)
|
emitTunnelState(state)
|
||||||
if (state == Tunnel.State.UP) {
|
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
||||||
|
if (state == State.UP) {
|
||||||
statsJob =
|
statsJob =
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val handshakeMap = HashMap<Key, Long>()
|
|
||||||
var neverHadHandshakeCounter = 0
|
|
||||||
while (true) {
|
while (true) {
|
||||||
val statistics = backend.getStatistics(tunnel)
|
val statistics = backend.getStatistics(tunnel)
|
||||||
_statistics.emit(statistics)
|
emitBackendStatistics(statistics)
|
||||||
statistics.peers().forEach { key ->
|
|
||||||
val handshakeEpoch =
|
|
||||||
statistics.peer(key)?.latestHandshakeEpochMillis ?: 0L
|
|
||||||
handshakeMap[key] = 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 += (1 * Constants.VPN_STATISTIC_CHECK_INTERVAL / 1000).toInt()
|
|
||||||
}
|
|
||||||
return@forEach
|
|
||||||
}
|
|
||||||
// TODO one day make each peer have their own dedicated status
|
|
||||||
val lastHandshake = NumberUtils.getSecondsBetweenTimestampAndNow(
|
|
||||||
handshakeEpoch
|
|
||||||
)
|
|
||||||
if (lastHandshake != null) {
|
|
||||||
if (lastHandshake >= HandshakeStatus.STALE_TIME_LIMIT_SEC) {
|
|
||||||
_handshakeStatus.emit(HandshakeStatus.STALE)
|
|
||||||
} else {
|
|
||||||
_handshakeStatus.emit(HandshakeStatus.HEALTHY)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_lastHandshake.emit(handshakeMap)
|
|
||||||
delay(Constants.VPN_STATISTIC_CHECK_INTERVAL)
|
delay(Constants.VPN_STATISTIC_CHECK_INTERVAL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (state == Tunnel.State.DOWN) {
|
if (state == State.DOWN) {
|
||||||
if (this::statsJob.isInitialized) {
|
if (this::statsJob.isInitialized) {
|
||||||
statsJob.cancel()
|
statsJob.cancel()
|
||||||
}
|
}
|
||||||
_handshakeStatus.tryEmit(HandshakeStatus.NOT_STARTED)
|
|
||||||
_lastHandshake.tryEmit(emptyMap())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui
|
package com.zaneschepke.wireguardautotunnel.ui
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class ActivityViewModel @Inject constructor() : ViewModel() {
|
@HiltViewModel
|
||||||
// TODO move shared logic to shared viewmodel
|
class ActivityViewModel
|
||||||
|
@Inject
|
||||||
|
constructor() : ViewModel() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,15 +6,13 @@ import android.net.Uri
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.view.KeyEvent
|
import androidx.activity.SystemBarStyle
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.animation.ExitTransition
|
import androidx.compose.foundation.focusable
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.animation.slideInHorizontally
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.SnackbarData
|
import androidx.compose.material3.SnackbarData
|
||||||
@@ -24,13 +22,12 @@ import androidx.compose.material3.SnackbarHostState
|
|||||||
import androidx.compose.material3.SnackbarResult
|
import androidx.compose.material3.SnackbarResult
|
||||||
import androidx.compose.material3.surfaceColorAtElevation
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.input.key.onKeyEvent
|
import androidx.compose.ui.focus.focusProperties
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.toArgb
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
@@ -39,9 +36,10 @@ import androidx.navigation.compose.rememberNavController
|
|||||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
import com.google.accompanist.permissions.isGranted
|
import com.google.accompanist.permissions.isGranted
|
||||||
import com.google.accompanist.permissions.rememberPermissionState
|
import com.google.accompanist.permissions.rememberPermissionState
|
||||||
import com.wireguard.android.backend.GoBackend
|
|
||||||
import com.zaneschepke.wireguardautotunnel.Constants
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.PermissionRequestFailedScreen
|
import com.zaneschepke.wireguardautotunnel.ui.common.PermissionRequestFailedScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar
|
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar
|
||||||
@@ -49,57 +47,61 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen
|
|||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.TransparentSystemBars
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
|
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.io.IOException
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var dataStoreManager: DataStoreManager
|
||||||
|
|
||||||
|
@Inject lateinit var settingsRepository: SettingsRepository
|
||||||
@OptIn(
|
@OptIn(
|
||||||
ExperimentalPermissionsApi::class
|
ExperimentalPermissionsApi::class,
|
||||||
)
|
)
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
enableEdgeToEdge(navigationBarStyle = SystemBarStyle.dark(Color.Transparent.toArgb()))
|
||||||
|
|
||||||
|
// load preferences into memory and init data
|
||||||
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
dataStoreManager.init()
|
||||||
|
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Timber.e("Failed to load preferences")
|
||||||
|
}
|
||||||
|
}
|
||||||
setContent {
|
setContent {
|
||||||
// TODO move shared logic to shared viewmodel
|
//val activityViewModel = hiltViewModel<ActivityViewModel>()
|
||||||
// val sharedViewModel = hiltViewModel<ActivityViewModel>()
|
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
WireguardAutoTunnelTheme {
|
WireguardAutoTunnelTheme {
|
||||||
TransparentSystemBars()
|
|
||||||
|
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
|
||||||
val notificationPermissionState =
|
val notificationPermissionState = if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
|
||||||
rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS)
|
rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS) else null
|
||||||
|
|
||||||
fun requestNotificationPermission() {
|
fun requestNotificationPermission() {
|
||||||
if (!notificationPermissionState.status.isGranted && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
if (notificationPermissionState != null && !notificationPermissionState.status.isGranted
|
||||||
|
) {
|
||||||
notificationPermissionState.launchPermissionRequest()
|
notificationPermissionState.launchPermissionRequest()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var vpnIntent by remember { mutableStateOf(GoBackend.VpnService.prepare(this)) }
|
LaunchedEffect(Unit) {
|
||||||
val vpnActivityResultState =
|
requestNotificationPermission()
|
||||||
rememberLauncherForActivityResult(
|
|
||||||
ActivityResultContracts.StartActivityForResult(),
|
|
||||||
onResult = {
|
|
||||||
val accepted = (it.resultCode == RESULT_OK)
|
|
||||||
if (accepted) {
|
|
||||||
vpnIntent = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
LaunchedEffect(vpnIntent) {
|
|
||||||
if (vpnIntent != null) {
|
|
||||||
vpnActivityResultState.launch(vpnIntent)
|
|
||||||
} else {
|
|
||||||
requestNotificationPermission()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showSnackBarMessage(message: String) {
|
fun showSnackBarMessage(message: String) {
|
||||||
@@ -108,13 +110,10 @@ class MainActivity : AppCompatActivity() {
|
|||||||
snackbarHostState.showSnackbar(
|
snackbarHostState.showSnackbar(
|
||||||
message = message,
|
message = message,
|
||||||
actionLabel = applicationContext.getString(R.string.okay),
|
actionLabel = applicationContext.getString(R.string.okay),
|
||||||
duration = SnackbarDuration.Short
|
duration = SnackbarDuration.Short,
|
||||||
)
|
)
|
||||||
when (result) {
|
when (result) {
|
||||||
SnackbarResult.ActionPerformed -> {
|
SnackbarResult.ActionPerformed,
|
||||||
snackbarHostState.currentSnackbarData?.dismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
SnackbarResult.Dismissed -> {
|
SnackbarResult.Dismissed -> {
|
||||||
snackbarHostState.currentSnackbarData?.dismiss()
|
snackbarHostState.currentSnackbarData?.dismiss()
|
||||||
}
|
}
|
||||||
@@ -128,160 +127,96 @@ class MainActivity : AppCompatActivity() {
|
|||||||
CustomSnackBar(
|
CustomSnackBar(
|
||||||
snackbarData.visuals.message,
|
snackbarData.visuals.message,
|
||||||
isRtl = false,
|
isRtl = false,
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(
|
containerColor =
|
||||||
2.dp
|
MaterialTheme.colorScheme.surfaceColorAtElevation(
|
||||||
)
|
2.dp,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier =
|
modifier = Modifier
|
||||||
Modifier.onKeyEvent {
|
.focusable()
|
||||||
if (it.nativeKeyEvent.action == KeyEvent.ACTION_UP) {
|
.focusProperties { up = focusRequester },
|
||||||
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 =
|
bottomBar =
|
||||||
if (vpnIntent == null && notificationPermissionState.status.isGranted) {
|
if (notificationPermissionState == null || notificationPermissionState.status.isGranted) {
|
||||||
{ BottomNavBar(navController, Routes.navItems) }
|
{
|
||||||
} else {
|
BottomNavBar(
|
||||||
{}
|
navController,
|
||||||
}
|
listOf(
|
||||||
) { padding ->
|
Screen.Main.navItem,
|
||||||
if (vpnIntent != null) {
|
Screen.Settings.navItem,
|
||||||
PermissionRequestFailedScreen(
|
Screen.Support.navItem,
|
||||||
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(
|
|
||||||
Constants.URI_PACKAGE_SCHEME,
|
|
||||||
this.packageName,
|
|
||||||
null
|
|
||||||
)
|
|
||||||
startActivity(intentSettings)
|
|
||||||
},
|
|
||||||
message = getString(R.string.notification_permission_required),
|
|
||||||
getString(R.string.open_settings)
|
|
||||||
)
|
|
||||||
return@Scaffold
|
|
||||||
}
|
|
||||||
|
|
||||||
NavHost(navController, startDestination = Routes.Main.name) {
|
|
||||||
composable(
|
|
||||||
Routes.Main.name,
|
|
||||||
enterTransition = {
|
|
||||||
when (initialState.destination.route) {
|
|
||||||
Routes.Settings.name, Routes.Support.name ->
|
|
||||||
slideInHorizontally(
|
|
||||||
initialOffsetX = {
|
|
||||||
-Constants.SLIDE_IN_TRANSITION_OFFSET
|
|
||||||
},
|
|
||||||
animationSpec = tween(
|
|
||||||
Constants.SLIDE_IN_ANIMATION_DURATION
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
fadeIn(
|
|
||||||
animationSpec = tween(
|
|
||||||
Constants.FADE_IN_ANIMATION_DURATION
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
exitTransition = {
|
|
||||||
ExitTransition.None
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
MainScreen(padding = padding, showSnackbarMessage = { message ->
|
|
||||||
showSnackBarMessage(message)
|
|
||||||
}, navController = navController)
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
fadeIn(
|
|
||||||
animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
SettingsScreen(padding = padding, showSnackbarMessage = { message ->
|
|
||||||
showSnackBarMessage(message)
|
|
||||||
}, focusRequester = focusRequester)
|
|
||||||
}
|
|
||||||
composable(Routes.Support.name, enterTransition = {
|
|
||||||
when (initialState.destination.route) {
|
|
||||||
Routes.Settings.name, Routes.Main.name ->
|
|
||||||
slideInHorizontally(
|
|
||||||
initialOffsetX = { Constants.SLIDE_IN_ANIMATION_DURATION },
|
|
||||||
animationSpec = tween(Constants.SLIDE_IN_ANIMATION_DURATION)
|
|
||||||
)
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
fadeIn(
|
|
||||||
animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}) { SupportScreen(padding = padding, focusRequester = focusRequester) }
|
|
||||||
composable("${Routes.Config.name}/{id}", enterTransition = {
|
|
||||||
fadeIn(animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION))
|
|
||||||
}) {
|
|
||||||
val id = it.arguments?.getString("id")
|
|
||||||
if (!id.isNullOrBlank()) {
|
|
||||||
ConfigScreen(
|
|
||||||
navController = navController,
|
|
||||||
id = id,
|
|
||||||
showSnackbarMessage = { message ->
|
|
||||||
showSnackBarMessage(message)
|
|
||||||
},
|
|
||||||
focusRequester = focusRequester
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
{}
|
||||||
|
},
|
||||||
|
) { padding ->
|
||||||
|
if (notificationPermissionState != null && !notificationPermissionState.status.isGranted) {
|
||||||
|
Column(modifier = Modifier.padding(padding)) {
|
||||||
|
PermissionRequestFailedScreen(
|
||||||
|
onRequestAgain = {
|
||||||
|
val intentSettings =
|
||||||
|
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||||
|
intentSettings.data =
|
||||||
|
Uri.fromParts(
|
||||||
|
Constants.URI_PACKAGE_SCHEME,
|
||||||
|
this@MainActivity.packageName,
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
startActivity(intentSettings)
|
||||||
|
},
|
||||||
|
message = getString(R.string.notification_permission_required),
|
||||||
|
getString(R.string.open_settings),
|
||||||
|
)
|
||||||
|
return@Scaffold
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NavHost(navController, startDestination = Screen.Main.route) {
|
||||||
|
composable(
|
||||||
|
Screen.Main.route,
|
||||||
|
) {
|
||||||
|
MainScreen(
|
||||||
|
focusRequester = focusRequester,
|
||||||
|
showSnackbarMessage = { message -> showSnackBarMessage(message) },
|
||||||
|
navController = navController,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composable(
|
||||||
|
Screen.Settings.route,
|
||||||
|
) {
|
||||||
|
SettingsScreen(
|
||||||
|
padding = padding,
|
||||||
|
showSnackbarMessage = { message -> showSnackBarMessage(message) },
|
||||||
|
focusRequester = focusRequester,
|
||||||
|
)
|
||||||
|
//
|
||||||
|
}
|
||||||
|
composable(
|
||||||
|
Screen.Support.route,
|
||||||
|
) {
|
||||||
|
SupportScreen(
|
||||||
|
padding = padding,
|
||||||
|
focusRequester = focusRequester,
|
||||||
|
showSnackbarMessage = { message -> showSnackBarMessage(message) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composable("${Screen.Config.route}/{id}") {
|
||||||
|
val id = it.arguments?.getString("id")
|
||||||
|
if (!id.isNullOrBlank()) {
|
||||||
|
ConfigScreen(
|
||||||
|
padding = padding,
|
||||||
|
navController = navController,
|
||||||
|
id = id,
|
||||||
|
showSnackbarMessage = { message ->
|
||||||
|
showSnackBarMessage(message)
|
||||||
|
},
|
||||||
|
focusRequester = focusRequester,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
;
|
|
||||||
|
|
||||||
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,38 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
sealed class Screen(val route: String) {
|
||||||
|
data object Main : Screen("main") {
|
||||||
|
val navItem =
|
||||||
|
BottomNavItem(
|
||||||
|
name = "Tunnels",
|
||||||
|
route = route,
|
||||||
|
icon = Icons.Rounded.Home,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data object Settings : Screen("settings") {
|
||||||
|
val navItem =
|
||||||
|
BottomNavItem(
|
||||||
|
name = "Settings",
|
||||||
|
route = route,
|
||||||
|
icon = Icons.Rounded.Settings,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data object Support : Screen("support") {
|
||||||
|
val navItem =
|
||||||
|
BottomNavItem(
|
||||||
|
name = "Support",
|
||||||
|
route = route,
|
||||||
|
icon = Icons.Rounded.QuestionMark,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data object Config : Screen("config")
|
||||||
|
}
|
||||||
+8
-10
@@ -1,10 +1,8 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.common
|
package com.zaneschepke.wireguardautotunnel.ui.common
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.IntrinsicSize
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@@ -13,19 +11,19 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ClickableIconButton(
|
fun ClickableIconButton(
|
||||||
|
onClick: () -> Unit,
|
||||||
onIconClick: () -> Unit,
|
onIconClick: () -> Unit,
|
||||||
text: String,
|
text: String,
|
||||||
icon: ImageVector,
|
icon: ImageVector,
|
||||||
enabled: Boolean
|
enabled: Boolean
|
||||||
) {
|
) {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {},
|
onClick = onClick,
|
||||||
enabled = enabled
|
enabled = enabled,
|
||||||
) {
|
) {
|
||||||
Text(text, Modifier.weight(1f, false))
|
Text(text, Modifier.weight(1f, false))
|
||||||
Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing))
|
Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing))
|
||||||
@@ -33,11 +31,11 @@ fun ClickableIconButton(
|
|||||||
imageVector = icon,
|
imageVector = icon,
|
||||||
contentDescription = stringResource(R.string.delete),
|
contentDescription = stringResource(R.string.delete),
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.size(ButtonDefaults.IconSize).weight(1f, false).clickable {
|
Modifier.size(ButtonDefaults.IconSize).weight(1f, false).clickable {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
onIconClick()
|
onIconClick()
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-11
@@ -2,7 +2,6 @@ package com.zaneschepke.wireguardautotunnel.ui.common
|
|||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
@@ -17,7 +16,6 @@ import kotlinx.coroutines.launch
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PermissionRequestFailedScreen(
|
fun PermissionRequestFailedScreen(
|
||||||
padding: PaddingValues,
|
|
||||||
onRequestAgain: () -> Unit,
|
onRequestAgain: () -> Unit,
|
||||||
message: String,
|
message: String,
|
||||||
buttonText: String
|
buttonText: String
|
||||||
@@ -26,17 +24,12 @@ fun PermissionRequestFailedScreen(
|
|||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
modifier =
|
modifier = Modifier.fillMaxSize(),
|
||||||
Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(padding)
|
|
||||||
) {
|
) {
|
||||||
Text(message, textAlign = TextAlign.Center, modifier = Modifier.padding(15.dp))
|
Text(message, textAlign = TextAlign.Center, modifier = Modifier.padding(15.dp))
|
||||||
Button(onClick = {
|
Button(
|
||||||
scope.launch {
|
onClick = { scope.launch { onRequestAgain() } },
|
||||||
onRequestAgain()
|
) {
|
||||||
}
|
|
||||||
}) {
|
|
||||||
Text(buttonText)
|
Text(buttonText)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ import androidx.compose.ui.draw.clip
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.wireguard.android.backend.Statistics
|
import com.wireguard.android.backend.Statistics
|
||||||
import com.zaneschepke.wireguardautotunnel.toThreeDecimalPlaceString
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.toThreeDecimalPlaceString
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -34,30 +34,22 @@ fun RowListItem(
|
|||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier.animateContentSize()
|
||||||
.animateContentSize()
|
.clip(RoundedCornerShape(30.dp))
|
||||||
.clip(RoundedCornerShape(30.dp))
|
.combinedClickable(
|
||||||
.combinedClickable(
|
onClick = { onClick() },
|
||||||
onClick = {
|
onLongClick = { onHold() },
|
||||||
onClick()
|
),
|
||||||
},
|
|
||||||
onLongClick = {
|
|
||||||
onHold()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier = Modifier.fillMaxWidth().padding(horizontal = 15.dp, vertical = 5.dp),
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 14.dp, vertical = 5.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.fillMaxWidth(.60f)
|
modifier = Modifier.fillMaxWidth(.60f),
|
||||||
) {
|
) {
|
||||||
icon()
|
icon()
|
||||||
Text(text)
|
Text(text)
|
||||||
@@ -68,11 +60,10 @@ fun RowListItem(
|
|||||||
statistics?.peers()?.forEach {
|
statistics?.peers()?.forEach {
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier.fillMaxWidth()
|
||||||
.fillMaxWidth()
|
.padding(end = 10.dp, bottom = 10.dp, start = 10.dp),
|
||||||
.padding(end = 10.dp, bottom = 10.dp, start = 10.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
) {
|
) {
|
||||||
val handshakeEpoch = statistics.peer(it)!!.latestHandshakeEpochMillis
|
val handshakeEpoch = statistics.peer(it)!!.latestHandshakeEpochMillis
|
||||||
val peerTx = statistics.peer(it)!!.txBytes
|
val peerTx = statistics.peer(it)!!.txBytes
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ fun SearchBar(onQuery: (queryString: String) -> Unit) {
|
|||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Rounded.Search,
|
imageVector = Icons.Rounded.Search,
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
tint = MaterialTheme.colorScheme.onBackground,
|
||||||
contentDescription = stringResource(id = R.string.search_icon)
|
contentDescription = stringResource(id = R.string.search_icon),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
@@ -56,25 +56,24 @@ fun SearchBar(onQuery: (queryString: String) -> Unit) {
|
|||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Rounded.Clear,
|
imageVector = Icons.Rounded.Clear,
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
tint = MaterialTheme.colorScheme.onBackground,
|
||||||
contentDescription = stringResource(id = R.string.clear_icon)
|
contentDescription = stringResource(id = R.string.clear_icon),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
colors =
|
colors =
|
||||||
TextFieldDefaults.colors(
|
TextFieldDefaults.colors(
|
||||||
focusedContainerColor = Color.Transparent,
|
focusedContainerColor = Color.Transparent,
|
||||||
unfocusedContainerColor = Color.Transparent,
|
unfocusedContainerColor = Color.Transparent,
|
||||||
disabledContainerColor = Color.Transparent
|
disabledContainerColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
placeholder = { Text(text = stringResource(R.string.hint_search_packages)) },
|
placeholder = { Text(text = stringResource(R.string.hint_search_packages)) },
|
||||||
textStyle = MaterialTheme.typography.bodySmall,
|
textStyle = MaterialTheme.typography.bodySmall,
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier.fillMaxWidth()
|
||||||
.fillMaxWidth()
|
.background(color = MaterialTheme.colorScheme.background, shape = RectangleShape),
|
||||||
.background(color = MaterialTheme.colorScheme.background, shape = RectangleShape)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-11
@@ -22,19 +22,15 @@ fun ConfigurationTextBox(
|
|||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
value = value,
|
value = value,
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
onValueChange = {
|
onValueChange = { onValueChange(it) },
|
||||||
onValueChange(it)
|
|
||||||
},
|
|
||||||
label = { Text(label) },
|
label = { Text(label) },
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
placeholder = {
|
placeholder = { Text(hint) },
|
||||||
Text(hint)
|
|
||||||
},
|
|
||||||
keyboardOptions =
|
keyboardOptions =
|
||||||
KeyboardOptions(
|
KeyboardOptions(
|
||||||
capitalization = KeyboardCapitalization.None,
|
capitalization = KeyboardCapitalization.None,
|
||||||
imeAction = ImeAction.Done
|
imeAction = ImeAction.Done,
|
||||||
),
|
),
|
||||||
keyboardActions = keyboardActions
|
keyboardActions = keyboardActions,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-8
@@ -21,21 +21,16 @@ fun ConfigurationToggle(
|
|||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier = Modifier.fillMaxWidth().padding(padding),
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(padding),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
) {
|
) {
|
||||||
Text(label)
|
Text(label)
|
||||||
Switch(
|
Switch(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
checked = checked,
|
checked = checked,
|
||||||
onCheckedChange = {
|
onCheckedChange = { onCheckChanged() },
|
||||||
onCheckChanged()
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-8
@@ -11,14 +11,11 @@ import androidx.navigation.NavController
|
|||||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BottomNavBar(
|
fun BottomNavBar(navController: NavController, bottomNavItems: List<BottomNavItem>) {
|
||||||
navController: NavController,
|
|
||||||
bottomNavItems: List<BottomNavItem>
|
|
||||||
) {
|
|
||||||
val backStackEntry = navController.currentBackStackEntryAsState()
|
val backStackEntry = navController.currentBackStackEntryAsState()
|
||||||
|
|
||||||
NavigationBar(
|
NavigationBar(
|
||||||
containerColor = MaterialTheme.colorScheme.background
|
containerColor = MaterialTheme.colorScheme.background,
|
||||||
) {
|
) {
|
||||||
bottomNavItems.forEach { item ->
|
bottomNavItems.forEach { item ->
|
||||||
val selected = item.route == backStackEntry.value?.destination?.route
|
val selected = item.route == backStackEntry.value?.destination?.route
|
||||||
@@ -29,15 +26,15 @@ fun BottomNavBar(
|
|||||||
label = {
|
label = {
|
||||||
Text(
|
Text(
|
||||||
text = item.name,
|
text = item.name,
|
||||||
fontWeight = FontWeight.SemiBold
|
fontWeight = FontWeight.SemiBold,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
icon = {
|
icon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = item.icon,
|
imageVector = item.icon,
|
||||||
contentDescription = "${item.name} Icon"
|
contentDescription = "${item.name} Icon",
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-45
@@ -11,51 +11,40 @@ import androidx.core.content.ContextCompat
|
|||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AuthorizationPrompt(
|
fun AuthorizationPrompt(onSuccess: () -> Unit, onFailure: () -> Unit, onError: (String) -> Unit) {
|
||||||
onSuccess: () -> Unit,
|
|
||||||
onFailure: () -> Unit,
|
|
||||||
onError: (String) -> Unit
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val biometricManager = BiometricManager.from(context)
|
val biometricManager = BiometricManager.from(context)
|
||||||
val bio = biometricManager.canAuthenticate(BIOMETRIC_WEAK or DEVICE_CREDENTIAL)
|
val bio = biometricManager.canAuthenticate(BIOMETRIC_WEAK or DEVICE_CREDENTIAL)
|
||||||
val isBiometricAvailable =
|
val isBiometricAvailable = remember {
|
||||||
remember {
|
when (bio) {
|
||||||
when (bio) {
|
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> {
|
||||||
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> {
|
onError("Biometrics not available")
|
||||||
onError("Biometrics not available")
|
false
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
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) {
|
if (isBiometricAvailable) {
|
||||||
val executor = remember { ContextCompat.getMainExecutor(context) }
|
val executor = remember { ContextCompat.getMainExecutor(context) }
|
||||||
|
|
||||||
@@ -71,10 +60,7 @@ fun AuthorizationPrompt(
|
|||||||
context as FragmentActivity,
|
context as FragmentActivity,
|
||||||
executor,
|
executor,
|
||||||
object : BiometricPrompt.AuthenticationCallback() {
|
object : BiometricPrompt.AuthenticationCallback() {
|
||||||
override fun onAuthenticationError(
|
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||||
errorCode: Int,
|
|
||||||
errString: CharSequence
|
|
||||||
) {
|
|
||||||
super.onAuthenticationError(errorCode, errString)
|
super.onAuthenticationError(errorCode, errString)
|
||||||
onFailure()
|
onFailure()
|
||||||
}
|
}
|
||||||
@@ -90,7 +76,7 @@ fun AuthorizationPrompt(
|
|||||||
super.onAuthenticationFailed()
|
super.onAuthenticationFailed()
|
||||||
onFailure()
|
onFailure()
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
biometricPrompt.authenticate(promptInfo)
|
biometricPrompt.authenticate(promptInfo)
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-10
@@ -19,7 +19,6 @@ import androidx.compose.runtime.CompositionLocalProvider
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.LayoutDirection
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
@@ -33,29 +32,28 @@ fun CustomSnackBar(
|
|||||||
isRtl: Boolean = true,
|
isRtl: Boolean = true,
|
||||||
containerColor: Color = MaterialTheme.colorScheme.surface
|
containerColor: Color = MaterialTheme.colorScheme.surface
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
|
||||||
Snackbar(
|
Snackbar(
|
||||||
containerColor = containerColor,
|
containerColor = containerColor,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.fillMaxWidth(
|
Modifier.fillMaxWidth(
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) 1 / 3f else 2 / 3f
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) 1 / 3f else 2 / 3f,
|
||||||
).padding(bottom = 100.dp),
|
)
|
||||||
shape = RoundedCornerShape(16.dp)
|
.padding(bottom = 100.dp),
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
) {
|
) {
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalLayoutDirection provides
|
LocalLayoutDirection provides if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr,
|
||||||
if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr
|
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.width(IntrinsicSize.Max).height(IntrinsicSize.Min),
|
modifier = Modifier.width(IntrinsicSize.Max).height(IntrinsicSize.Min),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.Start
|
horizontalArrangement = Arrangement.Start,
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Rounded.Info,
|
Icons.Rounded.Info,
|
||||||
contentDescription = stringResource(R.string.info),
|
contentDescription = stringResource(R.string.info),
|
||||||
tint = Color.White,
|
tint = Color.White,
|
||||||
modifier = Modifier.padding(end = 10.dp)
|
modifier = Modifier.padding(end = 10.dp),
|
||||||
)
|
)
|
||||||
Text(message, color = Color.White, modifier = Modifier.padding(end = 5.dp))
|
Text(message, color = Color.White, modifier = Modifier.padding(end = 5.dp))
|
||||||
}
|
}
|
||||||
|
|||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.common.screen
|
||||||
|
|
||||||
|
import androidx.compose.foundation.focusable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LoadingScreen() {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Top,
|
||||||
|
modifier = Modifier.fillMaxSize().focusable().padding(),
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(120.dp)) { CircularProgressIndicator() }
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-5
@@ -12,14 +12,11 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SectionTitle(
|
fun SectionTitle(title: String, padding: Dp) {
|
||||||
title: String,
|
|
||||||
padding: Dp
|
|
||||||
) {
|
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
style = TextStyle(fontSize = 18.sp, fontWeight = FontWeight.ExtraBold),
|
style = TextStyle(fontSize = 18.sp, fontWeight = FontWeight.ExtraBold),
|
||||||
modifier = Modifier.padding(padding, bottom = 5.dp, top = 5.dp)
|
modifier = Modifier.padding(padding, bottom = 5.dp, top = 5.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,13 +17,13 @@ data class InterfaceProxy(
|
|||||||
privateKey = i.keyPair.privateKey.toBase64().trim(),
|
privateKey = i.keyPair.privateKey.toBase64().trim(),
|
||||||
addresses = i.addresses.joinToString(", ").trim(),
|
addresses = i.addresses.joinToString(", ").trim(),
|
||||||
dnsServers = i.dnsServers.joinToString(", ").replace("/", "").trim(),
|
dnsServers = i.dnsServers.joinToString(", ").replace("/", "").trim(),
|
||||||
listenPort = if (i.listenPort.isPresent) {
|
listenPort =
|
||||||
i.listenPort.get().toString()
|
if (i.listenPort.isPresent) {
|
||||||
.trim()
|
i.listenPort.get().toString().trim()
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
},
|
},
|
||||||
mtu = if (i.mtu.isPresent) i.mtu.get().toString().trim() else ""
|
mtu = if (i.mtu.isPresent) i.mtu.get().toString().trim() else "",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,36 +13,60 @@ data class PeerProxy(
|
|||||||
fun from(peer: Peer): PeerProxy {
|
fun from(peer: Peer): PeerProxy {
|
||||||
return PeerProxy(
|
return PeerProxy(
|
||||||
publicKey = peer.publicKey.toBase64(),
|
publicKey = peer.publicKey.toBase64(),
|
||||||
preSharedKey = if (peer.preSharedKey.isPresent) {
|
preSharedKey =
|
||||||
peer.preSharedKey.get().toBase64()
|
if (peer.preSharedKey.isPresent) {
|
||||||
.trim()
|
peer.preSharedKey.get().toBase64().trim()
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
},
|
},
|
||||||
persistentKeepalive = if (peer.persistentKeepalive.isPresent) {
|
persistentKeepalive =
|
||||||
peer.persistentKeepalive.get()
|
if (peer.persistentKeepalive.isPresent) {
|
||||||
.toString().trim()
|
peer.persistentKeepalive.get().toString().trim()
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
},
|
},
|
||||||
endpoint = if (peer.endpoint.isPresent) {
|
endpoint =
|
||||||
peer.endpoint.get().toString()
|
if (peer.endpoint.isPresent) {
|
||||||
.trim()
|
peer.endpoint.get().toString().trim()
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
},
|
},
|
||||||
allowedIps = peer.allowedIps.joinToString(", ").trim()
|
allowedIps = peer.allowedIps.joinToString(", ").trim(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val IPV4_PUBLIC_NETWORKS =
|
val IPV4_PUBLIC_NETWORKS =
|
||||||
setOf(
|
setOf(
|
||||||
"0.0.0.0/5", "8.0.0.0/7", "11.0.0.0/8", "12.0.0.0/6", "16.0.0.0/4", "32.0.0.0/3",
|
"0.0.0.0/5",
|
||||||
"64.0.0.0/2", "128.0.0.0/3", "160.0.0.0/5", "168.0.0.0/6", "172.0.0.0/12",
|
"8.0.0.0/7",
|
||||||
"172.32.0.0/11", "172.64.0.0/10", "172.128.0.0/9", "173.0.0.0/8", "174.0.0.0/7",
|
"11.0.0.0/8",
|
||||||
"176.0.0.0/4", "192.0.0.0/9", "192.128.0.0/11", "192.160.0.0/13", "192.169.0.0/16",
|
"12.0.0.0/6",
|
||||||
"192.170.0.0/15", "192.172.0.0/14", "192.176.0.0/12", "192.192.0.0/10",
|
"16.0.0.0/4",
|
||||||
"193.0.0.0/8", "194.0.0.0/7", "196.0.0.0/6", "200.0.0.0/5", "208.0.0.0/4"
|
"32.0.0.0/3",
|
||||||
|
"64.0.0.0/2",
|
||||||
|
"128.0.0.0/3",
|
||||||
|
"160.0.0.0/5",
|
||||||
|
"168.0.0.0/6",
|
||||||
|
"172.0.0.0/12",
|
||||||
|
"172.32.0.0/11",
|
||||||
|
"172.64.0.0/10",
|
||||||
|
"172.128.0.0/9",
|
||||||
|
"173.0.0.0/8",
|
||||||
|
"174.0.0.0/7",
|
||||||
|
"176.0.0.0/4",
|
||||||
|
"192.0.0.0/9",
|
||||||
|
"192.128.0.0/11",
|
||||||
|
"192.160.0.0/13",
|
||||||
|
"192.169.0.0/16",
|
||||||
|
"192.170.0.0/15",
|
||||||
|
"192.172.0.0/14",
|
||||||
|
"192.176.0.0/12",
|
||||||
|
"192.192.0.0/10",
|
||||||
|
"193.0.0.0/8",
|
||||||
|
"194.0.0.0/7",
|
||||||
|
"196.0.0.0/6",
|
||||||
|
"200.0.0.0/5",
|
||||||
|
"208.0.0.0/4",
|
||||||
)
|
)
|
||||||
val IPV4_WILDCARD = setOf("0.0.0.0/0")
|
val IPV4_WILDCARD = setOf("0.0.0.0/0")
|
||||||
}
|
}
|
||||||
|
|||||||
+365
-469
File diff suppressed because it is too large
Load Diff
+18
@@ -0,0 +1,18 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.config
|
||||||
|
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.models.InterfaceProxy
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.models.PeerProxy
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Packages
|
||||||
|
|
||||||
|
data class ConfigUiState(
|
||||||
|
val proxyPeers: List<PeerProxy> = arrayListOf(PeerProxy()),
|
||||||
|
val interfaceProxy: InterfaceProxy = InterfaceProxy(),
|
||||||
|
val packages: Packages = emptyList(),
|
||||||
|
val checkedPackageNames: List<String> = emptyList(),
|
||||||
|
val include: Boolean = true,
|
||||||
|
val isAllApplicationsEnabled: Boolean = false,
|
||||||
|
val loading: Boolean = true,
|
||||||
|
val tunnel: TunnelConfig? = null,
|
||||||
|
val tunnelName: String = ""
|
||||||
|
)
|
||||||
+190
-275
@@ -5,8 +5,6 @@ import android.app.Application
|
|||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
|
||||||
import androidx.compose.runtime.toMutableStateList
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.wireguard.config.Config
|
import com.wireguard.config.Config
|
||||||
@@ -14,210 +12,112 @@ import com.wireguard.config.Interface
|
|||||||
import com.wireguard.config.Peer
|
import com.wireguard.config.Peer
|
||||||
import com.wireguard.crypto.Key
|
import com.wireguard.crypto.Key
|
||||||
import com.wireguard.crypto.KeyPair
|
import com.wireguard.crypto.KeyPair
|
||||||
import com.zaneschepke.wireguardautotunnel.Constants
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.models.InterfaceProxy
|
import com.zaneschepke.wireguardautotunnel.ui.models.InterfaceProxy
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.models.PeerProxy
|
import com.zaneschepke.wireguardautotunnel.ui.models.PeerProxy
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Event
|
||||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||||
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
|
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.removeAt
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.update
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class ConfigViewModel
|
class ConfigViewModel
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
private val application: Application,
|
private val application: Application,
|
||||||
private val tunnelRepo: TunnelConfigDao,
|
private val tunnelConfigRepository: TunnelConfigRepository,
|
||||||
private val settingsRepo: SettingsDoa
|
private val settingsRepository: SettingsRepository,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val _tunnel = MutableStateFlow<TunnelConfig?>(null)
|
|
||||||
private val _tunnelName = MutableStateFlow("")
|
|
||||||
val tunnelName get() = _tunnelName.asStateFlow()
|
|
||||||
val tunnel get() = _tunnel.asStateFlow()
|
|
||||||
|
|
||||||
private var _proxyPeers = MutableStateFlow(mutableStateListOf<PeerProxy>())
|
|
||||||
val proxyPeers get() = _proxyPeers.asStateFlow()
|
|
||||||
|
|
||||||
private var _interface = MutableStateFlow(InterfaceProxy())
|
|
||||||
val interfaceProxy = _interface.asStateFlow()
|
|
||||||
|
|
||||||
private val _packages = MutableStateFlow(emptyList<PackageInfo>())
|
|
||||||
val packages get() = _packages.asStateFlow()
|
|
||||||
private val packageManager = application.packageManager
|
private val packageManager = application.packageManager
|
||||||
|
|
||||||
private val _checkedPackages = MutableStateFlow(mutableStateListOf<String>())
|
private val _uiState = MutableStateFlow(ConfigUiState())
|
||||||
val checkedPackages get() = _checkedPackages.asStateFlow()
|
val uiState = _uiState.asStateFlow()
|
||||||
private val _include = MutableStateFlow(true)
|
|
||||||
val include get() = _include.asStateFlow()
|
|
||||||
|
|
||||||
private val _isAllApplicationsEnabled = MutableStateFlow(false)
|
fun init(tunnelId: String) =
|
||||||
val isAllApplicationsEnabled get() = _isAllApplicationsEnabled.asStateFlow()
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
private val _isDefaultTunnel = MutableStateFlow(false)
|
val packages = getQueriedPackages("")
|
||||||
|
val state =
|
||||||
private lateinit var tunnelConfig: TunnelConfig
|
if (tunnelId != Constants.MANUAL_TUNNEL_CONFIG_ID) {
|
||||||
|
val tunnelConfig =
|
||||||
suspend fun onScreenLoad(id: String) {
|
tunnelConfigRepository.getAll().firstOrNull { it.id.toString() == tunnelId }
|
||||||
if (id != Constants.MANUAL_TUNNEL_CONFIG_ID) {
|
if (tunnelConfig != null) {
|
||||||
tunnelConfig = getTunnelConfigById(id) ?: throw WgTunnelException(
|
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
||||||
"Config not found"
|
val proxyPeers = config.peers.map { PeerProxy.from(it) }
|
||||||
)
|
val proxyInterface = InterfaceProxy.from(config.`interface`)
|
||||||
emitScreenData()
|
var include = true
|
||||||
} else {
|
var isAllApplicationsEnabled = false
|
||||||
emitEmptyScreenData()
|
val checkedPackages =
|
||||||
|
if (config.`interface`.includedApplications.isNotEmpty()) {
|
||||||
|
config.`interface`.includedApplications
|
||||||
|
} else if (config.`interface`.excludedApplications.isNotEmpty()) {
|
||||||
|
include = false
|
||||||
|
config.`interface`.excludedApplications
|
||||||
|
} else {
|
||||||
|
isAllApplicationsEnabled = true
|
||||||
|
emptySet()
|
||||||
|
}
|
||||||
|
ConfigUiState(
|
||||||
|
proxyPeers,
|
||||||
|
proxyInterface,
|
||||||
|
packages,
|
||||||
|
checkedPackages.toList(),
|
||||||
|
include,
|
||||||
|
isAllApplicationsEnabled,
|
||||||
|
false,
|
||||||
|
tunnelConfig,
|
||||||
|
tunnelConfig.name,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ConfigUiState(loading = false, packages = packages)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ConfigUiState(loading = false, packages = packages)
|
||||||
|
}
|
||||||
|
_uiState.value = state
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun emitEmptyScreenData() {
|
|
||||||
tunnelConfig = TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = "")
|
|
||||||
viewModelScope.launch {
|
|
||||||
emitTunnelConfig()
|
|
||||||
emitPeerProxy(PeerProxy())
|
|
||||||
emitInterfaceProxy(InterfaceProxy())
|
|
||||||
emitTunnelConfigName()
|
|
||||||
emitDefaultTunnelStatus()
|
|
||||||
emitQueriedPackages("")
|
|
||||||
emitTunnelAllApplicationsEnabled()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun emitScreenData() {
|
|
||||||
emitTunnelConfig()
|
|
||||||
emitPeersFromConfig()
|
|
||||||
emitInterfaceFromConfig()
|
|
||||||
emitTunnelConfigName()
|
|
||||||
emitDefaultTunnelStatus()
|
|
||||||
emitQueriedPackages("")
|
|
||||||
emitCurrentPackageConfigurations()
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun emitDefaultTunnelStatus() {
|
|
||||||
val settings = settingsRepo.getAll()
|
|
||||||
if (settings.isNotEmpty()) {
|
|
||||||
_isDefaultTunnel.value = settings.first().isTunnelConfigDefault(tunnelConfig)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun emitInterfaceFromConfig() {
|
|
||||||
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
|
||||||
_interface.value = InterfaceProxy.from(config.`interface`)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun emitPeersFromConfig() {
|
|
||||||
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
|
||||||
config.peers.forEach {
|
|
||||||
_proxyPeers.value.add(PeerProxy.from(it))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun emitPeerProxy(peerProxy: PeerProxy) {
|
|
||||||
_proxyPeers.value.add(peerProxy)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun emitInterfaceProxy(interfaceProxy: InterfaceProxy) {
|
|
||||||
_interface.value = interfaceProxy
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun getTunnelConfigById(id: String): TunnelConfig? {
|
|
||||||
return try {
|
|
||||||
tunnelRepo.getById(id.toLong())
|
|
||||||
} catch (_: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun emitTunnelConfig() {
|
|
||||||
_tunnel.emit(tunnelConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun emitTunnelConfigName() {
|
|
||||||
_tunnelName.emit(tunnelConfig.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onTunnelNameChange(name: String) {
|
fun onTunnelNameChange(name: String) {
|
||||||
_tunnelName.value = name
|
_uiState.value = _uiState.value.copy(tunnelName = name)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onIncludeChange(include: Boolean) {
|
fun onIncludeChange(include: Boolean) {
|
||||||
_include.value = include
|
_uiState.value = _uiState.value.copy(include = include)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onAddCheckedPackage(packageName: String) {
|
fun onAddCheckedPackage(packageName: String) {
|
||||||
_checkedPackages.value.add(packageName)
|
_uiState.value =
|
||||||
|
_uiState.value.copy(
|
||||||
|
checkedPackageNames = _uiState.value.checkedPackageNames + packageName
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onAllApplicationsChange(isAllApplicationsEnabled: Boolean) {
|
fun onAllApplicationsChange(isAllApplicationsEnabled: Boolean) {
|
||||||
_isAllApplicationsEnabled.value = isAllApplicationsEnabled
|
_uiState.value = _uiState.value.copy(isAllApplicationsEnabled = isAllApplicationsEnabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onRemoveCheckedPackage(packageName: String) {
|
fun onRemoveCheckedPackage(packageName: String) {
|
||||||
_checkedPackages.value.remove(packageName)
|
_uiState.value =
|
||||||
|
_uiState.value.copy(
|
||||||
|
checkedPackageNames = _uiState.value.checkedPackageNames - packageName
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun emitSplitTunnelConfiguration(config: Config) {
|
private fun getQueriedPackages(query: String): List<PackageInfo> {
|
||||||
val excludedApps = config.`interface`.excludedApplications
|
return getAllInternetCapablePackages().filter {
|
||||||
val includedApps = config.`interface`.includedApplications
|
getPackageLabel(it).lowercase().contains(query.lowercase())
|
||||||
if (excludedApps.isNotEmpty() || includedApps.isNotEmpty()) {
|
|
||||||
emitTunnelAllApplicationsDisabled()
|
|
||||||
determineAppInclusionState(excludedApps, includedApps)
|
|
||||||
} else {
|
|
||||||
emitTunnelAllApplicationsEnabled()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun determineAppInclusionState(
|
|
||||||
excludedApps: Set<String>,
|
|
||||||
includedApps: Set<String>
|
|
||||||
) {
|
|
||||||
if (excludedApps.isEmpty()) {
|
|
||||||
emitIncludedAppsExist()
|
|
||||||
emitCheckedApps(includedApps)
|
|
||||||
} else {
|
|
||||||
emitExcludedAppsExist()
|
|
||||||
emitCheckedApps(excludedApps)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun emitIncludedAppsExist() {
|
|
||||||
_include.emit(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun emitExcludedAppsExist() {
|
|
||||||
_include.emit(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun emitCheckedApps(apps: Set<String>) {
|
|
||||||
_checkedPackages.emit(apps.toMutableStateList())
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun emitTunnelAllApplicationsEnabled() {
|
|
||||||
_isAllApplicationsEnabled.emit(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun emitTunnelAllApplicationsDisabled() {
|
|
||||||
_isAllApplicationsEnabled.emit(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun emitCurrentPackageConfigurations() {
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
|
||||||
emitSplitTunnelConfiguration(config)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun emitQueriedPackages(query: String) {
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
val packages =
|
|
||||||
getAllInternetCapablePackages().filter {
|
|
||||||
getPackageLabel(it).lowercase().contains(query.lowercase())
|
|
||||||
}
|
|
||||||
_packages.emit(packages)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,7 +133,7 @@ constructor(
|
|||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
packageManager.getPackagesHoldingPermissions(
|
packageManager.getPackagesHoldingPermissions(
|
||||||
permissions,
|
permissions,
|
||||||
PackageManager.PackageInfoFlags.of(0L)
|
PackageManager.PackageInfoFlags.of(0L),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
packageManager.getPackagesHoldingPermissions(permissions, 0)
|
packageManager.getPackagesHoldingPermissions(permissions, 0)
|
||||||
@@ -241,193 +141,200 @@ constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun isAllApplicationsEnabled(): Boolean {
|
private fun isAllApplicationsEnabled(): Boolean {
|
||||||
return _isAllApplicationsEnabled.value
|
return _uiState.value.isAllApplicationsEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun saveConfig(tunnelConfig: TunnelConfig) {
|
private fun saveConfig(tunnelConfig: TunnelConfig) =
|
||||||
tunnelRepo.save(tunnelConfig)
|
viewModelScope.launch { tunnelConfigRepository.save(tunnelConfig) }
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun updateTunnelConfig(tunnelConfig: TunnelConfig?) {
|
private fun updateTunnelConfig(tunnelConfig: TunnelConfig?) =
|
||||||
if (tunnelConfig != null) {
|
viewModelScope.launch {
|
||||||
saveConfig(tunnelConfig)
|
if (tunnelConfig != null) {
|
||||||
updateSettingsDefaultTunnel(tunnelConfig)
|
saveConfig(tunnelConfig).join()
|
||||||
|
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
||||||
|
updateSettingsDefaultTunnel(tunnelConfig)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun updateSettingsDefaultTunnel(tunnelConfig: TunnelConfig) {
|
private suspend fun updateSettingsDefaultTunnel(tunnelConfig: TunnelConfig) {
|
||||||
val settings = settingsRepo.getAll()
|
val settings = settingsRepository.getSettingsFlow().first()
|
||||||
if (settings.isNotEmpty()) {
|
if (settings.defaultTunnel != null) {
|
||||||
val setting = settings[0]
|
if (tunnelConfig.id == TunnelConfig.from(settings.defaultTunnel!!).id) {
|
||||||
if (setting.defaultTunnel != null) {
|
settingsRepository.save(settings.copy(defaultTunnel = tunnelConfig.toString()))
|
||||||
if (tunnelConfig.id == TunnelConfig.from(setting.defaultTunnel!!).id) {
|
|
||||||
settingsRepo.save(
|
|
||||||
setting.copy(
|
|
||||||
defaultTunnel = tunnelConfig.toString()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildPeerListFromProxyPeers(): List<Peer> {
|
private fun buildPeerListFromProxyPeers(): List<Peer> {
|
||||||
return _proxyPeers.value.map {
|
return _uiState.value.proxyPeers.map {
|
||||||
val builder = Peer.Builder()
|
val builder = Peer.Builder()
|
||||||
if (it.allowedIps.isNotEmpty()) builder.parseAllowedIPs(it.allowedIps.trim())
|
if (it.allowedIps.isNotEmpty()) builder.parseAllowedIPs(it.allowedIps.trim())
|
||||||
if (it.publicKey.isNotEmpty()) builder.parsePublicKey(it.publicKey.trim())
|
if (it.publicKey.isNotEmpty()) builder.parsePublicKey(it.publicKey.trim())
|
||||||
if (it.preSharedKey.isNotEmpty()) builder.parsePreSharedKey(it.preSharedKey.trim())
|
if (it.preSharedKey.isNotEmpty()) builder.parsePreSharedKey(it.preSharedKey.trim())
|
||||||
if (it.endpoint.isNotEmpty()) builder.parseEndpoint(it.endpoint.trim())
|
if (it.endpoint.isNotEmpty()) builder.parseEndpoint(it.endpoint.trim())
|
||||||
if (it.persistentKeepalive.isNotEmpty()) {
|
if (it.persistentKeepalive.isNotEmpty()) {
|
||||||
builder.parsePersistentKeepalive(
|
builder.parsePersistentKeepalive(it.persistentKeepalive.trim())
|
||||||
it.persistentKeepalive.trim()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
builder.build()
|
builder.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun emptyCheckedPackagesList() {
|
||||||
|
_uiState.value = _uiState.value.copy(checkedPackageNames = emptyList())
|
||||||
|
}
|
||||||
|
|
||||||
private fun buildInterfaceListFromProxyInterface(): Interface {
|
private fun buildInterfaceListFromProxyInterface(): Interface {
|
||||||
val builder = Interface.Builder()
|
val builder = Interface.Builder()
|
||||||
builder.parsePrivateKey(_interface.value.privateKey.trim())
|
builder.parsePrivateKey(_uiState.value.interfaceProxy.privateKey.trim())
|
||||||
builder.parseAddresses(_interface.value.addresses.trim())
|
builder.parseAddresses(_uiState.value.interfaceProxy.addresses.trim())
|
||||||
builder.parseDnsServers(_interface.value.dnsServers.trim())
|
if (_uiState.value.interfaceProxy.dnsServers.isNotEmpty()) {
|
||||||
if (_interface.value.mtu.isNotEmpty()) builder.parseMtu(_interface.value.mtu.trim())
|
builder.parseDnsServers(_uiState.value.interfaceProxy.dnsServers.trim())
|
||||||
if (_interface.value.listenPort.isNotEmpty()) {
|
|
||||||
builder.parseListenPort(
|
|
||||||
_interface.value.listenPort.trim()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
if (isAllApplicationsEnabled()) _checkedPackages.value.clear()
|
if (_uiState.value.interfaceProxy.mtu.isNotEmpty())
|
||||||
if (_include.value) builder.includeApplications(_checkedPackages.value)
|
builder.parseMtu(_uiState.value.interfaceProxy.mtu.trim())
|
||||||
if (!_include.value) builder.excludeApplications(_checkedPackages.value)
|
if (_uiState.value.interfaceProxy.listenPort.isNotEmpty()) {
|
||||||
|
builder.parseListenPort(_uiState.value.interfaceProxy.listenPort.trim())
|
||||||
|
}
|
||||||
|
if (isAllApplicationsEnabled()) emptyCheckedPackagesList()
|
||||||
|
if (_uiState.value.include) builder.includeApplications(_uiState.value.checkedPackageNames)
|
||||||
|
if (!_uiState.value.include) builder.excludeApplications(_uiState.value.checkedPackageNames)
|
||||||
return builder.build()
|
return builder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun onSaveAllChanges() {
|
fun onSaveAllChanges(): Result<Event> {
|
||||||
try {
|
return try {
|
||||||
val peerList = buildPeerListFromProxyPeers()
|
val peerList = buildPeerListFromProxyPeers()
|
||||||
val wgInterface = buildInterfaceListFromProxyInterface()
|
val wgInterface = buildInterfaceListFromProxyInterface()
|
||||||
val config = Config.Builder().addPeers(peerList).setInterface(wgInterface).build()
|
val config = Config.Builder().addPeers(peerList).setInterface(wgInterface).build()
|
||||||
val tunnelConfig =
|
val tunnelConfig = when(uiState.value.tunnel) {
|
||||||
_tunnel.value?.copy(
|
null -> TunnelConfig(name = _uiState.value.tunnelName, wgQuick = config.toWgQuickString())
|
||||||
name = _tunnelName.value,
|
else -> uiState.value.tunnel!!.copy(
|
||||||
wgQuick = config.toWgQuickString()
|
name = _uiState.value.tunnelName,
|
||||||
|
wgQuick = config.toWgQuickString(),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
updateTunnelConfig(tunnelConfig)
|
updateTunnelConfig(tunnelConfig)
|
||||||
|
Result.Success(Event.Message.ConfigSaved)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw WgTunnelException(
|
Result.Error(Event.Error.Exception(e))
|
||||||
"Error: ${e.cause?.message?.lowercase() ?: "unknown error occurred"}"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onPeerPublicKeyChange(
|
fun onPeerPublicKeyChange(index: Int, value: String) {
|
||||||
index: Int,
|
_uiState.value =
|
||||||
publicKey: String
|
_uiState.value.copy(
|
||||||
) {
|
proxyPeers =
|
||||||
_proxyPeers.value[index] =
|
_uiState.value.proxyPeers.update(
|
||||||
_proxyPeers.value[index].copy(
|
index,
|
||||||
publicKey = publicKey
|
_uiState.value.proxyPeers[index].copy(publicKey = value),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onPreSharedKeyChange(
|
fun onPreSharedKeyChange(index: Int, value: String) {
|
||||||
index: Int,
|
_uiState.value =
|
||||||
value: String
|
_uiState.value.copy(
|
||||||
) {
|
proxyPeers =
|
||||||
_proxyPeers.value[index] =
|
_uiState.value.proxyPeers.update(
|
||||||
_proxyPeers.value[index].copy(
|
index,
|
||||||
preSharedKey = value
|
_uiState.value.proxyPeers[index].copy(preSharedKey = value),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onEndpointChange(
|
fun onEndpointChange(index: Int, value: String) {
|
||||||
index: Int,
|
_uiState.value =
|
||||||
value: String
|
_uiState.value.copy(
|
||||||
) {
|
proxyPeers =
|
||||||
_proxyPeers.value[index] =
|
_uiState.value.proxyPeers.update(
|
||||||
_proxyPeers.value[index].copy(
|
index,
|
||||||
endpoint = value
|
_uiState.value.proxyPeers[index].copy(endpoint = value),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onAllowedIpsChange(
|
fun onAllowedIpsChange(index: Int, value: String) {
|
||||||
index: Int,
|
_uiState.value =
|
||||||
value: String
|
_uiState.value.copy(
|
||||||
) {
|
proxyPeers =
|
||||||
_proxyPeers.value[index] =
|
_uiState.value.proxyPeers.update(
|
||||||
_proxyPeers.value[index].copy(
|
index,
|
||||||
allowedIps = value
|
_uiState.value.proxyPeers[index].copy(allowedIps = value),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onPersistentKeepaliveChanged(
|
fun onPersistentKeepaliveChanged(index: Int, value: String) {
|
||||||
index: Int,
|
_uiState.value =
|
||||||
value: String
|
_uiState.value.copy(
|
||||||
) {
|
proxyPeers =
|
||||||
_proxyPeers.value[index] =
|
_uiState.value.proxyPeers.update(
|
||||||
_proxyPeers.value[index].copy(
|
index,
|
||||||
persistentKeepalive = value
|
_uiState.value.proxyPeers[index].copy(persistentKeepalive = value),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onDeletePeer(index: Int) {
|
fun onDeletePeer(index: Int) {
|
||||||
proxyPeers.value.removeAt(index)
|
_uiState.value =
|
||||||
|
_uiState.value.copy(
|
||||||
|
proxyPeers = _uiState.value.proxyPeers.removeAt(index),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addEmptyPeer() {
|
fun addEmptyPeer() {
|
||||||
_proxyPeers.value.add(PeerProxy())
|
_uiState.value = _uiState.value.copy(proxyPeers = _uiState.value.proxyPeers + PeerProxy())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun generateKeyPair() {
|
fun generateKeyPair() {
|
||||||
val keyPair = KeyPair()
|
val keyPair = KeyPair()
|
||||||
_interface.value =
|
_uiState.value =
|
||||||
_interface.value.copy(
|
_uiState.value.copy(
|
||||||
privateKey = keyPair.privateKey.toBase64(),
|
interfaceProxy =
|
||||||
publicKey = keyPair.publicKey.toBase64()
|
_uiState.value.interfaceProxy.copy(
|
||||||
|
privateKey = keyPair.privateKey.toBase64(),
|
||||||
|
publicKey = keyPair.publicKey.toBase64(),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onAddressesChanged(value: String) {
|
fun onAddressesChanged(value: String) {
|
||||||
_interface.value =
|
_uiState.value =
|
||||||
_interface.value.copy(
|
_uiState.value.copy(
|
||||||
addresses = value
|
interfaceProxy = _uiState.value.interfaceProxy.copy(addresses = value)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onListenPortChanged(value: String) {
|
fun onListenPortChanged(value: String) {
|
||||||
_interface.value =
|
_uiState.value =
|
||||||
_interface.value.copy(
|
_uiState.value.copy(
|
||||||
listenPort = value
|
interfaceProxy = _uiState.value.interfaceProxy.copy(listenPort = value)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onDnsServersChanged(value: String) {
|
fun onDnsServersChanged(value: String) {
|
||||||
_interface.value =
|
_uiState.value =
|
||||||
_interface.value.copy(
|
_uiState.value.copy(
|
||||||
dnsServers = value
|
interfaceProxy = _uiState.value.interfaceProxy.copy(dnsServers = value)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onMtuChanged(value: String) {
|
fun onMtuChanged(value: String) {
|
||||||
_interface.value =
|
_uiState.value =
|
||||||
_interface.value.copy(
|
_uiState.value.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(mtu = value))
|
||||||
mtu = value
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onInterfacePublicKeyChange(value: String) {
|
private fun onInterfacePublicKeyChange(value: String) {
|
||||||
_interface.value =
|
_uiState.value =
|
||||||
_interface.value.copy(
|
_uiState.value.copy(
|
||||||
publicKey = value
|
interfaceProxy = _uiState.value.interfaceProxy.copy(publicKey = value)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onPrivateKeyChange(value: String) {
|
fun onPrivateKeyChange(value: String) {
|
||||||
_interface.value =
|
_uiState.value =
|
||||||
_interface.value.copy(
|
_uiState.value.copy(
|
||||||
privateKey = value
|
interfaceProxy = _uiState.value.interfaceProxy.copy(privateKey = value)
|
||||||
)
|
)
|
||||||
if (NumberUtils.isValidKey(value)) {
|
if (NumberUtils.isValidKey(value)) {
|
||||||
val pair = KeyPair(Key.fromBase64(value))
|
val pair = KeyPair(Key.fromBase64(value))
|
||||||
@@ -436,4 +343,12 @@ constructor(
|
|||||||
onInterfacePublicKeyChange("")
|
onInterfacePublicKeyChange("")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun emitQueriedPackages(query: String) {
|
||||||
|
val packages =
|
||||||
|
getAllInternetCapablePackages().filter {
|
||||||
|
getPackageLabel(it).lowercase().contains(query.lowercase())
|
||||||
|
}
|
||||||
|
_uiState.value = _uiState.value.copy(packages = packages)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+416
-302
@@ -1,55 +1,68 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.main
|
package com.zaneschepke.wireguardautotunnel.ui.screens.main
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.animation.slideInVertically
|
import androidx.compose.animation.slideInVertically
|
||||||
import androidx.compose.animation.slideOutVertically
|
import androidx.compose.animation.slideOutVertically
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.focusable
|
import androidx.compose.foundation.focusable
|
||||||
|
import androidx.compose.foundation.gestures.ScrollableDefaults
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.requiredWidth
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.overscroll
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Create
|
import androidx.compose.material.icons.filled.Create
|
||||||
import androidx.compose.material.icons.filled.FileOpen
|
import androidx.compose.material.icons.filled.FileOpen
|
||||||
import androidx.compose.material.icons.filled.QrCode
|
import androidx.compose.material.icons.filled.QrCode
|
||||||
import androidx.compose.material.icons.rounded.Add
|
import androidx.compose.material.icons.rounded.Add
|
||||||
|
import androidx.compose.material.icons.rounded.Bolt
|
||||||
import androidx.compose.material.icons.rounded.Circle
|
import androidx.compose.material.icons.rounded.Circle
|
||||||
import androidx.compose.material.icons.rounded.Delete
|
import androidx.compose.material.icons.rounded.Delete
|
||||||
import androidx.compose.material.icons.rounded.Edit
|
import androidx.compose.material.icons.rounded.Edit
|
||||||
import androidx.compose.material.icons.rounded.Info
|
import androidx.compose.material.icons.rounded.Info
|
||||||
import androidx.compose.material.icons.rounded.Star
|
import androidx.compose.material.icons.rounded.Star
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Divider
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.FabPosition
|
import androidx.compose.material3.FabPosition
|
||||||
import androidx.compose.material3.FloatingActionButton
|
import androidx.compose.material3.FloatingActionButton
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.MaterialTheme.typography
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.material3.surfaceColorAtElevation
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -61,16 +74,14 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
@@ -78,28 +89,32 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import com.journeyapps.barcodescanner.ScanContract
|
import com.journeyapps.barcodescanner.ScanContract
|
||||||
import com.journeyapps.barcodescanner.ScanOptions
|
import com.journeyapps.barcodescanner.ScanOptions
|
||||||
|
import com.wireguard.android.backend.GoBackend
|
||||||
import com.wireguard.android.backend.Tunnel
|
import com.wireguard.android.backend.Tunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.Constants
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.CaptureActivityPortrait
|
import com.zaneschepke.wireguardautotunnel.ui.CaptureActivityPortrait
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.Routes
|
import com.zaneschepke.wireguardautotunnel.ui.Screen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem
|
import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.brickRed
|
import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.corn
|
import com.zaneschepke.wireguardautotunnel.ui.theme.corn
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.mint
|
import com.zaneschepke.wireguardautotunnel.ui.theme.mint
|
||||||
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Event
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.handshakeStatus
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.mapPeerStats
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MainScreen(
|
fun MainScreen(
|
||||||
viewModel: MainViewModel = hiltViewModel(),
|
viewModel: MainViewModel = hiltViewModel(),
|
||||||
padding: PaddingValues,
|
focusRequester: FocusRequester,
|
||||||
showSnackbarMessage: (String) -> Unit,
|
showSnackbarMessage: (String) -> Unit,
|
||||||
navController: NavController
|
navController: NavController
|
||||||
) {
|
) {
|
||||||
@@ -110,45 +125,39 @@ fun MainScreen(
|
|||||||
|
|
||||||
val sheetState = rememberModalBottomSheetState()
|
val sheetState = rememberModalBottomSheetState()
|
||||||
var showBottomSheet by remember { mutableStateOf(false) }
|
var showBottomSheet by remember { mutableStateOf(false) }
|
||||||
var showPrimaryChangeAlertDialog by remember { mutableStateOf(false) }
|
|
||||||
val tunnels by viewModel.tunnels.collectAsStateWithLifecycle(mutableListOf())
|
|
||||||
val handshakeStatus by viewModel.handshakeStatus.collectAsStateWithLifecycle(
|
|
||||||
HandshakeStatus.NOT_STARTED
|
|
||||||
)
|
|
||||||
var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) }
|
|
||||||
val state by viewModel.state.collectAsStateWithLifecycle(Tunnel.State.DOWN)
|
|
||||||
val tunnelName by viewModel.tunnelName.collectAsStateWithLifecycle("")
|
|
||||||
val settings by viewModel.settings.collectAsStateWithLifecycle()
|
|
||||||
val statistics by viewModel.statistics.collectAsStateWithLifecycle(null)
|
|
||||||
|
|
||||||
// Nested scroll for control FAB
|
var showPrimaryChangeAlertDialog by remember { mutableStateOf(false) }
|
||||||
val nestedScrollConnection =
|
var showDeleteTunnelAlertDialog by remember { mutableStateOf(false) }
|
||||||
remember {
|
var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) }
|
||||||
object : NestedScrollConnection {
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
override fun onPreScroll(
|
|
||||||
available: Offset,
|
var vpnIntent by remember { mutableStateOf(GoBackend.VpnService.prepare(WireGuardAutoTunnel.instance)) }
|
||||||
source: NestedScrollSource
|
val vpnActivityResultState =
|
||||||
): Offset {
|
rememberLauncherForActivityResult(
|
||||||
// Hide FAB
|
ActivityResultContracts.StartActivityForResult(),
|
||||||
if (available.y < -1) {
|
onResult = {
|
||||||
isVisible.value = false
|
val accepted = (it.resultCode == AppCompatActivity.RESULT_OK)
|
||||||
}
|
if (accepted) {
|
||||||
// Show FAB
|
vpnIntent = null
|
||||||
if (available.y > 1) {
|
|
||||||
isVisible.value = true
|
|
||||||
}
|
|
||||||
return Offset.Zero
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
)
|
||||||
|
LaunchedEffect(uiState.loading) {
|
||||||
|
if (!uiState.loading && WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
|
delay(Constants.FOCUS_REQUEST_DELAY)
|
||||||
|
focusRequester.requestFocus()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uiState.loading) {
|
||||||
|
LoadingScreen()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val tunnelFileImportResultLauncher =
|
val tunnelFileImportResultLauncher =
|
||||||
rememberLauncherForActivityResult(
|
rememberLauncherForActivityResult(
|
||||||
object : ActivityResultContracts.GetContent() {
|
object : ActivityResultContracts.GetContent() {
|
||||||
override fun createIntent(
|
override fun createIntent(context: Context, input: String): Intent {
|
||||||
context: Context,
|
|
||||||
input: String
|
|
||||||
): Intent {
|
|
||||||
val intent = super.createIntent(context, input)
|
val intent = super.createIntent(context, input)
|
||||||
|
|
||||||
/* AndroidTV now comes with stubs that do nothing but display a Toast less helpful than
|
/* AndroidTV now comes with stubs that do nothing but display a Toast less helpful than
|
||||||
@@ -158,154 +167,223 @@ fun MainScreen(
|
|||||||
context.packageManager.queryIntentActivities(
|
context.packageManager.queryIntentActivities(
|
||||||
intent,
|
intent,
|
||||||
PackageManager.ResolveInfoFlags.of(
|
PackageManager.ResolveInfoFlags.of(
|
||||||
PackageManager.MATCH_DEFAULT_ONLY.toLong()
|
PackageManager.MATCH_DEFAULT_ONLY.toLong(),
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
context.packageManager.queryIntentActivities(
|
context.packageManager.queryIntentActivities(
|
||||||
intent,
|
intent,
|
||||||
PackageManager.MATCH_DEFAULT_ONLY
|
PackageManager.MATCH_DEFAULT_ONLY,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (activitiesToResolveIntent.all {
|
if (
|
||||||
|
activitiesToResolveIntent.all {
|
||||||
val name = it.activityInfo.packageName
|
val name = it.activityInfo.packageName
|
||||||
name.startsWith(Constants.GOOGLE_TV_EXPLORER_STUB) || name.startsWith(
|
name.startsWith(Constants.GOOGLE_TV_EXPLORER_STUB) ||
|
||||||
Constants.ANDROID_TV_EXPLORER_STUB
|
name.startsWith(Constants.ANDROID_TV_EXPLORER_STUB)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
throw WgTunnelException(context.getString(R.string.no_file_explorer))
|
showSnackbarMessage(Event.Error.FileExplorerRequired.message)
|
||||||
}
|
}
|
||||||
return intent
|
return intent
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
) { data ->
|
) { data ->
|
||||||
if (data == null) return@rememberLauncherForActivityResult
|
if (data == null) return@rememberLauncherForActivityResult
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch {
|
||||||
try {
|
viewModel.onTunnelFileSelected(data).let {
|
||||||
viewModel.onTunnelFileSelected(data)
|
when (it) {
|
||||||
} catch (e: WgTunnelException) {
|
is Result.Error -> showSnackbarMessage(it.error.message)
|
||||||
showSnackbarMessage(e.message)
|
is Result.Success -> {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val scanLauncher =
|
val scanLauncher =
|
||||||
rememberLauncherForActivityResult(
|
rememberLauncherForActivityResult(
|
||||||
contract = ScanContract(),
|
contract = ScanContract(),
|
||||||
onResult = {
|
onResult = {
|
||||||
scope.launch {
|
if (it.contents != null) {
|
||||||
try {
|
scope.launch {
|
||||||
viewModel.onTunnelQrResult(it.contents)
|
viewModel.onTunnelQrResult(it.contents).let { result ->
|
||||||
} catch (e: Exception) {
|
when (result) {
|
||||||
when (e) {
|
is Result.Success -> {}
|
||||||
is WgTunnelException -> {
|
is Result.Error -> showSnackbarMessage(result.error.message)
|
||||||
showSnackbarMessage(e.message)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
showSnackbarMessage("No QR code scanned")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
if (showPrimaryChangeAlertDialog) {
|
AnimatedVisibility(showPrimaryChangeAlertDialog) {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = {
|
onDismissRequest = { showPrimaryChangeAlertDialog = false },
|
||||||
showPrimaryChangeAlertDialog = false
|
|
||||||
},
|
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(onClick = {
|
TextButton(
|
||||||
scope.launch {
|
onClick = {
|
||||||
viewModel.onDefaultTunnelChange(selectedTunnel)
|
viewModel.onDefaultTunnelChange(selectedTunnel)
|
||||||
showPrimaryChangeAlertDialog = false
|
showPrimaryChangeAlertDialog = false
|
||||||
selectedTunnel = null
|
selectedTunnel = null
|
||||||
}
|
},
|
||||||
}) { Text(text = stringResource(R.string.okay)) }
|
) {
|
||||||
|
Text(text = stringResource(R.string.okay))
|
||||||
|
}
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
TextButton(onClick = {
|
TextButton(onClick = { showPrimaryChangeAlertDialog = false }) {
|
||||||
showPrimaryChangeAlertDialog = false
|
Text(text = stringResource(R.string.cancel))
|
||||||
}) { Text(text = stringResource(R.string.cancel)) }
|
}
|
||||||
},
|
},
|
||||||
title = { Text(text = stringResource(R.string.primary_tunnel_change)) },
|
title = { Text(text = stringResource(R.string.primary_tunnel_change)) },
|
||||||
text = { Text(text = stringResource(R.string.primary_tunnel_change_question)) }
|
text = { Text(text = stringResource(R.string.primary_tunnel_change_question)) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onTunnelToggle(
|
AnimatedVisibility(showDeleteTunnelAlertDialog) {
|
||||||
checked: Boolean,
|
AlertDialog(
|
||||||
tunnel: TunnelConfig
|
onDismissRequest = { showDeleteTunnelAlertDialog = false },
|
||||||
) {
|
confirmButton = {
|
||||||
try {
|
TextButton(
|
||||||
if (checked) viewModel.onTunnelStart(tunnel) else viewModel.onTunnelStop()
|
onClick = {
|
||||||
} catch (e: Exception) {
|
selectedTunnel?.let { viewModel.onDelete(it) }
|
||||||
showSnackbarMessage(e.message!!)
|
showDeleteTunnelAlertDialog = false
|
||||||
|
selectedTunnel = null
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.yes))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { showDeleteTunnelAlertDialog = false }) {
|
||||||
|
Text(text = stringResource(R.string.cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title = { Text(text = stringResource(R.string.delete_tunnel)) },
|
||||||
|
text = { Text(text = stringResource(R.string.delete_tunnel_message)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) {
|
||||||
|
if (vpnIntent != null) {
|
||||||
|
return vpnActivityResultState.launch(vpnIntent)
|
||||||
}
|
}
|
||||||
|
if (checked) viewModel.onTunnelStart(tunnel) else viewModel.onTunnelStop()
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.pointerInput(Unit) {
|
Modifier.pointerInput(Unit) {
|
||||||
detectTapGestures(onTap = {
|
detectTapGestures(
|
||||||
selectedTunnel = null
|
onTap = {
|
||||||
})
|
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) selectedTunnel = null
|
||||||
},
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
floatingActionButtonPosition = FabPosition.End,
|
floatingActionButtonPosition = FabPosition.End,
|
||||||
|
topBar = {
|
||||||
|
if (uiState.settings.isAutoTunnelEnabled)
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.requiredWidth(LocalConfiguration.current.screenWidthDp.dp)
|
||||||
|
.padding(end = 5.dp)
|
||||||
|
) {
|
||||||
|
Row {
|
||||||
|
Icon(
|
||||||
|
Icons.Rounded.Bolt,
|
||||||
|
stringResource(id = R.string.auto),
|
||||||
|
modifier = Modifier.size(25.dp),
|
||||||
|
tint =
|
||||||
|
if (uiState.settings.isAutoTunnelPaused) Color.Gray
|
||||||
|
else mint,
|
||||||
|
)
|
||||||
|
val autoTunnelingLabel = buildAnnotatedString {
|
||||||
|
append(stringResource(id = R.string.auto_tunneling))
|
||||||
|
append(": ")
|
||||||
|
if(uiState.settings.isAutoTunnelPaused) append(stringResource(id = R.string.paused)) else append(
|
||||||
|
stringResource(id = R.string.active),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
autoTunnelingLabel.text,
|
||||||
|
style = typography.bodyLarge,
|
||||||
|
modifier = Modifier.padding(start = 10.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (uiState.settings.isAutoTunnelPaused)
|
||||||
|
TextButton(
|
||||||
|
onClick = { viewModel.resumeAutoTunneling() },
|
||||||
|
modifier = Modifier.padding(end = 10.dp),
|
||||||
|
) {
|
||||||
|
Text(stringResource(id = R.string.resume))
|
||||||
|
}
|
||||||
|
else
|
||||||
|
TextButton(
|
||||||
|
onClick = { viewModel.pauseAutoTunneling() },
|
||||||
|
modifier = Modifier.padding(end = 10.dp),
|
||||||
|
) {
|
||||||
|
Text(stringResource(id = R.string.pause))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = isVisible.value,
|
visible = isVisible.value,
|
||||||
enter = slideInVertically(initialOffsetY = { it * 2 }),
|
enter = slideInVertically(initialOffsetY = { it * 2 }),
|
||||||
exit = slideOutVertically(targetOffsetY = { it * 2 })
|
exit = slideOutVertically(targetOffsetY = { it * 2 }),
|
||||||
) {
|
) {
|
||||||
val secondaryColor = MaterialTheme.colorScheme.secondary
|
val secondaryColor = MaterialTheme.colorScheme.secondary
|
||||||
val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
|
val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
|
||||||
var fobColor by remember { mutableStateOf(secondaryColor) }
|
var fobColor by remember { mutableStateOf(secondaryColor) }
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
(if (
|
||||||
|
WireGuardAutoTunnel.isRunningOnAndroidTv() &&
|
||||||
|
uiState.tunnels.isEmpty()
|
||||||
|
)
|
||||||
|
Modifier.focusRequester(focusRequester)
|
||||||
|
else Modifier)
|
||||||
.padding(bottom = 90.dp)
|
.padding(bottom = 90.dp)
|
||||||
.onFocusChanged {
|
.onFocusChanged {
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
fobColor = if (it.isFocused) hoverColor else secondaryColor
|
fobColor = if (it.isFocused) hoverColor else secondaryColor
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = { showBottomSheet = true },
|
||||||
showBottomSheet = true
|
|
||||||
},
|
|
||||||
containerColor = fobColor,
|
containerColor = fobColor,
|
||||||
shape = RoundedCornerShape(16.dp)
|
shape = RoundedCornerShape(16.dp),
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Rounded.Add,
|
imageVector = Icons.Rounded.Add,
|
||||||
contentDescription = stringResource(id = R.string.add_tunnel),
|
contentDescription = stringResource(id = R.string.add_tunnel),
|
||||||
tint = Color.DarkGray
|
tint = Color.DarkGray,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
) {
|
) { innerPadding ->
|
||||||
if (tunnels.isEmpty()) {
|
AnimatedVisibility(uiState.tunnels.isEmpty(), exit = fadeOut(), enter = fadeIn()) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
modifier =
|
modifier = Modifier
|
||||||
Modifier
|
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(padding)
|
.padding(innerPadding),
|
||||||
) {
|
) {
|
||||||
Text(text = stringResource(R.string.no_tunnels), fontStyle = FontStyle.Italic)
|
Text(text = stringResource(R.string.no_tunnels), fontStyle = FontStyle.Italic)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (showBottomSheet) {
|
if (showBottomSheet) {
|
||||||
ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
onDismissRequest = {
|
onDismissRequest = { showBottomSheet = false },
|
||||||
showBottomSheet = false
|
sheetState = sheetState,
|
||||||
},
|
|
||||||
sheetState = sheetState
|
|
||||||
) {
|
) {
|
||||||
// Sheet content
|
// Sheet content
|
||||||
Row(
|
Row(
|
||||||
@@ -314,26 +392,22 @@ fun MainScreen(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable {
|
.clickable {
|
||||||
showBottomSheet = false
|
showBottomSheet = false
|
||||||
try {
|
tunnelFileImportResultLauncher.launch(Constants.ALLOWED_FILE_TYPES)
|
||||||
tunnelFileImportResultLauncher.launch(Constants.ALLOWED_FILE_TYPES)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
showSnackbarMessage(e.message!!)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.padding(10.dp)
|
.padding(10.dp),
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Filled.FileOpen,
|
Icons.Filled.FileOpen,
|
||||||
contentDescription = stringResource(id = R.string.open_file),
|
contentDescription = stringResource(id = R.string.open_file),
|
||||||
modifier = Modifier.padding(10.dp)
|
modifier = Modifier.padding(10.dp),
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
stringResource(id = R.string.add_tunnels_text),
|
stringResource(id = R.string.add_tunnels_text),
|
||||||
modifier = Modifier.padding(10.dp)
|
modifier = Modifier.padding(10.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
Divider()
|
HorizontalDivider()
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
@@ -344,27 +418,29 @@ fun MainScreen(
|
|||||||
val scanOptions = ScanOptions()
|
val scanOptions = ScanOptions()
|
||||||
scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE)
|
scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE)
|
||||||
scanOptions.setOrientationLocked(true)
|
scanOptions.setOrientationLocked(true)
|
||||||
scanOptions.setPrompt(context.getString(R.string.scanning_qr))
|
scanOptions.setPrompt(
|
||||||
|
context.getString(R.string.scanning_qr)
|
||||||
|
)
|
||||||
scanOptions.setBeepEnabled(false)
|
scanOptions.setBeepEnabled(false)
|
||||||
scanOptions.captureActivity =
|
scanOptions.captureActivity =
|
||||||
CaptureActivityPortrait::class.java
|
CaptureActivityPortrait::class.java
|
||||||
scanLauncher.launch(scanOptions)
|
scanLauncher.launch(scanOptions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(10.dp)
|
.padding(10.dp),
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Filled.QrCode,
|
Icons.Filled.QrCode,
|
||||||
contentDescription = stringResource(id = R.string.qr_scan),
|
contentDescription = stringResource(id = R.string.qr_scan),
|
||||||
modifier = Modifier.padding(10.dp)
|
modifier = Modifier.padding(10.dp),
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
stringResource(id = R.string.add_from_qr),
|
stringResource(id = R.string.add_from_qr),
|
||||||
modifier = Modifier.padding(10.dp)
|
modifier = Modifier.padding(10.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Divider()
|
HorizontalDivider()
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
@@ -372,232 +448,270 @@ fun MainScreen(
|
|||||||
.clickable {
|
.clickable {
|
||||||
showBottomSheet = false
|
showBottomSheet = false
|
||||||
navController.navigate(
|
navController.navigate(
|
||||||
"${Routes.Config.name}/${Constants.MANUAL_TUNNEL_CONFIG_ID}"
|
"${Screen.Config.route}/${Constants.MANUAL_TUNNEL_CONFIG_ID}",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.padding(10.dp)
|
.padding(10.dp),
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Filled.Create,
|
Icons.Filled.Create,
|
||||||
contentDescription = stringResource(id = R.string.create_import),
|
contentDescription = stringResource(id = R.string.create_import),
|
||||||
modifier = Modifier.padding(10.dp)
|
modifier = Modifier.padding(10.dp),
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
stringResource(id = R.string.create_import),
|
stringResource(id = R.string.create_import),
|
||||||
modifier = Modifier.padding(10.dp)
|
modifier = Modifier.padding(10.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Column(
|
|
||||||
|
LazyColumn(
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxWidth()
|
||||||
.padding(padding)
|
.fillMaxHeight(.90f)
|
||||||
|
.overscroll(ScrollableDefaults.overscrollEffect())
|
||||||
|
.padding(innerPadding),
|
||||||
|
state = rememberLazyListState(0, uiState.tunnels.count()),
|
||||||
|
userScrollEnabled = true,
|
||||||
|
reverseLayout = true,
|
||||||
|
flingBehavior = ScrollableDefaults.flingBehavior(),
|
||||||
) {
|
) {
|
||||||
LazyColumn(
|
items(
|
||||||
modifier =
|
uiState.tunnels,
|
||||||
Modifier
|
key = { tunnel -> tunnel.id },
|
||||||
.fillMaxSize()
|
) { tunnel ->
|
||||||
.padding(top = 10.dp)
|
val leadingIconColor =
|
||||||
.nestedScroll(nestedScrollConnection)
|
(if (
|
||||||
) {
|
uiState.vpnState.name == tunnel.name &&
|
||||||
items(tunnels, key = { tunnel -> tunnel.id }) { tunnel ->
|
uiState.vpnState.status == Tunnel.State.UP
|
||||||
val leadingIconColor = (
|
) {
|
||||||
if (tunnelName == tunnel.name) {
|
uiState.vpnState.statistics
|
||||||
when (handshakeStatus) {
|
?.mapPeerStats()
|
||||||
HandshakeStatus.HEALTHY -> mint
|
?.map { it.value?.handshakeStatus() }
|
||||||
HandshakeStatus.UNHEALTHY -> brickRed
|
.let { statuses ->
|
||||||
HandshakeStatus.STALE -> corn
|
when {
|
||||||
HandshakeStatus.NOT_STARTED -> Color.Gray
|
statuses?.all { it == HandshakeStatus.HEALTHY } == true -> mint
|
||||||
HandshakeStatus.NEVER_CONNECTED -> brickRed
|
statuses?.any { it == HandshakeStatus.STALE } == true -> corn
|
||||||
|
statuses?.all { it == HandshakeStatus.NOT_STARTED } == true ->
|
||||||
|
Color.Gray
|
||||||
|
else -> {
|
||||||
|
Color.Gray
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Color.Gray
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
Color.Gray
|
||||||
|
})
|
||||||
|
val expanded = remember { mutableStateOf(false) }
|
||||||
|
RowListItem(
|
||||||
|
icon = {
|
||||||
|
if (uiState.settings.isTunnelConfigDefault(tunnel)) {
|
||||||
|
Icon(
|
||||||
|
Icons.Rounded.Star,
|
||||||
|
stringResource(R.string.status),
|
||||||
|
tint = leadingIconColor,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = 10.dp)
|
||||||
|
.size(20.dp),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Icon(
|
||||||
|
Icons.Rounded.Circle,
|
||||||
|
stringResource(R.string.status),
|
||||||
|
tint = leadingIconColor,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = 15.dp)
|
||||||
|
.size(15.dp),
|
||||||
)
|
)
|
||||||
val focusRequester = remember { FocusRequester() }
|
|
||||||
val expanded =
|
|
||||||
remember {
|
|
||||||
mutableStateOf(false)
|
|
||||||
}
|
}
|
||||||
RowListItem(
|
},
|
||||||
icon = {
|
text = tunnel.name,
|
||||||
if (settings.isTunnelConfigDefault(tunnel)) {
|
onHold = {
|
||||||
Icon(
|
if (
|
||||||
Icons.Rounded.Star,
|
(uiState.vpnState.status == Tunnel.State.UP) &&
|
||||||
stringResource(R.string.status),
|
(tunnel.name == uiState.vpnState.name)
|
||||||
tint = leadingIconColor,
|
) {
|
||||||
modifier =
|
showSnackbarMessage(Event.Message.TunnelOffAction.message)
|
||||||
Modifier
|
return@RowListItem
|
||||||
.padding(end = 10.dp)
|
}
|
||||||
.size(20.dp)
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
)
|
selectedTunnel = tunnel
|
||||||
} else {
|
},
|
||||||
Icon(
|
onClick = {
|
||||||
Icons.Rounded.Circle,
|
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
stringResource(R.string.status),
|
if (
|
||||||
tint = leadingIconColor,
|
uiState.vpnState.status == Tunnel.State.UP &&
|
||||||
modifier =
|
(uiState.vpnState.name == tunnel.name)
|
||||||
Modifier
|
|
||||||
.padding(end = 15.dp)
|
|
||||||
.size(15.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
text = tunnel.name,
|
|
||||||
onHold = {
|
|
||||||
if ((state == Tunnel.State.UP) && (tunnel.name == tunnelName)) {
|
|
||||||
showSnackbarMessage(
|
|
||||||
context.resources.getString(R.string.turn_off_tunnel)
|
|
||||||
)
|
|
||||||
return@RowListItem
|
|
||||||
}
|
|
||||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
|
||||||
selectedTunnel = tunnel
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
|
||||||
if (state == Tunnel.State.UP && (tunnelName == tunnel.name)) {
|
|
||||||
expanded.value = !expanded.value
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
selectedTunnel = tunnel
|
|
||||||
focusRequester.requestFocus()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
statistics = statistics,
|
|
||||||
expanded = expanded.value,
|
|
||||||
rowButton = {
|
|
||||||
if (tunnel.id == selectedTunnel?.id && !WireGuardAutoTunnel.isRunningOnAndroidTv(
|
|
||||||
context
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
Row {
|
expanded.value = !expanded.value
|
||||||
if (!settings.isTunnelConfigDefault(tunnel)) {
|
}
|
||||||
IconButton(onClick = {
|
} else {
|
||||||
if (settings.isAutoTunnelEnabled) {
|
selectedTunnel = tunnel
|
||||||
|
focusRequester.requestFocus()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
statistics = uiState.vpnState.statistics,
|
||||||
|
expanded = expanded.value,
|
||||||
|
rowButton = {
|
||||||
|
if (
|
||||||
|
tunnel.id == selectedTunnel?.id &&
|
||||||
|
!WireGuardAutoTunnel.isRunningOnAndroidTv()
|
||||||
|
) {
|
||||||
|
Row {
|
||||||
|
if (!uiState.settings.isTunnelConfigDefault(tunnel)) {
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
if (
|
||||||
|
uiState.settings.isAutoTunnelEnabled &&
|
||||||
|
!uiState.settings.isAutoTunnelPaused
|
||||||
|
) {
|
||||||
showSnackbarMessage(
|
showSnackbarMessage(
|
||||||
context.resources.getString(
|
Event.Message.AutoTunnelOffAction.message,
|
||||||
R.string.turn_off_auto
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
showPrimaryChangeAlertDialog = true
|
showPrimaryChangeAlertDialog = true
|
||||||
}
|
}
|
||||||
}) {
|
},
|
||||||
Icon(
|
|
||||||
Icons.Rounded.Star,
|
|
||||||
stringResource(id = R.string.set_primary)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
IconButton(onClick = {
|
|
||||||
navController.navigate(
|
|
||||||
"${Routes.Config.name}/${selectedTunnel?.id}"
|
|
||||||
)
|
|
||||||
}) {
|
|
||||||
Icon(Icons.Rounded.Edit, stringResource(id = R.string.edit))
|
|
||||||
}
|
|
||||||
IconButton(
|
|
||||||
modifier = Modifier.focusable(),
|
|
||||||
onClick = { viewModel.onDelete(tunnel) }
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Rounded.Delete,
|
Icons.Rounded.Star,
|
||||||
stringResource(id = R.string.delete)
|
stringResource(id = R.string.set_primary),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
IconButton(
|
||||||
val checked = state == Tunnel.State.UP && tunnel.name == tunnelName
|
onClick = {
|
||||||
if (!checked) expanded.value = false
|
if (
|
||||||
|
uiState.settings.isAutoTunnelEnabled &&
|
||||||
|
uiState.settings.isTunnelConfigDefault(
|
||||||
|
tunnel,
|
||||||
|
) &&
|
||||||
|
!uiState.settings.isAutoTunnelPaused
|
||||||
|
) {
|
||||||
|
showSnackbarMessage(
|
||||||
|
Event.Message.AutoTunnelOffAction.message,
|
||||||
|
)
|
||||||
|
} else
|
||||||
|
navController.navigate(
|
||||||
|
"${Screen.Config.route}/${selectedTunnel?.id}",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Icon(Icons.Rounded.Edit, stringResource(id = R.string.edit))
|
||||||
|
}
|
||||||
|
IconButton(
|
||||||
|
modifier = Modifier.focusable(),
|
||||||
|
onClick = { showDeleteTunnelAlertDialog = true },
|
||||||
|
) {
|
||||||
|
Icon(Icons.Rounded.Delete, stringResource(id = R.string.delete))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val checked by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
(uiState.vpnState.status == Tunnel.State.UP &&
|
||||||
|
tunnel.name == uiState.vpnState.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!checked) expanded.value = false
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TunnelSwitch() =
|
fun TunnelSwitch() =
|
||||||
Switch(
|
Switch(
|
||||||
modifier = Modifier.focusRequester(focusRequester),
|
modifier = Modifier.focusRequester(focusRequester),
|
||||||
checked = checked,
|
checked = checked,
|
||||||
onCheckedChange = { checked ->
|
onCheckedChange = { checked ->
|
||||||
if (!checked) expanded.value = false
|
if (!checked) expanded.value = false
|
||||||
onTunnelToggle(checked, tunnel)
|
onTunnelToggle(checked, tunnel)
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
Row {
|
Row {
|
||||||
if (!settings.isTunnelConfigDefault(tunnel)) {
|
if (!uiState.settings.isTunnelConfigDefault(tunnel)) {
|
||||||
IconButton(onClick = {
|
IconButton(
|
||||||
if (settings.isAutoTunnelEnabled) {
|
onClick = {
|
||||||
|
if (uiState.settings.isAutoTunnelEnabled) {
|
||||||
showSnackbarMessage(
|
showSnackbarMessage(
|
||||||
context.resources.getString(
|
Event.Message.AutoTunnelOffAction.message,
|
||||||
R.string.turn_off_auto
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
selectedTunnel = tunnel
|
||||||
showPrimaryChangeAlertDialog = true
|
showPrimaryChangeAlertDialog = true
|
||||||
}
|
}
|
||||||
}) {
|
},
|
||||||
Icon(
|
) {
|
||||||
Icons.Rounded.Star,
|
Icon(
|
||||||
stringResource(id = R.string.set_primary)
|
Icons.Rounded.Star,
|
||||||
|
stringResource(id = R.string.set_primary),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
IconButton(
|
||||||
|
modifier = Modifier.focusRequester(focusRequester),
|
||||||
|
onClick = {
|
||||||
|
if (
|
||||||
|
uiState.vpnState.status == Tunnel.State.UP &&
|
||||||
|
(uiState.vpnState.name == tunnel.name)
|
||||||
|
) {
|
||||||
|
expanded.value = !expanded.value
|
||||||
|
} else {
|
||||||
|
showSnackbarMessage(
|
||||||
|
Event.Message.TunnelOnAction.message
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
IconButton(
|
) {
|
||||||
modifier = Modifier.focusRequester(focusRequester),
|
Icon(Icons.Rounded.Info, stringResource(R.string.info))
|
||||||
onClick = {
|
}
|
||||||
if (state == Tunnel.State.UP && (tunnelName == tunnel.name)) {
|
IconButton(
|
||||||
expanded.value = !expanded.value
|
onClick = {
|
||||||
}
|
if (
|
||||||
}
|
uiState.vpnState.status == Tunnel.State.UP &&
|
||||||
) {
|
tunnel.name == uiState.vpnState.name
|
||||||
Icon(Icons.Rounded.Info, stringResource(R.string.info))
|
) {
|
||||||
}
|
|
||||||
IconButton(onClick = {
|
|
||||||
if (state == Tunnel.State.UP && tunnel.name == tunnelName) {
|
|
||||||
showSnackbarMessage(
|
showSnackbarMessage(
|
||||||
context.resources.getString(
|
Event.Message.TunnelOffAction.message
|
||||||
R.string.turn_off_tunnel
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
navController.navigate(
|
navController.navigate(
|
||||||
"${Routes.Config.name}/${tunnel.id}"
|
"${Screen.Config.route}/${tunnel.id}",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}) {
|
},
|
||||||
Icon(
|
) {
|
||||||
Icons.Rounded.Edit,
|
Icon(Icons.Rounded.Edit, stringResource(id = R.string.edit))
|
||||||
stringResource(id = R.string.edit)
|
}
|
||||||
)
|
IconButton(
|
||||||
}
|
onClick = {
|
||||||
IconButton(onClick = {
|
if (
|
||||||
if (state == Tunnel.State.UP && tunnel.name == tunnelName) {
|
uiState.vpnState.status == Tunnel.State.UP &&
|
||||||
|
tunnel.name == uiState.vpnState.name
|
||||||
|
) {
|
||||||
showSnackbarMessage(
|
showSnackbarMessage(
|
||||||
context.resources.getString(
|
Event.Message.TunnelOffAction.message
|
||||||
R.string.turn_off_tunnel
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
viewModel.onDelete(tunnel)
|
showDeleteTunnelAlertDialog = true
|
||||||
}
|
}
|
||||||
}) {
|
},
|
||||||
Icon(
|
) {
|
||||||
Icons.Rounded.Delete,
|
Icon(
|
||||||
stringResource(id = R.string.delete)
|
Icons.Rounded.Delete,
|
||||||
)
|
stringResource(id = R.string.delete),
|
||||||
}
|
)
|
||||||
TunnelSwitch()
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
TunnelSwitch()
|
TunnelSwitch()
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
TunnelSwitch()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
},
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.main
|
||||||
|
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
||||||
|
|
||||||
|
data class MainUiState(
|
||||||
|
val settings: Settings = Settings(),
|
||||||
|
val tunnels: TunnelConfigs = emptyList(),
|
||||||
|
val vpnState: VpnState = VpnState(),
|
||||||
|
val loading: Boolean = true
|
||||||
|
)
|
||||||
+142
-149
@@ -8,162 +8,157 @@ import android.provider.OpenableColumns
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.wireguard.config.Config
|
import com.wireguard.config.Config
|
||||||
import com.zaneschepke.wireguardautotunnel.Constants
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceState
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Event
|
||||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||||
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
|
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import timber.log.Timber
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.util.zip.ZipInputStream
|
import java.util.zip.ZipInputStream
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.flow.filter
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class MainViewModel
|
class MainViewModel
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
private val application: Application,
|
private val application: Application,
|
||||||
private val tunnelRepo: TunnelConfigDao,
|
private val tunnelConfigRepository: TunnelConfigRepository,
|
||||||
private val settingsRepo: SettingsDoa,
|
private val settingsRepository: SettingsRepository,
|
||||||
private val vpnService: VpnService
|
private val vpnService: VpnService
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
val tunnels get() = tunnelRepo.getAllFlow()
|
|
||||||
val state get() = vpnService.state
|
|
||||||
|
|
||||||
val handshakeStatus get() = vpnService.handshakeStatus
|
val uiState =
|
||||||
val tunnelName get() = vpnService.tunnelName
|
combine(
|
||||||
private val _settings = MutableStateFlow(Settings())
|
settingsRepository.getSettingsFlow(),
|
||||||
val settings get() = _settings.asStateFlow()
|
tunnelConfigRepository.getTunnelConfigsFlow(),
|
||||||
val statistics get() = vpnService.statistics
|
vpnService.vpnState,
|
||||||
|
) { settings, tunnels, vpnState ->
|
||||||
init {
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
settingsRepo.getAllFlow().filter { it.isNotEmpty() }.collect {
|
|
||||||
val settings = it.first()
|
|
||||||
validateWatcherServiceState(settings)
|
validateWatcherServiceState(settings)
|
||||||
_settings.emit(settings)
|
MainUiState(settings, tunnels, vpnState, false)
|
||||||
|
}
|
||||||
|
.stateIn(
|
||||||
|
viewModelScope,
|
||||||
|
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
|
||||||
|
MainUiState(),
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun validateWatcherServiceState(settings: Settings) =
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
if (settings.isAutoTunnelEnabled) {
|
||||||
|
ServiceManager.startWatcherService(application.applicationContext)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun validateWatcherServiceState(settings: Settings) {
|
private fun stopWatcherService() =
|
||||||
val watcherState =
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
ServiceManager.getServiceState(
|
ServiceManager.stopWatcherService(application.applicationContext)
|
||||||
application.applicationContext,
|
|
||||||
WireGuardConnectivityWatcherService::class.java
|
|
||||||
)
|
|
||||||
if (settings.isAutoTunnelEnabled && watcherState == ServiceState.STOPPED && settings.defaultTunnel != null) {
|
|
||||||
ServiceManager.startWatcherService(
|
|
||||||
application.applicationContext,
|
|
||||||
settings.defaultTunnel!!
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun onDelete(tunnel: TunnelConfig) {
|
fun onDelete(tunnel: TunnelConfig) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
if (tunnelRepo.count() == 1L) {
|
if (tunnelConfigRepository.count() == 1) {
|
||||||
ServiceManager.stopWatcherService(application.applicationContext)
|
stopWatcherService()
|
||||||
val settings = settingsRepo.getAll()
|
val settings = settingsRepository.getSettings()
|
||||||
if (settings.isNotEmpty()) {
|
settings.defaultTunnel = null
|
||||||
val setting = settings[0]
|
settings.isAutoTunnelEnabled = false
|
||||||
setting.defaultTunnel = null
|
settings.isAlwaysOnVpnEnabled = false
|
||||||
setting.isAutoTunnelEnabled = false
|
saveSettings(settings)
|
||||||
setting.isAlwaysOnVpnEnabled = false
|
|
||||||
settingsRepo.save(setting)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
tunnelRepo.delete(tunnel)
|
tunnelConfigRepository.delete(tunnel)
|
||||||
|
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onTunnelStart(tunnelConfig: TunnelConfig) {
|
fun onTunnelStart(tunnelConfig: TunnelConfig) =
|
||||||
viewModelScope.launch {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
stopActiveTunnel()
|
Timber.d("On start called!")
|
||||||
|
stopActiveTunnel().await()
|
||||||
startTunnel(tunnelConfig)
|
startTunnel(tunnelConfig)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun startTunnel(tunnelConfig: TunnelConfig) {
|
private fun startTunnel(tunnelConfig: TunnelConfig) =
|
||||||
ServiceManager.startVpnService(application.applicationContext, tunnelConfig.toString())
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
}
|
Timber.d("Start tunnel via manager")
|
||||||
|
ServiceManager.startVpnService(application.applicationContext, tunnelConfig.toString())
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun stopActiveTunnel() {
|
private fun stopActiveTunnel() =
|
||||||
if (ServiceManager.getServiceState(
|
viewModelScope.async(Dispatchers.IO) {
|
||||||
application.applicationContext,
|
|
||||||
WireGuardTunnelService::class.java
|
|
||||||
) == ServiceState.STARTED
|
|
||||||
) {
|
|
||||||
onTunnelStop()
|
onTunnelStop()
|
||||||
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun onTunnelStop() {
|
fun onTunnelStop() =
|
||||||
ServiceManager.stopVpnService(application.applicationContext)
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
}
|
Timber.d("Stopping active tunnel")
|
||||||
|
ServiceManager.stopVpnService(application.applicationContext)
|
||||||
|
}
|
||||||
|
|
||||||
private fun validateConfigString(config: String) {
|
private fun validateConfigString(config: String) {
|
||||||
TunnelConfig.configFromQuick(config)
|
TunnelConfig.configFromQuick(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun onTunnelQrResult(result: String) {
|
suspend fun onTunnelQrResult(result: String): Result<Unit> {
|
||||||
try {
|
return try {
|
||||||
validateConfigString(result)
|
validateConfigString(result)
|
||||||
val tunnelConfig =
|
val tunnelConfig =
|
||||||
TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = result)
|
TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = result)
|
||||||
addTunnel(tunnelConfig)
|
addTunnel(tunnelConfig)
|
||||||
|
Result.Success(Unit)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw WgTunnelException(e)
|
Result.Error(Event.Error.InvalidQrCode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun saveTunnelConfigFromStream(
|
private suspend fun saveTunnelConfigFromStream(stream: InputStream, fileName: String) {
|
||||||
stream: InputStream,
|
|
||||||
fileName: String
|
|
||||||
) {
|
|
||||||
val bufferReader = stream.bufferedReader(charset = Charsets.UTF_8)
|
val bufferReader = stream.bufferedReader(charset = Charsets.UTF_8)
|
||||||
val config = Config.parse(bufferReader)
|
val config = Config.parse(bufferReader)
|
||||||
val tunnelName = getNameFromFileName(fileName)
|
val tunnelName = getNameFromFileName(fileName)
|
||||||
addTunnel(TunnelConfig(name = tunnelName, wgQuick = config.toWgQuickString()))
|
addTunnel(TunnelConfig(name = tunnelName, wgQuick = config.toWgQuickString()))
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) { stream.close() }
|
||||||
stream.close()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getInputStreamFromUri(uri: Uri): InputStream {
|
private fun getInputStreamFromUri(uri: Uri): InputStream? {
|
||||||
return application.applicationContext.contentResolver.openInputStream(uri)
|
return application.applicationContext.contentResolver.openInputStream(uri)
|
||||||
?: throw WgTunnelException(application.getString(R.string.stream_failed))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun onTunnelFileSelected(uri: Uri) {
|
suspend fun onTunnelFileSelected(uri: Uri): Result<Unit> {
|
||||||
try {
|
try {
|
||||||
val fileName = getFileName(application.applicationContext, uri)
|
if (isValidUriContentScheme(uri)) {
|
||||||
val fileExtension = getFileExtensionFromFileName(fileName)
|
val fileName = getFileName(application.applicationContext, uri)
|
||||||
when (fileExtension) {
|
when (getFileExtensionFromFileName(fileName)) {
|
||||||
Constants.CONF_FILE_EXTENSION -> saveTunnelFromConfUri(fileName, uri)
|
Constants.CONF_FILE_EXTENSION ->
|
||||||
Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri)
|
saveTunnelFromConfUri(fileName, uri).let {
|
||||||
else -> throw WgTunnelException(
|
when (it) {
|
||||||
application.getString(R.string.file_extension_message)
|
is Result.Error -> return Result.Error(Event.Error.FileReadFailed)
|
||||||
)
|
is Result.Success -> return it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri)
|
||||||
|
else -> return Result.Error(Event.Error.InvalidFileExtension)
|
||||||
|
}
|
||||||
|
return Result.Success(Unit)
|
||||||
|
} else {
|
||||||
|
return Result.Error(Event.Error.InvalidFileExtension)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw WgTunnelException(e)
|
return Result.Error(Event.Error.FileReadFailed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,7 +167,7 @@ constructor(
|
|||||||
generateSequence { zip.nextEntry }
|
generateSequence { zip.nextEntry }
|
||||||
.filterNot {
|
.filterNot {
|
||||||
it.isDirectory ||
|
it.isDirectory ||
|
||||||
getFileExtensionFromFileName(it.name) != Constants.CONF_FILE_EXTENSION
|
getFileExtensionFromFileName(it.name) != Constants.CONF_FILE_EXTENSION
|
||||||
}
|
}
|
||||||
.forEach {
|
.forEach {
|
||||||
val name = getNameFromFileName(it.name)
|
val name = getNameFromFileName(it.name)
|
||||||
@@ -184,69 +179,66 @@ constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun saveTunnelFromConfUri(
|
private suspend fun saveTunnelFromConfUri(name: String, uri: Uri): Result<Unit> {
|
||||||
name: String,
|
|
||||||
uri: Uri
|
|
||||||
) {
|
|
||||||
val stream = getInputStreamFromUri(uri)
|
val stream = getInputStreamFromUri(uri)
|
||||||
saveTunnelConfigFromStream(stream, name)
|
return if (stream != null) {
|
||||||
|
saveTunnelConfigFromStream(stream, name)
|
||||||
|
Result.Success(Unit)
|
||||||
|
} else {
|
||||||
|
Result.Error(Event.Error.FileReadFailed)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun addTunnel(tunnelConfig: TunnelConfig) {
|
private suspend fun addTunnel(tunnelConfig: TunnelConfig) {
|
||||||
saveTunnel(tunnelConfig)
|
saveTunnel(tunnelConfig)
|
||||||
|
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun pauseAutoTunneling() =
|
||||||
|
viewModelScope.launch {
|
||||||
|
settingsRepository.save(uiState.value.settings.copy(isAutoTunnelPaused = true))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resumeAutoTunneling() =
|
||||||
|
viewModelScope.launch {
|
||||||
|
settingsRepository.save(uiState.value.settings.copy(isAutoTunnelPaused = false))
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun saveTunnel(tunnelConfig: TunnelConfig) {
|
private suspend fun saveTunnel(tunnelConfig: TunnelConfig) {
|
||||||
tunnelRepo.save(tunnelConfig)
|
tunnelConfigRepository.save(tunnelConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getFileNameByCursor(
|
private fun getFileNameByCursor(context: Context, uri: Uri): String? {
|
||||||
context: Context,
|
context.contentResolver.query(uri, null, null, null, null)?.use {
|
||||||
uri: Uri
|
return getDisplayNameByCursor(it)
|
||||||
): String {
|
|
||||||
val cursor = context.contentResolver.query(uri, null, null, null, null)
|
|
||||||
if (cursor != null) {
|
|
||||||
cursor.use {
|
|
||||||
return getDisplayNameByCursor(it)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw WgTunnelException("Failed to initialize cursor")
|
|
||||||
}
|
}
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getDisplayNameColumnIndex(cursor: Cursor): Int {
|
private fun getDisplayNameColumnIndex(cursor: Cursor): Int? {
|
||||||
val columnIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
val columnIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||||
if (columnIndex == -1) {
|
return if (columnIndex != -1) {
|
||||||
throw WgTunnelException("Cursor out of bounds")
|
return columnIndex
|
||||||
}
|
|
||||||
return columnIndex
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getDisplayNameByCursor(cursor: Cursor): String {
|
|
||||||
if (cursor.moveToFirst()) {
|
|
||||||
val index = getDisplayNameColumnIndex(cursor)
|
|
||||||
return cursor.getString(index)
|
|
||||||
} else {
|
} else {
|
||||||
throw WgTunnelException("Cursor failed to move to first")
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun validateUriContentScheme(uri: Uri) {
|
private fun getDisplayNameByCursor(cursor: Cursor): String? {
|
||||||
if (uri.scheme != Constants.URI_CONTENT_SCHEME) {
|
return if (cursor.moveToFirst()) {
|
||||||
throw WgTunnelException(application.getString(R.string.file_extension_message))
|
val index = getDisplayNameColumnIndex(cursor)
|
||||||
}
|
if (index != null) {
|
||||||
|
cursor.getString(index)
|
||||||
|
} else null
|
||||||
|
} else null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getFileName(
|
private fun isValidUriContentScheme(uri: Uri): Boolean {
|
||||||
context: Context,
|
return uri.scheme == Constants.URI_CONTENT_SCHEME
|
||||||
uri: Uri
|
}
|
||||||
): String {
|
|
||||||
validateUriContentScheme(uri)
|
private fun getFileName(context: Context, uri: Uri): String {
|
||||||
return try {
|
return getFileNameByCursor(context, uri) ?: NumberUtils.generateRandomTunnelName()
|
||||||
getFileNameByCursor(context, uri)
|
|
||||||
} catch (_: Exception) {
|
|
||||||
NumberUtils.generateRandomTunnelName()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getNameFromFileName(fileName: String): String {
|
private fun getNameFromFileName(fileName: String): String {
|
||||||
@@ -261,14 +253,15 @@ constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun onDefaultTunnelChange(selectedTunnel: TunnelConfig?) {
|
private fun saveSettings(settings: Settings) =
|
||||||
if (selectedTunnel != null) {
|
viewModelScope.launch(Dispatchers.IO) { settingsRepository.save(settings) }
|
||||||
_settings.emit(
|
|
||||||
_settings.value.copy(
|
fun onDefaultTunnelChange(selectedTunnel: TunnelConfig?) =
|
||||||
defaultTunnel = selectedTunnel.toString()
|
viewModelScope.launch {
|
||||||
)
|
if (selectedTunnel != null) {
|
||||||
)
|
saveSettings(uiState.value.settings.copy(defaultTunnel = selectedTunnel.toString()))
|
||||||
settingsRepo.save(_settings.value)
|
.join()
|
||||||
|
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+331
-303
@@ -1,10 +1,17 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.settings
|
package com.zaneschepke.wireguardautotunnel.ui.screens.settings
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context.POWER_SERVICE
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.os.PowerManager
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
|
import android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.ActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
@@ -30,6 +37,7 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.outlined.Add
|
import androidx.compose.material.icons.outlined.Add
|
||||||
import androidx.compose.material.icons.rounded.LocationOff
|
import androidx.compose.material.icons.rounded.LocationOff
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -45,15 +53,13 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusProperties
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
@@ -70,235 +76,265 @@ import com.wireguard.android.backend.Tunnel
|
|||||||
import com.wireguard.android.backend.WgQuickBackend
|
import com.wireguard.android.backend.WgQuickBackend
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.datastore.DataStoreManager
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
|
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
|
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
|
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
|
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Event
|
||||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||||
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
|
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||||
import java.io.File
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
@OptIn(
|
@OptIn(
|
||||||
ExperimentalPermissionsApi::class,
|
ExperimentalPermissionsApi::class,
|
||||||
ExperimentalLayoutApi::class,
|
ExperimentalLayoutApi::class,
|
||||||
ExperimentalComposeUiApi::class
|
|
||||||
)
|
)
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsScreen(
|
fun SettingsScreen(
|
||||||
viewModel: SettingsViewModel = hiltViewModel(),
|
|
||||||
padding: PaddingValues,
|
padding: PaddingValues,
|
||||||
|
viewModel: SettingsViewModel = hiltViewModel(),
|
||||||
showSnackbarMessage: (String) -> Unit,
|
showSnackbarMessage: (String) -> Unit,
|
||||||
focusRequester: FocusRequester
|
focusRequester: FocusRequester
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope { Dispatchers.IO }
|
val scope = rememberCoroutineScope { Dispatchers.IO }
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
val scrollState = rememberScrollState()
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
|
||||||
val settings by viewModel.settings.collectAsStateWithLifecycle()
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
val trustedSSIDs by viewModel.trustedSSIDs.collectAsStateWithLifecycle()
|
|
||||||
val tunnels by viewModel.tunnels.collectAsStateWithLifecycle(mutableListOf())
|
|
||||||
val fineLocationState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
|
val fineLocationState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||||
var currentText by remember { mutableStateOf("") }
|
var currentText by remember { mutableStateOf("") }
|
||||||
val scrollState = rememberScrollState()
|
|
||||||
var isBackgroundLocationGranted by remember { mutableStateOf(true) }
|
var isBackgroundLocationGranted by remember { mutableStateOf(true) }
|
||||||
var showAuthPrompt by remember { mutableStateOf(false) }
|
var showLocationServicesAlertDialog by remember { mutableStateOf(false) }
|
||||||
var didExportFiles by remember { mutableStateOf(false) }
|
var didExportFiles by remember { mutableStateOf(false) }
|
||||||
val isLocationDisclosureShown by viewModel.disclosureShown.collectAsStateWithLifecycle(
|
var showAuthPrompt by remember { mutableStateOf(false) }
|
||||||
null
|
val focusRequester2 = remember { FocusRequester() }
|
||||||
)
|
|
||||||
val vpnState = viewModel.vpnState.collectAsStateWithLifecycle(initialValue = Tunnel.State.DOWN)
|
|
||||||
|
|
||||||
val screenPadding = 5.dp
|
val screenPadding = 5.dp
|
||||||
val fillMaxWidth = .85f
|
val fillMaxWidth = .85f
|
||||||
|
|
||||||
fun setLocationDisclosureShown() = scope.launch {
|
if (uiState.loading) {
|
||||||
viewModel.dataStoreManager.saveToDataStore(
|
LoadingScreen()
|
||||||
DataStoreManager.LOCATION_DISCLOSURE_SHOWN,
|
return
|
||||||
true
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val startForResult =
|
||||||
|
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
|
result: ActivityResult ->
|
||||||
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
|
result.data
|
||||||
|
// Handle the Intent
|
||||||
|
}
|
||||||
|
viewModel.setBatteryOptimizeDisableShown()
|
||||||
|
}
|
||||||
|
|
||||||
fun exportAllConfigs() {
|
fun exportAllConfigs() {
|
||||||
try {
|
try {
|
||||||
val files = tunnels.map { File(context.cacheDir, "${it.name}.conf") }
|
val files = uiState.tunnels.map { File(context.cacheDir, "${it.name}.conf") }
|
||||||
files.forEachIndexed { index, file ->
|
files.forEachIndexed { index, file ->
|
||||||
file.outputStream().use {
|
file.outputStream().use { it.write(uiState.tunnels[index].wgQuick.toByteArray()) }
|
||||||
it.write(tunnels[index].wgQuick.toByteArray())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
FileUtils.saveFilesToZip(context, files)
|
FileUtils.saveFilesToZip(context, files)
|
||||||
didExportFiles = true
|
didExportFiles = true
|
||||||
showSnackbarMessage(context.getString(R.string.exported_configs_message))
|
showSnackbarMessage(Event.Message.ConfigsExported.message)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
showSnackbarMessage(e.message!!)
|
showSnackbarMessage(Event.Error.Exception(e).message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isBatteryOptimizationsDisabled(): Boolean {
|
||||||
|
val pm = context.getSystemService(POWER_SERVICE) as PowerManager
|
||||||
|
return pm.isIgnoringBatteryOptimizations(context.packageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun requestBatteryOptimizationsDisabled() {
|
||||||
|
val intent =
|
||||||
|
Intent().apply {
|
||||||
|
this.action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
|
||||||
|
data = Uri.fromParts("package", context.packageName, null)
|
||||||
|
}
|
||||||
|
startForResult.launch(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun handleAutoTunnelToggle() {
|
||||||
|
if (uiState.isBatteryOptimizeDisableShown || isBatteryOptimizationsDisabled()) {
|
||||||
|
viewModel.toggleAutoTunnel()
|
||||||
|
} else {
|
||||||
|
requestBatteryOptimizationsDisabled()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveTrustedSSID() {
|
fun saveTrustedSSID() {
|
||||||
if (currentText.isNotEmpty()) {
|
if (currentText.isNotEmpty()) {
|
||||||
scope.launch {
|
viewModel.onSaveTrustedSSID(currentText).let {
|
||||||
try {
|
when (it) {
|
||||||
viewModel.onSaveTrustedSSID(currentText)
|
is Result.Success -> currentText = ""
|
||||||
currentText = ""
|
is Result.Error -> showSnackbarMessage(it.error.message)
|
||||||
} catch (e: Exception) {
|
|
||||||
showSnackbarMessage(e.message ?: context.getString(R.string.unknown_error))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isAllAutoTunnelPermissionsEnabled(): Boolean {
|
|
||||||
return (
|
|
||||||
isBackgroundLocationGranted &&
|
|
||||||
fineLocationState.status.isGranted &&
|
|
||||||
!viewModel.isLocationServicesNeeded()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun openSettings() {
|
fun openSettings() {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val intentSettings =
|
val intentSettings = Intent(ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||||
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
intentSettings.data = Uri.fromParts("package", context.packageName, null)
|
||||||
intentSettings.data =
|
|
||||||
Uri.fromParts("package", context.packageName, null)
|
|
||||||
context.startActivity(intentSettings)
|
context.startActivity(intentSettings)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
fun checkFineLocationGranted() {
|
||||||
val backgroundLocationState =
|
isBackgroundLocationGranted =
|
||||||
rememberPermissionState(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
|
if (!fineLocationState.status.isGranted) {
|
||||||
isBackgroundLocationGranted = if (!backgroundLocationState.status.isGranted) {
|
false
|
||||||
false
|
} else {
|
||||||
} else {
|
viewModel.setLocationDisclosureShown()
|
||||||
SideEffect {
|
true
|
||||||
setLocationDisclosureShown()
|
|
||||||
}
|
}
|
||||||
true
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
if (
|
||||||
|
WireGuardAutoTunnel.isRunningOnAndroidTv() &&
|
||||||
|
Build.VERSION.SDK_INT == Build.VERSION_CODES.Q
|
||||||
|
) {
|
||||||
|
checkFineLocationGranted()
|
||||||
|
} else {
|
||||||
|
val backgroundLocationState =
|
||||||
|
rememberPermissionState(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
|
||||||
|
isBackgroundLocationGranted =
|
||||||
|
if (!backgroundLocationState.status.isGranted) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
SideEffect { viewModel.setLocationDisclosureShown() }
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
|
||||||
if (!fineLocationState.status.isGranted) {
|
checkFineLocationGranted()
|
||||||
isBackgroundLocationGranted = false
|
}
|
||||||
} else {
|
|
||||||
SideEffect {
|
AnimatedVisibility(showLocationServicesAlertDialog) {
|
||||||
setLocationDisclosureShown()
|
AlertDialog(
|
||||||
|
onDismissRequest = { showLocationServicesAlertDialog = false },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
showLocationServicesAlertDialog = false
|
||||||
|
handleAutoTunnelToggle()
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.okay))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { showLocationServicesAlertDialog = false }) {
|
||||||
|
Text(text = stringResource(R.string.cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title = { Text(text = stringResource(R.string.location_services_not_detected)) },
|
||||||
|
text = { Text(text = stringResource(R.string.location_services_missing_message)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!uiState.isLocationDisclosureShown) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Top,
|
||||||
|
modifier = Modifier.fillMaxSize().verticalScroll(scrollState).padding(padding),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Rounded.LocationOff,
|
||||||
|
contentDescription = stringResource(id = R.string.map),
|
||||||
|
modifier = Modifier.padding(30.dp).size(128.dp),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.prominent_background_location_title),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(30.dp),
|
||||||
|
fontSize = 20.sp,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.prominent_background_location_message),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(30.dp),
|
||||||
|
fontSize = 15.sp,
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier =
|
||||||
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
|
Modifier.fillMaxWidth().padding(10.dp)
|
||||||
|
} else {
|
||||||
|
Modifier.fillMaxWidth().padding(30.dp)
|
||||||
|
},
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
|
) {
|
||||||
|
TextButton(onClick = { viewModel.setLocationDisclosureShown() }) {
|
||||||
|
Text(stringResource(id = R.string.no_thanks))
|
||||||
|
}
|
||||||
|
TextButton(
|
||||||
|
modifier = Modifier.focusRequester(focusRequester),
|
||||||
|
onClick = {
|
||||||
|
openSettings()
|
||||||
|
viewModel.setLocationDisclosureShown()
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Text(stringResource(id = R.string.turn_on))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
isBackgroundLocationGranted = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLocationDisclosureShown != true) {
|
if (showAuthPrompt) {
|
||||||
Column(
|
AuthorizationPrompt(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
onSuccess = {
|
||||||
verticalArrangement = Arrangement.Top,
|
showAuthPrompt = false
|
||||||
modifier =
|
exportAllConfigs()
|
||||||
Modifier
|
},
|
||||||
.fillMaxSize()
|
onError = { _ ->
|
||||||
.verticalScroll(scrollState)
|
showAuthPrompt = false
|
||||||
.padding(padding)
|
showSnackbarMessage(Event.Error.AuthenticationFailed.message)
|
||||||
) {
|
},
|
||||||
Icon(
|
onFailure = {
|
||||||
Icons.Rounded.LocationOff,
|
showAuthPrompt = false
|
||||||
contentDescription = stringResource(id = R.string.map),
|
showSnackbarMessage(Event.Error.AuthorizationFailed.message)
|
||||||
modifier =
|
},
|
||||||
Modifier
|
)
|
||||||
.padding(30.dp)
|
}
|
||||||
.size(128.dp)
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.prominent_background_location_title),
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
modifier = Modifier.padding(30.dp),
|
|
||||||
fontSize = 20.sp
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.prominent_background_location_message),
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
modifier = Modifier.padding(30.dp),
|
|
||||||
fontSize = 15.sp
|
|
||||||
)
|
|
||||||
Row(
|
|
||||||
modifier =
|
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(10.dp)
|
|
||||||
} else {
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(30.dp)
|
|
||||||
},
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly
|
|
||||||
) {
|
|
||||||
TextButton(onClick = {
|
|
||||||
setLocationDisclosureShown()
|
|
||||||
}) {
|
|
||||||
Text(stringResource(id = R.string.no_thanks))
|
|
||||||
}
|
|
||||||
TextButton(modifier = Modifier.focusRequester(focusRequester), onClick = {
|
|
||||||
openSettings()
|
|
||||||
setLocationDisclosureShown()
|
|
||||||
}) {
|
|
||||||
Text(stringResource(id = R.string.turn_on))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showAuthPrompt) {
|
if (uiState.tunnels.isEmpty() && uiState.isLocationDisclosureShown) {
|
||||||
AuthorizationPrompt(
|
Column(
|
||||||
onSuccess = {
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
showAuthPrompt = false
|
verticalArrangement = Arrangement.Center,
|
||||||
exportAllConfigs()
|
modifier = Modifier.fillMaxSize().padding(padding),
|
||||||
},
|
) {
|
||||||
onError = { error ->
|
Text(
|
||||||
showSnackbarMessage(error)
|
stringResource(R.string.one_tunnel_required),
|
||||||
showAuthPrompt = false
|
textAlign = TextAlign.Center,
|
||||||
},
|
modifier = Modifier.padding(15.dp),
|
||||||
onFailure = {
|
fontStyle = FontStyle.Italic,
|
||||||
showAuthPrompt = false
|
|
||||||
showSnackbarMessage(context.getString(R.string.authentication_failed))
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (tunnels.isEmpty()) {
|
if (uiState.isLocationDisclosureShown && uiState.tunnels.isNotEmpty()) {
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
verticalArrangement = Arrangement.Center,
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(padding)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.one_tunnel_required),
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
modifier = Modifier.padding(15.dp),
|
|
||||||
fontStyle = FontStyle.Italic
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier.fillMaxSize().padding(padding).verticalScroll(scrollState).clickable(
|
||||||
.fillMaxSize()
|
indication = null,
|
||||||
.verticalScroll(scrollState)
|
interactionSource = interactionSource,
|
||||||
.clickable(indication = null, interactionSource = interactionSource) {
|
) {
|
||||||
focusManager.clearFocus()
|
focusManager.clearFocus()
|
||||||
}
|
},
|
||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
tonalElevation = 2.dp,
|
tonalElevation = 2.dp,
|
||||||
@@ -306,179 +342,185 @@ fun SettingsScreen(
|
|||||||
shape = RoundedCornerShape(12.dp),
|
shape = RoundedCornerShape(12.dp),
|
||||||
color = MaterialTheme.colorScheme.surface,
|
color = MaterialTheme.colorScheme.surface,
|
||||||
modifier =
|
modifier =
|
||||||
(
|
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
Modifier.height(IntrinsicSize.Min)
|
||||||
Modifier
|
|
||||||
.height(IntrinsicSize.Min)
|
|
||||||
.fillMaxWidth(fillMaxWidth)
|
.fillMaxWidth(fillMaxWidth)
|
||||||
.padding(top = 10.dp)
|
.padding(top = 10.dp)
|
||||||
} else {
|
} else {
|
||||||
Modifier
|
Modifier.fillMaxWidth(fillMaxWidth).padding(top = 20.dp)
|
||||||
.fillMaxWidth(fillMaxWidth)
|
})
|
||||||
.padding(top = 60.dp)
|
.padding(bottom = 10.dp),
|
||||||
}
|
|
||||||
).padding(bottom = 10.dp)
|
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier = Modifier.padding(15.dp)
|
modifier = Modifier.padding(15.dp),
|
||||||
) {
|
) {
|
||||||
SectionTitle(
|
SectionTitle(
|
||||||
title = stringResource(id = R.string.auto_tunneling),
|
title = stringResource(id = R.string.auto_tunneling),
|
||||||
padding = screenPadding
|
padding = screenPadding,
|
||||||
)
|
)
|
||||||
ConfigurationToggle(
|
ConfigurationToggle(
|
||||||
stringResource(id = R.string.tunnel_on_wifi),
|
stringResource(id = R.string.tunnel_on_wifi),
|
||||||
enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled),
|
enabled =
|
||||||
checked = settings.isTunnelOnWifiEnabled,
|
!(uiState.settings.isAutoTunnelEnabled ||
|
||||||
|
uiState.settings.isAlwaysOnVpnEnabled),
|
||||||
|
checked = uiState.settings.isTunnelOnWifiEnabled,
|
||||||
padding = screenPadding,
|
padding = screenPadding,
|
||||||
onCheckChanged = {
|
onCheckChanged = { viewModel.onToggleTunnelOnWifi() },
|
||||||
scope.launch {
|
modifier =
|
||||||
viewModel.onToggleTunnelOnWifi()
|
if (uiState.settings.isAutoTunnelEnabled) Modifier
|
||||||
}
|
else
|
||||||
},
|
Modifier.focusRequester(focusRequester).focusProperties {
|
||||||
modifier = Modifier.focusRequester(focusRequester)
|
down = focusRequester2
|
||||||
|
},
|
||||||
)
|
)
|
||||||
AnimatedVisibility(visible = settings.isTunnelOnWifiEnabled) {
|
AnimatedVisibility(visible = uiState.settings.isTunnelOnWifiEnabled) {
|
||||||
Column {
|
Column {
|
||||||
FlowRow(
|
FlowRow(
|
||||||
modifier = Modifier
|
modifier = Modifier.padding(screenPadding).fillMaxWidth(),
|
||||||
.padding(screenPadding)
|
horizontalArrangement = Arrangement.spacedBy(5.dp),
|
||||||
.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(5.dp)
|
|
||||||
) {
|
) {
|
||||||
trustedSSIDs.forEach { ssid ->
|
uiState.settings.trustedNetworkSSIDs.forEach { ssid ->
|
||||||
ClickableIconButton(
|
ClickableIconButton(
|
||||||
onIconClick = {
|
onClick = {
|
||||||
scope.launch {
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
viewModel.onDeleteTrustedSSID(ssid)
|
viewModel.onDeleteTrustedSSID(ssid)
|
||||||
|
focusRequester2.requestFocus()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onIconClick = { viewModel.onDeleteTrustedSSID(ssid) },
|
||||||
text = ssid,
|
text = ssid,
|
||||||
icon = Icons.Filled.Close,
|
icon = Icons.Filled.Close,
|
||||||
enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled)
|
enabled =
|
||||||
|
!(uiState.settings.isAutoTunnelEnabled ||
|
||||||
|
uiState.settings.isAlwaysOnVpnEnabled),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (trustedSSIDs.isEmpty()) {
|
if (uiState.settings.trustedNetworkSSIDs.isEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.none),
|
stringResource(R.string.none),
|
||||||
fontStyle = FontStyle.Italic,
|
fontStyle = FontStyle.Italic,
|
||||||
color = Color.Gray
|
color = Color.Gray,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled),
|
enabled =
|
||||||
|
!(uiState.settings.isAutoTunnelEnabled ||
|
||||||
|
uiState.settings.isAlwaysOnVpnEnabled),
|
||||||
value = currentText,
|
value = currentText,
|
||||||
onValueChange = { currentText = it },
|
onValueChange = { currentText = it },
|
||||||
label = { Text(stringResource(R.string.add_trusted_ssid)) },
|
label = { Text(stringResource(R.string.add_trusted_ssid)) },
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier.padding(
|
||||||
.padding(start = screenPadding, top = 5.dp, bottom = 10.dp)
|
start = screenPadding,
|
||||||
.onFocusChanged {
|
top = 5.dp,
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
bottom = 10.dp,
|
||||||
keyboardController?.hide()
|
)
|
||||||
}
|
.focusRequester(focusRequester2),
|
||||||
},
|
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
keyboardOptions =
|
keyboardOptions =
|
||||||
KeyboardOptions(
|
KeyboardOptions(
|
||||||
capitalization = KeyboardCapitalization.None,
|
capitalization = KeyboardCapitalization.None,
|
||||||
imeAction = ImeAction.Done
|
imeAction = ImeAction.Done,
|
||||||
),
|
),
|
||||||
keyboardActions =
|
keyboardActions = KeyboardActions(onDone = { saveTrustedSSID() }),
|
||||||
KeyboardActions(
|
|
||||||
onDone = {
|
|
||||||
saveTrustedSSID()
|
|
||||||
}
|
|
||||||
),
|
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
if (currentText != "") {
|
if (currentText != "") {
|
||||||
IconButton(onClick = { saveTrustedSSID() }) {
|
IconButton(onClick = { saveTrustedSSID() }) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.Add,
|
imageVector = Icons.Outlined.Add,
|
||||||
contentDescription =
|
contentDescription =
|
||||||
if (currentText == "") {
|
if (currentText == "") {
|
||||||
stringResource(
|
stringResource(
|
||||||
id = R.string.trusted_ssid_empty_description
|
id =
|
||||||
)
|
R.string
|
||||||
} else {
|
.trusted_ssid_empty_description,
|
||||||
stringResource(
|
)
|
||||||
id = R.string.trusted_ssid_value_description
|
} else {
|
||||||
)
|
stringResource(
|
||||||
},
|
id =
|
||||||
tint = MaterialTheme.colorScheme.primary
|
R.string
|
||||||
|
.trusted_ssid_value_description,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ConfigurationToggle(
|
ConfigurationToggle(
|
||||||
stringResource(R.string.tunnel_mobile_data),
|
stringResource(R.string.tunnel_mobile_data),
|
||||||
enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled),
|
enabled =
|
||||||
checked = settings.isTunnelOnMobileDataEnabled,
|
!(uiState.settings.isAutoTunnelEnabled ||
|
||||||
|
uiState.settings.isAlwaysOnVpnEnabled),
|
||||||
|
checked = uiState.settings.isTunnelOnMobileDataEnabled,
|
||||||
padding = screenPadding,
|
padding = screenPadding,
|
||||||
onCheckChanged = {
|
onCheckChanged = { viewModel.onToggleTunnelOnMobileData() },
|
||||||
scope.launch {
|
|
||||||
viewModel.onToggleTunnelOnMobileData()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
ConfigurationToggle(
|
ConfigurationToggle(
|
||||||
stringResource(id = R.string.tunnel_on_ethernet),
|
stringResource(id = R.string.tunnel_on_ethernet),
|
||||||
enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled),
|
enabled =
|
||||||
checked = settings.isTunnelOnEthernetEnabled,
|
!(uiState.settings.isAutoTunnelEnabled ||
|
||||||
|
uiState.settings.isAlwaysOnVpnEnabled),
|
||||||
|
checked = uiState.settings.isTunnelOnEthernetEnabled,
|
||||||
padding = screenPadding,
|
padding = screenPadding,
|
||||||
onCheckChanged = {
|
onCheckChanged = { viewModel.onToggleTunnelOnEthernet() },
|
||||||
scope.launch {
|
|
||||||
viewModel.onToggleTunnelOnEthernet()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
ConfigurationToggle(
|
ConfigurationToggle(
|
||||||
stringResource(R.string.battery_saver),
|
stringResource(R.string.battery_saver),
|
||||||
enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled),
|
enabled =
|
||||||
checked = settings.isBatterySaverEnabled,
|
!(uiState.settings.isAutoTunnelEnabled ||
|
||||||
|
uiState.settings.isAlwaysOnVpnEnabled),
|
||||||
|
checked = uiState.settings.isBatterySaverEnabled,
|
||||||
padding = screenPadding,
|
padding = screenPadding,
|
||||||
onCheckChanged = {
|
onCheckChanged = { viewModel.onToggleBatterySaver() },
|
||||||
scope.launch {
|
|
||||||
viewModel.onToggleBatterySaver()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
(if (!uiState.settings.isAutoTunnelEnabled) Modifier
|
||||||
.fillMaxSize()
|
else
|
||||||
.padding(top = 5.dp),
|
Modifier.focusRequester(
|
||||||
horizontalArrangement = Arrangement.Center
|
focusRequester,
|
||||||
|
))
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(top = 5.dp),
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
) {
|
) {
|
||||||
TextButton(
|
TextButton(
|
||||||
enabled = !settings.isAlwaysOnVpnEnabled,
|
enabled = !uiState.settings.isAlwaysOnVpnEnabled,
|
||||||
onClick = {
|
onClick = {
|
||||||
if (!isAllAutoTunnelPermissionsEnabled() && settings.isTunnelOnWifiEnabled) {
|
if (
|
||||||
val message =
|
uiState.settings.isTunnelOnWifiEnabled &&
|
||||||
if (!isBackgroundLocationGranted) {
|
!uiState.settings.isAutoTunnelEnabled
|
||||||
context.getString(R.string.background_location_required)
|
) {
|
||||||
} else if (viewModel.isLocationServicesNeeded()) {
|
when (false) {
|
||||||
context.getString(R.string.location_services_required)
|
isBackgroundLocationGranted ->
|
||||||
} else {
|
showSnackbarMessage(
|
||||||
context.getString(R.string.precise_location_required)
|
Event.Error.BackgroundLocationRequired.message
|
||||||
|
)
|
||||||
|
fineLocationState.status.isGranted ->
|
||||||
|
showSnackbarMessage(
|
||||||
|
Event.Error.PreciseLocationRequired.message
|
||||||
|
)
|
||||||
|
viewModel.isLocationEnabled(context) ->
|
||||||
|
showLocationServicesAlertDialog = true
|
||||||
|
else -> {
|
||||||
|
handleAutoTunnelToggle()
|
||||||
}
|
}
|
||||||
showSnackbarMessage(message)
|
|
||||||
} else {
|
|
||||||
scope.launch {
|
|
||||||
viewModel.toggleAutoTunnel()
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
handleAutoTunnelToggle()
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
) {
|
) {
|
||||||
val autoTunnelButtonText =
|
val autoTunnelButtonText =
|
||||||
if (settings.isAutoTunnelEnabled) {
|
if (uiState.settings.isAutoTunnelEnabled) {
|
||||||
stringResource(R.string.disable_auto_tunnel)
|
stringResource(R.string.disable_auto_tunnel)
|
||||||
} else {
|
} else {
|
||||||
stringResource(id = R.string.enable_auto_tunnel)
|
stringResource(id = R.string.enable_auto_tunnel)
|
||||||
@@ -494,94 +536,79 @@ fun SettingsScreen(
|
|||||||
shadowElevation = 2.dp,
|
shadowElevation = 2.dp,
|
||||||
shape = RoundedCornerShape(12.dp),
|
shape = RoundedCornerShape(12.dp),
|
||||||
color = MaterialTheme.colorScheme.surface,
|
color = MaterialTheme.colorScheme.surface,
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth(fillMaxWidth).padding(vertical = 10.dp),
|
||||||
.fillMaxWidth(fillMaxWidth)
|
|
||||||
.padding(vertical = 10.dp)
|
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier = Modifier.padding(15.dp)
|
modifier = Modifier.padding(15.dp),
|
||||||
) {
|
) {
|
||||||
SectionTitle(
|
SectionTitle(
|
||||||
title = stringResource(id = R.string.kernel),
|
title = stringResource(id = R.string.kernel),
|
||||||
padding = screenPadding
|
padding = screenPadding,
|
||||||
)
|
)
|
||||||
ConfigurationToggle(
|
ConfigurationToggle(
|
||||||
stringResource(R.string.use_kernel),
|
stringResource(R.string.use_kernel),
|
||||||
enabled = !(
|
enabled =
|
||||||
settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled ||
|
!(uiState.settings.isAutoTunnelEnabled ||
|
||||||
(vpnState.value == Tunnel.State.UP)
|
uiState.settings.isAlwaysOnVpnEnabled ||
|
||||||
),
|
(uiState.vpnState.status == Tunnel.State.UP)),
|
||||||
checked = settings.isKernelEnabled,
|
checked = uiState.settings.isKernelEnabled,
|
||||||
padding = screenPadding,
|
padding = screenPadding,
|
||||||
onCheckChanged = {
|
onCheckChanged = {
|
||||||
scope.launch {
|
viewModel.onToggleKernelMode().let {
|
||||||
try {
|
when (it) {
|
||||||
viewModel.onToggleKernelMode()
|
is Result.Error -> showSnackbarMessage(it.error.message)
|
||||||
} catch (e: WgTunnelException) {
|
is Result.Success -> {}
|
||||||
showSnackbarMessage(e.message)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
Surface(
|
Surface(
|
||||||
tonalElevation = 2.dp,
|
tonalElevation = 2.dp,
|
||||||
shadowElevation = 2.dp,
|
shadowElevation = 2.dp,
|
||||||
shape = RoundedCornerShape(12.dp),
|
shape = RoundedCornerShape(12.dp),
|
||||||
color = MaterialTheme.colorScheme.surface,
|
color = MaterialTheme.colorScheme.surface,
|
||||||
modifier = Modifier
|
modifier =
|
||||||
.fillMaxWidth(fillMaxWidth).padding(vertical = 10.dp)
|
Modifier.fillMaxWidth(fillMaxWidth)
|
||||||
.padding(bottom = 140.dp)
|
.padding(vertical = 10.dp)
|
||||||
|
.padding(bottom = 140.dp),
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier = Modifier.padding(15.dp)
|
modifier = Modifier.padding(15.dp),
|
||||||
) {
|
) {
|
||||||
SectionTitle(
|
SectionTitle(
|
||||||
title = stringResource(id = R.string.other),
|
title = stringResource(id = R.string.other),
|
||||||
padding = screenPadding
|
padding = screenPadding,
|
||||||
)
|
)
|
||||||
ConfigurationToggle(
|
ConfigurationToggle(
|
||||||
stringResource(R.string.always_on_vpn_support),
|
stringResource(R.string.always_on_vpn_support),
|
||||||
enabled = !settings.isAutoTunnelEnabled,
|
enabled = !uiState.settings.isAutoTunnelEnabled,
|
||||||
checked = settings.isAlwaysOnVpnEnabled,
|
checked = uiState.settings.isAlwaysOnVpnEnabled,
|
||||||
padding = screenPadding,
|
padding = screenPadding,
|
||||||
onCheckChanged = {
|
onCheckChanged = { viewModel.onToggleAlwaysOnVPN() },
|
||||||
scope.launch {
|
|
||||||
viewModel.onToggleAlwaysOnVPN()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
ConfigurationToggle(
|
ConfigurationToggle(
|
||||||
stringResource(R.string.enabled_app_shortcuts),
|
stringResource(R.string.enabled_app_shortcuts),
|
||||||
enabled = true,
|
enabled = true,
|
||||||
checked = settings.isShortcutsEnabled,
|
checked = uiState.settings.isShortcutsEnabled,
|
||||||
padding = screenPadding,
|
padding = screenPadding,
|
||||||
onCheckChanged = {
|
onCheckChanged = { viewModel.onToggleShortcutsEnabled() },
|
||||||
scope.launch {
|
|
||||||
viewModel.onToggleShortcutsEnabled()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier =
|
modifier = Modifier.fillMaxSize().padding(top = 5.dp),
|
||||||
Modifier
|
horizontalArrangement = Arrangement.Center,
|
||||||
.fillMaxSize()
|
|
||||||
.padding(top = 5.dp),
|
|
||||||
horizontalArrangement = Arrangement.Center
|
|
||||||
) {
|
) {
|
||||||
TextButton(
|
TextButton(
|
||||||
enabled = !didExportFiles,
|
enabled = !didExportFiles,
|
||||||
onClick = {
|
onClick = { showAuthPrompt = true },
|
||||||
showAuthPrompt = true
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
Text(stringResource(R.string.export_configs))
|
Text(stringResource(R.string.export_configs))
|
||||||
}
|
}
|
||||||
@@ -589,8 +616,9 @@ fun SettingsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
Spacer(modifier = Modifier.weight(.17f))
|
Spacer(modifier = Modifier.weight(.17f))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.settings
|
||||||
|
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
|
||||||
|
|
||||||
|
data class SettingsUiState(
|
||||||
|
val settings: Settings = Settings(),
|
||||||
|
val tunnels: List<TunnelConfig> = emptyList(),
|
||||||
|
val vpnState: VpnState = VpnState(),
|
||||||
|
val isLocationDisclosureShown: Boolean = true,
|
||||||
|
val isBatteryOptimizeDisableShown: Boolean = false,
|
||||||
|
val loading: Boolean = true
|
||||||
|
)
|
||||||
+131
-131
@@ -3,195 +3,195 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings
|
|||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.location.LocationManager
|
import android.location.LocationManager
|
||||||
import android.os.Build
|
import androidx.core.location.LocationManagerCompat
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.wireguard.android.util.RootShell
|
import com.wireguard.android.util.RootShell
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.datastore.DataStoreManager
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||||
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Event
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import javax.inject.Inject
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.flow.filter
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class SettingsViewModel
|
class SettingsViewModel
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
private val application: Application,
|
private val application: Application,
|
||||||
private val tunnelRepo: TunnelConfigDao,
|
private val tunnelConfigRepository: TunnelConfigRepository,
|
||||||
private val settingsRepo: SettingsDoa,
|
private val settingsRepository: SettingsRepository,
|
||||||
val dataStoreManager: DataStoreManager,
|
private val dataStoreManager: DataStoreManager,
|
||||||
private val rootShell: RootShell,
|
private val rootShell: RootShell,
|
||||||
private val vpnService: VpnService
|
private val vpnService: VpnService
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _trustedSSIDs = MutableStateFlow(emptyList<String>())
|
val uiState =
|
||||||
val trustedSSIDs = _trustedSSIDs.asStateFlow()
|
combine(
|
||||||
private val _settings = MutableStateFlow(Settings())
|
settingsRepository.getSettingsFlow(),
|
||||||
val settings get() = _settings.asStateFlow()
|
tunnelConfigRepository.getTunnelConfigsFlow(),
|
||||||
val vpnState get() = vpnService.state
|
vpnService.vpnState,
|
||||||
val tunnels get() = tunnelRepo.getAllFlow()
|
dataStoreManager.preferencesFlow,
|
||||||
val disclosureShown = dataStoreManager.locationDisclosureFlow
|
) { settings, tunnels, tunnelState, preferences ->
|
||||||
|
SettingsUiState(
|
||||||
init {
|
settings,
|
||||||
isLocationServicesEnabled()
|
tunnels,
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
tunnelState,
|
||||||
settingsRepo.getAllFlow().filter { it.isNotEmpty() }.collect {
|
preferences?.get(DataStoreManager.LOCATION_DISCLOSURE_SHOWN) ?: false,
|
||||||
val settings = it.first()
|
preferences?.get(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN) ?: false,
|
||||||
_settings.emit(settings)
|
false
|
||||||
_trustedSSIDs.emit(settings.trustedNetworkSSIDs.toList())
|
)
|
||||||
}
|
}
|
||||||
}
|
.stateIn(
|
||||||
}
|
viewModelScope,
|
||||||
suspend fun onSaveTrustedSSID(ssid: String) {
|
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
|
||||||
|
SettingsUiState(),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun onSaveTrustedSSID(ssid: String): Result<Unit> {
|
||||||
val trimmed = ssid.trim()
|
val trimmed = ssid.trim()
|
||||||
if (!_settings.value.trustedNetworkSSIDs.contains(trimmed)) {
|
return if (!uiState.value.settings.trustedNetworkSSIDs.contains(trimmed)) {
|
||||||
_settings.value.trustedNetworkSSIDs.add(trimmed)
|
uiState.value.settings.trustedNetworkSSIDs.add(trimmed)
|
||||||
settingsRepo.save(_settings.value)
|
saveSettings(uiState.value.settings)
|
||||||
|
Result.Success(Unit)
|
||||||
} else {
|
} else {
|
||||||
throw WgTunnelException("SSID already exists.")
|
Result.Error(Event.Error.SsidConflict)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun onToggleTunnelOnMobileData() {
|
fun setLocationDisclosureShown() =
|
||||||
settingsRepo.save(
|
viewModelScope.launch {
|
||||||
_settings.value.copy(
|
dataStoreManager.saveToDataStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN, true)
|
||||||
isTunnelOnMobileDataEnabled = !_settings.value.isTunnelOnMobileDataEnabled
|
}
|
||||||
)
|
|
||||||
|
fun setBatteryOptimizeDisableShown() =
|
||||||
|
viewModelScope.launch {
|
||||||
|
dataStoreManager.saveToDataStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onToggleTunnelOnMobileData() {
|
||||||
|
saveSettings(
|
||||||
|
uiState.value.settings.copy(
|
||||||
|
isTunnelOnMobileDataEnabled = !uiState.value.settings.isTunnelOnMobileDataEnabled,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun onDeleteTrustedSSID(ssid: String) {
|
fun onDeleteTrustedSSID(ssid: String) {
|
||||||
_settings.value.trustedNetworkSSIDs.remove(ssid)
|
saveSettings(
|
||||||
settingsRepo.save(_settings.value)
|
uiState.value.settings.copy(
|
||||||
|
trustedNetworkSSIDs =
|
||||||
|
(uiState.value.settings.trustedNetworkSSIDs - ssid).toMutableList(),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun emitFirstTunnelAsDefault() =
|
private suspend fun getDefaultTunnelOrFirst(): String {
|
||||||
viewModelScope.async {
|
return uiState.value.settings.defaultTunnel
|
||||||
_settings.emit(_settings.value.copy(defaultTunnel = getFirstTunnelConfig().toString()))
|
?: tunnelConfigRepository.getAll().first().toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun toggleAutoTunnel() {
|
fun toggleAutoTunnel() =
|
||||||
if (_settings.value.isAutoTunnelEnabled) {
|
viewModelScope.launch {
|
||||||
ServiceManager.stopWatcherService(application)
|
val isAutoTunnelEnabled = uiState.value.settings.isAutoTunnelEnabled
|
||||||
} else {
|
var isAutoTunnelPaused = uiState.value.settings.isAutoTunnelPaused
|
||||||
if (_settings.value.defaultTunnel == null) {
|
|
||||||
emitFirstTunnelAsDefault().await()
|
if (isAutoTunnelEnabled) {
|
||||||
|
ServiceManager.stopWatcherService(application)
|
||||||
|
} else {
|
||||||
|
ServiceManager.startWatcherService(application)
|
||||||
|
isAutoTunnelPaused = false
|
||||||
}
|
}
|
||||||
val defaultTunnel = _settings.value.defaultTunnel
|
saveSettings(
|
||||||
ServiceManager.startWatcherService(application, defaultTunnel!!)
|
uiState.value.settings.copy(
|
||||||
|
isAutoTunnelEnabled = !isAutoTunnelEnabled,
|
||||||
|
isAutoTunnelPaused = isAutoTunnelPaused,
|
||||||
|
defaultTunnel = getDefaultTunnelOrFirst(),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
settingsRepo.save(
|
|
||||||
_settings.value.copy(
|
|
||||||
isAutoTunnelEnabled = !_settings.value.isAutoTunnelEnabled
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun getFirstTunnelConfig(): TunnelConfig {
|
fun onToggleAlwaysOnVPN() =
|
||||||
return tunnelRepo.getAll().first()
|
viewModelScope.launch {
|
||||||
}
|
val updatedSettings =
|
||||||
|
uiState.value.settings.copy(
|
||||||
suspend fun onToggleAlwaysOnVPN() {
|
isAlwaysOnVpnEnabled = !uiState.value.settings.isAlwaysOnVpnEnabled,
|
||||||
if (_settings.value.defaultTunnel == null) {
|
defaultTunnel = getDefaultTunnelOrFirst(),
|
||||||
emitFirstTunnelAsDefault().await()
|
)
|
||||||
|
saveSettings(updatedSettings)
|
||||||
}
|
}
|
||||||
val updatedSettings =
|
|
||||||
_settings.value.copy(
|
|
||||||
isAlwaysOnVpnEnabled = !_settings.value.isAlwaysOnVpnEnabled
|
|
||||||
)
|
|
||||||
emitSettings(updatedSettings)
|
|
||||||
saveSettings(updatedSettings)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun emitSettings(settings: Settings) {
|
private fun saveSettings(settings: Settings) =
|
||||||
_settings.emit(
|
viewModelScope.launch { settingsRepository.save(settings) }
|
||||||
settings
|
|
||||||
|
fun onToggleTunnelOnEthernet() {
|
||||||
|
saveSettings(
|
||||||
|
uiState.value.settings.copy(
|
||||||
|
isTunnelOnEthernetEnabled = !uiState.value.settings.isTunnelOnEthernetEnabled,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun saveSettings(settings: Settings) {
|
fun isLocationEnabled(context: Context): Boolean {
|
||||||
settingsRepo.save(settings)
|
val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||||
|
return LocationManagerCompat.isLocationEnabled(locationManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun onToggleTunnelOnEthernet() {
|
fun onToggleShortcutsEnabled() {
|
||||||
if (_settings.value.defaultTunnel == null) {
|
saveSettings(
|
||||||
emitFirstTunnelAsDefault().await()
|
uiState.value.settings.copy(
|
||||||
}
|
isShortcutsEnabled = !uiState.value.settings.isShortcutsEnabled,
|
||||||
_settings.emit(
|
),
|
||||||
_settings.value.copy(
|
|
||||||
isTunnelOnEthernetEnabled = !_settings.value.isTunnelOnEthernetEnabled
|
|
||||||
)
|
|
||||||
)
|
|
||||||
settingsRepo.save(_settings.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isLocationServicesEnabled(): Boolean {
|
|
||||||
val locationManager =
|
|
||||||
application.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
|
||||||
return locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isLocationServicesNeeded(): Boolean {
|
|
||||||
return (!isLocationServicesEnabled() && Build.VERSION.SDK_INT > Build.VERSION_CODES.P)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun onToggleShortcutsEnabled() {
|
|
||||||
settingsRepo.save(
|
|
||||||
_settings.value.copy(
|
|
||||||
isShortcutsEnabled = !_settings.value.isShortcutsEnabled
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun onToggleBatterySaver() {
|
fun onToggleBatterySaver() {
|
||||||
settingsRepo.save(
|
saveSettings(
|
||||||
_settings.value.copy(
|
uiState.value.settings.copy(
|
||||||
isBatterySaverEnabled = !_settings.value.isBatterySaverEnabled
|
isBatterySaverEnabled = !uiState.value.settings.isBatterySaverEnabled,
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun saveKernelMode(on: Boolean) {
|
private fun saveKernelMode(on: Boolean) {
|
||||||
settingsRepo.save(
|
saveSettings(
|
||||||
_settings.value.copy(
|
uiState.value.settings.copy(
|
||||||
isKernelEnabled = on
|
isKernelEnabled = on,
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun onToggleKernelMode() {
|
fun onToggleTunnelOnWifi() {
|
||||||
if (!_settings.value.isKernelEnabled) {
|
saveSettings(
|
||||||
|
uiState.value.settings.copy(
|
||||||
|
isTunnelOnWifiEnabled = !uiState.value.settings.isTunnelOnWifiEnabled,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onToggleKernelMode(): Result<Unit> {
|
||||||
|
if (!uiState.value.settings.isKernelEnabled) {
|
||||||
try {
|
try {
|
||||||
rootShell.start()
|
rootShell.start()
|
||||||
Timber.d("Root shell accepted!")
|
Timber.d("Root shell accepted!")
|
||||||
saveKernelMode(on = true)
|
saveKernelMode(on = true)
|
||||||
} catch (e: RootShell.RootShellException) {
|
} catch (e: RootShell.RootShellException) {
|
||||||
saveKernelMode(on = false)
|
saveKernelMode(on = false)
|
||||||
throw WgTunnelException("Root shell denied!")
|
return Result.Error(Event.Error.RootDenied)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
saveKernelMode(on = false)
|
saveKernelMode(on = false)
|
||||||
}
|
}
|
||||||
}
|
return Result.Success(Unit)
|
||||||
|
|
||||||
suspend fun onToggleTunnelOnWifi() {
|
|
||||||
settingsRepo.save(
|
|
||||||
_settings.value.copy(
|
|
||||||
isTunnelOnWifiEnabled = !_settings.value.isTunnelOnWifiEnabled
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+100
-71
@@ -20,10 +20,10 @@ import androidx.compose.foundation.rememberScrollState
|
|||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.rounded.ArrowForward
|
import androidx.compose.material.icons.automirrored.rounded.ArrowForward
|
||||||
import androidx.compose.material.icons.rounded.Book
|
import androidx.compose.material.icons.rounded.Book
|
||||||
import androidx.compose.material.icons.rounded.Mail
|
import androidx.compose.material.icons.rounded.Mail
|
||||||
import androidx.compose.material3.Divider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
@@ -40,6 +40,7 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.res.vectorResource
|
import androidx.compose.ui.res.vectorResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextDecoration
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -48,51 +49,64 @@ import androidx.core.content.ContextCompat.startActivity
|
|||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.Constants
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsViewModel
|
import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Event
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SupportScreen(
|
fun SupportScreen(
|
||||||
viewModel: SettingsViewModel = hiltViewModel(),
|
|
||||||
padding: PaddingValues,
|
padding: PaddingValues,
|
||||||
|
viewModel: SupportViewModel = hiltViewModel(),
|
||||||
|
showSnackbarMessage: (String) -> Unit,
|
||||||
focusRequester: FocusRequester
|
focusRequester: FocusRequester
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val fillMaxWidth = .85f
|
val fillMaxWidth = .85f
|
||||||
|
|
||||||
val settings by viewModel.settings.collectAsStateWithLifecycle()
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
fun openWebPage(url: String) {
|
fun openWebPage(url: String) {
|
||||||
val webpage: Uri = Uri.parse(url)
|
try {
|
||||||
val intent = Intent(Intent.ACTION_VIEW, webpage)
|
val webpage: Uri = Uri.parse(url)
|
||||||
context.startActivity(intent)
|
val intent = Intent(Intent.ACTION_VIEW, webpage)
|
||||||
|
context.startActivity(intent)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
showSnackbarMessage(Event.Error.Exception(e).message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun launchEmail() {
|
fun launchEmail() {
|
||||||
val intent =
|
try {
|
||||||
Intent(Intent.ACTION_SEND).apply {
|
val intent =
|
||||||
type = Constants.EMAIL_MIME_TYPE
|
Intent(Intent.ACTION_SENDTO).apply {
|
||||||
putExtra(Intent.EXTRA_EMAIL, context.getString(R.string.my_email))
|
type = Constants.EMAIL_MIME_TYPE
|
||||||
putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.email_subject))
|
putExtra(Intent.EXTRA_EMAIL, arrayOf(context.getString(R.string.my_email)))
|
||||||
}
|
putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.email_subject))
|
||||||
startActivity(
|
}
|
||||||
context,
|
startActivity(
|
||||||
createChooser(intent, context.getString(R.string.email_chooser)),
|
context,
|
||||||
null
|
createChooser(intent, context.getString(R.string.email_chooser)),
|
||||||
)
|
null,
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
showSnackbarMessage(Event.Error.Exception(e).message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uiState.loading) {
|
||||||
|
LoadingScreen()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier.fillMaxSize().padding(padding)
|
||||||
.fillMaxSize()
|
.verticalScroll(rememberScrollState())
|
||||||
.verticalScroll(rememberScrollState())
|
.focusable()
|
||||||
.focusable()
|
|
||||||
.padding(padding)
|
|
||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
tonalElevation = 2.dp,
|
tonalElevation = 2.dp,
|
||||||
@@ -100,124 +114,139 @@ fun SupportScreen(
|
|||||||
shape = RoundedCornerShape(12.dp),
|
shape = RoundedCornerShape(12.dp),
|
||||||
color = MaterialTheme.colorScheme.surface,
|
color = MaterialTheme.colorScheme.surface,
|
||||||
modifier =
|
modifier =
|
||||||
(
|
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
Modifier.height(IntrinsicSize.Min)
|
||||||
Modifier
|
|
||||||
.height(IntrinsicSize.Min)
|
|
||||||
.fillMaxWidth(fillMaxWidth)
|
.fillMaxWidth(fillMaxWidth)
|
||||||
.padding(top = 10.dp)
|
.padding(top = 10.dp)
|
||||||
} else {
|
} else {
|
||||||
Modifier
|
Modifier.fillMaxWidth(fillMaxWidth).padding(top = 20.dp)
|
||||||
.fillMaxWidth(fillMaxWidth)
|
})
|
||||||
.padding(top = 20.dp)
|
.padding(bottom = 25.dp),
|
||||||
}
|
|
||||||
).padding(bottom = 25.dp)
|
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(20.dp)) {
|
Column(modifier = Modifier.padding(20.dp)) {
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.thank_you),
|
stringResource(R.string.thank_you),
|
||||||
textAlign = TextAlign.Start,
|
textAlign = TextAlign.Start,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
modifier = Modifier.padding(bottom = 20.dp),
|
modifier = Modifier.padding(bottom = 20.dp),
|
||||||
fontSize = 16.sp
|
fontSize = 16.sp,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
stringResource(id = R.string.support_help_text),
|
stringResource(id = R.string.support_help_text),
|
||||||
textAlign = TextAlign.Start,
|
textAlign = TextAlign.Start,
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
modifier = Modifier.padding(bottom = 20.dp)
|
modifier = Modifier.padding(bottom = 20.dp),
|
||||||
)
|
)
|
||||||
TextButton(onClick = {
|
TextButton(
|
||||||
openWebPage(context.resources.getString(R.string.docs_url))
|
onClick = { openWebPage(context.resources.getString(R.string.docs_url)) },
|
||||||
}, modifier = Modifier.padding(vertical = 5.dp).focusRequester(focusRequester)) {
|
modifier = Modifier.padding(vertical = 5.dp).focusRequester(focusRequester),
|
||||||
|
) {
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
Row {
|
Row {
|
||||||
Icon(Icons.Rounded.Book, stringResource(id = R.string.docs))
|
Icon(Icons.Rounded.Book, stringResource(id = R.string.docs))
|
||||||
Text(
|
Text(
|
||||||
stringResource(id = R.string.docs_description),
|
stringResource(id = R.string.docs_description),
|
||||||
textAlign = TextAlign.Justify,
|
textAlign = TextAlign.Justify,
|
||||||
modifier = Modifier.padding(start = 10.dp)
|
modifier = Modifier.padding(start = 10.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
Icon(
|
||||||
|
Icons.AutoMirrored.Rounded.ArrowForward,
|
||||||
|
stringResource(id = R.string.go)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
|
HorizontalDivider(
|
||||||
|
thickness = 0.5.dp,
|
||||||
|
color = MaterialTheme.colorScheme.onBackground
|
||||||
|
)
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = { openWebPage(context.resources.getString(R.string.discord_url)) },
|
onClick = { openWebPage(context.resources.getString(R.string.discord_url)) },
|
||||||
modifier = Modifier.padding(vertical = 5.dp)
|
modifier = Modifier.padding(vertical = 5.dp),
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
Row {
|
Row {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = ImageVector.vectorResource(R.drawable.discord),
|
imageVector = ImageVector.vectorResource(R.drawable.discord),
|
||||||
stringResource(
|
stringResource(id = R.string.discord),
|
||||||
id = R.string.discord
|
Modifier.size(25.dp),
|
||||||
),
|
|
||||||
Modifier.size(25.dp)
|
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
stringResource(id = R.string.discord_description),
|
stringResource(id = R.string.discord_description),
|
||||||
textAlign = TextAlign.Justify,
|
textAlign = TextAlign.Justify,
|
||||||
modifier = Modifier.padding(start = 10.dp)
|
modifier = Modifier.padding(start = 10.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
Icon(
|
||||||
|
Icons.AutoMirrored.Rounded.ArrowForward,
|
||||||
|
stringResource(id = R.string.go)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
|
HorizontalDivider(
|
||||||
|
thickness = 0.5.dp,
|
||||||
|
color = MaterialTheme.colorScheme.onBackground
|
||||||
|
)
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = { openWebPage(context.resources.getString(R.string.github_url)) },
|
onClick = { openWebPage(context.resources.getString(R.string.github_url)) },
|
||||||
modifier = Modifier.padding(vertical = 5.dp)
|
modifier = Modifier.padding(vertical = 5.dp),
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
Row {
|
Row {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = ImageVector.vectorResource(R.drawable.github),
|
imageVector = ImageVector.vectorResource(R.drawable.github),
|
||||||
stringResource(
|
stringResource(id = R.string.github),
|
||||||
id = R.string.github
|
Modifier.size(25.dp),
|
||||||
),
|
|
||||||
Modifier.size(25.dp)
|
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
"Open an issue",
|
"Open an issue",
|
||||||
textAlign = TextAlign.Justify,
|
textAlign = TextAlign.Justify,
|
||||||
modifier = Modifier.padding(start = 10.dp)
|
modifier = Modifier.padding(start = 10.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
Icon(
|
||||||
|
Icons.AutoMirrored.Rounded.ArrowForward,
|
||||||
|
stringResource(id = R.string.go)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
|
HorizontalDivider(
|
||||||
|
thickness = 0.5.dp,
|
||||||
|
color = MaterialTheme.colorScheme.onBackground
|
||||||
|
)
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = { launchEmail() },
|
onClick = { launchEmail() },
|
||||||
modifier = Modifier.padding(vertical = 5.dp)
|
modifier = Modifier.padding(vertical = 5.dp),
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
Row {
|
Row {
|
||||||
Icon(Icons.Rounded.Mail, stringResource(id = R.string.email))
|
Icon(Icons.Rounded.Mail, stringResource(id = R.string.email))
|
||||||
Text(
|
Text(
|
||||||
stringResource(id = R.string.email_description),
|
stringResource(id = R.string.email_description),
|
||||||
textAlign = TextAlign.Justify,
|
textAlign = TextAlign.Justify,
|
||||||
modifier = Modifier.padding(start = 10.dp)
|
modifier = Modifier.padding(start = 10.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
Icon(
|
||||||
|
Icons.AutoMirrored.Rounded.ArrowForward,
|
||||||
|
stringResource(id = R.string.go)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -228,17 +257,17 @@ fun SupportScreen(
|
|||||||
style = TextStyle(textDecoration = TextDecoration.Underline),
|
style = TextStyle(textDecoration = TextDecoration.Underline),
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.clickable {
|
Modifier.clickable {
|
||||||
openWebPage(context.resources.getString(R.string.privacy_policy_url))
|
openWebPage(context.resources.getString(R.string.privacy_policy_url))
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.spacedBy(25.dp),
|
horizontalArrangement = Arrangement.spacedBy(25.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.padding(25.dp)
|
modifier = Modifier.padding(25.dp),
|
||||||
) {
|
) {
|
||||||
Text("Version: ${BuildConfig.VERSION_NAME}")
|
Text("Version: ${BuildConfig.VERSION_NAME}", modifier = Modifier.focusable())
|
||||||
Text("Mode: ${if (settings.isKernelEnabled) "Kernel" else "Userspace" }")
|
Text("Mode: ${if (uiState.settings.isKernelEnabled) "Kernel" else "Userspace"}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.support
|
||||||
|
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
|
|
||||||
|
data class SupportUiState(val settings: Settings = Settings(), val loading: Boolean = true)
|
||||||
+17
-16
@@ -2,24 +2,25 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.support
|
|||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class SupportViewModel @Inject constructor(
|
class SupportViewModel @Inject constructor(private val settingsRepository: SettingsRepository) :
|
||||||
private val settingsRepo: SettingsDoa
|
ViewModel() {
|
||||||
) : ViewModel() {
|
|
||||||
private val _settings = MutableStateFlow(Settings())
|
val uiState =
|
||||||
val settings get() = _settings.asStateFlow()
|
settingsRepository
|
||||||
init {
|
.getSettingsFlow()
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
.map { SupportUiState(it, false) }
|
||||||
_settings.value = settingsRepo.getAll().first()
|
.stateIn(
|
||||||
}
|
viewModelScope,
|
||||||
}
|
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
|
||||||
|
SupportUiState(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ private val DarkColorScheme =
|
|||||||
primary = virdigris,
|
primary = virdigris,
|
||||||
secondary = virdigris,
|
secondary = virdigris,
|
||||||
// secondary = PurpleGrey80,
|
// secondary = PurpleGrey80,
|
||||||
tertiary = virdigris
|
tertiary = virdigris,
|
||||||
// tertiary = Pink80
|
// tertiary = Pink80
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ private val LightColorScheme =
|
|||||||
lightColorScheme(
|
lightColorScheme(
|
||||||
primary = Purple40,
|
primary = Purple40,
|
||||||
secondary = PurpleGrey40,
|
secondary = PurpleGrey40,
|
||||||
tertiary = Pink40
|
tertiary = Pink40,
|
||||||
/* Other default colors to override
|
/* Other default colors to override
|
||||||
background = Color(0xFFFFFBFE),
|
background = Color(0xFFFFFBFE),
|
||||||
surface = Color(0xFFFFFBFE),
|
surface = Color(0xFFFFFBFE),
|
||||||
@@ -57,7 +57,6 @@ fun WireguardAutoTunnelTheme(
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
darkTheme -> DarkColorScheme
|
darkTheme -> DarkColorScheme
|
||||||
else -> LightColorScheme
|
else -> LightColorScheme
|
||||||
}
|
}
|
||||||
@@ -68,14 +67,14 @@ fun WireguardAutoTunnelTheme(
|
|||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
window.statusBarColor = Color.Transparent.toArgb()
|
window.statusBarColor = Color.Transparent.toArgb()
|
||||||
window.navigationBarColor = Color.Transparent.toArgb()
|
window.navigationBarColor = Color.Transparent.toArgb()
|
||||||
WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightStatusBars = !darkTheme
|
WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightStatusBars =
|
||||||
WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightNavigationBars = !darkTheme
|
!darkTheme
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MaterialTheme(
|
MaterialTheme(
|
||||||
colorScheme = colorScheme,
|
colorScheme = colorScheme,
|
||||||
typography = Typography,
|
typography = Typography,
|
||||||
content = content
|
content = content,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
-22
@@ -1,22 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.theme
|
|
||||||
|
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.DisposableEffect
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun TransparentSystemBars() {
|
|
||||||
val systemUiController = rememberSystemUiController()
|
|
||||||
val useDarkIcons = !isSystemInDarkTheme()
|
|
||||||
|
|
||||||
DisposableEffect(systemUiController, useDarkIcons) {
|
|
||||||
systemUiController.setSystemBarsColor(
|
|
||||||
color = Color.Transparent,
|
|
||||||
darkIcons = useDarkIcons
|
|
||||||
)
|
|
||||||
|
|
||||||
onDispose {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,13 +10,13 @@ import androidx.compose.ui.unit.sp
|
|||||||
val Typography =
|
val Typography =
|
||||||
Typography(
|
Typography(
|
||||||
bodyLarge =
|
bodyLarge =
|
||||||
TextStyle(
|
TextStyle(
|
||||||
fontFamily = FontFamily.Default,
|
fontFamily = FontFamily.Default,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
lineHeight = 24.sp,
|
lineHeight = 24.sp,
|
||||||
letterSpacing = 0.5.sp
|
letterSpacing = 0.5.sp,
|
||||||
)
|
),
|
||||||
/* Other default text styles to override
|
/* Other default text styles to override
|
||||||
titleLarge = TextStyle(
|
titleLarge = TextStyle(
|
||||||
fontFamily = FontFamily.Default,
|
fontFamily = FontFamily.Default,
|
||||||
|
|||||||
+8
-7
@@ -1,14 +1,12 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel
|
package com.zaneschepke.wireguardautotunnel.util
|
||||||
|
|
||||||
object Constants {
|
object Constants {
|
||||||
const val MANUAL_TUNNEL_CONFIG_ID = "0"
|
const val MANUAL_TUNNEL_CONFIG_ID = "0"
|
||||||
const val WATCHER_SERVICE_WAKE_LOCK_TIMEOUT = 10 * 60 * 1000L // 10 minutes
|
const val BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT = 10 * 60 * 1000L // 10 minutes
|
||||||
const val VPN_CONNECTIVITY_CHECK_INTERVAL = 3000L
|
const val DEFAULT_WATCHER_WAKE_LOCK_TIMEOUT = 30 * 60 * 1000L // 30 minutes
|
||||||
const val VPN_STATISTIC_CHECK_INTERVAL = 1000L
|
const val VPN_STATISTIC_CHECK_INTERVAL = 1000L
|
||||||
const val TOGGLE_TUNNEL_DELAY = 500L
|
const val VPN_CONNECTED_NOTIFICATION_DELAY = 3000L
|
||||||
const val FADE_IN_ANIMATION_DURATION = 1000
|
const val TOGGLE_TUNNEL_DELAY = 300L
|
||||||
const val SLIDE_IN_ANIMATION_DURATION = 500
|
|
||||||
const val SLIDE_IN_TRANSITION_OFFSET = 1000
|
|
||||||
const val CONF_FILE_EXTENSION = ".conf"
|
const val CONF_FILE_EXTENSION = ".conf"
|
||||||
const val ZIP_FILE_EXTENSION = ".zip"
|
const val ZIP_FILE_EXTENSION = ".zip"
|
||||||
const val URI_CONTENT_SCHEME = "content"
|
const val URI_CONTENT_SCHEME = "content"
|
||||||
@@ -18,4 +16,7 @@ object Constants {
|
|||||||
const val ANDROID_TV_EXPLORER_STUB = "com.android.tv.frameworkpackagestubs"
|
const val ANDROID_TV_EXPLORER_STUB = "com.android.tv.frameworkpackagestubs"
|
||||||
const val EMAIL_MIME_TYPE = "message/rfc822"
|
const val EMAIL_MIME_TYPE = "message/rfc822"
|
||||||
const val SYSTEM_EXEMPT_SERVICE_TYPE_ID = 1024
|
const val SYSTEM_EXEMPT_SERVICE_TYPE_ID = 1024
|
||||||
|
|
||||||
|
const val SUBSCRIPTION_TIMEOUT = 5_000L
|
||||||
|
const val FOCUS_REQUEST_DELAY = 500L
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.util
|
||||||
|
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
|
|
||||||
|
sealed class Event {
|
||||||
|
|
||||||
|
abstract val message: String
|
||||||
|
|
||||||
|
sealed class Error : Event() {
|
||||||
|
data object None : Error() {
|
||||||
|
override val message: String
|
||||||
|
get() = WireGuardAutoTunnel.instance.getString(R.string.error_none)
|
||||||
|
}
|
||||||
|
|
||||||
|
data object SsidConflict : Error() {
|
||||||
|
override val message: String
|
||||||
|
get() = WireGuardAutoTunnel.instance.getString(R.string.error_ssid_exists)
|
||||||
|
}
|
||||||
|
|
||||||
|
data object RootDenied : Error() {
|
||||||
|
override val message: String
|
||||||
|
get() = WireGuardAutoTunnel.instance.getString(R.string.error_root_denied)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class General(val customMessage: String) : Error() {
|
||||||
|
override val message: String
|
||||||
|
get() = customMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Exception(val exception: kotlin.Exception) : Error() {
|
||||||
|
override val message: String
|
||||||
|
get() =
|
||||||
|
exception.message
|
||||||
|
?: WireGuardAutoTunnel.instance.getString(R.string.unknown_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
data object InvalidQrCode : Error() {
|
||||||
|
override val message: String
|
||||||
|
get() = WireGuardAutoTunnel.instance.getString(R.string.error_invalid_code)
|
||||||
|
}
|
||||||
|
|
||||||
|
data object InvalidFileExtension : Error() {
|
||||||
|
override val message: String
|
||||||
|
get() = WireGuardAutoTunnel.instance.getString(R.string.error_file_extension)
|
||||||
|
}
|
||||||
|
|
||||||
|
data object FileReadFailed : Error() {
|
||||||
|
override val message: String
|
||||||
|
get() = WireGuardAutoTunnel.instance.getString(R.string.error_file_extension)
|
||||||
|
}
|
||||||
|
|
||||||
|
data object AuthenticationFailed : Error() {
|
||||||
|
override val message: String
|
||||||
|
get() = WireGuardAutoTunnel.instance.getString(R.string.error_authentication_failed)
|
||||||
|
}
|
||||||
|
|
||||||
|
data object AuthorizationFailed : Error() {
|
||||||
|
override val message: String
|
||||||
|
get() = WireGuardAutoTunnel.instance.getString(R.string.error_authorization_failed)
|
||||||
|
}
|
||||||
|
|
||||||
|
data object BackgroundLocationRequired : Error() {
|
||||||
|
override val message: String
|
||||||
|
get() =
|
||||||
|
WireGuardAutoTunnel.instance.getString(R.string.background_location_required)
|
||||||
|
}
|
||||||
|
|
||||||
|
data object LocationServicesRequired : Error() {
|
||||||
|
override val message: String
|
||||||
|
get() = WireGuardAutoTunnel.instance.getString(R.string.location_services_required)
|
||||||
|
}
|
||||||
|
|
||||||
|
data object PreciseLocationRequired : Error() {
|
||||||
|
override val message: String
|
||||||
|
get() = WireGuardAutoTunnel.instance.getString(R.string.precise_location_required)
|
||||||
|
}
|
||||||
|
|
||||||
|
data object FileExplorerRequired : Error() {
|
||||||
|
override val message: String
|
||||||
|
get() = WireGuardAutoTunnel.instance.getString(R.string.error_no_file_explorer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class Message : Event() {
|
||||||
|
data object ConfigSaved : Message() {
|
||||||
|
override val message: String
|
||||||
|
get() = WireGuardAutoTunnel.instance.getString(R.string.config_changes_saved)
|
||||||
|
}
|
||||||
|
|
||||||
|
data object ConfigsExported : Message() {
|
||||||
|
override val message: String
|
||||||
|
get() = WireGuardAutoTunnel.instance.getString(R.string.exported_configs_message)
|
||||||
|
}
|
||||||
|
|
||||||
|
data object TunnelOffAction : Message() {
|
||||||
|
override val message: String
|
||||||
|
get() = WireGuardAutoTunnel.instance.getString(R.string.turn_off_tunnel)
|
||||||
|
}
|
||||||
|
|
||||||
|
data object TunnelOnAction : Message() {
|
||||||
|
override val message: String
|
||||||
|
get() = WireGuardAutoTunnel.instance.getString(R.string.turn_on_tunnel)
|
||||||
|
}
|
||||||
|
|
||||||
|
data object AutoTunnelOffAction : Message() {
|
||||||
|
override val message: String
|
||||||
|
get() = WireGuardAutoTunnel.instance.getString(R.string.turn_off_auto)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.util
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.pm.PackageInfo
|
||||||
|
import com.wireguard.android.backend.Statistics
|
||||||
|
import com.wireguard.android.backend.Statistics.PeerStats
|
||||||
|
import com.wireguard.crypto.Key
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.math.BigDecimal
|
||||||
|
import java.text.DecimalFormat
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
|
|
||||||
|
fun BroadcastReceiver.goAsync(
|
||||||
|
context: CoroutineContext = EmptyCoroutineContext,
|
||||||
|
block: suspend CoroutineScope.() -> Unit
|
||||||
|
) {
|
||||||
|
val pendingResult = goAsync()
|
||||||
|
@OptIn(DelicateCoroutinesApi::class) // Must run globally; there's no teardown callback.
|
||||||
|
GlobalScope.launch(context) {
|
||||||
|
try {
|
||||||
|
block()
|
||||||
|
} finally {
|
||||||
|
pendingResult.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun BigDecimal.toThreeDecimalPlaceString(): String {
|
||||||
|
val df = DecimalFormat("#.###")
|
||||||
|
return df.format(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> List<T>.update(index: Int, item: T): List<T> = toMutableList().apply { this[index] = item }
|
||||||
|
|
||||||
|
fun <T> List<T>.removeAt(index: Int): List<T> = toMutableList().apply { this.removeAt(index) }
|
||||||
|
|
||||||
|
typealias TunnelConfigs = List<TunnelConfig>
|
||||||
|
|
||||||
|
typealias Packages = List<PackageInfo>
|
||||||
|
|
||||||
|
fun Statistics.mapPeerStats(): Map<Key, PeerStats?> {
|
||||||
|
return this.peers().associateWith { key -> (this.peer(key)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun PeerStats.latestHandshakeSeconds(): Long? {
|
||||||
|
return NumberUtils.getSecondsBetweenTimestampAndNow(this.latestHandshakeEpochMillis)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun PeerStats.handshakeStatus(): HandshakeStatus {
|
||||||
|
// TODO add never connected status after duration
|
||||||
|
return this.latestHandshakeSeconds().let {
|
||||||
|
when {
|
||||||
|
it == null -> HandshakeStatus.NOT_STARTED
|
||||||
|
it <= HandshakeStatus.STALE_TIME_LIMIT_SEC -> HandshakeStatus.HEALTHY
|
||||||
|
it > HandshakeStatus.STALE_TIME_LIMIT_SEC -> HandshakeStatus.STALE
|
||||||
|
else -> {
|
||||||
|
HandshakeStatus.UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,6 @@ import android.os.Build
|
|||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.provider.MediaStore.MediaColumns
|
import android.provider.MediaStore.MediaColumns
|
||||||
import com.zaneschepke.wireguardautotunnel.Constants
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@@ -37,22 +36,20 @@ object FileUtils {
|
|||||||
val target =
|
val target =
|
||||||
File(
|
File(
|
||||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
||||||
fileName
|
fileName,
|
||||||
)
|
)
|
||||||
return target.outputStream()
|
return target.outputStream()
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveFilesToZip(
|
fun saveFilesToZip(context: Context, files: List<File>) {
|
||||||
context: Context,
|
val zipOutputStream =
|
||||||
files: List<File>
|
createDownloadsFileOutputStream(
|
||||||
) {
|
context,
|
||||||
val zipOutputStream = createDownloadsFileOutputStream(
|
"wg-export_${Instant.now().epochSecond}.zip",
|
||||||
context,
|
ZIP_FILE_MIME_TYPE,
|
||||||
"wg-export_${Instant.now().epochSecond}.zip",
|
)
|
||||||
ZIP_FILE_MIME_TYPE
|
|
||||||
)
|
|
||||||
ZipOutputStream(zipOutputStream).use { zos ->
|
ZipOutputStream(zipOutputStream).use { zos ->
|
||||||
files.forEach { file ->
|
files.forEach { file ->
|
||||||
val entry = ZipEntry(file.name)
|
val entry = ZipEntry(file.name)
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.util
|
||||||
|
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
sealed class Result<T> {
|
||||||
|
class Success<T>(val data: T) : Result<T>()
|
||||||
|
|
||||||
|
class Error<T>(val error: Event.Error) : Result<T>() {
|
||||||
|
init {
|
||||||
|
when (this.error) {
|
||||||
|
is Event.Error.Exception -> Timber.e(this.error.exception)
|
||||||
|
else -> Timber.e(this.error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.util
|
|
||||||
|
|
||||||
import com.wireguard.config.BadConfigException
|
|
||||||
|
|
||||||
class WgTunnelException(e: Exception) : Exception() {
|
|
||||||
constructor(message: String) : this(Exception(message))
|
|
||||||
|
|
||||||
override val message: String = generateExceptionMessage(e)
|
|
||||||
|
|
||||||
private fun generateExceptionMessage(e: Exception): String {
|
|
||||||
return when (e) {
|
|
||||||
is BadConfigException -> "${e.section.name} ${e.location.name} ${e.reason.name}"
|
|
||||||
else -> e.message ?: "Unknown error occurred"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="640"
|
||||||
|
android:viewportHeight="640"
|
||||||
|
android:tint="#FFFFFF">
|
||||||
|
<group android:scaleX="1.0132159"
|
||||||
|
android:scaleY="1.0132159"
|
||||||
|
android:translateX="-4.229075"
|
||||||
|
android:translateY="-4.229075">
|
||||||
|
<path
|
||||||
|
android:pathData="M316.72,80.15C314.94,80.82 312.08,83.95 308.79,88.84C275.66,138.15 157.88,161.96 119.66,127.08C109.97,118.24 101.21,118.84 98.97,128.5C96.01,141.29 98.49,204.07 103.12,233.5C123.71,364.32 186.77,465.69 303.03,554.88C314.06,563.34 316.63,563.42 326.93,555.61C329.91,553.35 336.21,548.63 340.93,545.13C345.64,541.63 350.87,537.46 352.55,535.88C354.23,534.29 357.92,531.53 360.75,529.74C413.56,496.43 481.74,399.04 510.38,316C527.22,267.19 534.86,236.66 539.96,197.9C547.99,136.74 545.31,124.46 526,134.01C469.85,161.74 361.02,137.16 333.39,90.49C327.63,80.75 322.93,77.84 316.72,80.15M307.5,195.34C282.24,203.76 266.16,237.38 269.85,274.04C270.99,285.39 271.18,285 264.63,285C254.13,285 254.15,284.87 255,352.05C255.64,402.94 250.1,397.02 298.5,398.52C352.54,400.2 377.63,400.23 379.23,398.63C381.67,396.18 381.86,392.86 382.46,339.89C383.09,283.93 383.39,286 374.51,286C367.07,286 367.21,286.26 367.77,273.58C369.89,225.38 338.74,184.92 307.5,195.34M308.93,216.98C294.31,224.7 281.68,270.05 290.33,283.75C291.74,285.99 346.85,285.51 347.78,283.25C359.48,254.75 330.62,205.51 308.93,216.98M309.54,317.1C304.18,321.81 304.39,327.76 310.11,332.99C314.78,337.26 314.82,336.51 309.33,349.89C307.44,354.51 306.13,358.89 306.42,359.64C306.96,361.06 329,361.75 329,360.35C329,359.98 327.42,354.82 325.5,348.86C323.58,342.91 322,337.52 322,336.89C322,336.26 323.58,334 325.5,331.87C335.69,320.59 321.02,307.02 309.54,317.1"
|
||||||
|
android:fillColor="#53bdb6"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:strokeColor="#00000000"/>
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 544 B |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user