Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c8205c4c59 | |||
| 3247e94358 | |||
| 2690ce29e1 | |||
| 500b85f687 | |||
| 84b2b75271 | |||
| 0197198f7b | |||
| 0b271778c9 | |||
| 6427b2f832 | |||
| 097097f620 | |||
| 20dfaed8de | |||
| 07aa37fc2a | |||
| 091cd2e028 | |||
| 7efa6d0bf4 | |||
| e31fb01410 | |||
| 76674323e7 | |||
| f1fc9ca6f7 | |||
| cb301e74eb | |||
| 8141fe19be | |||
| 0fdb3d0b31 | |||
| d9f3a21cc3 | |||
| fec84bc6ac | |||
| d6ee36edc0 | |||
| e3fcf712d5 | |||
| 5a15776bb3 | |||
| 3339448424 | |||
| 7ec294b789 | |||
| e59c72788d | |||
| 62435d549c | |||
| 96800037d1 | |||
| f5b3bb1cb7 | |||
| 654b4a4719 | |||
| a19b5ce22a | |||
| 86f592255c | |||
| 4a5dd76b5b | |||
| 1139d17f13 | |||
| 36855319a2 | |||
| 61e3751321 |
@@ -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/33100.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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ 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.data.AppDatabase
|
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
||||||
|
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
|
||||||
@@ -14,10 +15,11 @@ 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)
|
||||||
@@ -25,36 +27,9 @@ class MigrationTest {
|
|||||||
helper.createDatabase(dbName, 4).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," +
|
|
||||||
"is_kernel_enabled," +
|
|
||||||
"is_restore_on_boot_enabled," +
|
|
||||||
"is_multi_tunnel_enabled)" +
|
|
||||||
" VALUES " +
|
|
||||||
"('false'," +
|
|
||||||
"'false'," +
|
|
||||||
"'[trustedSSID1,trustedSSID2]'," +
|
|
||||||
"'defaultTunnel'," +
|
|
||||||
"'false'," +
|
|
||||||
"'false'," +
|
|
||||||
"'false'," +
|
|
||||||
"'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()
|
||||||
|
|||||||
@@ -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,65 +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:exported="false">
|
android:stopWithTask="false"
|
||||||
</service>
|
tools:node="merge" />
|
||||||
<receiver android:enabled="true" android:name=".receiver.BootReceiver"
|
|
||||||
|
<receiver
|
||||||
|
android:name=".receiver.BootReceiver"
|
||||||
|
android:enabled="true"
|
||||||
android:exported="false">
|
android:exported="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
<action android:name="android.intent.action.ACTION_BOOT_COMPLETED" />
|
<action android:name="android.intent.action.ACTION_BOOT_COMPLETED" />
|
||||||
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
|
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
|
||||||
<action android:name="com.htc.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>
|
||||||
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 16 KiB |
@@ -4,61 +4,31 @@ import android.app.Application
|
|||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.service.quicksettings.TileService
|
import android.service.quicksettings.TileService
|
||||||
import androidx.lifecycle.ProcessLifecycleOwner
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
|
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.IOException
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltAndroidApp
|
@HiltAndroidApp
|
||||||
class WireGuardAutoTunnel : Application() {
|
class WireGuardAutoTunnel : Application() {
|
||||||
@Inject
|
|
||||||
lateinit var settingsRepository: SettingsRepository
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var dataStoreManager: DataStoreManager
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
instance = this
|
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()
|
|
||||||
requestTileServiceStateUpdate()
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Timber.e("Failed to load preferences")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initSettings() {
|
|
||||||
with(ProcessLifecycleOwner.get()) {
|
|
||||||
lifecycleScope.launch {
|
|
||||||
if (settingsRepository.getAll().isEmpty()) {
|
|
||||||
settingsRepository.save(Settings())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
lateinit var instance: WireGuardAutoTunnel private set
|
lateinit var instance: WireGuardAutoTunnel
|
||||||
|
private set
|
||||||
|
|
||||||
fun isRunningOnAndroidTv(): Boolean {
|
fun isRunningOnAndroidTv(): Boolean {
|
||||||
return instance.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
return instance.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun requestTileServiceStateUpdate() {
|
fun requestTileServiceStateUpdate() {
|
||||||
TileService.requestListeningState(instance, ComponentName(instance, TunnelControlTile::class.java))
|
TileService.requestListeningState(
|
||||||
|
instance,
|
||||||
|
ComponentName(instance, TunnelControlTile::class.java),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,16 +10,20 @@ import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
|||||||
@Database(
|
@Database(
|
||||||
entities = [Settings::class, TunnelConfig::class],
|
entities = [Settings::class, TunnelConfig::class],
|
||||||
version = 5,
|
version = 5,
|
||||||
autoMigrations = [
|
autoMigrations =
|
||||||
AutoMigration(from = 1, to = 2), AutoMigration(from = 2, to = 3), AutoMigration(
|
[
|
||||||
from = 3,
|
AutoMigration(from = 1, to = 2),
|
||||||
to = 4
|
AutoMigration(from = 2, to = 3),
|
||||||
),AutoMigration(
|
AutoMigration(
|
||||||
from = 4,
|
from = 3,
|
||||||
to = 5
|
to = 4,
|
||||||
)
|
),
|
||||||
],
|
AutoMigration(
|
||||||
exportSchema = true
|
from = 4,
|
||||||
|
to = 5,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
exportSchema = true,
|
||||||
)
|
)
|
||||||
@TypeConverters(DatabaseListConverters::class)
|
@TypeConverters(DatabaseListConverters::class)
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.data
|
||||||
|
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
class DatabaseCallback : RoomDatabase.Callback() {
|
||||||
|
override fun onCreate(db: SupportSQLiteDatabase) = db.run {
|
||||||
|
// Notice non-ui thread is here
|
||||||
|
beginTransaction()
|
||||||
|
try {
|
||||||
|
execSQL(Queries.createDefaultSettings())
|
||||||
|
Timber.i("Bootstrapping settings data")
|
||||||
|
setTransactionSuccessful()
|
||||||
|
} catch (e : Exception) {
|
||||||
|
Timber.e(e)
|
||||||
|
} finally {
|
||||||
|
endTransaction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.data
|
||||||
|
|
||||||
|
object Queries {
|
||||||
|
fun createDefaultSettings() : String {
|
||||||
|
return """
|
||||||
|
INSERT INTO Settings (is_tunnel_enabled,
|
||||||
|
is_tunnel_on_mobile_data_enabled,
|
||||||
|
trusted_network_ssids,
|
||||||
|
default_tunnel,
|
||||||
|
is_always_on_vpn_enabled,
|
||||||
|
is_tunnel_on_ethernet_enabled,
|
||||||
|
is_shortcuts_enabled,
|
||||||
|
is_battery_saver_enabled,
|
||||||
|
is_tunnel_on_wifi_enabled,
|
||||||
|
is_kernel_enabled,
|
||||||
|
is_restore_on_boot_enabled,
|
||||||
|
is_multi_tunnel_enabled)
|
||||||
|
VALUES
|
||||||
|
('false',
|
||||||
|
'false',
|
||||||
|
'[trustedSSID1,trustedSSID2]',
|
||||||
|
NULL,
|
||||||
|
'false',
|
||||||
|
'false',
|
||||||
|
'false',
|
||||||
|
'false',
|
||||||
|
'false',
|
||||||
|
'false',
|
||||||
|
'false',
|
||||||
|
'false')
|
||||||
|
""".trimIndent()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,27 +10,19 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface SettingsDao {
|
interface SettingsDao {
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun save(t: Settings)
|
||||||
suspend fun save(t: Settings)
|
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun saveAll(t: List<Settings>)
|
||||||
suspend fun saveAll(t: List<Settings>)
|
|
||||||
|
|
||||||
@Query("SELECT * FROM settings WHERE id=:id")
|
@Query("SELECT * FROM settings WHERE id=:id") suspend fun getById(id: Long): Settings?
|
||||||
suspend fun getById(id: Long): Settings?
|
|
||||||
|
|
||||||
@Query("SELECT * FROM settings")
|
@Query("SELECT * FROM settings") suspend fun getAll(): List<Settings>
|
||||||
suspend fun getAll(): List<Settings>
|
|
||||||
|
|
||||||
@Query("SELECT * FROM settings LIMIT 1")
|
@Query("SELECT * FROM settings LIMIT 1") fun getSettingsFlow(): Flow<Settings>
|
||||||
fun getSettingsFlow(): Flow<Settings>
|
|
||||||
|
|
||||||
@Query("SELECT * FROM settings")
|
@Query("SELECT * FROM settings") fun getAllFlow(): Flow<MutableList<Settings>>
|
||||||
fun getAllFlow(): Flow<MutableList<Settings>>
|
|
||||||
|
|
||||||
@Delete
|
@Delete suspend fun delete(t: Settings)
|
||||||
suspend fun delete(t: Settings)
|
|
||||||
|
|
||||||
@Query("SELECT COUNT('id') FROM settings")
|
@Query("SELECT COUNT('id') FROM settings") suspend fun count(): Long
|
||||||
suspend fun count(): Long
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,24 +10,17 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface TunnelConfigDao {
|
interface TunnelConfigDao {
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun save(t: TunnelConfig)
|
||||||
suspend fun save(t: TunnelConfig)
|
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun saveAll(t: List<TunnelConfig>)
|
||||||
suspend fun saveAll(t: List<TunnelConfig>)
|
|
||||||
|
|
||||||
@Query("SELECT * FROM TunnelConfig WHERE id=:id")
|
@Query("SELECT * FROM TunnelConfig WHERE id=:id") suspend fun getById(id: Long): TunnelConfig?
|
||||||
suspend fun getById(id: Long): TunnelConfig?
|
|
||||||
|
|
||||||
@Query("SELECT * FROM TunnelConfig")
|
@Query("SELECT * FROM TunnelConfig") suspend fun getAll(): List<TunnelConfig>
|
||||||
suspend fun getAll(): List<TunnelConfig>
|
|
||||||
|
|
||||||
@Delete
|
@Delete suspend fun delete(t: TunnelConfig)
|
||||||
suspend fun delete(t: TunnelConfig)
|
|
||||||
|
|
||||||
@Query("SELECT COUNT('id') FROM TunnelConfig")
|
@Query("SELECT COUNT('id') FROM TunnelConfig") suspend fun count(): Long
|
||||||
suspend fun count(): Long
|
|
||||||
|
|
||||||
@Query("SELECT * FROM tunnelconfig")
|
@Query("SELECT * FROM tunnelconfig") fun getAllFlow(): Flow<MutableList<TunnelConfig>>
|
||||||
fun getAllFlow(): Flow<MutableList<TunnelConfig>>
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.data.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> getFromStoreFlow(key: Preferences.Key<T>) = context.dataStore.data.map {
|
|
||||||
it[key]
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun <T> getFromStore(key: Preferences.Key<T>) = context.dataStore.data.first { it.contains(key) }[key]
|
fun <T> getFromStoreFlow(key: Preferences.Key<T>) = 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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,39 +8,49 @@ import androidx.room.PrimaryKey
|
|||||||
data class Settings(
|
data class Settings(
|
||||||
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
||||||
@ColumnInfo(name = "is_tunnel_enabled") var isAutoTunnelEnabled: Boolean = false,
|
@ColumnInfo(name = "is_tunnel_enabled") var isAutoTunnelEnabled: Boolean = false,
|
||||||
@ColumnInfo(name = "is_tunnel_on_mobile_data_enabled") var isTunnelOnMobileDataEnabled: Boolean = false,
|
@ColumnInfo(name = "is_tunnel_on_mobile_data_enabled")
|
||||||
@ColumnInfo(name = "trusted_network_ssids") var trustedNetworkSSIDs: MutableList<String> = mutableListOf(),
|
var isTunnelOnMobileDataEnabled: Boolean = false,
|
||||||
|
@ColumnInfo(name = "trusted_network_ssids")
|
||||||
|
var trustedNetworkSSIDs: MutableList<String> = mutableListOf(),
|
||||||
@ColumnInfo(name = "default_tunnel") var defaultTunnel: String? = null,
|
@ColumnInfo(name = "default_tunnel") var defaultTunnel: String? = null,
|
||||||
@ColumnInfo(name = "is_always_on_vpn_enabled") var isAlwaysOnVpnEnabled: Boolean = false,
|
@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_tunnel_on_ethernet_enabled")
|
||||||
|
var isTunnelOnEthernetEnabled: Boolean = false,
|
||||||
@ColumnInfo(
|
@ColumnInfo(
|
||||||
name = "is_shortcuts_enabled",
|
name = "is_shortcuts_enabled",
|
||||||
defaultValue = "false"
|
defaultValue = "false",
|
||||||
) var isShortcutsEnabled: Boolean = false,
|
)
|
||||||
|
var isShortcutsEnabled: Boolean = false,
|
||||||
@ColumnInfo(
|
@ColumnInfo(
|
||||||
name = "is_battery_saver_enabled",
|
name = "is_battery_saver_enabled",
|
||||||
defaultValue = "false"
|
defaultValue = "false",
|
||||||
) var isBatterySaverEnabled: Boolean = false,
|
)
|
||||||
|
var isBatterySaverEnabled: Boolean = false,
|
||||||
@ColumnInfo(
|
@ColumnInfo(
|
||||||
name = "is_tunnel_on_wifi_enabled",
|
name = "is_tunnel_on_wifi_enabled",
|
||||||
defaultValue = "false"
|
defaultValue = "false",
|
||||||
) var isTunnelOnWifiEnabled: Boolean = false,
|
)
|
||||||
|
var isTunnelOnWifiEnabled: Boolean = false,
|
||||||
@ColumnInfo(
|
@ColumnInfo(
|
||||||
name = "is_kernel_enabled",
|
name = "is_kernel_enabled",
|
||||||
defaultValue = "false"
|
defaultValue = "false",
|
||||||
) var isKernelEnabled: Boolean = false,
|
)
|
||||||
|
var isKernelEnabled: Boolean = false,
|
||||||
@ColumnInfo(
|
@ColumnInfo(
|
||||||
name = "is_restore_on_boot_enabled",
|
name = "is_restore_on_boot_enabled",
|
||||||
defaultValue = "false"
|
defaultValue = "false",
|
||||||
) var isRestoreOnBootEnabled: Boolean = false,
|
)
|
||||||
|
var isRestoreOnBootEnabled: Boolean = false,
|
||||||
@ColumnInfo(
|
@ColumnInfo(
|
||||||
name = "is_multi_tunnel_enabled",
|
name = "is_multi_tunnel_enabled",
|
||||||
defaultValue = "false"
|
defaultValue = "false",
|
||||||
) var isMultiTunnelEnabled: Boolean = false,
|
)
|
||||||
|
var isMultiTunnelEnabled: Boolean = false,
|
||||||
@ColumnInfo(
|
@ColumnInfo(
|
||||||
name = "is_auto_tunnel_paused",
|
name = "is_auto_tunnel_paused",
|
||||||
defaultValue = "false"
|
defaultValue = "false",
|
||||||
) var isAutoTunnelPaused: Boolean = false,
|
)
|
||||||
|
var isAutoTunnelPaused: Boolean = false,
|
||||||
) {
|
) {
|
||||||
fun isTunnelConfigDefault(tunnelConfig: TunnelConfig): Boolean {
|
fun isTunnelConfigDefault(tunnelConfig: TunnelConfig): Boolean {
|
||||||
return if (defaultTunnel != null) {
|
return if (defaultTunnel != null) {
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
|||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
interface SettingsRepository {
|
interface SettingsRepository {
|
||||||
suspend fun save(settings : Settings)
|
suspend fun save(settings: Settings)
|
||||||
fun getSettingsFlow() : Flow<Settings>
|
|
||||||
|
|
||||||
suspend fun getSettings() : Settings
|
fun getSettingsFlow(): Flow<Settings>
|
||||||
suspend fun getAll() : List<Settings>
|
|
||||||
|
suspend fun getSettings(): Settings
|
||||||
|
|
||||||
|
suspend fun getAll(): List<Settings>
|
||||||
}
|
}
|
||||||
@@ -6,9 +6,13 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
|
|
||||||
interface TunnelConfigRepository {
|
interface TunnelConfigRepository {
|
||||||
|
|
||||||
fun getTunnelConfigsFlow() : Flow<TunnelConfigs>
|
fun getTunnelConfigsFlow(): Flow<TunnelConfigs>
|
||||||
suspend fun getAll() : TunnelConfigs
|
|
||||||
|
suspend fun getAll(): TunnelConfigs
|
||||||
|
|
||||||
suspend fun save(tunnelConfig: TunnelConfig)
|
suspend fun save(tunnelConfig: TunnelConfig)
|
||||||
|
|
||||||
suspend fun delete(tunnelConfig: TunnelConfig)
|
suspend fun delete(tunnelConfig: TunnelConfig)
|
||||||
suspend fun count() : Int
|
|
||||||
|
suspend fun count(): Int
|
||||||
}
|
}
|
||||||
@@ -5,7 +5,8 @@ import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
|||||||
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
class TunnelConfigRepositoryImpl(private val tunnelConfigDao: TunnelConfigDao) : TunnelConfigRepository {
|
class TunnelConfigRepositoryImpl(private val tunnelConfigDao: TunnelConfigDao) :
|
||||||
|
TunnelConfigRepository {
|
||||||
override fun getTunnelConfigsFlow(): Flow<TunnelConfigs> {
|
override fun getTunnelConfigsFlow(): Flow<TunnelConfigs> {
|
||||||
return tunnelConfigDao.getAllFlow()
|
return tunnelConfigDao.getAllFlow()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ 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.data.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
|
|
||||||
|
|||||||
@@ -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>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,7 +44,7 @@ class TunnelModule {
|
|||||||
fun provideVpnService(
|
fun provideVpnService(
|
||||||
@Userspace userspaceBackend: Backend,
|
@Userspace userspaceBackend: Backend,
|
||||||
@Kernel kernelBackend: Backend,
|
@Kernel kernelBackend: Backend,
|
||||||
settingsRepository : SettingsRepository
|
settingsRepository: SettingsRepository
|
||||||
): VpnService {
|
): VpnService {
|
||||||
return WireGuardTunnel(userspaceBackend, kernelBackend, settingsRepository)
|
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
|
|
||||||
|
|||||||
@@ -4,22 +4,29 @@ import android.content.BroadcastReceiver
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
|
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 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
|
||||||
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class BootReceiver : BroadcastReceiver() {
|
class BootReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var settingsRepository: SettingsRepository
|
||||||
lateinit var settingsRepository: SettingsRepository
|
|
||||||
|
@Inject lateinit var tunnelConfigRepository: TunnelConfigRepository
|
||||||
|
|
||||||
override fun onReceive(context: Context?, intent: Intent?) = goAsync {
|
override fun onReceive(context: Context?, intent: Intent?) = goAsync {
|
||||||
if (Intent.ACTION_BOOT_COMPLETED != intent?.action) return@goAsync
|
if (Intent.ACTION_BOOT_COMPLETED != intent?.action) return@goAsync
|
||||||
if(settingsRepository.getSettings().isAutoTunnelEnabled) {
|
val settings = settingsRepository.getSettings()
|
||||||
|
if (settings.isAutoTunnelEnabled) {
|
||||||
|
Timber.i("Starting watcher service from boot")
|
||||||
ServiceManager.startWatcherServiceForeground(context!!)
|
ServiceManager.startWatcherServiceForeground(context!!)
|
||||||
|
} else if(settings.isAlwaysOnVpnEnabled) {
|
||||||
|
Timber.i("Starting tunnel from boot")
|
||||||
|
ServiceManager.startVpnServicePrimaryTunnel(context!!, settings, tunnelConfigRepository.getAll().firstOrNull())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,19 +14,15 @@ import javax.inject.Inject
|
|||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class NotificationActionReceiver : BroadcastReceiver() {
|
class NotificationActionReceiver : BroadcastReceiver() {
|
||||||
@Inject
|
@Inject lateinit var settingsRepository: SettingsRepository
|
||||||
lateinit var settingsRepository: SettingsRepository
|
|
||||||
|
|
||||||
override fun onReceive(
|
override fun onReceive(context: Context, intent: Intent?) = goAsync {
|
||||||
context: Context,
|
|
||||||
intent: Intent?
|
|
||||||
) = goAsync {
|
|
||||||
try {
|
try {
|
||||||
val settings = settingsRepository.getSettings()
|
val settings = settingsRepository.getSettings()
|
||||||
if (settings.defaultTunnel != null) {
|
if (settings.defaultTunnel != null) {
|
||||||
ServiceManager.stopVpnService(context)
|
ServiceManager.stopVpnService(context)
|
||||||
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
||||||
ServiceManager.startVpnService(context, settings.defaultTunnel.toString())
|
ServiceManager.startVpnServiceForeground(context, settings.defaultTunnel.toString())
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
cancel()
|
cancel()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,55 +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),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(
|
fun startWatcherServiceForeground(
|
||||||
context: Context,
|
context: Context,
|
||||||
) {
|
) {
|
||||||
actionOnService(
|
actionOnService(
|
||||||
Action.START_FOREGROUND,
|
Action.START_FOREGROUND,
|
||||||
context,
|
context,
|
||||||
WireGuardConnectivityWatcherService::class.java
|
WireGuardConnectivityWatcherService::class.java,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startWatcherService(
|
fun startWatcherService(context: Context) {
|
||||||
context: Context
|
|
||||||
) {
|
|
||||||
actionOnService(
|
actionOnService(
|
||||||
Action.START,
|
Action.START,
|
||||||
context,
|
context,
|
||||||
WireGuardConnectivityWatcherService::class.java
|
WireGuardConnectivityWatcherService::class.java,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,7 +110,7 @@ object ServiceManager {
|
|||||||
actionOnService(
|
actionOnService(
|
||||||
Action.STOP,
|
Action.STOP,
|
||||||
context,
|
context,
|
||||||
WireGuardConnectivityWatcherService::class.java
|
WireGuardConnectivityWatcherService::class.java,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,334 +34,365 @@ import javax.inject.Inject
|
|||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class WireGuardConnectivityWatcherService : ForegroundService() {
|
class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||||
private val foregroundId = 122
|
private val foregroundId = 122
|
||||||
|
|
||||||
@Inject lateinit var wifiService: NetworkService<WifiService>
|
@Inject lateinit var wifiService: NetworkService<WifiService>
|
||||||
|
|
||||||
@Inject lateinit var mobileDataService: NetworkService<MobileDataService>
|
@Inject lateinit var mobileDataService: NetworkService<MobileDataService>
|
||||||
|
|
||||||
@Inject lateinit var ethernetService: NetworkService<EthernetService>
|
@Inject lateinit var ethernetService: NetworkService<EthernetService>
|
||||||
|
|
||||||
@Inject lateinit var settingsRepository: SettingsRepository
|
@Inject lateinit var settingsRepository: SettingsRepository
|
||||||
|
|
||||||
@Inject lateinit var notificationService: NotificationService
|
@Inject lateinit var notificationService: NotificationService
|
||||||
|
|
||||||
@Inject lateinit var vpnService: VpnService
|
@Inject lateinit var vpnService: VpnService
|
||||||
|
|
||||||
private val networkEventsFlow = MutableStateFlow(WatcherState())
|
private val networkEventsFlow = MutableStateFlow(WatcherState())
|
||||||
data class WatcherState(
|
|
||||||
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
|
data class WatcherState(
|
||||||
|
val isWifiConnected: Boolean = false,
|
||||||
|
val isVpnConnected: Boolean = false,
|
||||||
|
val isEthernetConnected: Boolean = false,
|
||||||
|
val isMobileDataConnected: Boolean = false,
|
||||||
|
val currentNetworkSSID: String = "",
|
||||||
|
val settings: Settings = Settings()
|
||||||
|
)
|
||||||
|
|
||||||
private var wakeLock: PowerManager.WakeLock? = null
|
private lateinit var watcherJob: Job
|
||||||
private val tag = this.javaClass.name
|
|
||||||
|
|
||||||
override fun onCreate() {
|
private var wakeLock: PowerManager.WakeLock? = null
|
||||||
super.onCreate()
|
private val tag = this.javaClass.name
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
|
||||||
try {
|
override fun onCreate() {
|
||||||
if(settingsRepository.getSettings().isAutoTunnelPaused) {
|
super.onCreate()
|
||||||
launchWatcherPausedNotification()
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
} else launchWatcherNotification()
|
try {
|
||||||
} catch (e: Exception) {
|
if (settingsRepository.getSettings().isAutoTunnelPaused) {
|
||||||
Timber.e("Failed to start watcher service, not enough permissions")
|
launchWatcherPausedNotification()
|
||||||
}
|
} else launchWatcherNotification()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e("Failed to start watcher service, not enough permissions")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun startService(extras: Bundle?) {
|
override fun startService(extras: Bundle?) {
|
||||||
super.startService(extras)
|
super.startService(extras)
|
||||||
try {
|
try {
|
||||||
// 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 { initWakeLock() }
|
lifecycleScope.launch { initWakeLock() }
|
||||||
cancelWatcherJob()
|
cancelWatcherJob()
|
||||||
startWatcherJob()
|
startWatcherJob()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e("Failed to launch watcher service, no permissions")
|
Timber.e("Failed to launch watcher service, no permissions")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun stopService(extras: Bundle?) {
|
override fun stopService(extras: Bundle?) {
|
||||||
super.stopService(extras)
|
super.stopService(extras)
|
||||||
wakeLock?.let {
|
wakeLock?.let {
|
||||||
if (it.isHeld) {
|
if (it.isHeld) {
|
||||||
it.release()
|
it.release()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
cancelWatcherJob()
|
||||||
|
stopSelf()
|
||||||
}
|
}
|
||||||
cancelWatcherJob()
|
|
||||||
stopSelf()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun launchWatcherNotification(description: String = getString(R.string.watcher_notification_text_active)) {
|
private fun launchWatcherNotification(
|
||||||
val notification =
|
description: String = getString(R.string.watcher_notification_text_active)
|
||||||
notificationService.createNotification(
|
) {
|
||||||
channelId = getString(R.string.watcher_channel_id),
|
val notification =
|
||||||
channelName = getString(R.string.watcher_channel_name),
|
notificationService.createNotification(
|
||||||
title = getString(R.string.auto_tunnel_title),
|
channelId = getString(R.string.watcher_channel_id),
|
||||||
description = description)
|
channelName = getString(R.string.watcher_channel_name),
|
||||||
ServiceCompat.startForeground(
|
title = getString(R.string.auto_tunnel_title),
|
||||||
this, foregroundId, notification, Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID)
|
description = description,
|
||||||
}
|
)
|
||||||
|
ServiceCompat.startForeground(
|
||||||
|
this,
|
||||||
|
foregroundId,
|
||||||
|
notification,
|
||||||
|
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun launchWatcherPausedNotification() {
|
private fun launchWatcherPausedNotification() {
|
||||||
launchWatcherNotification(getString(R.string.watcher_notification_text_paused))
|
launchWatcherNotification(getString(R.string.watcher_notification_text_paused))
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO could this be restarting service in a bad state?
|
// 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")
|
||||||
val restartServiceIntent = Intent(rootIntent)
|
val restartServiceIntent = Intent(rootIntent)
|
||||||
val restartServicePendingIntent: PendingIntent =
|
val restartServicePendingIntent: PendingIntent =
|
||||||
PendingIntent.getService(
|
PendingIntent.getService(
|
||||||
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)
|
)
|
||||||
val alarmService: AlarmManager =
|
applicationContext.getSystemService(Context.ALARM_SERVICE)
|
||||||
applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
val alarmService: AlarmManager =
|
||||||
alarmService.set(
|
applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||||
AlarmManager.ELAPSED_REALTIME,
|
alarmService.set(
|
||||||
SystemClock.elapsedRealtime() + 1000,
|
AlarmManager.ELAPSED_REALTIME,
|
||||||
restartServicePendingIntent)
|
SystemClock.elapsedRealtime() + 1000,
|
||||||
}
|
restartServicePendingIntent,
|
||||||
|
)
|
||||||
private suspend fun initWakeLock() {
|
|
||||||
val isBatterySaverOn =
|
|
||||||
withContext(lifecycleScope.coroutineContext) {
|
|
||||||
settingsRepository.getSettings().isBatterySaverEnabled
|
|
||||||
}
|
|
||||||
wakeLock =
|
|
||||||
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
|
|
||||||
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
|
|
||||||
if (isBatterySaverOn) {
|
|
||||||
Timber.d("Initiating wakelock with timeout")
|
|
||||||
acquire(Constants.BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT)
|
|
||||||
} else {
|
|
||||||
Timber.d("Initiating wakelock with zero timeout")
|
|
||||||
acquire(Constants.DEFAULT_WATCHER_WAKE_LOCK_TIMEOUT)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun cancelWatcherJob() {
|
|
||||||
if (this::watcherJob.isInitialized) {
|
|
||||||
watcherJob.cancel()
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun startWatcherJob() {
|
private suspend fun initWakeLock() {
|
||||||
watcherJob =
|
val isBatterySaverOn =
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
withContext(lifecycleScope.coroutineContext) {
|
||||||
val setting = settingsRepository.getSettings()
|
settingsRepository.getSettings().isBatterySaverEnabled
|
||||||
launch {
|
|
||||||
Timber.d("Starting wifi watcher")
|
|
||||||
watchForWifiConnectivityChanges()
|
|
||||||
}
|
|
||||||
if (setting.isTunnelOnMobileDataEnabled) {
|
|
||||||
launch {
|
|
||||||
Timber.d("Starting mobile data watcher")
|
|
||||||
watchForMobileDataConnectivityChanges()
|
|
||||||
}
|
}
|
||||||
}
|
wakeLock =
|
||||||
if (setting.isTunnelOnEthernetEnabled) {
|
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
|
||||||
launch {
|
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
|
||||||
Timber.d("Starting ethernet data watcher")
|
try {
|
||||||
watchForEthernetConnectivityChanges()
|
if (isBatterySaverOn) {
|
||||||
|
Timber.d("Initiating wakelock with timeout")
|
||||||
|
acquire(Constants.BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT)
|
||||||
|
} else {
|
||||||
|
Timber.d("Initiating wakelock with zero timeout")
|
||||||
|
acquire(Constants.DEFAULT_WATCHER_WAKE_LOCK_TIMEOUT)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
release()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
launch {
|
|
||||||
Timber.d("Starting vpn state watcher")
|
|
||||||
watchForVpnConnectivityChanges()
|
|
||||||
}
|
|
||||||
launch {
|
|
||||||
Timber.d("Starting settings watcher")
|
|
||||||
watchForSettingsChanges()
|
|
||||||
}
|
|
||||||
launch {
|
|
||||||
Timber.d("Starting management watcher")
|
|
||||||
manageVpn()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun watchForMobileDataConnectivityChanges() {
|
|
||||||
mobileDataService.networkStatus.collect {
|
|
||||||
when (it) {
|
|
||||||
is NetworkStatus.Available -> {
|
|
||||||
Timber.d("Gained Mobile data connection")
|
|
||||||
networkEventsFlow.value = networkEventsFlow.value.copy(
|
|
||||||
isMobileDataConnected = true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is NetworkStatus.CapabilitiesChanged -> {
|
|
||||||
networkEventsFlow.value = networkEventsFlow.value.copy(
|
|
||||||
isMobileDataConnected = true
|
|
||||||
)
|
|
||||||
Timber.d("Mobile data capabilities changed")
|
|
||||||
}
|
|
||||||
is NetworkStatus.Unavailable -> {
|
|
||||||
networkEventsFlow.value = networkEventsFlow.value.copy(
|
|
||||||
isMobileDataConnected = false
|
|
||||||
)
|
|
||||||
Timber.d("Lost mobile data connection")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private fun cancelWatcherJob() {
|
||||||
|
if (this::watcherJob.isInitialized) {
|
||||||
|
watcherJob.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startWatcherJob() {
|
||||||
|
watcherJob =
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
val setting = settingsRepository.getSettings()
|
||||||
|
launch {
|
||||||
|
Timber.d("Starting wifi watcher")
|
||||||
|
watchForWifiConnectivityChanges()
|
||||||
|
}
|
||||||
|
if (setting.isTunnelOnMobileDataEnabled) {
|
||||||
|
launch {
|
||||||
|
Timber.d("Starting mobile data watcher")
|
||||||
|
watchForMobileDataConnectivityChanges()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (setting.isTunnelOnEthernetEnabled) {
|
||||||
|
launch {
|
||||||
|
Timber.d("Starting ethernet data watcher")
|
||||||
|
watchForEthernetConnectivityChanges()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
Timber.d("Starting vpn state watcher")
|
||||||
|
watchForVpnConnectivityChanges()
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
Timber.d("Starting settings watcher")
|
||||||
|
watchForSettingsChanges()
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
Timber.d("Starting management watcher")
|
||||||
|
manageVpn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun watchForMobileDataConnectivityChanges() {
|
||||||
|
mobileDataService.networkStatus.collect {
|
||||||
|
when (it) {
|
||||||
|
is NetworkStatus.Available -> {
|
||||||
|
Timber.d("Gained Mobile data connection")
|
||||||
|
networkEventsFlow.value =
|
||||||
|
networkEventsFlow.value.copy(
|
||||||
|
isMobileDataConnected = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is NetworkStatus.CapabilitiesChanged -> {
|
||||||
|
networkEventsFlow.value =
|
||||||
|
networkEventsFlow.value.copy(
|
||||||
|
isMobileDataConnected = true,
|
||||||
|
)
|
||||||
|
Timber.d("Mobile data capabilities changed")
|
||||||
|
}
|
||||||
|
is NetworkStatus.Unavailable -> {
|
||||||
|
networkEventsFlow.value =
|
||||||
|
networkEventsFlow.value.copy(
|
||||||
|
isMobileDataConnected = false,
|
||||||
|
)
|
||||||
|
Timber.d("Lost mobile data connection")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun watchForSettingsChanges() {
|
private suspend fun watchForSettingsChanges() {
|
||||||
settingsRepository.getSettingsFlow().collect {
|
settingsRepository.getSettingsFlow().collect {
|
||||||
if(networkEventsFlow.value.settings.isAutoTunnelPaused != it.isAutoTunnelPaused) {
|
if (networkEventsFlow.value.settings.isAutoTunnelPaused != it.isAutoTunnelPaused) {
|
||||||
when(it.isAutoTunnelPaused) {
|
when (it.isAutoTunnelPaused) {
|
||||||
true -> launchWatcherPausedNotification()
|
true -> launchWatcherPausedNotification()
|
||||||
false -> launchWatcherNotification()
|
false -> launchWatcherNotification()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
networkEventsFlow.value = networkEventsFlow.value.copy(
|
networkEventsFlow.value =
|
||||||
settings = it
|
networkEventsFlow.value.copy(
|
||||||
)
|
settings = it,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun watchForVpnConnectivityChanges() {
|
private suspend fun watchForVpnConnectivityChanges() {
|
||||||
vpnService.vpnState.collect {
|
vpnService.vpnState.collect {
|
||||||
when(it.status) {
|
when (it.status) {
|
||||||
Tunnel.State.DOWN -> networkEventsFlow.value = networkEventsFlow.value.copy(
|
Tunnel.State.DOWN ->
|
||||||
isVpnConnected = false
|
networkEventsFlow.value =
|
||||||
)
|
networkEventsFlow.value.copy(
|
||||||
Tunnel.State.UP -> networkEventsFlow.value = networkEventsFlow.value.copy(
|
isVpnConnected = false,
|
||||||
isVpnConnected = true
|
)
|
||||||
)
|
Tunnel.State.UP ->
|
||||||
|
networkEventsFlow.value =
|
||||||
|
networkEventsFlow.value.copy(
|
||||||
|
isVpnConnected = true,
|
||||||
|
)
|
||||||
else -> {}
|
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")
|
||||||
networkEventsFlow.value = networkEventsFlow.value.copy(
|
networkEventsFlow.value =
|
||||||
isEthernetConnected = true
|
networkEventsFlow.value.copy(
|
||||||
)
|
isEthernetConnected = true,
|
||||||
}
|
)
|
||||||
is NetworkStatus.CapabilitiesChanged -> {
|
|
||||||
Timber.d("Ethernet capabilities changed")
|
|
||||||
networkEventsFlow.value = networkEventsFlow.value.copy(
|
|
||||||
isEthernetConnected = true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is NetworkStatus.Unavailable -> {
|
|
||||||
networkEventsFlow.value = networkEventsFlow.value.copy(
|
|
||||||
isEthernetConnected = false
|
|
||||||
)
|
|
||||||
Timber.d("Lost Ethernet connection")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun watchForWifiConnectivityChanges() {
|
|
||||||
wifiService.networkStatus.collect {
|
|
||||||
when (it) {
|
|
||||||
is NetworkStatus.Available -> {
|
|
||||||
Timber.d("Gained Wi-Fi connection")
|
|
||||||
networkEventsFlow.value = networkEventsFlow.value.copy(
|
|
||||||
isWifiConnected = true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is NetworkStatus.CapabilitiesChanged -> {
|
|
||||||
Timber.d("Wifi capabilities changed")
|
|
||||||
networkEventsFlow.value = networkEventsFlow.value.copy(
|
|
||||||
isWifiConnected = true
|
|
||||||
)
|
|
||||||
val ssid = wifiService.getNetworkName(it.networkCapabilities) ?: ""
|
|
||||||
Timber.d("Detected SSID: $ssid")
|
|
||||||
networkEventsFlow.value = networkEventsFlow.value.copy(
|
|
||||||
currentNetworkSSID = ssid
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is NetworkStatus.Unavailable -> {
|
|
||||||
networkEventsFlow.value = networkEventsFlow.value.copy(
|
|
||||||
isWifiConnected = false
|
|
||||||
)
|
|
||||||
Timber.d("Lost Wi-Fi connection")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//TODO clean this up
|
|
||||||
private suspend fun manageVpn() {
|
|
||||||
networkEventsFlow.collectLatest {
|
|
||||||
Timber.i("New watcher state: $it")
|
|
||||||
if (!it.settings.isAutoTunnelPaused && it.settings.defaultTunnel != null) {
|
|
||||||
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
|
||||||
when {
|
|
||||||
((it.isEthernetConnected &&
|
|
||||||
it.settings.isTunnelOnEthernetEnabled &&
|
|
||||||
!it.isVpnConnected)) -> {
|
|
||||||
ServiceManager.startVpnService(this, it.settings.defaultTunnel!!)
|
|
||||||
Timber.i("Condition 1 met")
|
|
||||||
}
|
}
|
||||||
(!it.isEthernetConnected &&
|
is NetworkStatus.CapabilitiesChanged -> {
|
||||||
it.settings.isTunnelOnMobileDataEnabled &&
|
Timber.d("Ethernet capabilities changed")
|
||||||
!it.isWifiConnected &&
|
networkEventsFlow.value =
|
||||||
it.isMobileDataConnected &&
|
networkEventsFlow.value.copy(
|
||||||
!it.isVpnConnected) -> {
|
isEthernetConnected = true,
|
||||||
ServiceManager.startVpnService(this, it.settings.defaultTunnel!!)
|
)
|
||||||
Timber.i("Condition 2 met")
|
|
||||||
}
|
}
|
||||||
(!it.isEthernetConnected &&
|
is NetworkStatus.Unavailable -> {
|
||||||
!it.settings.isTunnelOnMobileDataEnabled &&
|
networkEventsFlow.value =
|
||||||
!it.isWifiConnected &&
|
networkEventsFlow.value.copy(
|
||||||
it.isVpnConnected) -> {
|
isEthernetConnected = false,
|
||||||
ServiceManager.stopVpnService(this)
|
)
|
||||||
Timber.i("Condition 3 met")
|
Timber.d("Lost Ethernet connection")
|
||||||
}
|
}
|
||||||
(!it.isEthernetConnected &&
|
}
|
||||||
it.isWifiConnected &&
|
}
|
||||||
!it.settings.trustedNetworkSSIDs.contains(it.currentNetworkSSID) &&
|
}
|
||||||
it.settings.isTunnelOnWifiEnabled &&
|
|
||||||
(!it.isVpnConnected)) -> {
|
private suspend fun watchForWifiConnectivityChanges() {
|
||||||
ServiceManager.startVpnService(this, it.settings.defaultTunnel!!)
|
wifiService.networkStatus.collect {
|
||||||
Timber.i("Condition 4 met")
|
when (it) {
|
||||||
}
|
is NetworkStatus.Available -> {
|
||||||
(!it.isEthernetConnected &&
|
Timber.d("Gained Wi-Fi connection")
|
||||||
(it.isWifiConnected && it.settings.trustedNetworkSSIDs.contains(it.currentNetworkSSID)) &&
|
networkEventsFlow.value =
|
||||||
(it.isVpnConnected)) -> {
|
networkEventsFlow.value.copy(
|
||||||
ServiceManager.stopVpnService(this)
|
isWifiConnected = true,
|
||||||
Timber.i("Condition 5 met")
|
)
|
||||||
}
|
}
|
||||||
(!it.isEthernetConnected &&
|
is NetworkStatus.CapabilitiesChanged -> {
|
||||||
(it.isWifiConnected &&
|
Timber.d("Wifi capabilities changed")
|
||||||
!it.settings.isTunnelOnWifiEnabled &&
|
networkEventsFlow.value =
|
||||||
(it.isVpnConnected))) -> {
|
networkEventsFlow.value.copy(
|
||||||
ServiceManager.stopVpnService(this)
|
isWifiConnected = true,
|
||||||
Timber.i("Condition 6 met")
|
)
|
||||||
}
|
val ssid = wifiService.getNetworkName(it.networkCapabilities) ?: ""
|
||||||
(!it.isEthernetConnected &&
|
Timber.d("Detected SSID: $ssid")
|
||||||
!it.isWifiConnected &&
|
networkEventsFlow.value =
|
||||||
!it.isMobileDataConnected &&
|
networkEventsFlow.value.copy(
|
||||||
(it.isVpnConnected)) -> {
|
currentNetworkSSID = ssid,
|
||||||
ServiceManager.stopVpnService(this)
|
)
|
||||||
Timber.i("Condition 7 met")
|
}
|
||||||
}
|
is NetworkStatus.Unavailable -> {
|
||||||
else -> {
|
networkEventsFlow.value =
|
||||||
Timber.i("No condition met")
|
networkEventsFlow.value.copy(
|
||||||
|
isWifiConnected = false,
|
||||||
|
)
|
||||||
|
Timber.d("Lost Wi-Fi connection")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO clean this up
|
||||||
|
private suspend fun manageVpn() {
|
||||||
|
networkEventsFlow.collectLatest {
|
||||||
|
Timber.i("New watcher state: $it")
|
||||||
|
if (!it.settings.isAutoTunnelPaused && it.settings.defaultTunnel != null) {
|
||||||
|
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
||||||
|
when {
|
||||||
|
((it.isEthernetConnected &&
|
||||||
|
it.settings.isTunnelOnEthernetEnabled &&
|
||||||
|
!it.isVpnConnected)) -> {
|
||||||
|
ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!)
|
||||||
|
Timber.i("Condition 1 met")
|
||||||
|
}
|
||||||
|
(!it.isEthernetConnected &&
|
||||||
|
it.settings.isTunnelOnMobileDataEnabled &&
|
||||||
|
!it.isWifiConnected &&
|
||||||
|
it.isMobileDataConnected &&
|
||||||
|
!it.isVpnConnected) -> {
|
||||||
|
ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!)
|
||||||
|
Timber.i("Condition 2 met")
|
||||||
|
}
|
||||||
|
(!it.isEthernetConnected &&
|
||||||
|
!it.settings.isTunnelOnMobileDataEnabled &&
|
||||||
|
!it.isWifiConnected &&
|
||||||
|
it.isVpnConnected) -> {
|
||||||
|
ServiceManager.stopVpnService(this)
|
||||||
|
Timber.i("Condition 3 met")
|
||||||
|
}
|
||||||
|
(!it.isEthernetConnected &&
|
||||||
|
it.isWifiConnected &&
|
||||||
|
!it.settings.trustedNetworkSSIDs.contains(it.currentNetworkSSID) &&
|
||||||
|
it.settings.isTunnelOnWifiEnabled &&
|
||||||
|
(!it.isVpnConnected)) -> {
|
||||||
|
ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!)
|
||||||
|
Timber.i("Condition 4 met")
|
||||||
|
}
|
||||||
|
(!it.isEthernetConnected &&
|
||||||
|
(it.isWifiConnected &&
|
||||||
|
it.settings.trustedNetworkSSIDs.contains(it.currentNetworkSSID)) &&
|
||||||
|
(it.isVpnConnected)) -> {
|
||||||
|
ServiceManager.stopVpnService(this)
|
||||||
|
Timber.i("Condition 5 met")
|
||||||
|
}
|
||||||
|
(!it.isEthernetConnected &&
|
||||||
|
(it.isWifiConnected &&
|
||||||
|
!it.settings.isTunnelOnWifiEnabled &&
|
||||||
|
(it.isVpnConnected))) -> {
|
||||||
|
ServiceManager.stopVpnService(this)
|
||||||
|
Timber.i("Condition 6 met")
|
||||||
|
}
|
||||||
|
(!it.isEthernetConnected &&
|
||||||
|
!it.isWifiConnected &&
|
||||||
|
!it.isMobileDataConnected &&
|
||||||
|
(it.isVpnConnected)) -> {
|
||||||
|
ServiceManager.stopVpnService(this)
|
||||||
|
Timber.i("Condition 7 met")
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Timber.i("No condition met")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,17 +28,13 @@ import javax.inject.Inject
|
|||||||
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 settingsRepository: SettingsRepository
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var tunnelConfigRepository: TunnelConfigRepository
|
||||||
lateinit var tunnelConfigRepository: TunnelConfigRepository
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var notificationService: NotificationService
|
||||||
lateinit var notificationService: NotificationService
|
|
||||||
|
|
||||||
private lateinit var job: Job
|
private lateinit var job: Job
|
||||||
|
|
||||||
@@ -48,7 +44,7 @@ class WireGuardTunnelService : ForegroundService() {
|
|||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
if(tunnelConfigRepository.getAll().isNotEmpty()) {
|
if (tunnelConfigRepository.getAll().isNotEmpty()) {
|
||||||
launchVpnNotification()
|
launchVpnNotification()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,11 +54,10 @@ class WireGuardTunnelService : ForegroundService() {
|
|||||||
super.startService(extras)
|
super.startService(extras)
|
||||||
cancelJob()
|
cancelJob()
|
||||||
val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key))
|
val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key))
|
||||||
val tunnelConfig = tunnelConfigString?.let {
|
val tunnelConfig = tunnelConfigString?.let { TunnelConfig.from(it) }
|
||||||
TunnelConfig.from(it)
|
|
||||||
}
|
|
||||||
tunnelName = tunnelConfig?.name ?: ""
|
tunnelName = tunnelConfig?.name ?: ""
|
||||||
job = lifecycleScope.launch(Dispatchers.IO) {
|
job =
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
launch {
|
launch {
|
||||||
if (tunnelConfig != null) {
|
if (tunnelConfig != null) {
|
||||||
try {
|
try {
|
||||||
@@ -77,22 +72,22 @@ class WireGuardTunnelService : ForegroundService() {
|
|||||||
val settings = settingsRepository.getSettings()
|
val settings = settingsRepository.getSettings()
|
||||||
val tunnels = tunnelConfigRepository.getAll()
|
val tunnels = tunnelConfigRepository.getAll()
|
||||||
if (settings.isAlwaysOnVpnEnabled) {
|
if (settings.isAlwaysOnVpnEnabled) {
|
||||||
val tunnel = if(settings.defaultTunnel != null) {
|
val tunnel =
|
||||||
TunnelConfig.from(settings.defaultTunnel!!)
|
if (settings.defaultTunnel != null) {
|
||||||
} else if(tunnels.isNotEmpty()) {
|
TunnelConfig.from(settings.defaultTunnel!!)
|
||||||
tunnels.first()
|
} else if (tunnels.isNotEmpty()) {
|
||||||
} else {
|
tunnels.first()
|
||||||
null
|
} else {
|
||||||
}
|
null
|
||||||
if(tunnel != null) {
|
}
|
||||||
|
if (tunnel != null) {
|
||||||
tunnelName = tunnel.name
|
tunnelName = tunnel.name
|
||||||
vpnService.startTunnel(tunnel)
|
vpnService.startTunnel(tunnel)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//TODO add failed to connect notification
|
// TODO add failed to connect notification
|
||||||
launch {
|
launch {
|
||||||
vpnService.vpnState.collect { state ->
|
vpnService.vpnState.collect { state ->
|
||||||
state.statistics
|
state.statistics
|
||||||
@@ -101,14 +96,18 @@ class WireGuardTunnelService : ForegroundService() {
|
|||||||
.let { statuses ->
|
.let { statuses ->
|
||||||
when {
|
when {
|
||||||
statuses?.all { it == HandshakeStatus.HEALTHY } == true -> {
|
statuses?.all { it == HandshakeStatus.HEALTHY } == true -> {
|
||||||
if(!didShowConnected){
|
if (!didShowConnected) {
|
||||||
delay(Constants.VPN_CONNECTED_NOTIFICATION_DELAY)
|
delay(Constants.VPN_CONNECTED_NOTIFICATION_DELAY)
|
||||||
launchVpnNotification(getString(R.string.tunnel_start_title),"${getString(R.string.tunnel_start_text)} $tunnelName")
|
launchVpnNotification(
|
||||||
|
getString(R.string.tunnel_start_title),
|
||||||
|
"${getString(R.string.tunnel_start_text)} $tunnelName",
|
||||||
|
)
|
||||||
didShowConnected = true
|
didShowConnected = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
statuses?.any { it == HandshakeStatus.STALE } == true -> {}
|
statuses?.any { it == HandshakeStatus.STALE } == true -> {}
|
||||||
statuses?.all { it == HandshakeStatus.NOT_STARTED } == true -> {}
|
statuses?.all { it == HandshakeStatus.NOT_STARTED } ==
|
||||||
|
true -> {}
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,7 +126,10 @@ class WireGuardTunnelService : ForegroundService() {
|
|||||||
stopSelf()
|
stopSelf()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchVpnNotification(title : String = getString(R.string.vpn_starting),description : String = getString(R.string.attempt_connection)) {
|
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),
|
||||||
@@ -136,13 +138,13 @@ class WireGuardTunnelService : ForegroundService() {
|
|||||||
onGoing = false,
|
onGoing = false,
|
||||||
vibration = false,
|
vibration = false,
|
||||||
showTimestamp = true,
|
showTimestamp = true,
|
||||||
description = description
|
description = description,
|
||||||
)
|
)
|
||||||
ServiceCompat.startForeground(
|
ServiceCompat.startForeground(
|
||||||
this,
|
this,
|
||||||
foregroundId,
|
foregroundId,
|
||||||
notification,
|
notification,
|
||||||
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID
|
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,24 +154,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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package com.zaneschepke.wireguardautotunnel.service.notification
|
|||||||
import android.app.Notification
|
import android.app.Notification
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
|
|
||||||
interface NotificationService {
|
interface NotificationService {
|
||||||
fun createNotification(
|
fun createNotification(
|
||||||
|
|||||||
@@ -13,23 +13,21 @@ 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 =
|
private val watcherBuilder: NotificationCompat.Builder =
|
||||||
NotificationCompat.Builder(
|
NotificationCompat.Builder(
|
||||||
context,
|
context,
|
||||||
context.getString(R.string.watcher_channel_id)
|
context.getString(R.string.watcher_channel_id),
|
||||||
|
)
|
||||||
|
private val tunnelBuilder: NotificationCompat.Builder =
|
||||||
|
NotificationCompat.Builder(
|
||||||
|
context,
|
||||||
|
context.getString(R.string.vpn_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,
|
||||||
@@ -47,17 +45,18 @@ constructor(
|
|||||||
): 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)
|
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 ->
|
||||||
@@ -65,26 +64,26 @@ constructor(
|
|||||||
context,
|
context,
|
||||||
0,
|
0,
|
||||||
notificationIntent,
|
notificationIntent,
|
||||||
PendingIntent.FLAG_IMMUTABLE
|
PendingIntent.FLAG_IMMUTABLE,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val builder = when(channelId) {
|
val builder =
|
||||||
context.getString(R.string.watcher_channel_id) -> watcherBuilder
|
when (channelId) {
|
||||||
context.getString(R.string.vpn_channel_id) -> tunnelBuilder
|
context.getString(R.string.watcher_channel_id) -> watcherBuilder
|
||||||
else -> {
|
context.getString(R.string.vpn_channel_id) -> tunnelBuilder
|
||||||
NotificationCompat.Builder(
|
else -> {
|
||||||
context,
|
NotificationCompat.Builder(
|
||||||
channelId
|
context,
|
||||||
)
|
channelId,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return builder.let {
|
return builder.let {
|
||||||
if (action != null && actionText != null) {
|
if (action != null && actionText != null) {
|
||||||
it.addAction(
|
it.addAction(
|
||||||
NotificationCompat.Action.Builder(0, actionText, action)
|
NotificationCompat.Action.Builder(0, actionText, action).build(),
|
||||||
.build()
|
|
||||||
)
|
)
|
||||||
it.setAutoCancel(true)
|
it.setAutoCancel(true)
|
||||||
}
|
}
|
||||||
@@ -95,7 +94,7 @@ constructor(
|
|||||||
.setOngoing(onGoing)
|
.setOngoing(onGoing)
|
||||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
.setShowWhen(showTimestamp)
|
.setShowWhen(showTimestamp)
|
||||||
.setSmallIcon(R.mipmap.ic_launcher_foreground)
|
.setSmallIcon(R.drawable.ic_launcher)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,33 +12,34 @@ import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
|||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
|
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class ShortcutsActivity : ComponentActivity() {
|
class ShortcutsActivity : ComponentActivity() {
|
||||||
@Inject
|
@Inject lateinit var settingsRepository: SettingsRepository
|
||||||
lateinit var settingsRepository: SettingsRepository
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var tunnelConfigRepository: TunnelConfigRepository
|
||||||
lateinit var tunnelConfigRepository: TunnelConfigRepository
|
|
||||||
|
|
||||||
private suspend fun toggleWatcherServicePause() {
|
private suspend fun toggleWatcherServicePause() {
|
||||||
val settings = settingsRepository.getSettings()
|
val settings = settingsRepository.getSettings()
|
||||||
if (settings.isAutoTunnelEnabled) {
|
if (settings.isAutoTunnelEnabled) {
|
||||||
val pauseAutoTunnel = !settings.isAutoTunnelPaused
|
val pauseAutoTunnel = !settings.isAutoTunnelPaused
|
||||||
settingsRepository.save(settings.copy(
|
settingsRepository.save(
|
||||||
isAutoTunnelPaused = pauseAutoTunnel
|
settings.copy(
|
||||||
))
|
isAutoTunnelPaused = pauseAutoTunnel,
|
||||||
}
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(View(this))
|
setContentView(View(this))
|
||||||
if (intent.getStringExtra(CLASS_NAME_EXTRA_KEY)
|
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) {
|
||||||
@@ -48,7 +49,9 @@ class ShortcutsActivity : ComponentActivity() {
|
|||||||
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) {
|
||||||
tunnelConfigRepository.getAll().firstOrNull { it.name == tunnelName }
|
tunnelConfigRepository.getAll().firstOrNull {
|
||||||
|
it.name == tunnelName
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (settings.defaultTunnel == null) {
|
if (settings.defaultTunnel == null) {
|
||||||
tunnelConfigRepository.getAll().first()
|
tunnelConfigRepository.getAll().first()
|
||||||
@@ -59,13 +62,15 @@ class ShortcutsActivity : ComponentActivity() {
|
|||||||
tunnelConfig ?: return@launch
|
tunnelConfig ?: return@launch
|
||||||
toggleWatcherServicePause()
|
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)
|
||||||
|
|||||||
@@ -20,44 +20,43 @@ import javax.inject.Inject
|
|||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class TunnelControlTile() : TileService() {
|
class TunnelControlTile() : TileService() {
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var tunnelConfigRepository: TunnelConfigRepository
|
||||||
lateinit var tunnelConfigRepository: TunnelConfigRepository
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var settingsRepository: SettingsRepository
|
||||||
lateinit var settingsRepository: SettingsRepository
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var vpnService: VpnService
|
||||||
lateinit var vpnService: VpnService
|
|
||||||
|
|
||||||
private val scope = CoroutineScope(Dispatchers.IO)
|
private val scope = CoroutineScope(Dispatchers.IO)
|
||||||
|
|
||||||
private var tunnelName : String? = null
|
private var tunnelName: String? = null
|
||||||
|
|
||||||
override fun onStartListening() {
|
override fun onStartListening() {
|
||||||
super.onStartListening()
|
super.onStartListening()
|
||||||
Timber.d("On start listening called")
|
Timber.d("On start listening called")
|
||||||
scope.launch {
|
scope.launch {
|
||||||
vpnService.vpnState.collect {
|
vpnService.vpnState.collect {
|
||||||
when(it.status) {
|
when (it.status) {
|
||||||
Tunnel.State.UP -> setActive()
|
Tunnel.State.UP -> setActive()
|
||||||
Tunnel.State.DOWN -> setInactive()
|
Tunnel.State.DOWN -> setInactive()
|
||||||
else -> setInactive()
|
else -> setInactive()
|
||||||
}
|
}
|
||||||
val tunnels = tunnelConfigRepository.getAll()
|
val tunnels = tunnelConfigRepository.getAll()
|
||||||
if(tunnels.isEmpty()) {
|
if (tunnels.isEmpty()) {
|
||||||
setUnavailable()
|
setUnavailable()
|
||||||
return@collect
|
return@collect
|
||||||
}
|
}
|
||||||
tunnelName = it.name.ifBlank {
|
tunnelName =
|
||||||
val settings = settingsRepository.getSettings()
|
it.name.ifBlank {
|
||||||
if (settings.defaultTunnel != null) {
|
val settings = settingsRepository.getSettings()
|
||||||
TunnelConfig.from(settings.defaultTunnel!!).name
|
if (settings.defaultTunnel != null) {
|
||||||
} else tunnels.firstOrNull()?.name
|
TunnelConfig.from(settings.defaultTunnel!!).name
|
||||||
}
|
} else tunnels.firstOrNull()?.name
|
||||||
|
}
|
||||||
setTileDescription(tunnelName ?: "")
|
setTileDescription(tunnelName ?: "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
scope.cancel()
|
scope.cancel()
|
||||||
@@ -73,14 +72,15 @@ class TunnelControlTile() : TileService() {
|
|||||||
unlockAndRun {
|
unlockAndRun {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
val tunnelConfig = tunnelConfigRepository.getAll().first { it.name == tunnelName }
|
val tunnelConfig =
|
||||||
|
tunnelConfigRepository.getAll().first { it.name == tunnelName }
|
||||||
toggleWatcherServicePause()
|
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,
|
||||||
tunnelConfig.toString()
|
tunnelConfig.toString(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -97,9 +97,11 @@ class TunnelControlTile() : TileService() {
|
|||||||
val settings = settingsRepository.getSettings()
|
val settings = settingsRepository.getSettings()
|
||||||
if (settings.isAutoTunnelEnabled) {
|
if (settings.isAutoTunnelEnabled) {
|
||||||
val pauseAutoTunnel = !settings.isAutoTunnelPaused
|
val pauseAutoTunnel = !settings.isAutoTunnelPaused
|
||||||
settingsRepository.save(settings.copy(
|
settingsRepository.save(
|
||||||
isAutoTunnelPaused = pauseAutoTunnel
|
settings.copy(
|
||||||
))
|
isAutoTunnelPaused = pauseAutoTunnel,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ enum class HandshakeStatus {
|
|||||||
HEALTHY,
|
HEALTHY,
|
||||||
STALE,
|
STALE,
|
||||||
UNKNOWN,
|
UNKNOWN,
|
||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import com.wireguard.android.backend.Statistics
|
|||||||
import com.wireguard.android.backend.Tunnel
|
import com.wireguard.android.backend.Tunnel
|
||||||
|
|
||||||
data class VpnState(
|
data class VpnState(
|
||||||
val status : Tunnel.State = Tunnel.State.DOWN,
|
val status: Tunnel.State = Tunnel.State.DOWN,
|
||||||
val name : String = "",
|
val name: String = "",
|
||||||
val statistics : Statistics? = null
|
val statistics: Statistics? = null
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ 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 settingsRepository: SettingsRepository
|
private val settingsRepository: SettingsRepository
|
||||||
) : VpnService {
|
) : VpnService {
|
||||||
@@ -67,7 +67,7 @@ constructor(
|
|||||||
backend.setState(
|
backend.setState(
|
||||||
this,
|
this,
|
||||||
State.UP,
|
State.UP,
|
||||||
config
|
config,
|
||||||
)
|
)
|
||||||
emitTunnelState(state)
|
emitTunnelState(state)
|
||||||
state
|
state
|
||||||
@@ -80,24 +80,24 @@ constructor(
|
|||||||
private fun emitTunnelState(state: State) {
|
private fun emitTunnelState(state: State) {
|
||||||
_vpnState.tryEmit(
|
_vpnState.tryEmit(
|
||||||
_vpnState.value.copy(
|
_vpnState.value.copy(
|
||||||
status = state
|
status = state,
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun emitBackendStatistics(statistics: Statistics) {
|
private fun emitBackendStatistics(statistics: Statistics) {
|
||||||
_vpnState.tryEmit(
|
_vpnState.tryEmit(
|
||||||
_vpnState.value.copy(
|
_vpnState.value.copy(
|
||||||
statistics = statistics
|
statistics = statistics,
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun emitTunnelName(name: String) {
|
private suspend fun emitTunnelName(name: String) {
|
||||||
_vpnState.emit(
|
_vpnState.emit(
|
||||||
_vpnState.value.copy(
|
_vpnState.value.copy(
|
||||||
name = name
|
name = name,
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui
|
package com.zaneschepke.wireguardautotunnel.ui
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import com.zaneschepke.wireguardautotunnel.data.SettingsDao
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class ActivityViewModel @Inject constructor(
|
class ActivityViewModel
|
||||||
private val settingsRepo: SettingsDao,
|
@Inject
|
||||||
) : ViewModel() {
|
constructor() : ViewModel() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +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 androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.SystemBarStyle
|
||||||
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.foundation.focusable
|
import androidx.compose.foundation.focusable
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
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
|
||||||
@@ -20,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.focus.focusProperties
|
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
|
||||||
@@ -35,8 +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.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
|
||||||
@@ -44,68 +47,74 @@ 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 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 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 {
|
||||||
// val activityViewModel = hiltViewModel<ActivityViewModel>()
|
//val activityViewModel = 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) {
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
val result = snackbarHostState.showSnackbar(
|
val result =
|
||||||
|
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.Dismissed -> {
|
SnackbarResult.ActionPerformed,
|
||||||
|
SnackbarResult.Dismissed -> {
|
||||||
snackbarHostState.currentSnackbarData?.dismiss()
|
snackbarHostState.currentSnackbarData?.dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,87 +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.focusable().focusProperties { up = focusRequester },
|
modifier = Modifier
|
||||||
|
.focusable()
|
||||||
|
.focusProperties { up = focusRequester },
|
||||||
bottomBar =
|
bottomBar =
|
||||||
if (vpnIntent == null && notificationPermissionState.status.isGranted) {
|
if (notificationPermissionState == null || notificationPermissionState.status.isGranted) {
|
||||||
{ BottomNavBar(navController, listOf(
|
{
|
||||||
Screen.Main.navItem,
|
BottomNavBar(
|
||||||
Screen.Settings.navItem,
|
navController,
|
||||||
Screen.Support.navItem)) }
|
listOf(
|
||||||
} else {
|
Screen.Main.navItem,
|
||||||
{}
|
Screen.Settings.navItem,
|
||||||
}
|
Screen.Support.navItem,
|
||||||
) { padding ->
|
),
|
||||||
if (vpnIntent != null) {
|
|
||||||
PermissionRequestFailedScreen(
|
|
||||||
padding = padding,
|
|
||||||
onRequestAgain = { vpnActivityResultState.launch(vpnIntent) },
|
|
||||||
message = getString(R.string.vpn_permission_required),
|
|
||||||
getString(R.string.retry)
|
|
||||||
)
|
|
||||||
return@Scaffold
|
|
||||||
}
|
|
||||||
if (!notificationPermissionState.status.isGranted) {
|
|
||||||
PermissionRequestFailedScreen(
|
|
||||||
padding = padding,
|
|
||||||
onRequestAgain = {
|
|
||||||
val intentSettings =
|
|
||||||
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
|
||||||
intentSettings.data =
|
|
||||||
Uri.fromParts(
|
|
||||||
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 = Screen.Main.route) {
|
|
||||||
composable(
|
|
||||||
Screen.Main.route,
|
|
||||||
) {
|
|
||||||
MainScreen(padding = padding, 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()) {
|
|
||||||
//https://dagger.dev/hilt/view-model#assisted-injection
|
|
||||||
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,28 +6,33 @@ import androidx.compose.material.icons.rounded.QuestionMark
|
|||||||
import androidx.compose.material.icons.rounded.Settings
|
import androidx.compose.material.icons.rounded.Settings
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
|
||||||
|
|
||||||
sealed class Screen(val route : String) {
|
sealed class Screen(val route: String) {
|
||||||
data object Main: Screen("main") {
|
data object Main : Screen("main") {
|
||||||
val navItem = BottomNavItem(
|
val navItem =
|
||||||
name = "Tunnels",
|
BottomNavItem(
|
||||||
route = route,
|
name = "Tunnels",
|
||||||
icon = Icons.Rounded.Home
|
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")
|
|
||||||
|
|
||||||
|
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")
|
||||||
}
|
}
|
||||||
@@ -23,7 +23,7 @@ fun ClickableIconButton(
|
|||||||
) {
|
) {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = 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))
|
||||||
@@ -31,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()
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = 15.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)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()) 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))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,10 +13,11 @@ import androidx.compose.ui.unit.dp
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LoadingScreen() {
|
fun LoadingScreen() {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier = Modifier.fillMaxSize().focusable().padding()) {
|
modifier = Modifier.fillMaxSize().focusable().padding(),
|
||||||
Column(modifier = Modifier.padding(120.dp)) { CircularProgressIndicator() }
|
) {
|
||||||
|
Column(modifier = Modifier.padding(120.dp)) { CircularProgressIndicator() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.config
|
package com.zaneschepke.wireguardautotunnel.ui.screens.config
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.focusGroup
|
import androidx.compose.foundation.focusGroup
|
||||||
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.IntrinsicSize
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
@@ -29,7 +29,7 @@ import androidx.compose.material.icons.rounded.ContentCopy
|
|||||||
import androidx.compose.material.icons.rounded.Delete
|
import androidx.compose.material.icons.rounded.Delete
|
||||||
import androidx.compose.material.icons.rounded.Refresh
|
import androidx.compose.material.icons.rounded.Refresh
|
||||||
import androidx.compose.material.icons.rounded.Save
|
import androidx.compose.material.icons.rounded.Save
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.BasicAlertDialog
|
||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.FabPosition
|
import androidx.compose.material3.FabPosition
|
||||||
@@ -51,7 +51,6 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
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.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
@@ -63,6 +62,7 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
import androidx.compose.ui.text.input.VisualTransformation
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
@@ -86,358 +86,418 @@ import kotlinx.coroutines.delay
|
|||||||
|
|
||||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||||
@OptIn(
|
@OptIn(
|
||||||
ExperimentalComposeUiApi::class,
|
|
||||||
ExperimentalMaterial3Api::class,
|
ExperimentalMaterial3Api::class,
|
||||||
ExperimentalFoundationApi::class)
|
)
|
||||||
@Composable
|
@Composable
|
||||||
fun ConfigScreen(
|
fun ConfigScreen(
|
||||||
|
padding: PaddingValues,
|
||||||
viewModel: ConfigViewModel = hiltViewModel(),
|
viewModel: ConfigViewModel = hiltViewModel(),
|
||||||
focusRequester: FocusRequester,
|
focusRequester: FocusRequester,
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
showSnackbarMessage: (String) -> Unit,
|
showSnackbarMessage: (String) -> Unit,
|
||||||
id: String
|
id: String
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val clipboardManager: ClipboardManager = LocalClipboardManager.current
|
val clipboardManager: ClipboardManager = LocalClipboardManager.current
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
var showApplicationsDialog by remember { mutableStateOf(false) }
|
var showApplicationsDialog by remember { mutableStateOf(false) }
|
||||||
var showAuthPrompt by remember { mutableStateOf(false) }
|
var showAuthPrompt by remember { mutableStateOf(false) }
|
||||||
var isAuthenticated by remember { mutableStateOf(false) }
|
var isAuthenticated by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) { viewModel.init(id) }
|
||||||
viewModel.init(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(uiState.loading) {
|
LaunchedEffect(uiState.loading) {
|
||||||
if(!uiState.loading && WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
if (!uiState.loading && WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
delay(Constants.FOCUS_REQUEST_DELAY)
|
delay(Constants.FOCUS_REQUEST_DELAY)
|
||||||
focusRequester.requestFocus()
|
focusRequester.requestFocus()
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uiState.loading) {
|
|
||||||
LoadingScreen()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() })
|
|
||||||
|
|
||||||
val keyboardOptions =
|
|
||||||
KeyboardOptions(imeAction = ImeAction.Done)
|
|
||||||
|
|
||||||
val fillMaxHeight = .85f
|
|
||||||
val fillMaxWidth = .85f
|
|
||||||
val screenPadding = 5.dp
|
|
||||||
|
|
||||||
val applicationButtonText = {
|
|
||||||
"Tunneling apps: " +
|
|
||||||
if (uiState.isAllApplicationsEnabled) {
|
|
||||||
"all"
|
|
||||||
} else {
|
|
||||||
"${uiState.checkedPackageNames.size} " + (if (uiState.include) "included" else "excluded")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showAuthPrompt) {
|
if (uiState.loading) {
|
||||||
AuthorizationPrompt(
|
LoadingScreen()
|
||||||
onSuccess = {
|
return
|
||||||
showAuthPrompt = false
|
}
|
||||||
isAuthenticated = true
|
|
||||||
},
|
|
||||||
onError = { error ->
|
|
||||||
showAuthPrompt = false
|
|
||||||
showSnackbarMessage(Event.Error.AuthenticationFailed.message)
|
|
||||||
},
|
|
||||||
onFailure = {
|
|
||||||
showAuthPrompt = false
|
|
||||||
showSnackbarMessage(Event.Error.AuthorizationFailed.message)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showApplicationsDialog) {
|
val keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() })
|
||||||
val sortedPackages =
|
|
||||||
remember(uiState.packages) { uiState.packages.sortedBy { viewModel.getPackageLabel(it) } }
|
val keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done)
|
||||||
AlertDialog(onDismissRequest = { showApplicationsDialog = false }) {
|
|
||||||
Surface(
|
val fillMaxHeight = .85f
|
||||||
tonalElevation = 2.dp,
|
val fillMaxWidth = .85f
|
||||||
shadowElevation = 2.dp,
|
val screenPadding = 5.dp
|
||||||
shape = RoundedCornerShape(12.dp),
|
|
||||||
color = MaterialTheme.colorScheme.surface,
|
val applicationButtonText = buildAnnotatedString {
|
||||||
modifier =
|
append(stringResource(id = R.string.tunneling_apps))
|
||||||
Modifier.fillMaxWidth()
|
append(": ")
|
||||||
.fillMaxHeight(if (uiState.isAllApplicationsEnabled) 1 / 5f else 4 / 5f)) {
|
if (uiState.isAllApplicationsEnabled) {
|
||||||
Column(modifier = Modifier.fillMaxWidth()) {
|
append(stringResource(id = R.string.all))
|
||||||
Row(
|
} else {
|
||||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp, vertical = 7.dp),
|
append("${uiState.checkedPackageNames.size} ")
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
(if (uiState.include) append(stringResource(id = R.string.included)) else append(
|
||||||
horizontalArrangement = Arrangement.SpaceBetween) {
|
stringResource(id = R.string.excluded),
|
||||||
Text(stringResource(id = R.string.tunnel_all))
|
))
|
||||||
Switch(
|
}
|
||||||
checked = uiState.isAllApplicationsEnabled,
|
}
|
||||||
onCheckedChange = { viewModel.onAllApplicationsChange(it) })
|
|
||||||
}
|
if (showAuthPrompt) {
|
||||||
if (!uiState.isAllApplicationsEnabled) {
|
AuthorizationPrompt(
|
||||||
Row(
|
onSuccess = {
|
||||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp, vertical = 7.dp),
|
showAuthPrompt = false
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
isAuthenticated = true
|
||||||
horizontalArrangement = Arrangement.SpaceBetween) {
|
},
|
||||||
Row(
|
onError = {
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
showAuthPrompt = false
|
||||||
horizontalArrangement = Arrangement.SpaceBetween) {
|
showSnackbarMessage(Event.Error.AuthenticationFailed.message)
|
||||||
Text(stringResource(id = R.string.include))
|
},
|
||||||
Checkbox(
|
onFailure = {
|
||||||
checked = uiState.include,
|
showAuthPrompt = false
|
||||||
onCheckedChange = { viewModel.onIncludeChange(!uiState.include) })
|
showSnackbarMessage(Event.Error.AuthorizationFailed.message)
|
||||||
}
|
},
|
||||||
Row(
|
)
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
}
|
||||||
horizontalArrangement = Arrangement.SpaceBetween) {
|
|
||||||
Text(stringResource(id = R.string.exclude))
|
if (showApplicationsDialog) {
|
||||||
Checkbox(
|
val sortedPackages =
|
||||||
checked = !uiState.include,
|
remember(uiState.packages) {
|
||||||
onCheckedChange = { viewModel.onIncludeChange(!uiState.include) })
|
uiState.packages.sortedBy { viewModel.getPackageLabel(it) }
|
||||||
}
|
}
|
||||||
|
BasicAlertDialog(onDismissRequest = { showApplicationsDialog = false }) {
|
||||||
|
Surface(
|
||||||
|
tonalElevation = 2.dp,
|
||||||
|
shadowElevation = 2.dp,
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight(if (uiState.isAllApplicationsEnabled) 1 / 5f else 4 / 5f),
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier
|
||||||
|
.fillMaxWidth()) {
|
||||||
|
Row(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 20.dp, vertical = 7.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
) {
|
||||||
|
Text(stringResource(id = R.string.tunnel_all))
|
||||||
|
Switch(
|
||||||
|
checked = uiState.isAllApplicationsEnabled,
|
||||||
|
onCheckedChange = { viewModel.onAllApplicationsChange(it) },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Row(
|
if (!uiState.isAllApplicationsEnabled) {
|
||||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp, vertical = 7.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween) {
|
|
||||||
SearchBar(viewModel::emitQueriedPackages)
|
|
||||||
}
|
|
||||||
Spacer(Modifier.padding(5.dp))
|
|
||||||
LazyColumn(
|
|
||||||
horizontalAlignment = Alignment.Start,
|
|
||||||
verticalArrangement = Arrangement.Top,
|
|
||||||
modifier = Modifier.fillMaxHeight(4 / 5f)) {
|
|
||||||
items(sortedPackages, key = { it.packageName }) { pack ->
|
|
||||||
Row(
|
Row(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 20.dp, vertical = 7.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
modifier = Modifier.fillMaxSize().padding(5.dp)) {
|
) {
|
||||||
Row(modifier = Modifier.fillMaxWidth(fillMaxWidth)) {
|
Row(
|
||||||
val drawable =
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
pack.applicationInfo?.loadIcon(context.packageManager)
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
if (drawable != null) {
|
) {
|
||||||
Image(
|
Text(stringResource(id = R.string.include))
|
||||||
painter = DrawablePainter(drawable),
|
Checkbox(
|
||||||
stringResource(id = R.string.icon),
|
checked = uiState.include,
|
||||||
modifier = Modifier.size(50.dp, 50.dp))
|
onCheckedChange = {
|
||||||
} else {
|
viewModel.onIncludeChange(!uiState.include)
|
||||||
Icon(
|
},
|
||||||
Icons.Rounded.Android,
|
)
|
||||||
stringResource(id = R.string.edit),
|
|
||||||
modifier = Modifier.size(50.dp, 50.dp))
|
|
||||||
}
|
|
||||||
Text(
|
|
||||||
viewModel.getPackageLabel(pack),
|
|
||||||
modifier = Modifier.padding(5.dp))
|
|
||||||
}
|
|
||||||
Checkbox(
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
checked =
|
|
||||||
(uiState.checkedPackageNames.contains(pack.packageName)),
|
|
||||||
onCheckedChange = {
|
|
||||||
if (it) {
|
|
||||||
viewModel.onAddCheckedPackage(pack.packageName)
|
|
||||||
} else {
|
|
||||||
viewModel.onRemoveCheckedPackage(pack.packageName)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
) {
|
||||||
|
Text(stringResource(id = R.string.exclude))
|
||||||
|
Checkbox(
|
||||||
|
checked = !uiState.include,
|
||||||
|
onCheckedChange = {
|
||||||
|
viewModel.onIncludeChange(!uiState.include)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 20.dp, vertical = 7.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
) {
|
||||||
|
SearchBar(viewModel::emitQueriedPackages)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.padding(5.dp))
|
||||||
|
LazyColumn(
|
||||||
|
horizontalAlignment = Alignment.Start,
|
||||||
|
verticalArrangement = Arrangement.Top,
|
||||||
|
modifier = Modifier.fillMaxHeight(4 / 5f),
|
||||||
|
) {
|
||||||
|
items(sortedPackages, key = { it.packageName }) { pack ->
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(5.dp),
|
||||||
|
) {
|
||||||
|
Row(modifier = Modifier.fillMaxWidth(fillMaxWidth)) {
|
||||||
|
val drawable =
|
||||||
|
pack.applicationInfo?.loadIcon(context.packageManager)
|
||||||
|
if (drawable != null) {
|
||||||
|
Image(
|
||||||
|
painter = DrawablePainter(drawable),
|
||||||
|
stringResource(id = R.string.icon),
|
||||||
|
modifier = Modifier.size(50.dp, 50.dp),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Icon(
|
||||||
|
Icons.Rounded.Android,
|
||||||
|
stringResource(id = R.string.edit),
|
||||||
|
modifier = Modifier.size(50.dp, 50.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
viewModel.getPackageLabel(pack),
|
||||||
|
modifier = Modifier.padding(5.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Checkbox(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
checked =
|
||||||
|
(uiState.checkedPackageNames.contains(
|
||||||
|
pack.packageName
|
||||||
|
)),
|
||||||
|
onCheckedChange = {
|
||||||
|
if (it) {
|
||||||
|
viewModel.onAddCheckedPackage(pack.packageName)
|
||||||
|
} else {
|
||||||
|
viewModel.onRemoveCheckedPackage(pack.packageName)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
Row(
|
||||||
Row(
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
modifier = Modifier
|
||||||
modifier = Modifier.fillMaxSize().padding(top = 5.dp),
|
.fillMaxSize()
|
||||||
horizontalArrangement = Arrangement.Center) {
|
.padding(top = 5.dp),
|
||||||
TextButton(onClick = { showApplicationsDialog = false }) {
|
horizontalArrangement = Arrangement.Center,
|
||||||
Text(stringResource(R.string.done))
|
) {
|
||||||
|
TextButton(onClick = { showApplicationsDialog = false }) {
|
||||||
|
Text(stringResource(R.string.done))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
floatingActionButtonPosition = FabPosition.End,
|
floatingActionButtonPosition = FabPosition.End,
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
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.padding(bottom = 90.dp).onFocusChanged {
|
Modifier.onFocusChanged {
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
fobColor = if (it.isFocused) hoverColor else secondaryColor
|
fobColor = if (it.isFocused) hoverColor else secondaryColor
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
viewModel.onSaveAllChanges().let {
|
||||||
|
when (it) {
|
||||||
|
is Result.Success -> {
|
||||||
|
showSnackbarMessage(it.data.message)
|
||||||
|
navController.navigate(Screen.Main.route)
|
||||||
|
}
|
||||||
|
is Result.Error -> showSnackbarMessage(it.error.message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onClick = {
|
containerColor = fobColor,
|
||||||
viewModel.onSaveAllChanges().let {
|
shape = RoundedCornerShape(16.dp),
|
||||||
when (it) {
|
) {
|
||||||
is Result.Success -> {
|
|
||||||
showSnackbarMessage(it.data.message)
|
|
||||||
navController.navigate(Screen.Main.route)
|
|
||||||
}
|
|
||||||
is Result.Error -> showSnackbarMessage(it.error.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
containerColor = fobColor,
|
|
||||||
shape = RoundedCornerShape(16.dp)) {
|
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Rounded.Save,
|
imageVector = Icons.Rounded.Save,
|
||||||
contentDescription = stringResource(id = R.string.save_changes),
|
contentDescription = stringResource(id = R.string.save_changes),
|
||||||
tint = Color.DarkGray)
|
tint = Color.DarkGray,
|
||||||
}
|
)
|
||||||
}) {
|
}
|
||||||
Column {
|
},
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.verticalScroll(rememberScrollState()).weight(1f, true).fillMaxSize()) {
|
Modifier
|
||||||
Surface(
|
.verticalScroll(rememberScrollState())
|
||||||
tonalElevation = 2.dp,
|
.weight(1f, true)
|
||||||
shadowElevation = 2.dp,
|
.fillMaxSize()
|
||||||
shape = RoundedCornerShape(12.dp),
|
.padding(padding),
|
||||||
color = MaterialTheme.colorScheme.surface,
|
) {
|
||||||
modifier =
|
Surface(
|
||||||
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
tonalElevation = 2.dp,
|
||||||
Modifier.fillMaxHeight(fillMaxHeight).fillMaxWidth(fillMaxWidth)
|
shadowElevation = 2.dp,
|
||||||
} else {
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
modifier =
|
||||||
|
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
|
Modifier
|
||||||
|
.fillMaxHeight(fillMaxHeight)
|
||||||
|
.fillMaxWidth(fillMaxWidth)
|
||||||
|
} else {
|
||||||
Modifier.fillMaxWidth(fillMaxWidth)
|
Modifier.fillMaxWidth(fillMaxWidth)
|
||||||
})
|
})
|
||||||
.padding(top = 50.dp, bottom = 10.dp)) {
|
.padding(bottom = 10.dp),
|
||||||
Column(
|
) {
|
||||||
horizontalAlignment = Alignment.Start,
|
Column(
|
||||||
verticalArrangement = Arrangement.Top,
|
horizontalAlignment = Alignment.Start,
|
||||||
modifier = Modifier.padding(15.dp).focusGroup()) {
|
verticalArrangement = Arrangement.Top,
|
||||||
SectionTitle(
|
modifier = Modifier
|
||||||
stringResource(R.string.interface_), padding = screenPadding)
|
.padding(15.dp)
|
||||||
ConfigurationTextBox(
|
.focusGroup(),
|
||||||
value = uiState.tunnelName,
|
) {
|
||||||
onValueChange = { value -> viewModel.onTunnelNameChange(value) },
|
SectionTitle(
|
||||||
keyboardActions = keyboardActions,
|
stringResource(R.string.interface_),
|
||||||
label = stringResource(R.string.name),
|
padding = screenPadding,
|
||||||
hint = stringResource(R.string.tunnel_name).lowercase(),
|
)
|
||||||
modifier =
|
ConfigurationTextBox(
|
||||||
Modifier
|
value = uiState.tunnelName,
|
||||||
.fillMaxWidth()
|
onValueChange = { value -> viewModel.onTunnelNameChange(value) },
|
||||||
.focusRequester(focusRequester))
|
keyboardActions = keyboardActions,
|
||||||
OutlinedTextField(
|
label = stringResource(R.string.name),
|
||||||
modifier =
|
hint = stringResource(R.string.tunnel_name).lowercase(),
|
||||||
Modifier.fillMaxWidth().clickable {
|
modifier = Modifier
|
||||||
showAuthPrompt = true
|
.fillMaxWidth()
|
||||||
},
|
.focusRequester(focusRequester),
|
||||||
value = uiState.interfaceProxy.privateKey,
|
)
|
||||||
visualTransformation =
|
OutlinedTextField(
|
||||||
if ((id == Constants.MANUAL_TUNNEL_CONFIG_ID) ||
|
modifier = Modifier
|
||||||
isAuthenticated)
|
.fillMaxWidth()
|
||||||
VisualTransformation.None
|
.clickable { showAuthPrompt = true },
|
||||||
else PasswordVisualTransformation(),
|
value = uiState.interfaceProxy.privateKey,
|
||||||
enabled =
|
visualTransformation =
|
||||||
(id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated,
|
if ((id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated)
|
||||||
onValueChange = { value -> viewModel.onPrivateKeyChange(value) },
|
VisualTransformation.None
|
||||||
trailingIcon = {
|
else PasswordVisualTransformation(),
|
||||||
IconButton(
|
enabled = (id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated,
|
||||||
modifier = Modifier.focusRequester(FocusRequester.Default),
|
onValueChange = { value -> viewModel.onPrivateKeyChange(value) },
|
||||||
onClick = { viewModel.generateKeyPair() }) {
|
trailingIcon = {
|
||||||
Icon(
|
IconButton(
|
||||||
Icons.Rounded.Refresh,
|
modifier = Modifier.focusRequester(FocusRequester.Default),
|
||||||
stringResource(R.string.rotate_keys),
|
onClick = { viewModel.generateKeyPair() },
|
||||||
tint = Color.White)
|
) {
|
||||||
}
|
Icon(
|
||||||
},
|
Icons.Rounded.Refresh,
|
||||||
label = { Text(stringResource(R.string.private_key)) },
|
stringResource(R.string.rotate_keys),
|
||||||
singleLine = true,
|
tint = Color.White,
|
||||||
placeholder = { Text(stringResource(R.string.base64_key)) },
|
)
|
||||||
keyboardOptions = keyboardOptions,
|
}
|
||||||
keyboardActions = keyboardActions)
|
},
|
||||||
OutlinedTextField(
|
label = { Text(stringResource(R.string.private_key)) },
|
||||||
modifier =
|
singleLine = true,
|
||||||
Modifier
|
placeholder = { Text(stringResource(R.string.base64_key)) },
|
||||||
.fillMaxWidth()
|
keyboardOptions = keyboardOptions,
|
||||||
.focusRequester(FocusRequester.Default),
|
keyboardActions = keyboardActions,
|
||||||
value = uiState.interfaceProxy.publicKey,
|
)
|
||||||
enabled = false,
|
OutlinedTextField(
|
||||||
onValueChange = {},
|
modifier =
|
||||||
trailingIcon = {
|
Modifier
|
||||||
IconButton(
|
.fillMaxWidth()
|
||||||
modifier = Modifier.focusRequester(FocusRequester.Default),
|
.focusRequester(FocusRequester.Default),
|
||||||
onClick = {
|
value = uiState.interfaceProxy.publicKey,
|
||||||
clipboardManager.setText(
|
enabled = false,
|
||||||
AnnotatedString(uiState.interfaceProxy.publicKey))
|
onValueChange = {},
|
||||||
}) {
|
trailingIcon = {
|
||||||
Icon(
|
IconButton(
|
||||||
Icons.Rounded.ContentCopy,
|
modifier = Modifier.focusRequester(FocusRequester.Default),
|
||||||
stringResource(R.string.copy_public_key),
|
onClick = {
|
||||||
tint = Color.White)
|
clipboardManager.setText(
|
||||||
}
|
AnnotatedString(uiState.interfaceProxy.publicKey),
|
||||||
},
|
)
|
||||||
label = { Text(stringResource(R.string.public_key)) },
|
|
||||||
singleLine = true,
|
|
||||||
placeholder = { Text(stringResource(R.string.base64_key)) },
|
|
||||||
keyboardOptions = keyboardOptions,
|
|
||||||
keyboardActions = keyboardActions)
|
|
||||||
Row(modifier = Modifier.fillMaxWidth()) {
|
|
||||||
ConfigurationTextBox(
|
|
||||||
value = uiState.interfaceProxy.addresses,
|
|
||||||
onValueChange = { value ->
|
|
||||||
viewModel.onAddressesChanged(value)
|
|
||||||
},
|
},
|
||||||
keyboardActions = keyboardActions,
|
) {
|
||||||
label = stringResource(R.string.addresses),
|
Icon(
|
||||||
hint = stringResource(R.string.comma_separated_list),
|
Icons.Rounded.ContentCopy,
|
||||||
modifier =
|
stringResource(R.string.copy_public_key),
|
||||||
Modifier
|
tint = Color.White,
|
||||||
.fillMaxWidth(3 / 5f)
|
)
|
||||||
.padding(end = 5.dp))
|
}
|
||||||
ConfigurationTextBox(
|
},
|
||||||
value = uiState.interfaceProxy.listenPort,
|
label = { Text(stringResource(R.string.public_key)) },
|
||||||
onValueChange = { value ->
|
singleLine = true,
|
||||||
viewModel.onListenPortChanged(value)
|
placeholder = { Text(stringResource(R.string.base64_key)) },
|
||||||
},
|
keyboardOptions = keyboardOptions,
|
||||||
keyboardActions = keyboardActions,
|
keyboardActions = keyboardActions,
|
||||||
label = stringResource(R.string.listen_port),
|
)
|
||||||
hint = stringResource(R.string.random),
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
modifier = Modifier.width(IntrinsicSize.Min))
|
ConfigurationTextBox(
|
||||||
}
|
value = uiState.interfaceProxy.addresses,
|
||||||
Row(modifier = Modifier.fillMaxWidth()) {
|
onValueChange = { value -> viewModel.onAddressesChanged(value) },
|
||||||
ConfigurationTextBox(
|
keyboardActions = keyboardActions,
|
||||||
value = uiState.interfaceProxy.dnsServers,
|
label = stringResource(R.string.addresses),
|
||||||
onValueChange = { value ->
|
hint = stringResource(R.string.comma_separated_list),
|
||||||
viewModel.onDnsServersChanged(value)
|
modifier = Modifier
|
||||||
},
|
.fillMaxWidth(3 / 5f)
|
||||||
keyboardActions = keyboardActions,
|
.padding(end = 5.dp),
|
||||||
label = stringResource(R.string.dns_servers),
|
)
|
||||||
hint = stringResource(R.string.comma_separated_list),
|
ConfigurationTextBox(
|
||||||
modifier =
|
value = uiState.interfaceProxy.listenPort,
|
||||||
Modifier
|
onValueChange = { value -> viewModel.onListenPortChanged(value) },
|
||||||
.fillMaxWidth(3 / 5f)
|
keyboardActions = keyboardActions,
|
||||||
.padding(end = 5.dp))
|
label = stringResource(R.string.listen_port),
|
||||||
ConfigurationTextBox(
|
hint = stringResource(R.string.random),
|
||||||
value = uiState.interfaceProxy.mtu,
|
modifier = Modifier.width(IntrinsicSize.Min),
|
||||||
onValueChange = { value -> viewModel.onMtuChanged(value) },
|
)
|
||||||
keyboardActions = keyboardActions,
|
}
|
||||||
label = stringResource(R.string.mtu),
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
hint = stringResource(R.string.auto),
|
ConfigurationTextBox(
|
||||||
modifier = Modifier.width(IntrinsicSize.Min))
|
value = uiState.interfaceProxy.dnsServers,
|
||||||
}
|
onValueChange = { value -> viewModel.onDnsServersChanged(value) },
|
||||||
Row(
|
keyboardActions = keyboardActions,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
label = stringResource(R.string.dns_servers),
|
||||||
modifier = Modifier.fillMaxSize().padding(top = 5.dp),
|
hint = stringResource(R.string.comma_separated_list),
|
||||||
horizontalArrangement = Arrangement.Center) {
|
modifier = Modifier
|
||||||
TextButton(onClick = { showApplicationsDialog = true }) {
|
.fillMaxWidth(3 / 5f)
|
||||||
Text(applicationButtonText())
|
.padding(end = 5.dp),
|
||||||
}
|
)
|
||||||
}
|
ConfigurationTextBox(
|
||||||
|
value = uiState.interfaceProxy.mtu,
|
||||||
|
onValueChange = { value -> viewModel.onMtuChanged(value) },
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
label = stringResource(R.string.mtu),
|
||||||
|
hint = stringResource(R.string.auto),
|
||||||
|
modifier = Modifier.width(IntrinsicSize.Min),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(top = 5.dp),
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
TextButton(onClick = { showApplicationsDialog = true }) {
|
||||||
|
Text(applicationButtonText.text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
uiState.proxyPeers.forEachIndexed { index, peer ->
|
}
|
||||||
|
}
|
||||||
|
uiState.proxyPeers.forEachIndexed { index, peer ->
|
||||||
Surface(
|
Surface(
|
||||||
tonalElevation = 2.dp,
|
tonalElevation = 2.dp,
|
||||||
shadowElevation = 2.dp,
|
shadowElevation = 2.dp,
|
||||||
@@ -445,106 +505,126 @@ fun ConfigScreen(
|
|||||||
color = MaterialTheme.colorScheme.surface,
|
color = MaterialTheme.colorScheme.surface,
|
||||||
modifier =
|
modifier =
|
||||||
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
Modifier.fillMaxHeight(fillMaxHeight).fillMaxWidth(fillMaxWidth)
|
Modifier
|
||||||
|
.fillMaxHeight(fillMaxHeight)
|
||||||
|
.fillMaxWidth(fillMaxWidth)
|
||||||
} else {
|
} else {
|
||||||
Modifier.fillMaxWidth(fillMaxWidth)
|
Modifier.fillMaxWidth(fillMaxWidth)
|
||||||
})
|
})
|
||||||
.padding(top = 10.dp, bottom = 10.dp)) {
|
.padding(top = 10.dp, bottom = 10.dp),
|
||||||
Column(
|
) {
|
||||||
horizontalAlignment = Alignment.Start,
|
Column(
|
||||||
verticalArrangement = Arrangement.Top,
|
horizontalAlignment = Alignment.Start,
|
||||||
modifier =
|
verticalArrangement = Arrangement.Top,
|
||||||
Modifier.padding(horizontal = 15.dp).padding(bottom = 10.dp)) {
|
modifier = Modifier
|
||||||
Row(
|
.padding(horizontal = 15.dp)
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
.padding(bottom = 10.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
) {
|
||||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 5.dp)) {
|
Row(
|
||||||
SectionTitle(
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
stringResource(R.string.peer), padding = screenPadding)
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
IconButton(onClick = { viewModel.onDeletePeer(index) }) {
|
modifier = Modifier
|
||||||
Icon(Icons.Rounded.Delete, stringResource(R.string.delete))
|
.fillMaxWidth()
|
||||||
}
|
.padding(horizontal = 5.dp),
|
||||||
}
|
) {
|
||||||
|
SectionTitle(
|
||||||
ConfigurationTextBox(
|
stringResource(R.string.peer),
|
||||||
value = peer.publicKey,
|
padding = screenPadding,
|
||||||
onValueChange = { value ->
|
)
|
||||||
viewModel.onPeerPublicKeyChange(index, value)
|
IconButton(onClick = { viewModel.onDeletePeer(index) }) {
|
||||||
},
|
Icon(Icons.Rounded.Delete, stringResource(R.string.delete))
|
||||||
keyboardActions = keyboardActions,
|
}
|
||||||
label = stringResource(R.string.public_key),
|
|
||||||
hint = stringResource(R.string.base64_key),
|
|
||||||
modifier = Modifier.fillMaxWidth())
|
|
||||||
ConfigurationTextBox(
|
|
||||||
value = peer.preSharedKey,
|
|
||||||
onValueChange = { value ->
|
|
||||||
viewModel.onPreSharedKeyChange(index, value)
|
|
||||||
},
|
|
||||||
keyboardActions = keyboardActions,
|
|
||||||
label = stringResource(R.string.preshared_key),
|
|
||||||
hint = stringResource(R.string.optional),
|
|
||||||
modifier = Modifier.fillMaxWidth())
|
|
||||||
OutlinedTextField(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
value = peer.persistentKeepalive,
|
|
||||||
enabled = true,
|
|
||||||
onValueChange = { value ->
|
|
||||||
viewModel.onPersistentKeepaliveChanged(index, value)
|
|
||||||
},
|
|
||||||
trailingIcon = {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.seconds),
|
|
||||||
modifier = Modifier.padding(end = 10.dp))
|
|
||||||
},
|
|
||||||
label = { Text(stringResource(R.string.persistent_keepalive)) },
|
|
||||||
singleLine = true,
|
|
||||||
placeholder = {
|
|
||||||
Text(stringResource(R.string.optional_no_recommend))
|
|
||||||
},
|
|
||||||
keyboardOptions = keyboardOptions,
|
|
||||||
keyboardActions = keyboardActions)
|
|
||||||
ConfigurationTextBox(
|
|
||||||
value = peer.endpoint,
|
|
||||||
onValueChange = { value ->
|
|
||||||
viewModel.onEndpointChange(index, value)
|
|
||||||
},
|
|
||||||
keyboardActions = keyboardActions,
|
|
||||||
label = stringResource(R.string.endpoint),
|
|
||||||
hint = stringResource(R.string.endpoint).lowercase(),
|
|
||||||
modifier = Modifier.fillMaxWidth())
|
|
||||||
OutlinedTextField(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
value = peer.allowedIps,
|
|
||||||
enabled = true,
|
|
||||||
onValueChange = { value ->
|
|
||||||
viewModel.onAllowedIpsChange(index, value)
|
|
||||||
},
|
|
||||||
label = { Text(stringResource(R.string.allowed_ips)) },
|
|
||||||
singleLine = true,
|
|
||||||
placeholder = {
|
|
||||||
Text(stringResource(R.string.comma_separated_list))
|
|
||||||
},
|
|
||||||
keyboardOptions = keyboardOptions,
|
|
||||||
keyboardActions = keyboardActions)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier.fillMaxSize().padding(bottom = 140.dp)) {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.Center) {
|
|
||||||
TextButton(onClick = { viewModel.addEmptyPeer() }) {
|
|
||||||
Text(stringResource(R.string.add_peer))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = peer.publicKey,
|
||||||
|
onValueChange = { value ->
|
||||||
|
viewModel.onPeerPublicKeyChange(index, value)
|
||||||
|
},
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
label = stringResource(R.string.public_key),
|
||||||
|
hint = stringResource(R.string.base64_key),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = peer.preSharedKey,
|
||||||
|
onValueChange = { value ->
|
||||||
|
viewModel.onPreSharedKeyChange(index, value)
|
||||||
|
},
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
label = stringResource(R.string.preshared_key),
|
||||||
|
hint = stringResource(R.string.optional),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
value = peer.persistentKeepalive,
|
||||||
|
enabled = true,
|
||||||
|
onValueChange = { value ->
|
||||||
|
viewModel.onPersistentKeepaliveChanged(index, value)
|
||||||
|
},
|
||||||
|
trailingIcon = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.seconds),
|
||||||
|
modifier = Modifier.padding(end = 10.dp),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
label = { Text(stringResource(R.string.persistent_keepalive)) },
|
||||||
|
singleLine = true,
|
||||||
|
placeholder = {
|
||||||
|
Text(stringResource(R.string.optional_no_recommend))
|
||||||
|
},
|
||||||
|
keyboardOptions = keyboardOptions,
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
)
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = peer.endpoint,
|
||||||
|
onValueChange = { value ->
|
||||||
|
viewModel.onEndpointChange(index, value)
|
||||||
|
},
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
label = stringResource(R.string.endpoint),
|
||||||
|
hint = stringResource(R.string.endpoint).lowercase(),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
value = peer.allowedIps,
|
||||||
|
enabled = true,
|
||||||
|
onValueChange = { value ->
|
||||||
|
viewModel.onAllowedIpsChange(index, value)
|
||||||
|
},
|
||||||
|
label = { Text(stringResource(R.string.allowed_ips)) },
|
||||||
|
singleLine = true,
|
||||||
|
placeholder = {
|
||||||
|
Text(stringResource(R.string.comma_separated_list))
|
||||||
|
},
|
||||||
|
keyboardOptions = keyboardOptions,
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(bottom = 140.dp),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
TextButton(onClick = { viewModel.addEmptyPeer() }) {
|
||||||
|
Text(stringResource(R.string.add_peer))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
|
||||||
Spacer(modifier = Modifier.weight(.17f))
|
|
||||||
}
|
}
|
||||||
}
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
|
Spacer(modifier = Modifier.weight(.17f))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ data class ConfigUiState(
|
|||||||
val packages: Packages = emptyList(),
|
val packages: Packages = emptyList(),
|
||||||
val checkedPackageNames: List<String> = emptyList(),
|
val checkedPackageNames: List<String> = emptyList(),
|
||||||
val include: Boolean = true,
|
val include: Boolean = true,
|
||||||
val isAllApplicationsEnabled : Boolean = false,
|
val isAllApplicationsEnabled: Boolean = false,
|
||||||
val loading: Boolean = true,
|
val loading: Boolean = true,
|
||||||
val tunnel: TunnelConfig? = null,
|
val tunnel: TunnelConfig? = null,
|
||||||
val tunnelName: String = ""
|
val tunnelName: String = ""
|
||||||
|
|||||||
@@ -41,272 +41,314 @@ constructor(
|
|||||||
private val settingsRepository: SettingsRepository,
|
private val settingsRepository: SettingsRepository,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val packageManager = application.packageManager
|
private val packageManager = application.packageManager
|
||||||
|
|
||||||
private val _uiState = MutableStateFlow(ConfigUiState())
|
private val _uiState = MutableStateFlow(ConfigUiState())
|
||||||
val uiState = _uiState.asStateFlow()
|
val uiState = _uiState.asStateFlow()
|
||||||
|
|
||||||
fun init(tunnelId : String) = viewModelScope.launch(Dispatchers.IO) {
|
fun init(tunnelId: String) =
|
||||||
val packages = getQueriedPackages("")
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val state = if(tunnelId != Constants.MANUAL_TUNNEL_CONFIG_ID) {
|
val packages = getQueriedPackages("")
|
||||||
val tunnelConfig =
|
val state =
|
||||||
tunnelConfigRepository.getAll().firstOrNull { it.id.toString() == tunnelId }
|
if (tunnelId != Constants.MANUAL_TUNNEL_CONFIG_ID) {
|
||||||
if (tunnelConfig != null) {
|
val tunnelConfig =
|
||||||
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
tunnelConfigRepository.getAll().firstOrNull { it.id.toString() == tunnelId }
|
||||||
val proxyPeers = config.peers.map { PeerProxy.from(it) }
|
if (tunnelConfig != null) {
|
||||||
val proxyInterface = InterfaceProxy.from(config.`interface`)
|
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
||||||
var include = true
|
val proxyPeers = config.peers.map { PeerProxy.from(it) }
|
||||||
var isAllApplicationsEnabled = false
|
val proxyInterface = InterfaceProxy.from(config.`interface`)
|
||||||
val checkedPackages =
|
var include = true
|
||||||
if (config.`interface`.includedApplications.isNotEmpty()) {
|
var isAllApplicationsEnabled = false
|
||||||
config.`interface`.includedApplications
|
val checkedPackages =
|
||||||
} else if (config.`interface`.excludedApplications.isNotEmpty()) {
|
if (config.`interface`.includedApplications.isNotEmpty()) {
|
||||||
include = false
|
config.`interface`.includedApplications
|
||||||
config.`interface`.excludedApplications
|
} 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 {
|
} else {
|
||||||
isAllApplicationsEnabled = true
|
ConfigUiState(loading = false, packages = packages)
|
||||||
emptySet()
|
|
||||||
}
|
}
|
||||||
ConfigUiState(
|
} else {
|
||||||
proxyPeers,
|
ConfigUiState(loading = false, packages = packages)
|
||||||
proxyInterface,
|
}
|
||||||
packages,
|
_uiState.value = state
|
||||||
checkedPackages.toList(),
|
}
|
||||||
include,
|
|
||||||
isAllApplicationsEnabled,
|
fun onTunnelNameChange(name: String) {
|
||||||
false,
|
_uiState.value = _uiState.value.copy(tunnelName = name)
|
||||||
tunnelConfig,
|
}
|
||||||
tunnelConfig.name)
|
|
||||||
} else {
|
fun onIncludeChange(include: Boolean) {
|
||||||
ConfigUiState(loading = false, packages = packages)
|
_uiState.value = _uiState.value.copy(include = include)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onAddCheckedPackage(packageName: String) {
|
||||||
|
_uiState.value =
|
||||||
|
_uiState.value.copy(
|
||||||
|
checkedPackageNames = _uiState.value.checkedPackageNames + packageName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onAllApplicationsChange(isAllApplicationsEnabled: Boolean) {
|
||||||
|
_uiState.value = _uiState.value.copy(isAllApplicationsEnabled = isAllApplicationsEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onRemoveCheckedPackage(packageName: String) {
|
||||||
|
_uiState.value =
|
||||||
|
_uiState.value.copy(
|
||||||
|
checkedPackageNames = _uiState.value.checkedPackageNames - packageName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getQueriedPackages(query: String): List<PackageInfo> {
|
||||||
|
return getAllInternetCapablePackages().filter {
|
||||||
|
getPackageLabel(it).lowercase().contains(query.lowercase())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPackageLabel(packageInfo: PackageInfo): String {
|
||||||
|
return packageInfo.applicationInfo.loadLabel(application.packageManager).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getAllInternetCapablePackages(): List<PackageInfo> {
|
||||||
|
return getPackagesHoldingPermissions(arrayOf(Manifest.permission.INTERNET))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getPackagesHoldingPermissions(permissions: Array<String>): List<PackageInfo> {
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
packageManager.getPackagesHoldingPermissions(
|
||||||
|
permissions,
|
||||||
|
PackageManager.PackageInfoFlags.of(0L),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
ConfigUiState(loading = false, packages = packages)
|
packageManager.getPackagesHoldingPermissions(permissions, 0)
|
||||||
}
|
}
|
||||||
_uiState.value = state
|
|
||||||
}
|
}
|
||||||
fun onTunnelNameChange(name: String) {
|
|
||||||
_uiState.value = _uiState.value.copy(tunnelName = name)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onIncludeChange(include: Boolean) {
|
private fun isAllApplicationsEnabled(): Boolean {
|
||||||
_uiState.value = _uiState.value.copy(include = include)
|
return _uiState.value.isAllApplicationsEnabled
|
||||||
}
|
|
||||||
|
|
||||||
fun onAddCheckedPackage(packageName: String) {
|
|
||||||
_uiState.value =
|
|
||||||
_uiState.value.copy(checkedPackageNames = _uiState.value.checkedPackageNames + packageName)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onAllApplicationsChange(isAllApplicationsEnabled: Boolean) {
|
|
||||||
_uiState.value = _uiState.value.copy(isAllApplicationsEnabled = isAllApplicationsEnabled)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onRemoveCheckedPackage(packageName: String) {
|
|
||||||
_uiState.value =
|
|
||||||
_uiState.value.copy(checkedPackageNames = _uiState.value.checkedPackageNames - packageName)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getQueriedPackages(query: String): List<PackageInfo> {
|
|
||||||
return getAllInternetCapablePackages().filter {
|
|
||||||
getPackageLabel(it).lowercase().contains(query.lowercase())
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun getPackageLabel(packageInfo: PackageInfo): String {
|
private fun saveConfig(tunnelConfig: TunnelConfig) =
|
||||||
return packageInfo.applicationInfo.loadLabel(application.packageManager).toString()
|
viewModelScope.launch { tunnelConfigRepository.save(tunnelConfig) }
|
||||||
}
|
|
||||||
|
|
||||||
private fun getAllInternetCapablePackages(): List<PackageInfo> {
|
private fun updateTunnelConfig(tunnelConfig: TunnelConfig?) =
|
||||||
return getPackagesHoldingPermissions(arrayOf(Manifest.permission.INTERNET))
|
viewModelScope.launch {
|
||||||
}
|
if (tunnelConfig != null) {
|
||||||
|
saveConfig(tunnelConfig).join()
|
||||||
private fun getPackagesHoldingPermissions(permissions: Array<String>): List<PackageInfo> {
|
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
updateSettingsDefaultTunnel(tunnelConfig)
|
||||||
packageManager.getPackagesHoldingPermissions(
|
}
|
||||||
permissions, PackageManager.PackageInfoFlags.of(0L))
|
|
||||||
} else {
|
|
||||||
packageManager.getPackagesHoldingPermissions(permissions, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isAllApplicationsEnabled(): Boolean {
|
|
||||||
return _uiState.value.isAllApplicationsEnabled
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun saveConfig(tunnelConfig: TunnelConfig) =
|
|
||||||
viewModelScope.launch {
|
|
||||||
tunnelConfigRepository.save(tunnelConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateTunnelConfig(tunnelConfig: TunnelConfig?) =
|
|
||||||
viewModelScope.launch {
|
|
||||||
if (tunnelConfig != null) {
|
|
||||||
saveConfig(tunnelConfig).join()
|
|
||||||
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
|
||||||
updateSettingsDefaultTunnel(tunnelConfig)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun updateSettingsDefaultTunnel(tunnelConfig: TunnelConfig) {
|
private suspend fun updateSettingsDefaultTunnel(tunnelConfig: TunnelConfig) {
|
||||||
val settings = settingsRepository.getSettingsFlow().first()
|
val settings = settingsRepository.getSettingsFlow().first()
|
||||||
if (settings.defaultTunnel != null) {
|
if (settings.defaultTunnel != null) {
|
||||||
if (tunnelConfig.id == TunnelConfig.from(settings.defaultTunnel!!).id) {
|
if (tunnelConfig.id == TunnelConfig.from(settings.defaultTunnel!!).id) {
|
||||||
settingsRepository.save(settings.copy(defaultTunnel = tunnelConfig.toString()))
|
settingsRepository.save(settings.copy(defaultTunnel = tunnelConfig.toString()))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildPeerListFromProxyPeers(): List<Peer> {
|
|
||||||
return _uiState.value.proxyPeers.map {
|
|
||||||
val builder = Peer.Builder()
|
|
||||||
if (it.allowedIps.isNotEmpty()) builder.parseAllowedIPs(it.allowedIps.trim())
|
|
||||||
if (it.publicKey.isNotEmpty()) builder.parsePublicKey(it.publicKey.trim())
|
|
||||||
if (it.preSharedKey.isNotEmpty()) builder.parsePreSharedKey(it.preSharedKey.trim())
|
|
||||||
if (it.endpoint.isNotEmpty()) builder.parseEndpoint(it.endpoint.trim())
|
|
||||||
if (it.persistentKeepalive.isNotEmpty()) {
|
|
||||||
builder.parsePersistentKeepalive(it.persistentKeepalive.trim())
|
|
||||||
}
|
|
||||||
builder.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun emptyCheckedPackagesList() {
|
|
||||||
_uiState.value = _uiState.value.copy(checkedPackageNames = emptyList())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildInterfaceListFromProxyInterface(): Interface {
|
|
||||||
val builder = Interface.Builder()
|
|
||||||
builder.parsePrivateKey(_uiState.value.interfaceProxy.privateKey.trim())
|
|
||||||
builder.parseAddresses(_uiState.value.interfaceProxy.addresses.trim())
|
|
||||||
builder.parseDnsServers(_uiState.value.interfaceProxy.dnsServers.trim())
|
|
||||||
if (_uiState.value.interfaceProxy.mtu.isNotEmpty())
|
|
||||||
builder.parseMtu(_uiState.value.interfaceProxy.mtu.trim())
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onSaveAllChanges(): Result<Event> {
|
|
||||||
return try {
|
|
||||||
val peerList = buildPeerListFromProxyPeers()
|
|
||||||
val wgInterface = buildInterfaceListFromProxyInterface()
|
|
||||||
val config = Config.Builder().addPeers(peerList).setInterface(wgInterface).build()
|
|
||||||
val tunnelConfig =
|
|
||||||
_uiState.value.tunnel?.copy(
|
|
||||||
name = _uiState.value.tunnelName, wgQuick = config.toWgQuickString())
|
|
||||||
updateTunnelConfig(tunnelConfig)
|
|
||||||
Result.Success(Event.Message.ConfigSaved)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Result.Error(Event.Error.Exception(e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onPeerPublicKeyChange(index: Int, value: String) {
|
|
||||||
_uiState.value =
|
|
||||||
_uiState.value.copy(
|
|
||||||
proxyPeers =
|
|
||||||
_uiState.value.proxyPeers.update(
|
|
||||||
index, _uiState.value.proxyPeers[index].copy(publicKey = value)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onPreSharedKeyChange(index: Int, value: String) {
|
|
||||||
_uiState.value =
|
|
||||||
_uiState.value.copy(
|
|
||||||
proxyPeers =
|
|
||||||
_uiState.value.proxyPeers.update(
|
|
||||||
index, _uiState.value.proxyPeers[index].copy(preSharedKey = value)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onEndpointChange(index: Int, value: String) {
|
|
||||||
_uiState.value =
|
|
||||||
_uiState.value.copy(
|
|
||||||
proxyPeers =
|
|
||||||
_uiState.value.proxyPeers.update(
|
|
||||||
index, _uiState.value.proxyPeers[index].copy(endpoint = value)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onAllowedIpsChange(index: Int, value: String) {
|
|
||||||
_uiState.value =
|
|
||||||
_uiState.value.copy(
|
|
||||||
proxyPeers =
|
|
||||||
_uiState.value.proxyPeers.update(
|
|
||||||
index, _uiState.value.proxyPeers[index].copy(allowedIps = value)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onPersistentKeepaliveChanged(index: Int, value: String) {
|
|
||||||
_uiState.value =
|
|
||||||
_uiState.value.copy(
|
|
||||||
proxyPeers =
|
|
||||||
_uiState.value.proxyPeers.update(
|
|
||||||
index, _uiState.value.proxyPeers[index].copy(persistentKeepalive = value)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onDeletePeer(index: Int) {
|
|
||||||
_uiState.value = _uiState.value.copy(
|
|
||||||
proxyPeers = _uiState.value.proxyPeers.removeAt(index)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addEmptyPeer() {
|
|
||||||
_uiState.value = _uiState.value.copy(proxyPeers = _uiState.value.proxyPeers + PeerProxy())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun generateKeyPair() {
|
|
||||||
val keyPair = KeyPair()
|
|
||||||
_uiState.value =
|
|
||||||
_uiState.value.copy(
|
|
||||||
interfaceProxy =
|
|
||||||
_uiState.value.interfaceProxy.copy(
|
|
||||||
privateKey = keyPair.privateKey.toBase64(),
|
|
||||||
publicKey = keyPair.publicKey.toBase64()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onAddressesChanged(value: String) {
|
|
||||||
_uiState.value =
|
|
||||||
_uiState.value.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(addresses = value))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onListenPortChanged(value: String) {
|
|
||||||
_uiState.value =
|
|
||||||
_uiState.value.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(listenPort = value))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onDnsServersChanged(value: String) {
|
|
||||||
_uiState.value =
|
|
||||||
_uiState.value.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(dnsServers = value))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onMtuChanged(value: String) {
|
|
||||||
_uiState.value =
|
|
||||||
_uiState.value.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(mtu = value))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onInterfacePublicKeyChange(value: String) {
|
|
||||||
_uiState.value =
|
|
||||||
_uiState.value.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(publicKey = value))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onPrivateKeyChange(value: String) {
|
|
||||||
_uiState.value =
|
|
||||||
_uiState.value.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(privateKey = value))
|
|
||||||
if (NumberUtils.isValidKey(value)) {
|
|
||||||
val pair = KeyPair(Key.fromBase64(value))
|
|
||||||
onInterfacePublicKeyChange(pair.publicKey.toBase64())
|
|
||||||
} else {
|
|
||||||
onInterfacePublicKeyChange("")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun emitQueriedPackages(query: String) {
|
|
||||||
val packages =
|
|
||||||
getAllInternetCapablePackages().filter {
|
|
||||||
getPackageLabel(it).lowercase().contains(query.lowercase())
|
|
||||||
}
|
}
|
||||||
_uiState.value = _uiState.value.copy(packages = packages)
|
}
|
||||||
}
|
|
||||||
|
private fun buildPeerListFromProxyPeers(): List<Peer> {
|
||||||
|
return _uiState.value.proxyPeers.map {
|
||||||
|
val builder = Peer.Builder()
|
||||||
|
if (it.allowedIps.isNotEmpty()) builder.parseAllowedIPs(it.allowedIps.trim())
|
||||||
|
if (it.publicKey.isNotEmpty()) builder.parsePublicKey(it.publicKey.trim())
|
||||||
|
if (it.preSharedKey.isNotEmpty()) builder.parsePreSharedKey(it.preSharedKey.trim())
|
||||||
|
if (it.endpoint.isNotEmpty()) builder.parseEndpoint(it.endpoint.trim())
|
||||||
|
if (it.persistentKeepalive.isNotEmpty()) {
|
||||||
|
builder.parsePersistentKeepalive(it.persistentKeepalive.trim())
|
||||||
|
}
|
||||||
|
builder.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun emptyCheckedPackagesList() {
|
||||||
|
_uiState.value = _uiState.value.copy(checkedPackageNames = emptyList())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildInterfaceListFromProxyInterface(): Interface {
|
||||||
|
val builder = Interface.Builder()
|
||||||
|
builder.parsePrivateKey(_uiState.value.interfaceProxy.privateKey.trim())
|
||||||
|
builder.parseAddresses(_uiState.value.interfaceProxy.addresses.trim())
|
||||||
|
if (_uiState.value.interfaceProxy.dnsServers.isNotEmpty()) {
|
||||||
|
builder.parseDnsServers(_uiState.value.interfaceProxy.dnsServers.trim())
|
||||||
|
}
|
||||||
|
if (_uiState.value.interfaceProxy.mtu.isNotEmpty())
|
||||||
|
builder.parseMtu(_uiState.value.interfaceProxy.mtu.trim())
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onSaveAllChanges(): Result<Event> {
|
||||||
|
return try {
|
||||||
|
val peerList = buildPeerListFromProxyPeers()
|
||||||
|
val wgInterface = buildInterfaceListFromProxyInterface()
|
||||||
|
val config = Config.Builder().addPeers(peerList).setInterface(wgInterface).build()
|
||||||
|
val tunnelConfig = when(uiState.value.tunnel) {
|
||||||
|
null -> TunnelConfig(name = _uiState.value.tunnelName, wgQuick = config.toWgQuickString())
|
||||||
|
else -> uiState.value.tunnel!!.copy(
|
||||||
|
name = _uiState.value.tunnelName,
|
||||||
|
wgQuick = config.toWgQuickString(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
updateTunnelConfig(tunnelConfig)
|
||||||
|
Result.Success(Event.Message.ConfigSaved)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Result.Error(Event.Error.Exception(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onPeerPublicKeyChange(index: Int, value: String) {
|
||||||
|
_uiState.value =
|
||||||
|
_uiState.value.copy(
|
||||||
|
proxyPeers =
|
||||||
|
_uiState.value.proxyPeers.update(
|
||||||
|
index,
|
||||||
|
_uiState.value.proxyPeers[index].copy(publicKey = value),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onPreSharedKeyChange(index: Int, value: String) {
|
||||||
|
_uiState.value =
|
||||||
|
_uiState.value.copy(
|
||||||
|
proxyPeers =
|
||||||
|
_uiState.value.proxyPeers.update(
|
||||||
|
index,
|
||||||
|
_uiState.value.proxyPeers[index].copy(preSharedKey = value),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onEndpointChange(index: Int, value: String) {
|
||||||
|
_uiState.value =
|
||||||
|
_uiState.value.copy(
|
||||||
|
proxyPeers =
|
||||||
|
_uiState.value.proxyPeers.update(
|
||||||
|
index,
|
||||||
|
_uiState.value.proxyPeers[index].copy(endpoint = value),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onAllowedIpsChange(index: Int, value: String) {
|
||||||
|
_uiState.value =
|
||||||
|
_uiState.value.copy(
|
||||||
|
proxyPeers =
|
||||||
|
_uiState.value.proxyPeers.update(
|
||||||
|
index,
|
||||||
|
_uiState.value.proxyPeers[index].copy(allowedIps = value),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onPersistentKeepaliveChanged(index: Int, value: String) {
|
||||||
|
_uiState.value =
|
||||||
|
_uiState.value.copy(
|
||||||
|
proxyPeers =
|
||||||
|
_uiState.value.proxyPeers.update(
|
||||||
|
index,
|
||||||
|
_uiState.value.proxyPeers[index].copy(persistentKeepalive = value),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onDeletePeer(index: Int) {
|
||||||
|
_uiState.value =
|
||||||
|
_uiState.value.copy(
|
||||||
|
proxyPeers = _uiState.value.proxyPeers.removeAt(index),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addEmptyPeer() {
|
||||||
|
_uiState.value = _uiState.value.copy(proxyPeers = _uiState.value.proxyPeers + PeerProxy())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun generateKeyPair() {
|
||||||
|
val keyPair = KeyPair()
|
||||||
|
_uiState.value =
|
||||||
|
_uiState.value.copy(
|
||||||
|
interfaceProxy =
|
||||||
|
_uiState.value.interfaceProxy.copy(
|
||||||
|
privateKey = keyPair.privateKey.toBase64(),
|
||||||
|
publicKey = keyPair.publicKey.toBase64(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onAddressesChanged(value: String) {
|
||||||
|
_uiState.value =
|
||||||
|
_uiState.value.copy(
|
||||||
|
interfaceProxy = _uiState.value.interfaceProxy.copy(addresses = value)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onListenPortChanged(value: String) {
|
||||||
|
_uiState.value =
|
||||||
|
_uiState.value.copy(
|
||||||
|
interfaceProxy = _uiState.value.interfaceProxy.copy(listenPort = value)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onDnsServersChanged(value: String) {
|
||||||
|
_uiState.value =
|
||||||
|
_uiState.value.copy(
|
||||||
|
interfaceProxy = _uiState.value.interfaceProxy.copy(dnsServers = value)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onMtuChanged(value: String) {
|
||||||
|
_uiState.value =
|
||||||
|
_uiState.value.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(mtu = value))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onInterfacePublicKeyChange(value: String) {
|
||||||
|
_uiState.value =
|
||||||
|
_uiState.value.copy(
|
||||||
|
interfaceProxy = _uiState.value.interfaceProxy.copy(publicKey = value)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onPrivateKeyChange(value: String) {
|
||||||
|
_uiState.value =
|
||||||
|
_uiState.value.copy(
|
||||||
|
interfaceProxy = _uiState.value.interfaceProxy.copy(privateKey = value)
|
||||||
|
)
|
||||||
|
if (NumberUtils.isValidKey(value)) {
|
||||||
|
val pair = KeyPair(Key.fromBase64(value))
|
||||||
|
onInterfacePublicKeyChange(pair.publicKey.toBase64())
|
||||||
|
} else {
|
||||||
|
onInterfacePublicKeyChange("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun emitQueriedPackages(query: String) {
|
||||||
|
val packages =
|
||||||
|
getAllInternetCapablePackages().filter {
|
||||||
|
getPackageLabel(it).lowercase().contains(query.lowercase())
|
||||||
|
}
|
||||||
|
_uiState.value = _uiState.value.copy(packages = packages)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
|
|||||||
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
||||||
|
|
||||||
data class MainUiState(
|
data class MainUiState(
|
||||||
val settings : Settings = Settings(),
|
val settings: Settings = Settings(),
|
||||||
val tunnels : TunnelConfigs = emptyList(),
|
val tunnels: TunnelConfigs = emptyList(),
|
||||||
val vpnState: VpnState = VpnState(),
|
val vpnState: VpnState = VpnState(),
|
||||||
val loading : Boolean = true
|
val loading: Boolean = true
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -14,11 +14,7 @@ import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
|||||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
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.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.service.tunnel.WireGuardTunnel
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Event
|
import com.zaneschepke.wireguardautotunnel.util.Event
|
||||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||||
@@ -32,6 +28,7 @@ import kotlinx.coroutines.flow.combine
|
|||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
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
|
||||||
@@ -46,216 +43,225 @@ constructor(
|
|||||||
private val vpnService: VpnService
|
private val vpnService: VpnService
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
val uiState =
|
val uiState =
|
||||||
combine(
|
combine(
|
||||||
settingsRepository.getSettingsFlow(),
|
settingsRepository.getSettingsFlow(),
|
||||||
tunnelConfigRepository.getTunnelConfigsFlow(),
|
tunnelConfigRepository.getTunnelConfigsFlow(),
|
||||||
vpnService.vpnState,
|
vpnService.vpnState,
|
||||||
) { settings, tunnels, vpnState ->
|
) { settings, tunnels, vpnState ->
|
||||||
validateWatcherServiceState(settings)
|
validateWatcherServiceState(settings)
|
||||||
MainUiState(settings, tunnels, vpnState, false)
|
MainUiState(settings, tunnels, vpnState, false)
|
||||||
}
|
}
|
||||||
.stateIn(
|
.stateIn(
|
||||||
viewModelScope,
|
viewModelScope,
|
||||||
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
|
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
|
||||||
MainUiState())
|
MainUiState(),
|
||||||
|
)
|
||||||
|
|
||||||
private fun validateWatcherServiceState(settings: Settings) = viewModelScope.launch(Dispatchers.IO) {
|
private fun validateWatcherServiceState(settings: Settings) =
|
||||||
val watcherState =
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
ServiceManager.getServiceState(
|
if (settings.isAutoTunnelEnabled) {
|
||||||
application.applicationContext, WireGuardConnectivityWatcherService::class.java)
|
ServiceManager.startWatcherService(application.applicationContext)
|
||||||
if (settings.isAutoTunnelEnabled &&
|
}
|
||||||
watcherState == ServiceState.STOPPED) {
|
|
||||||
ServiceManager.startWatcherService(application.applicationContext)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun stopWatcherService() = viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
ServiceManager.stopWatcherService(application.applicationContext)
|
|
||||||
}
|
|
||||||
fun onDelete(tunnel: TunnelConfig) {
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
if (tunnelConfigRepository.count() == 1) {
|
|
||||||
stopWatcherService()
|
|
||||||
val settings = settingsRepository.getSettings()
|
|
||||||
settings.defaultTunnel = null
|
|
||||||
settings.isAutoTunnelEnabled = false
|
|
||||||
settings.isAlwaysOnVpnEnabled = false
|
|
||||||
saveSettings(settings)
|
|
||||||
}
|
|
||||||
tunnelConfigRepository.delete(tunnel)
|
|
||||||
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onTunnelStart(tunnelConfig: TunnelConfig) = viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
stopActiveTunnel().await()
|
|
||||||
startTunnel(tunnelConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startTunnel(tunnelConfig: TunnelConfig) = viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
ServiceManager.startVpnService(application.applicationContext, tunnelConfig.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun stopActiveTunnel() =
|
|
||||||
viewModelScope.async(Dispatchers.IO) {
|
|
||||||
if (ServiceManager.getServiceState(
|
|
||||||
application.applicationContext, WireGuardTunnelService::class.java) ==
|
|
||||||
ServiceState.STARTED) {
|
|
||||||
onTunnelStop()
|
|
||||||
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun onTunnelStop() = viewModelScope.launch(Dispatchers.IO) {
|
private fun stopWatcherService() =
|
||||||
ServiceManager.stopVpnService(application.applicationContext)
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
}
|
ServiceManager.stopWatcherService(application.applicationContext)
|
||||||
|
}
|
||||||
|
|
||||||
private fun validateConfigString(config: String) {
|
fun onDelete(tunnel: TunnelConfig) {
|
||||||
TunnelConfig.configFromQuick(config)
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
}
|
if (tunnelConfigRepository.count() == 1) {
|
||||||
|
stopWatcherService()
|
||||||
|
val settings = settingsRepository.getSettings()
|
||||||
|
settings.defaultTunnel = null
|
||||||
|
settings.isAutoTunnelEnabled = false
|
||||||
|
settings.isAlwaysOnVpnEnabled = false
|
||||||
|
saveSettings(settings)
|
||||||
|
}
|
||||||
|
tunnelConfigRepository.delete(tunnel)
|
||||||
|
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun onTunnelQrResult(result: String) : Result<Unit> {
|
fun onTunnelStart(tunnelConfig: TunnelConfig) =
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
Timber.d("On start called!")
|
||||||
|
stopActiveTunnel().await()
|
||||||
|
startTunnel(tunnelConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startTunnel(tunnelConfig: TunnelConfig) =
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
Timber.d("Start tunnel via manager")
|
||||||
|
ServiceManager.startVpnService(application.applicationContext, tunnelConfig.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopActiveTunnel() =
|
||||||
|
viewModelScope.async(Dispatchers.IO) {
|
||||||
|
onTunnelStop()
|
||||||
|
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onTunnelStop() =
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
Timber.d("Stopping active tunnel")
|
||||||
|
ServiceManager.stopVpnService(application.applicationContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun validateConfigString(config: String) {
|
||||||
|
TunnelConfig.configFromQuick(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun onTunnelQrResult(result: String): Result<Unit> {
|
||||||
return 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)
|
Result.Success(Unit)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Result.Error(Event.Error.InvalidQrCode)
|
Result.Error(Event.Error.InvalidQrCode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun saveTunnelConfigFromStream(stream: InputStream, fileName: String) {
|
private suspend fun saveTunnelConfigFromStream(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) { stream.close() }
|
withContext(Dispatchers.IO) { 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun onTunnelFileSelected(uri: Uri) : Result<Unit> {
|
suspend fun onTunnelFileSelected(uri: Uri): Result<Unit> {
|
||||||
try {
|
try {
|
||||||
if(isValidUriContentScheme(uri)){
|
if (isValidUriContentScheme(uri)) {
|
||||||
val fileName = getFileName(application.applicationContext, uri)
|
val fileName = getFileName(application.applicationContext, uri)
|
||||||
when (getFileExtensionFromFileName(fileName)) {
|
when (getFileExtensionFromFileName(fileName)) {
|
||||||
Constants.CONF_FILE_EXTENSION -> saveTunnelFromConfUri(fileName, uri).let {
|
Constants.CONF_FILE_EXTENSION ->
|
||||||
when(it) {
|
saveTunnelFromConfUri(fileName, uri).let {
|
||||||
is Result.Error -> return Result.Error(Event.Error.FileReadFailed)
|
when (it) {
|
||||||
is Result.Success -> return it
|
is Result.Error -> return Result.Error(Event.Error.FileReadFailed)
|
||||||
|
is Result.Success -> return it
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri)
|
Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri)
|
||||||
else -> return Result.Error(Event.Error.InvalidFileExtension)
|
else -> return Result.Error(Event.Error.InvalidFileExtension)
|
||||||
}
|
}
|
||||||
return Result.Success(Unit)
|
return Result.Success(Unit)
|
||||||
} else {
|
} else {
|
||||||
return Result.Error(Event.Error.InvalidFileExtension)
|
return Result.Error(Event.Error.InvalidFileExtension)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
return Result.Error(Event.Error.FileReadFailed)
|
return Result.Error(Event.Error.FileReadFailed)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun saveTunnelsFromZipUri(uri: Uri) {
|
|
||||||
ZipInputStream(getInputStreamFromUri(uri)).use { zip ->
|
|
||||||
generateSequence { zip.nextEntry }
|
|
||||||
.filterNot {
|
|
||||||
it.isDirectory || getFileExtensionFromFileName(it.name) != Constants.CONF_FILE_EXTENSION
|
|
||||||
}
|
|
||||||
.forEach {
|
|
||||||
val name = getNameFromFileName(it.name)
|
|
||||||
val config = Config.parse(zip)
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
addTunnel(TunnelConfig(name = name, wgQuick = config.toWgQuickString()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun saveTunnelFromConfUri(name: String, uri: Uri) : Result<Unit> {
|
|
||||||
val stream = getInputStreamFromUri(uri)
|
|
||||||
return if(stream != null) {
|
|
||||||
saveTunnelConfigFromStream(stream, name)
|
|
||||||
Result.Success(Unit)
|
|
||||||
} else {
|
|
||||||
Result.Error(Event.Error.FileReadFailed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun addTunnel(tunnelConfig: TunnelConfig) {
|
|
||||||
saveTunnel(tunnelConfig)
|
|
||||||
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun pauseAutoTunneling() = viewModelScope.launch {
|
|
||||||
settingsRepository.save(uiState.value.settings.copy(isAutoTunnelPaused = true))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resumeAutoTunneling() = viewModelScope.launch {
|
private suspend fun saveTunnelsFromZipUri(uri: Uri) {
|
||||||
settingsRepository.save(uiState.value.settings.copy(isAutoTunnelPaused = false))
|
ZipInputStream(getInputStreamFromUri(uri)).use { zip ->
|
||||||
|
generateSequence { zip.nextEntry }
|
||||||
|
.filterNot {
|
||||||
|
it.isDirectory ||
|
||||||
|
getFileExtensionFromFileName(it.name) != Constants.CONF_FILE_EXTENSION
|
||||||
|
}
|
||||||
|
.forEach {
|
||||||
|
val name = getNameFromFileName(it.name)
|
||||||
|
val config = Config.parse(zip)
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
addTunnel(TunnelConfig(name = name, wgQuick = config.toWgQuickString()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun saveTunnel(tunnelConfig: TunnelConfig) {
|
private suspend fun saveTunnelFromConfUri(name: String, uri: Uri): Result<Unit> {
|
||||||
tunnelConfigRepository.save(tunnelConfig)
|
val stream = getInputStreamFromUri(uri)
|
||||||
}
|
return if (stream != null) {
|
||||||
|
saveTunnelConfigFromStream(stream, name)
|
||||||
private fun getFileNameByCursor(context: Context, uri: Uri): String? {
|
Result.Success(Unit)
|
||||||
context.contentResolver.query(uri, null, null, null, null)?.use {
|
} else {
|
||||||
return getDisplayNameByCursor(it)
|
Result.Error(Event.Error.FileReadFailed)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getDisplayNameColumnIndex(cursor: Cursor): Int? {
|
private suspend fun addTunnel(tunnelConfig: TunnelConfig) {
|
||||||
val columnIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
saveTunnel(tunnelConfig)
|
||||||
return if (columnIndex != -1) {
|
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
||||||
return columnIndex
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun getDisplayNameByCursor(cursor: Cursor): String? {
|
fun pauseAutoTunneling() =
|
||||||
return if (cursor.moveToFirst()) {
|
viewModelScope.launch {
|
||||||
val index = getDisplayNameColumnIndex(cursor)
|
settingsRepository.save(uiState.value.settings.copy(isAutoTunnelPaused = true))
|
||||||
if (index != null) {
|
}
|
||||||
cursor.getString(index)
|
|
||||||
} else null
|
|
||||||
} else null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isValidUriContentScheme(uri: Uri): Boolean {
|
fun resumeAutoTunneling() =
|
||||||
return uri.scheme == Constants.URI_CONTENT_SCHEME
|
viewModelScope.launch {
|
||||||
}
|
settingsRepository.save(uiState.value.settings.copy(isAutoTunnelPaused = false))
|
||||||
private fun getFileName(context: Context, uri: Uri): String {
|
}
|
||||||
|
|
||||||
|
private suspend fun saveTunnel(tunnelConfig: TunnelConfig) {
|
||||||
|
tunnelConfigRepository.save(tunnelConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getFileNameByCursor(context: Context, uri: Uri): String? {
|
||||||
|
context.contentResolver.query(uri, null, null, null, null)?.use {
|
||||||
|
return getDisplayNameByCursor(it)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getDisplayNameColumnIndex(cursor: Cursor): Int? {
|
||||||
|
val columnIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||||
|
return if (columnIndex != -1) {
|
||||||
|
return columnIndex
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getDisplayNameByCursor(cursor: Cursor): String? {
|
||||||
|
return if (cursor.moveToFirst()) {
|
||||||
|
val index = getDisplayNameColumnIndex(cursor)
|
||||||
|
if (index != null) {
|
||||||
|
cursor.getString(index)
|
||||||
|
} else null
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isValidUriContentScheme(uri: Uri): Boolean {
|
||||||
|
return uri.scheme == Constants.URI_CONTENT_SCHEME
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getFileName(context: Context, uri: Uri): String {
|
||||||
return getFileNameByCursor(context, uri) ?: NumberUtils.generateRandomTunnelName()
|
return getFileNameByCursor(context, uri) ?: NumberUtils.generateRandomTunnelName()
|
||||||
}
|
|
||||||
|
|
||||||
private fun getNameFromFileName(fileName: String): String {
|
|
||||||
return fileName.substring(0, fileName.lastIndexOf('.'))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getFileExtensionFromFileName(fileName: String): String {
|
|
||||||
return try {
|
|
||||||
fileName.substring(fileName.lastIndexOf('.'))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
""
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun saveSettings(settings: Settings) =
|
private fun getNameFromFileName(fileName: String): String {
|
||||||
viewModelScope.launch(Dispatchers.IO) { settingsRepository.save(settings) }
|
return fileName.substring(0, fileName.lastIndexOf('.'))
|
||||||
|
|
||||||
fun onDefaultTunnelChange(selectedTunnel: TunnelConfig?) = viewModelScope.launch {
|
|
||||||
if (selectedTunnel != null) {
|
|
||||||
saveSettings(uiState.value.settings.copy(defaultTunnel = selectedTunnel.toString())).join()
|
|
||||||
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private fun getFileExtensionFromFileName(fileName: String): String {
|
||||||
|
return try {
|
||||||
|
fileName.substring(fileName.lastIndexOf('.'))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveSettings(settings: Settings) =
|
||||||
|
viewModelScope.launch(Dispatchers.IO) { settingsRepository.save(settings) }
|
||||||
|
|
||||||
|
fun onDefaultTunnelChange(selectedTunnel: TunnelConfig?) =
|
||||||
|
viewModelScope.launch {
|
||||||
|
if (selectedTunnel != null) {
|
||||||
|
saveSettings(uiState.value.settings.copy(defaultTunnel = selectedTunnel.toString()))
|
||||||
|
.join()
|
||||||
|
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.ACTION_APPLICATION_DETAILS_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
|
||||||
@@ -83,100 +90,136 @@ import java.io.File
|
|||||||
|
|
||||||
@OptIn(
|
@OptIn(
|
||||||
ExperimentalPermissionsApi::class,
|
ExperimentalPermissionsApi::class,
|
||||||
ExperimentalLayoutApi::class)
|
ExperimentalLayoutApi::class,
|
||||||
|
)
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsScreen(
|
fun SettingsScreen(
|
||||||
viewModel: SettingsViewModel = hiltViewModel(),
|
|
||||||
padding: PaddingValues,
|
padding: PaddingValues,
|
||||||
|
viewModel: SettingsViewModel = hiltViewModel(),
|
||||||
showSnackbarMessage: (String) -> Unit,
|
showSnackbarMessage: (String) -> Unit,
|
||||||
focusRequester: FocusRequester
|
focusRequester: FocusRequester
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope { Dispatchers.IO }
|
val scope = rememberCoroutineScope { Dispatchers.IO }
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
|
||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
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("") }
|
||||||
var isBackgroundLocationGranted by remember { mutableStateOf(true) }
|
var isBackgroundLocationGranted by remember { mutableStateOf(true) }
|
||||||
var showLocationServicesAlertDialog by remember { mutableStateOf(false) }
|
var showLocationServicesAlertDialog by remember { mutableStateOf(false) }
|
||||||
var didExportFiles by remember { mutableStateOf(false) }
|
var didExportFiles by remember { mutableStateOf(false) }
|
||||||
var showAuthPrompt by remember { mutableStateOf(false) }
|
var showAuthPrompt by remember { mutableStateOf(false) }
|
||||||
val focusRequester2 = remember { FocusRequester() }
|
val focusRequester2 = remember { FocusRequester() }
|
||||||
|
|
||||||
val screenPadding = 5.dp
|
val screenPadding = 5.dp
|
||||||
val fillMaxWidth = .85f
|
val fillMaxWidth = .85f
|
||||||
|
|
||||||
if (uiState.loading) {
|
if (uiState.loading) {
|
||||||
LoadingScreen()
|
LoadingScreen()
|
||||||
return
|
return
|
||||||
}
|
|
||||||
|
|
||||||
fun exportAllConfigs() {
|
|
||||||
try {
|
|
||||||
val files = uiState.tunnels.map { File(context.cacheDir, "${it.name}.conf") }
|
|
||||||
files.forEachIndexed { index, file ->
|
|
||||||
file.outputStream().use { it.write(uiState.tunnels[index].wgQuick.toByteArray()) }
|
|
||||||
}
|
|
||||||
FileUtils.saveFilesToZip(context, files)
|
|
||||||
didExportFiles = true
|
|
||||||
showSnackbarMessage(Event.Message.ConfigsExported.message)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
showSnackbarMessage(Event.Error.Exception(e).message)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun saveTrustedSSID() {
|
val startForResult =
|
||||||
if (currentText.isNotEmpty()) {
|
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
viewModel.onSaveTrustedSSID(currentText).let {
|
result: ActivityResult ->
|
||||||
when(it) {
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
is Result.Success -> currentText = ""
|
result.data
|
||||||
is Result.Error -> showSnackbarMessage(it.error.message)
|
// Handle the Intent
|
||||||
}
|
}
|
||||||
}
|
viewModel.setBatteryOptimizeDisableShown()
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun openSettings() {
|
|
||||||
scope.launch {
|
|
||||||
val intentSettings = Intent(ACTION_APPLICATION_DETAILS_SETTINGS)
|
|
||||||
intentSettings.data = Uri.fromParts("package", context.packageName, null)
|
|
||||||
context.startActivity(intentSettings)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun checkFineLocationGranted() {
|
|
||||||
isBackgroundLocationGranted =
|
|
||||||
if (!fineLocationState.status.isGranted) {
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
viewModel.setLocationDisclosureShown()
|
|
||||||
true
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
fun exportAllConfigs() {
|
||||||
if(WireGuardAutoTunnel.isRunningOnAndroidTv() && Build.VERSION.SDK_INT == Build.VERSION_CODES.Q){
|
try {
|
||||||
checkFineLocationGranted()
|
val files = uiState.tunnels.map { File(context.cacheDir, "${it.name}.conf") }
|
||||||
} else {
|
files.forEachIndexed { index, file ->
|
||||||
val backgroundLocationState =
|
file.outputStream().use { it.write(uiState.tunnels[index].wgQuick.toByteArray()) }
|
||||||
rememberPermissionState(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
|
}
|
||||||
isBackgroundLocationGranted =
|
FileUtils.saveFilesToZip(context, files)
|
||||||
if (!backgroundLocationState.status.isGranted) {
|
didExportFiles = true
|
||||||
false
|
showSnackbarMessage(Event.Message.ConfigsExported.message)
|
||||||
} else {
|
} catch (e: Exception) {
|
||||||
SideEffect { viewModel.setLocationDisclosureShown() }
|
showSnackbarMessage(Event.Error.Exception(e).message)
|
||||||
true
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
|
fun isBatteryOptimizationsDisabled(): Boolean {
|
||||||
checkFineLocationGranted()
|
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() {
|
||||||
|
if (currentText.isNotEmpty()) {
|
||||||
|
viewModel.onSaveTrustedSSID(currentText).let {
|
||||||
|
when (it) {
|
||||||
|
is Result.Success -> currentText = ""
|
||||||
|
is Result.Error -> showSnackbarMessage(it.error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun openSettings() {
|
||||||
|
scope.launch {
|
||||||
|
val intentSettings = Intent(ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||||
|
intentSettings.data = Uri.fromParts("package", context.packageName, null)
|
||||||
|
context.startActivity(intentSettings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun checkFineLocationGranted() {
|
||||||
|
isBackgroundLocationGranted =
|
||||||
|
if (!fineLocationState.status.isGranted) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
viewModel.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) {
|
||||||
|
checkFineLocationGranted()
|
||||||
|
}
|
||||||
|
|
||||||
AnimatedVisibility(showLocationServicesAlertDialog) {
|
AnimatedVisibility(showLocationServicesAlertDialog) {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
@@ -185,8 +228,9 @@ fun SettingsScreen(
|
|||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
showLocationServicesAlertDialog = false
|
showLocationServicesAlertDialog = false
|
||||||
viewModel.toggleAutoTunnel()
|
handleAutoTunnelToggle()
|
||||||
}) {
|
},
|
||||||
|
) {
|
||||||
Text(text = stringResource(R.string.okay))
|
Text(text = stringResource(R.string.okay))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -196,253 +240,313 @@ fun SettingsScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
title = { Text(text = stringResource(R.string.location_services_not_detected)) },
|
title = { Text(text = stringResource(R.string.location_services_not_detected)) },
|
||||||
text = { Text(text = stringResource(R.string.location_services_missing_message)) })
|
text = { Text(text = stringResource(R.string.location_services_missing_message)) },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!uiState.isLocationDisclosureShown) {
|
if (!uiState.isLocationDisclosureShown) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier = Modifier.fillMaxSize().verticalScroll(scrollState).padding(padding)) {
|
modifier = Modifier.fillMaxSize().verticalScroll(scrollState).padding(padding),
|
||||||
Icon(
|
) {
|
||||||
Icons.Rounded.LocationOff,
|
Icon(
|
||||||
contentDescription = stringResource(id = R.string.map),
|
Icons.Rounded.LocationOff,
|
||||||
modifier = Modifier.padding(30.dp).size(128.dp))
|
contentDescription = stringResource(id = R.string.map),
|
||||||
Text(
|
modifier = Modifier.padding(30.dp).size(128.dp),
|
||||||
stringResource(R.string.prominent_background_location_title),
|
)
|
||||||
textAlign = TextAlign.Center,
|
Text(
|
||||||
modifier = Modifier.padding(30.dp),
|
stringResource(R.string.prominent_background_location_title),
|
||||||
fontSize = 20.sp)
|
textAlign = TextAlign.Center,
|
||||||
Text(
|
modifier = Modifier.padding(30.dp),
|
||||||
stringResource(R.string.prominent_background_location_message),
|
fontSize = 20.sp,
|
||||||
textAlign = TextAlign.Center,
|
)
|
||||||
modifier = Modifier.padding(30.dp),
|
Text(
|
||||||
fontSize = 15.sp)
|
stringResource(R.string.prominent_background_location_message),
|
||||||
Row(
|
textAlign = TextAlign.Center,
|
||||||
modifier =
|
modifier = Modifier.padding(30.dp),
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
fontSize = 15.sp,
|
||||||
Modifier.fillMaxWidth().padding(10.dp)
|
)
|
||||||
} else {
|
Row(
|
||||||
Modifier.fillMaxWidth().padding(30.dp)
|
modifier =
|
||||||
},
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
Modifier.fillMaxWidth().padding(10.dp)
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly) {
|
} else {
|
||||||
|
Modifier.fillMaxWidth().padding(30.dp)
|
||||||
|
},
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
|
) {
|
||||||
TextButton(onClick = { viewModel.setLocationDisclosureShown() }) {
|
TextButton(onClick = { viewModel.setLocationDisclosureShown() }) {
|
||||||
Text(stringResource(id = R.string.no_thanks))
|
Text(stringResource(id = R.string.no_thanks))
|
||||||
}
|
}
|
||||||
TextButton(
|
TextButton(
|
||||||
modifier = Modifier.focusRequester(focusRequester),
|
modifier = Modifier.focusRequester(focusRequester),
|
||||||
onClick = {
|
onClick = {
|
||||||
openSettings()
|
openSettings()
|
||||||
viewModel.setLocationDisclosureShown()
|
viewModel.setLocationDisclosureShown()
|
||||||
}) {
|
},
|
||||||
Text(stringResource(id = R.string.turn_on))
|
) {
|
||||||
}
|
Text(stringResource(id = R.string.turn_on))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(showAuthPrompt) {
|
if (showAuthPrompt) {
|
||||||
AuthorizationPrompt(
|
AuthorizationPrompt(
|
||||||
onSuccess = {
|
onSuccess = {
|
||||||
showAuthPrompt = false
|
showAuthPrompt = false
|
||||||
exportAllConfigs()
|
exportAllConfigs()
|
||||||
},
|
},
|
||||||
onError = { _ ->
|
onError = { _ ->
|
||||||
showAuthPrompt = false
|
showAuthPrompt = false
|
||||||
showSnackbarMessage(Event.Error.AuthenticationFailed.message)
|
showSnackbarMessage(Event.Error.AuthenticationFailed.message)
|
||||||
},
|
},
|
||||||
onFailure = {
|
onFailure = {
|
||||||
showAuthPrompt = false
|
showAuthPrompt = false
|
||||||
showSnackbarMessage(Event.Error.AuthorizationFailed.message)
|
showSnackbarMessage(Event.Error.AuthorizationFailed.message)
|
||||||
})
|
},
|
||||||
}
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (uiState.tunnels.isEmpty() && uiState.isLocationDisclosureShown) {
|
if (uiState.tunnels.isEmpty() && uiState.isLocationDisclosureShown) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
modifier = Modifier.fillMaxSize().padding(padding)) {
|
modifier = Modifier.fillMaxSize().padding(padding),
|
||||||
Text(
|
) {
|
||||||
stringResource(R.string.one_tunnel_required),
|
Text(
|
||||||
textAlign = TextAlign.Center,
|
stringResource(R.string.one_tunnel_required),
|
||||||
modifier = Modifier.padding(15.dp),
|
textAlign = TextAlign.Center,
|
||||||
fontStyle = FontStyle.Italic)
|
modifier = Modifier.padding(15.dp),
|
||||||
|
fontStyle = FontStyle.Italic,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (uiState.isLocationDisclosureShown && uiState.tunnels.isNotEmpty()) {
|
if (uiState.isLocationDisclosureShown && uiState.tunnels.isNotEmpty()) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.fillMaxSize().verticalScroll(scrollState).clickable(
|
Modifier.fillMaxSize().padding(padding).verticalScroll(scrollState).clickable(
|
||||||
indication = null, interactionSource = interactionSource) {
|
indication = null,
|
||||||
focusManager.clearFocus()
|
interactionSource = interactionSource,
|
||||||
}) {
|
) {
|
||||||
Surface(
|
focusManager.clearFocus()
|
||||||
tonalElevation = 2.dp,
|
},
|
||||||
shadowElevation = 2.dp,
|
) {
|
||||||
shape = RoundedCornerShape(12.dp),
|
|
||||||
color = MaterialTheme.colorScheme.surface,
|
|
||||||
modifier =
|
|
||||||
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
|
||||||
Modifier.height(IntrinsicSize.Min)
|
|
||||||
.fillMaxWidth(fillMaxWidth)
|
|
||||||
.padding(top = 10.dp)
|
|
||||||
} else {
|
|
||||||
Modifier.fillMaxWidth(fillMaxWidth).padding(top = 60.dp)
|
|
||||||
})
|
|
||||||
.padding(bottom = 10.dp)) {
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.Start,
|
|
||||||
verticalArrangement = Arrangement.Top,
|
|
||||||
modifier = Modifier.padding(15.dp)) {
|
|
||||||
SectionTitle(
|
|
||||||
title = stringResource(id = R.string.auto_tunneling),
|
|
||||||
padding = screenPadding)
|
|
||||||
ConfigurationToggle(
|
|
||||||
stringResource(id = R.string.tunnel_on_wifi),
|
|
||||||
enabled =
|
|
||||||
!(uiState.settings.isAutoTunnelEnabled ||
|
|
||||||
uiState.settings.isAlwaysOnVpnEnabled),
|
|
||||||
checked = uiState.settings.isTunnelOnWifiEnabled,
|
|
||||||
padding = screenPadding,
|
|
||||||
onCheckChanged = { viewModel.onToggleTunnelOnWifi() },
|
|
||||||
modifier = if(uiState.settings.isAutoTunnelEnabled) Modifier else Modifier.focusRequester(focusRequester).focusProperties { down = focusRequester2 })
|
|
||||||
AnimatedVisibility(visible = uiState.settings.isTunnelOnWifiEnabled) {
|
|
||||||
Column {
|
|
||||||
FlowRow(
|
|
||||||
modifier = Modifier.padding(screenPadding).fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(5.dp)) {
|
|
||||||
uiState.settings.trustedNetworkSSIDs.forEach { ssid ->
|
|
||||||
ClickableIconButton(
|
|
||||||
onClick = { if(WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
|
||||||
viewModel.onDeleteTrustedSSID(ssid)
|
|
||||||
focusRequester2.requestFocus()
|
|
||||||
}},
|
|
||||||
onIconClick = { viewModel.onDeleteTrustedSSID(ssid) },
|
|
||||||
text = ssid,
|
|
||||||
icon = Icons.Filled.Close,
|
|
||||||
enabled =
|
|
||||||
!(uiState.settings.isAutoTunnelEnabled ||
|
|
||||||
uiState.settings.isAlwaysOnVpnEnabled))
|
|
||||||
}
|
|
||||||
if (uiState.settings.trustedNetworkSSIDs.isEmpty()) {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.none),
|
|
||||||
fontStyle = FontStyle.Italic,
|
|
||||||
color = Color.Gray)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
OutlinedTextField(
|
|
||||||
enabled =
|
|
||||||
!(uiState.settings.isAutoTunnelEnabled ||
|
|
||||||
uiState.settings.isAlwaysOnVpnEnabled),
|
|
||||||
value = currentText,
|
|
||||||
onValueChange = { currentText = it },
|
|
||||||
label = { Text(stringResource(R.string.add_trusted_ssid)) },
|
|
||||||
modifier =
|
|
||||||
Modifier.padding(
|
|
||||||
start = screenPadding, top = 5.dp, bottom = 10.dp)
|
|
||||||
.focusRequester(focusRequester2)
|
|
||||||
,
|
|
||||||
maxLines = 1,
|
|
||||||
keyboardOptions =
|
|
||||||
KeyboardOptions(
|
|
||||||
capitalization = KeyboardCapitalization.None,
|
|
||||||
imeAction = ImeAction.Done),
|
|
||||||
keyboardActions = KeyboardActions(onDone = { saveTrustedSSID() }),
|
|
||||||
trailingIcon = {
|
|
||||||
if (currentText != "") {
|
|
||||||
IconButton(onClick = { saveTrustedSSID() }) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Outlined.Add,
|
|
||||||
contentDescription =
|
|
||||||
if (currentText == "") {
|
|
||||||
stringResource(
|
|
||||||
id = R.string.trusted_ssid_empty_description)
|
|
||||||
} else {
|
|
||||||
stringResource(
|
|
||||||
id = R.string.trusted_ssid_value_description)
|
|
||||||
},
|
|
||||||
tint = MaterialTheme.colorScheme.primary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ConfigurationToggle(
|
|
||||||
stringResource(R.string.tunnel_mobile_data),
|
|
||||||
enabled =
|
|
||||||
!(uiState.settings.isAutoTunnelEnabled ||
|
|
||||||
uiState.settings.isAlwaysOnVpnEnabled),
|
|
||||||
checked = uiState.settings.isTunnelOnMobileDataEnabled,
|
|
||||||
padding = screenPadding,
|
|
||||||
onCheckChanged = { viewModel.onToggleTunnelOnMobileData() })
|
|
||||||
ConfigurationToggle(
|
|
||||||
stringResource(id = R.string.tunnel_on_ethernet),
|
|
||||||
enabled =
|
|
||||||
!(uiState.settings.isAutoTunnelEnabled ||
|
|
||||||
uiState.settings.isAlwaysOnVpnEnabled),
|
|
||||||
checked = uiState.settings.isTunnelOnEthernetEnabled,
|
|
||||||
padding = screenPadding,
|
|
||||||
onCheckChanged = { viewModel.onToggleTunnelOnEthernet() })
|
|
||||||
ConfigurationToggle(
|
|
||||||
stringResource(R.string.battery_saver),
|
|
||||||
enabled =
|
|
||||||
!(uiState.settings.isAutoTunnelEnabled ||
|
|
||||||
uiState.settings.isAlwaysOnVpnEnabled),
|
|
||||||
checked = uiState.settings.isBatterySaverEnabled,
|
|
||||||
padding = screenPadding,
|
|
||||||
onCheckChanged = { viewModel.onToggleBatterySaver() })
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = (if(!uiState.settings.isAutoTunnelEnabled) Modifier else Modifier.focusRequester(focusRequester))
|
|
||||||
.fillMaxSize().padding(top = 5.dp),
|
|
||||||
horizontalArrangement = Arrangement.Center) {
|
|
||||||
TextButton(
|
|
||||||
enabled = !uiState.settings.isAlwaysOnVpnEnabled,
|
|
||||||
onClick = {
|
|
||||||
if (uiState.settings.isTunnelOnWifiEnabled && !uiState.settings.isAutoTunnelEnabled) {
|
|
||||||
when(false) {
|
|
||||||
isBackgroundLocationGranted ->
|
|
||||||
showSnackbarMessage(Event.Error.BackgroundLocationRequired.message)
|
|
||||||
fineLocationState.status.isGranted ->
|
|
||||||
showSnackbarMessage(Event.Error.PreciseLocationRequired.message)
|
|
||||||
viewModel.isLocationEnabled(context) ->
|
|
||||||
showLocationServicesAlertDialog = true
|
|
||||||
else -> {
|
|
||||||
viewModel.toggleAutoTunnel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
viewModel.toggleAutoTunnel()
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
val autoTunnelButtonText =
|
|
||||||
if (uiState.settings.isAutoTunnelEnabled) {
|
|
||||||
stringResource(R.string.disable_auto_tunnel)
|
|
||||||
} else {
|
|
||||||
stringResource(id = R.string.enable_auto_tunnel)
|
|
||||||
}
|
|
||||||
Text(autoTunnelButtonText)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (WgQuickBackend.hasKernelSupport()) {
|
|
||||||
Surface(
|
Surface(
|
||||||
tonalElevation = 2.dp,
|
tonalElevation = 2.dp,
|
||||||
shadowElevation = 2.dp,
|
shadowElevation = 2.dp,
|
||||||
shape = RoundedCornerShape(12.dp),
|
shape = RoundedCornerShape(12.dp),
|
||||||
color = MaterialTheme.colorScheme.surface,
|
color = MaterialTheme.colorScheme.surface,
|
||||||
modifier = Modifier.fillMaxWidth(fillMaxWidth).padding(vertical = 10.dp)) {
|
modifier =
|
||||||
Column(
|
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
horizontalAlignment = Alignment.Start,
|
Modifier.height(IntrinsicSize.Min)
|
||||||
verticalArrangement = Arrangement.Top,
|
.fillMaxWidth(fillMaxWidth)
|
||||||
modifier = Modifier.padding(15.dp)) {
|
.padding(top = 10.dp)
|
||||||
|
} else {
|
||||||
|
Modifier.fillMaxWidth(fillMaxWidth).padding(top = 20.dp)
|
||||||
|
})
|
||||||
|
.padding(bottom = 10.dp),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.Start,
|
||||||
|
verticalArrangement = Arrangement.Top,
|
||||||
|
modifier = Modifier.padding(15.dp),
|
||||||
|
) {
|
||||||
|
SectionTitle(
|
||||||
|
title = stringResource(id = R.string.auto_tunneling),
|
||||||
|
padding = screenPadding,
|
||||||
|
)
|
||||||
|
ConfigurationToggle(
|
||||||
|
stringResource(id = R.string.tunnel_on_wifi),
|
||||||
|
enabled =
|
||||||
|
!(uiState.settings.isAutoTunnelEnabled ||
|
||||||
|
uiState.settings.isAlwaysOnVpnEnabled),
|
||||||
|
checked = uiState.settings.isTunnelOnWifiEnabled,
|
||||||
|
padding = screenPadding,
|
||||||
|
onCheckChanged = { viewModel.onToggleTunnelOnWifi() },
|
||||||
|
modifier =
|
||||||
|
if (uiState.settings.isAutoTunnelEnabled) Modifier
|
||||||
|
else
|
||||||
|
Modifier.focusRequester(focusRequester).focusProperties {
|
||||||
|
down = focusRequester2
|
||||||
|
},
|
||||||
|
)
|
||||||
|
AnimatedVisibility(visible = uiState.settings.isTunnelOnWifiEnabled) {
|
||||||
|
Column {
|
||||||
|
FlowRow(
|
||||||
|
modifier = Modifier.padding(screenPadding).fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(5.dp),
|
||||||
|
) {
|
||||||
|
uiState.settings.trustedNetworkSSIDs.forEach { ssid ->
|
||||||
|
ClickableIconButton(
|
||||||
|
onClick = {
|
||||||
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
|
viewModel.onDeleteTrustedSSID(ssid)
|
||||||
|
focusRequester2.requestFocus()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onIconClick = { viewModel.onDeleteTrustedSSID(ssid) },
|
||||||
|
text = ssid,
|
||||||
|
icon = Icons.Filled.Close,
|
||||||
|
enabled =
|
||||||
|
!(uiState.settings.isAutoTunnelEnabled ||
|
||||||
|
uiState.settings.isAlwaysOnVpnEnabled),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (uiState.settings.trustedNetworkSSIDs.isEmpty()) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.none),
|
||||||
|
fontStyle = FontStyle.Italic,
|
||||||
|
color = Color.Gray,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OutlinedTextField(
|
||||||
|
enabled =
|
||||||
|
!(uiState.settings.isAutoTunnelEnabled ||
|
||||||
|
uiState.settings.isAlwaysOnVpnEnabled),
|
||||||
|
value = currentText,
|
||||||
|
onValueChange = { currentText = it },
|
||||||
|
label = { Text(stringResource(R.string.add_trusted_ssid)) },
|
||||||
|
modifier =
|
||||||
|
Modifier.padding(
|
||||||
|
start = screenPadding,
|
||||||
|
top = 5.dp,
|
||||||
|
bottom = 10.dp,
|
||||||
|
)
|
||||||
|
.focusRequester(focusRequester2),
|
||||||
|
maxLines = 1,
|
||||||
|
keyboardOptions =
|
||||||
|
KeyboardOptions(
|
||||||
|
capitalization = KeyboardCapitalization.None,
|
||||||
|
imeAction = ImeAction.Done,
|
||||||
|
),
|
||||||
|
keyboardActions = KeyboardActions(onDone = { saveTrustedSSID() }),
|
||||||
|
trailingIcon = {
|
||||||
|
if (currentText != "") {
|
||||||
|
IconButton(onClick = { saveTrustedSSID() }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Add,
|
||||||
|
contentDescription =
|
||||||
|
if (currentText == "") {
|
||||||
|
stringResource(
|
||||||
|
id =
|
||||||
|
R.string
|
||||||
|
.trusted_ssid_empty_description,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
stringResource(
|
||||||
|
id =
|
||||||
|
R.string
|
||||||
|
.trusted_ssid_value_description,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ConfigurationToggle(
|
||||||
|
stringResource(R.string.tunnel_mobile_data),
|
||||||
|
enabled =
|
||||||
|
!(uiState.settings.isAutoTunnelEnabled ||
|
||||||
|
uiState.settings.isAlwaysOnVpnEnabled),
|
||||||
|
checked = uiState.settings.isTunnelOnMobileDataEnabled,
|
||||||
|
padding = screenPadding,
|
||||||
|
onCheckChanged = { viewModel.onToggleTunnelOnMobileData() },
|
||||||
|
)
|
||||||
|
ConfigurationToggle(
|
||||||
|
stringResource(id = R.string.tunnel_on_ethernet),
|
||||||
|
enabled =
|
||||||
|
!(uiState.settings.isAutoTunnelEnabled ||
|
||||||
|
uiState.settings.isAlwaysOnVpnEnabled),
|
||||||
|
checked = uiState.settings.isTunnelOnEthernetEnabled,
|
||||||
|
padding = screenPadding,
|
||||||
|
onCheckChanged = { viewModel.onToggleTunnelOnEthernet() },
|
||||||
|
)
|
||||||
|
ConfigurationToggle(
|
||||||
|
stringResource(R.string.battery_saver),
|
||||||
|
enabled =
|
||||||
|
!(uiState.settings.isAutoTunnelEnabled ||
|
||||||
|
uiState.settings.isAlwaysOnVpnEnabled),
|
||||||
|
checked = uiState.settings.isBatterySaverEnabled,
|
||||||
|
padding = screenPadding,
|
||||||
|
onCheckChanged = { viewModel.onToggleBatterySaver() },
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier =
|
||||||
|
(if (!uiState.settings.isAutoTunnelEnabled) Modifier
|
||||||
|
else
|
||||||
|
Modifier.focusRequester(
|
||||||
|
focusRequester,
|
||||||
|
))
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(top = 5.dp),
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
TextButton(
|
||||||
|
enabled = !uiState.settings.isAlwaysOnVpnEnabled,
|
||||||
|
onClick = {
|
||||||
|
if (
|
||||||
|
uiState.settings.isTunnelOnWifiEnabled &&
|
||||||
|
!uiState.settings.isAutoTunnelEnabled
|
||||||
|
) {
|
||||||
|
when (false) {
|
||||||
|
isBackgroundLocationGranted ->
|
||||||
|
showSnackbarMessage(
|
||||||
|
Event.Error.BackgroundLocationRequired.message
|
||||||
|
)
|
||||||
|
fineLocationState.status.isGranted ->
|
||||||
|
showSnackbarMessage(
|
||||||
|
Event.Error.PreciseLocationRequired.message
|
||||||
|
)
|
||||||
|
viewModel.isLocationEnabled(context) ->
|
||||||
|
showLocationServicesAlertDialog = true
|
||||||
|
else -> {
|
||||||
|
handleAutoTunnelToggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
handleAutoTunnelToggle()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
val autoTunnelButtonText =
|
||||||
|
if (uiState.settings.isAutoTunnelEnabled) {
|
||||||
|
stringResource(R.string.disable_auto_tunnel)
|
||||||
|
} else {
|
||||||
|
stringResource(id = R.string.enable_auto_tunnel)
|
||||||
|
}
|
||||||
|
Text(autoTunnelButtonText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (WgQuickBackend.hasKernelSupport()) {
|
||||||
|
Surface(
|
||||||
|
tonalElevation = 2.dp,
|
||||||
|
shadowElevation = 2.dp,
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
modifier = Modifier.fillMaxWidth(fillMaxWidth).padding(vertical = 10.dp),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.Start,
|
||||||
|
verticalArrangement = Arrangement.Top,
|
||||||
|
modifier = Modifier.padding(15.dp),
|
||||||
|
) {
|
||||||
SectionTitle(
|
SectionTitle(
|
||||||
title = stringResource(id = R.string.kernel), padding = screenPadding)
|
title = stringResource(id = R.string.kernel),
|
||||||
|
padding = screenPadding,
|
||||||
|
)
|
||||||
ConfigurationToggle(
|
ConfigurationToggle(
|
||||||
stringResource(R.string.use_kernel),
|
stringResource(R.string.use_kernel),
|
||||||
enabled =
|
enabled =
|
||||||
@@ -451,58 +555,70 @@ fun SettingsScreen(
|
|||||||
(uiState.vpnState.status == Tunnel.State.UP)),
|
(uiState.vpnState.status == Tunnel.State.UP)),
|
||||||
checked = uiState.settings.isKernelEnabled,
|
checked = uiState.settings.isKernelEnabled,
|
||||||
padding = screenPadding,
|
padding = screenPadding,
|
||||||
onCheckChanged = { viewModel.onToggleKernelMode().let {
|
onCheckChanged = {
|
||||||
when(it) {
|
viewModel.onToggleKernelMode().let {
|
||||||
is Result.Error -> showSnackbarMessage(it.error.message)
|
when (it) {
|
||||||
is Result.Success -> {}
|
is Result.Error -> showSnackbarMessage(it.error.message)
|
||||||
|
is Result.Success -> {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} })
|
},
|
||||||
}
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
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)
|
Modifier.fillMaxWidth(fillMaxWidth)
|
||||||
.padding(vertical = 10.dp)
|
.padding(vertical = 10.dp)
|
||||||
.padding(bottom = 140.dp)) {
|
.padding(bottom = 140.dp),
|
||||||
Column(
|
) {
|
||||||
horizontalAlignment = Alignment.Start,
|
Column(
|
||||||
verticalArrangement = Arrangement.Top,
|
horizontalAlignment = Alignment.Start,
|
||||||
modifier = Modifier.padding(15.dp)) {
|
verticalArrangement = Arrangement.Top,
|
||||||
|
modifier = Modifier.padding(15.dp),
|
||||||
|
) {
|
||||||
SectionTitle(
|
SectionTitle(
|
||||||
title = stringResource(id = R.string.other), padding = screenPadding)
|
title = stringResource(id = R.string.other),
|
||||||
|
padding = screenPadding,
|
||||||
|
)
|
||||||
ConfigurationToggle(
|
ConfigurationToggle(
|
||||||
stringResource(R.string.always_on_vpn_support),
|
stringResource(R.string.always_on_vpn_support),
|
||||||
enabled = !uiState.settings.isAutoTunnelEnabled,
|
enabled = !uiState.settings.isAutoTunnelEnabled,
|
||||||
checked = uiState.settings.isAlwaysOnVpnEnabled,
|
checked = uiState.settings.isAlwaysOnVpnEnabled,
|
||||||
padding = screenPadding,
|
padding = screenPadding,
|
||||||
onCheckChanged = { viewModel.onToggleAlwaysOnVPN() })
|
onCheckChanged = { viewModel.onToggleAlwaysOnVPN() },
|
||||||
|
)
|
||||||
ConfigurationToggle(
|
ConfigurationToggle(
|
||||||
stringResource(R.string.enabled_app_shortcuts),
|
stringResource(R.string.enabled_app_shortcuts),
|
||||||
enabled = true,
|
enabled = true,
|
||||||
checked = uiState.settings.isShortcutsEnabled,
|
checked = uiState.settings.isShortcutsEnabled,
|
||||||
padding = screenPadding,
|
padding = screenPadding,
|
||||||
onCheckChanged = { viewModel.onToggleShortcutsEnabled() })
|
onCheckChanged = { viewModel.onToggleShortcutsEnabled() },
|
||||||
|
)
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.fillMaxSize().padding(top = 5.dp),
|
modifier = Modifier.fillMaxSize().padding(top = 5.dp),
|
||||||
horizontalArrangement = Arrangement.Center) {
|
horizontalArrangement = Arrangement.Center,
|
||||||
TextButton(
|
) {
|
||||||
enabled = !didExportFiles, onClick = { showAuthPrompt = true }) {
|
TextButton(
|
||||||
Text(stringResource(R.string.export_configs))
|
enabled = !didExportFiles,
|
||||||
}
|
onClick = { showAuthPrompt = true },
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.export_configs))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
Spacer(modifier = Modifier.weight(.17f))
|
Spacer(modifier = Modifier.weight(.17f))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
|||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
|
||||||
|
|
||||||
data class SettingsUiState(
|
data class SettingsUiState(
|
||||||
val settings : Settings = Settings(),
|
val settings: Settings = Settings(),
|
||||||
val tunnels : List<TunnelConfig> = emptyList(),
|
val tunnels: List<TunnelConfig> = emptyList(),
|
||||||
val vpnState: VpnState = VpnState(),
|
val vpnState: VpnState = VpnState(),
|
||||||
val isLocationDisclosureShown : Boolean = true,
|
val isLocationDisclosureShown: Boolean = true,
|
||||||
val loading : Boolean = true
|
val isBatteryOptimizeDisableShown: Boolean = false,
|
||||||
|
val loading: Boolean = true
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -36,18 +36,29 @@ constructor(
|
|||||||
private val vpnService: VpnService
|
private val vpnService: VpnService
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
val uiState = combine(
|
val uiState =
|
||||||
settingsRepository.getSettingsFlow(),
|
combine(
|
||||||
tunnelConfigRepository.getTunnelConfigsFlow(),
|
settingsRepository.getSettingsFlow(),
|
||||||
vpnService.vpnState,
|
tunnelConfigRepository.getTunnelConfigsFlow(),
|
||||||
dataStoreManager.locationDisclosureFlow,
|
vpnService.vpnState,
|
||||||
){ settings, tunnels, tunnelState, locationDisclosure ->
|
dataStoreManager.preferencesFlow,
|
||||||
SettingsUiState(settings, tunnels, tunnelState, locationDisclosure
|
) { settings, tunnels, tunnelState, preferences ->
|
||||||
?: false, false)
|
SettingsUiState(
|
||||||
}.stateIn(viewModelScope,
|
settings,
|
||||||
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT), SettingsUiState())
|
tunnels,
|
||||||
|
tunnelState,
|
||||||
|
preferences?.get(DataStoreManager.LOCATION_DISCLOSURE_SHOWN) ?: false,
|
||||||
|
preferences?.get(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN) ?: false,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.stateIn(
|
||||||
|
viewModelScope,
|
||||||
|
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
|
||||||
|
SettingsUiState(),
|
||||||
|
)
|
||||||
|
|
||||||
fun onSaveTrustedSSID(ssid: String) : Result<Unit>{
|
fun onSaveTrustedSSID(ssid: String): Result<Unit> {
|
||||||
val trimmed = ssid.trim()
|
val trimmed = ssid.trim()
|
||||||
return if (!uiState.value.settings.trustedNetworkSSIDs.contains(trimmed)) {
|
return if (!uiState.value.settings.trustedNetworkSSIDs.contains(trimmed)) {
|
||||||
uiState.value.settings.trustedNetworkSSIDs.add(trimmed)
|
uiState.value.settings.trustedNetworkSSIDs.add(trimmed)
|
||||||
@@ -58,64 +69,77 @@ constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setLocationDisclosureShown() = viewModelScope.launch {
|
fun setLocationDisclosureShown() =
|
||||||
|
viewModelScope.launch {
|
||||||
dataStoreManager.saveToDataStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN, true)
|
dataStoreManager.saveToDataStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setBatteryOptimizeDisableShown() =
|
||||||
|
viewModelScope.launch {
|
||||||
|
dataStoreManager.saveToDataStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN, true)
|
||||||
|
}
|
||||||
|
|
||||||
fun onToggleTunnelOnMobileData() {
|
fun onToggleTunnelOnMobileData() {
|
||||||
saveSettings(
|
saveSettings(
|
||||||
uiState.value.settings.copy(
|
uiState.value.settings.copy(
|
||||||
isTunnelOnMobileDataEnabled = !uiState.value.settings.isTunnelOnMobileDataEnabled
|
isTunnelOnMobileDataEnabled = !uiState.value.settings.isTunnelOnMobileDataEnabled,
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onDeleteTrustedSSID(ssid: String) {
|
fun onDeleteTrustedSSID(ssid: String) {
|
||||||
saveSettings(uiState.value.settings.copy(
|
|
||||||
trustedNetworkSSIDs = (uiState.value.settings.trustedNetworkSSIDs - ssid).toMutableList()
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun getDefaultTunnelOrFirst() : String {
|
|
||||||
return uiState.value.settings.defaultTunnel ?: tunnelConfigRepository.getAll().first().toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toggleAutoTunnel() = viewModelScope.launch {
|
|
||||||
val isAutoTunnelEnabled = uiState.value.settings.isAutoTunnelEnabled
|
|
||||||
var isAutoTunnelPaused = uiState.value.settings.isAutoTunnelPaused
|
|
||||||
|
|
||||||
if (isAutoTunnelEnabled) {
|
|
||||||
ServiceManager.stopWatcherService(application)
|
|
||||||
} else {
|
|
||||||
ServiceManager.startWatcherService(application)
|
|
||||||
isAutoTunnelPaused = false
|
|
||||||
}
|
|
||||||
saveSettings(
|
saveSettings(
|
||||||
uiState.value.settings.copy(
|
uiState.value.settings.copy(
|
||||||
isAutoTunnelEnabled = !isAutoTunnelEnabled,
|
trustedNetworkSSIDs =
|
||||||
isAutoTunnelPaused = isAutoTunnelPaused,
|
(uiState.value.settings.trustedNetworkSSIDs - ssid).toMutableList(),
|
||||||
defaultTunnel = getDefaultTunnelOrFirst()
|
),
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun getDefaultTunnelOrFirst(): String {
|
||||||
|
return uiState.value.settings.defaultTunnel
|
||||||
|
?: tunnelConfigRepository.getAll().first().toString()
|
||||||
|
}
|
||||||
|
|
||||||
fun onToggleAlwaysOnVPN() = viewModelScope.launch {
|
fun toggleAutoTunnel() =
|
||||||
val updatedSettings = uiState.value.settings.copy(
|
viewModelScope.launch {
|
||||||
isAlwaysOnVpnEnabled = !uiState.value.settings.isAlwaysOnVpnEnabled,
|
val isAutoTunnelEnabled = uiState.value.settings.isAutoTunnelEnabled
|
||||||
defaultTunnel = getDefaultTunnelOrFirst()
|
var isAutoTunnelPaused = uiState.value.settings.isAutoTunnelPaused
|
||||||
|
|
||||||
|
if (isAutoTunnelEnabled) {
|
||||||
|
ServiceManager.stopWatcherService(application)
|
||||||
|
} else {
|
||||||
|
ServiceManager.startWatcherService(application)
|
||||||
|
isAutoTunnelPaused = false
|
||||||
|
}
|
||||||
|
saveSettings(
|
||||||
|
uiState.value.settings.copy(
|
||||||
|
isAutoTunnelEnabled = !isAutoTunnelEnabled,
|
||||||
|
isAutoTunnelPaused = isAutoTunnelPaused,
|
||||||
|
defaultTunnel = getDefaultTunnelOrFirst(),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
saveSettings(updatedSettings)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun saveSettings(settings: Settings) = viewModelScope.launch {
|
fun onToggleAlwaysOnVPN() =
|
||||||
settingsRepository.save(settings)
|
viewModelScope.launch {
|
||||||
}
|
val updatedSettings =
|
||||||
|
uiState.value.settings.copy(
|
||||||
|
isAlwaysOnVpnEnabled = !uiState.value.settings.isAlwaysOnVpnEnabled,
|
||||||
|
defaultTunnel = getDefaultTunnelOrFirst(),
|
||||||
|
)
|
||||||
|
saveSettings(updatedSettings)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveSettings(settings: Settings) =
|
||||||
|
viewModelScope.launch { settingsRepository.save(settings) }
|
||||||
|
|
||||||
fun onToggleTunnelOnEthernet() {
|
fun onToggleTunnelOnEthernet() {
|
||||||
saveSettings(uiState.value.settings.copy(
|
saveSettings(
|
||||||
isTunnelOnEthernetEnabled = !uiState.value.settings.isTunnelOnEthernetEnabled
|
uiState.value.settings.copy(
|
||||||
))
|
isTunnelOnEthernetEnabled = !uiState.value.settings.isTunnelOnEthernetEnabled,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isLocationEnabled(context: Context): Boolean {
|
fun isLocationEnabled(context: Context): Boolean {
|
||||||
@@ -126,36 +150,36 @@ constructor(
|
|||||||
fun onToggleShortcutsEnabled() {
|
fun onToggleShortcutsEnabled() {
|
||||||
saveSettings(
|
saveSettings(
|
||||||
uiState.value.settings.copy(
|
uiState.value.settings.copy(
|
||||||
isShortcutsEnabled = !uiState.value.settings.isShortcutsEnabled
|
isShortcutsEnabled = !uiState.value.settings.isShortcutsEnabled,
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onToggleBatterySaver() {
|
fun onToggleBatterySaver() {
|
||||||
saveSettings(
|
saveSettings(
|
||||||
uiState.value.settings.copy(
|
uiState.value.settings.copy(
|
||||||
isBatterySaverEnabled = !uiState.value.settings.isBatterySaverEnabled
|
isBatterySaverEnabled = !uiState.value.settings.isBatterySaverEnabled,
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveKernelMode(on: Boolean) {
|
private fun saveKernelMode(on: Boolean) {
|
||||||
saveSettings(
|
saveSettings(
|
||||||
uiState.value.settings.copy(
|
uiState.value.settings.copy(
|
||||||
isKernelEnabled = on
|
isKernelEnabled = on,
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onToggleTunnelOnWifi() {
|
fun onToggleTunnelOnWifi() {
|
||||||
saveSettings(
|
saveSettings(
|
||||||
uiState.value.settings.copy(
|
uiState.value.settings.copy(
|
||||||
isTunnelOnWifiEnabled = !uiState.value.settings.isTunnelOnWifiEnabled
|
isTunnelOnWifiEnabled = !uiState.value.settings.isTunnelOnWifiEnabled,
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onToggleKernelMode() : Result<Unit> {
|
fun onToggleKernelMode(): Result<Unit> {
|
||||||
if (!uiState.value.settings.isKernelEnabled) {
|
if (!uiState.value.settings.isKernelEnabled) {
|
||||||
try {
|
try {
|
||||||
rootShell.start()
|
rootShell.start()
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -57,172 +57,217 @@ import com.zaneschepke.wireguardautotunnel.util.Event
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SupportScreen(
|
fun SupportScreen(
|
||||||
viewModel: SupportViewModel = hiltViewModel(),
|
|
||||||
padding: PaddingValues,
|
padding: PaddingValues,
|
||||||
|
viewModel: SupportViewModel = hiltViewModel(),
|
||||||
showSnackbarMessage: (String) -> Unit,
|
showSnackbarMessage: (String) -> Unit,
|
||||||
focusRequester: FocusRequester
|
focusRequester: FocusRequester
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val fillMaxWidth = .85f
|
val fillMaxWidth = .85f
|
||||||
|
|
||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
fun openWebPage(url: String) {
|
fun openWebPage(url: String) {
|
||||||
try {
|
try {
|
||||||
val webpage: Uri = Uri.parse(url)
|
val webpage: Uri = Uri.parse(url)
|
||||||
val intent = Intent(Intent.ACTION_VIEW, webpage)
|
val intent = Intent(Intent.ACTION_VIEW, webpage)
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
} catch (e : Exception) {
|
} catch (e: Exception) {
|
||||||
showSnackbarMessage(Event.Error.Exception(e).message)
|
showSnackbarMessage(Event.Error.Exception(e).message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun launchEmail() {
|
fun launchEmail() {
|
||||||
try {
|
try {
|
||||||
val intent =
|
val intent =
|
||||||
Intent(Intent.ACTION_SEND).apply {
|
Intent(Intent.ACTION_SENDTO).apply {
|
||||||
type = Constants.EMAIL_MIME_TYPE
|
type = Constants.EMAIL_MIME_TYPE
|
||||||
putExtra(Intent.EXTRA_EMAIL, context.getString(R.string.my_email))
|
putExtra(Intent.EXTRA_EMAIL, arrayOf(context.getString(R.string.my_email)))
|
||||||
putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.email_subject))
|
putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.email_subject))
|
||||||
}
|
}
|
||||||
startActivity(context, createChooser(intent, context.getString(R.string.email_chooser)), null)
|
startActivity(
|
||||||
} catch (e : Exception) {
|
context,
|
||||||
showSnackbarMessage(Event.Error.Exception(e).message)
|
createChooser(intent, context.getString(R.string.email_chooser)),
|
||||||
}
|
null,
|
||||||
}
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
showSnackbarMessage(Event.Error.Exception(e).message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (uiState.loading) {
|
if (uiState.loading) {
|
||||||
LoadingScreen()
|
LoadingScreen()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.fillMaxSize()
|
Modifier.fillMaxSize().padding(padding)
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
.focusable()
|
.focusable()
|
||||||
.padding(padding)) {
|
) {
|
||||||
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 =
|
||||||
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
Modifier.height(IntrinsicSize.Min)
|
Modifier.height(IntrinsicSize.Min)
|
||||||
.fillMaxWidth(fillMaxWidth)
|
.fillMaxWidth(fillMaxWidth)
|
||||||
.padding(top = 10.dp)
|
.padding(top = 10.dp)
|
||||||
} else {
|
} else {
|
||||||
Modifier.fillMaxWidth(fillMaxWidth).padding(top = 20.dp)
|
Modifier.fillMaxWidth(fillMaxWidth).padding(top = 20.dp)
|
||||||
})
|
})
|
||||||
.padding(bottom = 25.dp)) {
|
.padding(bottom = 25.dp),
|
||||||
Column(modifier = Modifier.padding(20.dp)) {
|
) {
|
||||||
Text(
|
Column(modifier = Modifier.padding(20.dp)) {
|
||||||
stringResource(R.string.thank_you),
|
Text(
|
||||||
textAlign = TextAlign.Start,
|
stringResource(R.string.thank_you),
|
||||||
fontWeight = FontWeight.Bold,
|
textAlign = TextAlign.Start,
|
||||||
modifier = Modifier.padding(bottom = 20.dp),
|
fontWeight = FontWeight.Bold,
|
||||||
fontSize = 16.sp)
|
modifier = Modifier.padding(bottom = 20.dp),
|
||||||
Text(
|
fontSize = 16.sp,
|
||||||
stringResource(id = R.string.support_help_text),
|
)
|
||||||
textAlign = TextAlign.Start,
|
Text(
|
||||||
fontSize = 16.sp,
|
stringResource(id = R.string.support_help_text),
|
||||||
modifier = Modifier.padding(bottom = 20.dp))
|
textAlign = TextAlign.Start,
|
||||||
TextButton(
|
fontSize = 16.sp,
|
||||||
onClick = { openWebPage(context.resources.getString(R.string.docs_url)) },
|
modifier = Modifier.padding(bottom = 20.dp),
|
||||||
modifier = Modifier.padding(vertical = 5.dp).focusRequester(focusRequester)) {
|
)
|
||||||
Row(
|
TextButton(
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
onClick = { openWebPage(context.resources.getString(R.string.docs_url)) },
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
modifier = Modifier.padding(vertical = 5.dp).focusRequester(focusRequester),
|
||||||
modifier = Modifier.fillMaxWidth()) {
|
) {
|
||||||
Row {
|
Row(
|
||||||
Icon(Icons.Rounded.Book, stringResource(id = R.string.docs))
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
Text(
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
stringResource(id = R.string.docs_description),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
textAlign = TextAlign.Justify,
|
) {
|
||||||
modifier = Modifier.padding(start = 10.dp))
|
Row {
|
||||||
}
|
Icon(Icons.Rounded.Book, stringResource(id = R.string.docs))
|
||||||
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
Text(
|
||||||
}
|
stringResource(id = R.string.docs_description),
|
||||||
}
|
textAlign = TextAlign.Justify,
|
||||||
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
|
modifier = Modifier.padding(start = 10.dp),
|
||||||
TextButton(
|
)
|
||||||
onClick = { openWebPage(context.resources.getString(R.string.discord_url)) },
|
}
|
||||||
modifier = Modifier.padding(vertical = 5.dp)) {
|
Icon(
|
||||||
Row(
|
Icons.AutoMirrored.Rounded.ArrowForward,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
stringResource(id = R.string.go)
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
)
|
||||||
modifier = Modifier.fillMaxWidth()) {
|
}
|
||||||
Row {
|
|
||||||
Icon(
|
|
||||||
imageVector = ImageVector.vectorResource(R.drawable.discord),
|
|
||||||
stringResource(id = R.string.discord),
|
|
||||||
Modifier.size(25.dp))
|
|
||||||
Text(
|
|
||||||
stringResource(id = R.string.discord_description),
|
|
||||||
textAlign = TextAlign.Justify,
|
|
||||||
modifier = Modifier.padding(start = 10.dp))
|
|
||||||
}
|
|
||||||
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
|
|
||||||
TextButton(
|
|
||||||
onClick = { openWebPage(context.resources.getString(R.string.github_url)) },
|
|
||||||
modifier = Modifier.padding(vertical = 5.dp)) {
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier.fillMaxWidth()) {
|
|
||||||
Row {
|
|
||||||
Icon(
|
|
||||||
imageVector = ImageVector.vectorResource(R.drawable.github),
|
|
||||||
stringResource(id = R.string.github),
|
|
||||||
Modifier.size(25.dp))
|
|
||||||
Text(
|
|
||||||
"Open an issue",
|
|
||||||
textAlign = TextAlign.Justify,
|
|
||||||
modifier = Modifier.padding(start = 10.dp))
|
|
||||||
}
|
|
||||||
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
|
|
||||||
TextButton(
|
|
||||||
onClick = { launchEmail() }, modifier = Modifier.padding(vertical = 5.dp)) {
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier.fillMaxWidth()) {
|
|
||||||
Row {
|
|
||||||
Icon(Icons.Rounded.Mail, stringResource(id = R.string.email))
|
|
||||||
Text(
|
|
||||||
stringResource(id = R.string.email_description),
|
|
||||||
textAlign = TextAlign.Justify,
|
|
||||||
modifier = Modifier.padding(start = 10.dp))
|
|
||||||
}
|
|
||||||
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
HorizontalDivider(
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
thickness = 0.5.dp,
|
||||||
Text(
|
color = MaterialTheme.colorScheme.onBackground
|
||||||
stringResource(id = R.string.privacy_policy),
|
)
|
||||||
style = TextStyle(textDecoration = TextDecoration.Underline),
|
TextButton(
|
||||||
fontSize = 16.sp,
|
onClick = { openWebPage(context.resources.getString(R.string.discord_url)) },
|
||||||
modifier =
|
modifier = Modifier.padding(vertical = 5.dp),
|
||||||
Modifier.clickable {
|
) {
|
||||||
openWebPage(context.resources.getString(R.string.privacy_policy_url))
|
Row(
|
||||||
})
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
Row(
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(25.dp),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
) {
|
||||||
modifier = Modifier.padding(25.dp)) {
|
Row {
|
||||||
Text("Version: ${BuildConfig.VERSION_NAME}", modifier = Modifier.focusable())
|
Icon(
|
||||||
Text("Mode: ${if (uiState.settings.isKernelEnabled) "Kernel" else "Userspace" }")
|
imageVector = ImageVector.vectorResource(R.drawable.discord),
|
||||||
}
|
stringResource(id = R.string.discord),
|
||||||
|
Modifier.size(25.dp),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
stringResource(id = R.string.discord_description),
|
||||||
|
textAlign = TextAlign.Justify,
|
||||||
|
modifier = Modifier.padding(start = 10.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Icon(
|
||||||
|
Icons.AutoMirrored.Rounded.ArrowForward,
|
||||||
|
stringResource(id = R.string.go)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HorizontalDivider(
|
||||||
|
thickness = 0.5.dp,
|
||||||
|
color = MaterialTheme.colorScheme.onBackground
|
||||||
|
)
|
||||||
|
TextButton(
|
||||||
|
onClick = { openWebPage(context.resources.getString(R.string.github_url)) },
|
||||||
|
modifier = Modifier.padding(vertical = 5.dp),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
Row {
|
||||||
|
Icon(
|
||||||
|
imageVector = ImageVector.vectorResource(R.drawable.github),
|
||||||
|
stringResource(id = R.string.github),
|
||||||
|
Modifier.size(25.dp),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"Open an issue",
|
||||||
|
textAlign = TextAlign.Justify,
|
||||||
|
modifier = Modifier.padding(start = 10.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Icon(
|
||||||
|
Icons.AutoMirrored.Rounded.ArrowForward,
|
||||||
|
stringResource(id = R.string.go)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HorizontalDivider(
|
||||||
|
thickness = 0.5.dp,
|
||||||
|
color = MaterialTheme.colorScheme.onBackground
|
||||||
|
)
|
||||||
|
TextButton(
|
||||||
|
onClick = { launchEmail() },
|
||||||
|
modifier = Modifier.padding(vertical = 5.dp),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
Row {
|
||||||
|
Icon(Icons.Rounded.Mail, stringResource(id = R.string.email))
|
||||||
|
Text(
|
||||||
|
stringResource(id = R.string.email_description),
|
||||||
|
textAlign = TextAlign.Justify,
|
||||||
|
modifier = Modifier.padding(start = 10.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Icon(
|
||||||
|
Icons.AutoMirrored.Rounded.ArrowForward,
|
||||||
|
stringResource(id = R.string.go)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
Text(
|
||||||
|
stringResource(id = R.string.privacy_policy),
|
||||||
|
style = TextStyle(textDecoration = TextDecoration.Underline),
|
||||||
|
fontSize = 16.sp,
|
||||||
|
modifier =
|
||||||
|
Modifier.clickable {
|
||||||
|
openWebPage(context.resources.getString(R.string.privacy_policy_url))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(25.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.padding(25.dp),
|
||||||
|
) {
|
||||||
|
Text("Version: ${BuildConfig.VERSION_NAME}", modifier = Modifier.focusable())
|
||||||
|
Text("Mode: ${if (uiState.settings.isKernelEnabled) "Kernel" else "Userspace"}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,4 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.support
|
|||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
|
|
||||||
data class SupportUiState(
|
data class SupportUiState(val settings: Settings = Settings(), val loading: Boolean = true)
|
||||||
val settings : Settings = Settings(),
|
|
||||||
val loading : Boolean = true
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -11,15 +11,16 @@ import kotlinx.coroutines.flow.stateIn
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class SupportViewModel @Inject constructor(
|
class SupportViewModel @Inject constructor(private val settingsRepository: SettingsRepository) :
|
||||||
private val settingsRepository: SettingsRepository
|
ViewModel() {
|
||||||
) : ViewModel() {
|
|
||||||
|
|
||||||
val uiState = settingsRepository.getSettingsFlow().map {
|
val uiState =
|
||||||
SupportUiState(it, false)
|
settingsRepository
|
||||||
}.stateIn(
|
.getSettingsFlow()
|
||||||
viewModelScope,
|
.map { SupportUiState(it, false) }
|
||||||
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
|
.stateIn(
|
||||||
SupportUiState()
|
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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -12,77 +12,98 @@ sealed class Event {
|
|||||||
override val message: String
|
override val message: String
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_none)
|
get() = WireGuardAutoTunnel.instance.getString(R.string.error_none)
|
||||||
}
|
}
|
||||||
|
|
||||||
data object SsidConflict : Error() {
|
data object SsidConflict : Error() {
|
||||||
override val message: String
|
override val message: String
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_ssid_exists)
|
get() = WireGuardAutoTunnel.instance.getString(R.string.error_ssid_exists)
|
||||||
}
|
}
|
||||||
|
|
||||||
data object RootDenied : Error() {
|
data object RootDenied : Error() {
|
||||||
override val message: String
|
override val message: String
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_root_denied)
|
get() = WireGuardAutoTunnel.instance.getString(R.string.error_root_denied)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class General(val customMessage: String) : Error() {
|
data class General(val customMessage: String) : Error() {
|
||||||
override val message: String
|
override val message: String
|
||||||
get() = customMessage
|
get() = customMessage
|
||||||
}
|
}
|
||||||
data class Exception(val exception : kotlin.Exception) : Error() {
|
|
||||||
|
data class Exception(val exception: kotlin.Exception) : Error() {
|
||||||
override val message: String
|
override val message: String
|
||||||
get() = exception.message ?: WireGuardAutoTunnel.instance.getString(R.string.unknown_error)
|
get() =
|
||||||
|
exception.message
|
||||||
|
?: WireGuardAutoTunnel.instance.getString(R.string.unknown_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
data object InvalidQrCode : Error() {
|
data object InvalidQrCode : Error() {
|
||||||
override val message: String
|
override val message: String
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_invalid_code)
|
get() = WireGuardAutoTunnel.instance.getString(R.string.error_invalid_code)
|
||||||
}
|
}
|
||||||
|
|
||||||
data object InvalidFileExtension : Error() {
|
data object InvalidFileExtension : Error() {
|
||||||
override val message: String
|
override val message: String
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_file_extension)
|
get() = WireGuardAutoTunnel.instance.getString(R.string.error_file_extension)
|
||||||
}
|
}
|
||||||
|
|
||||||
data object FileReadFailed : Error() {
|
data object FileReadFailed : Error() {
|
||||||
override val message: String
|
override val message: String
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_file_extension)
|
get() = WireGuardAutoTunnel.instance.getString(R.string.error_file_extension)
|
||||||
}
|
}
|
||||||
|
|
||||||
data object AuthenticationFailed : Error() {
|
data object AuthenticationFailed : Error() {
|
||||||
override val message: String
|
override val message: String
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_authentication_failed)
|
get() = WireGuardAutoTunnel.instance.getString(R.string.error_authentication_failed)
|
||||||
}
|
}
|
||||||
|
|
||||||
data object AuthorizationFailed : Error() {
|
data object AuthorizationFailed : Error() {
|
||||||
override val message: String
|
override val message: String
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_authorization_failed)
|
get() = WireGuardAutoTunnel.instance.getString(R.string.error_authorization_failed)
|
||||||
}
|
}
|
||||||
|
|
||||||
data object BackgroundLocationRequired : Error() {
|
data object BackgroundLocationRequired : Error() {
|
||||||
override val message: String
|
override val message: String
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.background_location_required)
|
get() =
|
||||||
|
WireGuardAutoTunnel.instance.getString(R.string.background_location_required)
|
||||||
}
|
}
|
||||||
|
|
||||||
data object LocationServicesRequired : Error() {
|
data object LocationServicesRequired : Error() {
|
||||||
override val message: String
|
override val message: String
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.location_services_required)
|
get() = WireGuardAutoTunnel.instance.getString(R.string.location_services_required)
|
||||||
}
|
}
|
||||||
|
|
||||||
data object PreciseLocationRequired : Error() {
|
data object PreciseLocationRequired : Error() {
|
||||||
override val message: String
|
override val message: String
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.precise_location_required)
|
get() = WireGuardAutoTunnel.instance.getString(R.string.precise_location_required)
|
||||||
}
|
}
|
||||||
|
|
||||||
data object FileExplorerRequired : Error() {
|
data object FileExplorerRequired : Error() {
|
||||||
override val message: String
|
override val message: String
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_no_file_explorer)
|
get() = WireGuardAutoTunnel.instance.getString(R.string.error_no_file_explorer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class Message : Event() {
|
sealed class Message : Event() {
|
||||||
data object ConfigSaved: Message() {
|
data object ConfigSaved : Message() {
|
||||||
override val message: String
|
override val message: String
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.config_changes_saved)
|
get() = WireGuardAutoTunnel.instance.getString(R.string.config_changes_saved)
|
||||||
}
|
}
|
||||||
data object ConfigsExported: Message() {
|
|
||||||
|
data object ConfigsExported : Message() {
|
||||||
override val message: String
|
override val message: String
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.exported_configs_message)
|
get() = WireGuardAutoTunnel.instance.getString(R.string.exported_configs_message)
|
||||||
}
|
}
|
||||||
data object TunnelOffAction: Message() {
|
|
||||||
|
data object TunnelOffAction : Message() {
|
||||||
override val message: String
|
override val message: String
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.turn_off_tunnel)
|
get() = WireGuardAutoTunnel.instance.getString(R.string.turn_off_tunnel)
|
||||||
}
|
}
|
||||||
data object TunnelOnAction: Message() {
|
|
||||||
|
data object TunnelOnAction : Message() {
|
||||||
override val message: String
|
override val message: String
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.turn_on_tunnel)
|
get() = WireGuardAutoTunnel.instance.getString(R.string.turn_on_tunnel)
|
||||||
}
|
}
|
||||||
data object AutoTunnelOffAction: Message() {
|
|
||||||
|
data object AutoTunnelOffAction : Message() {
|
||||||
override val message: String
|
override val message: String
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.turn_off_auto)
|
get() = WireGuardAutoTunnel.instance.getString(R.string.turn_off_auto)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,23 +37,23 @@ fun BigDecimal.toThreeDecimalPlaceString(): String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun <T> List<T>.update(index: Int, item: T): List<T> = toMutableList().apply { this[index] = item }
|
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) }
|
fun <T> List<T>.removeAt(index: Int): List<T> = toMutableList().apply { this.removeAt(index) }
|
||||||
|
|
||||||
typealias TunnelConfigs = List<TunnelConfig>
|
typealias TunnelConfigs = List<TunnelConfig>
|
||||||
|
|
||||||
typealias Packages = List<PackageInfo>
|
typealias Packages = List<PackageInfo>
|
||||||
|
|
||||||
fun Statistics.mapPeerStats(): Map<Key, PeerStats?> {
|
fun Statistics.mapPeerStats(): Map<Key, PeerStats?> {
|
||||||
return this.peers().associateWith { key ->
|
return this.peers().associateWith { key -> (this.peer(key)) }
|
||||||
(this.peer(key))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun PeerStats.latestHandshakeSeconds() : Long? {
|
fun PeerStats.latestHandshakeSeconds(): Long? {
|
||||||
return NumberUtils.getSecondsBetweenTimestampAndNow(this.latestHandshakeEpochMillis)
|
return NumberUtils.getSecondsBetweenTimestampAndNow(this.latestHandshakeEpochMillis)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun PeerStats.handshakeStatus() : HandshakeStatus {
|
fun PeerStats.handshakeStatus(): HandshakeStatus {
|
||||||
//TODO add never connected status after duration
|
// TODO add never connected status after duration
|
||||||
return this.latestHandshakeSeconds().let {
|
return this.latestHandshakeSeconds().let {
|
||||||
when {
|
when {
|
||||||
it == null -> HandshakeStatus.NOT_STARTED
|
it == null -> HandshakeStatus.NOT_STARTED
|
||||||
@@ -65,4 +65,3 @@ fun PeerStats.handshakeStatus() : HandshakeStatus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,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)
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ package com.zaneschepke.wireguardautotunnel.util
|
|||||||
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
|
|
||||||
sealed class Result<T> {
|
sealed class Result<T> {
|
||||||
class Success<T>(val data: T): Result<T>()
|
class Success<T>(val data: T) : Result<T>()
|
||||||
class Error<T>(val error : Event.Error): Result<T>() {
|
|
||||||
|
class Error<T>(val error: Event.Error) : Result<T>() {
|
||||||
init {
|
init {
|
||||||
when(this.error) {
|
when (this.error) {
|
||||||
is Event.Error.Exception -> Timber.e(this.error.exception)
|
is Event.Error.Exception -> Timber.e(this.error.exception)
|
||||||
else -> Timber.e(this.error.message)
|
else -> Timber.e(this.error.message)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
After Width: | Height: | Size: 544 B |
|
After Width: | Height: | Size: 382 B |
|
After Width: | Height: | Size: 698 B |
|
After Width: | Height: | Size: 1.0 KiB |
@@ -3,8 +3,8 @@
|
|||||||
android:height="800dp"
|
android:height="800dp"
|
||||||
android:viewportWidth="256"
|
android:viewportWidth="256"
|
||||||
android:viewportHeight="256">
|
android:viewportHeight="256">
|
||||||
<path
|
<path
|
||||||
android:pathData="M216.86,45.1C200.29,37.34 182.57,31.71 164.04,28.5C161.77,32.61 159.11,38.15 157.28,42.55C137.58,39.58 118.07,39.58 98.74,42.55C96.91,38.15 94.19,32.61 91.9,28.5C73.35,31.71 55.61,37.36 39.04,45.14C5.62,95.65 -3.44,144.9 1.09,193.46C23.26,210.01 44.74,220.07 65.86,226.65C71.08,219.47 75.73,211.84 79.74,203.8C72.1,200.9 64.79,197.32 57.89,193.17C59.72,191.81 61.51,190.39 63.24,188.93C105.37,208.63 151.13,208.63 192.75,188.93C194.51,190.39 196.3,191.81 198.11,193.17C191.18,197.34 183.85,200.92 176.22,203.82C180.23,211.84 184.86,219.49 190.1,226.67C211.24,220.09 232.74,210.03 254.91,193.46C260.23,137.17 245.83,88.37 216.86,45.1ZM85.47,163.59C72.83,163.59 62.46,151.79 62.46,137.41C62.46,123.04 72.61,111.21 85.47,111.21C98.34,111.21 108.71,123.02 108.49,137.41C108.51,151.79 98.34,163.59 85.47,163.59ZM170.53,163.59C157.88,163.59 147.51,151.79 147.51,137.41C147.51,123.04 157.66,111.21 170.53,111.21C183.39,111.21 193.76,123.02 193.54,137.41C193.54,151.79 183.39,163.59 170.53,163.59Z"
|
android:fillColor="#5865F2"
|
||||||
android:fillColor="#5865F2"
|
android:fillType="nonZero"
|
||||||
android:fillType="nonZero"/>
|
android:pathData="M216.86,45.1C200.29,37.34 182.57,31.71 164.04,28.5C161.77,32.61 159.11,38.15 157.28,42.55C137.58,39.58 118.07,39.58 98.74,42.55C96.91,38.15 94.19,32.61 91.9,28.5C73.35,31.71 55.61,37.36 39.04,45.14C5.62,95.65 -3.44,144.9 1.09,193.46C23.26,210.01 44.74,220.07 65.86,226.65C71.08,219.47 75.73,211.84 79.74,203.8C72.1,200.9 64.79,197.32 57.89,193.17C59.72,191.81 61.51,190.39 63.24,188.93C105.37,208.63 151.13,208.63 192.75,188.93C194.51,190.39 196.3,191.81 198.11,193.17C191.18,197.34 183.85,200.92 176.22,203.82C180.23,211.84 184.86,219.49 190.1,226.67C211.24,220.09 232.74,210.03 254.91,193.46C260.23,137.17 245.83,88.37 216.86,45.1ZM85.47,163.59C72.83,163.59 62.46,151.79 62.46,137.41C62.46,123.04 72.61,111.21 85.47,111.21C98.34,111.21 108.71,123.02 108.49,137.41C108.51,151.79 98.34,163.59 85.47,163.59ZM170.53,163.59C157.88,163.59 147.51,151.79 147.51,137.41C147.51,123.04 157.66,111.21 170.53,111.21C183.39,111.21 193.76,123.02 193.54,137.41C193.54,151.79 183.39,163.59 170.53,163.59Z" />
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
android:height="800dp"
|
android:height="800dp"
|
||||||
android:viewportWidth="20"
|
android:viewportWidth="20"
|
||||||
android:viewportHeight="20">
|
android:viewportHeight="20">
|
||||||
<path
|
<path
|
||||||
android:pathData="M10,0C15.523,0 20,4.59 20,10.253C20,14.782 17.138,18.624 13.167,19.981C12.66,20.082 12.48,19.762 12.48,19.489C12.48,19.151 12.492,18.047 12.492,16.675C12.492,15.719 12.172,15.095 11.813,14.777C14.04,14.523 16.38,13.656 16.38,9.718C16.38,8.598 15.992,7.684 15.35,6.966C15.454,6.707 15.797,5.664 15.252,4.252C15.252,4.252 14.414,3.977 12.505,5.303C11.706,5.076 10.85,4.962 10,4.958C9.15,4.962 8.295,5.076 7.497,5.303C5.586,3.977 4.746,4.252 4.746,4.252C4.203,5.664 4.546,6.707 4.649,6.966C4.01,7.684 3.619,8.598 3.619,9.718C3.619,13.646 5.954,14.526 8.175,14.785C7.889,15.041 7.63,15.493 7.54,16.156C6.97,16.418 5.522,16.871 4.63,15.304C4.63,15.304 4.101,14.319 3.097,14.247C3.097,14.247 2.122,14.234 3.029,14.87C3.029,14.87 3.684,15.185 4.139,16.37C4.139,16.37 4.726,18.2 7.508,17.58C7.513,18.437 7.522,19.245 7.522,19.489C7.522,19.76 7.338,20.077 6.839,19.982C2.865,18.627 0,14.783 0,10.253C0,4.59 4.478,0 10,0"
|
android:fillColor="#000000"
|
||||||
android:strokeWidth="1"
|
android:fillType="evenOdd"
|
||||||
android:fillColor="#000000"
|
android:pathData="M10,0C15.523,0 20,4.59 20,10.253C20,14.782 17.138,18.624 13.167,19.981C12.66,20.082 12.48,19.762 12.48,19.489C12.48,19.151 12.492,18.047 12.492,16.675C12.492,15.719 12.172,15.095 11.813,14.777C14.04,14.523 16.38,13.656 16.38,9.718C16.38,8.598 15.992,7.684 15.35,6.966C15.454,6.707 15.797,5.664 15.252,4.252C15.252,4.252 14.414,3.977 12.505,5.303C11.706,5.076 10.85,4.962 10,4.958C9.15,4.962 8.295,5.076 7.497,5.303C5.586,3.977 4.746,4.252 4.746,4.252C4.203,5.664 4.546,6.707 4.649,6.966C4.01,7.684 3.619,8.598 3.619,9.718C3.619,13.646 5.954,14.526 8.175,14.785C7.889,15.041 7.63,15.493 7.54,16.156C6.97,16.418 5.522,16.871 4.63,15.304C4.63,15.304 4.101,14.319 3.097,14.247C3.097,14.247 2.122,14.234 3.029,14.87C3.029,14.87 3.684,15.185 4.139,16.37C4.139,16.37 4.726,18.2 7.508,17.58C7.513,18.437 7.522,19.245 7.522,19.489C7.522,19.76 7.338,20.077 6.839,19.982C2.865,18.627 0,14.783 0,10.253C0,4.59 4.478,0 10,0"
|
||||||
android:fillType="evenOdd"
|
android:strokeWidth="1"
|
||||||
android:strokeColor="#00000000"/>
|
android:strokeColor="#00000000" />
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="320dp"
|
||||||
|
android:height="180dp"
|
||||||
|
android:viewportWidth="640"
|
||||||
|
android:viewportHeight="640">
|
||||||
|
<group android:scaleX="0.6666667"
|
||||||
|
android:scaleY="0.6666667"
|
||||||
|
android:translateX="106.666664"
|
||||||
|
android:translateY="106.666664">
|
||||||
|
<group android:scaleX="0.315"
|
||||||
|
android:scaleY="0.56"
|
||||||
|
android:translateX="46.4"
|
||||||
|
android:translateY="140.8">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<group android:scaleX="0.52644"
|
||||||
|
android:scaleY="0.20707752"
|
||||||
|
android:translateX="320.4"
|
||||||
|
android:translateY="70.26513">
|
||||||
|
<group android:translateY="132.92308">
|
||||||
|
<path android:pathData="M76.06154,-101.68616L87.95077,-101.68616L72.44308,0L55.606155,0L43.93846,-76.72615L32.196922,0L14.990769,0L0.6646154,-101.68616L13.513846,-101.68616L24.36923,-14.104615L36.553844,-89.870766L52.283077,-89.870766L63.876923,-14.104615Z"
|
||||||
|
android:fillColor="#53BDB6"/>
|
||||||
|
<path android:pathData="M135.30154,1.6984615Q123.11692,1.6984615,114.25539,-4.1723075Q105.393845,-10.0430765,100.55692,-21.821539Q95.72,-33.6,95.72,-50.88Q95.72,-67.643074,101.62769,-79.495384Q107.535385,-91.347694,117.246155,-97.32923Q126.956924,-103.31077,138.25539,-103.31077Q147.78154,-103.31077,154.42769,-100.61539Q161.07385,-97.92,167.35077,-92.16L159.59692,-84.11077Q154.94461,-88.46769,149.77539,-90.535385Q144.60616,-92.60307,138.32922,-92.60307Q130.42769,-92.60307,123.92923,-88.504616Q117.43077,-84.40615,113.44308,-75.10154Q109.45538,-65.79692,109.45538,-50.88Q109.45538,-29.095385,116.175385,-19.089231Q122.895386,-9.0830765,136.48308,-9.0830765Q146.7477,-9.0830765,155.83076,-13.9569235L155.83076,-44.97231L135.81847,-44.97231L134.26768,-55.606155L168.5323,-55.606155L168.5323,-7.163077Q160.70462,-2.88,152.98769,-0.59076923Q145.27077,1.6984615,135.30154,1.6984615Z"
|
||||||
|
android:fillColor="#53BDB6"/>
|
||||||
|
<path android:pathData="M348.30463,-90.683075L317.43692,-90.683075L317.43692,0L304.66153,0L304.66153,-90.683075L272.90768,-90.683075L272.90768,-101.68616L349.63385,-101.68616Z"
|
||||||
|
android:fillColor="#53BDB6"/>
|
||||||
|
<path android:pathData="M383.24924,-22.670769Q383.24924,-15.064615,386.3877,-11.630769Q389.52615,-8.196923,396.32,-8.196923Q402.44922,-8.196923,408.24615,-11.741538Q414.0431,-15.286154,417.36615,-20.52923L417.36615,-77.76L429.7723,-77.76L429.7723,0L419.2123,0L418.17847,-10.486154Q413.67386,-4.726154,406.99078,-1.5138462Q400.30768,1.6984615,393.2923,1.6984615Q382.14154,1.6984615,376.4923,-4.246154Q370.84308,-10.190769,370.84308,-21.193846L370.84308,-77.76L383.24924,-77.76Z"
|
||||||
|
android:fillColor="#53BDB6"/>
|
||||||
|
<path android:pathData="M459.84308,0L459.84308,-77.76L470.40308,-77.76L471.36307,-66.97846Q476.01538,-72.81231,483.0677,-76.098465Q490.12,-79.38461,497.06152,-79.38461Q508.2123,-79.38461,513.56616,-73.47692Q518.92,-67.56923,518.92,-56.49231L518.92,0L506.51385,0L506.51385,-47.335384Q506.51385,-56.049232,505.59076,-60.627693Q504.6677,-65.206154,501.82462,-67.42154Q498.98154,-69.636925,493.22153,-69.636925Q486.9446,-69.636925,481.36923,-65.76Q475.79385,-61.883076,472.24924,-56.64L472.24924,0Z"
|
||||||
|
android:fillColor="#53BDB6"/>
|
||||||
|
<path android:pathData="M548.8431,0L548.8431,-77.76L559.4031,-77.76L560.3631,-66.97846Q565.0154,-72.81231,572.0677,-76.098465Q579.12,-79.38461,586.0615,-79.38461Q597.2123,-79.38461,602.56616,-73.47692Q607.92,-67.56923,607.92,-56.49231L607.92,0L595.51385,0L595.51385,-47.335384Q595.51385,-56.049232,594.59076,-60.627693Q593.66766,-65.206154,590.8246,-67.42154Q587.98157,-69.636925,582.22156,-69.636925Q575.94464,-69.636925,570.3692,-65.76Q564.7938,-61.883076,561.2492,-56.64L561.2492,0Z"
|
||||||
|
android:fillColor="#53BDB6"/>
|
||||||
|
<path android:pathData="M647.59076,-34.486153Q647.88617,-25.772308,650.9877,-19.975384Q654.08923,-14.178461,659.1108,-11.409231Q664.1323,-8.64,670.3354,-8.64Q676.0954,-8.64,680.9323,-10.338462Q685.7692,-12.036923,691.0862,-15.655385L696.92,-7.4584618Q691.4554,-3.1753845,684.4031,-0.73846155Q677.35077,1.6984615,670.1877,1.6984615Q659.0369,1.6984615,650.9877,-3.323077Q642.9385,-8.344615,638.7662,-17.50154Q634.5939,-26.65846,634.5939,-38.76923Q634.5939,-50.51077,638.8031,-59.74154Q643.0123,-68.972305,650.6923,-74.17846Q658.3723,-79.38461,668.56305,-79.38461Q678.3108,-79.38461,685.43695,-74.80615Q692.56305,-70.22769,696.36615,-61.624615Q700.16925,-53.021538,700.16925,-41.28Q700.16925,-37.883076,699.87384,-34.486153ZM668.71075,-69.19385Q659.70154,-69.19385,654.0523,-62.88Q648.4031,-56.566154,647.6646,-44.086155L688.2062,-44.086155Q687.9846,-56.344616,682.81537,-62.76923Q677.6462,-69.19385,668.71075,-69.19385Z"
|
||||||
|
android:fillColor="#53BDB6"/>
|
||||||
|
<path android:pathData="M757.12,-19.2Q757.12,-13.735385,760.55383,-11.187693Q763.9877,-8.64,769.96924,-8.64Q776.24615,-8.64,783.04,-11.372308L786.3631,-2.2892308Q782.8185,-0.51692307,778.0923,0.59076923Q773.36615,1.6984615,767.90155,1.6984615Q761.0339,1.6984615,755.75385,-0.8861538Q750.4738,-3.4707692,747.5939,-8.344615Q744.71387,-13.218462,744.71387,-19.864614L744.71387,-99.24923L720.8615,-99.24923L720.8615,-109.07077L757.12,-109.07077Z"
|
||||||
|
android:fillColor="#53BDB6"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="108dp"
|
|
||||||
android:height="108dp"
|
|
||||||
android:viewportWidth="108"
|
|
||||||
android:viewportHeight="108">
|
|
||||||
<path
|
|
||||||
android:fillColor="#3DDC84"
|
|
||||||
android:pathData="M0,0h108v108h-108z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M9,0L9,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,0L19,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M29,0L29,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M39,0L39,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M49,0L49,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M59,0L59,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M69,0L69,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M79,0L79,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M89,0L89,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M99,0L99,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,9L108,9"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,19L108,19"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,29L108,29"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,39L108,39"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,49L108,49"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,59L108,59"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,69L108,69"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,79L108,79"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,89L108,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,99L108,99"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,29L89,29"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,39L89,39"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,49L89,49"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,59L89,59"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,69L89,69"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,79L89,79"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M29,19L29,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M39,19L39,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M49,19L49,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M59,19L59,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M69,19L69,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M79,19L79,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
</vector>
|
|
||||||
@@ -1,30 +1,16 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:aapt="http://schemas.android.com/aapt"
|
|
||||||
android:width="108dp"
|
android:width="108dp"
|
||||||
android:height="108dp"
|
android:height="108dp"
|
||||||
android:viewportWidth="108"
|
android:viewportWidth="640"
|
||||||
android:viewportHeight="108">
|
android:viewportHeight="640">
|
||||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
<group android:scaleX="0.6"
|
||||||
<aapt:attr name="android:fillColor">
|
android:scaleY="0.6"
|
||||||
<gradient
|
android:translateX="128"
|
||||||
android:endX="85.84757"
|
android:translateY="128">
|
||||||
android:endY="92.4963"
|
|
||||||
android:startX="42.9492"
|
|
||||||
android:startY="49.59793"
|
|
||||||
android:type="linear">
|
|
||||||
<item
|
|
||||||
android:color="#44000000"
|
|
||||||
android:offset="0.0" />
|
|
||||||
<item
|
|
||||||
android:color="#00000000"
|
|
||||||
android:offset="1.0" />
|
|
||||||
</gradient>
|
|
||||||
</aapt:attr>
|
|
||||||
</path>
|
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FFFFFF"
|
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:fillType="nonZero"
|
android:fillColor="#53bdb6"
|
||||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
android:fillType="evenOdd"
|
||||||
android:strokeWidth="1"
|
android:strokeColor="#00000000"/>
|
||||||
android:strokeColor="#00000000" />
|
</group>
|
||||||
</vector>
|
</vector>
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<vector android:height="24dp" android:tint="#000000"
|
|
||||||
android:viewportHeight="24" android:viewportWidth="24"
|
|
||||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<path android:fillColor="@android:color/white" android:pathData="M12,1L3,5v6c0,5.55 3.84,10.74 9,12 5.16,-1.26 9,-6.45 9,-12V5l-9,-4z"/>
|
|
||||||
</vector>
|
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
<vector android:height="24dp" android:tint="#000000"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:viewportHeight="24" android:viewportWidth="24"
|
android:width="24dp"
|
||||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
android:height="24dp"
|
||||||
<path android:fillColor="@android:color/white" android:pathData="M20.83,18H21v-4h2v-4H12.83L20.83,18zM19.78,22.61l1.41,-1.41L2.81,2.81L1.39,4.22l2.59,2.59C2.2,7.85 1,9.79 1,12c0,3.31 2.69,6 6,6c2.21,0 4.15,-1.2 5.18,-2.99L19.78,22.61zM8.99,11.82C9,11.88 9,11.94 9,12c0,1.1 -0.9,2 -2,2s-2,-0.9 -2,-2s0.9,-2 2,-2c0.06,0 0.12,0 0.18,0.01L8.99,11.82z"/>
|
android:tint="#000000"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M20.83,18H21v-4h2v-4H12.83L20.83,18zM19.78,22.61l1.41,-1.41L2.81,2.81L1.39,4.22l2.59,2.59C2.2,7.85 1,9.79 1,12c0,3.31 2.69,6 6,6c2.21,0 4.15,-1.2 5.18,-2.99L19.78,22.61zM8.99,11.82C9,11.88 9,11.94 9,12c0,1.1 -0.9,2 -2,2s-2,-0.9 -2,-2s0.9,-2 2,-2c0.06,0 0.12,0 0.18,0.01L8.99,11.82z" />
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
<vector android:height="24dp" android:tint="#000000"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:viewportHeight="24" android:viewportWidth="24"
|
android:width="24dp"
|
||||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
android:height="24dp"
|
||||||
<path android:fillColor="@android:color/white" android:pathData="M12.65,10C11.83,7.67 9.61,6 7,6c-3.31,0 -6,2.69 -6,6s2.69,6 6,6c2.61,0 4.83,-1.67 5.65,-4H17v4h4v-4h2v-4H12.65zM7,14c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2z"/>
|
android:tint="#000000"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M12.65,10C11.83,7.67 9.61,6 7,6c-3.31,0 -6,2.69 -6,6s2.69,6 6,6c2.61,0 4.83,-1.67 5.65,-4H17v4h4v-4h2v-4H12.65zM7,14c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2z" />
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_banner_background"/>
|
<background android:drawable="@color/ic_banner_background"/>
|
||||||
<foreground android:drawable="@mipmap/ic_banner_foreground"/>
|
<foreground android:drawable="@drawable/ic_banner_foreground"/>
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
<!-- support for adaptive theme icons -->
|
||||||
|
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
<!-- support for adaptive theme icons -->
|
||||||
|
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 796 B |
|
Before Width: | Height: | Size: 1.9 KiB |