mirror of
https://github.com/wgtunnel/android.git
synced 2026-07-03 14:07:49 +02:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2690ce29e1 | |||
| 500b85f687 | |||
| 0197198f7b | |||
| 0b271778c9 | |||
| 6427b2f832 | |||
| 097097f620 | |||
| 20dfaed8de | |||
| 07aa37fc2a | |||
| 091cd2e028 | |||
| 7efa6d0bf4 | |||
| e31fb01410 | |||
| 76674323e7 | |||
| f1fc9ca6f7 | |||
| cb301e74eb | |||
| 8141fe19be | |||
| 0fdb3d0b31 | |||
| d9f3a21cc3 | |||
| fec84bc6ac | |||
| d6ee36edc0 | |||
| e3fcf712d5 | |||
| 5a15776bb3 | |||
| 3339448424 | |||
| 7ec294b789 | |||
| e59c72788d | |||
| 62435d549c | |||
| 96800037d1 | |||
| f5b3bb1cb7 | |||
| 654b4a4719 | |||
| a19b5ce22a | |||
| 86f592255c | |||
| 4a5dd76b5b | |||
| 1139d17f13 | |||
| 36855319a2 | |||
| 61e3751321 | |||
| dd16bd977f | |||
| aeb4a13389 | |||
| f0ec661223 |
@@ -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.
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- Android Version: [e.g. iOS8.1]
|
||||
- App Version [e.g. 22]
|
||||
|
||||
- Device: [e.g. Pixel 4a]
|
||||
- Android Version: [e.g. Android 13]
|
||||
- App Version [e.g. 3.3.3]
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# Support
|
||||
|
||||
If you are experiencing issues with the app, the following resources are available to help you.
|
||||
If you are experiencing issues with the app, the following resources are available to help you.
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
|
||||
@@ -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: Android CI Tag Deployment
|
||||
name: Android CI Tag Deployment (Pre-release)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- '*.*.*'
|
||||
- '*.*.*-**'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Signed APK
|
||||
# change to macos because of hilt issues on ubuntu in gradle 8.3
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
KEY_STORE_PATH: ${{ secrets.KEY_STORE_PATH }}
|
||||
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@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
@@ -35,10 +39,16 @@ jobs:
|
||||
id: decode_keystore
|
||||
uses: timheuer/base64-to-file@v1.2
|
||||
with:
|
||||
fileName: 'android_keystore.jks'
|
||||
fileDir: ${{ github.workspace }}/app/keystore/
|
||||
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
|
||||
@@ -52,15 +62,18 @@ jobs:
|
||||
- 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@v3.1.2
|
||||
uses: actions/upload-artifact@v4.3.0
|
||||
with:
|
||||
name: wgtunnel
|
||||
path: ${{ steps.apk-path.outputs.path }}
|
||||
- name: Download APK from build
|
||||
uses: actions/download-artifact@v1
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: wgtunnel
|
||||
- name: Create Release with Fastlane changelog notes
|
||||
@@ -70,18 +83,17 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
# fix hardcode changelog file name
|
||||
body_path: ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/32400.txt
|
||||
body_path: ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/${{ env.VERSION_CODE }}.txt
|
||||
tag_name: ${{ github.ref_name }}
|
||||
name: Release ${{ github.ref_name }}
|
||||
name: ${{ github.ref_name }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
prerelease: true
|
||||
files: ${{ github.workspace }}/${{ steps.apk-path.outputs.path }}
|
||||
- name: Deploy with fastlane
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: '3.2' # Not needed with a .ruby-version file
|
||||
bundler-cache: true
|
||||
|
||||
- name: Distribute app to Beta track 🚀
|
||||
run: (cd ${{ github.workspace }} && bundle install && bundle exec fastlane beta)
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
# name of the workflow
|
||||
name: Android CI Tag Deployment (Release)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- '*.*.*'
|
||||
- '!*.*.*-**'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Signed APK
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
|
||||
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
|
||||
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
|
||||
KEY_STORE_FILE: 'android_keystore.jks'
|
||||
KEY_STORE_LOCATION: ${{ github.workspace }}/app/keystore/
|
||||
GH_USER: ${{ secrets.GH_USER }}
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
cache: gradle
|
||||
- name: Grant execute permission for gradlew
|
||||
run: chmod +x gradlew
|
||||
|
||||
# Here we need to decode keystore.jks from base64 string and place it
|
||||
# in the folder specified in the release signing configuration
|
||||
- name: Decode Keystore
|
||||
id: decode_keystore
|
||||
uses: timheuer/base64-to-file@v1.2
|
||||
with:
|
||||
fileName: ${{ env.KEY_STORE_FILE }}
|
||||
fileDir: ${{ env.KEY_STORE_LOCATION }}
|
||||
encodedString: ${{ secrets.KEYSTORE }}
|
||||
|
||||
# create keystore path for gradle to read
|
||||
- name: Create keystore path env var
|
||||
run: |
|
||||
store_path=${{ env.KEY_STORE_LOCATION }}${{ env.KEY_STORE_FILE }}
|
||||
echo "KEY_STORE_PATH=$store_path" >> $GITHUB_ENV
|
||||
|
||||
- name: Create service_account.json
|
||||
id: createServiceAccount
|
||||
run: echo '${{ secrets.SERVICE_ACCOUNT_JSON }}' > service_account.json
|
||||
|
||||
# Build and sign APK ("-x test" argument is used to skip tests)
|
||||
# add fdroid flavor for apk upload
|
||||
- name: Build Fdroid Release APK
|
||||
run: ./gradlew :app:assembleFdroidRelease -x test
|
||||
|
||||
# get fdroid flavor release apk path
|
||||
- name: Get apk path
|
||||
id: apk-path
|
||||
run: echo "path=$(find . -regex '^.*/build/outputs/apk/fdroid/release/.*\.apk$' -type f | head -1)" >> $GITHUB_OUTPUT
|
||||
- name: Get version code
|
||||
run: |
|
||||
version_code=$(grep "VERSION_CODE" buildSrc/src/main/kotlin/Constants.kt | awk '{print $5}' | tr -d '\n')
|
||||
echo "VERSION_CODE=$version_code" >> $GITHUB_ENV
|
||||
# Save the APK after the Build job is complete to publish it as a Github release in the next job
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@v4.3.0
|
||||
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://www.amazon.com/gp/product/B0CFGGL7WK)
|
||||
[](https://f-droid.org/packages/com.zaneschepke.wireguardautotunnel/)
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://ko-fi.com/N4N8NMJN2)
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<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>
|
||||
|
||||
@@ -37,38 +33,69 @@ This is an alternative Android Application for [WireGuard](https://www.wireguard
|
||||
## Screenshots
|
||||
|
||||
<p float="center">
|
||||
<img label="Main" style="padding-right:25px" src="asset/main_screen.png" width="200" />
|
||||
<img label="Config" style="padding-left:25px" src="asset/config_screen.png" width="200" />
|
||||
<img label="Settings" style="padding-left:25px" src="asset/settings_screen.png" width="200" />
|
||||
<img label="Support" style="padding-left:25px" src="asset/support_screen.png" width="200" />
|
||||
<img label="Main" style="padding-right:25px" src="fastlane/metadata/android/en-US/images/phoneScreenshots/main_screen.png" width="200" />
|
||||
<img label="Config" style="padding-left:25px" src="fastlane/metadata/android/en-US/images/phoneScreenshots/config_screen.png" width="200" />
|
||||
<img label="Settings" style="padding-left:25px" src="fastlane/metadata/android/en-US/images/phoneScreenshots/settings_screen.png" width="200" />
|
||||
<img label="Support" style="padding-left:25px" src="fastlane/metadata/android/en-US/images/phoneScreenshots/support_screen.png" width="200" />
|
||||
</p>
|
||||
|
||||
<div align="left">
|
||||
|
||||
## 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
|
||||
|
||||
* 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
|
||||
* Always-on VPN for Android support
|
||||
* WireGuard support for kernel and userspace modes
|
||||
* Always-On VPN support
|
||||
* Export tunnels to zip
|
||||
* Quick tile support for VPN toggling
|
||||
* Static shortcuts support for primary tunnel for automation integration
|
||||
* Intent automation support for all tunnels
|
||||
* Optional auto connect on mobile data, ethernet
|
||||
* 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
|
||||
|
||||
|
||||
```
|
||||
$ git clone https://github.com/zaneschepke/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
|
||||
```
|
||||
|
||||
|
||||
+38
-42
@@ -19,9 +19,7 @@ android {
|
||||
versionCode = Constants.VERSION_CODE
|
||||
versionName = Constants.VERSION_NAME
|
||||
|
||||
ksp {
|
||||
arg("room.schemaLocation", "$projectDir/schemas")
|
||||
}
|
||||
ksp { arg("room.schemaLocation", "$projectDir/schemas") }
|
||||
|
||||
sourceSets {
|
||||
getByName("debug").assets.srcDirs(files("$projectDir/schemas")) // Room
|
||||
@@ -30,9 +28,7 @@ android {
|
||||
resourceConfigurations.addAll(listOf("en"))
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
useSupportLibrary = true
|
||||
}
|
||||
vectorDrawables { useSupportLibrary = true }
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
@@ -47,32 +43,41 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
// try to get secrets from env first for pipeline build, then properties file for local build
|
||||
storeFile = file(
|
||||
System.getenv().getOrDefault(
|
||||
Constants.KEY_STORE_PATH_VAR,
|
||||
properties.getProperty(Constants.KEY_STORE_PATH_VAR)
|
||||
// try to get secrets from env first for pipeline build, then properties file for local
|
||||
// build
|
||||
storeFile =
|
||||
file(
|
||||
System.getenv()
|
||||
.getOrDefault(
|
||||
Constants.KEY_STORE_PATH_VAR,
|
||||
properties.getProperty(Constants.KEY_STORE_PATH_VAR),
|
||||
),
|
||||
)
|
||||
)
|
||||
storePassword = System.getenv().getOrDefault(
|
||||
Constants.STORE_PASS_VAR,
|
||||
properties.getProperty(Constants.STORE_PASS_VAR)
|
||||
)
|
||||
keyAlias = System.getenv().getOrDefault(
|
||||
Constants.KEY_ALIAS_VAR,
|
||||
properties.getProperty(Constants.KEY_ALIAS_VAR)
|
||||
)
|
||||
keyPassword = System.getenv().getOrDefault(
|
||||
Constants.KEY_PASS_VAR,
|
||||
properties.getProperty(Constants.KEY_PASS_VAR)
|
||||
)
|
||||
storePassword =
|
||||
System.getenv()
|
||||
.getOrDefault(
|
||||
Constants.STORE_PASS_VAR,
|
||||
properties.getProperty(Constants.STORE_PASS_VAR),
|
||||
)
|
||||
keyAlias =
|
||||
System.getenv()
|
||||
.getOrDefault(
|
||||
Constants.KEY_ALIAS_VAR,
|
||||
properties.getProperty(Constants.KEY_ALIAS_VAR),
|
||||
)
|
||||
keyPassword =
|
||||
System.getenv()
|
||||
.getOrDefault(
|
||||
Constants.KEY_PASS_VAR,
|
||||
properties.getProperty(Constants.KEY_PASS_VAR),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
// don't strip
|
||||
packaging.jniLibs.keepDebugSymbols.addAll(
|
||||
listOf("libwg-go.so", "libwg-quick.so", "libwg.so")
|
||||
listOf("libwg-go.so", "libwg-quick.so", "libwg.so"),
|
||||
)
|
||||
|
||||
applicationVariants.all {
|
||||
@@ -91,13 +96,11 @@ android {
|
||||
isShrinkResources = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
"proguard-rules.pro",
|
||||
)
|
||||
signingConfig = signingConfigs.getByName(Constants.RELEASE)
|
||||
}
|
||||
debug {
|
||||
isDebuggable = true
|
||||
}
|
||||
debug { isDebuggable = true }
|
||||
}
|
||||
flavorDimensions.add(Constants.TYPE)
|
||||
productFlavors {
|
||||
@@ -118,24 +121,17 @@ android {
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = Constants.JVM_TARGET
|
||||
}
|
||||
kotlinOptions { jvmTarget = Constants.JVM_TARGET }
|
||||
buildFeatures {
|
||||
compose = true
|
||||
buildConfig = true
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = libs.versions.composeCompiler.get()
|
||||
}
|
||||
packaging {
|
||||
resources {
|
||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||
}
|
||||
}
|
||||
composeOptions { kotlinCompilerExtensionVersion = Constants.COMPOSE_COMPILER_EXTENSION_VERSION }
|
||||
packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } }
|
||||
}
|
||||
|
||||
val generalImplementation by configurations
|
||||
|
||||
dependencies {
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
@@ -160,13 +156,14 @@ dependencies {
|
||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||
debugImplementation(libs.androidx.compose.manifest)
|
||||
|
||||
// wg
|
||||
// get tunnel lib from github packages or mavenLocal
|
||||
implementation(libs.tunnel)
|
||||
coreLibraryDesugaring(libs.desugar.jdk.libs)
|
||||
|
||||
// logging
|
||||
implementation(libs.timber)
|
||||
|
||||
|
||||
// compose navigation
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.androidx.hilt.navigation.compose)
|
||||
@@ -176,7 +173,6 @@ dependencies {
|
||||
ksp(libs.hilt.android.compiler)
|
||||
|
||||
// accompanist
|
||||
implementation(libs.accompanist.systemuicontroller)
|
||||
implementation(libs.accompanist.permissions)
|
||||
implementation(libs.accompanist.flowlayout)
|
||||
implementation(libs.accompanist.drawablepainter)
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
|
||||
<fields>;
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+2
@@ -22,3 +22,5 @@
|
||||
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
|
||||
<fields>;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 5,
|
||||
"identityHash": "bc15003a44746e18b9c260ec49737089",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "Settings",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `default_tunnel` TEXT, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_battery_saver_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_auto_tunnel_paused` INTEGER NOT NULL DEFAULT false)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAutoTunnelEnabled",
|
||||
"columnName": "is_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnMobileDataEnabled",
|
||||
"columnName": "is_tunnel_on_mobile_data_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "trustedNetworkSSIDs",
|
||||
"columnName": "trusted_network_ssids",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "defaultTunnel",
|
||||
"columnName": "default_tunnel",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAlwaysOnVpnEnabled",
|
||||
"columnName": "is_always_on_vpn_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnEthernetEnabled",
|
||||
"columnName": "is_tunnel_on_ethernet_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isShortcutsEnabled",
|
||||
"columnName": "is_shortcuts_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isBatterySaverEnabled",
|
||||
"columnName": "is_battery_saver_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnWifiEnabled",
|
||||
"columnName": "is_tunnel_on_wifi_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isKernelEnabled",
|
||||
"columnName": "is_kernel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isRestoreOnBootEnabled",
|
||||
"columnName": "is_restore_on_boot_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isMultiTunnelEnabled",
|
||||
"columnName": "is_multi_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAutoTunnelPaused",
|
||||
"columnName": "is_auto_tunnel_paused",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "TunnelConfig",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "wgQuick",
|
||||
"columnName": "wg_quick",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_TunnelConfig_name",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"name"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'bc15003a44746e18b9c260ec49737089')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -3,52 +3,33 @@ package com.zaneschepke.wireguardautotunnel
|
||||
import androidx.room.testing.MigrationTestHelper
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.zaneschepke.wireguardautotunnel.repository.AppDatabase
|
||||
import java.io.IOException
|
||||
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
||||
import com.zaneschepke.wireguardautotunnel.data.Queries
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import java.io.IOException
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class MigrationTest {
|
||||
private val dbName = "migration-test"
|
||||
|
||||
@get:Rule
|
||||
val helper: MigrationTestHelper = MigrationTestHelper(
|
||||
InstrumentationRegistry.getInstrumentation(),
|
||||
AppDatabase::class.java
|
||||
)
|
||||
val helper: MigrationTestHelper =
|
||||
MigrationTestHelper(
|
||||
InstrumentationRegistry.getInstrumentation(),
|
||||
AppDatabase::class.java,
|
||||
)
|
||||
|
||||
@Test
|
||||
@Throws(IOException::class)
|
||||
fun migrate2To3() {
|
||||
helper.createDatabase(dbName, 3).apply {
|
||||
fun migrate4To5() {
|
||||
helper.createDatabase(dbName, 4).apply {
|
||||
// Database has schema version 1. Insert some data using SQL queries.
|
||||
// You can't use DAO classes because they expect the latest schema.
|
||||
execSQL(Queries.createDefaultSettings())
|
||||
execSQL(
|
||||
"INSERT INTO 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)" +
|
||||
" VALUES (" +
|
||||
"false," +
|
||||
"false," +
|
||||
"'[trustedSSID1,trustedSSID2]'," +
|
||||
"'defaultTunnel'," +
|
||||
"false," +
|
||||
"false," +
|
||||
"false," +
|
||||
"false," +
|
||||
"false)"
|
||||
)
|
||||
execSQL(
|
||||
"INSERT INTO TunnelConfig (name, wg_quick)" +
|
||||
" VALUES ('hello', 'hello')"
|
||||
"INSERT INTO TunnelConfig (name, wg_quick)" + " VALUES ('hello', 'hello')",
|
||||
)
|
||||
// Prepare for the next version.
|
||||
close()
|
||||
@@ -56,7 +37,7 @@ class MigrationTest {
|
||||
|
||||
// Re-open the database with version 2 and provide
|
||||
// MIGRATION_1_2 as the migration process.
|
||||
helper.runMigrationsAndValidate(dbName, 4, true)
|
||||
helper.runMigrationsAndValidate(dbName, 5, true)
|
||||
// MigrationTestHelper automatically verifies the schema changes,
|
||||
// but you need to validate that the data was migrated properly.
|
||||
}
|
||||
|
||||
@@ -1,72 +1,82 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32"
|
||||
tools:ignore="ScopedStorage" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"
|
||||
<uses-permission
|
||||
android:name="android.permission.ACCESS_WIFI_STATE"
|
||||
android:maxSdkVersion="30"
|
||||
tools:ignore="LeanbackUsesWifi" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
||||
<!--foreground service exempt android 14-->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED"/>
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING"/>
|
||||
<!--foreground service permissions-->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
<!--start service on boot permission-->
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<!--android tv support-->
|
||||
<uses-feature android:name="android.software.leanback"
|
||||
<uses-feature
|
||||
android:name="android.software.leanback"
|
||||
android:required="false" />
|
||||
<uses-feature android:name="android.hardware.touchscreen"
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.location.gps"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.screen.portrait"
|
||||
android:required="false" />
|
||||
android:name="android.hardware.screen.portrait"
|
||||
android:required="false" />
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
</intent>
|
||||
</queries>
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:name=".WireGuardAutoTunnel"
|
||||
android:allowBackup="true"
|
||||
android:banner="@mipmap/ic_banner"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:banner="@mipmap/ic_banner"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.WireguardAutoTunnel"
|
||||
tools:targetApi="31">
|
||||
tools:targetApi="tiramisu">
|
||||
<activity
|
||||
android:name=".ui.MainActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.WireguardAutoTunnel">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES"/>
|
||||
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.app.shortcuts"
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts" />
|
||||
</activity>
|
||||
<activity
|
||||
@@ -76,58 +86,74 @@
|
||||
android:theme="@style/zxing_CaptureTheme"
|
||||
android:windowSoftInputMode="stateAlwaysHidden" />
|
||||
<activity
|
||||
android:finishOnTaskLaunch="true"
|
||||
android:name=".service.shortcut.ShortcutsActivity"
|
||||
android:enabled="true"
|
||||
android:exported="true"
|
||||
android:theme="@android:style/Theme.NoDisplay"
|
||||
android:name=".service.shortcut.ShortcutsActivity"/>
|
||||
android:finishOnTaskLaunch="true"
|
||||
android:theme="@android:style/Theme.NoDisplay" />
|
||||
|
||||
<service
|
||||
android:name=".service.foreground.ForegroundService"
|
||||
android:enabled="true"
|
||||
android:foregroundServiceType="remoteMessaging"
|
||||
android:exported="false">
|
||||
</service>
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="systemExempted|specialUse"
|
||||
tools:node="merge" />
|
||||
<service
|
||||
android:exported="true"
|
||||
android:name=".service.tile.TunnelControlTile"
|
||||
android:icon="@drawable/shield"
|
||||
android:exported="true"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="WG Tunnel"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
<meta-data android:name="android.service.quicksettings.ACTIVE_TILE"
|
||||
<meta-data
|
||||
android:name="android.service.quicksettings.ACTIVE_TILE"
|
||||
android:value="true" />
|
||||
<meta-data android:name="android.service.quicksettings.TOGGLEABLE_TILE"
|
||||
<meta-data
|
||||
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
|
||||
android:value="true" />
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service
|
||||
android:name=".service.foreground.WireGuardTunnelService"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="systemExempted|specialUse"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||
android:persistent="true"
|
||||
android:foregroundServiceType="systemExempted"
|
||||
android:exported="false">
|
||||
tools:node="merge">
|
||||
<intent-filter>
|
||||
<action android:name="android.net.VpnService"/>
|
||||
<action android:name="android.net.VpnService" />
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
|
||||
<meta-data
|
||||
android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
|
||||
android:value="true" />
|
||||
</service>
|
||||
<service
|
||||
android:name=".service.foreground.WireGuardConnectivityWatcherService"
|
||||
android:enabled="true"
|
||||
android:stopWithTask="false"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="systemExempted|specialUse"
|
||||
android:persistent="true"
|
||||
android:foregroundServiceType="systemExempted"
|
||||
android:stopWithTask="false"
|
||||
tools:node="merge" />
|
||||
|
||||
<receiver
|
||||
android:name=".receiver.BootReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false">
|
||||
</service>
|
||||
<receiver android:enabled="true" android:name=".receiver.BootReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="android.intent.action.ACTION_BOOT_COMPLETED" />
|
||||
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
|
||||
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver android:exported="false" android:name=".receiver.NotificationActionReceiver"/>
|
||||
<receiver
|
||||
android:name=".receiver.NotificationActionReceiver"
|
||||
android:exported="false" />
|
||||
</application>
|
||||
</manifest>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 16 KiB |
@@ -1,31 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import java.math.BigDecimal
|
||||
import java.text.DecimalFormat
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
fun BroadcastReceiver.goAsync(
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
block: suspend CoroutineScope.() -> Unit
|
||||
) {
|
||||
val pendingResult = goAsync()
|
||||
@OptIn(DelicateCoroutinesApi::class) // Must run globally; there's no teardown callback.
|
||||
GlobalScope.launch(context) {
|
||||
try {
|
||||
block()
|
||||
} finally {
|
||||
pendingResult.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun BigDecimal.toThreeDecimalPlaceString(): String {
|
||||
val df = DecimalFormat("#.###")
|
||||
return df.format(this)
|
||||
}
|
||||
@@ -1,56 +1,34 @@
|
||||
package com.zaneschepke.wireguardautotunnel
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.ComponentName
|
||||
import android.content.pm.PackageManager
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||
import com.zaneschepke.wireguardautotunnel.repository.datastore.DataStoreManager
|
||||
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
||||
import android.service.quicksettings.TileService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
@HiltAndroidApp
|
||||
class WireGuardAutoTunnel : Application() {
|
||||
@Inject
|
||||
lateinit var settingsRepo: SettingsDoa
|
||||
|
||||
@Inject
|
||||
lateinit var dataStoreManager: DataStoreManager
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
instance = this
|
||||
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
||||
initSettings()
|
||||
with(ProcessLifecycleOwner.get()) {
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
// load preferences into memory
|
||||
dataStoreManager.init()
|
||||
} catch (e: IOException) {
|
||||
Timber.e("Failed to load preferences")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initSettings() {
|
||||
with(ProcessLifecycleOwner.get()) {
|
||||
lifecycleScope.launch {
|
||||
if (settingsRepo.getAll().isEmpty()) {
|
||||
settingsRepo.save(Settings())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun isRunningOnAndroidTv(context: Context): Boolean {
|
||||
return context.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
||||
lateinit var instance: WireGuardAutoTunnel
|
||||
private set
|
||||
|
||||
fun isRunningOnAndroidTv(): Boolean {
|
||||
return instance.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
||||
}
|
||||
|
||||
fun requestTileServiceStateUpdate() {
|
||||
TileService.requestListeningState(
|
||||
instance,
|
||||
ComponentName(instance, TunnelControlTile::class.java),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data
|
||||
|
||||
import androidx.room.AutoMigration
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
|
||||
@Database(
|
||||
entities = [Settings::class, TunnelConfig::class],
|
||||
version = 5,
|
||||
autoMigrations =
|
||||
[
|
||||
AutoMigration(from = 1, to = 2),
|
||||
AutoMigration(from = 2, to = 3),
|
||||
AutoMigration(
|
||||
from = 3,
|
||||
to = 4,
|
||||
),
|
||||
AutoMigration(
|
||||
from = 4,
|
||||
to = 5,
|
||||
),
|
||||
],
|
||||
exportSchema = true,
|
||||
)
|
||||
@TypeConverters(DatabaseListConverters::class)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun settingDao(): SettingsDao
|
||||
|
||||
abstract fun tunnelConfigDoa(): TunnelConfigDao
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data
|
||||
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import timber.log.Timber
|
||||
|
||||
class DatabaseCallback : RoomDatabase.Callback() {
|
||||
override fun onCreate(db: SupportSQLiteDatabase) = db.run {
|
||||
// Notice non-ui thread is here
|
||||
beginTransaction()
|
||||
try {
|
||||
execSQL(Queries.createDefaultSettings())
|
||||
Timber.i("Bootstrapping settings data")
|
||||
setTransactionSuccessful()
|
||||
} catch (e : Exception) {
|
||||
Timber.e(e)
|
||||
} finally {
|
||||
endTransaction()
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.zaneschepke.wireguardautotunnel.repository
|
||||
package com.zaneschepke.wireguardautotunnel.data
|
||||
|
||||
import androidx.room.TypeConverter
|
||||
import kotlinx.serialization.encodeToString
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data
|
||||
|
||||
object Queries {
|
||||
fun createDefaultSettings() : String {
|
||||
return """
|
||||
INSERT INTO Settings (is_tunnel_enabled,
|
||||
is_tunnel_on_mobile_data_enabled,
|
||||
trusted_network_ssids,
|
||||
default_tunnel,
|
||||
is_always_on_vpn_enabled,
|
||||
is_tunnel_on_ethernet_enabled,
|
||||
is_shortcuts_enabled,
|
||||
is_battery_saver_enabled,
|
||||
is_tunnel_on_wifi_enabled,
|
||||
is_kernel_enabled,
|
||||
is_restore_on_boot_enabled,
|
||||
is_multi_tunnel_enabled)
|
||||
VALUES
|
||||
('false',
|
||||
'false',
|
||||
'[trustedSSID1,trustedSSID2]',
|
||||
NULL,
|
||||
'false',
|
||||
'false',
|
||||
'false',
|
||||
'false',
|
||||
'false',
|
||||
'false',
|
||||
'false',
|
||||
'false')
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface SettingsDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun save(t: Settings)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun saveAll(t: List<Settings>)
|
||||
|
||||
@Query("SELECT * FROM settings WHERE id=:id") suspend fun getById(id: Long): Settings?
|
||||
|
||||
@Query("SELECT * FROM settings") suspend fun getAll(): List<Settings>
|
||||
|
||||
@Query("SELECT * FROM settings LIMIT 1") fun getSettingsFlow(): Flow<Settings>
|
||||
|
||||
@Query("SELECT * FROM settings") fun getAllFlow(): Flow<MutableList<Settings>>
|
||||
|
||||
@Delete suspend fun delete(t: Settings)
|
||||
|
||||
@Query("SELECT COUNT('id') FROM settings") suspend fun count(): Long
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface TunnelConfigDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun save(t: TunnelConfig)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun saveAll(t: List<TunnelConfig>)
|
||||
|
||||
@Query("SELECT * FROM TunnelConfig WHERE id=:id") suspend fun getById(id: Long): TunnelConfig?
|
||||
|
||||
@Query("SELECT * FROM TunnelConfig") suspend fun getAll(): List<TunnelConfig>
|
||||
|
||||
@Delete suspend fun delete(t: TunnelConfig)
|
||||
|
||||
@Query("SELECT COUNT('id') FROM TunnelConfig") suspend fun count(): Long
|
||||
|
||||
@Query("SELECT * FROM tunnelconfig") fun getAllFlow(): Flow<MutableList<TunnelConfig>>
|
||||
}
|
||||
+13
-14
@@ -1,4 +1,5 @@
|
||||
package com.zaneschepke.wireguardautotunnel.repository.datastore
|
||||
package com.zaneschepke.wireguardautotunnel.data.datastore
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
@@ -11,29 +12,27 @@ import kotlinx.coroutines.flow.map
|
||||
class DataStoreManager(private val context: Context) {
|
||||
companion object {
|
||||
val LOCATION_DISCLOSURE_SHOWN = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN")
|
||||
val BATTERY_OPTIMIZE_DISABLE_SHOWN = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN")
|
||||
}
|
||||
|
||||
// preferences
|
||||
private val preferencesKey = "preferences"
|
||||
private val Context.dataStore by preferencesDataStore(
|
||||
name = preferencesKey
|
||||
)
|
||||
private val Context.dataStore by
|
||||
preferencesDataStore(
|
||||
name = preferencesKey,
|
||||
)
|
||||
|
||||
suspend fun init() {
|
||||
context.dataStore.data.first()
|
||||
}
|
||||
|
||||
suspend fun <T> saveToDataStore(key: Preferences.Key<T>, value: T) =
|
||||
context.dataStore.edit {
|
||||
it[key] = value
|
||||
}
|
||||
context.dataStore.edit { it[key] = value }
|
||||
|
||||
fun <T> getFromStore(key: Preferences.Key<T>) =
|
||||
context.dataStore.data.map {
|
||||
it[key]
|
||||
}
|
||||
fun <T> getFromStoreFlow(key: Preferences.Key<T>) = context.dataStore.data.map { it[key] }
|
||||
|
||||
val locationDisclosureFlow: Flow<Boolean?> = context.dataStore.data.map {
|
||||
it[LOCATION_DISCLOSURE_SHOWN]
|
||||
}
|
||||
suspend fun <T> getFromStore(key: Preferences.Key<T>) =
|
||||
context.dataStore.data.first { it.contains(key) }[key]
|
||||
|
||||
val preferencesFlow: Flow<Preferences?> = context.dataStore.data
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.model
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity
|
||||
data class Settings(
|
||||
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
||||
@ColumnInfo(name = "is_tunnel_enabled") var isAutoTunnelEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "is_tunnel_on_mobile_data_enabled")
|
||||
var isTunnelOnMobileDataEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "trusted_network_ssids")
|
||||
var trustedNetworkSSIDs: MutableList<String> = mutableListOf(),
|
||||
@ColumnInfo(name = "default_tunnel") var defaultTunnel: String? = null,
|
||||
@ColumnInfo(name = "is_always_on_vpn_enabled") var isAlwaysOnVpnEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "is_tunnel_on_ethernet_enabled")
|
||||
var isTunnelOnEthernetEnabled: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "is_shortcuts_enabled",
|
||||
defaultValue = "false",
|
||||
)
|
||||
var isShortcutsEnabled: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "is_battery_saver_enabled",
|
||||
defaultValue = "false",
|
||||
)
|
||||
var isBatterySaverEnabled: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "is_tunnel_on_wifi_enabled",
|
||||
defaultValue = "false",
|
||||
)
|
||||
var isTunnelOnWifiEnabled: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "is_kernel_enabled",
|
||||
defaultValue = "false",
|
||||
)
|
||||
var isKernelEnabled: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "is_restore_on_boot_enabled",
|
||||
defaultValue = "false",
|
||||
)
|
||||
var isRestoreOnBootEnabled: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "is_multi_tunnel_enabled",
|
||||
defaultValue = "false",
|
||||
)
|
||||
var isMultiTunnelEnabled: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "is_auto_tunnel_paused",
|
||||
defaultValue = "false",
|
||||
)
|
||||
var isAutoTunnelPaused: Boolean = false,
|
||||
) {
|
||||
fun isTunnelConfigDefault(tunnelConfig: TunnelConfig): Boolean {
|
||||
return if (defaultTunnel != null) {
|
||||
val defaultConfig = TunnelConfig.from(defaultTunnel!!)
|
||||
(tunnelConfig.id == defaultConfig.id)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,13 +1,13 @@
|
||||
package com.zaneschepke.wireguardautotunnel.repository.model
|
||||
package com.zaneschepke.wireguardautotunnel.data.model
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import com.wireguard.config.Config
|
||||
import java.io.InputStream
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.InputStream
|
||||
|
||||
@Entity(indices = [Index(value = ["name"], unique = true)])
|
||||
@Serializable
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface SettingsRepository {
|
||||
suspend fun save(settings: Settings)
|
||||
|
||||
fun getSettingsFlow(): Flow<Settings>
|
||||
|
||||
suspend fun getSettings(): Settings
|
||||
|
||||
suspend fun getAll(): List<Settings>
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.SettingsDao
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class SettingsRepositoryImpl(private val settingsDoa: SettingsDao) : SettingsRepository {
|
||||
|
||||
override suspend fun save(settings: Settings) {
|
||||
settingsDoa.save(settings)
|
||||
}
|
||||
|
||||
override fun getSettingsFlow(): Flow<Settings> {
|
||||
return settingsDoa.getSettingsFlow()
|
||||
}
|
||||
|
||||
override suspend fun getSettings(): Settings {
|
||||
return settingsDoa.getAll().firstOrNull() ?: Settings()
|
||||
}
|
||||
|
||||
override suspend fun getAll(): List<Settings> {
|
||||
return settingsDoa.getAll()
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface TunnelConfigRepository {
|
||||
|
||||
fun getTunnelConfigsFlow(): Flow<TunnelConfigs>
|
||||
|
||||
suspend fun getAll(): TunnelConfigs
|
||||
|
||||
suspend fun save(tunnelConfig: TunnelConfig)
|
||||
|
||||
suspend fun delete(tunnelConfig: TunnelConfig)
|
||||
|
||||
suspend fun count(): Int
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class TunnelConfigRepositoryImpl(private val tunnelConfigDao: TunnelConfigDao) :
|
||||
TunnelConfigRepository {
|
||||
override fun getTunnelConfigsFlow(): Flow<TunnelConfigs> {
|
||||
return tunnelConfigDao.getAllFlow()
|
||||
}
|
||||
|
||||
override suspend fun getAll(): TunnelConfigs {
|
||||
return tunnelConfigDao.getAll()
|
||||
}
|
||||
|
||||
override suspend fun save(tunnelConfig: TunnelConfig) {
|
||||
tunnelConfigDao.save(tunnelConfig)
|
||||
}
|
||||
|
||||
override suspend fun delete(tunnelConfig: TunnelConfig) {
|
||||
tunnelConfigDao.delete(tunnelConfig)
|
||||
}
|
||||
|
||||
override suspend fun count(): Int {
|
||||
return tunnelConfigDao.count().toInt()
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,8 @@ package com.zaneschepke.wireguardautotunnel.module
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.repository.AppDatabase
|
||||
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
||||
import com.zaneschepke.wireguardautotunnel.data.DatabaseCallback
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
@@ -16,15 +17,14 @@ import javax.inject.Singleton
|
||||
class DatabaseModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDatabase(
|
||||
@ApplicationContext context: Context
|
||||
): AppDatabase {
|
||||
fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
|
||||
return Room.databaseBuilder(
|
||||
context,
|
||||
AppDatabase::class.java,
|
||||
context.getString(R.string.db_name)
|
||||
)
|
||||
context,
|
||||
AppDatabase::class.java,
|
||||
context.getString(R.string.db_name),
|
||||
)
|
||||
.fallbackToDestructiveMigration()
|
||||
.addCallback(DatabaseCallback())
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,4 @@ package com.zaneschepke.wireguardautotunnel.module
|
||||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Qualifier
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
annotation class Kernel
|
||||
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class Kernel
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
package com.zaneschepke.wireguardautotunnel.module
|
||||
|
||||
import android.content.Context
|
||||
import com.zaneschepke.wireguardautotunnel.repository.AppDatabase
|
||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
|
||||
import com.zaneschepke.wireguardautotunnel.repository.datastore.DataStoreManager
|
||||
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
||||
import com.zaneschepke.wireguardautotunnel.data.SettingsDao
|
||||
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
|
||||
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepositoryImpl
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepositoryImpl
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
@@ -17,16 +21,28 @@ import javax.inject.Singleton
|
||||
class RepositoryModule {
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideSettingsRepository(appDatabase: AppDatabase): SettingsDoa {
|
||||
fun provideSettingsDoa(appDatabase: AppDatabase): SettingsDao {
|
||||
return appDatabase.settingDao()
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideTunnelConfigRepository(appDatabase: AppDatabase): TunnelConfigDao {
|
||||
fun provideTunnelConfigDoa(appDatabase: AppDatabase): TunnelConfigDao {
|
||||
return appDatabase.tunnelConfigDoa()
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideTunnelConfigRepository(tunnelConfigDao: TunnelConfigDao): TunnelConfigRepository {
|
||||
return TunnelConfigRepositoryImpl(tunnelConfigDao)
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideSettingsRepository(settingsDao: SettingsDao): SettingsRepository {
|
||||
return SettingsRepositoryImpl(settingsDao)
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun providePreferencesDataStore(@ApplicationContext context: Context): DataStoreManager {
|
||||
|
||||
@@ -17,7 +17,9 @@ import dagger.hilt.android.scopes.ServiceScoped
|
||||
abstract class ServiceModule {
|
||||
@Binds
|
||||
@ServiceScoped
|
||||
abstract fun provideNotificationService(wireGuardNotification: WireGuardNotification): NotificationService
|
||||
abstract fun provideNotificationService(
|
||||
wireGuardNotification: WireGuardNotification
|
||||
): NotificationService
|
||||
|
||||
@Binds
|
||||
@ServiceScoped
|
||||
@@ -25,9 +27,13 @@ abstract class ServiceModule {
|
||||
|
||||
@Binds
|
||||
@ServiceScoped
|
||||
abstract fun provideMobileDataService(mobileDataService: MobileDataService): NetworkService<MobileDataService>
|
||||
abstract fun provideMobileDataService(
|
||||
mobileDataService: MobileDataService
|
||||
): NetworkService<MobileDataService>
|
||||
|
||||
@Binds
|
||||
@ServiceScoped
|
||||
abstract fun provideEthernetService(ethernetService: EthernetService): NetworkService<EthernetService>
|
||||
abstract fun provideEthernetService(
|
||||
ethernetService: EthernetService
|
||||
): NetworkService<EthernetService>
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import com.wireguard.android.backend.GoBackend
|
||||
import com.wireguard.android.backend.WgQuickBackend
|
||||
import com.wireguard.android.util.RootShell
|
||||
import com.wireguard.android.util.ToolsInstaller
|
||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.WireGuardTunnel
|
||||
import dagger.Module
|
||||
@@ -21,28 +21,21 @@ import javax.inject.Singleton
|
||||
class TunnelModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideRootShell(
|
||||
@ApplicationContext context: Context
|
||||
): RootShell {
|
||||
fun provideRootShell(@ApplicationContext context: Context): RootShell {
|
||||
return RootShell(context)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Userspace
|
||||
fun provideUserspaceBackend(
|
||||
@ApplicationContext context: Context
|
||||
): Backend {
|
||||
fun provideUserspaceBackend(@ApplicationContext context: Context): Backend {
|
||||
return GoBackend(context)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Kernel
|
||||
fun provideKernelBackend(
|
||||
@ApplicationContext context: Context,
|
||||
rootShell: RootShell
|
||||
): Backend {
|
||||
fun provideKernelBackend(@ApplicationContext context: Context, rootShell: RootShell): Backend {
|
||||
return WgQuickBackend(context, rootShell, ToolsInstaller(context, rootShell))
|
||||
}
|
||||
|
||||
@@ -51,8 +44,8 @@ class TunnelModule {
|
||||
fun provideVpnService(
|
||||
@Userspace userspaceBackend: Backend,
|
||||
@Kernel kernelBackend: Backend,
|
||||
settingsDoa: SettingsDoa
|
||||
settingsRepository: SettingsRepository
|
||||
): VpnService {
|
||||
return WireGuardTunnel(userspaceBackend, kernelBackend, settingsDoa)
|
||||
return WireGuardTunnel(userspaceBackend, kernelBackend, settingsRepository)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,4 @@ package com.zaneschepke.wireguardautotunnel.module
|
||||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Qualifier
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
annotation class Userspace
|
||||
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class Userspace
|
||||
|
||||
@@ -3,34 +3,30 @@ package com.zaneschepke.wireguardautotunnel.receiver
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.zaneschepke.wireguardautotunnel.goAsync
|
||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||
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.util.goAsync
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.cancel
|
||||
|
||||
@AndroidEntryPoint
|
||||
class BootReceiver : BroadcastReceiver() {
|
||||
@Inject
|
||||
lateinit var settingsRepo: SettingsDoa
|
||||
|
||||
override fun onReceive(
|
||||
context: Context,
|
||||
intent: Intent
|
||||
) = goAsync {
|
||||
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
|
||||
try {
|
||||
val settings = settingsRepo.getAll()
|
||||
if (settings.isNotEmpty()) {
|
||||
val setting = settings.first()
|
||||
if (setting.isAutoTunnelEnabled && setting.defaultTunnel != null) {
|
||||
ServiceManager.startWatcherService(context, setting.defaultTunnel!!)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
cancel()
|
||||
}
|
||||
@Inject lateinit var settingsRepository: SettingsRepository
|
||||
|
||||
@Inject lateinit var tunnelConfigRepository: TunnelConfigRepository
|
||||
|
||||
override fun onReceive(context: Context?, intent: Intent?) = goAsync {
|
||||
if (Intent.ACTION_BOOT_COMPLETED != intent?.action) return@goAsync
|
||||
val settings = settingsRepository.getSettings()
|
||||
if (settings.isAutoTunnelEnabled) {
|
||||
Timber.i("Starting watcher service from boot")
|
||||
ServiceManager.startWatcherServiceForeground(context!!)
|
||||
} else if(settings.isAlwaysOnVpnEnabled) {
|
||||
Timber.i("Starting tunnel from boot")
|
||||
ServiceManager.startVpnServicePrimaryTunnel(context!!, settings, tunnelConfigRepository.getAll().firstOrNull())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-18
@@ -3,33 +3,26 @@ package com.zaneschepke.wireguardautotunnel.receiver
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.zaneschepke.wireguardautotunnel.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.goAsync
|
||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.goAsync
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class NotificationActionReceiver : BroadcastReceiver() {
|
||||
@Inject
|
||||
lateinit var settingsRepo: SettingsDoa
|
||||
@Inject lateinit var settingsRepository: SettingsRepository
|
||||
|
||||
override fun onReceive(
|
||||
context: Context,
|
||||
intent: Intent?
|
||||
) = goAsync {
|
||||
override fun onReceive(context: Context, intent: Intent?) = goAsync {
|
||||
try {
|
||||
val settings = settingsRepo.getAll()
|
||||
if (settings.isNotEmpty()) {
|
||||
val setting = settings.first()
|
||||
if (setting.defaultTunnel != null) {
|
||||
ServiceManager.stopVpnService(context)
|
||||
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
||||
ServiceManager.startVpnService(context, setting.defaultTunnel.toString())
|
||||
}
|
||||
val settings = settingsRepository.getSettings()
|
||||
if (settings.defaultTunnel != null) {
|
||||
ServiceManager.stopVpnService(context)
|
||||
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
||||
ServiceManager.startVpnServiceForeground(context, settings.defaultTunnel.toString())
|
||||
}
|
||||
} finally {
|
||||
cancel()
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.repository
|
||||
|
||||
import androidx.room.AutoMigration
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
||||
|
||||
@Database(
|
||||
entities = [Settings::class, TunnelConfig::class],
|
||||
version = 4,
|
||||
autoMigrations = [
|
||||
AutoMigration(from = 1, to = 2), AutoMigration(from = 2, to = 3), AutoMigration(
|
||||
from = 3,
|
||||
to = 4
|
||||
)
|
||||
],
|
||||
exportSchema = true
|
||||
)
|
||||
@TypeConverters(DatabaseListConverters::class)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun settingDao(): SettingsDoa
|
||||
|
||||
abstract fun tunnelConfigDoa(): TunnelConfigDao
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.repository
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface SettingsDoa {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun save(t: Settings)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun saveAll(t: List<Settings>)
|
||||
|
||||
@Query("SELECT * FROM settings WHERE id=:id")
|
||||
suspend fun getById(id: Long): Settings?
|
||||
|
||||
@Query("SELECT * FROM settings")
|
||||
suspend fun getAll(): List<Settings>
|
||||
|
||||
@Query("SELECT * FROM settings")
|
||||
fun getAllFlow(): Flow<MutableList<Settings>>
|
||||
|
||||
@Delete
|
||||
suspend fun delete(t: Settings)
|
||||
|
||||
@Query("SELECT COUNT('id') FROM settings")
|
||||
suspend fun count(): Long
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.repository
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface TunnelConfigDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun save(t: TunnelConfig)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun saveAll(t: List<TunnelConfig>)
|
||||
|
||||
@Query("SELECT * FROM TunnelConfig WHERE id=:id")
|
||||
suspend fun getById(id: Long): TunnelConfig?
|
||||
|
||||
@Query("SELECT * FROM TunnelConfig")
|
||||
suspend fun getAll(): List<TunnelConfig>
|
||||
|
||||
@Delete
|
||||
suspend fun delete(t: TunnelConfig)
|
||||
|
||||
@Query("SELECT COUNT('id') FROM TunnelConfig")
|
||||
suspend fun count(): Long
|
||||
|
||||
@Query("SELECT * FROM tunnelconfig")
|
||||
fun getAllFlow(): Flow<MutableList<TunnelConfig>>
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.repository.model
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity
|
||||
data class Settings(
|
||||
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
||||
@ColumnInfo(name = "is_tunnel_enabled") var isAutoTunnelEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "is_tunnel_on_mobile_data_enabled") var isTunnelOnMobileDataEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "trusted_network_ssids") var trustedNetworkSSIDs: MutableList<String> = mutableListOf(),
|
||||
@ColumnInfo(name = "default_tunnel") var defaultTunnel: String? = null,
|
||||
@ColumnInfo(name = "is_always_on_vpn_enabled") var isAlwaysOnVpnEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "is_tunnel_on_ethernet_enabled") var isTunnelOnEthernetEnabled: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "is_shortcuts_enabled",
|
||||
defaultValue = "false"
|
||||
) var isShortcutsEnabled: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "is_battery_saver_enabled",
|
||||
defaultValue = "false"
|
||||
) var isBatterySaverEnabled: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "is_tunnel_on_wifi_enabled",
|
||||
defaultValue = "false"
|
||||
) var isTunnelOnWifiEnabled: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "is_kernel_enabled",
|
||||
defaultValue = "false"
|
||||
) var isKernelEnabled: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "is_restore_on_boot_enabled",
|
||||
defaultValue = "false"
|
||||
) var isRestoreOnBootEnabled: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "is_multi_tunnel_enabled",
|
||||
defaultValue = "false"
|
||||
) var isMultiTunnelEnabled: Boolean = false
|
||||
) {
|
||||
fun isTunnelConfigDefault(tunnelConfig: TunnelConfig): Boolean {
|
||||
return if (defaultTunnel != null) {
|
||||
val defaultConfig = TunnelConfig.from(defaultTunnel!!)
|
||||
(tunnelConfig.id == defaultConfig.id)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-8
@@ -15,18 +15,14 @@ open class ForegroundService : LifecycleService() {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun onStartCommand(
|
||||
intent: Intent?,
|
||||
flags: Int,
|
||||
startId: Int
|
||||
): Int {
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
super.onStartCommand(intent, flags, startId)
|
||||
Timber.d("onStartCommand executed with startId: $startId")
|
||||
if (intent != null) {
|
||||
val action = intent.action
|
||||
Timber.d("using an intent with action $action")
|
||||
when (action) {
|
||||
Action.START.name, Action.START_FOREGROUND.name -> startService(intent.extras)
|
||||
Action.START.name,
|
||||
Action.START_FOREGROUND.name -> startService(intent.extras)
|
||||
Action.STOP.name -> stopService(intent.extras)
|
||||
"android.net.VpnService" -> {
|
||||
Timber.d("Always-on VPN starting service")
|
||||
@@ -36,7 +32,7 @@ open class ForegroundService : LifecycleService() {
|
||||
}
|
||||
} else {
|
||||
Timber.d(
|
||||
"with a null intent. It has been probably restarted by the system."
|
||||
"with a null intent. It has been probably restarted by the system.",
|
||||
)
|
||||
}
|
||||
// by returning this we make sure the service is restarted if the system kills the service
|
||||
|
||||
+42
-72
@@ -1,28 +1,29 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.foreground
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Context.ACTIVITY_SERVICE
|
||||
import android.content.Intent
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import timber.log.Timber
|
||||
|
||||
object ServiceManager {
|
||||
@Suppress("DEPRECATION")
|
||||
private // Deprecated for third party Services.
|
||||
fun <T> Context.isServiceRunning(service: Class<T>) =
|
||||
(getSystemService(ACTIVITY_SERVICE) as ActivityManager)
|
||||
.getRunningServices(Integer.MAX_VALUE)
|
||||
.any { it.service.className == service.name }
|
||||
|
||||
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> Context.isServiceRunning(service: Class<T>) =
|
||||
// (getSystemService(ACTIVITY_SERVICE) as ActivityManager)
|
||||
// .runningAppProcesses.any {
|
||||
// it.processName == service.name
|
||||
// }
|
||||
//
|
||||
// fun <T : Service> getServiceState(
|
||||
// context: Context,
|
||||
// cls: Class<T>
|
||||
// ): ServiceState {
|
||||
// val isServiceRunning = context.isServiceRunning(cls)
|
||||
// return if (isServiceRunning) ServiceState.STARTED else ServiceState.STOPPED
|
||||
// }
|
||||
|
||||
private fun <T : Service> actionOnService(
|
||||
action: Action,
|
||||
@@ -30,14 +31,10 @@ object ServiceManager {
|
||||
cls: Class<T>,
|
||||
extras: Map<String, String>? = null
|
||||
) {
|
||||
if (getServiceState(context, cls) == ServiceState.STOPPED && action == Action.STOP) return
|
||||
if (getServiceState(context, cls) == ServiceState.STARTED && action == Action.START) return
|
||||
val intent =
|
||||
Intent(context, cls).also {
|
||||
it.action = action.name
|
||||
extras?.forEach { (k, v) ->
|
||||
it.putExtra(k, v)
|
||||
}
|
||||
extras?.forEach { (k, v) -> it.putExtra(k, v) }
|
||||
}
|
||||
intent.component?.javaClass
|
||||
try {
|
||||
@@ -45,11 +42,9 @@ object ServiceManager {
|
||||
Action.START_FOREGROUND -> {
|
||||
context.startForegroundService(intent)
|
||||
}
|
||||
|
||||
Action.START -> {
|
||||
context.startService(intent)
|
||||
}
|
||||
|
||||
Action.STOP -> context.startService(intent)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
@@ -57,67 +52,57 @@ object ServiceManager {
|
||||
}
|
||||
}
|
||||
|
||||
fun startVpnService(
|
||||
context: Context,
|
||||
tunnelConfig: String
|
||||
) {
|
||||
fun startVpnService(context: Context, tunnelConfig: String) {
|
||||
actionOnService(
|
||||
Action.START,
|
||||
context,
|
||||
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) {
|
||||
Timber.d("Stopping vpn service action")
|
||||
actionOnService(
|
||||
Action.STOP,
|
||||
context,
|
||||
WireGuardTunnelService::class.java
|
||||
WireGuardTunnelService::class.java,
|
||||
)
|
||||
}
|
||||
|
||||
fun startVpnServiceForeground(
|
||||
context: Context,
|
||||
tunnelConfig: String
|
||||
) {
|
||||
fun startVpnServiceForeground(context: Context, tunnelConfig: String) {
|
||||
actionOnService(
|
||||
Action.START_FOREGROUND,
|
||||
context,
|
||||
WireGuardTunnelService::class.java,
|
||||
mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig)
|
||||
mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig),
|
||||
)
|
||||
}
|
||||
|
||||
private fun startWatcherServiceForeground(
|
||||
fun startVpnServicePrimaryTunnel(context: Context, settings: Settings, fallbackTunnel: TunnelConfig? = null) {
|
||||
if(settings.defaultTunnel != null) {
|
||||
return startVpnServiceForeground(context, settings.defaultTunnel!!)
|
||||
}
|
||||
if(fallbackTunnel != null) {
|
||||
startVpnServiceForeground(context, fallbackTunnel.toString())
|
||||
}
|
||||
}
|
||||
|
||||
fun startWatcherServiceForeground(
|
||||
context: Context,
|
||||
tunnelConfig: String
|
||||
) {
|
||||
actionOnService(
|
||||
Action.START_FOREGROUND,
|
||||
context,
|
||||
WireGuardConnectivityWatcherService::class.java,
|
||||
)
|
||||
}
|
||||
|
||||
fun startWatcherService(context: Context) {
|
||||
actionOnService(
|
||||
Action.START,
|
||||
context,
|
||||
WireGuardConnectivityWatcherService::class.java,
|
||||
mapOf(
|
||||
context
|
||||
.getString(R.string.tunnel_extras_key) to
|
||||
tunnelConfig
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun startWatcherService(
|
||||
context: Context,
|
||||
tunnelConfig: String
|
||||
) {
|
||||
actionOnService(
|
||||
Action.START,
|
||||
context,
|
||||
WireGuardConnectivityWatcherService::class.java,
|
||||
mapOf(
|
||||
context
|
||||
.getString(R.string.tunnel_extras_key) to
|
||||
tunnelConfig
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -125,22 +110,7 @@ object ServiceManager {
|
||||
actionOnService(
|
||||
Action.STOP,
|
||||
context,
|
||||
WireGuardConnectivityWatcherService::class.java
|
||||
WireGuardConnectivityWatcherService::class.java,
|
||||
)
|
||||
}
|
||||
|
||||
fun toggleWatcherServiceForeground(
|
||||
context: Context,
|
||||
tunnelConfig: String
|
||||
) {
|
||||
when (
|
||||
getServiceState(
|
||||
context,
|
||||
WireGuardConnectivityWatcherService::class.java
|
||||
)
|
||||
) {
|
||||
ServiceState.STARTED -> stopWatcherService(context)
|
||||
ServiceState.STOPPED -> startWatcherServiceForeground(context, tunnelConfig)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+196
-133
@@ -10,10 +10,9 @@ import android.os.SystemClock
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.zaneschepke.wireguardautotunnel.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
|
||||
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
|
||||
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
|
||||
@@ -21,45 +20,46 @@ import com.zaneschepke.wireguardautotunnel.service.network.NetworkStatus
|
||||
import com.zaneschepke.wireguardautotunnel.service.network.WifiService
|
||||
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
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 settingsRepo: SettingsDoa
|
||||
@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 var isWifiConnected = false
|
||||
private var isEthernetConnected = false
|
||||
private var isMobileDataConnected = false
|
||||
private var currentNetworkSSID = ""
|
||||
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
|
||||
private lateinit var setting: Settings
|
||||
private lateinit var tunnelConfig: String
|
||||
|
||||
private var wakeLock: PowerManager.WakeLock? = null
|
||||
private val tag = this.javaClass.name
|
||||
@@ -68,7 +68,9 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
super.onCreate()
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
launchWatcherNotification()
|
||||
if (settingsRepository.getSettings().isAutoTunnelPaused) {
|
||||
launchWatcherPausedNotification()
|
||||
} else launchWatcherNotification()
|
||||
} catch (e: Exception) {
|
||||
Timber.e("Failed to start watcher service, not enough permissions")
|
||||
}
|
||||
@@ -78,21 +80,10 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
override fun startService(extras: Bundle?) {
|
||||
super.startService(extras)
|
||||
try {
|
||||
launchWatcherNotification()
|
||||
val tunnelId = extras?.getString(getString(R.string.tunnel_extras_key))
|
||||
if (tunnelId != null) {
|
||||
this.tunnelConfig = tunnelId
|
||||
}
|
||||
// we need this lock so our service gets not affected by Doze Mode
|
||||
lifecycleScope.launch {
|
||||
initWakeLock()
|
||||
}
|
||||
lifecycleScope.launch { initWakeLock() }
|
||||
cancelWatcherJob()
|
||||
if (this::tunnelConfig.isInitialized) {
|
||||
startWatcherJob()
|
||||
} else {
|
||||
stopService(extras)
|
||||
}
|
||||
startWatcherJob()
|
||||
} catch (e: Exception) {
|
||||
Timber.e("Failed to launch watcher service, no permissions")
|
||||
}
|
||||
@@ -109,22 +100,29 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
private fun launchWatcherNotification() {
|
||||
private fun launchWatcherNotification(
|
||||
description: String = getString(R.string.watcher_notification_text_active)
|
||||
) {
|
||||
val notification =
|
||||
notificationService.createNotification(
|
||||
channelId = getString(R.string.watcher_channel_id),
|
||||
channelName = getString(R.string.watcher_channel_name),
|
||||
description = getString(R.string.watcher_notification_text),
|
||||
vibration = false
|
||||
title = getString(R.string.auto_tunnel_title),
|
||||
description = description,
|
||||
)
|
||||
ServiceCompat.startForeground(
|
||||
this,
|
||||
foregroundId,
|
||||
notification,
|
||||
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID
|
||||
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
|
||||
)
|
||||
}
|
||||
|
||||
private fun launchWatcherPausedNotification() {
|
||||
launchWatcherNotification(getString(R.string.watcher_notification_text_paused))
|
||||
}
|
||||
|
||||
// TODO could this be restarting service in a bad state?
|
||||
// try to start task again if killed
|
||||
override fun onTaskRemoved(rootIntent: Intent) {
|
||||
Timber.d("Task Removed called")
|
||||
@@ -134,7 +132,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
this,
|
||||
1,
|
||||
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 =
|
||||
@@ -142,24 +140,28 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
alarmService.set(
|
||||
AlarmManager.ELAPSED_REALTIME,
|
||||
SystemClock.elapsedRealtime() + 1000,
|
||||
restartServicePendingIntent
|
||||
restartServicePendingIntent,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun initWakeLock() {
|
||||
val isBatterySaverOn =
|
||||
withContext(lifecycleScope.coroutineContext) {
|
||||
settingsRepo.getAll().firstOrNull()?.isBatterySaverEnabled ?: false
|
||||
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.WATCHER_SERVICE_WAKE_LOCK_TIMEOUT)
|
||||
} else {
|
||||
Timber.d("Initiating wakelock with zero timeout")
|
||||
acquire()
|
||||
try {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -174,24 +176,33 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
private fun startWatcherJob() {
|
||||
watcherJob =
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val settings = settingsRepo.getAll()
|
||||
if (settings.isNotEmpty()) {
|
||||
setting = settings[0]
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -202,37 +213,84 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
when (it) {
|
||||
is NetworkStatus.Available -> {
|
||||
Timber.d("Gained Mobile data connection")
|
||||
isMobileDataConnected = true
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isMobileDataConnected = true,
|
||||
)
|
||||
}
|
||||
|
||||
is NetworkStatus.CapabilitiesChanged -> {
|
||||
isMobileDataConnected = true
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isMobileDataConnected = true,
|
||||
)
|
||||
Timber.d("Mobile data capabilities changed")
|
||||
}
|
||||
|
||||
is NetworkStatus.Unavailable -> {
|
||||
isMobileDataConnected = false
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isMobileDataConnected = false,
|
||||
)
|
||||
Timber.d("Lost mobile data connection")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun watchForSettingsChanges() {
|
||||
settingsRepository.getSettingsFlow().collect {
|
||||
if (networkEventsFlow.value.settings.isAutoTunnelPaused != it.isAutoTunnelPaused) {
|
||||
when (it.isAutoTunnelPaused) {
|
||||
true -> launchWatcherPausedNotification()
|
||||
false -> launchWatcherNotification()
|
||||
}
|
||||
}
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
settings = it,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun watchForVpnConnectivityChanges() {
|
||||
vpnService.vpnState.collect {
|
||||
when (it.status) {
|
||||
Tunnel.State.DOWN ->
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isVpnConnected = false,
|
||||
)
|
||||
Tunnel.State.UP ->
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isVpnConnected = true,
|
||||
)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun watchForEthernetConnectivityChanges() {
|
||||
ethernetService.networkStatus.collect {
|
||||
when (it) {
|
||||
is NetworkStatus.Available -> {
|
||||
Timber.d("Gained Ethernet connection")
|
||||
isEthernetConnected = true
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isEthernetConnected = true,
|
||||
)
|
||||
}
|
||||
|
||||
is NetworkStatus.CapabilitiesChanged -> {
|
||||
Timber.d("Ethernet capabilities changed")
|
||||
isEthernetConnected = true
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isEthernetConnected = true,
|
||||
)
|
||||
}
|
||||
|
||||
is NetworkStatus.Unavailable -> {
|
||||
isEthernetConnected = false
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isEthernetConnected = false,
|
||||
)
|
||||
Timber.d("Lost Ethernet connection")
|
||||
}
|
||||
}
|
||||
@@ -244,92 +302,97 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
when (it) {
|
||||
is NetworkStatus.Available -> {
|
||||
Timber.d("Gained Wi-Fi connection")
|
||||
isWifiConnected = true
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isWifiConnected = true,
|
||||
)
|
||||
}
|
||||
|
||||
is NetworkStatus.CapabilitiesChanged -> {
|
||||
Timber.d("Wifi capabilities changed")
|
||||
isWifiConnected = true
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isWifiConnected = true,
|
||||
)
|
||||
val ssid = wifiService.getNetworkName(it.networkCapabilities) ?: ""
|
||||
Timber.d("Detected SSID: $ssid")
|
||||
currentNetworkSSID = ssid
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
currentNetworkSSID = ssid,
|
||||
)
|
||||
}
|
||||
|
||||
is NetworkStatus.Unavailable -> {
|
||||
isWifiConnected = false
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isWifiConnected = false,
|
||||
)
|
||||
Timber.d("Lost Wi-Fi connection")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO clean this up
|
||||
private suspend fun manageVpn() {
|
||||
while (true) {
|
||||
when {
|
||||
(
|
||||
(
|
||||
isEthernetConnected &&
|
||||
setting.isTunnelOnEthernetEnabled &&
|
||||
vpnService.getState() == Tunnel.State.DOWN
|
||||
)
|
||||
) ->
|
||||
ServiceManager.startVpnService(this, tunnelConfig)
|
||||
|
||||
(
|
||||
!isEthernetConnected &&
|
||||
setting.isTunnelOnMobileDataEnabled &&
|
||||
!isWifiConnected &&
|
||||
isMobileDataConnected &&
|
||||
vpnService.getState() == Tunnel.State.DOWN
|
||||
) ->
|
||||
ServiceManager.startVpnService(this, tunnelConfig)
|
||||
|
||||
(
|
||||
!isEthernetConnected &&
|
||||
!setting.isTunnelOnMobileDataEnabled &&
|
||||
!isWifiConnected &&
|
||||
vpnService.getState() == Tunnel.State.UP
|
||||
) ->
|
||||
ServiceManager.stopVpnService(this)
|
||||
|
||||
(
|
||||
!isEthernetConnected && isWifiConnected &&
|
||||
!setting.trustedNetworkSSIDs.contains(currentNetworkSSID) &&
|
||||
setting.isTunnelOnWifiEnabled &&
|
||||
(vpnService.getState() != Tunnel.State.UP)
|
||||
) ->
|
||||
ServiceManager.startVpnService(this, tunnelConfig)
|
||||
|
||||
(
|
||||
!isEthernetConnected && (
|
||||
isWifiConnected &&
|
||||
setting.trustedNetworkSSIDs.contains(currentNetworkSSID)
|
||||
) &&
|
||||
(vpnService.getState() == Tunnel.State.UP)
|
||||
) ->
|
||||
ServiceManager.stopVpnService(this)
|
||||
|
||||
(
|
||||
!isEthernetConnected && (
|
||||
isWifiConnected &&
|
||||
!setting.isTunnelOnWifiEnabled &&
|
||||
(vpnService.getState() == Tunnel.State.UP)
|
||||
)
|
||||
) ->
|
||||
ServiceManager.stopVpnService(this)
|
||||
|
||||
(
|
||||
!isEthernetConnected && !isWifiConnected &&
|
||||
!isMobileDataConnected &&
|
||||
(vpnService.getState() == Tunnel.State.UP)
|
||||
) ->
|
||||
ServiceManager.stopVpnService(this)
|
||||
|
||||
else -> {
|
||||
// Do nothing
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
delay(Constants.VPN_CONNECTIVITY_CHECK_INTERVAL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+78
-85
@@ -5,56 +5,62 @@ import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.zaneschepke.wireguardautotunnel.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
||||
import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver
|
||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.handshakeStatus
|
||||
import com.zaneschepke.wireguardautotunnel.util.mapPeerStats
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class WireGuardTunnelService : ForegroundService() {
|
||||
private val foregroundId = 123
|
||||
|
||||
@Inject
|
||||
lateinit var vpnService: VpnService
|
||||
@Inject lateinit var vpnService: VpnService
|
||||
|
||||
@Inject
|
||||
lateinit var settingsRepo: SettingsDoa
|
||||
@Inject lateinit var settingsRepository: SettingsRepository
|
||||
|
||||
@Inject
|
||||
lateinit var notificationService: NotificationService
|
||||
@Inject lateinit var tunnelConfigRepository: TunnelConfigRepository
|
||||
|
||||
@Inject lateinit var notificationService: NotificationService
|
||||
|
||||
private lateinit var job: Job
|
||||
|
||||
private var tunnelName: String = ""
|
||||
private var didShowConnected = false
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
launchVpnStartingNotification()
|
||||
if (tunnelConfigRepository.getAll().isNotEmpty()) {
|
||||
launchVpnNotification()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun startService(extras: Bundle?) {
|
||||
super.startService(extras)
|
||||
launchVpnStartingNotification()
|
||||
val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key))
|
||||
cancelJob()
|
||||
val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key))
|
||||
val tunnelConfig = tunnelConfigString?.let { TunnelConfig.from(it) }
|
||||
tunnelName = tunnelConfig?.name ?: ""
|
||||
job =
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
launch {
|
||||
if (tunnelConfigString != null) {
|
||||
if (tunnelConfig != null) {
|
||||
try {
|
||||
val tunnelConfig = TunnelConfig.from(tunnelConfigString)
|
||||
tunnelName = tunnelConfig.name
|
||||
vpnService.startTunnel(tunnelConfig)
|
||||
} catch (e: Exception) {
|
||||
@@ -62,52 +68,49 @@ class WireGuardTunnelService : ForegroundService() {
|
||||
stopService(extras)
|
||||
}
|
||||
} else {
|
||||
Timber.d("Tunnel config null, starting default tunnel")
|
||||
val settings = settingsRepo.getAll()
|
||||
if (settings.isNotEmpty()) {
|
||||
val setting = settings[0]
|
||||
if (setting.defaultTunnel != null && setting.isAlwaysOnVpnEnabled) {
|
||||
val tunnelConfig = TunnelConfig.from(setting.defaultTunnel!!)
|
||||
tunnelName = tunnelConfig.name
|
||||
vpnService.startTunnel(tunnelConfig)
|
||||
Timber.d("Tunnel config null, starting default tunnel or first")
|
||||
val settings = settingsRepository.getSettings()
|
||||
val tunnels = tunnelConfigRepository.getAll()
|
||||
if (settings.isAlwaysOnVpnEnabled) {
|
||||
val tunnel =
|
||||
if (settings.defaultTunnel != null) {
|
||||
TunnelConfig.from(settings.defaultTunnel!!)
|
||||
} else if (tunnels.isNotEmpty()) {
|
||||
tunnels.first()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
if (tunnel != null) {
|
||||
tunnelName = tunnel.name
|
||||
vpnService.startTunnel(tunnel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO add failed to connect notification
|
||||
launch {
|
||||
var didShowConnected = false
|
||||
var didShowFailedHandshakeNotification = false
|
||||
vpnService.handshakeStatus.collect {
|
||||
when (it) {
|
||||
HandshakeStatus.NOT_STARTED -> {
|
||||
}
|
||||
HandshakeStatus.NEVER_CONNECTED -> {
|
||||
if (!didShowFailedHandshakeNotification) {
|
||||
launchVpnConnectionFailedNotification(
|
||||
getString(R.string.initial_connection_failure_message)
|
||||
)
|
||||
didShowFailedHandshakeNotification = true
|
||||
didShowConnected = false
|
||||
vpnService.vpnState.collect { state ->
|
||||
state.statistics
|
||||
?.mapPeerStats()
|
||||
?.map { it.value?.handshakeStatus() }
|
||||
.let { statuses ->
|
||||
when {
|
||||
statuses?.all { it == HandshakeStatus.HEALTHY } == true -> {
|
||||
if (!didShowConnected) {
|
||||
delay(Constants.VPN_CONNECTED_NOTIFICATION_DELAY)
|
||||
launchVpnNotification(
|
||||
getString(R.string.tunnel_start_title),
|
||||
"${getString(R.string.tunnel_start_text)} $tunnelName",
|
||||
)
|
||||
didShowConnected = true
|
||||
}
|
||||
}
|
||||
statuses?.any { it == HandshakeStatus.STALE } == true -> {}
|
||||
statuses?.all { it == HandshakeStatus.NOT_STARTED } ==
|
||||
true -> {}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
HandshakeStatus.HEALTHY -> {
|
||||
if (!didShowConnected) {
|
||||
launchVpnConnectedNotification()
|
||||
didShowConnected = true
|
||||
}
|
||||
}
|
||||
HandshakeStatus.STALE -> {}
|
||||
HandshakeStatus.UNHEALTHY -> {
|
||||
if (!didShowFailedHandshakeNotification) {
|
||||
launchVpnConnectionFailedNotification(
|
||||
getString(R.string.lost_connection_failure_message)
|
||||
)
|
||||
didShowFailedHandshakeNotification = true
|
||||
didShowConnected = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -117,46 +120,31 @@ class WireGuardTunnelService : ForegroundService() {
|
||||
super.stopService(extras)
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
vpnService.stopTunnel()
|
||||
didShowConnected = false
|
||||
}
|
||||
cancelJob()
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
private fun launchVpnConnectedNotification() {
|
||||
private fun launchVpnNotification(
|
||||
title: String = getString(R.string.vpn_starting),
|
||||
description: String = getString(R.string.attempt_connection)
|
||||
) {
|
||||
val notification =
|
||||
notificationService.createNotification(
|
||||
channelId = getString(R.string.vpn_channel_id),
|
||||
channelName = getString(R.string.vpn_channel_name),
|
||||
title = getString(R.string.tunnel_start_title),
|
||||
title = title,
|
||||
onGoing = false,
|
||||
vibration = false,
|
||||
showTimestamp = true,
|
||||
description = "${getString(R.string.tunnel_start_text)} $tunnelName"
|
||||
description = description,
|
||||
)
|
||||
ServiceCompat.startForeground(
|
||||
this,
|
||||
foregroundId,
|
||||
notification,
|
||||
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID
|
||||
)
|
||||
}
|
||||
|
||||
private fun launchVpnStartingNotification() {
|
||||
val notification =
|
||||
notificationService.createNotification(
|
||||
channelId = getString(R.string.vpn_channel_id),
|
||||
channelName = getString(R.string.vpn_channel_name),
|
||||
title = getString(R.string.vpn_starting),
|
||||
onGoing = false,
|
||||
vibration = false,
|
||||
showTimestamp = true,
|
||||
description = getString(R.string.attempt_connection)
|
||||
)
|
||||
ServiceCompat.startForeground(
|
||||
this,
|
||||
foregroundId,
|
||||
notification,
|
||||
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID
|
||||
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -166,20 +154,25 @@ class WireGuardTunnelService : ForegroundService() {
|
||||
channelId = getString(R.string.vpn_channel_id),
|
||||
channelName = getString(R.string.vpn_channel_name),
|
||||
action =
|
||||
PendingIntent.getBroadcast(
|
||||
this,
|
||||
0,
|
||||
Intent(this, NotificationActionReceiver::class.java),
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
),
|
||||
PendingIntent.getBroadcast(
|
||||
this,
|
||||
0,
|
||||
Intent(this, NotificationActionReceiver::class.java),
|
||||
PendingIntent.FLAG_IMMUTABLE,
|
||||
),
|
||||
actionText = getString(R.string.restart),
|
||||
title = getString(R.string.vpn_connection_failed),
|
||||
onGoing = false,
|
||||
vibration = true,
|
||||
showTimestamp = true,
|
||||
description = message
|
||||
description = message,
|
||||
)
|
||||
super.startForeground(foregroundId, notification)
|
||||
ServiceCompat.startForeground(
|
||||
this,
|
||||
foregroundId,
|
||||
notification,
|
||||
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
|
||||
)
|
||||
}
|
||||
|
||||
private fun cancelJob() {
|
||||
|
||||
+64
-69
@@ -24,72 +24,69 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(
|
||||
private val wifiManager =
|
||||
context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
|
||||
|
||||
override val networkStatus =
|
||||
callbackFlow {
|
||||
val networkStatusCallback =
|
||||
when (Build.VERSION.SDK_INT) {
|
||||
in Build.VERSION_CODES.S..Int.MAX_VALUE -> {
|
||||
object : ConnectivityManager.NetworkCallback(
|
||||
FLAG_INCLUDE_LOCATION_INFO
|
||||
override val networkStatus = callbackFlow {
|
||||
val networkStatusCallback =
|
||||
when (Build.VERSION.SDK_INT) {
|
||||
in Build.VERSION_CODES.S..Int.MAX_VALUE -> {
|
||||
object :
|
||||
ConnectivityManager.NetworkCallback(
|
||||
FLAG_INCLUDE_LOCATION_INFO,
|
||||
) {
|
||||
override fun onAvailable(network: Network) {
|
||||
trySend(NetworkStatus.Available(network))
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
trySend(NetworkStatus.Unavailable(network))
|
||||
}
|
||||
|
||||
override fun onCapabilitiesChanged(
|
||||
network: Network,
|
||||
networkCapabilities: NetworkCapabilities
|
||||
) {
|
||||
trySend(
|
||||
NetworkStatus.CapabilitiesChanged(
|
||||
network,
|
||||
networkCapabilities
|
||||
)
|
||||
)
|
||||
}
|
||||
override fun onAvailable(network: Network) {
|
||||
trySend(NetworkStatus.Available(network))
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
trySend(NetworkStatus.Available(network))
|
||||
}
|
||||
override fun onLost(network: Network) {
|
||||
trySend(NetworkStatus.Unavailable(network))
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
trySend(NetworkStatus.Unavailable(network))
|
||||
}
|
||||
|
||||
override fun onCapabilitiesChanged(
|
||||
network: Network,
|
||||
networkCapabilities: NetworkCapabilities
|
||||
) {
|
||||
trySend(
|
||||
NetworkStatus.CapabilitiesChanged(
|
||||
network,
|
||||
networkCapabilities
|
||||
)
|
||||
)
|
||||
}
|
||||
override fun onCapabilitiesChanged(
|
||||
network: Network,
|
||||
networkCapabilities: NetworkCapabilities
|
||||
) {
|
||||
trySend(
|
||||
NetworkStatus.CapabilitiesChanged(
|
||||
network,
|
||||
networkCapabilities,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
val request =
|
||||
NetworkRequest.Builder()
|
||||
.addTransportType(networkCapability)
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||
.build()
|
||||
connectivityManager.registerNetworkCallback(request, networkStatusCallback)
|
||||
else -> {
|
||||
object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
trySend(NetworkStatus.Available(network))
|
||||
}
|
||||
|
||||
awaitClose {
|
||||
connectivityManager.unregisterNetworkCallback(networkStatusCallback)
|
||||
override fun onLost(network: Network) {
|
||||
trySend(NetworkStatus.Unavailable(network))
|
||||
}
|
||||
|
||||
override fun onCapabilitiesChanged(
|
||||
network: Network,
|
||||
networkCapabilities: NetworkCapabilities
|
||||
) {
|
||||
trySend(
|
||||
NetworkStatus.CapabilitiesChanged(
|
||||
network,
|
||||
networkCapabilities,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val request =
|
||||
NetworkRequest.Builder()
|
||||
.addTransportType(networkCapability)
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||
.build()
|
||||
connectivityManager.registerNetworkCallback(request, networkStatusCallback)
|
||||
|
||||
awaitClose { connectivityManager.unregisterNetworkCallback(networkStatusCallback) }
|
||||
}
|
||||
|
||||
override fun getNetworkName(networkCapabilities: NetworkCapabilities): String? {
|
||||
var ssid: String? = getWifiNameFromCapabilities(networkCapabilities)
|
||||
@@ -119,18 +116,16 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(
|
||||
inline fun <Result> Flow<NetworkStatus>.map(
|
||||
crossinline onUnavailable: suspend (network: Network) -> Result,
|
||||
crossinline onAvailable: suspend (network: Network) -> Result,
|
||||
crossinline onCapabilitiesChanged: suspend (
|
||||
network: Network,
|
||||
networkCapabilities: NetworkCapabilities
|
||||
) -> Result
|
||||
): Flow<Result> =
|
||||
map { status ->
|
||||
when (status) {
|
||||
is NetworkStatus.Unavailable -> onUnavailable(status.network)
|
||||
is NetworkStatus.Available -> onAvailable(status.network)
|
||||
is NetworkStatus.CapabilitiesChanged -> onCapabilitiesChanged(
|
||||
crossinline onCapabilitiesChanged:
|
||||
suspend (network: Network, networkCapabilities: NetworkCapabilities) -> Result
|
||||
): Flow<Result> = map { status ->
|
||||
when (status) {
|
||||
is NetworkStatus.Unavailable -> onUnavailable(status.network)
|
||||
is NetworkStatus.Available -> onAvailable(status.network)
|
||||
is NetworkStatus.CapabilitiesChanged ->
|
||||
onCapabilitiesChanged(
|
||||
status.network,
|
||||
status.networkCapabilities
|
||||
status.networkCapabilities,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-5
@@ -5,9 +5,5 @@ import android.net.NetworkCapabilities
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class EthernetService
|
||||
@Inject
|
||||
constructor(
|
||||
@ApplicationContext context: Context
|
||||
) :
|
||||
class EthernetService @Inject constructor(@ApplicationContext context: Context) :
|
||||
BaseNetworkService<EthernetService>(context, NetworkCapabilities.TRANSPORT_ETHERNET)
|
||||
|
||||
+1
-5
@@ -5,9 +5,5 @@ import android.net.NetworkCapabilities
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class MobileDataService
|
||||
@Inject
|
||||
constructor(
|
||||
@ApplicationContext context: Context
|
||||
) :
|
||||
class MobileDataService @Inject constructor(@ApplicationContext context: Context) :
|
||||
BaseNetworkService<MobileDataService>(context, NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||
|
||||
+1
-5
@@ -5,9 +5,5 @@ import android.net.NetworkCapabilities
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class WifiService
|
||||
@Inject
|
||||
constructor(
|
||||
@ApplicationContext context: Context
|
||||
) :
|
||||
class WifiService @Inject constructor(@ApplicationContext context: Context) :
|
||||
BaseNetworkService<WifiService>(context, NetworkCapabilities.TRANSPORT_WIFI)
|
||||
|
||||
+2
-1
@@ -16,6 +16,7 @@ interface NotificationService {
|
||||
importance: Int = NotificationManager.IMPORTANCE_HIGH,
|
||||
vibration: Boolean = false,
|
||||
onGoing: Boolean = true,
|
||||
lights: Boolean = true
|
||||
lights: Boolean = true,
|
||||
onlyAlertOnce: Boolean = true,
|
||||
): Notification
|
||||
}
|
||||
|
||||
+45
-27
@@ -7,19 +7,28 @@ import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.MainActivity
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class WireGuardNotification
|
||||
@Inject
|
||||
constructor(
|
||||
@ApplicationContext private val context: Context
|
||||
) : NotificationService {
|
||||
class WireGuardNotification @Inject constructor(@ApplicationContext private val context: Context) :
|
||||
NotificationService {
|
||||
private val notificationManager =
|
||||
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
private val watcherBuilder: NotificationCompat.Builder =
|
||||
NotificationCompat.Builder(
|
||||
context,
|
||||
context.getString(R.string.watcher_channel_id),
|
||||
)
|
||||
private val tunnelBuilder: NotificationCompat.Builder =
|
||||
NotificationCompat.Builder(
|
||||
context,
|
||||
context.getString(R.string.vpn_channel_id),
|
||||
)
|
||||
|
||||
override fun createNotification(
|
||||
channelId: String,
|
||||
channelName: String,
|
||||
@@ -31,21 +40,23 @@ constructor(
|
||||
importance: Int,
|
||||
vibration: Boolean,
|
||||
onGoing: Boolean,
|
||||
lights: Boolean
|
||||
lights: Boolean,
|
||||
onlyAlertOnce: Boolean,
|
||||
): Notification {
|
||||
val channel =
|
||||
NotificationChannel(
|
||||
channelId,
|
||||
channelName,
|
||||
importance
|
||||
).let {
|
||||
it.description = title
|
||||
it.enableLights(lights)
|
||||
it.lightColor = Color.RED
|
||||
it.enableVibration(vibration)
|
||||
it.vibrationPattern = longArrayOf(100, 200, 300, 400, 500, 400, 300, 200, 400)
|
||||
it
|
||||
}
|
||||
channelId,
|
||||
channelName,
|
||||
importance,
|
||||
)
|
||||
.let {
|
||||
it.description = title
|
||||
it.enableLights(lights)
|
||||
it.lightColor = Color.RED
|
||||
it.enableVibration(vibration)
|
||||
it.vibrationPattern = longArrayOf(100, 200, 300)
|
||||
it
|
||||
}
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
val pendingIntent: PendingIntent =
|
||||
Intent(context, MainActivity::class.java).let { notificationIntent ->
|
||||
@@ -53,30 +64,37 @@ constructor(
|
||||
context,
|
||||
0,
|
||||
notificationIntent,
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
}
|
||||
|
||||
val builder: Notification.Builder =
|
||||
Notification.Builder(
|
||||
context,
|
||||
channelId
|
||||
)
|
||||
val builder =
|
||||
when (channelId) {
|
||||
context.getString(R.string.watcher_channel_id) -> watcherBuilder
|
||||
context.getString(R.string.vpn_channel_id) -> tunnelBuilder
|
||||
else -> {
|
||||
NotificationCompat.Builder(
|
||||
context,
|
||||
channelId,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return builder.let {
|
||||
if (action != null && actionText != null) {
|
||||
// TODO find a not deprecated way to do this
|
||||
it.addAction(
|
||||
Notification.Action.Builder(0, actionText, action)
|
||||
.build()
|
||||
NotificationCompat.Action.Builder(0, actionText, action).build(),
|
||||
)
|
||||
it.setAutoCancel(true)
|
||||
}
|
||||
it.setContentTitle(title)
|
||||
.setContentText(description)
|
||||
.setOnlyAlertOnce(onlyAlertOnce)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setOngoing(onGoing)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setShowWhen(showTimestamp)
|
||||
.setSmallIcon(R.mipmap.ic_launcher_foreground)
|
||||
.setSmallIcon(R.drawable.ic_launcher)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
+36
-37
@@ -1,72 +1,80 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.shortcut
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
|
||||
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
|
||||
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ShortcutsActivity : ComponentActivity() {
|
||||
@Inject
|
||||
lateinit var settingsRepo: SettingsDoa
|
||||
@Inject lateinit var settingsRepository: SettingsRepository
|
||||
|
||||
@Inject
|
||||
lateinit var tunnelConfigRepo: TunnelConfigDao
|
||||
@Inject lateinit var tunnelConfigRepository: TunnelConfigRepository
|
||||
|
||||
private fun attemptWatcherServiceToggle(tunnelConfig: String) {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
val settings = getSettings()
|
||||
if (settings.isAutoTunnelEnabled) {
|
||||
ServiceManager.toggleWatcherServiceForeground(this@ShortcutsActivity, tunnelConfig)
|
||||
}
|
||||
private suspend fun toggleWatcherServicePause() {
|
||||
val settings = settingsRepository.getSettings()
|
||||
if (settings.isAutoTunnelEnabled) {
|
||||
val pauseAutoTunnel = !settings.isAutoTunnelPaused
|
||||
settingsRepository.save(
|
||||
settings.copy(
|
||||
isAutoTunnelPaused = pauseAutoTunnel,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (intent.getStringExtra(CLASS_NAME_EXTRA_KEY)
|
||||
setContentView(View(this))
|
||||
if (
|
||||
intent
|
||||
.getStringExtra(CLASS_NAME_EXTRA_KEY)
|
||||
.equals(WireGuardTunnelService::class.java.simpleName)
|
||||
) {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
val settings = getSettings()
|
||||
val settings = settingsRepository.getSettings()
|
||||
if (settings.isShortcutsEnabled) {
|
||||
try {
|
||||
val tunnelName = intent.getStringExtra(TUNNEL_NAME_EXTRA_KEY)
|
||||
val tunnelConfig =
|
||||
if (tunnelName != null) {
|
||||
tunnelConfigRepo.getAll().firstOrNull { it.name == tunnelName }
|
||||
tunnelConfigRepository.getAll().firstOrNull {
|
||||
it.name == tunnelName
|
||||
}
|
||||
} else {
|
||||
if (settings.defaultTunnel == null) {
|
||||
tunnelConfigRepo.getAll().first()
|
||||
tunnelConfigRepository.getAll().first()
|
||||
} else {
|
||||
TunnelConfig.from(settings.defaultTunnel!!)
|
||||
}
|
||||
}
|
||||
tunnelConfig ?: return@launch
|
||||
attemptWatcherServiceToggle(tunnelConfig.toString())
|
||||
toggleWatcherServicePause()
|
||||
when (intent.action) {
|
||||
Action.STOP.name -> ServiceManager.stopVpnService(
|
||||
this@ShortcutsActivity
|
||||
)
|
||||
Action.START.name -> ServiceManager.startVpnService(
|
||||
this@ShortcutsActivity,
|
||||
tunnelConfig.toString()
|
||||
)
|
||||
Action.STOP.name ->
|
||||
ServiceManager.stopVpnService(
|
||||
this@ShortcutsActivity,
|
||||
)
|
||||
Action.START.name ->
|
||||
ServiceManager.startVpnServiceForeground(
|
||||
this@ShortcutsActivity,
|
||||
tunnelConfig.toString(),
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e.message)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,15 +82,6 @@ class ShortcutsActivity : ComponentActivity() {
|
||||
finish()
|
||||
}
|
||||
|
||||
private suspend fun getSettings(): Settings {
|
||||
val settings = settingsRepo.getAll()
|
||||
return if (settings.isNotEmpty()) {
|
||||
settings.first()
|
||||
} else {
|
||||
throw WgTunnelException("Settings empty")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TUNNEL_NAME_EXTRA_KEY = "tunnelName"
|
||||
const val CLASS_NAME_EXTRA_KEY = "className"
|
||||
|
||||
+70
-96
@@ -4,47 +4,57 @@ import android.os.Build
|
||||
import android.service.quicksettings.Tile
|
||||
import android.service.quicksettings.TileService
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
|
||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class TunnelControlTile : TileService() {
|
||||
@Inject
|
||||
lateinit var settingsRepo: SettingsDoa
|
||||
class TunnelControlTile() : TileService() {
|
||||
|
||||
@Inject
|
||||
lateinit var configRepo: TunnelConfigDao
|
||||
@Inject lateinit var tunnelConfigRepository: TunnelConfigRepository
|
||||
|
||||
@Inject
|
||||
lateinit var vpnService: VpnService
|
||||
@Inject lateinit var settingsRepository: SettingsRepository
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.Main)
|
||||
@Inject lateinit var vpnService: VpnService
|
||||
|
||||
private lateinit var job: Job
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
private var tunnelName: String? = null
|
||||
|
||||
override fun onStartListening() {
|
||||
job =
|
||||
scope.launch {
|
||||
updateTileState()
|
||||
}
|
||||
super.onStartListening()
|
||||
}
|
||||
|
||||
override fun onTileRemoved() {
|
||||
super.onTileRemoved()
|
||||
cancelJob()
|
||||
Timber.d("On start listening called")
|
||||
scope.launch {
|
||||
vpnService.vpnState.collect {
|
||||
when (it.status) {
|
||||
Tunnel.State.UP -> setActive()
|
||||
Tunnel.State.DOWN -> setInactive()
|
||||
else -> setInactive()
|
||||
}
|
||||
val tunnels = tunnelConfigRepository.getAll()
|
||||
if (tunnels.isEmpty()) {
|
||||
setUnavailable()
|
||||
return@collect
|
||||
}
|
||||
tunnelName =
|
||||
it.name.ifBlank {
|
||||
val settings = settingsRepository.getSettings()
|
||||
if (settings.defaultTunnel != null) {
|
||||
TunnelConfig.from(settings.defaultTunnel!!).name
|
||||
} else tunnels.firstOrNull()?.name
|
||||
}
|
||||
setTileDescription(tunnelName ?: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
@@ -52,22 +62,26 @@ class TunnelControlTile : TileService() {
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
override fun onTileRemoved() {
|
||||
super.onTileRemoved()
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
override fun onClick() {
|
||||
super.onClick()
|
||||
unlockAndRun {
|
||||
scope.launch {
|
||||
try {
|
||||
val tunnel = determineTileTunnel()
|
||||
if (tunnel != null) {
|
||||
attemptWatcherServiceToggle(tunnel.toString())
|
||||
if (vpnService.getState() == Tunnel.State.UP) {
|
||||
ServiceManager.stopVpnService(this@TunnelControlTile)
|
||||
} else {
|
||||
ServiceManager.startVpnServiceForeground(
|
||||
this@TunnelControlTile,
|
||||
tunnel.toString()
|
||||
)
|
||||
}
|
||||
val tunnelConfig =
|
||||
tunnelConfigRepository.getAll().first { it.name == tunnelName }
|
||||
toggleWatcherServicePause()
|
||||
if (vpnService.getState() == Tunnel.State.UP) {
|
||||
ServiceManager.stopVpnService(this@TunnelControlTile)
|
||||
} else {
|
||||
ServiceManager.startVpnServiceForeground(
|
||||
this@TunnelControlTile,
|
||||
tunnelConfig.toString(),
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e.message)
|
||||
@@ -78,68 +92,33 @@ class TunnelControlTile : TileService() {
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun determineTileTunnel(): TunnelConfig? {
|
||||
var tunnelConfig: TunnelConfig? = null
|
||||
val settings = settingsRepo.getAll()
|
||||
if (settings.isNotEmpty()) {
|
||||
val setting = settings.first()
|
||||
tunnelConfig =
|
||||
if (setting.defaultTunnel != null) {
|
||||
TunnelConfig.from(setting.defaultTunnel!!)
|
||||
} else {
|
||||
val configs = configRepo.getAll()
|
||||
val config =
|
||||
if (configs.isNotEmpty()) {
|
||||
configs.first()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
config
|
||||
}
|
||||
}
|
||||
return tunnelConfig
|
||||
}
|
||||
|
||||
private fun attemptWatcherServiceToggle(tunnelConfig: String) {
|
||||
private fun toggleWatcherServicePause() {
|
||||
scope.launch {
|
||||
val settings = settingsRepo.getAll()
|
||||
if (settings.isNotEmpty()) {
|
||||
val setting = settings.first()
|
||||
if (setting.isAutoTunnelEnabled) {
|
||||
ServiceManager.toggleWatcherServiceForeground(
|
||||
this@TunnelControlTile,
|
||||
tunnelConfig
|
||||
)
|
||||
}
|
||||
val settings = settingsRepository.getSettings()
|
||||
if (settings.isAutoTunnelEnabled) {
|
||||
val pauseAutoTunnel = !settings.isAutoTunnelPaused
|
||||
settingsRepository.save(
|
||||
settings.copy(
|
||||
isAutoTunnelPaused = pauseAutoTunnel,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateTileState() {
|
||||
vpnService.state.collect {
|
||||
try {
|
||||
when (it) {
|
||||
Tunnel.State.UP -> {
|
||||
qsTile.state = Tile.STATE_ACTIVE
|
||||
}
|
||||
private fun setActive() {
|
||||
qsTile.state = Tile.STATE_ACTIVE
|
||||
qsTile.updateTile()
|
||||
}
|
||||
|
||||
Tunnel.State.DOWN -> {
|
||||
qsTile.state = Tile.STATE_INACTIVE
|
||||
}
|
||||
private fun setInactive() {
|
||||
qsTile.state = Tile.STATE_INACTIVE
|
||||
qsTile.updateTile()
|
||||
}
|
||||
|
||||
else -> {
|
||||
qsTile.state = Tile.STATE_UNAVAILABLE
|
||||
}
|
||||
}
|
||||
val config = determineTileTunnel()
|
||||
setTileDescription(
|
||||
config?.name ?: this.resources.getString(R.string.no_tunnel_available)
|
||||
)
|
||||
qsTile.updateTile()
|
||||
} catch (e: Exception) {
|
||||
Timber.e("Unable to update tile state")
|
||||
}
|
||||
}
|
||||
private fun setUnavailable() {
|
||||
qsTile.state = Tile.STATE_UNAVAILABLE
|
||||
qsTile.updateTile()
|
||||
}
|
||||
|
||||
private fun setTileDescription(description: String) {
|
||||
@@ -149,11 +128,6 @@ class TunnelControlTile : TileService() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
qsTile.stateDescription = description
|
||||
}
|
||||
}
|
||||
|
||||
private fun cancelJob() {
|
||||
if (this::job.isInitialized) {
|
||||
job.cancel()
|
||||
}
|
||||
qsTile.updateTile()
|
||||
}
|
||||
}
|
||||
|
||||
+4
-5
@@ -3,15 +3,14 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel
|
||||
enum class HandshakeStatus {
|
||||
HEALTHY,
|
||||
STALE,
|
||||
UNHEALTHY,
|
||||
NEVER_CONNECTED,
|
||||
NOT_STARTED
|
||||
;
|
||||
UNKNOWN,
|
||||
NOT_STARTED;
|
||||
|
||||
companion object {
|
||||
private const val WG_TYPICAL_HANDSHAKE_INTERVAL_WHEN_HEALTHY_SEC = 180
|
||||
const val STATUS_CHANGE_TIME_BUFFER = 30
|
||||
const val STALE_TIME_LIMIT_SEC = WG_TYPICAL_HANDSHAKE_INTERVAL_WHEN_HEALTHY_SEC + STATUS_CHANGE_TIME_BUFFER
|
||||
const val STALE_TIME_LIMIT_SEC =
|
||||
WG_TYPICAL_HANDSHAKE_INTERVAL_WHEN_HEALTHY_SEC + STATUS_CHANGE_TIME_BUFFER
|
||||
const val NEVER_CONNECTED_TO_UNHEALTHY_TIME_LIMIT_SEC = 30
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.tunnel
|
||||
|
||||
import com.wireguard.android.backend.Statistics
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.wireguard.crypto.Key
|
||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
interface VpnService : Tunnel {
|
||||
suspend fun startTunnel(tunnelConfig: TunnelConfig): Tunnel.State
|
||||
|
||||
suspend fun stopTunnel()
|
||||
|
||||
val state: SharedFlow<Tunnel.State>
|
||||
val tunnelName: SharedFlow<String>
|
||||
val statistics: SharedFlow<Statistics>
|
||||
val lastHandshake: SharedFlow<Map<Key, Long>>
|
||||
val handshakeStatus: SharedFlow<HandshakeStatus>
|
||||
val vpnState: StateFlow<VpnState>
|
||||
|
||||
fun getState(): Tunnel.State
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.tunnel
|
||||
|
||||
import com.wireguard.android.backend.Statistics
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
|
||||
data class VpnState(
|
||||
val status: Tunnel.State = Tunnel.State.DOWN,
|
||||
val name: String = "",
|
||||
val statistics: Statistics? = null
|
||||
)
|
||||
+52
-90
@@ -3,60 +3,34 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel
|
||||
import com.wireguard.android.backend.Backend
|
||||
import com.wireguard.android.backend.BackendException
|
||||
import com.wireguard.android.backend.Statistics
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.wireguard.android.backend.Tunnel.State
|
||||
import com.wireguard.config.Config
|
||||
import com.wireguard.crypto.Key
|
||||
import com.zaneschepke.wireguardautotunnel.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.Kernel
|
||||
import com.zaneschepke.wireguardautotunnel.module.Userspace
|
||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||
import javax.inject.Inject
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class WireGuardTunnel
|
||||
@Inject
|
||||
constructor(
|
||||
@Userspace private val userspaceBackend: Backend,
|
||||
@Userspace private val userspaceBackend : Backend,
|
||||
@Kernel private val kernelBackend: Backend,
|
||||
private val settingsRepo: SettingsDoa
|
||||
private val settingsRepository: SettingsRepository
|
||||
) : VpnService {
|
||||
private val _tunnelName = MutableStateFlow("")
|
||||
override val tunnelName get() = _tunnelName.asStateFlow()
|
||||
|
||||
private val _state =
|
||||
MutableSharedFlow<Tunnel.State>(
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||
replay = 1
|
||||
)
|
||||
|
||||
private val _handshakeStatus =
|
||||
MutableSharedFlow<HandshakeStatus>(
|
||||
replay = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||
)
|
||||
override val state get() = _state.asSharedFlow()
|
||||
|
||||
private val _statistics = MutableSharedFlow<Statistics>(replay = 1)
|
||||
override val statistics get() = _statistics.asSharedFlow()
|
||||
|
||||
private val _lastHandshake = MutableSharedFlow<Map<Key, Long>>(replay = 1)
|
||||
override val lastHandshake get() = _lastHandshake.asSharedFlow()
|
||||
|
||||
override val handshakeStatus: SharedFlow<HandshakeStatus>
|
||||
get() = _handshakeStatus.asSharedFlow()
|
||||
private val _vpnState = MutableStateFlow(VpnState())
|
||||
override val vpnState: StateFlow<VpnState> = _vpnState.asStateFlow()
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
@@ -70,13 +44,12 @@ constructor(
|
||||
|
||||
init {
|
||||
scope.launch {
|
||||
settingsRepo.getAllFlow().collect {
|
||||
val settings = it.first()
|
||||
if (settings.isKernelEnabled && backendIsUserspace) {
|
||||
settingsRepository.getSettingsFlow().collect {
|
||||
if (it.isKernelEnabled && backendIsUserspace) {
|
||||
Timber.d("Setting kernel backend")
|
||||
backend = kernelBackend
|
||||
backendIsUserspace = false
|
||||
} else if (!settings.isKernelEnabled && !backendIsUserspace) {
|
||||
} else if (!it.isKernelEnabled && !backendIsUserspace) {
|
||||
Timber.d("Setting userspace backend")
|
||||
backend = userspaceBackend
|
||||
backendIsUserspace = true
|
||||
@@ -85,7 +58,7 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun startTunnel(tunnelConfig: TunnelConfig): Tunnel.State {
|
||||
override suspend fun startTunnel(tunnelConfig: TunnelConfig): State {
|
||||
return try {
|
||||
stopTunnelOnConfigChange(tunnelConfig)
|
||||
emitTunnelName(tunnelConfig.name)
|
||||
@@ -93,95 +66,84 @@ constructor(
|
||||
val state =
|
||||
backend.setState(
|
||||
this,
|
||||
Tunnel.State.UP,
|
||||
config
|
||||
State.UP,
|
||||
config,
|
||||
)
|
||||
_state.emit(state)
|
||||
emitTunnelState(state)
|
||||
state
|
||||
} catch (e: Exception) {
|
||||
Timber.e("Failed to start tunnel with error: ${e.message}")
|
||||
Tunnel.State.DOWN
|
||||
State.DOWN
|
||||
}
|
||||
}
|
||||
|
||||
private fun emitTunnelState(state: State) {
|
||||
_vpnState.tryEmit(
|
||||
_vpnState.value.copy(
|
||||
status = state,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun emitBackendStatistics(statistics: Statistics) {
|
||||
_vpnState.tryEmit(
|
||||
_vpnState.value.copy(
|
||||
statistics = statistics,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun emitTunnelName(name: String) {
|
||||
_tunnelName.emit(name)
|
||||
_vpnState.emit(
|
||||
_vpnState.value.copy(
|
||||
name = name,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun stopTunnelOnConfigChange(tunnelConfig: TunnelConfig) {
|
||||
if (getState() == Tunnel.State.UP && _tunnelName.value != tunnelConfig.name) {
|
||||
if (getState() == State.UP && _vpnState.value.name != tunnelConfig.name) {
|
||||
stopTunnel()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getName(): String {
|
||||
return _tunnelName.value
|
||||
return _vpnState.value.name
|
||||
}
|
||||
|
||||
override suspend fun stopTunnel() {
|
||||
try {
|
||||
if (getState() == Tunnel.State.UP) {
|
||||
val state = backend.setState(this, Tunnel.State.DOWN, null)
|
||||
_state.emit(state)
|
||||
if (getState() == State.UP) {
|
||||
val state = backend.setState(this, State.DOWN, null)
|
||||
emitTunnelState(state)
|
||||
}
|
||||
} catch (e: BackendException) {
|
||||
Timber.e("Failed to stop tunnel with error: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun getState(): Tunnel.State {
|
||||
override fun getState(): State {
|
||||
return backend.getState(this)
|
||||
}
|
||||
|
||||
override fun onStateChange(state: Tunnel.State) {
|
||||
override fun onStateChange(state: State) {
|
||||
val tunnel = this
|
||||
_state.tryEmit(state)
|
||||
if (state == Tunnel.State.UP) {
|
||||
emitTunnelState(state)
|
||||
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
||||
if (state == State.UP) {
|
||||
statsJob =
|
||||
scope.launch {
|
||||
val handshakeMap = HashMap<Key, Long>()
|
||||
var neverHadHandshakeCounter = 0
|
||||
while (true) {
|
||||
val statistics = backend.getStatistics(tunnel)
|
||||
_statistics.emit(statistics)
|
||||
statistics.peers().forEach { key ->
|
||||
val handshakeEpoch =
|
||||
statistics.peer(key)?.latestHandshakeEpochMillis ?: 0L
|
||||
handshakeMap[key] = handshakeEpoch
|
||||
if (handshakeEpoch == 0L) {
|
||||
if (neverHadHandshakeCounter >= HandshakeStatus.NEVER_CONNECTED_TO_UNHEALTHY_TIME_LIMIT_SEC) {
|
||||
_handshakeStatus.emit(HandshakeStatus.NEVER_CONNECTED)
|
||||
} else {
|
||||
_handshakeStatus.emit(HandshakeStatus.NOT_STARTED)
|
||||
}
|
||||
if (neverHadHandshakeCounter <= HandshakeStatus.NEVER_CONNECTED_TO_UNHEALTHY_TIME_LIMIT_SEC) {
|
||||
neverHadHandshakeCounter += (1 * Constants.VPN_STATISTIC_CHECK_INTERVAL / 1000).toInt()
|
||||
}
|
||||
return@forEach
|
||||
}
|
||||
// TODO one day make each peer have their own dedicated status
|
||||
val lastHandshake = NumberUtils.getSecondsBetweenTimestampAndNow(
|
||||
handshakeEpoch
|
||||
)
|
||||
if (lastHandshake != null) {
|
||||
if (lastHandshake >= HandshakeStatus.STALE_TIME_LIMIT_SEC) {
|
||||
_handshakeStatus.emit(HandshakeStatus.STALE)
|
||||
} else {
|
||||
_handshakeStatus.emit(HandshakeStatus.HEALTHY)
|
||||
}
|
||||
}
|
||||
}
|
||||
_lastHandshake.emit(handshakeMap)
|
||||
emitBackendStatistics(statistics)
|
||||
delay(Constants.VPN_STATISTIC_CHECK_INTERVAL)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (state == Tunnel.State.DOWN) {
|
||||
if (state == State.DOWN) {
|
||||
if (this::statsJob.isInitialized) {
|
||||
statsJob.cancel()
|
||||
}
|
||||
_handshakeStatus.tryEmit(HandshakeStatus.NOT_STARTED)
|
||||
_lastHandshake.tryEmit(emptyMap())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
class ActivityViewModel @Inject constructor() : ViewModel() {
|
||||
// TODO move shared logic to shared viewmodel
|
||||
@HiltViewModel
|
||||
class ActivityViewModel
|
||||
@Inject
|
||||
constructor() : ViewModel() {
|
||||
|
||||
}
|
||||
|
||||
@@ -6,15 +6,13 @@ import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.view.KeyEvent
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.SystemBarStyle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.animation.ExitTransition
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarData
|
||||
@@ -24,13 +22,12 @@ import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.SnackbarResult
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.input.key.onKeyEvent
|
||||
import 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.lifecycle.lifecycleScope
|
||||
import androidx.navigation.compose.NavHost
|
||||
@@ -39,9 +36,10 @@ import androidx.navigation.compose.rememberNavController
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.wireguard.android.backend.GoBackend
|
||||
import com.zaneschepke.wireguardautotunnel.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.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.navigation.BottomNavBar
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar
|
||||
@@ -49,57 +47,61 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
|
||||
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.util.Constants
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
@Inject
|
||||
lateinit var dataStoreManager: DataStoreManager
|
||||
|
||||
@Inject lateinit var settingsRepository: SettingsRepository
|
||||
@OptIn(
|
||||
ExperimentalPermissionsApi::class
|
||||
ExperimentalPermissionsApi::class,
|
||||
)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
enableEdgeToEdge(navigationBarStyle = SystemBarStyle.dark(Color.Transparent.toArgb()))
|
||||
|
||||
// load preferences into memory and init data
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
dataStoreManager.init()
|
||||
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
||||
} catch (e: IOException) {
|
||||
Timber.e("Failed to load preferences")
|
||||
}
|
||||
}
|
||||
setContent {
|
||||
// TODO move shared logic to shared viewmodel
|
||||
// val sharedViewModel = hiltViewModel<ActivityViewModel>()
|
||||
//val activityViewModel = hiltViewModel<ActivityViewModel>()
|
||||
|
||||
val navController = rememberNavController()
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
WireguardAutoTunnelTheme {
|
||||
TransparentSystemBars()
|
||||
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
val notificationPermissionState =
|
||||
rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS)
|
||||
val notificationPermissionState = if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
|
||||
rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS) else null
|
||||
|
||||
fun requestNotificationPermission() {
|
||||
if (!notificationPermissionState.status.isGranted && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
if (notificationPermissionState != null && !notificationPermissionState.status.isGranted
|
||||
) {
|
||||
notificationPermissionState.launchPermissionRequest()
|
||||
}
|
||||
}
|
||||
|
||||
var vpnIntent by remember { mutableStateOf(GoBackend.VpnService.prepare(this)) }
|
||||
val vpnActivityResultState =
|
||||
rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult(),
|
||||
onResult = {
|
||||
val accepted = (it.resultCode == RESULT_OK)
|
||||
if (accepted) {
|
||||
vpnIntent = null
|
||||
}
|
||||
}
|
||||
)
|
||||
LaunchedEffect(vpnIntent) {
|
||||
if (vpnIntent != null) {
|
||||
vpnActivityResultState.launch(vpnIntent)
|
||||
} else {
|
||||
requestNotificationPermission()
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
requestNotificationPermission()
|
||||
}
|
||||
|
||||
fun showSnackBarMessage(message: String) {
|
||||
@@ -108,13 +110,10 @@ class MainActivity : AppCompatActivity() {
|
||||
snackbarHostState.showSnackbar(
|
||||
message = message,
|
||||
actionLabel = applicationContext.getString(R.string.okay),
|
||||
duration = SnackbarDuration.Short
|
||||
duration = SnackbarDuration.Short,
|
||||
)
|
||||
when (result) {
|
||||
SnackbarResult.ActionPerformed -> {
|
||||
snackbarHostState.currentSnackbarData?.dismiss()
|
||||
}
|
||||
|
||||
SnackbarResult.ActionPerformed,
|
||||
SnackbarResult.Dismissed -> {
|
||||
snackbarHostState.currentSnackbarData?.dismiss()
|
||||
}
|
||||
@@ -128,158 +127,90 @@ class MainActivity : AppCompatActivity() {
|
||||
CustomSnackBar(
|
||||
snackbarData.visuals.message,
|
||||
isRtl = false,
|
||||
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(
|
||||
2.dp
|
||||
)
|
||||
containerColor =
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(
|
||||
2.dp,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier =
|
||||
Modifier.onKeyEvent {
|
||||
if (it.nativeKeyEvent.action == KeyEvent.ACTION_UP) {
|
||||
when (it.nativeKeyEvent.keyCode) {
|
||||
KeyEvent.KEYCODE_DPAD_UP -> {
|
||||
try {
|
||||
focusRequester.requestFocus()
|
||||
} catch (e: IllegalStateException) {
|
||||
Timber.e(
|
||||
"No D-Pad focus request modifier added to element on screen"
|
||||
)
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
else -> {
|
||||
false
|
||||
}
|
||||
modifier = Modifier
|
||||
.focusable()
|
||||
.focusProperties { up = focusRequester },
|
||||
bottomBar =
|
||||
if (notificationPermissionState == null || notificationPermissionState.status.isGranted) {
|
||||
{
|
||||
BottomNavBar(
|
||||
navController,
|
||||
listOf(
|
||||
Screen.Main.navItem,
|
||||
Screen.Settings.navItem,
|
||||
Screen.Support.navItem,
|
||||
),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
},
|
||||
bottomBar =
|
||||
if (vpnIntent == null && notificationPermissionState.status.isGranted) {
|
||||
{ BottomNavBar(navController, Routes.navItems) }
|
||||
} else {
|
||||
{}
|
||||
}
|
||||
{}
|
||||
},
|
||||
) { padding ->
|
||||
if (vpnIntent != null) {
|
||||
PermissionRequestFailedScreen(
|
||||
padding = padding,
|
||||
onRequestAgain = { vpnActivityResultState.launch(vpnIntent) },
|
||||
message = getString(R.string.vpn_permission_required),
|
||||
getString(R.string.retry)
|
||||
)
|
||||
return@Scaffold
|
||||
}
|
||||
if (!notificationPermissionState.status.isGranted) {
|
||||
PermissionRequestFailedScreen(
|
||||
padding = padding,
|
||||
onRequestAgain = {
|
||||
val intentSettings =
|
||||
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||
intentSettings.data =
|
||||
Uri.fromParts(
|
||||
Constants.URI_PACKAGE_SCHEME,
|
||||
this.packageName,
|
||||
null
|
||||
)
|
||||
startActivity(intentSettings)
|
||||
},
|
||||
message = getString(R.string.notification_permission_required),
|
||||
getString(R.string.open_settings)
|
||||
)
|
||||
return@Scaffold
|
||||
}
|
||||
|
||||
NavHost(navController, startDestination = Routes.Main.name) {
|
||||
composable(
|
||||
Routes.Main.name,
|
||||
enterTransition = {
|
||||
when (initialState.destination.route) {
|
||||
Routes.Settings.name, Routes.Support.name ->
|
||||
slideInHorizontally(
|
||||
initialOffsetX = {
|
||||
-Constants.SLIDE_IN_TRANSITION_OFFSET
|
||||
},
|
||||
animationSpec = tween(
|
||||
Constants.SLIDE_IN_ANIMATION_DURATION
|
||||
)
|
||||
Column(modifier = Modifier.padding(padding)) {
|
||||
if (notificationPermissionState != null && !notificationPermissionState.status.isGranted) {
|
||||
PermissionRequestFailedScreen(
|
||||
onRequestAgain = {
|
||||
val intentSettings =
|
||||
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||
intentSettings.data =
|
||||
Uri.fromParts(
|
||||
Constants.URI_PACKAGE_SCHEME,
|
||||
this@MainActivity.packageName,
|
||||
null,
|
||||
)
|
||||
|
||||
else -> {
|
||||
fadeIn(
|
||||
animationSpec = tween(
|
||||
Constants.FADE_IN_ANIMATION_DURATION
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
exitTransition = {
|
||||
ExitTransition.None
|
||||
}
|
||||
) {
|
||||
MainScreen(padding = padding, showSnackbarMessage = { message ->
|
||||
showSnackBarMessage(message)
|
||||
}, navController = navController)
|
||||
startActivity(intentSettings)
|
||||
},
|
||||
message = getString(R.string.notification_permission_required),
|
||||
getString(R.string.open_settings),
|
||||
)
|
||||
return@Scaffold
|
||||
}
|
||||
composable(Routes.Settings.name, enterTransition = {
|
||||
when (initialState.destination.route) {
|
||||
Routes.Main.name ->
|
||||
slideInHorizontally(
|
||||
initialOffsetX = { Constants.SLIDE_IN_TRANSITION_OFFSET },
|
||||
animationSpec = tween(Constants.SLIDE_IN_ANIMATION_DURATION)
|
||||
)
|
||||
|
||||
Routes.Support.name -> {
|
||||
slideInHorizontally(
|
||||
initialOffsetX = { -Constants.SLIDE_IN_TRANSITION_OFFSET },
|
||||
animationSpec = tween(Constants.SLIDE_IN_ANIMATION_DURATION)
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
fadeIn(
|
||||
animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION)
|
||||
)
|
||||
}
|
||||
}
|
||||
}) {
|
||||
SettingsScreen(padding = padding, showSnackbarMessage = { message ->
|
||||
showSnackBarMessage(message)
|
||||
}, focusRequester = focusRequester)
|
||||
}
|
||||
composable(Routes.Support.name, enterTransition = {
|
||||
when (initialState.destination.route) {
|
||||
Routes.Settings.name, Routes.Main.name ->
|
||||
slideInHorizontally(
|
||||
initialOffsetX = { Constants.SLIDE_IN_ANIMATION_DURATION },
|
||||
animationSpec = tween(Constants.SLIDE_IN_ANIMATION_DURATION)
|
||||
)
|
||||
|
||||
else -> {
|
||||
fadeIn(
|
||||
animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION)
|
||||
)
|
||||
}
|
||||
}
|
||||
}) { SupportScreen(padding = padding, focusRequester = focusRequester) }
|
||||
composable("${Routes.Config.name}/{id}", enterTransition = {
|
||||
fadeIn(animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION))
|
||||
}) {
|
||||
val id = it.arguments?.getString("id")
|
||||
if (!id.isNullOrBlank()) {
|
||||
ConfigScreen(
|
||||
NavHost(navController, startDestination = Screen.Main.route) {
|
||||
composable(
|
||||
Screen.Main.route,
|
||||
) {
|
||||
MainScreen(
|
||||
focusRequester = focusRequester,
|
||||
showSnackbarMessage = { message -> showSnackBarMessage(message) },
|
||||
navController = navController,
|
||||
id = id,
|
||||
showSnackbarMessage = { message ->
|
||||
showSnackBarMessage(message)
|
||||
},
|
||||
focusRequester = focusRequester
|
||||
)
|
||||
}
|
||||
composable(
|
||||
Screen.Settings.route,
|
||||
) {
|
||||
SettingsScreen(
|
||||
showSnackbarMessage = { message -> showSnackBarMessage(message) },
|
||||
focusRequester = focusRequester,
|
||||
)
|
||||
}
|
||||
composable(
|
||||
Screen.Support.route,
|
||||
) {
|
||||
SupportScreen(
|
||||
focusRequester = focusRequester,
|
||||
showSnackbarMessage = { message -> showSnackBarMessage(message) },
|
||||
)
|
||||
}
|
||||
composable("${Screen.Config.route}/{id}") {
|
||||
val id = it.arguments?.getString("id")
|
||||
if (!id.isNullOrBlank()) {
|
||||
ConfigScreen(
|
||||
navController = navController,
|
||||
id = id,
|
||||
showSnackbarMessage = { message ->
|
||||
showSnackBarMessage(message)
|
||||
},
|
||||
focusRequester = focusRequester,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Home
|
||||
import androidx.compose.material.icons.rounded.QuestionMark
|
||||
import androidx.compose.material.icons.rounded.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
|
||||
|
||||
enum class Routes {
|
||||
Main,
|
||||
Settings,
|
||||
Support,
|
||||
Config
|
||||
;
|
||||
|
||||
companion object {
|
||||
val navItems =
|
||||
listOf(
|
||||
BottomNavItem(
|
||||
name = "Tunnels",
|
||||
route = Main.name,
|
||||
icon = Icons.Rounded.Home
|
||||
),
|
||||
BottomNavItem(
|
||||
name = "Settings",
|
||||
route = Settings.name,
|
||||
icon = Icons.Rounded.Settings
|
||||
),
|
||||
BottomNavItem(
|
||||
name = "Support",
|
||||
route = Support.name,
|
||||
icon = Icons.Rounded.QuestionMark
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Home
|
||||
import androidx.compose.material.icons.rounded.QuestionMark
|
||||
import androidx.compose.material.icons.rounded.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
|
||||
|
||||
sealed class Screen(val route: String) {
|
||||
data object Main : Screen("main") {
|
||||
val navItem =
|
||||
BottomNavItem(
|
||||
name = "Tunnels",
|
||||
route = route,
|
||||
icon = Icons.Rounded.Home,
|
||||
)
|
||||
}
|
||||
|
||||
data object Settings : Screen("settings") {
|
||||
val navItem =
|
||||
BottomNavItem(
|
||||
name = "Settings",
|
||||
route = route,
|
||||
icon = Icons.Rounded.Settings,
|
||||
)
|
||||
}
|
||||
|
||||
data object Support : Screen("support") {
|
||||
val navItem =
|
||||
BottomNavItem(
|
||||
name = "Support",
|
||||
route = route,
|
||||
icon = Icons.Rounded.QuestionMark,
|
||||
)
|
||||
}
|
||||
|
||||
data object Config : Screen("config")
|
||||
}
|
||||
+8
-10
@@ -1,10 +1,8 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.common
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
@@ -13,19 +11,19 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
|
||||
@Composable
|
||||
fun ClickableIconButton(
|
||||
onClick: () -> Unit,
|
||||
onIconClick: () -> Unit,
|
||||
text: String,
|
||||
icon: ImageVector,
|
||||
enabled: Boolean
|
||||
) {
|
||||
TextButton(
|
||||
onClick = {},
|
||||
enabled = enabled
|
||||
onClick = onClick,
|
||||
enabled = enabled,
|
||||
) {
|
||||
Text(text, Modifier.weight(1f, false))
|
||||
Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing))
|
||||
@@ -33,11 +31,11 @@ fun ClickableIconButton(
|
||||
imageVector = icon,
|
||||
contentDescription = stringResource(R.string.delete),
|
||||
modifier =
|
||||
Modifier.size(ButtonDefaults.IconSize).weight(1f, false).clickable {
|
||||
if (enabled) {
|
||||
onIconClick()
|
||||
}
|
||||
}
|
||||
Modifier.size(ButtonDefaults.IconSize).weight(1f, false).clickable {
|
||||
if (enabled) {
|
||||
onIconClick()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+4
-11
@@ -2,7 +2,6 @@ package com.zaneschepke.wireguardautotunnel.ui.common
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Button
|
||||
@@ -17,7 +16,6 @@ import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun PermissionRequestFailedScreen(
|
||||
padding: PaddingValues,
|
||||
onRequestAgain: () -> Unit,
|
||||
message: String,
|
||||
buttonText: String
|
||||
@@ -26,17 +24,12 @@ fun PermissionRequestFailedScreen(
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
Text(message, textAlign = TextAlign.Center, modifier = Modifier.padding(15.dp))
|
||||
Button(onClick = {
|
||||
scope.launch {
|
||||
onRequestAgain()
|
||||
}
|
||||
}) {
|
||||
Button(
|
||||
onClick = { scope.launch { onRequestAgain() } },
|
||||
) {
|
||||
Text(buttonText)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@ import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.wireguard.android.backend.Statistics
|
||||
import com.zaneschepke.wireguardautotunnel.toThreeDecimalPlaceString
|
||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||
import com.zaneschepke.wireguardautotunnel.util.toThreeDecimalPlaceString
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
@@ -34,30 +34,22 @@ fun RowListItem(
|
||||
) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.animateContentSize()
|
||||
.clip(RoundedCornerShape(30.dp))
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
onClick()
|
||||
},
|
||||
onLongClick = {
|
||||
onHold()
|
||||
}
|
||||
)
|
||||
Modifier.animateContentSize()
|
||||
.clip(RoundedCornerShape(30.dp))
|
||||
.combinedClickable(
|
||||
onClick = { onClick() },
|
||||
onLongClick = { onHold() },
|
||||
),
|
||||
) {
|
||||
Column {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 14.dp, vertical = 5.dp),
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 15.dp, vertical = 5.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth(.60f)
|
||||
modifier = Modifier.fillMaxWidth(.60f),
|
||||
) {
|
||||
icon()
|
||||
Text(text)
|
||||
@@ -68,11 +60,10 @@ fun RowListItem(
|
||||
statistics?.peers()?.forEach {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(end = 10.dp, bottom = 10.dp, start = 10.dp),
|
||||
Modifier.fillMaxWidth()
|
||||
.padding(end = 10.dp, bottom = 10.dp, start = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
) {
|
||||
val handshakeEpoch = statistics.peer(it)!!.latestHandshakeEpochMillis
|
||||
val peerTx = statistics.peer(it)!!.txBytes
|
||||
|
||||
@@ -47,7 +47,7 @@ fun SearchBar(onQuery: (queryString: String) -> Unit) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Search,
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
contentDescription = stringResource(id = R.string.search_icon)
|
||||
contentDescription = stringResource(id = R.string.search_icon),
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
@@ -56,25 +56,24 @@ fun SearchBar(onQuery: (queryString: String) -> Unit) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Clear,
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
contentDescription = stringResource(id = R.string.clear_icon)
|
||||
contentDescription = stringResource(id = R.string.clear_icon),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
maxLines = 1,
|
||||
colors =
|
||||
TextFieldDefaults.colors(
|
||||
focusedContainerColor = Color.Transparent,
|
||||
unfocusedContainerColor = Color.Transparent,
|
||||
disabledContainerColor = Color.Transparent
|
||||
),
|
||||
TextFieldDefaults.colors(
|
||||
focusedContainerColor = Color.Transparent,
|
||||
unfocusedContainerColor = Color.Transparent,
|
||||
disabledContainerColor = Color.Transparent,
|
||||
),
|
||||
placeholder = { Text(text = stringResource(R.string.hint_search_packages)) },
|
||||
textStyle = MaterialTheme.typography.bodySmall,
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.background(color = MaterialTheme.colorScheme.background, shape = RectangleShape)
|
||||
Modifier.fillMaxWidth()
|
||||
.background(color = MaterialTheme.colorScheme.background, shape = RectangleShape),
|
||||
)
|
||||
}
|
||||
|
||||
+7
-11
@@ -22,19 +22,15 @@ fun ConfigurationTextBox(
|
||||
modifier = modifier,
|
||||
value = value,
|
||||
singleLine = true,
|
||||
onValueChange = {
|
||||
onValueChange(it)
|
||||
},
|
||||
onValueChange = { onValueChange(it) },
|
||||
label = { Text(label) },
|
||||
maxLines = 1,
|
||||
placeholder = {
|
||||
Text(hint)
|
||||
},
|
||||
placeholder = { Text(hint) },
|
||||
keyboardOptions =
|
||||
KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.None,
|
||||
imeAction = ImeAction.Done
|
||||
),
|
||||
keyboardActions = keyboardActions
|
||||
KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.None,
|
||||
imeAction = ImeAction.Done,
|
||||
),
|
||||
keyboardActions = keyboardActions,
|
||||
)
|
||||
}
|
||||
|
||||
+3
-8
@@ -21,21 +21,16 @@ fun ConfigurationToggle(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(padding),
|
||||
modifier = Modifier.fillMaxWidth().padding(padding),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Text(label)
|
||||
Switch(
|
||||
modifier = modifier,
|
||||
enabled = enabled,
|
||||
checked = checked,
|
||||
onCheckedChange = {
|
||||
onCheckChanged()
|
||||
}
|
||||
onCheckedChange = { onCheckChanged() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+5
-8
@@ -11,14 +11,11 @@ import androidx.navigation.NavController
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
|
||||
@Composable
|
||||
fun BottomNavBar(
|
||||
navController: NavController,
|
||||
bottomNavItems: List<BottomNavItem>
|
||||
) {
|
||||
fun BottomNavBar(navController: NavController, bottomNavItems: List<BottomNavItem>) {
|
||||
val backStackEntry = navController.currentBackStackEntryAsState()
|
||||
|
||||
NavigationBar(
|
||||
containerColor = MaterialTheme.colorScheme.background
|
||||
containerColor = MaterialTheme.colorScheme.background,
|
||||
) {
|
||||
bottomNavItems.forEach { item ->
|
||||
val selected = item.route == backStackEntry.value?.destination?.route
|
||||
@@ -29,15 +26,15 @@ fun BottomNavBar(
|
||||
label = {
|
||||
Text(
|
||||
text = item.name,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = item.icon,
|
||||
contentDescription = "${item.name} Icon"
|
||||
contentDescription = "${item.name} Icon",
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+31
-45
@@ -11,51 +11,40 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
|
||||
@Composable
|
||||
fun AuthorizationPrompt(
|
||||
onSuccess: () -> Unit,
|
||||
onFailure: () -> Unit,
|
||||
onError: (String) -> Unit
|
||||
) {
|
||||
fun AuthorizationPrompt(onSuccess: () -> Unit, onFailure: () -> Unit, onError: (String) -> Unit) {
|
||||
val context = LocalContext.current
|
||||
val biometricManager = BiometricManager.from(context)
|
||||
val bio = biometricManager.canAuthenticate(BIOMETRIC_WEAK or DEVICE_CREDENTIAL)
|
||||
val isBiometricAvailable =
|
||||
remember {
|
||||
when (bio) {
|
||||
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> {
|
||||
onError("Biometrics not available")
|
||||
false
|
||||
}
|
||||
|
||||
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
|
||||
onError("Biometrics not created")
|
||||
false
|
||||
}
|
||||
|
||||
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> {
|
||||
onError("Biometric hardware not found")
|
||||
false
|
||||
}
|
||||
|
||||
BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> {
|
||||
onError("Biometric security update required")
|
||||
false
|
||||
}
|
||||
|
||||
BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> {
|
||||
onError("Biometrics not supported")
|
||||
false
|
||||
}
|
||||
|
||||
BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> {
|
||||
onError("Biometrics status unknown")
|
||||
false
|
||||
}
|
||||
|
||||
BiometricManager.BIOMETRIC_SUCCESS -> true
|
||||
else -> false
|
||||
val isBiometricAvailable = remember {
|
||||
when (bio) {
|
||||
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> {
|
||||
onError("Biometrics not available")
|
||||
false
|
||||
}
|
||||
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
|
||||
onError("Biometrics not created")
|
||||
false
|
||||
}
|
||||
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> {
|
||||
onError("Biometric hardware not found")
|
||||
false
|
||||
}
|
||||
BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> {
|
||||
onError("Biometric security update required")
|
||||
false
|
||||
}
|
||||
BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> {
|
||||
onError("Biometrics not supported")
|
||||
false
|
||||
}
|
||||
BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> {
|
||||
onError("Biometrics status unknown")
|
||||
false
|
||||
}
|
||||
BiometricManager.BIOMETRIC_SUCCESS -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
if (isBiometricAvailable) {
|
||||
val executor = remember { ContextCompat.getMainExecutor(context) }
|
||||
|
||||
@@ -71,10 +60,7 @@ fun AuthorizationPrompt(
|
||||
context as FragmentActivity,
|
||||
executor,
|
||||
object : BiometricPrompt.AuthenticationCallback() {
|
||||
override fun onAuthenticationError(
|
||||
errorCode: Int,
|
||||
errString: CharSequence
|
||||
) {
|
||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||
super.onAuthenticationError(errorCode, errString)
|
||||
onFailure()
|
||||
}
|
||||
@@ -90,7 +76,7 @@ fun AuthorizationPrompt(
|
||||
super.onAuthenticationFailed()
|
||||
onFailure()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
biometricPrompt.authenticate(promptInfo)
|
||||
}
|
||||
|
||||
+8
-10
@@ -19,7 +19,6 @@ import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
@@ -33,29 +32,28 @@ fun CustomSnackBar(
|
||||
isRtl: Boolean = true,
|
||||
containerColor: Color = MaterialTheme.colorScheme.surface
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
Snackbar(
|
||||
containerColor = containerColor,
|
||||
modifier =
|
||||
Modifier.fillMaxWidth(
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) 1 / 3f else 2 / 3f
|
||||
).padding(bottom = 100.dp),
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
Modifier.fillMaxWidth(
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) 1 / 3f else 2 / 3f,
|
||||
)
|
||||
.padding(bottom = 100.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
) {
|
||||
CompositionLocalProvider(
|
||||
LocalLayoutDirection provides
|
||||
if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr
|
||||
LocalLayoutDirection provides if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.width(IntrinsicSize.Max).height(IntrinsicSize.Min),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Start
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
) {
|
||||
Icon(
|
||||
Icons.Rounded.Info,
|
||||
contentDescription = stringResource(R.string.info),
|
||||
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))
|
||||
}
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.common.screen
|
||||
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun LoadingScreen() {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier = Modifier.fillMaxSize().focusable().padding(),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(120.dp)) { CircularProgressIndicator() }
|
||||
}
|
||||
}
|
||||
+2
-5
@@ -12,14 +12,11 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
@Composable
|
||||
fun SectionTitle(
|
||||
title: String,
|
||||
padding: Dp
|
||||
) {
|
||||
fun SectionTitle(title: String, padding: Dp) {
|
||||
Text(
|
||||
title,
|
||||
textAlign = TextAlign.Center,
|
||||
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(),
|
||||
addresses = i.addresses.joinToString(", ").trim(),
|
||||
dnsServers = i.dnsServers.joinToString(", ").replace("/", "").trim(),
|
||||
listenPort = if (i.listenPort.isPresent) {
|
||||
i.listenPort.get().toString()
|
||||
.trim()
|
||||
} else {
|
||||
""
|
||||
},
|
||||
mtu = if (i.mtu.isPresent) i.mtu.get().toString().trim() else ""
|
||||
listenPort =
|
||||
if (i.listenPort.isPresent) {
|
||||
i.listenPort.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 {
|
||||
return PeerProxy(
|
||||
publicKey = peer.publicKey.toBase64(),
|
||||
preSharedKey = if (peer.preSharedKey.isPresent) {
|
||||
peer.preSharedKey.get().toBase64()
|
||||
.trim()
|
||||
} else {
|
||||
""
|
||||
},
|
||||
persistentKeepalive = if (peer.persistentKeepalive.isPresent) {
|
||||
peer.persistentKeepalive.get()
|
||||
.toString().trim()
|
||||
} else {
|
||||
""
|
||||
},
|
||||
endpoint = if (peer.endpoint.isPresent) {
|
||||
peer.endpoint.get().toString()
|
||||
.trim()
|
||||
} else {
|
||||
""
|
||||
},
|
||||
allowedIps = peer.allowedIps.joinToString(", ").trim()
|
||||
preSharedKey =
|
||||
if (peer.preSharedKey.isPresent) {
|
||||
peer.preSharedKey.get().toBase64().trim()
|
||||
} else {
|
||||
""
|
||||
},
|
||||
persistentKeepalive =
|
||||
if (peer.persistentKeepalive.isPresent) {
|
||||
peer.persistentKeepalive.get().toString().trim()
|
||||
} else {
|
||||
""
|
||||
},
|
||||
endpoint =
|
||||
if (peer.endpoint.isPresent) {
|
||||
peer.endpoint.get().toString().trim()
|
||||
} else {
|
||||
""
|
||||
},
|
||||
allowedIps = peer.allowedIps.joinToString(", ").trim(),
|
||||
)
|
||||
}
|
||||
|
||||
val IPV4_PUBLIC_NETWORKS =
|
||||
setOf(
|
||||
"0.0.0.0/5", "8.0.0.0/7", "11.0.0.0/8", "12.0.0.0/6", "16.0.0.0/4", "32.0.0.0/3",
|
||||
"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"
|
||||
"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",
|
||||
"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")
|
||||
}
|
||||
|
||||
+333
-481
File diff suppressed because it is too large
Load Diff
+18
@@ -0,0 +1,18 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.config
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.ui.models.InterfaceProxy
|
||||
import com.zaneschepke.wireguardautotunnel.ui.models.PeerProxy
|
||||
import com.zaneschepke.wireguardautotunnel.util.Packages
|
||||
|
||||
data class ConfigUiState(
|
||||
val proxyPeers: List<PeerProxy> = arrayListOf(PeerProxy()),
|
||||
val interfaceProxy: InterfaceProxy = InterfaceProxy(),
|
||||
val packages: Packages = emptyList(),
|
||||
val checkedPackageNames: List<String> = emptyList(),
|
||||
val include: Boolean = true,
|
||||
val isAllApplicationsEnabled: Boolean = false,
|
||||
val loading: Boolean = true,
|
||||
val tunnel: TunnelConfig? = null,
|
||||
val tunnelName: String = ""
|
||||
)
|
||||
+190
-275
@@ -5,8 +5,6 @@ import android.app.Application
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.toMutableStateList
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.wireguard.config.Config
|
||||
@@ -14,210 +12,112 @@ import com.wireguard.config.Interface
|
||||
import com.wireguard.config.Peer
|
||||
import com.wireguard.crypto.Key
|
||||
import com.wireguard.crypto.KeyPair
|
||||
import com.zaneschepke.wireguardautotunnel.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
|
||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
||||
import com.zaneschepke.wireguardautotunnel.ui.models.InterfaceProxy
|
||||
import com.zaneschepke.wireguardautotunnel.ui.models.PeerProxy
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.Event
|
||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
|
||||
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||
import com.zaneschepke.wireguardautotunnel.util.removeAt
|
||||
import com.zaneschepke.wireguardautotunnel.util.update
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class ConfigViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val application: Application,
|
||||
private val tunnelRepo: TunnelConfigDao,
|
||||
private val settingsRepo: SettingsDoa
|
||||
private val tunnelConfigRepository: TunnelConfigRepository,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
) : ViewModel() {
|
||||
private val _tunnel = MutableStateFlow<TunnelConfig?>(null)
|
||||
private val _tunnelName = MutableStateFlow("")
|
||||
val tunnelName get() = _tunnelName.asStateFlow()
|
||||
val tunnel get() = _tunnel.asStateFlow()
|
||||
|
||||
private var _proxyPeers = MutableStateFlow(mutableStateListOf<PeerProxy>())
|
||||
val proxyPeers get() = _proxyPeers.asStateFlow()
|
||||
|
||||
private var _interface = MutableStateFlow(InterfaceProxy())
|
||||
val interfaceProxy = _interface.asStateFlow()
|
||||
|
||||
private val _packages = MutableStateFlow(emptyList<PackageInfo>())
|
||||
val packages get() = _packages.asStateFlow()
|
||||
private val packageManager = application.packageManager
|
||||
|
||||
private val _checkedPackages = MutableStateFlow(mutableStateListOf<String>())
|
||||
val checkedPackages get() = _checkedPackages.asStateFlow()
|
||||
private val _include = MutableStateFlow(true)
|
||||
val include get() = _include.asStateFlow()
|
||||
private val _uiState = MutableStateFlow(ConfigUiState())
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
private val _isAllApplicationsEnabled = MutableStateFlow(false)
|
||||
val isAllApplicationsEnabled get() = _isAllApplicationsEnabled.asStateFlow()
|
||||
private val _isDefaultTunnel = MutableStateFlow(false)
|
||||
|
||||
private lateinit var tunnelConfig: TunnelConfig
|
||||
|
||||
suspend fun onScreenLoad(id: String) {
|
||||
if (id != Constants.MANUAL_TUNNEL_CONFIG_ID) {
|
||||
tunnelConfig = getTunnelConfigById(id) ?: throw WgTunnelException(
|
||||
"Config not found"
|
||||
)
|
||||
emitScreenData()
|
||||
} else {
|
||||
emitEmptyScreenData()
|
||||
fun init(tunnelId: String) =
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val packages = getQueriedPackages("")
|
||||
val state =
|
||||
if (tunnelId != Constants.MANUAL_TUNNEL_CONFIG_ID) {
|
||||
val tunnelConfig =
|
||||
tunnelConfigRepository.getAll().firstOrNull { it.id.toString() == tunnelId }
|
||||
if (tunnelConfig != null) {
|
||||
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
||||
val proxyPeers = config.peers.map { PeerProxy.from(it) }
|
||||
val proxyInterface = InterfaceProxy.from(config.`interface`)
|
||||
var include = true
|
||||
var isAllApplicationsEnabled = false
|
||||
val checkedPackages =
|
||||
if (config.`interface`.includedApplications.isNotEmpty()) {
|
||||
config.`interface`.includedApplications
|
||||
} else if (config.`interface`.excludedApplications.isNotEmpty()) {
|
||||
include = false
|
||||
config.`interface`.excludedApplications
|
||||
} else {
|
||||
isAllApplicationsEnabled = true
|
||||
emptySet()
|
||||
}
|
||||
ConfigUiState(
|
||||
proxyPeers,
|
||||
proxyInterface,
|
||||
packages,
|
||||
checkedPackages.toList(),
|
||||
include,
|
||||
isAllApplicationsEnabled,
|
||||
false,
|
||||
tunnelConfig,
|
||||
tunnelConfig.name,
|
||||
)
|
||||
} else {
|
||||
ConfigUiState(loading = false, packages = packages)
|
||||
}
|
||||
} else {
|
||||
ConfigUiState(loading = false, packages = packages)
|
||||
}
|
||||
_uiState.value = state
|
||||
}
|
||||
}
|
||||
|
||||
private fun emitEmptyScreenData() {
|
||||
tunnelConfig = TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = "")
|
||||
viewModelScope.launch {
|
||||
emitTunnelConfig()
|
||||
emitPeerProxy(PeerProxy())
|
||||
emitInterfaceProxy(InterfaceProxy())
|
||||
emitTunnelConfigName()
|
||||
emitDefaultTunnelStatus()
|
||||
emitQueriedPackages("")
|
||||
emitTunnelAllApplicationsEnabled()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun emitScreenData() {
|
||||
emitTunnelConfig()
|
||||
emitPeersFromConfig()
|
||||
emitInterfaceFromConfig()
|
||||
emitTunnelConfigName()
|
||||
emitDefaultTunnelStatus()
|
||||
emitQueriedPackages("")
|
||||
emitCurrentPackageConfigurations()
|
||||
}
|
||||
|
||||
private suspend fun emitDefaultTunnelStatus() {
|
||||
val settings = settingsRepo.getAll()
|
||||
if (settings.isNotEmpty()) {
|
||||
_isDefaultTunnel.value = settings.first().isTunnelConfigDefault(tunnelConfig)
|
||||
}
|
||||
}
|
||||
|
||||
private fun emitInterfaceFromConfig() {
|
||||
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
||||
_interface.value = InterfaceProxy.from(config.`interface`)
|
||||
}
|
||||
|
||||
private fun emitPeersFromConfig() {
|
||||
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
||||
config.peers.forEach {
|
||||
_proxyPeers.value.add(PeerProxy.from(it))
|
||||
}
|
||||
}
|
||||
|
||||
private fun emitPeerProxy(peerProxy: PeerProxy) {
|
||||
_proxyPeers.value.add(peerProxy)
|
||||
}
|
||||
|
||||
private fun emitInterfaceProxy(interfaceProxy: InterfaceProxy) {
|
||||
_interface.value = interfaceProxy
|
||||
}
|
||||
|
||||
private suspend fun getTunnelConfigById(id: String): TunnelConfig? {
|
||||
return try {
|
||||
tunnelRepo.getById(id.toLong())
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun emitTunnelConfig() {
|
||||
_tunnel.emit(tunnelConfig)
|
||||
}
|
||||
|
||||
private suspend fun emitTunnelConfigName() {
|
||||
_tunnelName.emit(tunnelConfig.name)
|
||||
}
|
||||
|
||||
fun onTunnelNameChange(name: String) {
|
||||
_tunnelName.value = name
|
||||
_uiState.value = _uiState.value.copy(tunnelName = name)
|
||||
}
|
||||
|
||||
fun onIncludeChange(include: Boolean) {
|
||||
_include.value = include
|
||||
_uiState.value = _uiState.value.copy(include = include)
|
||||
}
|
||||
|
||||
fun onAddCheckedPackage(packageName: String) {
|
||||
_checkedPackages.value.add(packageName)
|
||||
_uiState.value =
|
||||
_uiState.value.copy(
|
||||
checkedPackageNames = _uiState.value.checkedPackageNames + packageName
|
||||
)
|
||||
}
|
||||
|
||||
fun onAllApplicationsChange(isAllApplicationsEnabled: Boolean) {
|
||||
_isAllApplicationsEnabled.value = isAllApplicationsEnabled
|
||||
_uiState.value = _uiState.value.copy(isAllApplicationsEnabled = isAllApplicationsEnabled)
|
||||
}
|
||||
|
||||
fun onRemoveCheckedPackage(packageName: String) {
|
||||
_checkedPackages.value.remove(packageName)
|
||||
_uiState.value =
|
||||
_uiState.value.copy(
|
||||
checkedPackageNames = _uiState.value.checkedPackageNames - packageName
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun emitSplitTunnelConfiguration(config: Config) {
|
||||
val excludedApps = config.`interface`.excludedApplications
|
||||
val includedApps = config.`interface`.includedApplications
|
||||
if (excludedApps.isNotEmpty() || includedApps.isNotEmpty()) {
|
||||
emitTunnelAllApplicationsDisabled()
|
||||
determineAppInclusionState(excludedApps, includedApps)
|
||||
} else {
|
||||
emitTunnelAllApplicationsEnabled()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun determineAppInclusionState(
|
||||
excludedApps: Set<String>,
|
||||
includedApps: Set<String>
|
||||
) {
|
||||
if (excludedApps.isEmpty()) {
|
||||
emitIncludedAppsExist()
|
||||
emitCheckedApps(includedApps)
|
||||
} else {
|
||||
emitExcludedAppsExist()
|
||||
emitCheckedApps(excludedApps)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun emitIncludedAppsExist() {
|
||||
_include.emit(true)
|
||||
}
|
||||
|
||||
private suspend fun emitExcludedAppsExist() {
|
||||
_include.emit(false)
|
||||
}
|
||||
|
||||
private suspend fun emitCheckedApps(apps: Set<String>) {
|
||||
_checkedPackages.emit(apps.toMutableStateList())
|
||||
}
|
||||
|
||||
private suspend fun emitTunnelAllApplicationsEnabled() {
|
||||
_isAllApplicationsEnabled.emit(true)
|
||||
}
|
||||
|
||||
private suspend fun emitTunnelAllApplicationsDisabled() {
|
||||
_isAllApplicationsEnabled.emit(false)
|
||||
}
|
||||
|
||||
private fun emitCurrentPackageConfigurations() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
||||
emitSplitTunnelConfiguration(config)
|
||||
}
|
||||
}
|
||||
|
||||
fun emitQueriedPackages(query: String) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val packages =
|
||||
getAllInternetCapablePackages().filter {
|
||||
getPackageLabel(it).lowercase().contains(query.lowercase())
|
||||
}
|
||||
_packages.emit(packages)
|
||||
private fun getQueriedPackages(query: String): List<PackageInfo> {
|
||||
return getAllInternetCapablePackages().filter {
|
||||
getPackageLabel(it).lowercase().contains(query.lowercase())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,7 +133,7 @@ constructor(
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
packageManager.getPackagesHoldingPermissions(
|
||||
permissions,
|
||||
PackageManager.PackageInfoFlags.of(0L)
|
||||
PackageManager.PackageInfoFlags.of(0L),
|
||||
)
|
||||
} else {
|
||||
packageManager.getPackagesHoldingPermissions(permissions, 0)
|
||||
@@ -241,193 +141,200 @@ constructor(
|
||||
}
|
||||
|
||||
private fun isAllApplicationsEnabled(): Boolean {
|
||||
return _isAllApplicationsEnabled.value
|
||||
return _uiState.value.isAllApplicationsEnabled
|
||||
}
|
||||
|
||||
private suspend fun saveConfig(tunnelConfig: TunnelConfig) {
|
||||
tunnelRepo.save(tunnelConfig)
|
||||
}
|
||||
private fun saveConfig(tunnelConfig: TunnelConfig) =
|
||||
viewModelScope.launch { tunnelConfigRepository.save(tunnelConfig) }
|
||||
|
||||
private suspend fun updateTunnelConfig(tunnelConfig: TunnelConfig?) {
|
||||
if (tunnelConfig != null) {
|
||||
saveConfig(tunnelConfig)
|
||||
updateSettingsDefaultTunnel(tunnelConfig)
|
||||
private fun updateTunnelConfig(tunnelConfig: TunnelConfig?) =
|
||||
viewModelScope.launch {
|
||||
if (tunnelConfig != null) {
|
||||
saveConfig(tunnelConfig).join()
|
||||
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
||||
updateSettingsDefaultTunnel(tunnelConfig)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateSettingsDefaultTunnel(tunnelConfig: TunnelConfig) {
|
||||
val settings = settingsRepo.getAll()
|
||||
if (settings.isNotEmpty()) {
|
||||
val setting = settings[0]
|
||||
if (setting.defaultTunnel != null) {
|
||||
if (tunnelConfig.id == TunnelConfig.from(setting.defaultTunnel!!).id) {
|
||||
settingsRepo.save(
|
||||
setting.copy(
|
||||
defaultTunnel = tunnelConfig.toString()
|
||||
)
|
||||
)
|
||||
}
|
||||
val settings = settingsRepository.getSettingsFlow().first()
|
||||
if (settings.defaultTunnel != null) {
|
||||
if (tunnelConfig.id == TunnelConfig.from(settings.defaultTunnel!!).id) {
|
||||
settingsRepository.save(settings.copy(defaultTunnel = tunnelConfig.toString()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildPeerListFromProxyPeers(): List<Peer> {
|
||||
return _proxyPeers.value.map {
|
||||
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.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(_interface.value.privateKey.trim())
|
||||
builder.parseAddresses(_interface.value.addresses.trim())
|
||||
builder.parseDnsServers(_interface.value.dnsServers.trim())
|
||||
if (_interface.value.mtu.isNotEmpty()) builder.parseMtu(_interface.value.mtu.trim())
|
||||
if (_interface.value.listenPort.isNotEmpty()) {
|
||||
builder.parseListenPort(
|
||||
_interface.value.listenPort.trim()
|
||||
)
|
||||
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 (isAllApplicationsEnabled()) _checkedPackages.value.clear()
|
||||
if (_include.value) builder.includeApplications(_checkedPackages.value)
|
||||
if (!_include.value) builder.excludeApplications(_checkedPackages.value)
|
||||
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()
|
||||
}
|
||||
|
||||
suspend fun onSaveAllChanges() {
|
||||
try {
|
||||
fun onSaveAllChanges(): Result<Event> {
|
||||
return try {
|
||||
val peerList = buildPeerListFromProxyPeers()
|
||||
val wgInterface = buildInterfaceListFromProxyInterface()
|
||||
val config = Config.Builder().addPeers(peerList).setInterface(wgInterface).build()
|
||||
val tunnelConfig =
|
||||
_tunnel.value?.copy(
|
||||
name = _tunnelName.value,
|
||||
wgQuick = config.toWgQuickString()
|
||||
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) {
|
||||
throw WgTunnelException(
|
||||
"Error: ${e.cause?.message?.lowercase() ?: "unknown error occurred"}"
|
||||
)
|
||||
Result.Error(Event.Error.Exception(e))
|
||||
}
|
||||
}
|
||||
|
||||
fun onPeerPublicKeyChange(
|
||||
index: Int,
|
||||
publicKey: String
|
||||
) {
|
||||
_proxyPeers.value[index] =
|
||||
_proxyPeers.value[index].copy(
|
||||
publicKey = publicKey
|
||||
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
|
||||
) {
|
||||
_proxyPeers.value[index] =
|
||||
_proxyPeers.value[index].copy(
|
||||
preSharedKey = 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
|
||||
) {
|
||||
_proxyPeers.value[index] =
|
||||
_proxyPeers.value[index].copy(
|
||||
endpoint = 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
|
||||
) {
|
||||
_proxyPeers.value[index] =
|
||||
_proxyPeers.value[index].copy(
|
||||
allowedIps = 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
|
||||
) {
|
||||
_proxyPeers.value[index] =
|
||||
_proxyPeers.value[index].copy(
|
||||
persistentKeepalive = 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) {
|
||||
proxyPeers.value.removeAt(index)
|
||||
_uiState.value =
|
||||
_uiState.value.copy(
|
||||
proxyPeers = _uiState.value.proxyPeers.removeAt(index),
|
||||
)
|
||||
}
|
||||
|
||||
fun addEmptyPeer() {
|
||||
_proxyPeers.value.add(PeerProxy())
|
||||
_uiState.value = _uiState.value.copy(proxyPeers = _uiState.value.proxyPeers + PeerProxy())
|
||||
}
|
||||
|
||||
fun generateKeyPair() {
|
||||
val keyPair = KeyPair()
|
||||
_interface.value =
|
||||
_interface.value.copy(
|
||||
privateKey = keyPair.privateKey.toBase64(),
|
||||
publicKey = keyPair.publicKey.toBase64()
|
||||
_uiState.value =
|
||||
_uiState.value.copy(
|
||||
interfaceProxy =
|
||||
_uiState.value.interfaceProxy.copy(
|
||||
privateKey = keyPair.privateKey.toBase64(),
|
||||
publicKey = keyPair.publicKey.toBase64(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun onAddressesChanged(value: String) {
|
||||
_interface.value =
|
||||
_interface.value.copy(
|
||||
addresses = value
|
||||
_uiState.value =
|
||||
_uiState.value.copy(
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(addresses = value)
|
||||
)
|
||||
}
|
||||
|
||||
fun onListenPortChanged(value: String) {
|
||||
_interface.value =
|
||||
_interface.value.copy(
|
||||
listenPort = value
|
||||
_uiState.value =
|
||||
_uiState.value.copy(
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(listenPort = value)
|
||||
)
|
||||
}
|
||||
|
||||
fun onDnsServersChanged(value: String) {
|
||||
_interface.value =
|
||||
_interface.value.copy(
|
||||
dnsServers = value
|
||||
_uiState.value =
|
||||
_uiState.value.copy(
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(dnsServers = value)
|
||||
)
|
||||
}
|
||||
|
||||
fun onMtuChanged(value: String) {
|
||||
_interface.value =
|
||||
_interface.value.copy(
|
||||
mtu = value
|
||||
)
|
||||
_uiState.value =
|
||||
_uiState.value.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(mtu = value))
|
||||
}
|
||||
|
||||
private fun onInterfacePublicKeyChange(value: String) {
|
||||
_interface.value =
|
||||
_interface.value.copy(
|
||||
publicKey = value
|
||||
_uiState.value =
|
||||
_uiState.value.copy(
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(publicKey = value)
|
||||
)
|
||||
}
|
||||
|
||||
fun onPrivateKeyChange(value: String) {
|
||||
_interface.value =
|
||||
_interface.value.copy(
|
||||
privateKey = value
|
||||
_uiState.value =
|
||||
_uiState.value.copy(
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(privateKey = value)
|
||||
)
|
||||
if (NumberUtils.isValidKey(value)) {
|
||||
val pair = KeyPair(Key.fromBase64(value))
|
||||
@@ -436,4 +343,12 @@ constructor(
|
||||
onInterfacePublicKeyChange("")
|
||||
}
|
||||
}
|
||||
|
||||
fun emitQueriedPackages(query: String) {
|
||||
val packages =
|
||||
getAllInternetCapablePackages().filter {
|
||||
getPackageLabel(it).lowercase().contains(query.lowercase())
|
||||
}
|
||||
_uiState.value = _uiState.value.copy(packages = packages)
|
||||
}
|
||||
}
|
||||
|
||||
+424
-332
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,12 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.main
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
|
||||
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
||||
|
||||
data class MainUiState(
|
||||
val settings: Settings = Settings(),
|
||||
val tunnels: TunnelConfigs = emptyList(),
|
||||
val vpnState: VpnState = VpnState(),
|
||||
val loading: Boolean = true
|
||||
)
|
||||
+142
-149
@@ -8,162 +8,157 @@ import android.provider.OpenableColumns
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.wireguard.config.Config
|
||||
import com.zaneschepke.wireguardautotunnel.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
|
||||
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
||||
import com.zaneschepke.wireguardautotunnel.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.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.Event
|
||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
|
||||
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.io.InputStream
|
||||
import java.util.zip.ZipInputStream
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@HiltViewModel
|
||||
class MainViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val application: Application,
|
||||
private val tunnelRepo: TunnelConfigDao,
|
||||
private val settingsRepo: SettingsDoa,
|
||||
private val tunnelConfigRepository: TunnelConfigRepository,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val vpnService: VpnService
|
||||
) : ViewModel() {
|
||||
val tunnels get() = tunnelRepo.getAllFlow()
|
||||
val state get() = vpnService.state
|
||||
|
||||
val handshakeStatus get() = vpnService.handshakeStatus
|
||||
val tunnelName get() = vpnService.tunnelName
|
||||
private val _settings = MutableStateFlow(Settings())
|
||||
val settings get() = _settings.asStateFlow()
|
||||
val statistics get() = vpnService.statistics
|
||||
|
||||
init {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
settingsRepo.getAllFlow().filter { it.isNotEmpty() }.collect {
|
||||
val settings = it.first()
|
||||
val uiState =
|
||||
combine(
|
||||
settingsRepository.getSettingsFlow(),
|
||||
tunnelConfigRepository.getTunnelConfigsFlow(),
|
||||
vpnService.vpnState,
|
||||
) { settings, tunnels, vpnState ->
|
||||
validateWatcherServiceState(settings)
|
||||
_settings.emit(settings)
|
||||
MainUiState(settings, tunnels, vpnState, false)
|
||||
}
|
||||
.stateIn(
|
||||
viewModelScope,
|
||||
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
|
||||
MainUiState(),
|
||||
)
|
||||
|
||||
private fun validateWatcherServiceState(settings: Settings) =
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
if (settings.isAutoTunnelEnabled) {
|
||||
ServiceManager.startWatcherService(application.applicationContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateWatcherServiceState(settings: Settings) {
|
||||
val watcherState =
|
||||
ServiceManager.getServiceState(
|
||||
application.applicationContext,
|
||||
WireGuardConnectivityWatcherService::class.java
|
||||
)
|
||||
if (settings.isAutoTunnelEnabled && watcherState == ServiceState.STOPPED && settings.defaultTunnel != null) {
|
||||
ServiceManager.startWatcherService(
|
||||
application.applicationContext,
|
||||
settings.defaultTunnel!!
|
||||
)
|
||||
private fun stopWatcherService() =
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
ServiceManager.stopWatcherService(application.applicationContext)
|
||||
}
|
||||
}
|
||||
|
||||
fun onDelete(tunnel: TunnelConfig) {
|
||||
viewModelScope.launch {
|
||||
if (tunnelRepo.count() == 1L) {
|
||||
ServiceManager.stopWatcherService(application.applicationContext)
|
||||
val settings = settingsRepo.getAll()
|
||||
if (settings.isNotEmpty()) {
|
||||
val setting = settings[0]
|
||||
setting.defaultTunnel = null
|
||||
setting.isAutoTunnelEnabled = false
|
||||
setting.isAlwaysOnVpnEnabled = false
|
||||
settingsRepo.save(setting)
|
||||
}
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
if (tunnelConfigRepository.count() == 1) {
|
||||
stopWatcherService()
|
||||
val settings = settingsRepository.getSettings()
|
||||
settings.defaultTunnel = null
|
||||
settings.isAutoTunnelEnabled = false
|
||||
settings.isAlwaysOnVpnEnabled = false
|
||||
saveSettings(settings)
|
||||
}
|
||||
tunnelRepo.delete(tunnel)
|
||||
tunnelConfigRepository.delete(tunnel)
|
||||
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
fun onTunnelStart(tunnelConfig: TunnelConfig) {
|
||||
viewModelScope.launch {
|
||||
stopActiveTunnel()
|
||||
fun onTunnelStart(tunnelConfig: TunnelConfig) =
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
Timber.d("On start called!")
|
||||
stopActiveTunnel().await()
|
||||
startTunnel(tunnelConfig)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startTunnel(tunnelConfig: TunnelConfig) {
|
||||
ServiceManager.startVpnService(application.applicationContext, tunnelConfig.toString())
|
||||
}
|
||||
private fun startTunnel(tunnelConfig: TunnelConfig) =
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
Timber.d("Start tunnel via manager")
|
||||
ServiceManager.startVpnService(application.applicationContext, tunnelConfig.toString())
|
||||
}
|
||||
|
||||
private suspend fun stopActiveTunnel() {
|
||||
if (ServiceManager.getServiceState(
|
||||
application.applicationContext,
|
||||
WireGuardTunnelService::class.java
|
||||
) == ServiceState.STARTED
|
||||
) {
|
||||
private fun stopActiveTunnel() =
|
||||
viewModelScope.async(Dispatchers.IO) {
|
||||
onTunnelStop()
|
||||
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
||||
}
|
||||
}
|
||||
|
||||
fun onTunnelStop() {
|
||||
ServiceManager.stopVpnService(application.applicationContext)
|
||||
}
|
||||
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) {
|
||||
try {
|
||||
suspend fun onTunnelQrResult(result: String): Result<Unit> {
|
||||
return try {
|
||||
validateConfigString(result)
|
||||
val tunnelConfig =
|
||||
TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = result)
|
||||
addTunnel(tunnelConfig)
|
||||
Result.Success(Unit)
|
||||
} catch (e: Exception) {
|
||||
throw WgTunnelException(e)
|
||||
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 config = Config.parse(bufferReader)
|
||||
val tunnelName = getNameFromFileName(fileName)
|
||||
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)
|
||||
?: throw WgTunnelException(application.getString(R.string.stream_failed))
|
||||
}
|
||||
|
||||
suspend fun onTunnelFileSelected(uri: Uri) {
|
||||
suspend fun onTunnelFileSelected(uri: Uri): Result<Unit> {
|
||||
try {
|
||||
val fileName = getFileName(application.applicationContext, uri)
|
||||
val fileExtension = getFileExtensionFromFileName(fileName)
|
||||
when (fileExtension) {
|
||||
Constants.CONF_FILE_EXTENSION -> saveTunnelFromConfUri(fileName, uri)
|
||||
Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri)
|
||||
else -> throw WgTunnelException(
|
||||
application.getString(R.string.file_extension_message)
|
||||
)
|
||||
if (isValidUriContentScheme(uri)) {
|
||||
val fileName = getFileName(application.applicationContext, uri)
|
||||
when (getFileExtensionFromFileName(fileName)) {
|
||||
Constants.CONF_FILE_EXTENSION ->
|
||||
saveTunnelFromConfUri(fileName, uri).let {
|
||||
when (it) {
|
||||
is Result.Error -> return Result.Error(Event.Error.FileReadFailed)
|
||||
is Result.Success -> return it
|
||||
}
|
||||
}
|
||||
Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri)
|
||||
else -> return Result.Error(Event.Error.InvalidFileExtension)
|
||||
}
|
||||
return Result.Success(Unit)
|
||||
} else {
|
||||
return Result.Error(Event.Error.InvalidFileExtension)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
throw WgTunnelException(e)
|
||||
return Result.Error(Event.Error.FileReadFailed)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,7 +167,7 @@ constructor(
|
||||
generateSequence { zip.nextEntry }
|
||||
.filterNot {
|
||||
it.isDirectory ||
|
||||
getFileExtensionFromFileName(it.name) != Constants.CONF_FILE_EXTENSION
|
||||
getFileExtensionFromFileName(it.name) != Constants.CONF_FILE_EXTENSION
|
||||
}
|
||||
.forEach {
|
||||
val name = getNameFromFileName(it.name)
|
||||
@@ -184,69 +179,66 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun saveTunnelFromConfUri(
|
||||
name: String,
|
||||
uri: Uri
|
||||
) {
|
||||
private suspend fun saveTunnelFromConfUri(name: String, uri: Uri): Result<Unit> {
|
||||
val stream = getInputStreamFromUri(uri)
|
||||
saveTunnelConfigFromStream(stream, name)
|
||||
return if (stream != null) {
|
||||
saveTunnelConfigFromStream(stream, name)
|
||||
Result.Success(Unit)
|
||||
} else {
|
||||
Result.Error(Event.Error.FileReadFailed)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun addTunnel(tunnelConfig: TunnelConfig) {
|
||||
saveTunnel(tunnelConfig)
|
||||
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
||||
}
|
||||
|
||||
fun pauseAutoTunneling() =
|
||||
viewModelScope.launch {
|
||||
settingsRepository.save(uiState.value.settings.copy(isAutoTunnelPaused = true))
|
||||
}
|
||||
|
||||
fun resumeAutoTunneling() =
|
||||
viewModelScope.launch {
|
||||
settingsRepository.save(uiState.value.settings.copy(isAutoTunnelPaused = false))
|
||||
}
|
||||
|
||||
private suspend fun saveTunnel(tunnelConfig: TunnelConfig) {
|
||||
tunnelRepo.save(tunnelConfig)
|
||||
tunnelConfigRepository.save(tunnelConfig)
|
||||
}
|
||||
|
||||
private fun getFileNameByCursor(
|
||||
context: Context,
|
||||
uri: Uri
|
||||
): String {
|
||||
val cursor = context.contentResolver.query(uri, null, null, null, null)
|
||||
if (cursor != null) {
|
||||
cursor.use {
|
||||
return getDisplayNameByCursor(it)
|
||||
}
|
||||
} else {
|
||||
throw WgTunnelException("Failed to initialize cursor")
|
||||
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 {
|
||||
private fun getDisplayNameColumnIndex(cursor: Cursor): Int? {
|
||||
val columnIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||
if (columnIndex == -1) {
|
||||
throw WgTunnelException("Cursor out of bounds")
|
||||
}
|
||||
return columnIndex
|
||||
}
|
||||
|
||||
private fun getDisplayNameByCursor(cursor: Cursor): String {
|
||||
if (cursor.moveToFirst()) {
|
||||
val index = getDisplayNameColumnIndex(cursor)
|
||||
return cursor.getString(index)
|
||||
return if (columnIndex != -1) {
|
||||
return columnIndex
|
||||
} else {
|
||||
throw WgTunnelException("Cursor failed to move to first")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateUriContentScheme(uri: Uri) {
|
||||
if (uri.scheme != Constants.URI_CONTENT_SCHEME) {
|
||||
throw WgTunnelException(application.getString(R.string.file_extension_message))
|
||||
}
|
||||
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 getFileName(
|
||||
context: Context,
|
||||
uri: Uri
|
||||
): String {
|
||||
validateUriContentScheme(uri)
|
||||
return try {
|
||||
getFileNameByCursor(context, uri)
|
||||
} catch (_: Exception) {
|
||||
NumberUtils.generateRandomTunnelName()
|
||||
}
|
||||
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()
|
||||
}
|
||||
|
||||
private fun getNameFromFileName(fileName: String): String {
|
||||
@@ -261,14 +253,15 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun onDefaultTunnelChange(selectedTunnel: TunnelConfig?) {
|
||||
if (selectedTunnel != null) {
|
||||
_settings.emit(
|
||||
_settings.value.copy(
|
||||
defaultTunnel = selectedTunnel.toString()
|
||||
)
|
||||
)
|
||||
settingsRepo.save(_settings.value)
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+330
-304
@@ -1,10 +1,17 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.settings
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.Context.POWER_SERVICE
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import android.provider.Settings
|
||||
import android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
@@ -13,7 +20,6 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
@@ -30,6 +36,7 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.outlined.Add
|
||||
import androidx.compose.material.icons.rounded.LocationOff
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -45,15 +52,13 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusProperties
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
@@ -70,235 +75,264 @@ import com.wireguard.android.backend.Tunnel
|
||||
import com.wireguard.android.backend.WgQuickBackend
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.repository.datastore.DataStoreManager
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
|
||||
import com.zaneschepke.wireguardautotunnel.util.Event
|
||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
|
||||
import java.io.File
|
||||
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
|
||||
@OptIn(
|
||||
ExperimentalPermissionsApi::class,
|
||||
ExperimentalLayoutApi::class,
|
||||
ExperimentalComposeUiApi::class
|
||||
)
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
viewModel: SettingsViewModel = hiltViewModel(),
|
||||
padding: PaddingValues,
|
||||
showSnackbarMessage: (String) -> Unit,
|
||||
focusRequester: FocusRequester
|
||||
) {
|
||||
val scope = rememberCoroutineScope { Dispatchers.IO }
|
||||
val context = LocalContext.current
|
||||
val focusManager = LocalFocusManager.current
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
val scrollState = rememberScrollState()
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
|
||||
val settings by viewModel.settings.collectAsStateWithLifecycle()
|
||||
val trustedSSIDs by viewModel.trustedSSIDs.collectAsStateWithLifecycle()
|
||||
val tunnels by viewModel.tunnels.collectAsStateWithLifecycle(mutableListOf())
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
val fineLocationState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
var currentText by remember { mutableStateOf("") }
|
||||
val scrollState = rememberScrollState()
|
||||
var isBackgroundLocationGranted by remember { mutableStateOf(true) }
|
||||
var showAuthPrompt by remember { mutableStateOf(false) }
|
||||
var showLocationServicesAlertDialog by remember { mutableStateOf(false) }
|
||||
var didExportFiles by remember { mutableStateOf(false) }
|
||||
val isLocationDisclosureShown by viewModel.disclosureShown.collectAsStateWithLifecycle(
|
||||
null
|
||||
)
|
||||
val vpnState = viewModel.vpnState.collectAsStateWithLifecycle(initialValue = Tunnel.State.DOWN)
|
||||
var showAuthPrompt by remember { mutableStateOf(false) }
|
||||
val focusRequester2 = remember { FocusRequester() }
|
||||
|
||||
val screenPadding = 5.dp
|
||||
val fillMaxWidth = .85f
|
||||
|
||||
fun setLocationDisclosureShown() = scope.launch {
|
||||
viewModel.dataStoreManager.saveToDataStore(
|
||||
DataStoreManager.LOCATION_DISCLOSURE_SHOWN,
|
||||
true
|
||||
)
|
||||
if (uiState.loading) {
|
||||
LoadingScreen()
|
||||
return
|
||||
}
|
||||
|
||||
val startForResult =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
result: ActivityResult ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
result.data
|
||||
// Handle the Intent
|
||||
}
|
||||
viewModel.setBatteryOptimizeDisableShown()
|
||||
}
|
||||
|
||||
fun exportAllConfigs() {
|
||||
try {
|
||||
val files = tunnels.map { File(context.cacheDir, "${it.name}.conf") }
|
||||
val files = uiState.tunnels.map { File(context.cacheDir, "${it.name}.conf") }
|
||||
files.forEachIndexed { index, file ->
|
||||
file.outputStream().use {
|
||||
it.write(tunnels[index].wgQuick.toByteArray())
|
||||
}
|
||||
file.outputStream().use { it.write(uiState.tunnels[index].wgQuick.toByteArray()) }
|
||||
}
|
||||
FileUtils.saveFilesToZip(context, files)
|
||||
didExportFiles = true
|
||||
showSnackbarMessage(context.getString(R.string.exported_configs_message))
|
||||
showSnackbarMessage(Event.Message.ConfigsExported.message)
|
||||
} catch (e: Exception) {
|
||||
showSnackbarMessage(e.message!!)
|
||||
showSnackbarMessage(Event.Error.Exception(e).message)
|
||||
}
|
||||
}
|
||||
|
||||
fun isBatteryOptimizationsDisabled(): Boolean {
|
||||
val pm = context.getSystemService(POWER_SERVICE) as PowerManager
|
||||
return pm.isIgnoringBatteryOptimizations(context.packageName)
|
||||
}
|
||||
|
||||
fun requestBatteryOptimizationsDisabled() {
|
||||
val intent =
|
||||
Intent().apply {
|
||||
this.action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
|
||||
data = Uri.fromParts("package", context.packageName, null)
|
||||
}
|
||||
startForResult.launch(intent)
|
||||
}
|
||||
|
||||
fun handleAutoTunnelToggle() {
|
||||
if (uiState.isBatteryOptimizeDisableShown || isBatteryOptimizationsDisabled()) {
|
||||
viewModel.toggleAutoTunnel()
|
||||
} else {
|
||||
requestBatteryOptimizationsDisabled()
|
||||
}
|
||||
}
|
||||
|
||||
fun saveTrustedSSID() {
|
||||
if (currentText.isNotEmpty()) {
|
||||
scope.launch {
|
||||
try {
|
||||
viewModel.onSaveTrustedSSID(currentText)
|
||||
currentText = ""
|
||||
} catch (e: Exception) {
|
||||
showSnackbarMessage(e.message ?: context.getString(R.string.unknown_error))
|
||||
viewModel.onSaveTrustedSSID(currentText).let {
|
||||
when (it) {
|
||||
is Result.Success -> currentText = ""
|
||||
is Result.Error -> showSnackbarMessage(it.error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun isAllAutoTunnelPermissionsEnabled(): Boolean {
|
||||
return (
|
||||
isBackgroundLocationGranted &&
|
||||
fineLocationState.status.isGranted &&
|
||||
!viewModel.isLocationServicesNeeded()
|
||||
)
|
||||
}
|
||||
|
||||
fun openSettings() {
|
||||
scope.launch {
|
||||
val intentSettings =
|
||||
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||
intentSettings.data =
|
||||
Uri.fromParts("package", context.packageName, null)
|
||||
val intentSettings = Intent(ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||
intentSettings.data = Uri.fromParts("package", context.packageName, null)
|
||||
context.startActivity(intentSettings)
|
||||
}
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val backgroundLocationState =
|
||||
rememberPermissionState(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
|
||||
isBackgroundLocationGranted = if (!backgroundLocationState.status.isGranted) {
|
||||
false
|
||||
} else {
|
||||
SideEffect {
|
||||
setLocationDisclosureShown()
|
||||
fun checkFineLocationGranted() {
|
||||
isBackgroundLocationGranted =
|
||||
if (!fineLocationState.status.isGranted) {
|
||||
false
|
||||
} else {
|
||||
viewModel.setLocationDisclosureShown()
|
||||
true
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
if (
|
||||
WireGuardAutoTunnel.isRunningOnAndroidTv() &&
|
||||
Build.VERSION.SDK_INT == Build.VERSION_CODES.Q
|
||||
) {
|
||||
checkFineLocationGranted()
|
||||
} else {
|
||||
val backgroundLocationState =
|
||||
rememberPermissionState(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
|
||||
isBackgroundLocationGranted =
|
||||
if (!backgroundLocationState.status.isGranted) {
|
||||
false
|
||||
} else {
|
||||
SideEffect { viewModel.setLocationDisclosureShown() }
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
|
||||
if (!fineLocationState.status.isGranted) {
|
||||
isBackgroundLocationGranted = false
|
||||
} else {
|
||||
SideEffect {
|
||||
setLocationDisclosureShown()
|
||||
checkFineLocationGranted()
|
||||
}
|
||||
|
||||
AnimatedVisibility(showLocationServicesAlertDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showLocationServicesAlertDialog = false },
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
showLocationServicesAlertDialog = false
|
||||
handleAutoTunnelToggle()
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(R.string.okay))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showLocationServicesAlertDialog = false }) {
|
||||
Text(text = stringResource(R.string.cancel))
|
||||
}
|
||||
},
|
||||
title = { Text(text = stringResource(R.string.location_services_not_detected)) },
|
||||
text = { Text(text = stringResource(R.string.location_services_missing_message)) },
|
||||
)
|
||||
}
|
||||
|
||||
if (!uiState.isLocationDisclosureShown) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier = Modifier.fillMaxSize().verticalScroll(scrollState),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Rounded.LocationOff,
|
||||
contentDescription = stringResource(id = R.string.map),
|
||||
modifier = Modifier.padding(30.dp).size(128.dp),
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.prominent_background_location_title),
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(30.dp),
|
||||
fontSize = 20.sp,
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.prominent_background_location_message),
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(30.dp),
|
||||
fontSize = 15.sp,
|
||||
)
|
||||
Row(
|
||||
modifier =
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
Modifier.fillMaxWidth().padding(10.dp)
|
||||
} else {
|
||||
Modifier.fillMaxWidth().padding(30.dp)
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
) {
|
||||
TextButton(onClick = { viewModel.setLocationDisclosureShown() }) {
|
||||
Text(stringResource(id = R.string.no_thanks))
|
||||
}
|
||||
TextButton(
|
||||
modifier = Modifier.focusRequester(focusRequester),
|
||||
onClick = {
|
||||
openSettings()
|
||||
viewModel.setLocationDisclosureShown()
|
||||
},
|
||||
) {
|
||||
Text(stringResource(id = R.string.turn_on))
|
||||
}
|
||||
}
|
||||
isBackgroundLocationGranted = true
|
||||
}
|
||||
}
|
||||
|
||||
if (isLocationDisclosureShown != true) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(scrollState)
|
||||
.padding(padding)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Rounded.LocationOff,
|
||||
contentDescription = stringResource(id = R.string.map),
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(30.dp)
|
||||
.size(128.dp)
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.prominent_background_location_title),
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(30.dp),
|
||||
fontSize = 20.sp
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.prominent_background_location_message),
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(30.dp),
|
||||
fontSize = 15.sp
|
||||
)
|
||||
Row(
|
||||
modifier =
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(10.dp)
|
||||
} else {
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(30.dp)
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
TextButton(onClick = {
|
||||
setLocationDisclosureShown()
|
||||
}) {
|
||||
Text(stringResource(id = R.string.no_thanks))
|
||||
}
|
||||
TextButton(modifier = Modifier.focusRequester(focusRequester), onClick = {
|
||||
openSettings()
|
||||
setLocationDisclosureShown()
|
||||
}) {
|
||||
Text(stringResource(id = R.string.turn_on))
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
if (showAuthPrompt) {
|
||||
AuthorizationPrompt(
|
||||
onSuccess = {
|
||||
showAuthPrompt = false
|
||||
exportAllConfigs()
|
||||
},
|
||||
onError = { _ ->
|
||||
showAuthPrompt = false
|
||||
showSnackbarMessage(Event.Error.AuthenticationFailed.message)
|
||||
},
|
||||
onFailure = {
|
||||
showAuthPrompt = false
|
||||
showSnackbarMessage(Event.Error.AuthorizationFailed.message)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (showAuthPrompt) {
|
||||
AuthorizationPrompt(
|
||||
onSuccess = {
|
||||
showAuthPrompt = false
|
||||
exportAllConfigs()
|
||||
},
|
||||
onError = { error ->
|
||||
showSnackbarMessage(error)
|
||||
showAuthPrompt = false
|
||||
},
|
||||
onFailure = {
|
||||
showAuthPrompt = false
|
||||
showSnackbarMessage(context.getString(R.string.authentication_failed))
|
||||
}
|
||||
if (uiState.tunnels.isEmpty() && uiState.isLocationDisclosureShown) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.one_tunnel_required),
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(15.dp),
|
||||
fontStyle = FontStyle.Italic,
|
||||
)
|
||||
}
|
||||
|
||||
if (tunnels.isEmpty()) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.one_tunnel_required),
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(15.dp),
|
||||
fontStyle = FontStyle.Italic
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
if (uiState.isLocationDisclosureShown && uiState.tunnels.isNotEmpty()) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(scrollState)
|
||||
.clickable(indication = null, interactionSource = interactionSource) {
|
||||
Modifier.fillMaxSize().verticalScroll(scrollState).clickable(
|
||||
indication = null,
|
||||
interactionSource = interactionSource,
|
||||
) {
|
||||
focusManager.clearFocus()
|
||||
}
|
||||
},
|
||||
) {
|
||||
Surface(
|
||||
tonalElevation = 2.dp,
|
||||
@@ -306,179 +340,185 @@ fun SettingsScreen(
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
modifier =
|
||||
(
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||
Modifier
|
||||
.height(IntrinsicSize.Min)
|
||||
(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)
|
||||
Modifier.fillMaxWidth(fillMaxWidth).padding(top = 20.dp)
|
||||
})
|
||||
.padding(bottom = 10.dp),
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier = Modifier.padding(15.dp)
|
||||
modifier = Modifier.padding(15.dp),
|
||||
) {
|
||||
SectionTitle(
|
||||
title = stringResource(id = R.string.auto_tunneling),
|
||||
padding = screenPadding
|
||||
padding = screenPadding,
|
||||
)
|
||||
ConfigurationToggle(
|
||||
stringResource(id = R.string.tunnel_on_wifi),
|
||||
enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled),
|
||||
checked = settings.isTunnelOnWifiEnabled,
|
||||
enabled =
|
||||
!(uiState.settings.isAutoTunnelEnabled ||
|
||||
uiState.settings.isAlwaysOnVpnEnabled),
|
||||
checked = uiState.settings.isTunnelOnWifiEnabled,
|
||||
padding = screenPadding,
|
||||
onCheckChanged = {
|
||||
scope.launch {
|
||||
viewModel.onToggleTunnelOnWifi()
|
||||
}
|
||||
},
|
||||
modifier = Modifier.focusRequester(focusRequester)
|
||||
onCheckChanged = { viewModel.onToggleTunnelOnWifi() },
|
||||
modifier =
|
||||
if (uiState.settings.isAutoTunnelEnabled) Modifier
|
||||
else
|
||||
Modifier.focusRequester(focusRequester).focusProperties {
|
||||
down = focusRequester2
|
||||
},
|
||||
)
|
||||
AnimatedVisibility(visible = settings.isTunnelOnWifiEnabled) {
|
||||
AnimatedVisibility(visible = uiState.settings.isTunnelOnWifiEnabled) {
|
||||
Column {
|
||||
FlowRow(
|
||||
modifier = Modifier
|
||||
.padding(screenPadding)
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(5.dp)
|
||||
modifier = Modifier.padding(screenPadding).fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(5.dp),
|
||||
) {
|
||||
trustedSSIDs.forEach { ssid ->
|
||||
uiState.settings.trustedNetworkSSIDs.forEach { ssid ->
|
||||
ClickableIconButton(
|
||||
onIconClick = {
|
||||
scope.launch {
|
||||
onClick = {
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
viewModel.onDeleteTrustedSSID(ssid)
|
||||
focusRequester2.requestFocus()
|
||||
}
|
||||
},
|
||||
onIconClick = { viewModel.onDeleteTrustedSSID(ssid) },
|
||||
text = ssid,
|
||||
icon = Icons.Filled.Close,
|
||||
enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled)
|
||||
enabled =
|
||||
!(uiState.settings.isAutoTunnelEnabled ||
|
||||
uiState.settings.isAlwaysOnVpnEnabled),
|
||||
)
|
||||
}
|
||||
if (trustedSSIDs.isEmpty()) {
|
||||
if (uiState.settings.trustedNetworkSSIDs.isEmpty()) {
|
||||
Text(
|
||||
stringResource(R.string.none),
|
||||
fontStyle = FontStyle.Italic,
|
||||
color = Color.Gray
|
||||
color = Color.Gray,
|
||||
)
|
||||
}
|
||||
}
|
||||
OutlinedTextField(
|
||||
enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled),
|
||||
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)
|
||||
.onFocusChanged {
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||
keyboardController?.hide()
|
||||
}
|
||||
},
|
||||
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()
|
||||
}
|
||||
),
|
||||
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
|
||||
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 = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled),
|
||||
checked = settings.isTunnelOnMobileDataEnabled,
|
||||
enabled =
|
||||
!(uiState.settings.isAutoTunnelEnabled ||
|
||||
uiState.settings.isAlwaysOnVpnEnabled),
|
||||
checked = uiState.settings.isTunnelOnMobileDataEnabled,
|
||||
padding = screenPadding,
|
||||
onCheckChanged = {
|
||||
scope.launch {
|
||||
viewModel.onToggleTunnelOnMobileData()
|
||||
}
|
||||
}
|
||||
onCheckChanged = { viewModel.onToggleTunnelOnMobileData() },
|
||||
)
|
||||
ConfigurationToggle(
|
||||
stringResource(id = R.string.tunnel_on_ethernet),
|
||||
enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled),
|
||||
checked = settings.isTunnelOnEthernetEnabled,
|
||||
enabled =
|
||||
!(uiState.settings.isAutoTunnelEnabled ||
|
||||
uiState.settings.isAlwaysOnVpnEnabled),
|
||||
checked = uiState.settings.isTunnelOnEthernetEnabled,
|
||||
padding = screenPadding,
|
||||
onCheckChanged = {
|
||||
scope.launch {
|
||||
viewModel.onToggleTunnelOnEthernet()
|
||||
}
|
||||
}
|
||||
onCheckChanged = { viewModel.onToggleTunnelOnEthernet() },
|
||||
)
|
||||
ConfigurationToggle(
|
||||
stringResource(R.string.battery_saver),
|
||||
enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled),
|
||||
checked = settings.isBatterySaverEnabled,
|
||||
enabled =
|
||||
!(uiState.settings.isAutoTunnelEnabled ||
|
||||
uiState.settings.isAlwaysOnVpnEnabled),
|
||||
checked = uiState.settings.isBatterySaverEnabled,
|
||||
padding = screenPadding,
|
||||
onCheckChanged = {
|
||||
scope.launch {
|
||||
viewModel.onToggleBatterySaver()
|
||||
}
|
||||
}
|
||||
onCheckChanged = { viewModel.onToggleBatterySaver() },
|
||||
)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(top = 5.dp),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
(if (!uiState.settings.isAutoTunnelEnabled) Modifier
|
||||
else
|
||||
Modifier.focusRequester(
|
||||
focusRequester,
|
||||
))
|
||||
.fillMaxSize()
|
||||
.padding(top = 5.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
TextButton(
|
||||
enabled = !settings.isAlwaysOnVpnEnabled,
|
||||
enabled = !uiState.settings.isAlwaysOnVpnEnabled,
|
||||
onClick = {
|
||||
if (!isAllAutoTunnelPermissionsEnabled() && settings.isTunnelOnWifiEnabled) {
|
||||
val message =
|
||||
if (!isBackgroundLocationGranted) {
|
||||
context.getString(R.string.background_location_required)
|
||||
} else if (viewModel.isLocationServicesNeeded()) {
|
||||
context.getString(R.string.location_services_required)
|
||||
} else {
|
||||
context.getString(R.string.precise_location_required)
|
||||
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()
|
||||
}
|
||||
showSnackbarMessage(message)
|
||||
} else {
|
||||
scope.launch {
|
||||
viewModel.toggleAutoTunnel()
|
||||
}
|
||||
} else {
|
||||
handleAutoTunnelToggle()
|
||||
}
|
||||
}
|
||||
},
|
||||
) {
|
||||
val autoTunnelButtonText =
|
||||
if (settings.isAutoTunnelEnabled) {
|
||||
if (uiState.settings.isAutoTunnelEnabled) {
|
||||
stringResource(R.string.disable_auto_tunnel)
|
||||
} else {
|
||||
stringResource(id = R.string.enable_auto_tunnel)
|
||||
@@ -494,94 +534,79 @@ fun SettingsScreen(
|
||||
shadowElevation = 2.dp,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
.padding(vertical = 10.dp)
|
||||
modifier = Modifier.fillMaxWidth(fillMaxWidth).padding(vertical = 10.dp),
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier = Modifier.padding(15.dp)
|
||||
modifier = Modifier.padding(15.dp),
|
||||
) {
|
||||
SectionTitle(
|
||||
title = stringResource(id = R.string.kernel),
|
||||
padding = screenPadding
|
||||
padding = screenPadding,
|
||||
)
|
||||
ConfigurationToggle(
|
||||
stringResource(R.string.use_kernel),
|
||||
enabled = !(
|
||||
settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled ||
|
||||
(vpnState.value == Tunnel.State.UP)
|
||||
),
|
||||
checked = settings.isKernelEnabled,
|
||||
enabled =
|
||||
!(uiState.settings.isAutoTunnelEnabled ||
|
||||
uiState.settings.isAlwaysOnVpnEnabled ||
|
||||
(uiState.vpnState.status == Tunnel.State.UP)),
|
||||
checked = uiState.settings.isKernelEnabled,
|
||||
padding = screenPadding,
|
||||
onCheckChanged = {
|
||||
scope.launch {
|
||||
try {
|
||||
viewModel.onToggleKernelMode()
|
||||
} catch (e: WgTunnelException) {
|
||||
showSnackbarMessage(e.message)
|
||||
viewModel.onToggleKernelMode().let {
|
||||
when (it) {
|
||||
is Result.Error -> showSnackbarMessage(it.error.message)
|
||||
is Result.Success -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
Surface(
|
||||
tonalElevation = 2.dp,
|
||||
shadowElevation = 2.dp,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(fillMaxWidth).padding(vertical = 10.dp)
|
||||
.padding(bottom = 140.dp)
|
||||
modifier =
|
||||
Modifier.fillMaxWidth(fillMaxWidth)
|
||||
.padding(vertical = 10.dp)
|
||||
.padding(bottom = 140.dp),
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier = Modifier.padding(15.dp)
|
||||
modifier = Modifier.padding(15.dp),
|
||||
) {
|
||||
SectionTitle(
|
||||
title = stringResource(id = R.string.other),
|
||||
padding = screenPadding
|
||||
padding = screenPadding,
|
||||
)
|
||||
ConfigurationToggle(
|
||||
stringResource(R.string.always_on_vpn_support),
|
||||
enabled = !settings.isAutoTunnelEnabled,
|
||||
checked = settings.isAlwaysOnVpnEnabled,
|
||||
enabled = !uiState.settings.isAutoTunnelEnabled,
|
||||
checked = uiState.settings.isAlwaysOnVpnEnabled,
|
||||
padding = screenPadding,
|
||||
onCheckChanged = {
|
||||
scope.launch {
|
||||
viewModel.onToggleAlwaysOnVPN()
|
||||
}
|
||||
}
|
||||
onCheckChanged = { viewModel.onToggleAlwaysOnVPN() },
|
||||
)
|
||||
ConfigurationToggle(
|
||||
stringResource(R.string.enabled_app_shortcuts),
|
||||
enabled = true,
|
||||
checked = settings.isShortcutsEnabled,
|
||||
checked = uiState.settings.isShortcutsEnabled,
|
||||
padding = screenPadding,
|
||||
onCheckChanged = {
|
||||
scope.launch {
|
||||
viewModel.onToggleShortcutsEnabled()
|
||||
}
|
||||
}
|
||||
onCheckChanged = { viewModel.onToggleShortcutsEnabled() },
|
||||
)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(top = 5.dp),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
modifier = Modifier.fillMaxSize().padding(top = 5.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
TextButton(
|
||||
enabled = !didExportFiles,
|
||||
onClick = {
|
||||
showAuthPrompt = true
|
||||
}
|
||||
onClick = { showAuthPrompt = true },
|
||||
) {
|
||||
Text(stringResource(R.string.export_configs))
|
||||
}
|
||||
@@ -589,8 +614,9 @@ fun SettingsScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
Spacer(modifier = Modifier.weight(.17f))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.settings
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
|
||||
|
||||
data class SettingsUiState(
|
||||
val settings: Settings = Settings(),
|
||||
val tunnels: List<TunnelConfig> = emptyList(),
|
||||
val vpnState: VpnState = VpnState(),
|
||||
val isLocationDisclosureShown: Boolean = true,
|
||||
val isBatteryOptimizeDisableShown: Boolean = false,
|
||||
val loading: Boolean = true
|
||||
)
|
||||
+131
-131
@@ -3,195 +3,195 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.location.LocationManager
|
||||
import android.os.Build
|
||||
import androidx.core.location.LocationManagerCompat
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.wireguard.android.util.RootShell
|
||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
|
||||
import com.zaneschepke.wireguardautotunnel.repository.datastore.DataStoreManager
|
||||
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
||||
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.data.repository.TunnelConfigRepository
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.Event
|
||||
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class SettingsViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val application: Application,
|
||||
private val tunnelRepo: TunnelConfigDao,
|
||||
private val settingsRepo: SettingsDoa,
|
||||
val dataStoreManager: DataStoreManager,
|
||||
private val tunnelConfigRepository: TunnelConfigRepository,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val dataStoreManager: DataStoreManager,
|
||||
private val rootShell: RootShell,
|
||||
private val vpnService: VpnService
|
||||
) : ViewModel() {
|
||||
|
||||
private val _trustedSSIDs = MutableStateFlow(emptyList<String>())
|
||||
val trustedSSIDs = _trustedSSIDs.asStateFlow()
|
||||
private val _settings = MutableStateFlow(Settings())
|
||||
val settings get() = _settings.asStateFlow()
|
||||
val vpnState get() = vpnService.state
|
||||
val tunnels get() = tunnelRepo.getAllFlow()
|
||||
val disclosureShown = dataStoreManager.locationDisclosureFlow
|
||||
|
||||
init {
|
||||
isLocationServicesEnabled()
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
settingsRepo.getAllFlow().filter { it.isNotEmpty() }.collect {
|
||||
val settings = it.first()
|
||||
_settings.emit(settings)
|
||||
_trustedSSIDs.emit(settings.trustedNetworkSSIDs.toList())
|
||||
val uiState =
|
||||
combine(
|
||||
settingsRepository.getSettingsFlow(),
|
||||
tunnelConfigRepository.getTunnelConfigsFlow(),
|
||||
vpnService.vpnState,
|
||||
dataStoreManager.preferencesFlow,
|
||||
) { settings, tunnels, tunnelState, preferences ->
|
||||
SettingsUiState(
|
||||
settings,
|
||||
tunnels,
|
||||
tunnelState,
|
||||
preferences?.get(DataStoreManager.LOCATION_DISCLOSURE_SHOWN) ?: false,
|
||||
preferences?.get(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN) ?: false,
|
||||
false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
suspend fun onSaveTrustedSSID(ssid: String) {
|
||||
.stateIn(
|
||||
viewModelScope,
|
||||
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
|
||||
SettingsUiState(),
|
||||
)
|
||||
|
||||
fun onSaveTrustedSSID(ssid: String): Result<Unit> {
|
||||
val trimmed = ssid.trim()
|
||||
if (!_settings.value.trustedNetworkSSIDs.contains(trimmed)) {
|
||||
_settings.value.trustedNetworkSSIDs.add(trimmed)
|
||||
settingsRepo.save(_settings.value)
|
||||
return if (!uiState.value.settings.trustedNetworkSSIDs.contains(trimmed)) {
|
||||
uiState.value.settings.trustedNetworkSSIDs.add(trimmed)
|
||||
saveSettings(uiState.value.settings)
|
||||
Result.Success(Unit)
|
||||
} else {
|
||||
throw WgTunnelException("SSID already exists.")
|
||||
Result.Error(Event.Error.SsidConflict)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun onToggleTunnelOnMobileData() {
|
||||
settingsRepo.save(
|
||||
_settings.value.copy(
|
||||
isTunnelOnMobileDataEnabled = !_settings.value.isTunnelOnMobileDataEnabled
|
||||
)
|
||||
fun setLocationDisclosureShown() =
|
||||
viewModelScope.launch {
|
||||
dataStoreManager.saveToDataStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN, true)
|
||||
}
|
||||
|
||||
fun setBatteryOptimizeDisableShown() =
|
||||
viewModelScope.launch {
|
||||
dataStoreManager.saveToDataStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN, true)
|
||||
}
|
||||
|
||||
fun onToggleTunnelOnMobileData() {
|
||||
saveSettings(
|
||||
uiState.value.settings.copy(
|
||||
isTunnelOnMobileDataEnabled = !uiState.value.settings.isTunnelOnMobileDataEnabled,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun onDeleteTrustedSSID(ssid: String) {
|
||||
_settings.value.trustedNetworkSSIDs.remove(ssid)
|
||||
settingsRepo.save(_settings.value)
|
||||
fun onDeleteTrustedSSID(ssid: String) {
|
||||
saveSettings(
|
||||
uiState.value.settings.copy(
|
||||
trustedNetworkSSIDs =
|
||||
(uiState.value.settings.trustedNetworkSSIDs - ssid).toMutableList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun emitFirstTunnelAsDefault() =
|
||||
viewModelScope.async {
|
||||
_settings.emit(_settings.value.copy(defaultTunnel = getFirstTunnelConfig().toString()))
|
||||
}
|
||||
private suspend fun getDefaultTunnelOrFirst(): String {
|
||||
return uiState.value.settings.defaultTunnel
|
||||
?: tunnelConfigRepository.getAll().first().toString()
|
||||
}
|
||||
|
||||
suspend fun toggleAutoTunnel() {
|
||||
if (_settings.value.isAutoTunnelEnabled) {
|
||||
ServiceManager.stopWatcherService(application)
|
||||
} else {
|
||||
if (_settings.value.defaultTunnel == null) {
|
||||
emitFirstTunnelAsDefault().await()
|
||||
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
|
||||
}
|
||||
val defaultTunnel = _settings.value.defaultTunnel
|
||||
ServiceManager.startWatcherService(application, defaultTunnel!!)
|
||||
saveSettings(
|
||||
uiState.value.settings.copy(
|
||||
isAutoTunnelEnabled = !isAutoTunnelEnabled,
|
||||
isAutoTunnelPaused = isAutoTunnelPaused,
|
||||
defaultTunnel = getDefaultTunnelOrFirst(),
|
||||
),
|
||||
)
|
||||
}
|
||||
settingsRepo.save(
|
||||
_settings.value.copy(
|
||||
isAutoTunnelEnabled = !_settings.value.isAutoTunnelEnabled
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun getFirstTunnelConfig(): TunnelConfig {
|
||||
return tunnelRepo.getAll().first()
|
||||
}
|
||||
|
||||
suspend fun onToggleAlwaysOnVPN() {
|
||||
if (_settings.value.defaultTunnel == null) {
|
||||
emitFirstTunnelAsDefault().await()
|
||||
fun onToggleAlwaysOnVPN() =
|
||||
viewModelScope.launch {
|
||||
val updatedSettings =
|
||||
uiState.value.settings.copy(
|
||||
isAlwaysOnVpnEnabled = !uiState.value.settings.isAlwaysOnVpnEnabled,
|
||||
defaultTunnel = getDefaultTunnelOrFirst(),
|
||||
)
|
||||
saveSettings(updatedSettings)
|
||||
}
|
||||
val updatedSettings =
|
||||
_settings.value.copy(
|
||||
isAlwaysOnVpnEnabled = !_settings.value.isAlwaysOnVpnEnabled
|
||||
)
|
||||
emitSettings(updatedSettings)
|
||||
saveSettings(updatedSettings)
|
||||
}
|
||||
|
||||
private suspend fun emitSettings(settings: Settings) {
|
||||
_settings.emit(
|
||||
settings
|
||||
private fun saveSettings(settings: Settings) =
|
||||
viewModelScope.launch { settingsRepository.save(settings) }
|
||||
|
||||
fun onToggleTunnelOnEthernet() {
|
||||
saveSettings(
|
||||
uiState.value.settings.copy(
|
||||
isTunnelOnEthernetEnabled = !uiState.value.settings.isTunnelOnEthernetEnabled,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun saveSettings(settings: Settings) {
|
||||
settingsRepo.save(settings)
|
||||
fun isLocationEnabled(context: Context): Boolean {
|
||||
val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||
return LocationManagerCompat.isLocationEnabled(locationManager)
|
||||
}
|
||||
|
||||
suspend fun onToggleTunnelOnEthernet() {
|
||||
if (_settings.value.defaultTunnel == null) {
|
||||
emitFirstTunnelAsDefault().await()
|
||||
}
|
||||
_settings.emit(
|
||||
_settings.value.copy(
|
||||
isTunnelOnEthernetEnabled = !_settings.value.isTunnelOnEthernetEnabled
|
||||
)
|
||||
)
|
||||
settingsRepo.save(_settings.value)
|
||||
}
|
||||
|
||||
private fun isLocationServicesEnabled(): Boolean {
|
||||
val locationManager =
|
||||
application.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||
return locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
|
||||
}
|
||||
|
||||
fun isLocationServicesNeeded(): Boolean {
|
||||
return (!isLocationServicesEnabled() && Build.VERSION.SDK_INT > Build.VERSION_CODES.P)
|
||||
}
|
||||
|
||||
suspend fun onToggleShortcutsEnabled() {
|
||||
settingsRepo.save(
|
||||
_settings.value.copy(
|
||||
isShortcutsEnabled = !_settings.value.isShortcutsEnabled
|
||||
)
|
||||
fun onToggleShortcutsEnabled() {
|
||||
saveSettings(
|
||||
uiState.value.settings.copy(
|
||||
isShortcutsEnabled = !uiState.value.settings.isShortcutsEnabled,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun onToggleBatterySaver() {
|
||||
settingsRepo.save(
|
||||
_settings.value.copy(
|
||||
isBatterySaverEnabled = !_settings.value.isBatterySaverEnabled
|
||||
)
|
||||
fun onToggleBatterySaver() {
|
||||
saveSettings(
|
||||
uiState.value.settings.copy(
|
||||
isBatterySaverEnabled = !uiState.value.settings.isBatterySaverEnabled,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun saveKernelMode(on: Boolean) {
|
||||
settingsRepo.save(
|
||||
_settings.value.copy(
|
||||
isKernelEnabled = on
|
||||
)
|
||||
private fun saveKernelMode(on: Boolean) {
|
||||
saveSettings(
|
||||
uiState.value.settings.copy(
|
||||
isKernelEnabled = on,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun onToggleKernelMode() {
|
||||
if (!_settings.value.isKernelEnabled) {
|
||||
fun onToggleTunnelOnWifi() {
|
||||
saveSettings(
|
||||
uiState.value.settings.copy(
|
||||
isTunnelOnWifiEnabled = !uiState.value.settings.isTunnelOnWifiEnabled,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun onToggleKernelMode(): Result<Unit> {
|
||||
if (!uiState.value.settings.isKernelEnabled) {
|
||||
try {
|
||||
rootShell.start()
|
||||
Timber.d("Root shell accepted!")
|
||||
saveKernelMode(on = true)
|
||||
} catch (e: RootShell.RootShellException) {
|
||||
saveKernelMode(on = false)
|
||||
throw WgTunnelException("Root shell denied!")
|
||||
return Result.Error(Event.Error.RootDenied)
|
||||
}
|
||||
} else {
|
||||
saveKernelMode(on = false)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun onToggleTunnelOnWifi() {
|
||||
settingsRepo.save(
|
||||
_settings.value.copy(
|
||||
isTunnelOnWifiEnabled = !_settings.value.isTunnelOnWifiEnabled
|
||||
)
|
||||
)
|
||||
return Result.Success(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
+100
-73
@@ -8,7 +8,6 @@ import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
@@ -20,10 +19,10 @@ import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
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.Mail
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
@@ -40,6 +39,7 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -48,51 +48,63 @@ import androidx.core.content.ContextCompat.startActivity
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
||||
import com.zaneschepke.wireguardautotunnel.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.Event
|
||||
|
||||
@Composable
|
||||
fun SupportScreen(
|
||||
viewModel: SettingsViewModel = hiltViewModel(),
|
||||
padding: PaddingValues,
|
||||
viewModel: SupportViewModel = hiltViewModel(),
|
||||
showSnackbarMessage: (String) -> Unit,
|
||||
focusRequester: FocusRequester
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val fillMaxWidth = .85f
|
||||
|
||||
val settings by viewModel.settings.collectAsStateWithLifecycle()
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
fun openWebPage(url: String) {
|
||||
val webpage: Uri = Uri.parse(url)
|
||||
val intent = Intent(Intent.ACTION_VIEW, webpage)
|
||||
context.startActivity(intent)
|
||||
try {
|
||||
val webpage: Uri = Uri.parse(url)
|
||||
val intent = Intent(Intent.ACTION_VIEW, webpage)
|
||||
context.startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
showSnackbarMessage(Event.Error.Exception(e).message)
|
||||
}
|
||||
}
|
||||
|
||||
fun launchEmail() {
|
||||
val intent =
|
||||
Intent(Intent.ACTION_SEND).apply {
|
||||
type = Constants.EMAIL_MIME_TYPE
|
||||
putExtra(Intent.EXTRA_EMAIL, context.getString(R.string.my_email))
|
||||
putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.email_subject))
|
||||
}
|
||||
startActivity(
|
||||
context,
|
||||
createChooser(intent, context.getString(R.string.email_chooser)),
|
||||
null
|
||||
)
|
||||
try {
|
||||
val intent =
|
||||
Intent(Intent.ACTION_SENDTO).apply {
|
||||
type = Constants.EMAIL_MIME_TYPE
|
||||
putExtra(Intent.EXTRA_EMAIL, arrayOf(context.getString(R.string.my_email)))
|
||||
putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.email_subject))
|
||||
}
|
||||
startActivity(
|
||||
context,
|
||||
createChooser(intent, context.getString(R.string.email_chooser)),
|
||||
null,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
showSnackbarMessage(Event.Error.Exception(e).message)
|
||||
}
|
||||
}
|
||||
|
||||
if (uiState.loading) {
|
||||
LoadingScreen()
|
||||
return
|
||||
}
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.focusable()
|
||||
.padding(padding)
|
||||
Modifier.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.focusable()
|
||||
) {
|
||||
Surface(
|
||||
tonalElevation = 2.dp,
|
||||
@@ -100,124 +112,139 @@ fun SupportScreen(
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
modifier =
|
||||
(
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||
Modifier
|
||||
.height(IntrinsicSize.Min)
|
||||
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
Modifier.height(IntrinsicSize.Min)
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
.padding(top = 10.dp)
|
||||
} else {
|
||||
Modifier
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
.padding(top = 20.dp)
|
||||
}
|
||||
).padding(bottom = 25.dp)
|
||||
Modifier.fillMaxWidth(fillMaxWidth).padding(top = 20.dp)
|
||||
})
|
||||
.padding(bottom = 25.dp),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(20.dp)) {
|
||||
Text(
|
||||
stringResource(R.string.thank_you),
|
||||
textAlign = TextAlign.Start,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(bottom = 20.dp),
|
||||
fontSize = 16.sp
|
||||
fontSize = 16.sp,
|
||||
)
|
||||
Text(
|
||||
stringResource(id = R.string.support_help_text),
|
||||
textAlign = TextAlign.Start,
|
||||
fontSize = 16.sp,
|
||||
modifier = Modifier.padding(bottom = 20.dp)
|
||||
modifier = Modifier.padding(bottom = 20.dp),
|
||||
)
|
||||
TextButton(onClick = {
|
||||
openWebPage(context.resources.getString(R.string.docs_url))
|
||||
}, modifier = Modifier.padding(vertical = 5.dp).focusRequester(focusRequester)) {
|
||||
TextButton(
|
||||
onClick = { openWebPage(context.resources.getString(R.string.docs_url)) },
|
||||
modifier = Modifier.padding(vertical = 5.dp).focusRequester(focusRequester),
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Row {
|
||||
Icon(Icons.Rounded.Book, stringResource(id = R.string.docs))
|
||||
Text(
|
||||
stringResource(id = R.string.docs_description),
|
||||
textAlign = TextAlign.Justify,
|
||||
modifier = Modifier.padding(start = 10.dp)
|
||||
modifier = Modifier.padding(start = 10.dp),
|
||||
)
|
||||
}
|
||||
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
||||
Icon(
|
||||
Icons.AutoMirrored.Rounded.ArrowForward,
|
||||
stringResource(id = R.string.go)
|
||||
)
|
||||
}
|
||||
}
|
||||
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
|
||||
HorizontalDivider(
|
||||
thickness = 0.5.dp,
|
||||
color = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
TextButton(
|
||||
onClick = { openWebPage(context.resources.getString(R.string.discord_url)) },
|
||||
modifier = Modifier.padding(vertical = 5.dp)
|
||||
modifier = Modifier.padding(vertical = 5.dp),
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Row {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.discord),
|
||||
stringResource(
|
||||
id = R.string.discord
|
||||
),
|
||||
Modifier.size(25.dp)
|
||||
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)
|
||||
modifier = Modifier.padding(start = 10.dp),
|
||||
)
|
||||
}
|
||||
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
||||
Icon(
|
||||
Icons.AutoMirrored.Rounded.ArrowForward,
|
||||
stringResource(id = R.string.go)
|
||||
)
|
||||
}
|
||||
}
|
||||
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
|
||||
HorizontalDivider(
|
||||
thickness = 0.5.dp,
|
||||
color = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
TextButton(
|
||||
onClick = { openWebPage(context.resources.getString(R.string.github_url)) },
|
||||
modifier = Modifier.padding(vertical = 5.dp)
|
||||
modifier = Modifier.padding(vertical = 5.dp),
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Row {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.github),
|
||||
stringResource(
|
||||
id = R.string.github
|
||||
),
|
||||
Modifier.size(25.dp)
|
||||
stringResource(id = R.string.github),
|
||||
Modifier.size(25.dp),
|
||||
)
|
||||
Text(
|
||||
"Open an issue",
|
||||
textAlign = TextAlign.Justify,
|
||||
modifier = Modifier.padding(start = 10.dp)
|
||||
modifier = Modifier.padding(start = 10.dp),
|
||||
)
|
||||
}
|
||||
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
||||
Icon(
|
||||
Icons.AutoMirrored.Rounded.ArrowForward,
|
||||
stringResource(id = R.string.go)
|
||||
)
|
||||
}
|
||||
}
|
||||
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
|
||||
HorizontalDivider(
|
||||
thickness = 0.5.dp,
|
||||
color = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
TextButton(
|
||||
onClick = { launchEmail() },
|
||||
modifier = Modifier.padding(vertical = 5.dp)
|
||||
modifier = Modifier.padding(vertical = 5.dp),
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
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)
|
||||
modifier = Modifier.padding(start = 10.dp),
|
||||
)
|
||||
}
|
||||
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
||||
Icon(
|
||||
Icons.AutoMirrored.Rounded.ArrowForward,
|
||||
stringResource(id = R.string.go)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -228,17 +255,17 @@ fun SupportScreen(
|
||||
style = TextStyle(textDecoration = TextDecoration.Underline),
|
||||
fontSize = 16.sp,
|
||||
modifier =
|
||||
Modifier.clickable {
|
||||
openWebPage(context.resources.getString(R.string.privacy_policy_url))
|
||||
}
|
||||
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)
|
||||
modifier = Modifier.padding(25.dp),
|
||||
) {
|
||||
Text("Version: ${BuildConfig.VERSION_NAME}")
|
||||
Text("Mode: ${if (settings.isKernelEnabled) "Kernel" else "Userspace" }")
|
||||
Text("Version: ${BuildConfig.VERSION_NAME}", modifier = Modifier.focusable())
|
||||
Text("Mode: ${if (uiState.settings.isKernelEnabled) "Kernel" else "Userspace"}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.support
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||
|
||||
data class SupportUiState(val settings: Settings = Settings(), val loading: Boolean = true)
|
||||
+17
-16
@@ -2,24 +2,25 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.support
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@HiltViewModel
|
||||
class SupportViewModel @Inject constructor(
|
||||
private val settingsRepo: SettingsDoa
|
||||
) : ViewModel() {
|
||||
private val _settings = MutableStateFlow(Settings())
|
||||
val settings get() = _settings.asStateFlow()
|
||||
init {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
_settings.value = settingsRepo.getAll().first()
|
||||
}
|
||||
}
|
||||
class SupportViewModel @Inject constructor(private val settingsRepository: SettingsRepository) :
|
||||
ViewModel() {
|
||||
|
||||
val uiState =
|
||||
settingsRepository
|
||||
.getSettingsFlow()
|
||||
.map { SupportUiState(it, false) }
|
||||
.stateIn(
|
||||
viewModelScope,
|
||||
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
|
||||
SupportUiState(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ private val DarkColorScheme =
|
||||
primary = virdigris,
|
||||
secondary = virdigris,
|
||||
// secondary = PurpleGrey80,
|
||||
tertiary = virdigris
|
||||
tertiary = virdigris,
|
||||
// tertiary = Pink80
|
||||
)
|
||||
|
||||
@@ -29,7 +29,7 @@ private val LightColorScheme =
|
||||
lightColorScheme(
|
||||
primary = Purple40,
|
||||
secondary = PurpleGrey40,
|
||||
tertiary = Pink40
|
||||
tertiary = Pink40,
|
||||
/* Other default colors to override
|
||||
background = Color(0xFFFFFBFE),
|
||||
surface = Color(0xFFFFFBFE),
|
||||
@@ -57,7 +57,6 @@ fun WireguardAutoTunnelTheme(
|
||||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}
|
||||
@@ -68,14 +67,14 @@ fun WireguardAutoTunnelTheme(
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
window.statusBarColor = Color.Transparent.toArgb()
|
||||
window.navigationBarColor = Color.Transparent.toArgb()
|
||||
WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightStatusBars = !darkTheme
|
||||
WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightNavigationBars = !darkTheme
|
||||
WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightStatusBars =
|
||||
!darkTheme
|
||||
}
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography,
|
||||
content = content
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.theme
|
||||
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
|
||||
@Composable
|
||||
fun TransparentSystemBars() {
|
||||
val systemUiController = rememberSystemUiController()
|
||||
val useDarkIcons = !isSystemInDarkTheme()
|
||||
|
||||
DisposableEffect(systemUiController, useDarkIcons) {
|
||||
systemUiController.setSystemBarsColor(
|
||||
color = Color.Transparent,
|
||||
darkIcons = useDarkIcons
|
||||
)
|
||||
|
||||
onDispose {}
|
||||
}
|
||||
}
|
||||
@@ -10,13 +10,13 @@ import androidx.compose.ui.unit.sp
|
||||
val Typography =
|
||||
Typography(
|
||||
bodyLarge =
|
||||
TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp,
|
||||
),
|
||||
/* Other default text styles to override
|
||||
titleLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
|
||||
+8
-7
@@ -1,14 +1,12 @@
|
||||
package com.zaneschepke.wireguardautotunnel
|
||||
package com.zaneschepke.wireguardautotunnel.util
|
||||
|
||||
object Constants {
|
||||
const val MANUAL_TUNNEL_CONFIG_ID = "0"
|
||||
const val WATCHER_SERVICE_WAKE_LOCK_TIMEOUT = 10 * 60 * 1000L // 10 minutes
|
||||
const val VPN_CONNECTIVITY_CHECK_INTERVAL = 3000L
|
||||
const val BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT = 10 * 60 * 1000L // 10 minutes
|
||||
const val DEFAULT_WATCHER_WAKE_LOCK_TIMEOUT = 30 * 60 * 1000L // 30 minutes
|
||||
const val VPN_STATISTIC_CHECK_INTERVAL = 1000L
|
||||
const val TOGGLE_TUNNEL_DELAY = 500L
|
||||
const val FADE_IN_ANIMATION_DURATION = 1000
|
||||
const val SLIDE_IN_ANIMATION_DURATION = 500
|
||||
const val SLIDE_IN_TRANSITION_OFFSET = 1000
|
||||
const val VPN_CONNECTED_NOTIFICATION_DELAY = 3000L
|
||||
const val TOGGLE_TUNNEL_DELAY = 300L
|
||||
const val CONF_FILE_EXTENSION = ".conf"
|
||||
const val ZIP_FILE_EXTENSION = ".zip"
|
||||
const val URI_CONTENT_SCHEME = "content"
|
||||
@@ -18,4 +16,7 @@ object Constants {
|
||||
const val ANDROID_TV_EXPLORER_STUB = "com.android.tv.frameworkpackagestubs"
|
||||
const val EMAIL_MIME_TYPE = "message/rfc822"
|
||||
const val SYSTEM_EXEMPT_SERVICE_TYPE_ID = 1024
|
||||
|
||||
const val SUBSCRIPTION_TIMEOUT = 5_000L
|
||||
const val FOCUS_REQUEST_DELAY = 500L
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package com.zaneschepke.wireguardautotunnel.util
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
|
||||
sealed class Event {
|
||||
|
||||
abstract val message: String
|
||||
|
||||
sealed class Error : Event() {
|
||||
data object None : Error() {
|
||||
override val message: String
|
||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_none)
|
||||
}
|
||||
|
||||
data object SsidConflict : Error() {
|
||||
override val message: String
|
||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_ssid_exists)
|
||||
}
|
||||
|
||||
data object RootDenied : Error() {
|
||||
override val message: String
|
||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_root_denied)
|
||||
}
|
||||
|
||||
data class General(val customMessage: String) : Error() {
|
||||
override val message: String
|
||||
get() = customMessage
|
||||
}
|
||||
|
||||
data class Exception(val exception: kotlin.Exception) : Error() {
|
||||
override val message: String
|
||||
get() =
|
||||
exception.message
|
||||
?: WireGuardAutoTunnel.instance.getString(R.string.unknown_error)
|
||||
}
|
||||
|
||||
data object InvalidQrCode : Error() {
|
||||
override val message: String
|
||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_invalid_code)
|
||||
}
|
||||
|
||||
data object InvalidFileExtension : Error() {
|
||||
override val message: String
|
||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_file_extension)
|
||||
}
|
||||
|
||||
data object FileReadFailed : Error() {
|
||||
override val message: String
|
||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_file_extension)
|
||||
}
|
||||
|
||||
data object AuthenticationFailed : Error() {
|
||||
override val message: String
|
||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_authentication_failed)
|
||||
}
|
||||
|
||||
data object AuthorizationFailed : Error() {
|
||||
override val message: String
|
||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_authorization_failed)
|
||||
}
|
||||
|
||||
data object BackgroundLocationRequired : Error() {
|
||||
override val message: String
|
||||
get() =
|
||||
WireGuardAutoTunnel.instance.getString(R.string.background_location_required)
|
||||
}
|
||||
|
||||
data object LocationServicesRequired : Error() {
|
||||
override val message: String
|
||||
get() = WireGuardAutoTunnel.instance.getString(R.string.location_services_required)
|
||||
}
|
||||
|
||||
data object PreciseLocationRequired : Error() {
|
||||
override val message: String
|
||||
get() = WireGuardAutoTunnel.instance.getString(R.string.precise_location_required)
|
||||
}
|
||||
|
||||
data object FileExplorerRequired : Error() {
|
||||
override val message: String
|
||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_no_file_explorer)
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Message : Event() {
|
||||
data object ConfigSaved : Message() {
|
||||
override val message: String
|
||||
get() = WireGuardAutoTunnel.instance.getString(R.string.config_changes_saved)
|
||||
}
|
||||
|
||||
data object ConfigsExported : Message() {
|
||||
override val message: String
|
||||
get() = WireGuardAutoTunnel.instance.getString(R.string.exported_configs_message)
|
||||
}
|
||||
|
||||
data object TunnelOffAction : Message() {
|
||||
override val message: String
|
||||
get() = WireGuardAutoTunnel.instance.getString(R.string.turn_off_tunnel)
|
||||
}
|
||||
|
||||
data object TunnelOnAction : Message() {
|
||||
override val message: String
|
||||
get() = WireGuardAutoTunnel.instance.getString(R.string.turn_on_tunnel)
|
||||
}
|
||||
|
||||
data object AutoTunnelOffAction : Message() {
|
||||
override val message: String
|
||||
get() = WireGuardAutoTunnel.instance.getString(R.string.turn_off_auto)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.zaneschepke.wireguardautotunnel.util
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.pm.PackageInfo
|
||||
import com.wireguard.android.backend.Statistics
|
||||
import com.wireguard.android.backend.Statistics.PeerStats
|
||||
import com.wireguard.crypto.Key
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.math.BigDecimal
|
||||
import java.text.DecimalFormat
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
|
||||
fun BroadcastReceiver.goAsync(
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
block: suspend CoroutineScope.() -> Unit
|
||||
) {
|
||||
val pendingResult = goAsync()
|
||||
@OptIn(DelicateCoroutinesApi::class) // Must run globally; there's no teardown callback.
|
||||
GlobalScope.launch(context) {
|
||||
try {
|
||||
block()
|
||||
} finally {
|
||||
pendingResult.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun BigDecimal.toThreeDecimalPlaceString(): String {
|
||||
val df = DecimalFormat("#.###")
|
||||
return df.format(this)
|
||||
}
|
||||
|
||||
fun <T> List<T>.update(index: Int, item: T): List<T> = toMutableList().apply { this[index] = item }
|
||||
|
||||
fun <T> List<T>.removeAt(index: Int): List<T> = toMutableList().apply { this.removeAt(index) }
|
||||
|
||||
typealias TunnelConfigs = List<TunnelConfig>
|
||||
|
||||
typealias Packages = List<PackageInfo>
|
||||
|
||||
fun Statistics.mapPeerStats(): Map<Key, PeerStats?> {
|
||||
return this.peers().associateWith { key -> (this.peer(key)) }
|
||||
}
|
||||
|
||||
fun PeerStats.latestHandshakeSeconds(): Long? {
|
||||
return NumberUtils.getSecondsBetweenTimestampAndNow(this.latestHandshakeEpochMillis)
|
||||
}
|
||||
|
||||
fun PeerStats.handshakeStatus(): HandshakeStatus {
|
||||
// TODO add never connected status after duration
|
||||
return this.latestHandshakeSeconds().let {
|
||||
when {
|
||||
it == null -> HandshakeStatus.NOT_STARTED
|
||||
it <= HandshakeStatus.STALE_TIME_LIMIT_SEC -> HandshakeStatus.HEALTHY
|
||||
it > HandshakeStatus.STALE_TIME_LIMIT_SEC -> HandshakeStatus.STALE
|
||||
else -> {
|
||||
HandshakeStatus.UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import android.provider.MediaStore.MediaColumns
|
||||
import com.zaneschepke.wireguardautotunnel.Constants
|
||||
import java.io.File
|
||||
import java.io.OutputStream
|
||||
import java.time.Instant
|
||||
@@ -37,22 +36,20 @@ object FileUtils {
|
||||
val target =
|
||||
File(
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
||||
fileName
|
||||
fileName,
|
||||
)
|
||||
return target.outputStream()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun saveFilesToZip(
|
||||
context: Context,
|
||||
files: List<File>
|
||||
) {
|
||||
val zipOutputStream = createDownloadsFileOutputStream(
|
||||
context,
|
||||
"wg-export_${Instant.now().epochSecond}.zip",
|
||||
ZIP_FILE_MIME_TYPE
|
||||
)
|
||||
fun saveFilesToZip(context: Context, files: List<File>) {
|
||||
val zipOutputStream =
|
||||
createDownloadsFileOutputStream(
|
||||
context,
|
||||
"wg-export_${Instant.now().epochSecond}.zip",
|
||||
ZIP_FILE_MIME_TYPE,
|
||||
)
|
||||
ZipOutputStream(zipOutputStream).use { zos ->
|
||||
files.forEach { file ->
|
||||
val entry = ZipEntry(file.name)
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.zaneschepke.wireguardautotunnel.util
|
||||
|
||||
import timber.log.Timber
|
||||
|
||||
sealed class Result<T> {
|
||||
class Success<T>(val data: T) : Result<T>()
|
||||
|
||||
class Error<T>(val error: Event.Error) : Result<T>() {
|
||||
init {
|
||||
when (this.error) {
|
||||
is Event.Error.Exception -> Timber.e(this.error.exception)
|
||||
else -> Timber.e(this.error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.util
|
||||
|
||||
import com.wireguard.config.BadConfigException
|
||||
|
||||
class WgTunnelException(e: Exception) : Exception() {
|
||||
constructor(message: String) : this(Exception(message))
|
||||
|
||||
override val message: String = generateExceptionMessage(e)
|
||||
|
||||
private fun generateExceptionMessage(e: Exception): String {
|
||||
return when (e) {
|
||||
is BadConfigException -> "${e.section.name} ${e.location.name} ${e.reason.name}"
|
||||
else -> e.message ?: "Unknown error occurred"
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user