mirror of
https://github.com/wgtunnel/android.git
synced 2026-06-02 08:33:40 +02:00
Compare commits
96 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c15943a81 | |||
| 05adf7539f | |||
| a9a49e3421 | |||
| 4dd8241fa1 | |||
| 431b2f9061 | |||
| f822292584 | |||
| 4ffc5d4069 | |||
| 680fbed28c | |||
| 737524831b | |||
| b8c36ac192 | |||
| 547686069f | |||
| ac18ac8274 | |||
| cb983da990 | |||
| cfe64dcb61 | |||
| 2db521d510 | |||
| ff6c763b7b | |||
| ebf7521fa1 | |||
| 7a2d96fcd7 | |||
| c6c8047982 | |||
| 9cfb7250de | |||
| 79b5b039b0 | |||
| 29616f8325 | |||
| 8bbe81d294 | |||
| 571fb1b12c | |||
| 02f6f97aa1 | |||
| 1d74d0984e | |||
| 6448386f76 | |||
| d09e85ba45 | |||
| a9bc1cc7f0 | |||
| 54d9653f04 | |||
| efc66821a6 | |||
| 3af7adc45b | |||
| 5754f2183c | |||
| f7f7f1bd9d | |||
| 57bb3f5e74 | |||
| 49196e7c7b | |||
| 894b63e668 | |||
| e16d44ff20 | |||
| b8b3f3001b | |||
| d142ecea6e | |||
| b793984ede | |||
| ae2532afe5 | |||
| 2720a3b35e | |||
| 2350364543 | |||
| f4172cb1fc | |||
| 90c482ae4f | |||
| 1eb8ad62e0 | |||
| d44baa84a8 | |||
| cb1b8ee7d6 | |||
| 4153351fc4 | |||
| 48e6f341cb | |||
| d531adede5 | |||
| 2df1bb07ab | |||
| a5e60c3fbe | |||
| e4af481402 | |||
| 77b3fc8360 | |||
| 4fd908f271 | |||
| 632da245ab | |||
| 04f22cb92d | |||
| 31194d8b88 | |||
| 421bf418d1 | |||
| e84d7e354c | |||
| 681b066d99 | |||
| ad53fca928 | |||
| f7e4b7e8ef | |||
| b04e8e7f60 | |||
| cbee5cfd1b | |||
| 440fe6ceda | |||
| 27def018bd | |||
| 16979dbb2b | |||
| 5d8190628d | |||
| 6d597a235d | |||
| 61fd2f01b9 | |||
| 914a641977 | |||
| c97a3afbc4 | |||
| fadc2d1562 | |||
| 106ab76b82 | |||
| 48b3f60b37 | |||
| e7f5cee6dd | |||
| 5e01fd6c85 | |||
| dcbd72c6f6 | |||
| f27aa1452a | |||
| 82c3521c58 | |||
| c6535ffe7e | |||
| be5fe94ce6 | |||
| 1e905c1cfb | |||
| a549c111aa | |||
| 5ab344044e | |||
| 7030f68548 | |||
| ecd51b6fe5 | |||
| 3e4f8e0791 | |||
| e940a0dbc5 | |||
| 8e233475df | |||
| 24789826ad | |||
| 787e0d1a71 | |||
| 914ef53ef0 |
@@ -0,0 +1,22 @@
|
||||
# Contributor Code of Conduct
|
||||
|
||||
## Pledge
|
||||
|
||||
We as individuals involved in this project, pledge to participate in this
|
||||
community in a respectful, constructive, and civil manner as we work towards a common goal
|
||||
of delivering free, open source, and value adding software for all.
|
||||
|
||||
## Standard
|
||||
|
||||
The standard for this community is the Golden Rule.
|
||||
|
||||
> “Do unto others as you would have them do unto you.”
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies to all spaces related to WG Tunnel.
|
||||
|
||||
## Incidents or Concerns
|
||||
|
||||
For any incidents or concerns, reach out to Zane at
|
||||
<support@zaneschepke.com>.
|
||||
@@ -0,0 +1,20 @@
|
||||
name: Issue Updates Workflow
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [ opened, closed, reopened ]
|
||||
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send Telegram Message
|
||||
run: |
|
||||
msg_text='${{ github.actor }} updated an issue:
|
||||
status: ${{ github.event.issue.state }} - #${{ github.event.issue.number }} ${{ github.event.issue.title }}
|
||||
https://github.com/zaneschepke/wgtunnel/issues/${{ github.event.issue.number }}'
|
||||
curl -s -X POST 'https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN }}/sendMessage' \
|
||||
-d "chat_id=${{ secrets.TELEGRAM_TO }}&text=${msg_text}&message_thread_id=${{ secrets.TELEGRAM_TOPIC }}"
|
||||
@@ -1,124 +0,0 @@
|
||||
# name of the workflow
|
||||
name: Android CI Tag Deployment (Pre-release)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- '*.*.*-**'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Signed APK
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
|
||||
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
|
||||
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
|
||||
KEY_STORE_FILE: 'android_keystore.jks'
|
||||
KEY_STORE_LOCATION: ${{ github.workspace }}/app/keystore/
|
||||
GH_USER: ${{ secrets.GH_USER }}
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
cache: gradle
|
||||
- name: Grant execute permission for gradlew
|
||||
run: chmod +x gradlew
|
||||
|
||||
# Here we need to decode keystore.jks from base64 string and place it
|
||||
# in the folder specified in the release signing configuration
|
||||
- name: Decode Keystore
|
||||
id: decode_keystore
|
||||
uses: timheuer/base64-to-file@v1.2
|
||||
with:
|
||||
fileName: ${{ env.KEY_STORE_FILE }}
|
||||
fileDir: ${{ env.KEY_STORE_LOCATION }}
|
||||
encodedString: ${{ secrets.KEYSTORE }}
|
||||
|
||||
# create keystore path for gradle to read
|
||||
- name: Create keystore path env var
|
||||
run: |
|
||||
store_path=${{ env.KEY_STORE_LOCATION }}${{ env.KEY_STORE_FILE }}
|
||||
echo "KEY_STORE_PATH=$store_path" >> $GITHUB_ENV
|
||||
|
||||
- name: Create service_account.json
|
||||
id: createServiceAccount
|
||||
run: echo '${{ secrets.SERVICE_ACCOUNT_JSON }}' > service_account.json
|
||||
|
||||
# Build and sign APK ("-x test" argument is used to skip tests)
|
||||
# add fdroid flavor for apk upload
|
||||
- name: Build Fdroid Release APK
|
||||
run: ./gradlew :app:assembleFdroidRelease -x test
|
||||
|
||||
# get fdroid flavor release apk path
|
||||
- name: Get apk path
|
||||
id: apk-path
|
||||
run: echo "path=$(find . -regex '^.*/build/outputs/apk/fdroid/release/.*\.apk$' -type f | head -1)" >> $GITHUB_OUTPUT
|
||||
- name: Get version code
|
||||
run: |
|
||||
version_code=$(grep "VERSION_CODE" buildSrc/src/main/kotlin/Constants.kt | awk '{print $5}' | tr -d '\n')
|
||||
echo "VERSION_CODE=$version_code" >> $GITHUB_ENV
|
||||
# Save the APK after the Build job is complete to publish it as a Github release in the next job
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@v4.3.1
|
||||
with:
|
||||
name: wgtunnel
|
||||
path: ${{ steps.apk-path.outputs.path }}
|
||||
- name: Download APK from build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: wgtunnel
|
||||
- name: Create Release with Fastlane changelog notes
|
||||
id: create_release
|
||||
uses: softprops/action-gh-release@v2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
# fix hardcode changelog file name
|
||||
body_path: ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/${{ env.VERSION_CODE }}.txt
|
||||
tag_name: ${{ github.ref_name }}
|
||||
name: ${{ github.ref_name }}
|
||||
draft: false
|
||||
prerelease: true
|
||||
files: ${{ github.workspace }}/${{ steps.apk-path.outputs.path }}
|
||||
|
||||
- name: Install apksigner
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y apksigner
|
||||
|
||||
- name: Get checksum
|
||||
id: checksum
|
||||
run: echo "checksum=$(apksigner verify -print-certs ${{ steps.apk-path.outputs.path }} | grep -Po "(?<=SHA-256 digest:) .*" | tr -d "[:blank:]")" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Append checksum
|
||||
id: append_checksum
|
||||
uses: softprops/action-gh-release@v2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
body: |
|
||||
SHA256 fingerprint:
|
||||
```${{ steps.checksum.outputs.checksum }}```
|
||||
tag_name: ${{ github.ref_name }}
|
||||
name: ${{ github.ref_name }}
|
||||
draft: false
|
||||
prerelease: true
|
||||
append_body: true
|
||||
|
||||
- name: Deploy with fastlane
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: '3.2' # Not needed with a .ruby-version file
|
||||
bundler-cache: true
|
||||
- name: Distribute app to Beta track 🚀
|
||||
run: (cd ${{ github.workspace }} && bundle install && bundle exec fastlane beta)
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
name: Release Updates Workflow
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [ published ]
|
||||
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send Telegram Message
|
||||
run: |
|
||||
msg_text='${{ github.actor }} published a new release:
|
||||
Release: ${{ github.event.release.tag_name }}
|
||||
${{ github.event.release.body }}
|
||||
https://github.com/zaneschepke/wgtunnel/releases/tag/${{ github.event.release.tag_name }}'
|
||||
curl -s -X POST 'https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN }}/sendMessage' \
|
||||
-d "chat_id=${{ secrets.TELEGRAM_TO }}&text=${msg_text}&message_thread_id=${{ secrets.TELEGRAM_TOPIC }}"
|
||||
+122
-41
@@ -1,17 +1,40 @@
|
||||
# name of the workflow
|
||||
name: Android CI Tag Deployment (Release)
|
||||
name: release-android
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "4 3 * * *"
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- '*.*.*'
|
||||
- '!*.*.*-**'
|
||||
inputs:
|
||||
track:
|
||||
type: choice
|
||||
description: "Google play release track"
|
||||
options:
|
||||
- none
|
||||
- internal
|
||||
- alpha
|
||||
- beta
|
||||
- production
|
||||
default: alpha
|
||||
required: true
|
||||
release_type:
|
||||
type: choice
|
||||
description: "GitHub release type"
|
||||
options:
|
||||
- none
|
||||
- prerelease
|
||||
- nightly
|
||||
- release
|
||||
default: release
|
||||
required: true
|
||||
tag_name:
|
||||
description: "Tag name for release"
|
||||
required: false
|
||||
default: nightly
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Signed APK
|
||||
|
||||
if: ${{ inputs.release_type != 'none' }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
@@ -21,7 +44,9 @@ jobs:
|
||||
KEY_STORE_FILE: 'android_keystore.jks'
|
||||
KEY_STORE_LOCATION: ${{ github.workspace }}/app/keystore/
|
||||
GH_USER: ${{ secrets.GH_USER }}
|
||||
# GH needed for gh cli
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -34,6 +59,10 @@ jobs:
|
||||
- name: Grant execute permission for gradlew
|
||||
run: chmod +x gradlew
|
||||
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt update && sudo apt install -y gh apksigner
|
||||
|
||||
# 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
|
||||
@@ -57,74 +86,126 @@ jobs:
|
||||
# Build and sign APK ("-x test" argument is used to skip tests)
|
||||
# add fdroid flavor for apk upload
|
||||
- name: Build Fdroid Release APK
|
||||
if: ${{ inputs.release_type != '' && inputs.release_type != 'nightly' }}
|
||||
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: Build Fdroid Nightly APK
|
||||
if: ${{ inputs.release_type == '' || inputs.release_type == 'nightly' }}
|
||||
run: ./gradlew :app:assembleFdroidNightly -x test
|
||||
|
||||
- if: ${{ inputs.release_type == '' || inputs.release_type == 'nightly' }}
|
||||
run: echo "APK_PATH=$(find . -regex '^.*/build/outputs/apk/fdroid/nightly/.*\.apk$' -type f | head -1)" >> $GITHUB_ENV
|
||||
- if: ${{ inputs.release_type != '' && inputs.release_type != 'nightly' }}
|
||||
run: echo "APK_PATH=$(find . -regex '^.*/build/outputs/apk/fdroid/release/.*\.apk$' -type f | head -1)" >> $GITHUB_ENV
|
||||
|
||||
- name: Get version code
|
||||
if: ${{ inputs.release_type == 'release' || inputs.release_type == 'prerelease' }}
|
||||
run: |
|
||||
version_code=$(grep "VERSION_CODE" buildSrc/src/main/kotlin/Constants.kt | awk '{print $5}' | tr -d '\n')
|
||||
echo "VERSION_CODE=$version_code" >> $GITHUB_ENV
|
||||
|
||||
# Save the APK after the Build job is complete to publish it as a Github release in the next job
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@v4.3.1
|
||||
uses: actions/upload-artifact@v4.3.4
|
||||
with:
|
||||
name: wgtunnel
|
||||
path: ${{ steps.apk-path.outputs.path }}
|
||||
path: ${{ env.APK_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
|
||||
if: ${{ inputs.release_type == 'release' }}
|
||||
with:
|
||||
token: ${{ secrets.PAT }}
|
||||
repository: zaneschepke/fdroid
|
||||
event-type: fdroid-update
|
||||
|
||||
- name: Set version release notes
|
||||
if: ${{ inputs.release_type == 'release' || inputs.release_type == 'prerelease' }}
|
||||
run: |
|
||||
RELEASE_NOTES="$(cat ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/${{ env.VERSION_CODE }}.txt)"
|
||||
echo "RELEASE_NOTES<<EOF" >> $GITHUB_ENV
|
||||
echo "$RELEASE_NOTES" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
|
||||
- name: On nightly release
|
||||
if: ${{ contains(env.TAG_NAME, 'nightly') }}
|
||||
run: |
|
||||
echo "RELEASE_NOTES=Nightly build for the latest development version of the app." >> $GITHUB_ENV
|
||||
gh release delete nightly --yes || true
|
||||
|
||||
# Setup TAG_NAME, which is used as a general "name"
|
||||
- if: github.event_name == 'workflow_dispatch'
|
||||
run: echo "TAG_NAME=${{ github.event.inputs.tag_name }}" >> $GITHUB_ENV
|
||||
- if: github.event_name == 'schedule'
|
||||
run: echo "TAG_NAME=nightly" >> $GITHUB_ENV
|
||||
|
||||
- name: On nightly release
|
||||
if: ${{ contains(env.TAG_NAME, 'nightly') }}
|
||||
run: |
|
||||
echo "RELEASE_NOTES=Nightly build of the latest development version of the android client." >> $GITHUB_ENV
|
||||
gh release delete nightly --yes || true
|
||||
|
||||
- name: Get checksum
|
||||
id: checksum
|
||||
run: echo "checksum=$(apksigner verify -print-certs ${{ env.APK_PATH }} | grep -Po "(?<=SHA-256 digest:) .*" | tr -d "[:blank:]")" >> $GITHUB_OUTPUT
|
||||
|
||||
|
||||
- name: Create Release with Fastlane changelog notes
|
||||
id: create_release
|
||||
uses: softprops/action-gh-release@v2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
body_path: ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/${{ env.VERSION_CODE }}.txt
|
||||
tag_name: ${{ github.ref_name }}
|
||||
name: ${{ github.ref_name }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
files: ${{ github.workspace }}/${{ steps.apk-path.outputs.path }}
|
||||
|
||||
- name: Install apksigner
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y apksigner
|
||||
|
||||
- name: Get checksum
|
||||
id: checksum
|
||||
run: echo "checksum=$(apksigner verify -print-certs ${{ steps.apk-path.outputs.path }} | grep -Po "(?<=SHA-256 digest:) .*" | tr -d "[:blank:]")" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Append checksum
|
||||
id: append_checksum
|
||||
uses: softprops/action-gh-release@v2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
body: |
|
||||
${{ env.RELEASE_NOTES }}
|
||||
|
||||
SHA256 fingerprint:
|
||||
```${{ steps.checksum.outputs.checksum }}```
|
||||
tag_name: ${{ github.ref_name }}
|
||||
name: ${{ github.ref_name }}
|
||||
tag_name: ${{ env.TAG_NAME }}
|
||||
name: ${{ env.TAG_NAME }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
append_body: true
|
||||
prerelease: ${{ inputs.release_type == 'prerelease' || inputs.release_type == '' || inputs.release_type == 'nightly' }}
|
||||
make_latest: ${{ inputs.release_type == 'release' }}
|
||||
files: ${{ github.workspace }}/${{ env.APK_PATH }}
|
||||
|
||||
publish-play:
|
||||
if: ${{ inputs.track != 'none' && inputs.track != '' }}
|
||||
name: Publish to Google Play
|
||||
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
|
||||
|
||||
- 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)
|
||||
|
||||
- name: Distribute app to Prod track 🚀
|
||||
run: (cd ${{ github.workspace }} && bundle install && bundle exec fastlane ${{ inputs.track }})
|
||||
|
||||
|
||||
@@ -4,8 +4,9 @@ WG Tunnel
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://discord.gg/rbRRNh6H7V)
|
||||
[](https://discord.gg/rbRRNh6H7V)
|
||||
[](https://twitter.com/i/communities/1780655267685736818)
|
||||
[](https://t.me/wgtunnel)
|
||||
|
||||
</div>
|
||||
|
||||
@@ -21,7 +22,8 @@ WG Tunnel
|
||||
|
||||
<div align="left">
|
||||
|
||||
This is an alternative Android Application for [WireGuard](https://www.wireguard.com/) with added
|
||||
This is an alternative Android Application for [WireGuard](https://www.wireguard.com/)
|
||||
and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) 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.
|
||||
@@ -52,8 +54,9 @@ and on while on different networks. This app was created to offer a free solutio
|
||||
* Auto connect to tunnels based on Wi-Fi SSID, ethernet, or mobile data
|
||||
* Split tunneling by application with search
|
||||
* WireGuard support for kernel and userspace modes
|
||||
* Amnezia support for userspace mode for DPI/censorship protection
|
||||
* Always-On VPN support
|
||||
* Export tunnels to zip
|
||||
* Export Amnezia and WireGuard tunnels to zip
|
||||
* Quick tile support for tunnel toggling, auto-tunneling
|
||||
* Static shortcuts support for tunnel toggling, auto-tunneling
|
||||
* Intent automation support for all tunnels
|
||||
@@ -62,13 +65,28 @@ and on while on different networks. This app was created to offer a free solutio
|
||||
* Battery preservation measures
|
||||
* Restart tunnel on ping failure (beta)
|
||||
|
||||
## Docs (WIP)
|
||||
## Fdroid
|
||||
|
||||
Basic documentation of the feature and behaviors of this app can be
|
||||
found [here](https://zaneschepke.com/wgtunnel-docs/overview.html).
|
||||
Want updates faster?
|
||||
|
||||
Check out my personal [fdroid repository](https://github.com/zaneschepke/fdroid) to get updates the
|
||||
moment they are released.
|
||||
|
||||
## Docs
|
||||
|
||||
Information about features, behaviors, and answers to common questions can be found in the
|
||||
app [documentation](https://zaneschepke.com/wgtunnel-docs/overview.html).
|
||||
|
||||
The repository for these docs can be found [here](https://github.com/zaneschepke/wgtunnel-docs).
|
||||
|
||||
## Translation
|
||||
|
||||
This app is using [Weblate](https://weblate.org) to assist with translations.
|
||||
|
||||
Help translate WG Tunnel into your language
|
||||
at [Hosted Weblate](https://hosted.weblate.org/engage/wg-tunnel/).\
|
||||
[](https://hosted.weblate.org/engage/wg-tunnel/)
|
||||
|
||||
## Building
|
||||
|
||||
```
|
||||
@@ -82,4 +100,11 @@ And then build the app:
|
||||
$ ./gradlew assembleDebug
|
||||
```
|
||||
|
||||
</span>
|
||||
## Contributing
|
||||
|
||||
Any contributions in the form of feedback, issues, code, or translations are welcome and much
|
||||
appreciated!
|
||||
|
||||
Please read
|
||||
the [code of conduct](https://github.com/zaneschepke/wgtunnel?tab=coc-ov-file#contributor-code-of-conduct)
|
||||
before contributing.
|
||||
|
||||
+28
-45
@@ -1,16 +1,17 @@
|
||||
import java.util.Properties
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.hilt.android)
|
||||
alias(libs.plugins.kotlinxSerialization)
|
||||
alias(libs.plugins.ksp)
|
||||
alias(libs.plugins.compose.compiler)
|
||||
alias(libs.plugins.grgit)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = Constants.APP_ID
|
||||
compileSdk = Constants.TARGET_SDK
|
||||
compileSdkPreview = "VanillaIceCream"
|
||||
|
||||
androidResources {
|
||||
generateLocaleConfig = true
|
||||
@@ -35,44 +36,10 @@ android {
|
||||
|
||||
signingConfigs {
|
||||
create(Constants.RELEASE) {
|
||||
val properties =
|
||||
Properties().apply {
|
||||
// created local file for signing details
|
||||
try {
|
||||
load(file("signing.properties").reader())
|
||||
} catch (_: Exception) {
|
||||
load(file("signing_template.properties").reader())
|
||||
}
|
||||
}
|
||||
|
||||
// try to get secrets from env first for pipeline build, then properties file for local
|
||||
// build
|
||||
storeFile =
|
||||
file(
|
||||
System.getenv()
|
||||
.getOrDefault(
|
||||
Constants.KEY_STORE_PATH_VAR,
|
||||
properties.getProperty(Constants.KEY_STORE_PATH_VAR),
|
||||
),
|
||||
)
|
||||
storePassword =
|
||||
System.getenv()
|
||||
.getOrDefault(
|
||||
Constants.STORE_PASS_VAR,
|
||||
properties.getProperty(Constants.STORE_PASS_VAR),
|
||||
)
|
||||
keyAlias =
|
||||
System.getenv()
|
||||
.getOrDefault(
|
||||
Constants.KEY_ALIAS_VAR,
|
||||
properties.getProperty(Constants.KEY_ALIAS_VAR),
|
||||
)
|
||||
keyPassword =
|
||||
System.getenv()
|
||||
.getOrDefault(
|
||||
Constants.KEY_PASS_VAR,
|
||||
properties.getProperty(Constants.KEY_PASS_VAR),
|
||||
)
|
||||
storeFile = getStoreFile()
|
||||
storePassword = getSigningProperty(Constants.STORE_PASS_VAR)
|
||||
keyAlias = getSigningProperty(Constants.KEY_ALIAS_VAR)
|
||||
keyPassword = getSigningProperty(Constants.KEY_PASS_VAR)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,6 +70,12 @@ android {
|
||||
signingConfig = signingConfigs.getByName(Constants.RELEASE)
|
||||
}
|
||||
debug { isDebuggable = true }
|
||||
|
||||
create(Constants.NIGHTLY) {
|
||||
initWith(getByName("release"))
|
||||
defaultConfig.versionName = nightlyVersionName()
|
||||
defaultConfig.versionCode = nightlyVersionCode()
|
||||
}
|
||||
}
|
||||
flavorDimensions.add(Constants.TYPE)
|
||||
productFlavors {
|
||||
@@ -112,9 +85,6 @@ android {
|
||||
}
|
||||
create("general") {
|
||||
dimension = Constants.TYPE
|
||||
if (BuildHelper.isReleaseBuild(gradle) && BuildHelper.isGeneralFlavor(gradle)) {
|
||||
//any plugins general specific
|
||||
}
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
@@ -127,7 +97,6 @@ android {
|
||||
compose = true
|
||||
buildConfig = true
|
||||
}
|
||||
composeOptions { kotlinCompilerExtensionVersion = Constants.COMPOSE_COMPILER_EXTENSION_VERSION }
|
||||
packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } }
|
||||
}
|
||||
|
||||
@@ -139,6 +108,7 @@ dependencies {
|
||||
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
|
||||
// helpers for implementing LifecycleOwner in a Service
|
||||
implementation(libs.androidx.lifecycle.service)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
@@ -162,6 +132,7 @@ dependencies {
|
||||
|
||||
// get tunnel lib from github packages or mavenLocal
|
||||
implementation(libs.tunnel)
|
||||
implementation(libs.amneziawg.android)
|
||||
coreLibraryDesugaring(libs.desugar.jdk.libs)
|
||||
|
||||
// logging
|
||||
@@ -172,6 +143,8 @@ dependencies {
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.androidx.hilt.navigation.compose)
|
||||
|
||||
implementation(libs.zaneschepke.multifab)
|
||||
|
||||
// hilt
|
||||
implementation(libs.hilt.android)
|
||||
ksp(libs.hilt.android.compiler)
|
||||
@@ -199,7 +172,6 @@ dependencies {
|
||||
|
||||
// barcode scanning
|
||||
implementation(libs.zxing.android.embedded)
|
||||
implementation(libs.zxing.core)
|
||||
|
||||
// bio
|
||||
implementation(libs.androidx.biometric.ktx)
|
||||
@@ -208,4 +180,15 @@ dependencies {
|
||||
// shortcuts
|
||||
implementation(libs.androidx.core)
|
||||
implementation(libs.androidx.core.google.shortcuts)
|
||||
|
||||
// splash
|
||||
implementation(libs.androidx.core.splashscreen)
|
||||
}
|
||||
|
||||
fun nightlyVersionCode() : Int {
|
||||
return Constants.VERSION_CODE + Constants.NIGHTLY_CODE
|
||||
}
|
||||
|
||||
fun nightlyVersionName() : String {
|
||||
return Constants.VERSION_NAME + "-${grgitService.service.get().grgit.head().abbreviatedId}"
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
|
||||
<fields>;
|
||||
}
|
||||
}
|
||||
Vendored
+1
-3
@@ -21,6 +21,4 @@
|
||||
#-renamesourcefileattribute SourceFile
|
||||
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
|
||||
<fields>;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 8,
|
||||
"identityHash": "b4d4a7c489f6b2f0d3aa4fa6f37b4935",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "Settings",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_auto_tunnel_paused` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `is_amnezia_enabled` INTEGER NOT NULL DEFAULT false)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAutoTunnelEnabled",
|
||||
"columnName": "is_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnMobileDataEnabled",
|
||||
"columnName": "is_tunnel_on_mobile_data_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "trustedNetworkSSIDs",
|
||||
"columnName": "trusted_network_ssids",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAlwaysOnVpnEnabled",
|
||||
"columnName": "is_always_on_vpn_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnEthernetEnabled",
|
||||
"columnName": "is_tunnel_on_ethernet_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isShortcutsEnabled",
|
||||
"columnName": "is_shortcuts_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnWifiEnabled",
|
||||
"columnName": "is_tunnel_on_wifi_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isKernelEnabled",
|
||||
"columnName": "is_kernel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isRestoreOnBootEnabled",
|
||||
"columnName": "is_restore_on_boot_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isMultiTunnelEnabled",
|
||||
"columnName": "is_multi_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAutoTunnelPaused",
|
||||
"columnName": "is_auto_tunnel_paused",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isPingEnabled",
|
||||
"columnName": "is_ping_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAmneziaEnabled",
|
||||
"columnName": "is_amnezia_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "TunnelConfig",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false, `is_primary_tunnel` INTEGER NOT NULL DEFAULT false, `am_quick` TEXT NOT NULL DEFAULT '')",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "wgQuick",
|
||||
"columnName": "wg_quick",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "tunnelNetworks",
|
||||
"columnName": "tunnel_networks",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "''"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isMobileDataTunnel",
|
||||
"columnName": "is_mobile_data_tunnel",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isPrimaryTunnel",
|
||||
"columnName": "is_primary_tunnel",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "amQuick",
|
||||
"columnName": "am_quick",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "''"
|
||||
}
|
||||
],
|
||||
"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, 'b4d4a7c489f6b2f0d3aa4fa6f37b4935')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@
|
||||
</queries>
|
||||
<application
|
||||
android:name=".WireGuardAutoTunnel"
|
||||
android:allowBackup="true"
|
||||
android:allowBackup="false"
|
||||
android:banner="@drawable/ic_banner"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
@@ -60,31 +60,35 @@
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.WireguardAutoTunnel"
|
||||
android:theme="@style/Theme.AppSplashScreen"
|
||||
tools:targetApi="tiramisu">
|
||||
<activity
|
||||
android:name=".ui.MainActivity"
|
||||
android:name=".ui.SplashActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.WireguardAutoTunnel">
|
||||
android:theme="@style/Theme.AppSplashScreen">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.CaptureActivityPortrait"
|
||||
android:screenOrientation="fullSensor"
|
||||
android:stateNotNeeded="true"
|
||||
android:theme="@style/zxing_CaptureTheme"
|
||||
android:windowSoftInputMode="stateAlwaysHidden"
|
||||
tools:ignore="DiscouragedApi" />
|
||||
android:name=".ui.MainActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.WireguardAutoTunnel">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="com.journeyapps.barcodescanner.CaptureActivity"
|
||||
android:screenOrientation="portrait"
|
||||
tools:replace="screenOrientation" />
|
||||
<activity
|
||||
android:name=".service.shortcut.ShortcutsActivity"
|
||||
android:enabled="true"
|
||||
|
||||
@@ -2,26 +2,37 @@ package com.zaneschepke.wireguardautotunnel
|
||||
|
||||
import android.app.Application
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.StrictMode
|
||||
import android.os.StrictMode.ThreadPolicy
|
||||
import android.service.quicksettings.TileService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelControlTile
|
||||
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
|
||||
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import timber.log.Timber
|
||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||
|
||||
@HiltAndroidApp
|
||||
class WireGuardAutoTunnel : Application() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
instance = this
|
||||
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) else Timber.plant(ReleaseTree())
|
||||
PinManager.initialize(this)
|
||||
if (BuildConfig.DEBUG) {
|
||||
Timber.plant(Timber.DebugTree())
|
||||
StrictMode.setThreadPolicy(
|
||||
ThreadPolicy.Builder()
|
||||
.detectDiskReads()
|
||||
.detectDiskWrites()
|
||||
.detectNetwork()
|
||||
.penaltyLog()
|
||||
.build(),
|
||||
)
|
||||
} else Timber.plant(ReleaseTree())
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
lateinit var instance: WireGuardAutoTunnel
|
||||
private set
|
||||
|
||||
@@ -29,16 +40,16 @@ class WireGuardAutoTunnel : Application() {
|
||||
return instance.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
||||
}
|
||||
|
||||
fun requestTunnelTileServiceStateUpdate(context: Context) {
|
||||
fun requestTunnelTileServiceStateUpdate() {
|
||||
TileService.requestListeningState(
|
||||
context,
|
||||
instance,
|
||||
ComponentName(instance, TunnelControlTile::class.java),
|
||||
)
|
||||
}
|
||||
|
||||
fun requestAutoTunnelTileServiceUpdate(context: Context) {
|
||||
fun requestAutoTunnelTileServiceUpdate() {
|
||||
TileService.requestListeningState(
|
||||
context,
|
||||
instance,
|
||||
ComponentName(instance, AutoTunnelControlTile::class.java),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,12 +6,12 @@ import androidx.room.DeleteColumn
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import androidx.room.migration.AutoMigrationSpec
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
|
||||
@Database(
|
||||
entities = [Settings::class, TunnelConfig::class],
|
||||
version = 7,
|
||||
version = 8,
|
||||
autoMigrations =
|
||||
[
|
||||
AutoMigration(from = 1, to = 2),
|
||||
@@ -33,6 +33,7 @@ import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
to = 7,
|
||||
spec = RemoveLegacySettingColumnsMigration::class,
|
||||
),
|
||||
AutoMigration(7, 8),
|
||||
],
|
||||
exportSchema = true,
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@ import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
|
||||
@@ -5,7 +5,7 @@ import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@@ -20,6 +20,9 @@ interface TunnelConfigDao {
|
||||
@Query("SELECT * FROM TunnelConfig WHERE id=:id")
|
||||
suspend fun getById(id: Long): TunnelConfig?
|
||||
|
||||
@Query("SELECT * FROM TunnelConfig WHERE name=:name")
|
||||
suspend fun getByName(name: String): TunnelConfig?
|
||||
|
||||
@Query("SELECT * FROM TunnelConfig")
|
||||
suspend fun getAll(): TunnelConfigs
|
||||
|
||||
@@ -33,10 +36,10 @@ interface TunnelConfigDao {
|
||||
suspend fun findByTunnelNetworkName(name: String): TunnelConfigs
|
||||
|
||||
@Query("UPDATE TunnelConfig SET is_primary_tunnel = 0 WHERE is_primary_tunnel =1")
|
||||
fun resetPrimaryTunnel()
|
||||
suspend fun resetPrimaryTunnel()
|
||||
|
||||
@Query("UPDATE TunnelConfig SET is_mobile_data_tunnel = 0 WHERE is_mobile_data_tunnel =1")
|
||||
fun resetMobileDataTunnel()
|
||||
suspend fun resetMobileDataTunnel()
|
||||
|
||||
@Query("SELECT * FROM TUNNELCONFIG WHERE is_primary_tunnel=1")
|
||||
suspend fun findByPrimary(): TunnelConfigs
|
||||
|
||||
+29
-16
@@ -7,14 +7,20 @@ import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.intPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
|
||||
class DataStoreManager(private val context: Context) {
|
||||
class DataStoreManager(
|
||||
private val context: Context,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
|
||||
) {
|
||||
companion object {
|
||||
val LOCATION_DISCLOSURE_SHOWN = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN")
|
||||
val BATTERY_OPTIMIZE_DISABLE_SHOWN = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN")
|
||||
@@ -22,6 +28,7 @@ class DataStoreManager(private val context: Context) {
|
||||
booleanPreferencesKey("TUNNEL_RUNNING_FROM_MANUAL_START")
|
||||
val ACTIVE_TUNNEL = intPreferencesKey("ACTIVE_TUNNEL")
|
||||
val CURRENT_SSID = stringPreferencesKey("CURRENT_SSID")
|
||||
val IS_PIN_LOCK_ENABLED = booleanPreferencesKey("PIN_LOCK_ENABLED")
|
||||
}
|
||||
|
||||
// preferences
|
||||
@@ -32,20 +39,24 @@ class DataStoreManager(private val context: Context) {
|
||||
)
|
||||
|
||||
suspend fun init() {
|
||||
try {
|
||||
context.dataStore.data.first()
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
withContext(ioDispatcher) {
|
||||
try {
|
||||
context.dataStore.data.first()
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun <T> saveToDataStore(key: Preferences.Key<T>, value: T) {
|
||||
try {
|
||||
context.dataStore.edit { it[key] = value }
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
withContext(ioDispatcher) {
|
||||
try {
|
||||
context.dataStore.edit { it[key] = value }
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,11 +64,13 @@ class DataStoreManager(private val context: Context) {
|
||||
fun <T> getFromStoreFlow(key: Preferences.Key<T>) = context.dataStore.data.map { it[key] }
|
||||
|
||||
suspend fun <T> getFromStore(key: Preferences.Key<T>): T? {
|
||||
return try {
|
||||
context.dataStore.data.map { it[key] }.first()
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
null
|
||||
return withContext(ioDispatcher) {
|
||||
try {
|
||||
context.dataStore.data.map { it[key] }.first()
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.domain
|
||||
|
||||
data class GeneralState(
|
||||
val isLocationDisclosureShown: Boolean = LOCATION_DISCLOSURE_SHOWN_DEFAULT,
|
||||
val isBatteryOptimizationDisableShown: Boolean = BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT,
|
||||
val isTunnelRunningFromManualStart: Boolean = TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT,
|
||||
val isPinLockEnabled: Boolean = PIN_LOCK_ENABLED_DEFAULT,
|
||||
val activeTunnelId: Int? = null
|
||||
) {
|
||||
companion object {
|
||||
const val LOCATION_DISCLOSURE_SHOWN_DEFAULT = false
|
||||
const val BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT = false
|
||||
const val TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT = false
|
||||
const val PIN_LOCK_ENABLED_DEFAULT = false
|
||||
}
|
||||
}
|
||||
+6
-1
@@ -1,4 +1,4 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.model
|
||||
package com.zaneschepke.wireguardautotunnel.data.domain
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
@@ -50,4 +50,9 @@ data class Settings(
|
||||
defaultValue = "false",
|
||||
)
|
||||
val isPingEnabled: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "is_amnezia_enabled",
|
||||
defaultValue = "false",
|
||||
)
|
||||
val isAmneziaEnabled: Boolean = false,
|
||||
)
|
||||
+19
-4
@@ -1,4 +1,4 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.model
|
||||
package com.zaneschepke.wireguardautotunnel.data.domain
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
@@ -27,12 +27,27 @@ data class TunnelConfig(
|
||||
defaultValue = "false",
|
||||
)
|
||||
val isPrimaryTunnel: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "am_quick",
|
||||
defaultValue = "",
|
||||
)
|
||||
val amQuick: String = AM_QUICK_DEFAULT,
|
||||
) {
|
||||
companion object {
|
||||
fun configFromQuick(wgQuick: String): Config {
|
||||
fun configFromWgQuick(wgQuick: String): Config {
|
||||
val inputStream: InputStream = wgQuick.byteInputStream()
|
||||
val reader = inputStream.bufferedReader(Charsets.UTF_8)
|
||||
return Config.parse(reader)
|
||||
return inputStream.bufferedReader(Charsets.UTF_8).use {
|
||||
Config.parse(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun configFromAmQuick(amQuick: String): org.amnezia.awg.config.Config {
|
||||
val inputStream: InputStream = amQuick.byteInputStream()
|
||||
return inputStream.bufferedReader(Charsets.UTF_8).use {
|
||||
org.amnezia.awg.config.Config.parse(it)
|
||||
}
|
||||
}
|
||||
|
||||
const val AM_QUICK_DEFAULT = ""
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.model
|
||||
|
||||
data class GeneralState(
|
||||
val locationDisclosureShown: Boolean = LOCATION_DISCLOSURE_SHOWN_DEFAULT,
|
||||
val batteryOptimizationDisableShown: Boolean = BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT,
|
||||
val tunnelRunningFromManualStart: Boolean = TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT,
|
||||
val activeTunnelId: Int? = null
|
||||
) {
|
||||
companion object {
|
||||
const val LOCATION_DISCLOSURE_SHOWN_DEFAULT = false
|
||||
const val BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT = false
|
||||
const val TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT = false
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
|
||||
interface AppDataRepository {
|
||||
suspend fun getPrimaryOrFirstTunnel(): TunnelConfig?
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import javax.inject.Inject
|
||||
|
||||
class AppDataRoomRepository @Inject constructor(
|
||||
|
||||
+4
-1
@@ -1,12 +1,15 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.GeneralState
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.GeneralState
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface AppStateRepository {
|
||||
suspend fun isLocationDisclosureShown(): Boolean
|
||||
suspend fun setLocationDisclosureShown(shown: Boolean)
|
||||
|
||||
suspend fun isPinLockEnabled(): Boolean
|
||||
suspend fun setPinLockEnabled(enabled: Boolean)
|
||||
|
||||
suspend fun isBatteryOptimizationDisableShown(): Boolean
|
||||
suspend fun setBatteryOptimizationDisableShown(shown: Boolean)
|
||||
|
||||
|
||||
+15
-4
@@ -1,7 +1,7 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.GeneralState
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.GeneralState
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import timber.log.Timber
|
||||
@@ -17,6 +17,15 @@ class DataStoreAppStateRepository(private val dataStoreManager: DataStoreManager
|
||||
dataStoreManager.saveToDataStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN, shown)
|
||||
}
|
||||
|
||||
override suspend fun isPinLockEnabled(): Boolean {
|
||||
return dataStoreManager.getFromStore(DataStoreManager.IS_PIN_LOCK_ENABLED)
|
||||
?: GeneralState.PIN_LOCK_ENABLED_DEFAULT
|
||||
}
|
||||
|
||||
override suspend fun setPinLockEnabled(enabled: Boolean) {
|
||||
dataStoreManager.saveToDataStore(DataStoreManager.IS_PIN_LOCK_ENABLED, enabled)
|
||||
}
|
||||
|
||||
override suspend fun isBatteryOptimizationDisableShown(): Boolean {
|
||||
return dataStoreManager.getFromStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN)
|
||||
?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT
|
||||
@@ -65,11 +74,13 @@ class DataStoreAppStateRepository(private val dataStoreManager: DataStoreManager
|
||||
prefs?.let { pref ->
|
||||
try {
|
||||
GeneralState(
|
||||
locationDisclosureShown = pref[DataStoreManager.LOCATION_DISCLOSURE_SHOWN]
|
||||
isLocationDisclosureShown = pref[DataStoreManager.LOCATION_DISCLOSURE_SHOWN]
|
||||
?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT,
|
||||
batteryOptimizationDisableShown = pref[DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN]
|
||||
isBatteryOptimizationDisableShown = pref[DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN]
|
||||
?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT,
|
||||
tunnelRunningFromManualStart = pref[DataStoreManager.TUNNEL_RUNNING_FROM_MANUAL_START]
|
||||
isTunnelRunningFromManualStart = pref[DataStoreManager.TUNNEL_RUNNING_FROM_MANUAL_START]
|
||||
?: GeneralState.TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT,
|
||||
isPinLockEnabled = pref[DataStoreManager.IS_PIN_LOCK_ENABLED]
|
||||
?: GeneralState.TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT,
|
||||
)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.SettingsDao
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class RoomSettingsRepository(private val settingsDoa: SettingsDao) : SettingsRepository {
|
||||
|
||||
+5
-1
@@ -1,7 +1,7 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@@ -54,6 +54,10 @@ class RoomTunnelConfigRepository(private val tunnelConfigDao: TunnelConfigDao) :
|
||||
return tunnelConfigDao.count().toInt()
|
||||
}
|
||||
|
||||
override suspend fun findByTunnelName(name: String): TunnelConfig? {
|
||||
return tunnelConfigDao.getByName(name)
|
||||
}
|
||||
|
||||
override suspend fun findByTunnelNetworksName(name: String): TunnelConfigs {
|
||||
return tunnelConfigDao.findByTunnelNetworkName(name)
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface SettingsRepository {
|
||||
|
||||
+3
-1
@@ -1,6 +1,6 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@@ -22,6 +22,8 @@ interface TunnelConfigRepository {
|
||||
|
||||
suspend fun count(): Int
|
||||
|
||||
suspend fun findByTunnelName(name: String): TunnelConfig?
|
||||
|
||||
suspend fun findByTunnelNetworksName(name: String): TunnelConfigs
|
||||
|
||||
suspend fun findByMobileDataTunnel(): TunnelConfigs
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.zaneschepke.wireguardautotunnel.module
|
||||
|
||||
import android.content.Context
|
||||
import com.zaneschepke.logcatter.LocalLogCollector
|
||||
import com.zaneschepke.logcatter.LogcatHelper
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
class AppModule {
|
||||
@Singleton
|
||||
@ApplicationScope
|
||||
@Provides
|
||||
fun providesApplicationScope(@DefaultDispatcher defaultDispatcher: CoroutineDispatcher): CoroutineScope =
|
||||
CoroutineScope(SupervisorJob() + defaultDispatcher)
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideLogCollect(@ApplicationContext context: Context): LocalLogCollector {
|
||||
return LogcatHelper.init(context = context)
|
||||
}
|
||||
}
|
||||
+4
@@ -2,6 +2,10 @@ package com.zaneschepke.wireguardautotunnel.module
|
||||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Qualifier
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
annotation class Kernel
|
||||
|
||||
@Qualifier
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
annotation class Userspace
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.zaneschepke.wireguardautotunnel.module
|
||||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@Qualifier
|
||||
annotation class DefaultDispatcher
|
||||
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@Qualifier
|
||||
annotation class IoDispatcher
|
||||
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@Qualifier
|
||||
annotation class MainDispatcher
|
||||
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
@Qualifier
|
||||
annotation class MainImmediateDispatcher
|
||||
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@Qualifier
|
||||
annotation class ApplicationScope
|
||||
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@Qualifier
|
||||
annotation class ServiceScope
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package com.zaneschepke.wireguardautotunnel.module
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object CoroutinesDispatchersModule {
|
||||
@DefaultDispatcher
|
||||
@Provides
|
||||
fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
|
||||
|
||||
@IoDispatcher
|
||||
@Provides
|
||||
fun providesIoDispatcher(): CoroutineDispatcher = Dispatchers.IO
|
||||
|
||||
@MainDispatcher
|
||||
@Provides
|
||||
fun providesMainDispatcher(): CoroutineDispatcher = Dispatchers.Main
|
||||
|
||||
@MainImmediateDispatcher
|
||||
@Provides
|
||||
fun providesMainImmediateDispatcher(): CoroutineDispatcher = Dispatchers.Main.immediate
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.module
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
||||
import com.zaneschepke.wireguardautotunnel.data.DatabaseCallback
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
class DatabaseModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
|
||||
return Room.databaseBuilder(
|
||||
context,
|
||||
AppDatabase::class.java,
|
||||
context.getString(R.string.db_name),
|
||||
)
|
||||
.fallbackToDestructiveMigration()
|
||||
.addCallback(DatabaseCallback())
|
||||
.build()
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.module
|
||||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Qualifier
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
annotation class Kernel
|
||||
@@ -1,7 +1,10 @@
|
||||
package com.zaneschepke.wireguardautotunnel.module
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
||||
import com.zaneschepke.wireguardautotunnel.data.DatabaseCallback
|
||||
import com.zaneschepke.wireguardautotunnel.data.SettingsDao
|
||||
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
|
||||
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
|
||||
@@ -18,11 +21,25 @@ import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
class RepositoryModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
|
||||
return Room.databaseBuilder(
|
||||
context,
|
||||
AppDatabase::class.java,
|
||||
context.getString(R.string.db_name),
|
||||
)
|
||||
.fallbackToDestructiveMigration()
|
||||
.addCallback(DatabaseCallback())
|
||||
.build()
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideSettingsDoa(appDatabase: AppDatabase): SettingsDao {
|
||||
@@ -49,8 +66,11 @@ class RepositoryModule {
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun providePreferencesDataStore(@ApplicationContext context: Context): DataStoreManager {
|
||||
return DataStoreManager(context)
|
||||
fun providePreferencesDataStore(
|
||||
@ApplicationContext context: Context,
|
||||
@IoDispatcher ioDispatcher: CoroutineDispatcher
|
||||
): DataStoreManager {
|
||||
return DataStoreManager(context, ioDispatcher)
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
||||
@@ -15,6 +15,9 @@ import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import javax.inject.Provider
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@@ -42,17 +45,36 @@ class TunnelModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideVpnService(
|
||||
@Userspace userspaceBackend: Backend,
|
||||
@Kernel kernelBackend: Backend,
|
||||
appDataRepository: AppDataRepository
|
||||
): VpnService {
|
||||
return WireGuardTunnel(userspaceBackend, kernelBackend, appDataRepository)
|
||||
fun provideAmneziaBackend(@ApplicationContext context: Context): org.amnezia.awg.backend.Backend {
|
||||
return org.amnezia.awg.backend.GoBackend(context)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideServiceManager(appDataRepository: AppDataRepository): ServiceManager {
|
||||
return ServiceManager(appDataRepository)
|
||||
fun provideVpnService(
|
||||
amneziaBackend: Provider<org.amnezia.awg.backend.Backend>,
|
||||
@Userspace userspaceBackend: Provider<Backend>,
|
||||
@Kernel kernelBackend: Provider<Backend>,
|
||||
appDataRepository: AppDataRepository,
|
||||
@ApplicationScope applicationScope: CoroutineScope,
|
||||
@IoDispatcher ioDispatcher: CoroutineDispatcher
|
||||
): VpnService {
|
||||
return WireGuardTunnel(
|
||||
amneziaBackend,
|
||||
userspaceBackend,
|
||||
kernelBackend,
|
||||
appDataRepository,
|
||||
applicationScope,
|
||||
ioDispatcher,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideServiceManager(
|
||||
appDataRepository: AppDataRepository,
|
||||
@IoDispatcher ioDispatcher: CoroutineDispatcher
|
||||
): ServiceManager {
|
||||
return ServiceManager(appDataRepository, ioDispatcher)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.zaneschepke.wireguardautotunnel.module
|
||||
|
||||
import android.content.Context
|
||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.components.ViewModelComponent
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.android.scopes.ViewModelScoped
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
|
||||
@Module
|
||||
@InstallIn(ViewModelComponent::class)
|
||||
class ViewModelModule {
|
||||
|
||||
@ViewModelScoped
|
||||
@Provides
|
||||
fun provideFileUtils(
|
||||
@ApplicationContext context: Context,
|
||||
@IoDispatcher ioDispatcher: CoroutineDispatcher
|
||||
): FileUtils {
|
||||
return FileUtils(context, ioDispatcher)
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,11 @@ import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.util.goAsync
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -19,27 +21,36 @@ class BootReceiver : BroadcastReceiver() {
|
||||
@Inject
|
||||
lateinit var serviceManager: ServiceManager
|
||||
|
||||
override fun onReceive(context: Context?, intent: Intent?) = goAsync {
|
||||
if (Intent.ACTION_BOOT_COMPLETED != intent?.action) return@goAsync
|
||||
@Inject
|
||||
@ApplicationScope
|
||||
lateinit var applicationScope: CoroutineScope
|
||||
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
if (Intent.ACTION_BOOT_COMPLETED != intent?.action) return
|
||||
context?.run {
|
||||
val settings = appDataRepository.settings.getSettings()
|
||||
if (settings.isAutoTunnelEnabled) {
|
||||
Timber.i("Starting watcher service from boot")
|
||||
serviceManager.startWatcherServiceForeground(context)
|
||||
}
|
||||
if (appDataRepository.appState.isTunnelRunningFromManualStart()) {
|
||||
appDataRepository.appState.getActiveTunnelId()?.let {
|
||||
Timber.i("Starting tunnel that was active before reboot")
|
||||
serviceManager.startVpnServiceForeground(
|
||||
context,
|
||||
appDataRepository.tunnels.getById(it)?.id,
|
||||
)
|
||||
applicationScope.launch {
|
||||
val settings = appDataRepository.settings.getSettings()
|
||||
if(settings.isRestoreOnBootEnabled) {
|
||||
if (settings.isAutoTunnelEnabled) {
|
||||
Timber.i("Starting watcher service from boot")
|
||||
serviceManager.startWatcherServiceForeground(context)
|
||||
}
|
||||
if (appDataRepository.appState.isTunnelRunningFromManualStart()) {
|
||||
appDataRepository.appState.getActiveTunnelId()?.let {
|
||||
Timber.i("Starting tunnel that was active before reboot")
|
||||
serviceManager.startVpnServiceForeground(
|
||||
context,
|
||||
appDataRepository.tunnels.getById(it)?.id,
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
if (settings.isAlwaysOnVpnEnabled) {
|
||||
Timber.i("Starting vpn service from boot AOVPN")
|
||||
serviceManager.startVpnServiceForeground(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (settings.isAlwaysOnVpnEnabled) {
|
||||
Timber.i("Starting vpn service from boot AOVPN")
|
||||
serviceManager.startVpnServiceForeground(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+19
-11
@@ -4,12 +4,14 @@ import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.goAsync
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -21,16 +23,22 @@ class NotificationActionReceiver : BroadcastReceiver() {
|
||||
@Inject
|
||||
lateinit var serviceManager: ServiceManager
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent?) = goAsync {
|
||||
try {
|
||||
//TODO fix for manual start changes when enabled
|
||||
serviceManager.stopVpnServiceForeground(context)
|
||||
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
||||
serviceManager.startVpnServiceForeground(context)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
} finally {
|
||||
cancel()
|
||||
@Inject
|
||||
@ApplicationScope
|
||||
lateinit var applicationScope: CoroutineScope
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
applicationScope.launch {
|
||||
try {
|
||||
//TODO fix for manual start changes when enabled
|
||||
serviceManager.stopVpnServiceForeground(context)
|
||||
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
||||
serviceManager.startVpnServiceForeground(context)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
} finally {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -24,6 +24,7 @@ open class ForegroundService : LifecycleService() {
|
||||
when (action) {
|
||||
Action.START.name,
|
||||
Action.START_FOREGROUND.name -> startService(intent.extras)
|
||||
|
||||
Action.STOP.name, Action.STOP_FOREGROUND.name -> stopService()
|
||||
Constants.ALWAYS_ON_VPN_ACTION -> {
|
||||
Timber.i("Always-on VPN starting service")
|
||||
|
||||
+38
-23
@@ -4,10 +4,16 @@ import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
class ServiceManager(private val appDataRepository: AppDataRepository) {
|
||||
class ServiceManager(
|
||||
private val appDataRepository: AppDataRepository,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
|
||||
) {
|
||||
|
||||
private fun <T : Service> actionOnService(
|
||||
action: Action,
|
||||
@@ -23,7 +29,10 @@ class ServiceManager(private val appDataRepository: AppDataRepository) {
|
||||
intent.component?.javaClass
|
||||
try {
|
||||
when (action) {
|
||||
Action.START_FOREGROUND, Action.STOP_FOREGROUND -> context.startForegroundService(intent)
|
||||
Action.START_FOREGROUND, Action.STOP_FOREGROUND -> context.startForegroundService(
|
||||
intent,
|
||||
)
|
||||
|
||||
Action.START, Action.STOP -> context.startService(intent)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
@@ -46,23 +55,27 @@ class ServiceManager(private val appDataRepository: AppDataRepository) {
|
||||
}
|
||||
|
||||
suspend fun stopVpnServiceForeground(context: Context, isManualStop: Boolean = false) {
|
||||
if (isManualStop) onManualStop()
|
||||
Timber.i("Stopping vpn service")
|
||||
actionOnService(
|
||||
Action.STOP_FOREGROUND,
|
||||
context,
|
||||
WireGuardTunnelService::class.java,
|
||||
)
|
||||
withContext(ioDispatcher) {
|
||||
if (isManualStop) onManualStop()
|
||||
Timber.i("Stopping vpn service")
|
||||
actionOnService(
|
||||
Action.STOP_FOREGROUND,
|
||||
context,
|
||||
WireGuardTunnelService::class.java,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun stopVpnService(context: Context, isManualStop: Boolean = false) {
|
||||
if (isManualStop) onManualStop()
|
||||
Timber.i("Stopping vpn service")
|
||||
actionOnService(
|
||||
Action.STOP,
|
||||
context,
|
||||
WireGuardTunnelService::class.java,
|
||||
)
|
||||
withContext(ioDispatcher) {
|
||||
if (isManualStop) onManualStop()
|
||||
Timber.i("Stopping vpn service")
|
||||
actionOnService(
|
||||
Action.STOP,
|
||||
context,
|
||||
WireGuardTunnelService::class.java,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun onManualStop() {
|
||||
@@ -80,13 +93,15 @@ class ServiceManager(private val appDataRepository: AppDataRepository) {
|
||||
tunnelId: Int? = null,
|
||||
isManualStart: Boolean = false
|
||||
) {
|
||||
if (isManualStart) onManualStart(tunnelId)
|
||||
actionOnService(
|
||||
Action.START_FOREGROUND,
|
||||
context,
|
||||
WireGuardTunnelService::class.java,
|
||||
tunnelId?.let { mapOf(Constants.TUNNEL_EXTRA_KEY to it) },
|
||||
)
|
||||
withContext(ioDispatcher) {
|
||||
if (isManualStart) onManualStart(tunnelId)
|
||||
actionOnService(
|
||||
Action.START_FOREGROUND,
|
||||
context,
|
||||
WireGuardTunnelService::class.java,
|
||||
tunnelId?.let { mapOf(Constants.TUNNEL_EXTRA_KEY to it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun startWatcherServiceForeground(
|
||||
|
||||
+8
-36
@@ -1,84 +1,56 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.foreground
|
||||
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
|
||||
|
||||
data class WatcherState(
|
||||
val isWifiConnected: Boolean = false,
|
||||
val config: TunnelConfig? = null,
|
||||
val vpnStatus: Tunnel.State = Tunnel.State.DOWN,
|
||||
val isEthernetConnected: Boolean = false,
|
||||
val isMobileDataConnected: Boolean = false,
|
||||
val currentNetworkSSID: String = "",
|
||||
val settings: Settings = Settings()
|
||||
) {
|
||||
|
||||
private fun isVpnConnected() = vpnStatus == Tunnel.State.UP
|
||||
fun isEthernetConditionMet(): Boolean {
|
||||
return (isEthernetConnected &&
|
||||
settings.isTunnelOnEthernetEnabled &&
|
||||
!isVpnConnected())
|
||||
settings.isTunnelOnEthernetEnabled)
|
||||
}
|
||||
|
||||
fun isMobileDataConditionMet(): Boolean {
|
||||
return (!isEthernetConnected &&
|
||||
settings.isTunnelOnMobileDataEnabled &&
|
||||
!isWifiConnected &&
|
||||
isMobileDataConnected &&
|
||||
!isVpnConnected())
|
||||
}
|
||||
|
||||
fun isTunnelNotMobileDataPreferredConditionMet(): Boolean {
|
||||
return (!isEthernetConnected &&
|
||||
settings.isTunnelOnMobileDataEnabled &&
|
||||
!isWifiConnected &&
|
||||
isMobileDataConnected &&
|
||||
config?.isMobileDataTunnel == false && isVpnConnected())
|
||||
isMobileDataConnected)
|
||||
}
|
||||
|
||||
fun isTunnelOffOnMobileDataConditionMet(): Boolean {
|
||||
return (!isEthernetConnected &&
|
||||
!settings.isTunnelOnMobileDataEnabled &&
|
||||
isMobileDataConnected &&
|
||||
!isWifiConnected &&
|
||||
isVpnConnected())
|
||||
!isWifiConnected)
|
||||
}
|
||||
|
||||
fun isUntrustedWifiConditionMet(): Boolean {
|
||||
return (!isEthernetConnected &&
|
||||
isWifiConnected &&
|
||||
!settings.trustedNetworkSSIDs.contains(currentNetworkSSID) &&
|
||||
settings.isTunnelOnWifiEnabled
|
||||
&& !isVpnConnected())
|
||||
}
|
||||
|
||||
fun isTunnelNotWifiNamePreferredMet(ssid: String): Boolean {
|
||||
return (!isEthernetConnected &&
|
||||
isWifiConnected &&
|
||||
!settings.trustedNetworkSSIDs.contains(currentNetworkSSID) &&
|
||||
settings.isTunnelOnWifiEnabled && config?.tunnelNetworks?.contains(ssid) == false && isVpnConnected())
|
||||
settings.isTunnelOnWifiEnabled)
|
||||
}
|
||||
|
||||
fun isTrustedWifiConditionMet(): Boolean {
|
||||
return (!isEthernetConnected &&
|
||||
(isWifiConnected &&
|
||||
settings.trustedNetworkSSIDs.contains(currentNetworkSSID)) &&
|
||||
(isVpnConnected()))
|
||||
settings.trustedNetworkSSIDs.contains(currentNetworkSSID)))
|
||||
}
|
||||
|
||||
fun isTunnelOffOnWifiConditionMet(): Boolean {
|
||||
return (!isEthernetConnected &&
|
||||
(isWifiConnected &&
|
||||
!settings.isTunnelOnWifiEnabled &&
|
||||
(isVpnConnected())))
|
||||
!settings.isTunnelOnWifiEnabled))
|
||||
}
|
||||
|
||||
fun isTunnelOffOnNoConnectivityMet(): Boolean {
|
||||
return (!isEthernetConnected &&
|
||||
!isWifiConnected &&
|
||||
!isMobileDataConnected &&
|
||||
(isVpnConnected()))
|
||||
!isMobileDataConnected)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+218
-192
@@ -5,26 +5,30 @@ import android.os.Bundle
|
||||
import android.os.PowerManager
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.module.MainImmediateDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
|
||||
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
|
||||
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
|
||||
import com.zaneschepke.wireguardautotunnel.service.network.NetworkStatus
|
||||
import com.zaneschepke.wireguardautotunnel.service.network.WifiService
|
||||
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.net.InetAddress
|
||||
import javax.inject.Inject
|
||||
@@ -55,6 +59,14 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
@Inject
|
||||
lateinit var serviceManager: ServiceManager
|
||||
|
||||
@Inject
|
||||
@IoDispatcher
|
||||
lateinit var ioDispatcher: CoroutineDispatcher
|
||||
|
||||
@Inject
|
||||
@MainImmediateDispatcher
|
||||
lateinit var mainImmediateDispatcher: CoroutineDispatcher
|
||||
|
||||
private val networkEventsFlow = MutableStateFlow(WatcherState())
|
||||
|
||||
private var watcherJob: Job? = null
|
||||
@@ -64,7 +76,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
lifecycleScope.launch(mainImmediateDispatcher) {
|
||||
try {
|
||||
if (appDataRepository.settings.getSettings().isAutoTunnelPaused) {
|
||||
launchWatcherPausedNotification()
|
||||
@@ -137,14 +149,14 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
private fun cancelWatcherJob() {
|
||||
try {
|
||||
watcherJob?.cancel()
|
||||
} catch (e : CancellationException) {
|
||||
} catch (e: CancellationException) {
|
||||
Timber.i("Watcher job cancelled")
|
||||
}
|
||||
}
|
||||
|
||||
private fun startWatcherJob() {
|
||||
watcherJob =
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
lifecycleScope.launch {
|
||||
val setting = appDataRepository.settings.getSettings()
|
||||
launch {
|
||||
Timber.i("Starting wifi watcher")
|
||||
@@ -162,10 +174,6 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
watchForEthernetConnectivityChanges()
|
||||
}
|
||||
}
|
||||
launch {
|
||||
Timber.i("Starting vpn state watcher")
|
||||
watchForVpnConnectivityChanges()
|
||||
}
|
||||
launch {
|
||||
Timber.i("Starting settings watcher")
|
||||
watchForSettingsChanges()
|
||||
@@ -185,160 +193,170 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
}
|
||||
|
||||
private suspend fun watchForMobileDataConnectivityChanges() {
|
||||
mobileDataService.networkStatus.collect {
|
||||
when (it) {
|
||||
is NetworkStatus.Available -> {
|
||||
Timber.i("Gained Mobile data connection")
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isMobileDataConnected = true,
|
||||
)
|
||||
}
|
||||
withContext(ioDispatcher) {
|
||||
mobileDataService.networkStatus.collect { status ->
|
||||
when (status) {
|
||||
is NetworkStatus.Available -> {
|
||||
Timber.i("Gained Mobile data connection")
|
||||
networkEventsFlow.update {
|
||||
it.copy(
|
||||
isMobileDataConnected = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is NetworkStatus.CapabilitiesChanged -> {
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isMobileDataConnected = true,
|
||||
)
|
||||
Timber.i("Mobile data capabilities changed")
|
||||
}
|
||||
is NetworkStatus.CapabilitiesChanged -> {
|
||||
networkEventsFlow.update {
|
||||
it.copy(
|
||||
isMobileDataConnected = true,
|
||||
)
|
||||
}
|
||||
Timber.i("Mobile data capabilities changed")
|
||||
}
|
||||
|
||||
is NetworkStatus.Unavailable -> {
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isMobileDataConnected = false,
|
||||
)
|
||||
Timber.i("Lost mobile data connection")
|
||||
is NetworkStatus.Unavailable -> {
|
||||
networkEventsFlow.update {
|
||||
it.copy(
|
||||
isMobileDataConnected = false,
|
||||
)
|
||||
}
|
||||
Timber.i("Lost mobile data connection")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun watchForPingFailure() {
|
||||
try {
|
||||
do {
|
||||
if (vpnService.vpnState.value.status == Tunnel.State.UP) {
|
||||
val tunnelConfig = vpnService.vpnState.value.tunnelConfig
|
||||
tunnelConfig?.let {
|
||||
val config = TunnelConfig.configFromQuick(it.wgQuick)
|
||||
val results = config.peers.map { peer ->
|
||||
val host = if (peer.endpoint.isPresent &&
|
||||
peer.endpoint.get().resolved.isPresent)
|
||||
peer.endpoint.get().resolved.get().host
|
||||
else Constants.BACKUP_PING_HOST
|
||||
Timber.i("Checking reachability of: $host")
|
||||
val reachable = InetAddress.getByName(host)
|
||||
.isReachable(Constants.PING_TIMEOUT.toInt())
|
||||
Timber.i("Result: reachable - $reachable")
|
||||
reachable
|
||||
}
|
||||
if (results.contains(false)) {
|
||||
Timber.i("Restarting VPN for ping failure")
|
||||
serviceManager.stopVpnServiceForeground(this)
|
||||
delay(Constants.VPN_RESTART_DELAY)
|
||||
serviceManager.startVpnServiceForeground(this)
|
||||
delay(Constants.PING_COOLDOWN)
|
||||
val context = this
|
||||
withContext(ioDispatcher) {
|
||||
try {
|
||||
do {
|
||||
if (vpnService.vpnState.value.status == TunnelState.UP) {
|
||||
val tunnelConfig = vpnService.vpnState.value.tunnelConfig
|
||||
tunnelConfig?.let {
|
||||
val config = TunnelConfig.configFromWgQuick(it.wgQuick)
|
||||
val results = config.peers.map { peer ->
|
||||
val host = if (peer.endpoint.isPresent &&
|
||||
peer.endpoint.get().resolved.isPresent)
|
||||
peer.endpoint.get().resolved.get().host
|
||||
else Constants.DEFAULT_PING_IP
|
||||
Timber.i("Checking reachability of: $host")
|
||||
val reachable = InetAddress.getByName(host)
|
||||
.isReachable(Constants.PING_TIMEOUT.toInt())
|
||||
Timber.i("Result: reachable - $reachable")
|
||||
reachable
|
||||
}
|
||||
if (results.contains(false)) {
|
||||
Timber.i("Restarting VPN for ping failure")
|
||||
serviceManager.stopVpnServiceForeground(context)
|
||||
delay(Constants.VPN_RESTART_DELAY)
|
||||
serviceManager.startVpnServiceForeground(context, it.id)
|
||||
delay(Constants.PING_COOLDOWN)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
delay(Constants.PING_INTERVAL)
|
||||
} while (true)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
delay(Constants.PING_INTERVAL)
|
||||
} while (true)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun watchForSettingsChanges() {
|
||||
appDataRepository.settings.getSettingsFlow().collect {
|
||||
if (networkEventsFlow.value.settings.isAutoTunnelPaused != it.isAutoTunnelPaused) {
|
||||
when (it.isAutoTunnelPaused) {
|
||||
appDataRepository.settings.getSettingsFlow().collect { settings ->
|
||||
if (networkEventsFlow.value.settings.isAutoTunnelPaused != settings.isAutoTunnelPaused) {
|
||||
when (settings.isAutoTunnelPaused) {
|
||||
true -> launchWatcherPausedNotification()
|
||||
false -> launchWatcherNotification()
|
||||
}
|
||||
}
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
settings = it,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun watchForVpnConnectivityChanges() {
|
||||
vpnService.vpnState.collect {
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
vpnStatus = it.status,
|
||||
config = it.tunnelConfig,
|
||||
networkEventsFlow.update {
|
||||
it.copy(
|
||||
settings = settings,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun watchForEthernetConnectivityChanges() {
|
||||
ethernetService.networkStatus.collect {
|
||||
when (it) {
|
||||
is NetworkStatus.Available -> {
|
||||
Timber.i("Gained Ethernet connection")
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isEthernetConnected = true,
|
||||
)
|
||||
}
|
||||
withContext(ioDispatcher) {
|
||||
ethernetService.networkStatus.collect { status ->
|
||||
when (status) {
|
||||
is NetworkStatus.Available -> {
|
||||
Timber.i("Gained Ethernet connection")
|
||||
networkEventsFlow.update {
|
||||
it.copy(
|
||||
isEthernetConnected = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is NetworkStatus.CapabilitiesChanged -> {
|
||||
Timber.i("Ethernet capabilities changed")
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isEthernetConnected = true,
|
||||
)
|
||||
}
|
||||
is NetworkStatus.CapabilitiesChanged -> {
|
||||
Timber.i("Ethernet capabilities changed")
|
||||
networkEventsFlow.update {
|
||||
it.copy(
|
||||
isEthernetConnected = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is NetworkStatus.Unavailable -> {
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isEthernetConnected = false,
|
||||
)
|
||||
Timber.i("Lost Ethernet connection")
|
||||
is NetworkStatus.Unavailable -> {
|
||||
networkEventsFlow.update {
|
||||
it.copy(
|
||||
isEthernetConnected = false,
|
||||
)
|
||||
}
|
||||
Timber.i("Lost Ethernet connection")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun watchForWifiConnectivityChanges() {
|
||||
wifiService.networkStatus.collect {
|
||||
when (it) {
|
||||
is NetworkStatus.Available -> {
|
||||
Timber.i("Gained Wi-Fi connection")
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isWifiConnected = true,
|
||||
)
|
||||
}
|
||||
|
||||
is NetworkStatus.CapabilitiesChanged -> {
|
||||
Timber.i("Wifi capabilities changed")
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isWifiConnected = true,
|
||||
)
|
||||
val ssid = wifiService.getNetworkName(it.networkCapabilities)
|
||||
ssid?.let {
|
||||
if(it.contains(Constants.UNREADABLE_SSID)) {
|
||||
Timber.w("SSID unreadable: missing permissions")
|
||||
} else Timber.i("Detected valid SSID")
|
||||
appDataRepository.appState.setCurrentSsid(ssid)
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
currentNetworkSSID = ssid,
|
||||
withContext(ioDispatcher) {
|
||||
wifiService.networkStatus.collect { status ->
|
||||
when (status) {
|
||||
is NetworkStatus.Available -> {
|
||||
Timber.i("Gained Wi-Fi connection")
|
||||
networkEventsFlow.update {
|
||||
it.copy(
|
||||
isWifiConnected = true,
|
||||
)
|
||||
} ?: Timber.w("Failed to read ssid")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is NetworkStatus.Unavailable -> {
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isWifiConnected = false,
|
||||
)
|
||||
Timber.i("Lost Wi-Fi connection")
|
||||
is NetworkStatus.CapabilitiesChanged -> {
|
||||
Timber.i("Wifi capabilities changed")
|
||||
networkEventsFlow.update {
|
||||
it.copy(
|
||||
isWifiConnected = true,
|
||||
)
|
||||
}
|
||||
val ssid = wifiService.getNetworkName(status.networkCapabilities)
|
||||
ssid?.let { name ->
|
||||
if (name.contains(Constants.UNREADABLE_SSID)) {
|
||||
Timber.w("SSID unreadable: missing permissions")
|
||||
} else Timber.i("Detected valid SSID")
|
||||
appDataRepository.appState.setCurrentSsid(name)
|
||||
networkEventsFlow.update {
|
||||
it.copy(
|
||||
currentNetworkSSID = name,
|
||||
)
|
||||
}
|
||||
} ?: Timber.w("Failed to read ssid")
|
||||
}
|
||||
|
||||
is NetworkStatus.Unavailable -> {
|
||||
networkEventsFlow.update {
|
||||
it.copy(
|
||||
isWifiConnected = false,
|
||||
)
|
||||
}
|
||||
Timber.i("Lost Wi-Fi connection")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -352,76 +370,84 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
return appDataRepository.tunnels.findByTunnelNetworksName(ssid).firstOrNull()
|
||||
}
|
||||
|
||||
private fun isTunnelDown(): Boolean {
|
||||
return vpnService.vpnState.value.status == TunnelState.DOWN
|
||||
}
|
||||
|
||||
private suspend fun manageVpn() {
|
||||
networkEventsFlow.collectLatest { watcherState ->
|
||||
val autoTunnel = "Auto-tunnel watcher"
|
||||
if (!watcherState.settings.isAutoTunnelPaused) {
|
||||
//delay for rapid network state changes and then collect latest
|
||||
delay(Constants.WATCHER_COLLECTION_DELAY)
|
||||
when {
|
||||
watcherState.isEthernetConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel on on ethernet condition met")
|
||||
serviceManager.startVpnServiceForeground(this)
|
||||
}
|
||||
|
||||
watcherState.isMobileDataConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel on on mobile data condition met")
|
||||
serviceManager.startVpnServiceForeground(this, getMobileDataTunnel()?.id)
|
||||
}
|
||||
|
||||
watcherState.isTunnelNotMobileDataPreferredConditionMet() -> {
|
||||
getMobileDataTunnel()?.let {
|
||||
Timber.i("$autoTunnel - tunnel connected on mobile data is not preferred condition met, switching to preferred")
|
||||
serviceManager.startVpnServiceForeground(
|
||||
this,
|
||||
getMobileDataTunnel()?.id,
|
||||
)
|
||||
val context = this
|
||||
withContext(ioDispatcher) {
|
||||
networkEventsFlow.collectLatest { watcherState ->
|
||||
val autoTunnel = "Auto-tunnel watcher"
|
||||
if (!watcherState.settings.isAutoTunnelPaused) {
|
||||
//delay for rapid network state changes and then collect latest
|
||||
delay(Constants.WATCHER_COLLECTION_DELAY)
|
||||
val tunnelConfig = vpnService.vpnState.value.tunnelConfig
|
||||
when {
|
||||
watcherState.isEthernetConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel on on ethernet condition met")
|
||||
if (isTunnelDown()) serviceManager.startVpnServiceForeground(context)
|
||||
}
|
||||
}
|
||||
|
||||
watcherState.isTunnelOffOnMobileDataConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel off on mobile data met, turning vpn off")
|
||||
serviceManager.stopVpnServiceForeground(this)
|
||||
}
|
||||
|
||||
watcherState.isTunnelNotWifiNamePreferredMet(watcherState.currentNetworkSSID) -> {
|
||||
Timber.i("$autoTunnel - tunnel on ssid not associated with current tunnel condition met")
|
||||
getSsidTunnel(watcherState.currentNetworkSSID)?.let {
|
||||
Timber.i("Found tunnel associated with this SSID, bringing tunnel up")
|
||||
serviceManager.startVpnServiceForeground(this, it.id)
|
||||
} ?: suspend {
|
||||
Timber.i("No tunnel associated with this SSID, using defaults")
|
||||
if (appDataRepository.getPrimaryOrFirstTunnel()?.name != vpnService.name) {
|
||||
serviceManager.startVpnServiceForeground(this)
|
||||
watcherState.isMobileDataConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel on mobile data condition met")
|
||||
val mobileDataTunnel = getMobileDataTunnel()
|
||||
val tunnel =
|
||||
mobileDataTunnel ?: appDataRepository.getPrimaryOrFirstTunnel()
|
||||
if (isTunnelDown() || tunnelConfig?.isMobileDataTunnel == false) {
|
||||
serviceManager.startVpnServiceForeground(
|
||||
context,
|
||||
tunnel?.id,
|
||||
)
|
||||
}
|
||||
}.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
watcherState.isUntrustedWifiConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel on untrusted wifi condition met")
|
||||
serviceManager.startVpnServiceForeground(
|
||||
this,
|
||||
getSsidTunnel(watcherState.currentNetworkSSID)?.id,
|
||||
)
|
||||
}
|
||||
watcherState.isTunnelOffOnMobileDataConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel off on mobile data met, turning vpn off")
|
||||
if (!isTunnelDown()) serviceManager.stopVpnServiceForeground(context)
|
||||
}
|
||||
|
||||
watcherState.isTrustedWifiConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel off on trusted wifi condition met, turning vpn off")
|
||||
serviceManager.stopVpnServiceForeground(this)
|
||||
}
|
||||
watcherState.isUntrustedWifiConditionMet() -> {
|
||||
if (tunnelConfig?.tunnelNetworks?.contains(watcherState.currentNetworkSSID) == false ||
|
||||
tunnelConfig == null) {
|
||||
Timber.i("$autoTunnel - tunnel on ssid not associated with current tunnel condition met")
|
||||
getSsidTunnel(watcherState.currentNetworkSSID)?.let {
|
||||
Timber.i("Found tunnel associated with this SSID, bringing tunnel up: ${it.name}")
|
||||
if (isTunnelDown() || tunnelConfig?.id != it.id) serviceManager.startVpnServiceForeground(
|
||||
context,
|
||||
it.id,
|
||||
)
|
||||
} ?: suspend {
|
||||
Timber.i("No tunnel associated with this SSID, using defaults")
|
||||
val default = appDataRepository.getPrimaryOrFirstTunnel()
|
||||
if (default?.name != vpnService.name) {
|
||||
default?.let {
|
||||
serviceManager.startVpnServiceForeground(context, it.id)
|
||||
}
|
||||
|
||||
watcherState.isTunnelOffOnWifiConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel off on wifi condition met, turning vpn off")
|
||||
serviceManager.stopVpnServiceForeground(this)
|
||||
}
|
||||
}
|
||||
}.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
watcherState.isTunnelOffOnNoConnectivityMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel off on no connectivity met, turning vpn off")
|
||||
serviceManager.stopVpnServiceForeground(this)
|
||||
}
|
||||
watcherState.isTrustedWifiConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel off on trusted wifi condition met, turning vpn off")
|
||||
if (!isTunnelDown()) serviceManager.stopVpnServiceForeground(context)
|
||||
}
|
||||
|
||||
else -> {
|
||||
Timber.i("$autoTunnel - no condition met")
|
||||
watcherState.isTunnelOffOnWifiConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel off on wifi condition met, turning vpn off")
|
||||
if (!isTunnelDown()) serviceManager.stopVpnServiceForeground(context)
|
||||
}
|
||||
|
||||
watcherState.isTunnelOffOnNoConnectivityMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel off on no connectivity met, turning vpn off")
|
||||
if (!isTunnelDown()) serviceManager.stopVpnServiceForeground(context)
|
||||
}
|
||||
|
||||
else -> {
|
||||
Timber.i("$autoTunnel - no condition met")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+48
-35
@@ -5,22 +5,25 @@ import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.module.MainImmediateDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver
|
||||
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
|
||||
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 kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -37,13 +40,21 @@ class WireGuardTunnelService : ForegroundService() {
|
||||
@Inject
|
||||
lateinit var notificationService: NotificationService
|
||||
|
||||
@Inject
|
||||
@MainImmediateDispatcher
|
||||
lateinit var mainImmediateDispatcher: CoroutineDispatcher
|
||||
|
||||
@Inject
|
||||
@IoDispatcher
|
||||
lateinit var ioDispatcher: CoroutineDispatcher
|
||||
|
||||
private var job: Job? = null
|
||||
|
||||
private var didShowConnected = false
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
lifecycleScope.launch(mainImmediateDispatcher) {
|
||||
//TODO fix this to not launch if AOVPN
|
||||
if (appDataRepository.tunnels.count() != 0) {
|
||||
launchVpnNotification()
|
||||
@@ -55,10 +66,10 @@ class WireGuardTunnelService : ForegroundService() {
|
||||
super.startService(extras)
|
||||
cancelJob()
|
||||
job =
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
lifecycleScope.launch {
|
||||
launch {
|
||||
val tunnelId = extras?.getInt(Constants.TUNNEL_EXTRA_KEY)
|
||||
if (vpnService.getState() == Tunnel.State.UP) {
|
||||
if (vpnService.getState() == TunnelState.UP) {
|
||||
vpnService.stopTunnel()
|
||||
}
|
||||
vpnService.startTunnel(
|
||||
@@ -75,39 +86,41 @@ class WireGuardTunnelService : ForegroundService() {
|
||||
|
||||
//TODO improve tunnel notifications
|
||||
private suspend fun handshakeNotifications() {
|
||||
var tunnelName: String? = null
|
||||
vpnService.vpnState.collect { state ->
|
||||
state.statistics
|
||||
?.mapPeerStats()
|
||||
?.map { it.value?.handshakeStatus() }
|
||||
.let { statuses ->
|
||||
when {
|
||||
statuses?.all { it == HandshakeStatus.HEALTHY } == true -> {
|
||||
if (!didShowConnected) {
|
||||
delay(Constants.VPN_CONNECTED_NOTIFICATION_DELAY)
|
||||
tunnelName = state.tunnelConfig?.name
|
||||
launchVpnNotification(
|
||||
getString(R.string.tunnel_start_title),
|
||||
"${getString(R.string.tunnel_start_text)} - $tunnelName",
|
||||
)
|
||||
didShowConnected = true
|
||||
withContext(ioDispatcher) {
|
||||
var tunnelName: String? = null
|
||||
vpnService.vpnState.collect { state ->
|
||||
state.statistics
|
||||
?.mapPeerStats()
|
||||
?.map { it.value?.handshakeStatus() }
|
||||
.let { statuses ->
|
||||
when {
|
||||
statuses?.all { it == HandshakeStatus.HEALTHY } == true -> {
|
||||
if (!didShowConnected) {
|
||||
delay(Constants.VPN_CONNECTED_NOTIFICATION_DELAY)
|
||||
tunnelName = state.tunnelConfig?.name
|
||||
launchVpnNotification(
|
||||
getString(R.string.tunnel_start_title),
|
||||
"${getString(R.string.tunnel_start_text)} - $tunnelName",
|
||||
)
|
||||
didShowConnected = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
statuses?.any { it == HandshakeStatus.STALE } == true -> {}
|
||||
statuses?.all { it == HandshakeStatus.NOT_STARTED } ==
|
||||
true -> {
|
||||
}
|
||||
statuses?.any { it == HandshakeStatus.STALE } == true -> {}
|
||||
statuses?.all { it == HandshakeStatus.NOT_STARTED } ==
|
||||
true -> {
|
||||
}
|
||||
|
||||
else -> {}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
if (state.status == TunnelState.UP && state.tunnelConfig?.name != tunnelName) {
|
||||
tunnelName = state.tunnelConfig?.name
|
||||
launchVpnNotification(
|
||||
getString(R.string.tunnel_start_title),
|
||||
"${getString(R.string.tunnel_start_text)} - $tunnelName",
|
||||
)
|
||||
}
|
||||
if (state.status == Tunnel.State.UP && state.tunnelConfig?.name != tunnelName) {
|
||||
tunnelName = state.tunnelConfig?.name
|
||||
launchVpnNotification(
|
||||
getString(R.string.tunnel_start_title),
|
||||
"${getString(R.string.tunnel_start_text)} - $tunnelName",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -121,7 +134,7 @@ class WireGuardTunnelService : ForegroundService() {
|
||||
|
||||
override fun stopService() {
|
||||
super.stopService()
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
lifecycleScope.launch {
|
||||
vpnService.stopTunnel()
|
||||
didShowConnected = false
|
||||
}
|
||||
@@ -181,7 +194,7 @@ class WireGuardTunnelService : ForegroundService() {
|
||||
private fun cancelJob() {
|
||||
try {
|
||||
job?.cancel()
|
||||
} catch (e : CancellationException) {
|
||||
} catch (e: CancellationException) {
|
||||
Timber.i("Tunnel job cancelled")
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -10,6 +10,7 @@ import android.graphics.Color
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.MainActivity
|
||||
import com.zaneschepke.wireguardautotunnel.ui.SplashActivity
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -59,7 +60,7 @@ class WireGuardNotification @Inject constructor(@ApplicationContext private val
|
||||
}
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
val pendingIntent: PendingIntent =
|
||||
Intent(context, MainActivity::class.java).let { notificationIntent ->
|
||||
Intent(context, SplashActivity::class.java).let { notificationIntent ->
|
||||
PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
|
||||
+7
-3
@@ -2,14 +2,14 @@ package com.zaneschepke.wireguardautotunnel.service.shortcut
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -22,9 +22,13 @@ class ShortcutsActivity : ComponentActivity() {
|
||||
@Inject
|
||||
lateinit var serviceManager: ServiceManager
|
||||
|
||||
@Inject
|
||||
@ApplicationScope
|
||||
lateinit var applicationScope: CoroutineScope
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
applicationScope.launch {
|
||||
val settings = appDataRepository.settings.getSettings()
|
||||
if (settings.isShortcutsEnabled) {
|
||||
when (intent.getStringExtra(CLASS_NAME_EXTRA_KEY)) {
|
||||
|
||||
+28
-32
@@ -3,21 +3,25 @@ package com.zaneschepke.wireguardautotunnel.service.tile
|
||||
import android.os.Build
|
||||
import android.service.quicksettings.Tile
|
||||
import android.service.quicksettings.TileService
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ServiceLifecycleDispatcher
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.module.ServiceScope
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class AutoTunnelControlTile : TileService() {
|
||||
class AutoTunnelControlTile : TileService(), LifecycleOwner {
|
||||
|
||||
@Inject
|
||||
lateinit var appDataRepository: AppDataRepository
|
||||
@@ -25,30 +29,28 @@ class AutoTunnelControlTile : TileService() {
|
||||
@Inject
|
||||
lateinit var serviceManager: ServiceManager
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
private val dispatcher = ServiceLifecycleDispatcher(this)
|
||||
|
||||
private var manualStartConfig: TunnelConfig? = null
|
||||
|
||||
override fun onStartListening() {
|
||||
super.onStartListening()
|
||||
scope.launch {
|
||||
appDataRepository.settings.getSettingsFlow().collectLatest {
|
||||
when (it.isAutoTunnelEnabled) {
|
||||
true -> {
|
||||
if (it.isAutoTunnelPaused) {
|
||||
setInactive()
|
||||
setTileDescription(this@AutoTunnelControlTile.getString(R.string.paused))
|
||||
} else {
|
||||
setActive()
|
||||
setTileDescription(this@AutoTunnelControlTile.getString(R.string.active))
|
||||
}
|
||||
}
|
||||
|
||||
false -> {
|
||||
setTileDescription(this@AutoTunnelControlTile.getString(R.string.disabled))
|
||||
setUnavailable()
|
||||
lifecycleScope.launch {
|
||||
val settings = appDataRepository.settings.getSettings()
|
||||
when (settings.isAutoTunnelEnabled) {
|
||||
true -> {
|
||||
if (settings.isAutoTunnelPaused) {
|
||||
setInactive()
|
||||
setTileDescription(this@AutoTunnelControlTile.getString(R.string.paused))
|
||||
} else {
|
||||
setActive()
|
||||
setTileDescription(this@AutoTunnelControlTile.getString(R.string.active))
|
||||
}
|
||||
}
|
||||
false -> {
|
||||
setTileDescription(this@AutoTunnelControlTile.getString(R.string.disabled))
|
||||
setUnavailable()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,22 +60,13 @@ class AutoTunnelControlTile : TileService() {
|
||||
onStartListening()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
override fun onTileRemoved() {
|
||||
super.onTileRemoved()
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
override fun onClick() {
|
||||
super.onClick()
|
||||
unlockAndRun {
|
||||
scope.launch {
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
appDataRepository.toggleWatcherServicePause()
|
||||
onStartListening()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e.message)
|
||||
} finally {
|
||||
@@ -108,4 +101,7 @@ class AutoTunnelControlTile : TileService() {
|
||||
}
|
||||
qsTile.updateTile()
|
||||
}
|
||||
|
||||
override val lifecycle: Lifecycle
|
||||
get() = dispatcher.lifecycle
|
||||
}
|
||||
|
||||
+32
-36
@@ -3,21 +3,26 @@ package com.zaneschepke.wireguardautotunnel.service.tile
|
||||
import android.os.Build
|
||||
import android.service.quicksettings.Tile
|
||||
import android.service.quicksettings.TileService
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.LifecycleService
|
||||
import androidx.lifecycle.ServiceLifecycleDispatcher
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class TunnelControlTile : TileService() {
|
||||
class TunnelControlTile : TileService(), LifecycleOwner {
|
||||
|
||||
@Inject
|
||||
lateinit var appDataRepository: AppDataRepository
|
||||
@@ -28,47 +33,35 @@ class TunnelControlTile : TileService() {
|
||||
@Inject
|
||||
lateinit var serviceManager: ServiceManager
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
private val dispatcher = ServiceLifecycleDispatcher(this)
|
||||
|
||||
private var manualStartConfig: TunnelConfig? = null
|
||||
|
||||
override fun onStartListening() {
|
||||
super.onStartListening()
|
||||
Timber.d("On start listening called")
|
||||
scope.launch {
|
||||
vpnService.vpnState.collect { it ->
|
||||
when (it.status) {
|
||||
Tunnel.State.UP -> {
|
||||
setActive()
|
||||
it.tunnelConfig?.name?.let { name -> setTileDescription(name) }
|
||||
}
|
||||
|
||||
Tunnel.State.DOWN -> {
|
||||
setInactive()
|
||||
val config = appDataRepository.getStartTunnelConfig()?.also { config ->
|
||||
manualStartConfig = config
|
||||
} ?: appDataRepository.getPrimaryOrFirstTunnel()
|
||||
config?.let {
|
||||
setTileDescription(it.name)
|
||||
} ?: setUnavailable()
|
||||
}
|
||||
|
||||
else -> setInactive()
|
||||
lifecycleScope.launch {
|
||||
when (vpnService.getState()) {
|
||||
TunnelState.UP -> {
|
||||
setActive()
|
||||
setTileDescription(vpnService.name)
|
||||
}
|
||||
|
||||
TunnelState.DOWN -> {
|
||||
setInactive()
|
||||
val config = appDataRepository.getStartTunnelConfig()?.also { config ->
|
||||
manualStartConfig = config
|
||||
} ?: appDataRepository.getPrimaryOrFirstTunnel()
|
||||
config?.let {
|
||||
setTileDescription(it.name)
|
||||
} ?: setUnavailable()
|
||||
}
|
||||
|
||||
else -> setInactive()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
override fun onTileRemoved() {
|
||||
super.onTileRemoved()
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
override fun onTileAdded() {
|
||||
super.onTileAdded()
|
||||
onStartListening()
|
||||
@@ -77,9 +70,9 @@ class TunnelControlTile : TileService() {
|
||||
override fun onClick() {
|
||||
super.onClick()
|
||||
unlockAndRun {
|
||||
scope.launch {
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
if (vpnService.getState() == Tunnel.State.UP) {
|
||||
if (vpnService.getState() == TunnelState.UP) {
|
||||
serviceManager.stopVpnServiceForeground(
|
||||
this@TunnelControlTile,
|
||||
isManualStop = true,
|
||||
@@ -123,4 +116,7 @@ class TunnelControlTile : TileService() {
|
||||
}
|
||||
qsTile.updateTile()
|
||||
}
|
||||
|
||||
override val lifecycle: Lifecycle
|
||||
get() = dispatcher.lifecycle
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.tunnel
|
||||
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
|
||||
enum class TunnelState {
|
||||
UP,
|
||||
DOWN,
|
||||
TOGGLE;
|
||||
|
||||
fun toWgState(): Tunnel.State {
|
||||
return when (this) {
|
||||
UP -> Tunnel.State.UP
|
||||
DOWN -> Tunnel.State.DOWN
|
||||
TOGGLE -> Tunnel.State.TOGGLE
|
||||
}
|
||||
}
|
||||
|
||||
fun toAmState(): org.amnezia.awg.backend.Tunnel.State {
|
||||
return when (this) {
|
||||
UP -> org.amnezia.awg.backend.Tunnel.State.UP
|
||||
DOWN -> org.amnezia.awg.backend.Tunnel.State.DOWN
|
||||
TOGGLE -> org.amnezia.awg.backend.Tunnel.State.TOGGLE
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun from(state: Tunnel.State): TunnelState {
|
||||
return when (state) {
|
||||
Tunnel.State.DOWN -> DOWN
|
||||
Tunnel.State.TOGGLE -> TOGGLE
|
||||
Tunnel.State.UP -> UP
|
||||
}
|
||||
}
|
||||
|
||||
fun from(state: org.amnezia.awg.backend.Tunnel.State): TunnelState {
|
||||
return when (state) {
|
||||
org.amnezia.awg.backend.Tunnel.State.DOWN -> DOWN
|
||||
org.amnezia.awg.backend.Tunnel.State.TOGGLE -> TOGGLE
|
||||
org.amnezia.awg.backend.Tunnel.State.UP -> UP
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.tunnel
|
||||
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
interface VpnService : Tunnel {
|
||||
suspend fun startTunnel(tunnelConfig: TunnelConfig? = null): Tunnel.State
|
||||
interface VpnService : Tunnel, org.amnezia.awg.backend.Tunnel {
|
||||
suspend fun startTunnel(tunnelConfig: TunnelConfig? = null): TunnelState
|
||||
|
||||
suspend fun stopTunnel()
|
||||
|
||||
val vpnState: StateFlow<VpnState>
|
||||
|
||||
fun getState(): Tunnel.State
|
||||
fun getState(): TunnelState
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.tunnel
|
||||
|
||||
import com.wireguard.android.backend.Statistics
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
|
||||
|
||||
data class VpnState(
|
||||
val status: Tunnel.State = Tunnel.State.DOWN,
|
||||
val status: TunnelState = TunnelState.DOWN,
|
||||
val tunnelConfig: TunnelConfig? = null,
|
||||
val statistics: Statistics? = null
|
||||
val statistics: TunnelStatistics? = null
|
||||
)
|
||||
|
||||
+136
-59
@@ -2,82 +2,131 @@ 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.State
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.module.Kernel
|
||||
import com.zaneschepke.wireguardautotunnel.module.Userspace
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.AmneziaStatistics
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.WireGuardStatistics
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.amnezia.awg.backend.Tunnel
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
|
||||
class WireGuardTunnel
|
||||
@Inject
|
||||
constructor(
|
||||
@Userspace private val userspaceBackend: Backend,
|
||||
@Kernel private val kernelBackend: Backend,
|
||||
private val userspaceAmneziaBackend: Provider<org.amnezia.awg.backend.Backend>,
|
||||
@Userspace private val userspaceBackend: Provider<Backend>,
|
||||
@Kernel private val kernelBackend: Provider<Backend>,
|
||||
private val appDataRepository: AppDataRepository,
|
||||
@ApplicationScope private val applicationScope: CoroutineScope,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
|
||||
) : VpnService {
|
||||
private val _vpnState = MutableStateFlow(VpnState())
|
||||
override val vpnState: StateFlow<VpnState> = _vpnState.asStateFlow()
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
private var statsJob: Job? = null
|
||||
|
||||
private var backend: Backend = userspaceBackend
|
||||
private var backendIsWgUserspace = true
|
||||
|
||||
private var backendIsUserspace = true
|
||||
private var backendIsAmneziaUserspace = false
|
||||
|
||||
init {
|
||||
scope.launch {
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
appDataRepository.settings.getSettingsFlow().collect {
|
||||
if (it.isKernelEnabled && backendIsUserspace) {
|
||||
Timber.d("Setting kernel backend")
|
||||
backend = kernelBackend
|
||||
backendIsUserspace = false
|
||||
} else if (!it.isKernelEnabled && !backendIsUserspace) {
|
||||
Timber.d("Setting userspace backend")
|
||||
backend = userspaceBackend
|
||||
backendIsUserspace = true
|
||||
if (it.isKernelEnabled && (backendIsWgUserspace || backendIsAmneziaUserspace)) {
|
||||
Timber.i("Setting kernel backend")
|
||||
backendIsWgUserspace = false
|
||||
backendIsAmneziaUserspace = false
|
||||
} else if (!it.isKernelEnabled && !it.isAmneziaEnabled && !backendIsWgUserspace) {
|
||||
Timber.i("Setting WireGuard userspace backend")
|
||||
backendIsWgUserspace = true
|
||||
backendIsAmneziaUserspace = false
|
||||
} else if (it.isAmneziaEnabled && !backendIsAmneziaUserspace) {
|
||||
Timber.i("Setting Amnezia userspace backend")
|
||||
backendIsAmneziaUserspace = true
|
||||
backendIsWgUserspace = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun startTunnel(tunnelConfig: TunnelConfig?): State {
|
||||
return try {
|
||||
//TODO we need better error handling here
|
||||
val config = tunnelConfig ?: appDataRepository.getPrimaryOrFirstTunnel()
|
||||
if (config != null) {
|
||||
emitTunnelConfig(config)
|
||||
val wgConfig = TunnelConfig.configFromQuick(config.wgQuick)
|
||||
val state =
|
||||
backend.setState(
|
||||
this,
|
||||
State.UP,
|
||||
wgConfig,
|
||||
)
|
||||
state
|
||||
} else throw Exception("No tunnels")
|
||||
} catch (e: BackendException) {
|
||||
Timber.e("Failed to start tunnel with error: ${e.message}")
|
||||
State.DOWN
|
||||
private fun setState(tunnelConfig: TunnelConfig?, tunnelState: TunnelState): TunnelState {
|
||||
return if (backendIsAmneziaUserspace) {
|
||||
Timber.i("Using Amnezia backend")
|
||||
val config = tunnelConfig?.let {
|
||||
if (it.amQuick != "") TunnelConfig.configFromAmQuick(it.amQuick) else {
|
||||
Timber.w("Using backwards compatible wg config, amnezia specific config not found.")
|
||||
TunnelConfig.configFromAmQuick(it.wgQuick)
|
||||
}
|
||||
}
|
||||
val state =
|
||||
userspaceAmneziaBackend.get().setState(this, tunnelState.toAmState(), config)
|
||||
TunnelState.from(state)
|
||||
} else {
|
||||
Timber.i("Using Wg backend")
|
||||
val wgConfig = tunnelConfig?.let { TunnelConfig.configFromWgQuick(it.wgQuick) }
|
||||
val state = backend().setState(
|
||||
this,
|
||||
tunnelState.toWgState(),
|
||||
wgConfig,
|
||||
)
|
||||
TunnelState.from(state)
|
||||
}
|
||||
}
|
||||
|
||||
private fun emitTunnelState(state: State) {
|
||||
override suspend fun startTunnel(tunnelConfig: TunnelConfig?): TunnelState {
|
||||
return withContext(ioDispatcher) {
|
||||
try {
|
||||
//TODO we need better error handling here
|
||||
// need to bubble up these errors to the UI
|
||||
val config = tunnelConfig ?: appDataRepository.getPrimaryOrFirstTunnel()
|
||||
if (config != null) {
|
||||
emitTunnelConfig(config)
|
||||
setState(config, TunnelState.UP)
|
||||
} else throw Exception("No tunnels")
|
||||
} catch (e: BackendException) {
|
||||
Timber.e("Failed to start tunnel with error: ${e.message}")
|
||||
TunnelState.from(State.DOWN)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun backend(): Backend {
|
||||
return when {
|
||||
backendIsWgUserspace -> {
|
||||
userspaceBackend.get()
|
||||
}
|
||||
|
||||
!backendIsWgUserspace && !backendIsAmneziaUserspace -> {
|
||||
kernelBackend.get()
|
||||
}
|
||||
|
||||
else -> {
|
||||
userspaceBackend.get()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun emitTunnelState(state: TunnelState) {
|
||||
_vpnState.tryEmit(
|
||||
_vpnState.value.copy(
|
||||
status = state,
|
||||
@@ -85,7 +134,7 @@ constructor(
|
||||
)
|
||||
}
|
||||
|
||||
private fun emitBackendStatistics(statistics: Statistics) {
|
||||
private fun emitBackendStatistics(statistics: TunnelStatistics) {
|
||||
_vpnState.tryEmit(
|
||||
_vpnState.value.copy(
|
||||
statistics = statistics,
|
||||
@@ -101,45 +150,73 @@ constructor(
|
||||
)
|
||||
}
|
||||
|
||||
private fun resetVpnState() {
|
||||
_vpnState.tryEmit(VpnState())
|
||||
}
|
||||
|
||||
override suspend fun stopTunnel() {
|
||||
try {
|
||||
if (getState() == State.UP) {
|
||||
val state = backend.setState(this, State.DOWN, null)
|
||||
emitTunnelState(state)
|
||||
withContext(ioDispatcher) {
|
||||
try {
|
||||
if (getState() == TunnelState.UP) {
|
||||
val state = setState(null, TunnelState.DOWN)
|
||||
resetVpnState()
|
||||
emitTunnelState(state)
|
||||
}
|
||||
} catch (e: BackendException) {
|
||||
Timber.e("Failed to stop wireguard tunnel with error: ${e.message}")
|
||||
} catch (e: org.amnezia.awg.backend.BackendException) {
|
||||
Timber.e("Failed to stop amnezia tunnel with error: ${e.message}")
|
||||
}
|
||||
} catch (e: BackendException) {
|
||||
Timber.e("Failed to stop tunnel with error: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun getState(): State {
|
||||
return backend.getState(this)
|
||||
override fun getState(): TunnelState {
|
||||
return if (backendIsAmneziaUserspace) TunnelState.from(
|
||||
userspaceAmneziaBackend.get().getState(this),
|
||||
)
|
||||
else TunnelState.from(backend().getState(this))
|
||||
}
|
||||
|
||||
override fun getName(): String {
|
||||
return _vpnState.value.tunnelConfig?.name ?: ""
|
||||
}
|
||||
|
||||
override fun onStateChange(state: State) {
|
||||
val tunnel = this
|
||||
|
||||
override fun onStateChange(newState: Tunnel.State) {
|
||||
handleStateChange(TunnelState.from(newState))
|
||||
}
|
||||
|
||||
private fun handleStateChange(state: TunnelState) {
|
||||
emitTunnelState(state)
|
||||
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(WireGuardAutoTunnel.instance)
|
||||
if (state == State.UP) {
|
||||
statsJob =
|
||||
scope.launch {
|
||||
while (true) {
|
||||
val statistics = backend.getStatistics(tunnel)
|
||||
emitBackendStatistics(statistics)
|
||||
delay(Constants.VPN_STATISTIC_CHECK_INTERVAL)
|
||||
}
|
||||
}
|
||||
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
|
||||
if (state == TunnelState.UP) {
|
||||
statsJob = startTunnelStatisticsJob()
|
||||
}
|
||||
if (state == State.DOWN) {
|
||||
if (state == TunnelState.DOWN) {
|
||||
try {
|
||||
statsJob?.cancel()
|
||||
} catch (e : CancellationException) {
|
||||
} catch (e: CancellationException) {
|
||||
Timber.i("Stats job cancelled")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startTunnelStatisticsJob() = applicationScope.launch(ioDispatcher) {
|
||||
while (true) {
|
||||
if (backendIsAmneziaUserspace) {
|
||||
emitBackendStatistics(
|
||||
AmneziaStatistics(
|
||||
userspaceAmneziaBackend.get().getStatistics(this@WireGuardTunnel),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
emitBackendStatistics(WireGuardStatistics(backend().getStatistics(this@WireGuardTunnel)))
|
||||
}
|
||||
delay(Constants.VPN_STATISTIC_CHECK_INTERVAL)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStateChange(state: State) {
|
||||
handleStateChange(TunnelState.from(state))
|
||||
}
|
||||
}
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.tunnel.statistics
|
||||
|
||||
import org.amnezia.awg.backend.Statistics
|
||||
import org.amnezia.awg.crypto.Key
|
||||
|
||||
class AmneziaStatistics(private val statistics: Statistics) : TunnelStatistics() {
|
||||
override fun peerStats(peer: Key): PeerStats? {
|
||||
val key = Key.fromBase64(peer.toBase64())
|
||||
val stats = statistics.peer(key)
|
||||
return stats?.let {
|
||||
PeerStats(
|
||||
rxBytes = stats.rxBytes,
|
||||
txBytes = stats.txBytes,
|
||||
latestHandshakeEpochMillis = stats.latestHandshakeEpochMillis,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun isTunnelStale(): Boolean {
|
||||
return statistics.isStale
|
||||
}
|
||||
|
||||
override fun getPeers(): Array<Key> {
|
||||
return statistics.peers()
|
||||
}
|
||||
|
||||
override fun rx(): Long {
|
||||
return statistics.totalRx()
|
||||
}
|
||||
|
||||
override fun tx(): Long {
|
||||
return statistics.totalTx()
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.tunnel.statistics
|
||||
|
||||
import org.amnezia.awg.crypto.Key
|
||||
|
||||
abstract class TunnelStatistics {
|
||||
@JvmRecord
|
||||
data class PeerStats(val rxBytes: Long, val txBytes: Long, val latestHandshakeEpochMillis: Long)
|
||||
|
||||
abstract fun peerStats(peer: Key): PeerStats?
|
||||
|
||||
abstract fun isTunnelStale(): Boolean
|
||||
|
||||
abstract fun getPeers(): Array<Key>
|
||||
|
||||
abstract fun rx(): Long
|
||||
|
||||
abstract fun tx(): Long
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.tunnel.statistics
|
||||
|
||||
import com.wireguard.android.backend.Statistics
|
||||
import org.amnezia.awg.crypto.Key
|
||||
|
||||
class WireGuardStatistics(private val statistics: Statistics) : TunnelStatistics() {
|
||||
override fun peerStats(peer: Key): PeerStats? {
|
||||
val key = com.wireguard.crypto.Key.fromBase64(peer.toBase64())
|
||||
val peerStats = statistics.peer(key)
|
||||
return peerStats?.let {
|
||||
PeerStats(
|
||||
txBytes = peerStats.txBytes,
|
||||
rxBytes = peerStats.rxBytes,
|
||||
latestHandshakeEpochMillis = peerStats.latestHandshakeEpochMillis,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun isTunnelStale(): Boolean {
|
||||
return statistics.isStale
|
||||
}
|
||||
|
||||
override fun getPeers(): Array<Key> {
|
||||
return statistics.peers().map {
|
||||
Key.fromBase64(it.toBase64())
|
||||
}.toTypedArray()
|
||||
}
|
||||
|
||||
override fun rx(): Long {
|
||||
return statistics.totalRx()
|
||||
}
|
||||
|
||||
override fun tx(): Long {
|
||||
return statistics.totalTx()
|
||||
}
|
||||
}
|
||||
@@ -1,35 +1,25 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui
|
||||
|
||||
import android.app.Application
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.wireguard.android.backend.GoBackend
|
||||
import com.zaneschepke.logcatter.Logcatter
|
||||
import com.zaneschepke.logcatter.model.LogMessage
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.flow.update
|
||||
import timber.log.Timber
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class AppViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val application: Application,
|
||||
) : ViewModel() {
|
||||
constructor() : ViewModel() {
|
||||
|
||||
val vpnIntent: Intent? = GoBackend.VpnService.prepare(WireGuardAutoTunnel.instance)
|
||||
|
||||
@@ -49,100 +39,85 @@ constructor(
|
||||
}
|
||||
|
||||
private fun requestPermissions() {
|
||||
_appUiState.value = _appUiState.value.copy(
|
||||
requestPermissions = true,
|
||||
)
|
||||
_appUiState.update {
|
||||
it.copy(
|
||||
requestPermissions = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun permissionsRequested() {
|
||||
_appUiState.value = _appUiState.value.copy(
|
||||
requestPermissions = false,
|
||||
)
|
||||
_appUiState.update {
|
||||
it.copy(
|
||||
requestPermissions = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun openWebPage(url: String) {
|
||||
fun openWebPage(url: String, context: Context) {
|
||||
try {
|
||||
val webpage: Uri = Uri.parse(url)
|
||||
val intent = Intent(Intent.ACTION_VIEW, webpage).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
application.startActivity(intent)
|
||||
context.startActivity(intent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Timber.e(e)
|
||||
showSnackbarMessage(application.getString(R.string.no_browser_detected))
|
||||
showSnackbarMessage(context.getString(R.string.no_browser_detected))
|
||||
}
|
||||
}
|
||||
|
||||
fun onVpnPermissionAccepted() {
|
||||
_appUiState.value = _appUiState.value.copy(
|
||||
vpnPermissionAccepted = true,
|
||||
)
|
||||
_appUiState.update {
|
||||
it.copy(
|
||||
vpnPermissionAccepted = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun launchEmail() {
|
||||
fun launchEmail(context: Context) {
|
||||
try {
|
||||
val intent =
|
||||
Intent(Intent.ACTION_SENDTO).apply {
|
||||
type = Constants.EMAIL_MIME_TYPE
|
||||
putExtra(Intent.EXTRA_EMAIL, arrayOf(application.getString(R.string.my_email)))
|
||||
putExtra(Intent.EXTRA_SUBJECT, application.getString(R.string.email_subject))
|
||||
putExtra(Intent.EXTRA_EMAIL, arrayOf(context.getString(R.string.my_email)))
|
||||
putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.email_subject))
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
application.startActivity(
|
||||
Intent.createChooser(intent, application.getString(R.string.email_chooser)).apply {
|
||||
context.startActivity(
|
||||
Intent.createChooser(intent, context.getString(R.string.email_chooser)).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
},
|
||||
)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Timber.e(e)
|
||||
showSnackbarMessage(application.getString(R.string.no_email_detected))
|
||||
showSnackbarMessage(context.getString(R.string.no_email_detected))
|
||||
}
|
||||
}
|
||||
|
||||
fun showSnackbarMessage(message: String) {
|
||||
_appUiState.value = _appUiState.value.copy(
|
||||
snackbarMessage = message,
|
||||
snackbarMessageConsumed = false,
|
||||
)
|
||||
_appUiState.update {
|
||||
it.copy(
|
||||
snackbarMessage = message,
|
||||
snackbarMessageConsumed = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun snackbarMessageConsumed() {
|
||||
_appUiState.value = _appUiState.value.copy(
|
||||
snackbarMessage = "",
|
||||
snackbarMessageConsumed = true,
|
||||
)
|
||||
}
|
||||
|
||||
val logs = mutableStateListOf<LogMessage>()
|
||||
|
||||
fun readLogCatOutput() =
|
||||
viewModelScope.launch(viewModelScope.coroutineContext + Dispatchers.IO) {
|
||||
launch {
|
||||
Logcatter.logs(callback = {
|
||||
logs.add(it)
|
||||
if (logs.size > Constants.LOG_BUFFER_SIZE) {
|
||||
logs.removeRange(0, (logs.size - Constants.LOG_BUFFER_SIZE).toInt())
|
||||
}
|
||||
})
|
||||
}
|
||||
_appUiState.update {
|
||||
it.copy(
|
||||
snackbarMessage = "",
|
||||
snackbarMessageConsumed = true,
|
||||
)
|
||||
}
|
||||
|
||||
fun clearLogs() {
|
||||
logs.clear()
|
||||
Logcatter.clear()
|
||||
}
|
||||
|
||||
fun saveLogsToFile() {
|
||||
val fileName = "${Constants.BASE_LOG_FILE_NAME}-${Instant.now().epochSecond}.txt"
|
||||
val content = logs.joinToString(separator = "\n")
|
||||
FileUtils.saveFileToDownloads(application.applicationContext, content, fileName)
|
||||
Toast.makeText(application, application.getString(R.string.logs_saved), Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
|
||||
fun setNotificationPermissionAccepted(accepted: Boolean) {
|
||||
_appUiState.value = _appUiState.value.copy(
|
||||
notificationPermissionAccepted = accepted,
|
||||
)
|
||||
_appUiState.update {
|
||||
it.copy(
|
||||
notificationPermissionAccepted = accepted,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui
|
||||
|
||||
import com.journeyapps.barcodescanner.CaptureActivity
|
||||
|
||||
class CaptureActivityPortrait : CaptureActivity()
|
||||
@@ -32,21 +32,24 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.navArgument
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.options.OptionsScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.pinlock.PinLockScreen
|
||||
@@ -58,14 +61,13 @@ import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
@Inject
|
||||
lateinit var dataStoreManager: DataStoreManager
|
||||
lateinit var appStateRepository: AppStateRepository
|
||||
|
||||
@Inject
|
||||
lateinit var settingsRepository: SettingsRepository
|
||||
@@ -79,17 +81,18 @@ class MainActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val isPinLockEnabled = intent.extras?.getBoolean(SplashActivity.IS_PIN_LOCK_ENABLED_KEY)
|
||||
|
||||
enableEdgeToEdge(navigationBarStyle = SystemBarStyle.dark(Color.Transparent.toArgb()))
|
||||
|
||||
// load preferences into memory and init data
|
||||
lifecycleScope.launch {
|
||||
dataStoreManager.init()
|
||||
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(this@MainActivity)
|
||||
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
|
||||
val settings = settingsRepository.getSettings()
|
||||
if (settings.isAutoTunnelEnabled) {
|
||||
serviceManager.startWatcherService(application.applicationContext)
|
||||
}
|
||||
}
|
||||
|
||||
setContent {
|
||||
val appViewModel = hiltViewModel<AppViewModel>()
|
||||
val appUiState by appViewModel.appUiState.collectAsStateWithLifecycle()
|
||||
@@ -138,7 +141,11 @@ class MainActivity : AppCompatActivity() {
|
||||
return@LaunchedEffect notificationPermissionState.launchPermissionRequest()
|
||||
}
|
||||
if (!appUiState.vpnPermissionAccepted) {
|
||||
return@LaunchedEffect vpnActivityResultState.launch(appViewModel.vpnIntent)
|
||||
return@LaunchedEffect appViewModel.vpnIntent?.let {
|
||||
vpnActivityResultState.launch(
|
||||
it,
|
||||
)
|
||||
}!!
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -148,7 +155,6 @@ class MainActivity : AppCompatActivity() {
|
||||
appViewModel.setNotificationPermissionAccepted(
|
||||
notificationPermissionState?.status?.isGranted ?: true,
|
||||
)
|
||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) appViewModel.readLogCatOutput()
|
||||
}
|
||||
|
||||
LaunchedEffect(appUiState.snackbarMessageConsumed) {
|
||||
@@ -195,10 +201,8 @@ class MainActivity : AppCompatActivity() {
|
||||
) { padding ->
|
||||
NavHost(
|
||||
navController,
|
||||
startDestination =
|
||||
(if (PinManager.pinExists()) Screen.Lock.route else Screen.Main.route),
|
||||
modifier =
|
||||
Modifier
|
||||
startDestination = (if (isPinLockEnabled == true) Screen.Lock.route else Screen.Main.route),
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
@@ -230,16 +234,33 @@ class MainActivity : AppCompatActivity() {
|
||||
)
|
||||
}
|
||||
composable(Screen.Support.Logs.route) {
|
||||
LogsScreen(appViewModel)
|
||||
LogsScreen()
|
||||
}
|
||||
composable("${Screen.Config.route}/{id}") {
|
||||
composable(
|
||||
"${Screen.Config.route}/{id}?configType={configType}",
|
||||
arguments =
|
||||
listOf(
|
||||
navArgument("id") {
|
||||
type = NavType.StringType
|
||||
defaultValue = "0"
|
||||
},
|
||||
navArgument("configType") {
|
||||
type = NavType.StringType
|
||||
defaultValue = ConfigType.WIREGUARD.name
|
||||
},
|
||||
),
|
||||
) {
|
||||
val id = it.arguments?.getString("id")
|
||||
val configType = ConfigType.valueOf(
|
||||
it.arguments?.getString("configType") ?: ConfigType.WIREGUARD.name,
|
||||
)
|
||||
if (!id.isNullOrBlank()) {
|
||||
ConfigScreen(
|
||||
navController = navController,
|
||||
tunnelId = id,
|
||||
appViewModel = appViewModel,
|
||||
focusRequester = focusRequester,
|
||||
configType = configType,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,9 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Home
|
||||
import androidx.compose.material.icons.rounded.QuestionMark
|
||||
import androidx.compose.material.icons.rounded.Settings
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
|
||||
sealed class Screen(val route: String) {
|
||||
data object Main : Screen("main") {
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.zaneschepke.logcatter.LocalLogCollector
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel.Companion.isRunningOnAndroidTv
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||
import javax.inject.Inject
|
||||
|
||||
@SuppressLint("CustomSplashScreen")
|
||||
@AndroidEntryPoint
|
||||
class SplashActivity : ComponentActivity() {
|
||||
|
||||
@Inject
|
||||
lateinit var appStateRepository: AppStateRepository
|
||||
|
||||
@Inject
|
||||
lateinit var localLogCollector: LocalLogCollector
|
||||
|
||||
@Inject
|
||||
@ApplicationScope
|
||||
lateinit var applicationScope: CoroutineScope
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
val splashScreen = installSplashScreen()
|
||||
splashScreen.setKeepOnScreenCondition { true }
|
||||
}
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
applicationScope.launch {
|
||||
if (!isRunningOnAndroidTv()) localLogCollector.start()
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||
val pinLockEnabled = appStateRepository.isPinLockEnabled()
|
||||
if (pinLockEnabled) {
|
||||
PinManager.initialize(WireGuardAutoTunnel.instance)
|
||||
}
|
||||
|
||||
val intent = Intent(this@SplashActivity, MainActivity::class.java).apply {
|
||||
putExtra(IS_PIN_LOCK_ENABLED_KEY, pinLockEnabled)
|
||||
}
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val IS_PIN_LOCK_ENABLED_KEY = "is_pin_lock_enabled"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,9 +15,12 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.wireguard.android.backend.Statistics
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
|
||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||
import com.zaneschepke.wireguardautotunnel.util.toThreeDecimalPlaceString
|
||||
|
||||
@@ -30,11 +33,12 @@ fun RowListItem(
|
||||
onClick: () -> Unit,
|
||||
rowButton: @Composable () -> Unit,
|
||||
expanded: Boolean,
|
||||
statistics: Statistics?
|
||||
statistics: TunnelStatistics?,
|
||||
focusRequester: FocusRequester,
|
||||
) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
Modifier.focusRequester(focusRequester)
|
||||
.animateContentSize()
|
||||
.clip(RoundedCornerShape(30.dp))
|
||||
.combinedClickable(
|
||||
@@ -52,14 +56,15 @@ fun RowListItem(
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth(13 / 20f),
|
||||
) {
|
||||
icon()
|
||||
Text(text)
|
||||
Text(text, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
rowButton()
|
||||
}
|
||||
if (expanded) {
|
||||
statistics?.peers()?.forEach {
|
||||
statistics?.getPeers()?.forEach {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
@@ -69,9 +74,9 @@ fun RowListItem(
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
) {
|
||||
//TODO change these to string resources
|
||||
val handshakeEpoch = statistics.peer(it)!!.latestHandshakeEpochMillis
|
||||
val peerTx = statistics.peer(it)!!.txBytes
|
||||
val peerRx = statistics.peer(it)!!.rxBytes
|
||||
val handshakeEpoch = statistics.peerStats(it)!!.latestHandshakeEpochMillis
|
||||
val peerTx = statistics.peerStats(it)!!.txBytes
|
||||
val peerRx = statistics.peerStats(it)!!.rxBytes
|
||||
val peerId = it.toBase64().subSequence(0, 3).toString() + "***"
|
||||
val handshakeSec =
|
||||
NumberUtils.getSecondsBetweenTimestampAndNow(handshakeEpoch)
|
||||
|
||||
@@ -44,19 +44,21 @@ fun SearchBar(onQuery: (queryString: String) -> Unit) {
|
||||
onQuery(onQueryChanged)
|
||||
},
|
||||
leadingIcon = {
|
||||
val icon = Icons.Rounded.Search
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Search,
|
||||
imageVector = icon,
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
contentDescription = stringResource(id = R.string.search_icon),
|
||||
contentDescription = icon.name,
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
if (showClearIcon) {
|
||||
IconButton(onClick = { query = "" }) {
|
||||
val icon = Icons.Rounded.Clear
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Clear,
|
||||
imageVector = icon,
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
contentDescription = stringResource(id = R.string.clear_icon),
|
||||
contentDescription = icon.name,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+9
-6
@@ -28,12 +28,15 @@ fun ConfigurationToggle(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Text(label, textAlign = TextAlign.Start, modifier = Modifier
|
||||
.weight(
|
||||
weight = 1.0f,
|
||||
fill = false,
|
||||
),
|
||||
softWrap = true)
|
||||
Text(
|
||||
label, textAlign = TextAlign.Start,
|
||||
modifier = Modifier
|
||||
.weight(
|
||||
weight = 1.0f,
|
||||
fill = false,
|
||||
),
|
||||
softWrap = true,
|
||||
)
|
||||
Switch(
|
||||
modifier = modifier,
|
||||
enabled = enabled,
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.models
|
||||
|
||||
import com.wireguard.config.Interface
|
||||
|
||||
data class InterfaceProxy(
|
||||
var privateKey: String = "",
|
||||
var publicKey: String = "",
|
||||
var addresses: String = "",
|
||||
var dnsServers: String = "",
|
||||
var listenPort: String = "",
|
||||
var mtu: String = ""
|
||||
) {
|
||||
companion object {
|
||||
fun from(i: Interface): InterfaceProxy {
|
||||
return InterfaceProxy(
|
||||
publicKey = i.keyPair.publicKey.toBase64().trim(),
|
||||
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 "",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+127
-14
@@ -79,9 +79,9 @@ import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.Event
|
||||
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||
import com.zaneschepke.wireguardautotunnel.util.getMessage
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||
@@ -94,7 +94,8 @@ fun ConfigScreen(
|
||||
focusRequester: FocusRequester,
|
||||
navController: NavController,
|
||||
appViewModel: AppViewModel,
|
||||
tunnelId: String
|
||||
tunnelId: String,
|
||||
configType: ConfigType
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val clipboardManager: ClipboardManager = LocalClipboardManager.current
|
||||
@@ -148,11 +149,11 @@ fun ConfigScreen(
|
||||
},
|
||||
onError = {
|
||||
showAuthPrompt = false
|
||||
appViewModel.showSnackbarMessage(Event.Error.AuthenticationFailed.message)
|
||||
appViewModel.showSnackbarMessage(context.getString(R.string.error_authentication_failed))
|
||||
},
|
||||
onFailure = {
|
||||
showAuthPrompt = false
|
||||
appViewModel.showSnackbarMessage(Event.Error.AuthorizationFailed.message)
|
||||
appViewModel.showSnackbarMessage(context.getString(R.string.error_authorization_failed))
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -319,15 +320,11 @@ fun ConfigScreen(
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
viewModel.onSaveAllChanges().let {
|
||||
when (it) {
|
||||
is Result.Success -> {
|
||||
appViewModel.showSnackbarMessage(it.data.message)
|
||||
navController.navigate(Screen.Main.route)
|
||||
}
|
||||
|
||||
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
|
||||
}
|
||||
viewModel.onSaveAllChanges(configType).onSuccess {
|
||||
appViewModel.showSnackbarMessage(context.getString(R.string.config_changes_saved))
|
||||
navController.navigate(Screen.Main.route)
|
||||
}.onFailure {
|
||||
appViewModel.showSnackbarMessage(it.getMessage(context))
|
||||
}
|
||||
},
|
||||
containerColor = fobColor,
|
||||
@@ -486,6 +483,122 @@ fun ConfigScreen(
|
||||
modifier = Modifier.width(IntrinsicSize.Min),
|
||||
)
|
||||
}
|
||||
if (configType == ConfigType.AMNEZIA) {
|
||||
ConfigurationTextBox(
|
||||
value = uiState.interfaceProxy.junkPacketCount,
|
||||
onValueChange = { value -> viewModel.onJunkPacketCountChanged(value) },
|
||||
keyboardActions = keyboardActions,
|
||||
label = stringResource(R.string.junk_packet_count),
|
||||
hint = stringResource(R.string.junk_packet_count).lowercase(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester),
|
||||
)
|
||||
ConfigurationTextBox(
|
||||
value = uiState.interfaceProxy.junkPacketMinSize,
|
||||
onValueChange = { value ->
|
||||
viewModel.onJunkPacketMinSizeChanged(
|
||||
value,
|
||||
)
|
||||
},
|
||||
keyboardActions = keyboardActions,
|
||||
label = stringResource(R.string.junk_packet_minimum_size),
|
||||
hint = stringResource(R.string.junk_packet_minimum_size).lowercase(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester),
|
||||
)
|
||||
ConfigurationTextBox(
|
||||
value = uiState.interfaceProxy.junkPacketMaxSize,
|
||||
onValueChange = { value ->
|
||||
viewModel.onJunkPacketMaxSizeChanged(
|
||||
value,
|
||||
)
|
||||
},
|
||||
keyboardActions = keyboardActions,
|
||||
label = stringResource(R.string.junk_packet_maximum_size),
|
||||
hint = stringResource(R.string.junk_packet_maximum_size).lowercase(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester),
|
||||
)
|
||||
ConfigurationTextBox(
|
||||
value = uiState.interfaceProxy.initPacketJunkSize,
|
||||
onValueChange = { value ->
|
||||
viewModel.onInitPacketJunkSizeChanged(
|
||||
value,
|
||||
)
|
||||
},
|
||||
keyboardActions = keyboardActions,
|
||||
label = stringResource(R.string.init_packet_junk_size),
|
||||
hint = stringResource(R.string.init_packet_junk_size).lowercase(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester),
|
||||
)
|
||||
ConfigurationTextBox(
|
||||
value = uiState.interfaceProxy.responsePacketJunkSize,
|
||||
onValueChange = { value -> viewModel.onResponsePacketJunkSize(value) },
|
||||
keyboardActions = keyboardActions,
|
||||
label = stringResource(R.string.response_packet_junk_size),
|
||||
hint = stringResource(R.string.response_packet_junk_size).lowercase(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester),
|
||||
)
|
||||
ConfigurationTextBox(
|
||||
value = uiState.interfaceProxy.initPacketMagicHeader,
|
||||
onValueChange = { value -> viewModel.onInitPacketMagicHeader(value) },
|
||||
keyboardActions = keyboardActions,
|
||||
label = stringResource(R.string.init_packet_magic_header),
|
||||
hint = stringResource(R.string.init_packet_magic_header).lowercase(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester),
|
||||
)
|
||||
ConfigurationTextBox(
|
||||
value = uiState.interfaceProxy.responsePacketMagicHeader,
|
||||
onValueChange = { value ->
|
||||
viewModel.onResponsePacketMagicHeader(
|
||||
value,
|
||||
)
|
||||
},
|
||||
keyboardActions = keyboardActions,
|
||||
label = stringResource(R.string.response_packet_magic_header),
|
||||
hint = stringResource(R.string.response_packet_magic_header).lowercase(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester),
|
||||
)
|
||||
ConfigurationTextBox(
|
||||
value = uiState.interfaceProxy.underloadPacketMagicHeader,
|
||||
onValueChange = { value ->
|
||||
viewModel.onUnderloadPacketMagicHeader(
|
||||
value,
|
||||
)
|
||||
},
|
||||
keyboardActions = keyboardActions,
|
||||
label = stringResource(R.string.underload_packet_magic_header),
|
||||
hint = stringResource(R.string.underload_packet_magic_header).lowercase(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester),
|
||||
)
|
||||
ConfigurationTextBox(
|
||||
value = uiState.interfaceProxy.transportPacketMagicHeader,
|
||||
onValueChange = { value ->
|
||||
viewModel.onTransportPacketMagicHeader(
|
||||
value,
|
||||
)
|
||||
},
|
||||
keyboardActions = keyboardActions,
|
||||
label = stringResource(R.string.transport_packet_magic_header),
|
||||
hint = stringResource(R.string.transport_packet_magic_header).lowercase(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester),
|
||||
)
|
||||
}
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
|
||||
+60
-5
@@ -1,8 +1,9 @@
|
||||
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.wireguard.config.Config
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.config.model.InterfaceProxy
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.config.model.PeerProxy
|
||||
import com.zaneschepke.wireguardautotunnel.util.Packages
|
||||
|
||||
data class ConfigUiState(
|
||||
@@ -14,5 +15,59 @@ data class ConfigUiState(
|
||||
val isAllApplicationsEnabled: Boolean = false,
|
||||
val loading: Boolean = true,
|
||||
val tunnel: TunnelConfig? = null,
|
||||
val tunnelName: String = ""
|
||||
)
|
||||
val tunnelName: String = "",
|
||||
val isAmneziaEnabled: Boolean = false
|
||||
) {
|
||||
companion object {
|
||||
fun from(config: Config): ConfigUiState {
|
||||
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()
|
||||
}
|
||||
return ConfigUiState(
|
||||
proxyPeers,
|
||||
proxyInterface,
|
||||
emptyList(),
|
||||
checkedPackages.toList(),
|
||||
include,
|
||||
isAllApplicationsEnabled,
|
||||
)
|
||||
}
|
||||
|
||||
fun from(config: org.amnezia.awg.config.Config): ConfigUiState {
|
||||
//TODO update with new values
|
||||
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()
|
||||
}
|
||||
return ConfigUiState(
|
||||
proxyPeers,
|
||||
proxyInterface,
|
||||
emptyList(),
|
||||
checkedPackages.toList(),
|
||||
include,
|
||||
isAllApplicationsEnabled,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+247
-74
@@ -1,7 +1,6 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.config
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Application
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
@@ -12,21 +11,25 @@ import com.wireguard.config.Interface
|
||||
import com.wireguard.config.Peer
|
||||
import com.wireguard.crypto.Key
|
||||
import com.wireguard.crypto.KeyPair
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.ui.models.InterfaceProxy
|
||||
import com.zaneschepke.wireguardautotunnel.ui.models.PeerProxy
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.config.model.PeerProxy
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.Event
|
||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions
|
||||
import com.zaneschepke.wireguardautotunnel.util.removeAt
|
||||
import com.zaneschepke.wireguardautotunnel.util.update
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
@@ -35,49 +38,36 @@ import javax.inject.Inject
|
||||
class ConfigViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val application: Application,
|
||||
private val appDataRepository: AppDataRepository
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val appDataRepository: AppDataRepository,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
|
||||
) : ViewModel() {
|
||||
|
||||
private val packageManager = application.packageManager
|
||||
private val packageManager = WireGuardAutoTunnel.instance.packageManager
|
||||
|
||||
private val _uiState = MutableStateFlow(ConfigUiState())
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
fun init(tunnelId: String) =
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
viewModelScope.launch(ioDispatcher) {
|
||||
val packages = getQueriedPackages("")
|
||||
val state =
|
||||
if (tunnelId != Constants.MANUAL_TUNNEL_CONFIG_ID) {
|
||||
val tunnelConfig =
|
||||
appDataRepository.tunnels.getAll()
|
||||
.firstOrNull { it.id.toString() == tunnelId }
|
||||
val isAmneziaEnabled = settingsRepository.getSettings().isAmneziaEnabled
|
||||
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,
|
||||
(if (isAmneziaEnabled) {
|
||||
val amConfig =
|
||||
if (tunnelConfig.amQuick == "") tunnelConfig.wgQuick else tunnelConfig.amQuick
|
||||
ConfigUiState.from(TunnelConfig.configFromAmQuick(amConfig))
|
||||
} else ConfigUiState.from(TunnelConfig.configFromWgQuick(tunnelConfig.wgQuick))).copy(
|
||||
packages = packages,
|
||||
loading = false,
|
||||
tunnel = tunnelConfig,
|
||||
tunnelName = tunnelConfig.name,
|
||||
isAmneziaEnabled = isAmneziaEnabled,
|
||||
)
|
||||
} else {
|
||||
ConfigUiState(loading = false, packages = packages)
|
||||
@@ -121,7 +111,7 @@ constructor(
|
||||
}
|
||||
|
||||
fun getPackageLabel(packageInfo: PackageInfo): String {
|
||||
return packageInfo.applicationInfo.loadLabel(application.packageManager).toString()
|
||||
return packageInfo.applicationInfo.loadLabel(packageManager).toString()
|
||||
}
|
||||
|
||||
private fun getAllInternetCapablePackages(): List<PackageInfo> {
|
||||
@@ -150,7 +140,7 @@ constructor(
|
||||
viewModelScope.launch {
|
||||
if (tunnelConfig != null) {
|
||||
saveConfig(tunnelConfig).join()
|
||||
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(application)
|
||||
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,6 +158,20 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildAmPeerListFromProxyPeers(): List<org.amnezia.awg.config.Peer> {
|
||||
return _uiState.value.proxyPeers.map {
|
||||
val builder = org.amnezia.awg.config.Peer.Builder()
|
||||
if (it.allowedIps.isNotEmpty()) builder.parseAllowedIPs(it.allowedIps.trim())
|
||||
if (it.publicKey.isNotEmpty()) builder.parsePublicKey(it.publicKey.trim())
|
||||
if (it.preSharedKey.isNotEmpty()) builder.parsePreSharedKey(it.preSharedKey.trim())
|
||||
if (it.endpoint.isNotEmpty()) builder.parseEndpoint(it.endpoint.trim())
|
||||
if (it.persistentKeepalive.isNotEmpty()) {
|
||||
builder.parsePersistentKeepalive(it.persistentKeepalive.trim())
|
||||
}
|
||||
builder.build()
|
||||
}
|
||||
}
|
||||
|
||||
private fun emptyCheckedPackagesList() {
|
||||
_uiState.value = _uiState.value.copy(checkedPackageNames = emptyList())
|
||||
}
|
||||
@@ -190,147 +194,244 @@ constructor(
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
fun onSaveAllChanges(): Result<Event> {
|
||||
private fun buildAmInterfaceListFromProxyInterface(): org.amnezia.awg.config.Interface {
|
||||
val builder = org.amnezia.awg.config.Interface.Builder()
|
||||
builder.parsePrivateKey(_uiState.value.interfaceProxy.privateKey.trim())
|
||||
builder.parseAddresses(_uiState.value.interfaceProxy.addresses.trim())
|
||||
if (_uiState.value.interfaceProxy.dnsServers.isNotEmpty()) {
|
||||
builder.parseDnsServers(_uiState.value.interfaceProxy.dnsServers.trim())
|
||||
}
|
||||
if (_uiState.value.interfaceProxy.mtu.isNotEmpty())
|
||||
builder.parseMtu(_uiState.value.interfaceProxy.mtu.trim())
|
||||
if (_uiState.value.interfaceProxy.listenPort.isNotEmpty()) {
|
||||
builder.parseListenPort(_uiState.value.interfaceProxy.listenPort.trim())
|
||||
}
|
||||
if (isAllApplicationsEnabled()) emptyCheckedPackagesList()
|
||||
if (_uiState.value.include) builder.includeApplications(_uiState.value.checkedPackageNames)
|
||||
if (!_uiState.value.include) builder.excludeApplications(_uiState.value.checkedPackageNames)
|
||||
if (_uiState.value.interfaceProxy.junkPacketCount.isNotEmpty()) {
|
||||
builder.setJunkPacketCount(_uiState.value.interfaceProxy.junkPacketCount.trim().toInt())
|
||||
}
|
||||
if (_uiState.value.interfaceProxy.junkPacketMinSize.isNotEmpty()) {
|
||||
builder.setJunkPacketMinSize(
|
||||
_uiState.value.interfaceProxy.junkPacketMinSize.trim().toInt(),
|
||||
)
|
||||
}
|
||||
if (_uiState.value.interfaceProxy.junkPacketMaxSize.isNotEmpty()) {
|
||||
builder.setJunkPacketMaxSize(
|
||||
_uiState.value.interfaceProxy.junkPacketMaxSize.trim().toInt(),
|
||||
)
|
||||
}
|
||||
if (_uiState.value.interfaceProxy.initPacketJunkSize.isNotEmpty()) {
|
||||
builder.setInitPacketJunkSize(
|
||||
_uiState.value.interfaceProxy.initPacketJunkSize.trim().toInt(),
|
||||
)
|
||||
}
|
||||
if (_uiState.value.interfaceProxy.responsePacketJunkSize.isNotEmpty()) {
|
||||
builder.setResponsePacketJunkSize(
|
||||
_uiState.value.interfaceProxy.responsePacketJunkSize.trim().toInt(),
|
||||
)
|
||||
}
|
||||
if (_uiState.value.interfaceProxy.initPacketMagicHeader.isNotEmpty()) {
|
||||
builder.setInitPacketMagicHeader(
|
||||
_uiState.value.interfaceProxy.initPacketMagicHeader.trim().toLong(),
|
||||
)
|
||||
}
|
||||
if (_uiState.value.interfaceProxy.responsePacketMagicHeader.isNotEmpty()) {
|
||||
builder.setResponsePacketMagicHeader(
|
||||
_uiState.value.interfaceProxy.responsePacketMagicHeader.trim().toLong(),
|
||||
)
|
||||
}
|
||||
if (_uiState.value.interfaceProxy.transportPacketMagicHeader.isNotEmpty()) {
|
||||
builder.setTransportPacketMagicHeader(
|
||||
_uiState.value.interfaceProxy.transportPacketMagicHeader.trim().toLong(),
|
||||
)
|
||||
}
|
||||
if (_uiState.value.interfaceProxy.underloadPacketMagicHeader.isNotEmpty()) {
|
||||
builder.setUnderloadPacketMagicHeader(
|
||||
_uiState.value.interfaceProxy.underloadPacketMagicHeader.trim().toLong(),
|
||||
)
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
private fun buildConfig(): Config {
|
||||
val peerList = buildPeerListFromProxyPeers()
|
||||
val wgInterface = buildInterfaceListFromProxyInterface()
|
||||
return Config.Builder().addPeers(peerList).setInterface(wgInterface).build()
|
||||
}
|
||||
|
||||
private fun buildAmConfig(): org.amnezia.awg.config.Config {
|
||||
val peerList = buildAmPeerListFromProxyPeers()
|
||||
val amInterface = buildAmInterfaceListFromProxyInterface()
|
||||
return org.amnezia.awg.config.Config.Builder().addPeers(peerList).setInterface(amInterface)
|
||||
.build()
|
||||
}
|
||||
|
||||
fun onSaveAllChanges(configType: ConfigType): Result<Unit> {
|
||||
return try {
|
||||
val peerList = buildPeerListFromProxyPeers()
|
||||
val wgInterface = buildInterfaceListFromProxyInterface()
|
||||
val config = Config.Builder().addPeers(peerList).setInterface(wgInterface).build()
|
||||
val wgQuick = buildConfig().toWgQuickString()
|
||||
val amQuick = if (configType == ConfigType.AMNEZIA) {
|
||||
buildAmConfig().toAwgQuickString()
|
||||
} else TunnelConfig.AM_QUICK_DEFAULT
|
||||
val tunnelConfig = when (uiState.value.tunnel) {
|
||||
null -> TunnelConfig(
|
||||
name = _uiState.value.tunnelName,
|
||||
wgQuick = config.toWgQuickString(),
|
||||
wgQuick = wgQuick,
|
||||
amQuick = amQuick,
|
||||
)
|
||||
|
||||
else -> uiState.value.tunnel!!.copy(
|
||||
name = _uiState.value.tunnelName,
|
||||
wgQuick = config.toWgQuickString(),
|
||||
wgQuick = wgQuick,
|
||||
amQuick = amQuick,
|
||||
)
|
||||
}
|
||||
updateTunnelConfig(tunnelConfig)
|
||||
Result.Success(Event.Message.ConfigSaved)
|
||||
Result.success(Unit)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
val message = e.message?.substringAfter(":", missingDelimiterValue = "")
|
||||
Result.Error(Event.Error.ConfigParseError(message ?: ""))
|
||||
val stringValue = message?.let {
|
||||
StringValue.DynamicString(message)
|
||||
} ?: StringValue.StringResource(R.string.unknown_error)
|
||||
Result.failure(WgTunnelExceptions.ConfigParseError(stringValue))
|
||||
}
|
||||
}
|
||||
|
||||
fun onPeerPublicKeyChange(index: Int, value: String) {
|
||||
_uiState.value =
|
||||
_uiState.value.copy(
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
proxyPeers =
|
||||
_uiState.value.proxyPeers.update(
|
||||
index,
|
||||
_uiState.value.proxyPeers[index].copy(publicKey = value),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onPreSharedKeyChange(index: Int, value: String) {
|
||||
_uiState.value =
|
||||
_uiState.value.copy(
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
proxyPeers =
|
||||
_uiState.value.proxyPeers.update(
|
||||
index,
|
||||
_uiState.value.proxyPeers[index].copy(preSharedKey = value),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onEndpointChange(index: Int, value: String) {
|
||||
_uiState.value =
|
||||
_uiState.value.copy(
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
proxyPeers =
|
||||
_uiState.value.proxyPeers.update(
|
||||
index,
|
||||
_uiState.value.proxyPeers[index].copy(endpoint = value),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onAllowedIpsChange(index: Int, value: String) {
|
||||
_uiState.value =
|
||||
_uiState.value.copy(
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
proxyPeers =
|
||||
_uiState.value.proxyPeers.update(
|
||||
index,
|
||||
_uiState.value.proxyPeers[index].copy(allowedIps = value),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onPersistentKeepaliveChanged(index: Int, value: String) {
|
||||
_uiState.value =
|
||||
_uiState.value.copy(
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
proxyPeers =
|
||||
_uiState.value.proxyPeers.update(
|
||||
index,
|
||||
_uiState.value.proxyPeers[index].copy(persistentKeepalive = value),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onDeletePeer(index: Int) {
|
||||
_uiState.value =
|
||||
_uiState.value.copy(
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
proxyPeers = _uiState.value.proxyPeers.removeAt(index),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun addEmptyPeer() {
|
||||
_uiState.value = _uiState.value.copy(proxyPeers = _uiState.value.proxyPeers + PeerProxy())
|
||||
_uiState.update {
|
||||
it.copy(proxyPeers = _uiState.value.proxyPeers + PeerProxy())
|
||||
}
|
||||
}
|
||||
|
||||
fun generateKeyPair() {
|
||||
val keyPair = KeyPair()
|
||||
_uiState.value =
|
||||
_uiState.value.copy(
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
interfaceProxy =
|
||||
_uiState.value.interfaceProxy.copy(
|
||||
privateKey = keyPair.privateKey.toBase64(),
|
||||
publicKey = keyPair.publicKey.toBase64(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onAddressesChanged(value: String) {
|
||||
_uiState.value =
|
||||
_uiState.value.copy(
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(addresses = value),
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun onListenPortChanged(value: String) {
|
||||
_uiState.value =
|
||||
_uiState.value.copy(
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(listenPort = value),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onDnsServersChanged(value: String) {
|
||||
_uiState.value =
|
||||
_uiState.value.copy(
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(dnsServers = value),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onMtuChanged(value: String) {
|
||||
_uiState.value =
|
||||
_uiState.value.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(mtu = value))
|
||||
_uiState.update {
|
||||
it.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(mtu = value))
|
||||
}
|
||||
}
|
||||
|
||||
private fun onInterfacePublicKeyChange(value: String) {
|
||||
_uiState.value =
|
||||
_uiState.value.copy(
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(publicKey = value),
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun onPrivateKeyChange(value: String) {
|
||||
_uiState.value =
|
||||
_uiState.value.copy(
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(privateKey = value),
|
||||
)
|
||||
}
|
||||
if (NumberUtils.isValidKey(value)) {
|
||||
val pair = KeyPair(Key.fromBase64(value))
|
||||
onInterfacePublicKeyChange(pair.publicKey.toBase64())
|
||||
@@ -344,6 +445,78 @@ constructor(
|
||||
getAllInternetCapablePackages().filter {
|
||||
getPackageLabel(it).lowercase().contains(query.lowercase())
|
||||
}
|
||||
_uiState.value = _uiState.value.copy(packages = packages)
|
||||
_uiState.update { it.copy(packages = packages) }
|
||||
}
|
||||
|
||||
fun onJunkPacketCountChanged(value: String) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(junkPacketCount = value),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onJunkPacketMinSizeChanged(value: String) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(junkPacketMinSize = value),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onJunkPacketMaxSizeChanged(value: String) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(junkPacketMaxSize = value),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onInitPacketJunkSizeChanged(value: String) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(initPacketJunkSize = value),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onResponsePacketJunkSize(value: String) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(responsePacketJunkSize = value),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onInitPacketMagicHeader(value: String) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(initPacketMagicHeader = value),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onResponsePacketMagicHeader(value: String) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(responsePacketMagicHeader = value),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onTransportPacketMagicHeader(value: String) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(transportPacketMagicHeader = value),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onUnderloadPacketMagicHeader(value: String) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(underloadPacketMagicHeader = value),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.config.model
|
||||
|
||||
import com.wireguard.config.Interface
|
||||
|
||||
data class InterfaceProxy(
|
||||
val privateKey: String = "",
|
||||
val publicKey: String = "",
|
||||
val addresses: String = "",
|
||||
val dnsServers: String = "",
|
||||
val listenPort: String = "",
|
||||
val mtu: String = "",
|
||||
val junkPacketCount: String = "",
|
||||
val junkPacketMinSize: String = "",
|
||||
val junkPacketMaxSize: String = "",
|
||||
val initPacketJunkSize: String = "",
|
||||
val responsePacketJunkSize: String = "",
|
||||
val initPacketMagicHeader: String = "",
|
||||
val responsePacketMagicHeader: String = "",
|
||||
val underloadPacketMagicHeader: String = "",
|
||||
val transportPacketMagicHeader: String = "",
|
||||
) {
|
||||
companion object {
|
||||
fun from(i: Interface): InterfaceProxy {
|
||||
return InterfaceProxy(
|
||||
publicKey = i.keyPair.publicKey.toBase64().trim(),
|
||||
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 "",
|
||||
)
|
||||
}
|
||||
|
||||
fun from(i: org.amnezia.awg.config.Interface): InterfaceProxy {
|
||||
return InterfaceProxy(
|
||||
publicKey = i.keyPair.publicKey.toBase64().trim(),
|
||||
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 "",
|
||||
junkPacketCount = if (i.junkPacketCount.isPresent) i.junkPacketCount.get()
|
||||
.toString() else "",
|
||||
junkPacketMinSize = if (i.junkPacketMinSize.isPresent) i.junkPacketMinSize.get()
|
||||
.toString() else "",
|
||||
junkPacketMaxSize = if (i.junkPacketMaxSize.isPresent) i.junkPacketMaxSize.get()
|
||||
.toString() else "",
|
||||
initPacketJunkSize = if (i.initPacketJunkSize.isPresent) i.initPacketJunkSize.get()
|
||||
.toString() else "",
|
||||
responsePacketJunkSize = if (i.responsePacketJunkSize.isPresent) i.responsePacketJunkSize.get()
|
||||
.toString() else "",
|
||||
initPacketMagicHeader = if (i.initPacketMagicHeader.isPresent) i.initPacketMagicHeader.get()
|
||||
.toString() else "",
|
||||
responsePacketMagicHeader = if (i.responsePacketMagicHeader.isPresent) i.responsePacketMagicHeader.get()
|
||||
.toString() else "",
|
||||
transportPacketMagicHeader = if (i.transportPacketMagicHeader.isPresent) i.transportPacketMagicHeader.get()
|
||||
.toString() else "",
|
||||
underloadPacketMagicHeader = if (i.underloadPacketMagicHeader.isPresent) i.underloadPacketMagicHeader.get()
|
||||
.toString() else "",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+31
-6
@@ -1,13 +1,13 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.models
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.config.model
|
||||
|
||||
import com.wireguard.config.Peer
|
||||
|
||||
data class PeerProxy(
|
||||
var publicKey: String = "",
|
||||
var preSharedKey: String = "",
|
||||
var persistentKeepalive: String = "",
|
||||
var endpoint: String = "",
|
||||
var allowedIps: String = IPV4_WILDCARD.joinToString(", ").trim()
|
||||
val publicKey: String = "",
|
||||
val preSharedKey: String = "",
|
||||
val persistentKeepalive: String = "",
|
||||
val endpoint: String = "",
|
||||
val allowedIps: String = IPV4_WILDCARD.joinToString(", ").trim()
|
||||
) {
|
||||
companion object {
|
||||
fun from(peer: Peer): PeerProxy {
|
||||
@@ -35,6 +35,31 @@ data class PeerProxy(
|
||||
)
|
||||
}
|
||||
|
||||
fun from(peer: org.amnezia.awg.config.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(),
|
||||
)
|
||||
}
|
||||
|
||||
val IPV4_PUBLIC_NETWORKS =
|
||||
setOf(
|
||||
"0.0.0.0/5",
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.main
|
||||
|
||||
enum class ConfigType {
|
||||
AMNEZIA,
|
||||
WIREGUARD
|
||||
}
|
||||
+182
-92
@@ -14,6 +14,7 @@ import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.focusGroup
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.gestures.ScrollableDefaults
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
@@ -29,11 +30,11 @@ import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.overscroll
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Create
|
||||
import androidx.compose.material.icons.filled.FileOpen
|
||||
import androidx.compose.material.icons.filled.QrCode
|
||||
import androidx.compose.material.icons.rounded.Add
|
||||
import androidx.compose.material.icons.rounded.Bolt
|
||||
import androidx.compose.material.icons.rounded.Circle
|
||||
import androidx.compose.material.icons.rounded.CopyAll
|
||||
@@ -45,7 +46,6 @@ import androidx.compose.material.icons.rounded.Star
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FabPosition
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
@@ -70,7 +70,6 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
@@ -81,33 +80,36 @@ import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavController
|
||||
import com.iamageo.multifablibrary.FabIcon
|
||||
import com.iamageo.multifablibrary.FabOption
|
||||
import com.iamageo.multifablibrary.MultiFabItem
|
||||
import com.iamageo.multifablibrary.MultiFloatingActionButton
|
||||
import com.journeyapps.barcodescanner.ScanContract
|
||||
import com.journeyapps.barcodescanner.ScanOptions
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.CaptureActivityPortrait
|
||||
import com.zaneschepke.wireguardautotunnel.ui.Screen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.corn
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.mint
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.Event
|
||||
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||
import com.zaneschepke.wireguardautotunnel.util.getMessage
|
||||
import com.zaneschepke.wireguardautotunnel.util.handshakeStatus
|
||||
import com.zaneschepke.wireguardautotunnel.util.mapPeerStats
|
||||
import com.zaneschepke.wireguardautotunnel.util.truncateWithEllipsis
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -123,10 +125,11 @@ fun MainScreen(
|
||||
val haptic = LocalHapticFeedback.current
|
||||
val context = LocalContext.current
|
||||
val isVisible = rememberSaveable { mutableStateOf(true) }
|
||||
val scope = rememberCoroutineScope { Dispatchers.IO }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val sheetState = rememberModalBottomSheetState()
|
||||
var showBottomSheet by remember { mutableStateOf(false) }
|
||||
var configType by remember { mutableStateOf(ConfigType.WIREGUARD) }
|
||||
|
||||
// Nested scroll for control FAB
|
||||
val nestedScrollConnection = remember {
|
||||
@@ -187,7 +190,7 @@ fun MainScreen(
|
||||
name.startsWith(Constants.ANDROID_TV_EXPLORER_STUB)
|
||||
}
|
||||
) {
|
||||
appViewModel.showSnackbarMessage(Event.Error.FileExplorerRequired.message)
|
||||
appViewModel.showSnackbarMessage(context.getString(R.string.error_no_file_explorer))
|
||||
}
|
||||
return intent
|
||||
}
|
||||
@@ -195,11 +198,8 @@ fun MainScreen(
|
||||
) { data ->
|
||||
if (data == null) return@rememberLauncherForActivityResult
|
||||
scope.launch {
|
||||
viewModel.onTunnelFileSelected(data).let {
|
||||
when (it) {
|
||||
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
|
||||
is Result.Success -> {}
|
||||
}
|
||||
viewModel.onTunnelFileSelected(data, configType, context).onFailure {
|
||||
appViewModel.showSnackbarMessage(it.getMessage(context))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -209,11 +209,8 @@ fun MainScreen(
|
||||
onResult = {
|
||||
if (it.contents != null) {
|
||||
scope.launch {
|
||||
viewModel.onTunnelQrResult(it.contents).let { result ->
|
||||
when (result) {
|
||||
is Result.Success -> {}
|
||||
is Result.Error -> appViewModel.showSnackbarMessage(result.error.message)
|
||||
}
|
||||
viewModel.onTunnelQrResult(it.contents, configType).onFailure { error ->
|
||||
appViewModel.showSnackbarMessage(error.getMessage(context))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -226,7 +223,7 @@ fun MainScreen(
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
selectedTunnel?.let { viewModel.onDelete(it) }
|
||||
selectedTunnel?.let { viewModel.onDelete(it, context) }
|
||||
showDeleteTunnelAlertDialog = false
|
||||
selectedTunnel = null
|
||||
},
|
||||
@@ -246,7 +243,9 @@ fun MainScreen(
|
||||
|
||||
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) {
|
||||
if (appViewModel.isRequiredPermissionGranted()) {
|
||||
if (checked) viewModel.onTunnelStart(tunnel) else viewModel.onTunnelStop()
|
||||
if (checked) viewModel.onTunnelStart(tunnel, context) else viewModel.onTunnelStop(
|
||||
context,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,14 +253,28 @@ fun MainScreen(
|
||||
return LoadingScreen()
|
||||
}
|
||||
|
||||
fun launchQrScanner() {
|
||||
val scanOptions = ScanOptions()
|
||||
scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE)
|
||||
scanOptions.setOrientationLocked(true)
|
||||
scanOptions.setPrompt(
|
||||
context.getString(R.string.scanning_qr),
|
||||
)
|
||||
scanOptions.setBeepEnabled(false)
|
||||
scanLauncher.launch(scanOptions)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
modifier =
|
||||
Modifier.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onTap = {
|
||||
selectedTunnel = null
|
||||
},
|
||||
)
|
||||
if (uiState.tunnels.isNotEmpty()) {
|
||||
detectTapGestures(
|
||||
onTap = {
|
||||
selectedTunnel = null
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
},
|
||||
floatingActionButtonPosition = FabPosition.End,
|
||||
floatingActionButton = {
|
||||
@@ -269,49 +282,77 @@ fun MainScreen(
|
||||
visible = isVisible.value,
|
||||
enter = slideInVertically(initialOffsetY = { it * 2 }),
|
||||
exit = slideOutVertically(targetOffsetY = { it * 2 }),
|
||||
modifier = Modifier
|
||||
.focusRequester(focusRequester)
|
||||
.focusGroup(),
|
||||
) {
|
||||
val secondaryColor = MaterialTheme.colorScheme.secondary
|
||||
val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
|
||||
var fobColor by remember { mutableStateOf(secondaryColor) }
|
||||
FloatingActionButton(
|
||||
modifier =
|
||||
(if (
|
||||
WireGuardAutoTunnel.isRunningOnAndroidTv() &&
|
||||
uiState.tunnels.isEmpty()
|
||||
)
|
||||
Modifier.focusRequester(focusRequester)
|
||||
else Modifier)
|
||||
.onFocusChanged {
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
fobColor = if (it.isFocused) hoverColor else secondaryColor
|
||||
}
|
||||
},
|
||||
onClick = { showBottomSheet = true },
|
||||
containerColor = fobColor,
|
||||
val tvFobColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
|
||||
val fobColor =
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) tvFobColor else secondaryColor
|
||||
val fobIconColor =
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) Color.White else MaterialTheme.colorScheme.background
|
||||
MultiFloatingActionButton(
|
||||
fabIcon = FabIcon(
|
||||
iconRes = R.drawable.add,
|
||||
iconResAfterRotate = R.drawable.close,
|
||||
iconRotate = 180f,
|
||||
),
|
||||
fabOption = FabOption(
|
||||
iconTint = fobIconColor,
|
||||
backgroundTint = fobColor,
|
||||
),
|
||||
itemsMultiFab = listOf(
|
||||
MultiFabItem(
|
||||
label = {
|
||||
Text(
|
||||
stringResource(id = R.string.amnezia),
|
||||
color = Color.White,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(end = 10.dp),
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.size(40.dp),
|
||||
icon = R.drawable.add,
|
||||
value = ConfigType.AMNEZIA.name,
|
||||
miniFabOption = FabOption(
|
||||
backgroundTint = fobColor,
|
||||
fobIconColor,
|
||||
),
|
||||
),
|
||||
MultiFabItem(
|
||||
label = {
|
||||
Text(
|
||||
stringResource(id = R.string.wireguard),
|
||||
color = Color.White,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(end = 10.dp),
|
||||
)
|
||||
},
|
||||
icon = R.drawable.add,
|
||||
value = ConfigType.WIREGUARD.name,
|
||||
miniFabOption = FabOption(
|
||||
backgroundTint = fobColor,
|
||||
fobIconColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
onFabItemClicked = {
|
||||
showBottomSheet = true
|
||||
configType = ConfigType.valueOf(it.value)
|
||||
},
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Add,
|
||||
contentDescription = stringResource(id = R.string.add_tunnel),
|
||||
tint = Color.DarkGray,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
) {
|
||||
AnimatedVisibility(uiState.tunnels.isEmpty(), exit = fadeOut(), enter = fadeIn()) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
Text(text = stringResource(R.string.no_tunnels), fontStyle = FontStyle.Italic)
|
||||
}
|
||||
}
|
||||
if (showBottomSheet) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { showBottomSheet = false },
|
||||
onDismissRequest = {
|
||||
showBottomSheet = false
|
||||
|
||||
},
|
||||
sheetState = sheetState,
|
||||
) {
|
||||
// Sheet content
|
||||
@@ -344,16 +385,7 @@ fun MainScreen(
|
||||
.clickable {
|
||||
scope.launch {
|
||||
showBottomSheet = false
|
||||
val scanOptions = ScanOptions()
|
||||
scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE)
|
||||
scanOptions.setOrientationLocked(true)
|
||||
scanOptions.setPrompt(
|
||||
context.getString(R.string.scanning_qr),
|
||||
)
|
||||
scanOptions.setBeepEnabled(false)
|
||||
scanOptions.captureActivity =
|
||||
CaptureActivityPortrait::class.java
|
||||
scanLauncher.launch(scanOptions)
|
||||
launchQrScanner()
|
||||
}
|
||||
}
|
||||
.padding(10.dp),
|
||||
@@ -377,7 +409,7 @@ fun MainScreen(
|
||||
.clickable {
|
||||
showBottomSheet = false
|
||||
navController.navigate(
|
||||
"${Screen.Config.route}/${Constants.MANUAL_TUNNEL_CONFIG_ID}",
|
||||
"${Screen.Config.route}/${Constants.MANUAL_TUNNEL_CONFIG_ID}?configType=${configType}",
|
||||
)
|
||||
}
|
||||
.padding(10.dp),
|
||||
@@ -401,14 +433,63 @@ fun MainScreen(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.overscroll(ScrollableDefaults.overscrollEffect()).nestedScroll(nestedScrollConnection),
|
||||
.overscroll(ScrollableDefaults.overscrollEffect())
|
||||
.nestedScroll(nestedScrollConnection),
|
||||
state = rememberLazyListState(0, uiState.tunnels.count()),
|
||||
userScrollEnabled = true,
|
||||
reverseLayout = false,
|
||||
flingBehavior = ScrollableDefaults.flingBehavior(),
|
||||
) {
|
||||
item {
|
||||
AnimatedVisibility(
|
||||
uiState.tunnels.isEmpty(), exit = fadeOut(), enter = fadeIn(),
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.padding(top = 100.dp)
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
val gettingStarted = buildAnnotatedString {
|
||||
append(stringResource(id = R.string.see_the))
|
||||
append(" ")
|
||||
pushStringAnnotation(
|
||||
tag = "gettingStarted",
|
||||
annotation = stringResource(id = R.string.getting_started_url),
|
||||
)
|
||||
withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.primary)) {
|
||||
append(stringResource(id = R.string.getting_started_guide))
|
||||
}
|
||||
pop()
|
||||
append(" ")
|
||||
append(stringResource(R.string.unsure_how))
|
||||
append(".")
|
||||
}
|
||||
Text(
|
||||
text = stringResource(R.string.no_tunnels),
|
||||
fontStyle = FontStyle.Italic,
|
||||
)
|
||||
ClickableText(
|
||||
modifier = Modifier
|
||||
.padding(vertical = 10.dp, horizontal = 24.dp),
|
||||
text = gettingStarted,
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
),
|
||||
) {
|
||||
gettingStarted.getStringAnnotations(tag = "gettingStarted", it, it)
|
||||
.firstOrNull()?.let { annotation ->
|
||||
appViewModel.openWebPage(annotation.item, context)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
if (uiState.settings.isAutoTunnelEnabled) {
|
||||
val itemFocusRequester = remember { FocusRequester() }
|
||||
val autoTunnelingLabel = buildAnnotatedString {
|
||||
append(stringResource(id = R.string.auto_tunneling))
|
||||
append(": ")
|
||||
@@ -436,22 +517,29 @@ fun MainScreen(
|
||||
rowButton = {
|
||||
if (uiState.settings.isAutoTunnelPaused) {
|
||||
TextButton(
|
||||
modifier = Modifier.focusRequester(itemFocusRequester),
|
||||
onClick = { viewModel.resumeAutoTunneling() },
|
||||
) {
|
||||
Text(stringResource(id = R.string.resume))
|
||||
}
|
||||
} else {
|
||||
TextButton(
|
||||
modifier = Modifier.focusRequester(itemFocusRequester),
|
||||
onClick = { viewModel.pauseAutoTunneling() },
|
||||
) {
|
||||
Text(stringResource(id = R.string.pause))
|
||||
}
|
||||
}
|
||||
},
|
||||
onClick = {},
|
||||
onClick = {
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
itemFocusRequester.requestFocus()
|
||||
}
|
||||
},
|
||||
onHold = {},
|
||||
expanded = false,
|
||||
statistics = null,
|
||||
focusRequester = focusRequester
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -462,7 +550,7 @@ fun MainScreen(
|
||||
val leadingIconColor =
|
||||
(if (
|
||||
uiState.vpnState.tunnelConfig?.name == tunnel.name &&
|
||||
uiState.vpnState.status == Tunnel.State.UP
|
||||
uiState.vpnState.status == TunnelState.UP
|
||||
) {
|
||||
uiState.vpnState.statistics
|
||||
?.mapPeerStats()
|
||||
@@ -482,6 +570,7 @@ fun MainScreen(
|
||||
} else {
|
||||
Color.Gray
|
||||
})
|
||||
val itemFocusRequester = remember { FocusRequester() }
|
||||
val expanded = remember { mutableStateOf(false) }
|
||||
RowListItem(
|
||||
icon = {
|
||||
@@ -505,13 +594,13 @@ fun MainScreen(
|
||||
.size(if (icon == circleIcon) 15.dp else 20.dp),
|
||||
)
|
||||
},
|
||||
text = tunnel.name.truncateWithEllipsis(Constants.ALLOWED_DISPLAY_NAME_LENGTH),
|
||||
text = tunnel.name,
|
||||
onHold = {
|
||||
if (
|
||||
(uiState.vpnState.status == Tunnel.State.UP) &&
|
||||
(uiState.vpnState.status == TunnelState.UP) &&
|
||||
(tunnel.name == uiState.vpnState.tunnelConfig?.name)
|
||||
) {
|
||||
appViewModel.showSnackbarMessage(Event.Message.TunnelOffAction.message)
|
||||
appViewModel.showSnackbarMessage(context.getString(R.string.turn_off_tunnel))
|
||||
return@RowListItem
|
||||
}
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
@@ -520,18 +609,19 @@ fun MainScreen(
|
||||
onClick = {
|
||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
if (
|
||||
uiState.vpnState.status == Tunnel.State.UP &&
|
||||
uiState.vpnState.status == TunnelState.UP &&
|
||||
(uiState.vpnState.tunnelConfig?.name == tunnel.name)
|
||||
) {
|
||||
expanded.value = !expanded.value
|
||||
}
|
||||
} else {
|
||||
selectedTunnel = tunnel
|
||||
focusRequester.requestFocus()
|
||||
itemFocusRequester.requestFocus()
|
||||
}
|
||||
},
|
||||
statistics = uiState.vpnState.statistics,
|
||||
expanded = expanded.value,
|
||||
focusRequester = focusRequester,
|
||||
rowButton = {
|
||||
if (
|
||||
tunnel.id == selectedTunnel?.id &&
|
||||
@@ -545,7 +635,7 @@ fun MainScreen(
|
||||
!uiState.settings.isAutoTunnelPaused
|
||||
) {
|
||||
appViewModel.showSnackbarMessage(
|
||||
Event.Message.AutoTunnelOffAction.message,
|
||||
context.getString(R.string.turn_off_tunnel),
|
||||
)
|
||||
} else {
|
||||
navController.navigate(
|
||||
@@ -578,7 +668,7 @@ fun MainScreen(
|
||||
} else {
|
||||
val checked by remember {
|
||||
derivedStateOf {
|
||||
(uiState.vpnState.status == Tunnel.State.UP &&
|
||||
(uiState.vpnState.status == TunnelState.UP &&
|
||||
tunnel.name == uiState.vpnState.tunnelConfig?.name)
|
||||
}
|
||||
}
|
||||
@@ -587,7 +677,7 @@ fun MainScreen(
|
||||
@Composable
|
||||
fun TunnelSwitch() =
|
||||
Switch(
|
||||
modifier = Modifier.focusRequester(focusRequester),
|
||||
modifier = Modifier.focusRequester(itemFocusRequester),
|
||||
checked = checked,
|
||||
onCheckedChange = { checked ->
|
||||
if (!checked) expanded.value = false
|
||||
@@ -598,9 +688,9 @@ fun MainScreen(
|
||||
Row {
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (uiState.settings.isAutoTunnelEnabled) {
|
||||
if (uiState.settings.isAutoTunnelEnabled && !uiState.settings.isAutoTunnelPaused) {
|
||||
appViewModel.showSnackbarMessage(
|
||||
Event.Message.AutoTunnelOffAction.message,
|
||||
context.getString(R.string.turn_off_auto),
|
||||
)
|
||||
} else {
|
||||
selectedTunnel = tunnel
|
||||
@@ -620,13 +710,13 @@ fun MainScreen(
|
||||
modifier = Modifier.focusRequester(focusRequester),
|
||||
onClick = {
|
||||
if (
|
||||
uiState.vpnState.status == Tunnel.State.UP &&
|
||||
uiState.vpnState.status == TunnelState.UP &&
|
||||
(uiState.vpnState.tunnelConfig?.name == tunnel.name)
|
||||
) {
|
||||
expanded.value = !expanded.value
|
||||
} else {
|
||||
appViewModel.showSnackbarMessage(
|
||||
Event.Message.TunnelOnAction.message,
|
||||
context.getString(R.string.turn_on_tunnel),
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -643,11 +733,11 @@ fun MainScreen(
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (
|
||||
uiState.vpnState.status == Tunnel.State.UP &&
|
||||
uiState.vpnState.status == TunnelState.UP &&
|
||||
tunnel.name == uiState.vpnState.tunnelConfig?.name
|
||||
) {
|
||||
appViewModel.showSnackbarMessage(
|
||||
Event.Message.TunnelOffAction.message,
|
||||
context.getString(R.string.turn_off_tunnel),
|
||||
)
|
||||
} else {
|
||||
selectedTunnel = tunnel
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.main
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
|
||||
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
||||
|
||||
|
||||
+220
-94
@@ -1,6 +1,5 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.main
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
@@ -9,17 +8,18 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.wireguard.config.Config
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
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.Result
|
||||
import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions
|
||||
import com.zaneschepke.wireguardautotunnel.util.toWgQuickString
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
@@ -34,10 +34,10 @@ import javax.inject.Inject
|
||||
class MainViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val application: Application,
|
||||
private val appDataRepository: AppDataRepository,
|
||||
private val serviceManager: ServiceManager,
|
||||
val vpnService: VpnService
|
||||
val vpnService: VpnService,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
|
||||
) : ViewModel() {
|
||||
|
||||
val uiState =
|
||||
@@ -54,21 +54,20 @@ constructor(
|
||||
MainUiState(),
|
||||
)
|
||||
|
||||
private fun stopWatcherService() =
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
serviceManager.stopWatcherService(application.applicationContext)
|
||||
}
|
||||
private fun stopWatcherService(context: Context) {
|
||||
serviceManager.stopWatcherService(context)
|
||||
}
|
||||
|
||||
fun onDelete(tunnel: TunnelConfig) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
fun onDelete(tunnel: TunnelConfig, context: Context) {
|
||||
viewModelScope.launch {
|
||||
val settings = appDataRepository.settings.getSettings()
|
||||
val isPrimary = tunnel.isPrimaryTunnel
|
||||
if (appDataRepository.tunnels.count() == 1 || isPrimary) {
|
||||
stopWatcherService()
|
||||
stopWatcherService(context)
|
||||
resetTunnelSetting(settings)
|
||||
}
|
||||
appDataRepository.tunnels.delete(tunnel)
|
||||
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(application)
|
||||
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,124 +80,251 @@ constructor(
|
||||
)
|
||||
}
|
||||
|
||||
fun onTunnelStart(tunnelConfig: TunnelConfig) =
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
fun onTunnelStart(tunnelConfig: TunnelConfig, context: Context) =
|
||||
viewModelScope.launch {
|
||||
Timber.d("On start called!")
|
||||
serviceManager.startVpnService(
|
||||
application.applicationContext,
|
||||
context,
|
||||
tunnelConfig.id,
|
||||
isManualStart = true,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
fun onTunnelStop() =
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
fun onTunnelStop(context: Context) =
|
||||
viewModelScope.launch {
|
||||
Timber.i("Stopping active tunnel")
|
||||
serviceManager.stopVpnService(application.applicationContext, isManualStop = true)
|
||||
serviceManager.stopVpnService(context, isManualStop = true)
|
||||
}
|
||||
|
||||
private fun validateConfigString(config: String) {
|
||||
TunnelConfig.configFromQuick(config)
|
||||
private fun validateConfigString(config: String, configType: ConfigType) {
|
||||
when (configType) {
|
||||
ConfigType.AMNEZIA -> TunnelConfig.configFromAmQuick(config)
|
||||
ConfigType.WIREGUARD -> TunnelConfig.configFromWgQuick(config)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun onTunnelQrResult(result: String): Result<Unit> {
|
||||
private fun generateQrCodeDefaultName(config: String, configType: ConfigType): String {
|
||||
return try {
|
||||
validateConfigString(result)
|
||||
val tunnelConfig =
|
||||
TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = result)
|
||||
addTunnel(tunnelConfig)
|
||||
Result.Success(Unit)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
Result.Error(Event.Error.InvalidQrCode)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun saveTunnelConfigFromStream(stream: InputStream, fileName: String) {
|
||||
val bufferReader = stream.bufferedReader(charset = Charsets.UTF_8)
|
||||
val config = Config.parse(bufferReader)
|
||||
val tunnelName = getNameFromFileName(fileName)
|
||||
addTunnel(TunnelConfig(name = tunnelName, wgQuick = config.toWgQuickString()))
|
||||
withContext(Dispatchers.IO) { stream.close() }
|
||||
}
|
||||
|
||||
private fun getInputStreamFromUri(uri: Uri): InputStream? {
|
||||
return application.applicationContext.contentResolver.openInputStream(uri)
|
||||
}
|
||||
|
||||
suspend fun onTunnelFileSelected(uri: Uri): Result<Unit> {
|
||||
try {
|
||||
if (isValidUriContentScheme(uri)) {
|
||||
val fileName = getFileName(application.applicationContext, uri)
|
||||
when (getFileExtensionFromFileName(fileName)) {
|
||||
Constants.CONF_FILE_EXTENSION ->
|
||||
saveTunnelFromConfUri(fileName, uri).let {
|
||||
when (it) {
|
||||
is Result.Error -> return Result.Error(Event.Error.FileReadFailed)
|
||||
is Result.Success -> return it
|
||||
}
|
||||
}
|
||||
|
||||
Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri)
|
||||
else -> return Result.Error(Event.Error.InvalidFileExtension)
|
||||
when (configType) {
|
||||
ConfigType.AMNEZIA -> {
|
||||
TunnelConfig.configFromAmQuick(config).peers[0].endpoint.get().host
|
||||
}
|
||||
|
||||
ConfigType.WIREGUARD -> {
|
||||
TunnelConfig.configFromWgQuick(config).peers[0].endpoint.get().host
|
||||
}
|
||||
return Result.Success(Unit)
|
||||
} else {
|
||||
return Result.Error(Event.Error.InvalidFileExtension)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
return Result.Error(Event.Error.FileReadFailed)
|
||||
NumberUtils.generateRandomTunnelName()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun saveTunnelsFromZipUri(uri: Uri) {
|
||||
ZipInputStream(getInputStreamFromUri(uri)).use { zip ->
|
||||
generateSequence { zip.nextEntry }
|
||||
.filterNot {
|
||||
it.isDirectory ||
|
||||
getFileExtensionFromFileName(it.name) != Constants.CONF_FILE_EXTENSION
|
||||
}
|
||||
.forEach {
|
||||
val name = getNameFromFileName(it.name)
|
||||
val config = Config.parse(zip)
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
addTunnel(TunnelConfig(name = name, wgQuick = config.toWgQuickString()))
|
||||
private fun generateQrCodeTunnelName(config: String, configType: ConfigType): String {
|
||||
var defaultName = generateQrCodeDefaultName(config, configType)
|
||||
val lines = config.lines().toMutableList()
|
||||
val linesIterator = lines.iterator()
|
||||
while (linesIterator.hasNext()) {
|
||||
val next = linesIterator.next()
|
||||
if (next.contains(Constants.QR_CODE_NAME_PROPERTY)) {
|
||||
defaultName = next.substringAfter(Constants.QR_CODE_NAME_PROPERTY).trim()
|
||||
break
|
||||
}
|
||||
}
|
||||
return defaultName
|
||||
}
|
||||
|
||||
suspend fun onTunnelQrResult(result: String, configType: ConfigType): Result<Unit> {
|
||||
return withContext(ioDispatcher) {
|
||||
try {
|
||||
validateConfigString(result, configType)
|
||||
val tunnelName = makeTunnelNameUnique(generateQrCodeTunnelName(result, configType))
|
||||
val tunnelConfig = when (configType) {
|
||||
ConfigType.AMNEZIA -> {
|
||||
TunnelConfig(
|
||||
name = tunnelName, amQuick = result,
|
||||
wgQuick = TunnelConfig.configFromAmQuick(result).toWgQuickString(),
|
||||
)
|
||||
}
|
||||
|
||||
ConfigType.WIREGUARD -> TunnelConfig(name = tunnelName, wgQuick = result)
|
||||
}
|
||||
addTunnel(tunnelConfig)
|
||||
Result.success(Unit)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
Result.failure(WgTunnelExceptions.InvalidQrCode())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun saveTunnelFromConfUri(name: String, uri: Uri): Result<Unit> {
|
||||
val stream = getInputStreamFromUri(uri)
|
||||
return if (stream != null) {
|
||||
saveTunnelConfigFromStream(stream, name)
|
||||
Result.Success(Unit)
|
||||
} else {
|
||||
Result.Error(Event.Error.FileReadFailed)
|
||||
private suspend fun makeTunnelNameUnique(name: String): String {
|
||||
return withContext(ioDispatcher) {
|
||||
val tunnels = appDataRepository.tunnels.getAll()
|
||||
var tunnelName = name
|
||||
var num = 1
|
||||
while (tunnels.any { it.name == tunnelName }) {
|
||||
tunnelName = name + "(${num})"
|
||||
num++
|
||||
}
|
||||
tunnelName
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun addTunnel(tunnelConfig: TunnelConfig) {
|
||||
private fun saveTunnelConfigFromStream(
|
||||
stream: InputStream,
|
||||
fileName: String,
|
||||
type: ConfigType
|
||||
) {
|
||||
var amQuick: String? = null
|
||||
val wgQuick = stream.use {
|
||||
when (type) {
|
||||
ConfigType.AMNEZIA -> {
|
||||
val config = org.amnezia.awg.config.Config.parse(it)
|
||||
amQuick = config.toAwgQuickString()
|
||||
config.toWgQuickString()
|
||||
}
|
||||
|
||||
ConfigType.WIREGUARD -> {
|
||||
Config.parse(it).toWgQuickString()
|
||||
}
|
||||
}
|
||||
}
|
||||
viewModelScope.launch {
|
||||
val tunnelName = makeTunnelNameUnique(getNameFromFileName(fileName))
|
||||
addTunnel(
|
||||
TunnelConfig(
|
||||
name = tunnelName,
|
||||
wgQuick = wgQuick,
|
||||
amQuick = amQuick ?: TunnelConfig.AM_QUICK_DEFAULT,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getInputStreamFromUri(uri: Uri, context: Context): InputStream? {
|
||||
return context.applicationContext.contentResolver.openInputStream(uri)
|
||||
}
|
||||
|
||||
suspend fun onTunnelFileSelected(
|
||||
uri: Uri,
|
||||
configType: ConfigType,
|
||||
context: Context
|
||||
): Result<Unit> {
|
||||
return withContext(ioDispatcher) {
|
||||
try {
|
||||
if (isValidUriContentScheme(uri)) {
|
||||
val fileName = getFileName(context, uri)
|
||||
return@withContext when (getFileExtensionFromFileName(fileName)) {
|
||||
Constants.CONF_FILE_EXTENSION ->
|
||||
saveTunnelFromConfUri(fileName, uri, configType, context)
|
||||
|
||||
Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(
|
||||
uri,
|
||||
configType,
|
||||
context,
|
||||
)
|
||||
|
||||
else -> Result.failure(WgTunnelExceptions.InvalidFileExtension())
|
||||
}
|
||||
} else {
|
||||
Result.failure(WgTunnelExceptions.InvalidFileExtension())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
Result.failure(WgTunnelExceptions.FileReadFailed())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun saveTunnelsFromZipUri(
|
||||
uri: Uri,
|
||||
configType: ConfigType,
|
||||
context: Context
|
||||
): Result<Unit> {
|
||||
return withContext(ioDispatcher) {
|
||||
ZipInputStream(getInputStreamFromUri(uri, context)).use { zip ->
|
||||
generateSequence { zip.nextEntry }
|
||||
.filterNot {
|
||||
it.isDirectory ||
|
||||
getFileExtensionFromFileName(it.name) != Constants.CONF_FILE_EXTENSION
|
||||
}
|
||||
.forEach {
|
||||
val name = getNameFromFileName(it.name)
|
||||
withContext(viewModelScope.coroutineContext) {
|
||||
try {
|
||||
var amQuick: String? = null
|
||||
val wgQuick =
|
||||
when (configType) {
|
||||
ConfigType.AMNEZIA -> {
|
||||
val config = org.amnezia.awg.config.Config.parse(zip)
|
||||
amQuick = config.toAwgQuickString()
|
||||
config.toWgQuickString()
|
||||
}
|
||||
|
||||
ConfigType.WIREGUARD -> {
|
||||
Config.parse(zip).toWgQuickString()
|
||||
}
|
||||
}
|
||||
addTunnel(
|
||||
TunnelConfig(
|
||||
name = makeTunnelNameUnique(name),
|
||||
wgQuick = wgQuick,
|
||||
amQuick = amQuick ?: TunnelConfig.AM_QUICK_DEFAULT,
|
||||
),
|
||||
)
|
||||
Result.success(Unit)
|
||||
} catch (e: Exception) {
|
||||
Result.failure(WgTunnelExceptions.FileReadFailed())
|
||||
}
|
||||
}
|
||||
}
|
||||
Result.success(Unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun saveTunnelFromConfUri(
|
||||
name: String,
|
||||
uri: Uri,
|
||||
configType: ConfigType,
|
||||
context: Context
|
||||
): Result<Unit> {
|
||||
return withContext(ioDispatcher) {
|
||||
val stream = getInputStreamFromUri(uri, context)
|
||||
return@withContext if (stream != null) {
|
||||
try {
|
||||
saveTunnelConfigFromStream(stream, name, configType)
|
||||
} catch (e: Exception) {
|
||||
return@withContext Result.failure(WgTunnelExceptions.ConfigParseError())
|
||||
}
|
||||
Result.success(Unit)
|
||||
} else {
|
||||
Result.failure(WgTunnelExceptions.FileReadFailed())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addTunnel(tunnelConfig: TunnelConfig) = viewModelScope.launch {
|
||||
val firstTunnel = appDataRepository.tunnels.count() == 0
|
||||
saveTunnel(tunnelConfig)
|
||||
if (firstTunnel) WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(application)
|
||||
if (firstTunnel) WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
|
||||
}
|
||||
|
||||
fun pauseAutoTunneling() =
|
||||
viewModelScope.launch {
|
||||
appDataRepository.settings.save(uiState.value.settings.copy(isAutoTunnelPaused = true))
|
||||
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate(application)
|
||||
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate()
|
||||
}
|
||||
|
||||
fun resumeAutoTunneling() =
|
||||
viewModelScope.launch {
|
||||
appDataRepository.settings.save(uiState.value.settings.copy(isAutoTunnelPaused = false))
|
||||
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate(application)
|
||||
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate()
|
||||
}
|
||||
|
||||
private suspend fun saveTunnel(tunnelConfig: TunnelConfig) {
|
||||
private fun saveTunnel(tunnelConfig: TunnelConfig) = viewModelScope.launch {
|
||||
appDataRepository.tunnels.save(tunnelConfig)
|
||||
}
|
||||
|
||||
@@ -239,17 +365,17 @@ constructor(
|
||||
return fileName.substring(0, fileName.lastIndexOf('.'))
|
||||
}
|
||||
|
||||
private fun getFileExtensionFromFileName(fileName: String): String {
|
||||
private fun getFileExtensionFromFileName(fileName: String): String? {
|
||||
return try {
|
||||
fileName.substring(fileName.lastIndexOf('.'))
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
""
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveSettings(settings: Settings) =
|
||||
viewModelScope.launch(Dispatchers.IO) { appDataRepository.settings.save(settings) }
|
||||
viewModelScope.launch { appDataRepository.settings.save(settings) }
|
||||
|
||||
|
||||
fun onCopyTunnel(tunnel: TunnelConfig?) = viewModelScope.launch {
|
||||
|
||||
+252
-180
@@ -1,17 +1,22 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.options
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.focusGroup
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
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.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
@@ -24,9 +29,10 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -39,15 +45,21 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavController
|
||||
import com.iamageo.multifablibrary.FabIcon
|
||||
import com.iamageo.multifablibrary.FabOption
|
||||
import com.iamageo.multifablibrary.MultiFabItem
|
||||
import com.iamageo.multifablibrary.MultiFloatingActionButton
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
||||
@@ -55,11 +67,13 @@ import com.zaneschepke.wireguardautotunnel.ui.Screen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||
import com.zaneschepke.wireguardautotunnel.util.getMessage
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun OptionsScreen(
|
||||
@@ -71,6 +85,8 @@ fun OptionsScreen(
|
||||
) {
|
||||
val scrollState = rememberScrollState()
|
||||
val uiState by optionsViewModel.uiState.collectAsStateWithLifecycle()
|
||||
val context = LocalContext.current
|
||||
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val scope = rememberCoroutineScope()
|
||||
val focusManager = LocalFocusManager.current
|
||||
@@ -90,196 +106,252 @@ fun OptionsScreen(
|
||||
fun saveTrustedSSID() {
|
||||
if (currentText.isNotEmpty()) {
|
||||
scope.launch {
|
||||
optionsViewModel.onSaveRunSSID(currentText).let {
|
||||
when (it) {
|
||||
is Result.Success -> currentText = ""
|
||||
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
|
||||
}
|
||||
optionsViewModel.onSaveRunSSID(currentText).onSuccess {
|
||||
currentText = ""
|
||||
}.onFailure {
|
||||
appViewModel.showSnackbarMessage(it.getMessage(context))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(scrollState)
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = interactionSource,
|
||||
Scaffold(
|
||||
floatingActionButton = {
|
||||
val secondaryColor = MaterialTheme.colorScheme.secondary
|
||||
val tvFobColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
|
||||
val fobColor =
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) tvFobColor else secondaryColor
|
||||
val fobIconColor =
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) Color.White else MaterialTheme.colorScheme.background
|
||||
AnimatedVisibility(
|
||||
visible = true,
|
||||
enter = slideInVertically(initialOffsetY = { it * 2 }),
|
||||
exit = slideOutVertically(targetOffsetY = { it * 2 }),
|
||||
modifier = Modifier
|
||||
.focusRequester(focusRequester)
|
||||
.focusGroup(),
|
||||
) {
|
||||
focusManager.clearFocus()
|
||||
},
|
||||
MultiFloatingActionButton(
|
||||
fabIcon = FabIcon(
|
||||
iconRes = R.drawable.edit,
|
||||
iconResAfterRotate = R.drawable.close,
|
||||
iconRotate = 180f,
|
||||
),
|
||||
fabOption = FabOption(
|
||||
iconTint = fobIconColor,
|
||||
backgroundTint = fobColor,
|
||||
),
|
||||
itemsMultiFab = listOf(
|
||||
MultiFabItem(
|
||||
label = {
|
||||
Text(
|
||||
stringResource(id = R.string.amnezia),
|
||||
color = Color.White,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(end = 10.dp),
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.size(40.dp),
|
||||
icon = R.drawable.edit,
|
||||
value = ConfigType.AMNEZIA.name,
|
||||
miniFabOption = FabOption(
|
||||
backgroundTint = fobColor,
|
||||
fobIconColor,
|
||||
),
|
||||
),
|
||||
MultiFabItem(
|
||||
label = {
|
||||
Text(
|
||||
stringResource(id = R.string.wireguard),
|
||||
color = Color.White,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(end = 10.dp),
|
||||
)
|
||||
},
|
||||
icon = R.drawable.edit,
|
||||
value = ConfigType.WIREGUARD.name,
|
||||
miniFabOption = FabOption(
|
||||
backgroundTint = fobColor,
|
||||
fobIconColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
onFabItemClicked = {
|
||||
val configType = ConfigType.valueOf(it.value)
|
||||
navController.navigate(
|
||||
"${Screen.Config.route}/${tunnelId}?configType=${configType.name}",
|
||||
)
|
||||
},
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
)
|
||||
}
|
||||
},
|
||||
) {
|
||||
Surface(
|
||||
tonalElevation = 2.dp,
|
||||
shadowElevation = 2.dp,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier =
|
||||
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
Modifier
|
||||
.height(IntrinsicSize.Min)
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
.padding(top = 10.dp)
|
||||
} else {
|
||||
Modifier
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
.padding(top = 20.dp)
|
||||
})
|
||||
.padding(bottom = 10.dp),
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier = Modifier.padding(15.dp),
|
||||
) {
|
||||
SectionTitle(
|
||||
title = stringResource(id = R.string.general),
|
||||
padding = screenPadding,
|
||||
)
|
||||
ConfigurationToggle(
|
||||
stringResource(R.string.set_primary_tunnel),
|
||||
enabled = true,
|
||||
checked = uiState.isDefaultTunnel,
|
||||
modifier = Modifier
|
||||
.focusRequester(focusRequester),
|
||||
padding = screenPadding,
|
||||
onCheckChanged = { optionsViewModel.onTogglePrimaryTunnel() },
|
||||
)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(top = 5.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(scrollState)
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = interactionSource,
|
||||
) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
navController.navigate(
|
||||
"${Screen.Config.route}/${tunnelId}",
|
||||
)
|
||||
},
|
||||
) {
|
||||
Text(stringResource(R.string.edit_tunnel))
|
||||
}
|
||||
focusManager.clearFocus()
|
||||
},
|
||||
) {
|
||||
Surface(
|
||||
tonalElevation = 2.dp,
|
||||
shadowElevation = 2.dp,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
modifier =
|
||||
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
Modifier
|
||||
.height(IntrinsicSize.Min)
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
.padding(top = 10.dp)
|
||||
} else {
|
||||
Modifier
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
.padding(top = 20.dp)
|
||||
})
|
||||
.padding(bottom = 10.dp),
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier = Modifier.padding(15.dp),
|
||||
) {
|
||||
SectionTitle(
|
||||
title = stringResource(id = R.string.general),
|
||||
padding = screenPadding,
|
||||
)
|
||||
ConfigurationToggle(
|
||||
stringResource(R.string.set_primary_tunnel),
|
||||
enabled = true,
|
||||
checked = uiState.isDefaultTunnel,
|
||||
modifier = Modifier
|
||||
.focusRequester(focusRequester),
|
||||
padding = screenPadding,
|
||||
onCheckChanged = { optionsViewModel.onTogglePrimaryTunnel() },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Surface(
|
||||
tonalElevation = 2.dp,
|
||||
shadowElevation = 2.dp,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
modifier =
|
||||
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
Modifier
|
||||
.height(IntrinsicSize.Min)
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
.padding(top = 10.dp)
|
||||
} else {
|
||||
Modifier
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
.padding(top = 20.dp)
|
||||
})
|
||||
.padding(bottom = 10.dp),
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier = Modifier.padding(15.dp),
|
||||
Surface(
|
||||
tonalElevation = 2.dp,
|
||||
shadowElevation = 2.dp,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
modifier =
|
||||
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
Modifier
|
||||
.height(IntrinsicSize.Min)
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
.padding(top = 10.dp)
|
||||
} else {
|
||||
Modifier
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
.padding(top = 20.dp)
|
||||
})
|
||||
.padding(bottom = 10.dp),
|
||||
) {
|
||||
SectionTitle(
|
||||
title = stringResource(id = R.string.auto_tunneling),
|
||||
padding = screenPadding,
|
||||
)
|
||||
ConfigurationToggle(
|
||||
stringResource(R.string.mobile_data_tunnel),
|
||||
enabled = true,
|
||||
checked = uiState.tunnel?.isMobileDataTunnel == true,
|
||||
padding = screenPadding,
|
||||
onCheckChanged = { optionsViewModel.onToggleIsMobileDataTunnel() },
|
||||
)
|
||||
Column {
|
||||
FlowRow(
|
||||
modifier = Modifier
|
||||
.padding(screenPadding)
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(5.dp),
|
||||
) {
|
||||
uiState.tunnel?.tunnelNetworks?.forEach { ssid ->
|
||||
ClickableIconButton(
|
||||
onClick = {
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
focusRequester.requestFocus()
|
||||
optionsViewModel.onDeleteRunSSID(ssid)
|
||||
}
|
||||
},
|
||||
onIconClick = {
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) focusRequester.requestFocus()
|
||||
optionsViewModel.onDeleteRunSSID(ssid)
|
||||
|
||||
},
|
||||
text = ssid,
|
||||
icon = Icons.Filled.Close,
|
||||
enabled = true,
|
||||
)
|
||||
}
|
||||
if (uiState.tunnel == null || uiState.tunnel?.tunnelNetworks?.isEmpty() == true) {
|
||||
Text(
|
||||
stringResource(R.string.no_wifi_names_configured),
|
||||
fontStyle = FontStyle.Italic,
|
||||
color = Color.Gray,
|
||||
)
|
||||
}
|
||||
}
|
||||
OutlinedTextField(
|
||||
enabled = true,
|
||||
value = currentText,
|
||||
onValueChange = { currentText = it },
|
||||
label = { Text(stringResource(id = R.string.use_tunnel_on_wifi_name)) },
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(
|
||||
start = screenPadding,
|
||||
top = 5.dp,
|
||||
bottom = 10.dp,
|
||||
),
|
||||
maxLines = 1,
|
||||
keyboardOptions =
|
||||
KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.None,
|
||||
imeAction = ImeAction.Done,
|
||||
),
|
||||
keyboardActions = KeyboardActions(onDone = { saveTrustedSSID() }),
|
||||
trailingIcon = {
|
||||
if (currentText != "") {
|
||||
IconButton(onClick = { saveTrustedSSID() }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Add,
|
||||
contentDescription =
|
||||
if (currentText == "") {
|
||||
stringResource(
|
||||
id =
|
||||
R.string
|
||||
.trusted_ssid_empty_description,
|
||||
)
|
||||
} else {
|
||||
stringResource(
|
||||
id =
|
||||
R.string
|
||||
.trusted_ssid_value_description,
|
||||
)
|
||||
},
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier = Modifier.padding(15.dp),
|
||||
) {
|
||||
SectionTitle(
|
||||
title = stringResource(id = R.string.auto_tunneling),
|
||||
padding = screenPadding,
|
||||
)
|
||||
ConfigurationToggle(
|
||||
stringResource(R.string.mobile_data_tunnel),
|
||||
enabled = true,
|
||||
checked = uiState.tunnel?.isMobileDataTunnel == true,
|
||||
padding = screenPadding,
|
||||
onCheckChanged = { optionsViewModel.onToggleIsMobileDataTunnel() },
|
||||
)
|
||||
Column {
|
||||
FlowRow(
|
||||
modifier = Modifier
|
||||
.padding(screenPadding)
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(5.dp),
|
||||
) {
|
||||
uiState.tunnel?.tunnelNetworks?.forEach { ssid ->
|
||||
ClickableIconButton(
|
||||
onClick = {
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
focusRequester.requestFocus()
|
||||
optionsViewModel.onDeleteRunSSID(ssid)
|
||||
}
|
||||
},
|
||||
onIconClick = {
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) focusRequester.requestFocus()
|
||||
optionsViewModel.onDeleteRunSSID(ssid)
|
||||
|
||||
},
|
||||
text = ssid,
|
||||
icon = Icons.Filled.Close,
|
||||
enabled = true,
|
||||
)
|
||||
}
|
||||
if (uiState.tunnel == null || uiState.tunnel?.tunnelNetworks?.isEmpty() == true) {
|
||||
Text(
|
||||
stringResource(R.string.no_wifi_names_configured),
|
||||
fontStyle = FontStyle.Italic,
|
||||
color = Color.Gray,
|
||||
)
|
||||
}
|
||||
}
|
||||
OutlinedTextField(
|
||||
enabled = true,
|
||||
value = currentText,
|
||||
onValueChange = { currentText = it },
|
||||
label = { Text(stringResource(id = R.string.use_tunnel_on_wifi_name)) },
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(
|
||||
start = screenPadding,
|
||||
top = 5.dp,
|
||||
bottom = 10.dp,
|
||||
),
|
||||
maxLines = 1,
|
||||
keyboardOptions =
|
||||
KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.None,
|
||||
imeAction = ImeAction.Done,
|
||||
),
|
||||
keyboardActions = KeyboardActions(onDone = { saveTrustedSSID() }),
|
||||
trailingIcon = {
|
||||
if (currentText != "") {
|
||||
IconButton(onClick = { saveTrustedSSID() }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Add,
|
||||
contentDescription =
|
||||
if (currentText == "") {
|
||||
stringResource(
|
||||
id =
|
||||
R.string
|
||||
.trusted_ssid_empty_description,
|
||||
)
|
||||
} else {
|
||||
stringResource(
|
||||
id =
|
||||
R.string
|
||||
.trusted_ssid_value_description,
|
||||
)
|
||||
},
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.options
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
|
||||
data class OptionsUiState(
|
||||
val id: String? = null,
|
||||
|
||||
+15
-14
@@ -4,17 +4,16 @@ import androidx.compose.ui.util.fastFirstOrNull
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.Event
|
||||
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||
import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
@@ -43,12 +42,14 @@ constructor(
|
||||
)
|
||||
|
||||
fun init(tunnelId: String) {
|
||||
_optionState.value = _optionState.value.copy(
|
||||
id = tunnelId,
|
||||
)
|
||||
_optionState.update {
|
||||
it.copy(
|
||||
id = tunnelId,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onDeleteRunSSID(ssid: String) = viewModelScope.launch(Dispatchers.IO) {
|
||||
fun onDeleteRunSSID(ssid: String) = viewModelScope.launch {
|
||||
uiState.value.tunnel?.let {
|
||||
appDataRepository.tunnels.save(
|
||||
tunnelConfig = it.copy(
|
||||
@@ -58,7 +59,7 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveTunnel(tunnelConfig: TunnelConfig?) = viewModelScope.launch(Dispatchers.IO) {
|
||||
private fun saveTunnel(tunnelConfig: TunnelConfig?) = viewModelScope.launch {
|
||||
tunnelConfig?.let {
|
||||
appDataRepository.tunnels.save(it)
|
||||
}
|
||||
@@ -73,13 +74,13 @@ constructor(
|
||||
tunnelsWithName.isEmpty()) {
|
||||
uiState.value.tunnel?.tunnelNetworks?.add(trimmed)
|
||||
saveTunnel(uiState.value.tunnel)
|
||||
Result.Success(Unit)
|
||||
Result.success(Unit)
|
||||
} else {
|
||||
Result.Error(Event.Error.SsidConflict)
|
||||
Result.failure(WgTunnelExceptions.SsidConflict())
|
||||
}
|
||||
}
|
||||
|
||||
fun onToggleIsMobileDataTunnel() = viewModelScope.launch(Dispatchers.IO) {
|
||||
fun onToggleIsMobileDataTunnel() = viewModelScope.launch {
|
||||
uiState.value.tunnel?.let {
|
||||
if (it.isMobileDataTunnel) {
|
||||
appDataRepository.tunnels.updateMobileDataTunnel(null)
|
||||
@@ -87,7 +88,7 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun onTogglePrimaryTunnel() = viewModelScope.launch(Dispatchers.IO) {
|
||||
fun onTogglePrimaryTunnel() = viewModelScope.launch {
|
||||
if (uiState.value.tunnel != null) {
|
||||
appDataRepository.tunnels.updatePrimaryTunnel(
|
||||
when (uiState.value.isDefaultTunnel) {
|
||||
@@ -95,7 +96,7 @@ constructor(
|
||||
false -> uiState.value.tunnel
|
||||
},
|
||||
)
|
||||
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(WireGuardAutoTunnel.instance)
|
||||
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+93
-60
@@ -26,7 +26,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
@@ -45,6 +44,7 @@ import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -71,23 +71,19 @@ import androidx.navigation.NavController
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.wireguard.android.backend.WgQuickBackend
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.Screen
|
||||
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.text.SectionTitle
|
||||
import com.zaneschepke.wireguardautotunnel.util.Event
|
||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import com.zaneschepke.wireguardautotunnel.util.getMessage
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||
import java.io.File
|
||||
|
||||
@OptIn(
|
||||
@@ -101,14 +97,14 @@ fun SettingsScreen(
|
||||
navController: NavController,
|
||||
focusRequester: FocusRequester,
|
||||
) {
|
||||
val scope = rememberCoroutineScope { Dispatchers.IO }
|
||||
val context = LocalContext.current
|
||||
val focusManager = LocalFocusManager.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val scrollState = rememberScrollState()
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val pinExists = remember { mutableStateOf(PinManager.pinExists()) }
|
||||
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
val kernelSupport by viewModel.kernelSupport.collectAsStateWithLifecycle()
|
||||
|
||||
val fineLocationState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
var currentText by remember { mutableStateOf("") }
|
||||
@@ -120,6 +116,10 @@ fun SettingsScreen(
|
||||
val screenPadding = 5.dp
|
||||
val fillMaxWidth = .85f
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.checkKernelSupport()
|
||||
}
|
||||
|
||||
val startForResult =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
@@ -131,16 +131,32 @@ fun SettingsScreen(
|
||||
|
||||
fun exportAllConfigs() {
|
||||
try {
|
||||
val files = uiState.tunnels.map { File(context.cacheDir, "${it.name}.conf") }
|
||||
files.forEachIndexed { index, file ->
|
||||
file.outputStream().use { it.write(uiState.tunnels[index].wgQuick.toByteArray()) }
|
||||
val wgFiles = uiState.tunnels.map { config ->
|
||||
val file = File(context.cacheDir, "${config.name}-wg.conf")
|
||||
file.outputStream().use {
|
||||
it.write(config.wgQuick.toByteArray())
|
||||
}
|
||||
file
|
||||
}
|
||||
val amFiles = uiState.tunnels.mapNotNull { config ->
|
||||
if (config.amQuick != TunnelConfig.AM_QUICK_DEFAULT) {
|
||||
val file = File(context.cacheDir, "${config.name}-am.conf")
|
||||
file.outputStream().use {
|
||||
it.write(config.amQuick.toByteArray())
|
||||
}
|
||||
file
|
||||
} else null
|
||||
}
|
||||
scope.launch {
|
||||
viewModel.onExportTunnels(wgFiles + amFiles).onFailure {
|
||||
appViewModel.showSnackbarMessage(it.getMessage(context))
|
||||
}.onSuccess {
|
||||
didExportFiles = true
|
||||
appViewModel.showSnackbarMessage(context.getString(R.string.exported_configs_message))
|
||||
}
|
||||
}
|
||||
FileUtils.saveFilesToZip(context, files)
|
||||
didExportFiles = true
|
||||
appViewModel.showSnackbarMessage(Event.Message.ConfigsExported.message)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
appViewModel.showSnackbarMessage(Event.Error.Exception(e).message)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,7 +177,7 @@ fun SettingsScreen(
|
||||
fun handleAutoTunnelToggle() {
|
||||
if (uiState.isBatteryOptimizeDisableShown || isBatteryOptimizationsDisabled()) {
|
||||
if (appViewModel.isRequiredPermissionGranted()) {
|
||||
viewModel.onToggleAutoTunnel()
|
||||
viewModel.onToggleAutoTunnel(context)
|
||||
}
|
||||
} else {
|
||||
requestBatteryOptimizationsDisabled()
|
||||
@@ -170,21 +186,18 @@ fun SettingsScreen(
|
||||
|
||||
fun saveTrustedSSID() {
|
||||
if (currentText.isNotEmpty()) {
|
||||
viewModel.onSaveTrustedSSID(currentText).let {
|
||||
when (it) {
|
||||
is Result.Success -> currentText = ""
|
||||
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
|
||||
}
|
||||
viewModel.onSaveTrustedSSID(currentText).onSuccess {
|
||||
currentText = ""
|
||||
}.onFailure {
|
||||
appViewModel.showSnackbarMessage(it.getMessage(context))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun openSettings() {
|
||||
scope.launch {
|
||||
val intentSettings = Intent(ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||
intentSettings.data = Uri.fromParts("package", context.packageName, null)
|
||||
context.startActivity(intentSettings)
|
||||
}
|
||||
val intentSettings = Intent(ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||
intentSettings.data = Uri.fromParts("package", context.packageName, null)
|
||||
context.startActivity(intentSettings)
|
||||
}
|
||||
|
||||
fun checkFineLocationGranted() {
|
||||
@@ -308,11 +321,11 @@ fun SettingsScreen(
|
||||
},
|
||||
onError = { _ ->
|
||||
showAuthPrompt = false
|
||||
appViewModel.showSnackbarMessage(Event.Error.AuthenticationFailed.message)
|
||||
appViewModel.showSnackbarMessage(context.getString(R.string.error_authentication_failed))
|
||||
},
|
||||
onFailure = {
|
||||
showAuthPrompt = false
|
||||
appViewModel.showSnackbarMessage(Event.Error.AuthorizationFailed.message)
|
||||
appViewModel.showSnackbarMessage(context.getString(R.string.error_authorization_failed))
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -520,12 +533,12 @@ fun SettingsScreen(
|
||||
when (false) {
|
||||
isBackgroundLocationGranted ->
|
||||
appViewModel.showSnackbarMessage(
|
||||
Event.Error.BackgroundLocationRequired.message,
|
||||
context.getString(R.string.background_location_required),
|
||||
)
|
||||
|
||||
fineLocationState.status.isGranted ->
|
||||
appViewModel.showSnackbarMessage(
|
||||
Event.Error.PreciseLocationRequired.message,
|
||||
context.getString(R.string.precise_location_required),
|
||||
)
|
||||
|
||||
viewModel.isLocationEnabled(context) ->
|
||||
@@ -551,38 +564,49 @@ fun SettingsScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (WgQuickBackend.hasKernelSupport()) {
|
||||
Surface(
|
||||
tonalElevation = 2.dp,
|
||||
shadowElevation = 2.dp,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
.padding(vertical = 10.dp),
|
||||
Surface(
|
||||
tonalElevation = 2.dp,
|
||||
shadowElevation = 2.dp,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
.padding(vertical = 10.dp),
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier = Modifier.padding(15.dp),
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier = Modifier.padding(15.dp),
|
||||
) {
|
||||
SectionTitle(
|
||||
title = stringResource(id = R.string.kernel),
|
||||
padding = screenPadding,
|
||||
)
|
||||
SectionTitle(
|
||||
title = stringResource(id = R.string.backend),
|
||||
padding = screenPadding,
|
||||
)
|
||||
ConfigurationToggle(
|
||||
stringResource(R.string.use_amnezia),
|
||||
enabled =
|
||||
!(uiState.settings.isAutoTunnelEnabled ||
|
||||
uiState.settings.isAlwaysOnVpnEnabled ||
|
||||
(uiState.vpnState.status == TunnelState.UP) || uiState.settings.isKernelEnabled),
|
||||
checked = uiState.settings.isAmneziaEnabled,
|
||||
padding = screenPadding,
|
||||
onCheckChanged = {
|
||||
viewModel.onToggleAmnezia()
|
||||
},
|
||||
)
|
||||
if (kernelSupport) {
|
||||
ConfigurationToggle(
|
||||
stringResource(R.string.use_kernel),
|
||||
enabled =
|
||||
!(uiState.settings.isAutoTunnelEnabled ||
|
||||
uiState.settings.isAlwaysOnVpnEnabled ||
|
||||
(uiState.vpnState.status == Tunnel.State.UP)),
|
||||
(uiState.vpnState.status == TunnelState.UP)),
|
||||
checked = uiState.settings.isKernelEnabled,
|
||||
padding = screenPadding,
|
||||
onCheckChanged = {
|
||||
viewModel.onToggleKernelMode().let {
|
||||
when (it) {
|
||||
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
|
||||
is Result.Success -> {}
|
||||
scope.launch {
|
||||
viewModel.onToggleKernelMode().onFailure {
|
||||
appViewModel.showSnackbarMessage(it.getMessage(context))
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -627,15 +651,24 @@ fun SettingsScreen(
|
||||
)
|
||||
}
|
||||
ConfigurationToggle(
|
||||
stringResource(R.string.enable_app_lock),
|
||||
stringResource(R.string.restart_at_boot),
|
||||
enabled = true,
|
||||
checked = pinExists.value,
|
||||
checked = uiState.settings.isRestoreOnBootEnabled,
|
||||
padding = screenPadding,
|
||||
onCheckChanged = {
|
||||
if (pinExists.value) {
|
||||
PinManager.clearPin()
|
||||
pinExists.value = PinManager.pinExists()
|
||||
viewModel.onToggleRestartAtBoot()
|
||||
},
|
||||
)
|
||||
ConfigurationToggle(
|
||||
stringResource(R.string.enable_app_lock),
|
||||
enabled = true,
|
||||
checked = uiState.isPinLockEnabled,
|
||||
padding = screenPadding,
|
||||
onCheckChanged = {
|
||||
if (uiState.isPinLockEnabled) {
|
||||
viewModel.onPinLockDisabled()
|
||||
} else {
|
||||
viewModel.onPinLockEnabled()
|
||||
navController.navigate(Screen.Lock.route)
|
||||
}
|
||||
},
|
||||
|
||||
+4
-3
@@ -1,7 +1,7 @@
|
||||
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.data.domain.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
|
||||
|
||||
data class SettingsUiState(
|
||||
@@ -9,5 +9,6 @@ data class SettingsUiState(
|
||||
val tunnels: List<TunnelConfig> = emptyList(),
|
||||
val vpnState: VpnState = VpnState(),
|
||||
val isLocationDisclosureShown: Boolean = true,
|
||||
val isBatteryOptimizeDisableShown: Boolean = false
|
||||
val isBatteryOptimizeDisableShown: Boolean = false,
|
||||
val isPinLockEnabled: Boolean = false
|
||||
)
|
||||
|
||||
+96
-28
@@ -1,39 +1,52 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.settings
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.location.LocationManager
|
||||
import androidx.core.location.LocationManagerCompat
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.wireguard.android.backend.WgQuickBackend
|
||||
import com.wireguard.android.util.RootShell
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.Event
|
||||
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||
import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
|
||||
@HiltViewModel
|
||||
class SettingsViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val application: Application,
|
||||
private val appDataRepository: AppDataRepository,
|
||||
private val serviceManager: ServiceManager,
|
||||
private val rootShell: RootShell,
|
||||
private val rootShell: Provider<RootShell>,
|
||||
private val fileUtils: FileUtils,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
vpnService: VpnService
|
||||
) : ViewModel() {
|
||||
|
||||
private val _kernelSupport = MutableStateFlow(false)
|
||||
val kernelSupport = _kernelSupport.asStateFlow()
|
||||
|
||||
val uiState =
|
||||
combine(
|
||||
appDataRepository.settings.getSettingsFlow(),
|
||||
@@ -45,8 +58,9 @@ constructor(
|
||||
settings,
|
||||
tunnels,
|
||||
tunnelState,
|
||||
generalState.locationDisclosureShown,
|
||||
generalState.batteryOptimizationDisableShown,
|
||||
generalState.isLocationDisclosureShown,
|
||||
generalState.isBatteryOptimizationDisableShown,
|
||||
generalState.isPinLockEnabled,
|
||||
)
|
||||
}
|
||||
.stateIn(
|
||||
@@ -60,9 +74,9 @@ constructor(
|
||||
return if (!uiState.value.settings.trustedNetworkSSIDs.contains(trimmed)) {
|
||||
uiState.value.settings.trustedNetworkSSIDs.add(trimmed)
|
||||
saveSettings(uiState.value.settings)
|
||||
Result.Success(Unit)
|
||||
Result.success(Unit)
|
||||
} else {
|
||||
Result.Error(Event.Error.SsidConflict)
|
||||
Result.failure(WgTunnelExceptions.SsidConflict())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,15 +107,19 @@ constructor(
|
||||
)
|
||||
}
|
||||
|
||||
fun onToggleAutoTunnel() =
|
||||
suspend fun onExportTunnels(files: List<File>): Result<Unit> {
|
||||
return fileUtils.saveFilesToZip(files)
|
||||
}
|
||||
|
||||
fun onToggleAutoTunnel(context: Context) =
|
||||
viewModelScope.launch {
|
||||
val isAutoTunnelEnabled = uiState.value.settings.isAutoTunnelEnabled
|
||||
var isAutoTunnelPaused = uiState.value.settings.isAutoTunnelPaused
|
||||
|
||||
if (isAutoTunnelEnabled) {
|
||||
serviceManager.stopWatcherService(application)
|
||||
serviceManager.stopWatcherService(context)
|
||||
} else {
|
||||
serviceManager.startWatcherService(application)
|
||||
serviceManager.startWatcherService(context)
|
||||
isAutoTunnelPaused = false
|
||||
}
|
||||
saveSettings(
|
||||
@@ -110,7 +128,7 @@ constructor(
|
||||
isAutoTunnelPaused = isAutoTunnelPaused,
|
||||
),
|
||||
)
|
||||
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate(application)
|
||||
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate()
|
||||
}
|
||||
|
||||
fun onToggleAlwaysOnVPN() =
|
||||
@@ -162,21 +180,44 @@ constructor(
|
||||
)
|
||||
}
|
||||
|
||||
fun onToggleKernelMode(): Result<Unit> {
|
||||
if (!uiState.value.settings.isKernelEnabled) {
|
||||
try {
|
||||
rootShell.start()
|
||||
Timber.i("Root shell accepted!")
|
||||
saveKernelMode(on = true)
|
||||
} catch (e: RootShell.RootShellException) {
|
||||
Timber.e(e)
|
||||
saveKernelMode(on = false)
|
||||
return Result.Error(Event.Error.RootDenied)
|
||||
}
|
||||
} else {
|
||||
saveKernelMode(on = false)
|
||||
fun onToggleAmnezia() = viewModelScope.launch {
|
||||
if (uiState.value.settings.isKernelEnabled) {
|
||||
saveKernelMode(false)
|
||||
}
|
||||
saveAmneziaMode(!uiState.value.settings.isAmneziaEnabled)
|
||||
}
|
||||
|
||||
private fun saveAmneziaMode(on: Boolean) {
|
||||
saveSettings(
|
||||
uiState.value.settings.copy(
|
||||
isAmneziaEnabled = on,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun onToggleKernelMode(): Result<Unit> {
|
||||
return withContext(ioDispatcher) {
|
||||
if (!uiState.value.settings.isKernelEnabled) {
|
||||
try {
|
||||
rootShell.get().start()
|
||||
Timber.i("Root shell accepted!")
|
||||
saveSettings(
|
||||
uiState.value.settings.copy(
|
||||
isKernelEnabled = true,
|
||||
isAmneziaEnabled = false,
|
||||
),
|
||||
)
|
||||
|
||||
} catch (e: RootShell.RootShellException) {
|
||||
Timber.e(e)
|
||||
saveKernelMode(on = false)
|
||||
return@withContext Result.failure(WgTunnelExceptions.RootDenied())
|
||||
}
|
||||
} else {
|
||||
saveKernelMode(on = false)
|
||||
}
|
||||
Result.success(Unit)
|
||||
}
|
||||
return Result.Success(Unit)
|
||||
}
|
||||
|
||||
fun onToggleRestartOnPing() = viewModelScope.launch {
|
||||
@@ -186,4 +227,31 @@ constructor(
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun checkKernelSupport() = viewModelScope.launch {
|
||||
val kernelSupport = withContext(ioDispatcher) {
|
||||
WgQuickBackend.hasKernelSupport()
|
||||
}
|
||||
_kernelSupport.update {
|
||||
kernelSupport
|
||||
}
|
||||
}
|
||||
|
||||
fun onPinLockDisabled() = viewModelScope.launch {
|
||||
PinManager.clearPin()
|
||||
appDataRepository.appState.setPinLockEnabled(false)
|
||||
}
|
||||
|
||||
fun onPinLockEnabled() = viewModelScope.launch {
|
||||
PinManager.initialize(WireGuardAutoTunnel.instance)
|
||||
appDataRepository.appState.setPinLockEnabled(true)
|
||||
}
|
||||
|
||||
fun onToggleRestartAtBoot() = viewModelScope.launch {
|
||||
saveSettings(
|
||||
uiState.value.settings.copy(
|
||||
isRestoreOnBootEnabled = !uiState.value.settings.isRestoreOnBootEnabled
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+26
-8
@@ -107,7 +107,12 @@ fun SupportScreen(
|
||||
modifier = Modifier.padding(bottom = 20.dp),
|
||||
)
|
||||
TextButton(
|
||||
onClick = { appViewModel.openWebPage(context.resources.getString(R.string.docs_url)) },
|
||||
onClick = {
|
||||
appViewModel.openWebPage(
|
||||
context.resources.getString(R.string.docs_url),
|
||||
context,
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.padding(vertical = 5.dp)
|
||||
.focusRequester(focusRequester),
|
||||
@@ -129,7 +134,7 @@ fun SupportScreen(
|
||||
weight = 1.0f,
|
||||
fill = false,
|
||||
),
|
||||
softWrap = true
|
||||
softWrap = true,
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
@@ -143,7 +148,12 @@ fun SupportScreen(
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
TextButton(
|
||||
onClick = { appViewModel.openWebPage(context.resources.getString(R.string.discord_url)) },
|
||||
onClick = {
|
||||
appViewModel.openWebPage(
|
||||
context.resources.getString(R.string.telegram_url),
|
||||
context,
|
||||
)
|
||||
},
|
||||
modifier = Modifier.padding(vertical = 5.dp),
|
||||
) {
|
||||
Row(
|
||||
@@ -152,7 +162,7 @@ fun SupportScreen(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Row {
|
||||
val icon = ImageVector.vectorResource(R.drawable.discord)
|
||||
val icon = ImageVector.vectorResource(R.drawable.telegram)
|
||||
Icon(
|
||||
icon,
|
||||
icon.name,
|
||||
@@ -175,7 +185,12 @@ fun SupportScreen(
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
TextButton(
|
||||
onClick = { appViewModel.openWebPage(context.resources.getString(R.string.github_url)) },
|
||||
onClick = {
|
||||
appViewModel.openWebPage(
|
||||
context.resources.getString(R.string.github_url),
|
||||
context,
|
||||
)
|
||||
},
|
||||
modifier = Modifier.padding(vertical = 5.dp),
|
||||
) {
|
||||
Row(
|
||||
@@ -207,7 +222,7 @@ fun SupportScreen(
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
TextButton(
|
||||
onClick = { appViewModel.launchEmail() },
|
||||
onClick = { appViewModel.launchEmail(context) },
|
||||
modifier = Modifier.padding(vertical = 5.dp),
|
||||
) {
|
||||
Row(
|
||||
@@ -269,7 +284,10 @@ fun SupportScreen(
|
||||
fontSize = 16.sp,
|
||||
modifier =
|
||||
Modifier.clickable {
|
||||
appViewModel.openWebPage(context.resources.getString(R.string.privacy_policy_url))
|
||||
appViewModel.openWebPage(
|
||||
context.resources.getString(R.string.privacy_policy_url),
|
||||
context,
|
||||
)
|
||||
},
|
||||
)
|
||||
Row(
|
||||
@@ -285,7 +303,7 @@ fun SupportScreen(
|
||||
val mode = buildAnnotatedString {
|
||||
append(stringResource(R.string.mode))
|
||||
append(": ")
|
||||
when(uiState.settings.isKernelEnabled){
|
||||
when (uiState.settings.isKernelEnabled) {
|
||||
true -> append(stringResource(id = R.string.kernel))
|
||||
false -> append(stringResource(id = R.string.userspace))
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.support
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
|
||||
|
||||
data class SupportUiState(val settings: Settings = Settings())
|
||||
|
||||
+24
-8
@@ -1,6 +1,7 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.support.logs
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -8,7 +9,7 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -27,21 +28,24 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.ClipboardManager
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.zaneschepke.logcatter.model.LogMessage
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.text.LogTypeLabel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||
@Composable
|
||||
fun LogsScreen(appViewModel: AppViewModel) {
|
||||
fun LogsScreen(viewModel: LogsViewModel = hiltViewModel()) {
|
||||
|
||||
val logs = remember {
|
||||
appViewModel.logs
|
||||
}
|
||||
val logs = viewModel.logs
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
val lazyColumnListState = rememberLazyListState()
|
||||
val clipboardManager: ClipboardManager = LocalClipboardManager.current
|
||||
@@ -57,7 +61,15 @@ fun LogsScreen(appViewModel: AppViewModel) {
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
appViewModel.saveLogsToFile()
|
||||
scope.launch {
|
||||
viewModel.saveLogsToFile().onSuccess {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.logs_saved),
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
}
|
||||
},
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
@@ -79,7 +91,11 @@ fun LogsScreen(appViewModel: AppViewModel) {
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 24.dp),
|
||||
) {
|
||||
items(logs) {
|
||||
itemsIndexed(
|
||||
logs,
|
||||
key = { index, _ -> index },
|
||||
contentType = { _: Int, _: LogMessage -> null },
|
||||
) { _, it ->
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(5.dp, Alignment.Start),
|
||||
verticalAlignment = Alignment.Top,
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.support.logs
|
||||
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.zaneschepke.logcatter.LocalLogCollector
|
||||
import com.zaneschepke.logcatter.model.LogMessage
|
||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.module.MainDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||
import com.zaneschepke.wireguardautotunnel.util.chunked
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class LogsViewModel
|
||||
@Inject constructor(
|
||||
private val localLogCollector: LocalLogCollector,
|
||||
private val fileUtils: FileUtils,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
@MainDispatcher private val mainDispatcher: CoroutineDispatcher
|
||||
) : ViewModel() {
|
||||
|
||||
val logs = mutableStateListOf<LogMessage>()
|
||||
|
||||
init {
|
||||
viewModelScope.launch(ioDispatcher) {
|
||||
localLogCollector.bufferedLogs.chunked(500, Duration.ofSeconds(1)).collect {
|
||||
withContext(mainDispatcher) {
|
||||
logs.addAll(it)
|
||||
}
|
||||
if (logs.size > Constants.LOG_BUFFER_SIZE) {
|
||||
withContext(mainDispatcher) {
|
||||
logs.removeRange(0, (logs.size - Constants.LOG_BUFFER_SIZE).toInt())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun saveLogsToFile(): Result<Unit> {
|
||||
val file = localLogCollector.getLogFile().getOrElse {
|
||||
return Result.failure(it)
|
||||
}
|
||||
val fileContent = fileUtils.readBytesFromFile(file)
|
||||
val fileName = "${Constants.BASE_LOG_FILE_NAME}-${Instant.now().epochSecond}.txt"
|
||||
return fileUtils.saveByteArrayToDownloads(fileContent, fileName)
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package com.zaneschepke.wireguardautotunnel.util
|
||||
|
||||
object Constants {
|
||||
|
||||
const val BASE_LOG_FILE_NAME = "wgtunnel-logs"
|
||||
const val BASE_LOG_FILE_NAME = "wg_tunnel_logs"
|
||||
const val LOG_BUFFER_SIZE = 3_000L
|
||||
|
||||
const val MANUAL_TUNNEL_CONFIG_ID = "0"
|
||||
@@ -16,16 +16,17 @@ object Constants {
|
||||
const val URI_CONTENT_SCHEME = "content"
|
||||
const val ALLOWED_FILE_TYPES = "*/*"
|
||||
const val TEXT_MIME_TYPE = "text/plain"
|
||||
const val ZIP_FILE_MIME_TYPE = "application/zip"
|
||||
const val GOOGLE_TV_EXPLORER_STUB = "com.google.android.tv.frameworkpackagestubs"
|
||||
const val ANDROID_TV_EXPLORER_STUB = "com.android.tv.frameworkpackagestubs"
|
||||
const val ALWAYS_ON_VPN_ACTION = "android.net.VpnService"
|
||||
const val EMAIL_MIME_TYPE = "message/rfc822"
|
||||
const val EMAIL_MIME_TYPE = "plain/text"
|
||||
const val SYSTEM_EXEMPT_SERVICE_TYPE_ID = 1024
|
||||
|
||||
const val SUBSCRIPTION_TIMEOUT = 5_000L
|
||||
const val FOCUS_REQUEST_DELAY = 500L
|
||||
|
||||
const val BACKUP_PING_HOST = "1.1.1.1"
|
||||
const val DEFAULT_PING_IP = "1.1.1.1"
|
||||
const val PING_TIMEOUT = 5_000L
|
||||
const val VPN_RESTART_DELAY = 1_000L
|
||||
const val PING_INTERVAL = 60_000L
|
||||
@@ -37,4 +38,7 @@ object Constants {
|
||||
|
||||
const val UNREADABLE_SSID = "<unknown ssid>"
|
||||
|
||||
val amneziaProperties = listOf("Jc", "Jmin", "Jmax", "S1", "S2", "H1", "H2", "H3", "H4")
|
||||
const val QR_CODE_NAME_PROPERTY = "# Name ="
|
||||
|
||||
}
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
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 class ConfigParseError(val appendedMessage: String) : Error() {
|
||||
override val message: String =
|
||||
WireGuardAutoTunnel.instance.getString(R.string.config_parse_error) + (
|
||||
if (appendedMessage != "") ": ${appendedMessage.trim()}" else "")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,41 +1,30 @@
|
||||
package com.zaneschepke.wireguardautotunnel.util
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
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.R
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.ObsoleteCoroutinesApi
|
||||
import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
||||
import kotlinx.coroutines.channels.ReceiveChannel
|
||||
import kotlinx.coroutines.channels.produce
|
||||
import kotlinx.coroutines.channels.ticker
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.selects.whileSelect
|
||||
import org.amnezia.awg.config.Config
|
||||
import timber.log.Timber
|
||||
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 String.truncateWithEllipsis(allowedLength: Int): String {
|
||||
return if (this.length > allowedLength + 3) {
|
||||
this.substring(0, allowedLength) + "***"
|
||||
} else this
|
||||
}
|
||||
import java.time.Duration
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
fun BigDecimal.toThreeDecimalPlaceString(): String {
|
||||
val df = DecimalFormat("#.###")
|
||||
@@ -50,15 +39,15 @@ typealias TunnelConfigs = List<TunnelConfig>
|
||||
|
||||
typealias Packages = List<PackageInfo>
|
||||
|
||||
fun Statistics.mapPeerStats(): Map<Key, PeerStats?> {
|
||||
return this.peers().associateWith { key -> (this.peer(key)) }
|
||||
fun TunnelStatistics.mapPeerStats(): Map<org.amnezia.awg.crypto.Key, TunnelStatistics.PeerStats?> {
|
||||
return this.getPeers().associateWith { key -> (this.peerStats(key)) }
|
||||
}
|
||||
|
||||
fun PeerStats.latestHandshakeSeconds(): Long? {
|
||||
fun TunnelStatistics.PeerStats.latestHandshakeSeconds(): Long? {
|
||||
return NumberUtils.getSecondsBetweenTimestampAndNow(this.latestHandshakeEpochMillis)
|
||||
}
|
||||
|
||||
fun PeerStats.handshakeStatus(): HandshakeStatus {
|
||||
fun TunnelStatistics.PeerStats.handshakeStatus(): HandshakeStatus {
|
||||
// TODO add never connected status after duration
|
||||
return this.latestHandshakeSeconds().let {
|
||||
when {
|
||||
@@ -71,3 +60,89 @@ fun PeerStats.handshakeStatus(): HandshakeStatus {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Config.toWgQuickString(): String {
|
||||
val amQuick = toAwgQuickString()
|
||||
val lines = amQuick.lines().toMutableList()
|
||||
val linesIterator = lines.iterator()
|
||||
while (linesIterator.hasNext()) {
|
||||
val next = linesIterator.next()
|
||||
Constants.amneziaProperties.forEach {
|
||||
if (next.startsWith(it, ignoreCase = true)) {
|
||||
linesIterator.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
return lines.joinToString(System.lineSeparator())
|
||||
}
|
||||
|
||||
fun Throwable.getMessage(context: Context): String {
|
||||
return when (this) {
|
||||
is WgTunnelExceptions -> this.getMessage(context)
|
||||
else -> this.message ?: StringValue.StringResource(R.string.unknown_error).asString(context)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Chunks based on a time or size threshold.
|
||||
*
|
||||
* Borrowed from this [Stack Overflow question](https://stackoverflow.com/questions/51022533/kotlin-chunk-sequence-based-on-size-and-time).
|
||||
*/
|
||||
@OptIn(ObsoleteCoroutinesApi::class, ExperimentalCoroutinesApi::class)
|
||||
fun <T> ReceiveChannel<T>.chunked(scope: CoroutineScope, size: Int, time: Duration) =
|
||||
scope.produce<List<T>> {
|
||||
while (true) { // this loop goes over each chunk
|
||||
val chunk = ConcurrentLinkedQueue<T>() // current chunk
|
||||
val ticker = ticker(time.toMillis()) // time-limit for this chunk
|
||||
try {
|
||||
whileSelect {
|
||||
ticker.onReceive {
|
||||
false // done with chunk when timer ticks, takes priority over received elements
|
||||
}
|
||||
this@chunked.onReceive {
|
||||
chunk += it
|
||||
chunk.size < size // continue whileSelect if chunk is not full
|
||||
}
|
||||
}
|
||||
} catch (e: ClosedReceiveChannelException) {
|
||||
Timber.e(e)
|
||||
return@produce
|
||||
} finally {
|
||||
ticker.cancel()
|
||||
if (chunk.isNotEmpty()) {
|
||||
send(chunk.toList())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class, ExperimentalCoroutinesApi::class)
|
||||
fun <T> Flow<T>.chunked(size: Int, time: Duration) = channelFlow {
|
||||
coroutineScope {
|
||||
val channel = asChannel(this@chunked).chunked(this, size, time)
|
||||
try {
|
||||
while (!channel.isClosedForReceive) {
|
||||
send(channel.receive())
|
||||
}
|
||||
} catch (e: ClosedReceiveChannelException) {
|
||||
// Channel was closed by the flow completing, nothing to do
|
||||
Timber.w(e)
|
||||
} catch (e: CancellationException) {
|
||||
channel.cancel(e)
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
channel.cancel(CancellationException("Closing channel due to flow exception", e))
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
fun <T> CoroutineScope.asChannel(flow: Flow<T>): ReceiveChannel<T> = produce {
|
||||
flow.collect { value ->
|
||||
channel.send(value)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -6,19 +6,102 @@ import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import android.provider.MediaStore.MediaColumns
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.io.OutputStream
|
||||
import java.time.Instant
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
object FileUtils {
|
||||
private const val ZIP_FILE_MIME_TYPE = "application/zip"
|
||||
class FileUtils(
|
||||
private val context: Context,
|
||||
private val ioDispatcher: CoroutineDispatcher,
|
||||
) {
|
||||
|
||||
suspend fun readBytesFromFile(file: File): ByteArray {
|
||||
return withContext(ioDispatcher) {
|
||||
FileInputStream(file).use {
|
||||
it.readBytes()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun readTextFromFileName(fileName: String): String {
|
||||
return withContext(ioDispatcher) {
|
||||
context.assets.open(fileName).use { stream ->
|
||||
stream.bufferedReader(Charsets.UTF_8).use {
|
||||
it.readText()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun saveByteArrayToDownloads(content: ByteArray, fileName: String): Result<Unit> {
|
||||
return withContext(ioDispatcher) {
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val contentValues =
|
||||
ContentValues().apply {
|
||||
put(MediaColumns.DISPLAY_NAME, fileName)
|
||||
put(MediaColumns.MIME_TYPE, Constants.TEXT_MIME_TYPE)
|
||||
put(MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
|
||||
}
|
||||
val resolver = context.contentResolver
|
||||
val uri =
|
||||
resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
|
||||
if (uri != null) {
|
||||
resolver.openOutputStream(uri).use { output ->
|
||||
output?.write(content)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val target =
|
||||
File(
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
||||
fileName,
|
||||
)
|
||||
FileOutputStream(target).use { output ->
|
||||
output.write(content)
|
||||
}
|
||||
}
|
||||
Result.success(Unit)
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun saveFilesToZip(files: List<File>): Result<Unit> {
|
||||
return withContext(ioDispatcher) {
|
||||
try {
|
||||
val zipOutputStream =
|
||||
createDownloadsFileOutputStream(
|
||||
"wg-export_${Instant.now().epochSecond}.zip",
|
||||
Constants.ZIP_FILE_MIME_TYPE,
|
||||
)
|
||||
ZipOutputStream(zipOutputStream).use { zos ->
|
||||
files.forEach { file ->
|
||||
val entry = ZipEntry(file.name)
|
||||
zos.putNextEntry(entry)
|
||||
if (file.isFile) {
|
||||
file.inputStream().use { fis -> fis.copyTo(zos) }
|
||||
}
|
||||
}
|
||||
return@withContext Result.success(Unit)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
Result.failure(WgTunnelExceptions.ConfigExportFailed())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//TODO issue with android 9
|
||||
private fun createDownloadsFileOutputStream(
|
||||
context: Context,
|
||||
fileName: String,
|
||||
mimeType: String = Constants.ALLOWED_FILE_TYPES
|
||||
): OutputStream? {
|
||||
@@ -44,47 +127,4 @@ object FileUtils {
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun saveFileToDownloads(context: Context, content: String, fileName: String) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val contentValues = ContentValues().apply {
|
||||
put(MediaColumns.DISPLAY_NAME, fileName)
|
||||
put(MediaColumns.MIME_TYPE, Constants.TEXT_MIME_TYPE)
|
||||
put(MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
|
||||
}
|
||||
val resolver = context.contentResolver
|
||||
val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
|
||||
if (uri != null) {
|
||||
resolver.openOutputStream(uri).use { output ->
|
||||
output?.write(content.toByteArray())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val target = File(
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
||||
fileName,
|
||||
)
|
||||
FileOutputStream(target).use { output ->
|
||||
output.write(content.toByteArray())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
zos.putNextEntry(entry)
|
||||
if (file.isFile) {
|
||||
file.inputStream().use { fis -> fis.copyTo(zos) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package com.zaneschepke.wireguardautotunnel.util
|
||||
|
||||
import android.content.Context
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
|
||||
sealed class WgTunnelExceptions : Exception() {
|
||||
abstract fun getMessage(context: Context): String
|
||||
data class General(private val userMessage: StringValue) : WgTunnelExceptions() {
|
||||
override fun getMessage(context: Context): String {
|
||||
return userMessage.asString(context)
|
||||
}
|
||||
}
|
||||
|
||||
data class SsidConflict(private val userMessage: StringValue = StringValue.StringResource(R.string.error_ssid_exists)) :
|
||||
WgTunnelExceptions() {
|
||||
override fun getMessage(context: Context): String {
|
||||
return userMessage.asString(context)
|
||||
}
|
||||
}
|
||||
|
||||
data class ConfigExportFailed(
|
||||
private val userMessage: StringValue = StringValue.StringResource(
|
||||
R.string.export_configs_failed,
|
||||
)
|
||||
) : WgTunnelExceptions() {
|
||||
override fun getMessage(context: Context): String {
|
||||
return userMessage.asString(context)
|
||||
}
|
||||
}
|
||||
|
||||
data class ConfigParseError(private val appendMessage: StringValue = StringValue.Empty) :
|
||||
WgTunnelExceptions() {
|
||||
override fun getMessage(context: Context): String {
|
||||
return StringValue.StringResource(R.string.config_parse_error).asString(context) + (
|
||||
if (appendMessage != StringValue.Empty) ": ${appendMessage.asString(context)}" else "")
|
||||
}
|
||||
}
|
||||
|
||||
data class RootDenied(private val userMessage: StringValue = StringValue.StringResource(R.string.error_root_denied)) :
|
||||
WgTunnelExceptions() {
|
||||
override fun getMessage(context: Context): String {
|
||||
return userMessage.asString(context)
|
||||
}
|
||||
}
|
||||
|
||||
data class InvalidQrCode(private val userMessage: StringValue = StringValue.StringResource(R.string.error_invalid_code)) :
|
||||
WgTunnelExceptions() {
|
||||
override fun getMessage(context: Context): String {
|
||||
return userMessage.asString(context)
|
||||
}
|
||||
}
|
||||
|
||||
data class InvalidFileExtension(
|
||||
private val userMessage: StringValue = StringValue.StringResource(
|
||||
R.string.error_file_extension,
|
||||
)
|
||||
) : WgTunnelExceptions() {
|
||||
override fun getMessage(context: Context): String {
|
||||
return userMessage.asString(context)
|
||||
}
|
||||
}
|
||||
|
||||
data class FileReadFailed(private val userMessage: StringValue = StringValue.StringResource(R.string.error_file_format)) :
|
||||
WgTunnelExceptions() {
|
||||
override fun getMessage(context: Context): String {
|
||||
return userMessage.asString(context)
|
||||
}
|
||||
}
|
||||
|
||||
data class AuthenticationFailed(
|
||||
private val userMessage: StringValue = StringValue.StringResource(
|
||||
R.string.error_authentication_failed,
|
||||
)
|
||||
) : WgTunnelExceptions() {
|
||||
override fun getMessage(context: Context): String {
|
||||
return userMessage.asString(context)
|
||||
}
|
||||
}
|
||||
|
||||
data class AuthorizationFailed(
|
||||
private val userMessage: StringValue = StringValue.StringResource(
|
||||
R.string.error_authorization_failed,
|
||||
)
|
||||
) : WgTunnelExceptions() {
|
||||
override fun getMessage(context: Context): String {
|
||||
return userMessage.asString(context)
|
||||
}
|
||||
}
|
||||
|
||||
data class BackgroundLocationRequired(
|
||||
private val userMessage: StringValue = StringValue.StringResource(
|
||||
R.string.background_location_required,
|
||||
)
|
||||
) : WgTunnelExceptions() {
|
||||
override fun getMessage(context: Context): String {
|
||||
return userMessage.asString(context)
|
||||
}
|
||||
}
|
||||
|
||||
data class LocationServicesRequired(
|
||||
private val userMessage: StringValue = StringValue.StringResource(
|
||||
R.string.location_services_required,
|
||||
)
|
||||
) : WgTunnelExceptions() {
|
||||
override fun getMessage(context: Context): String {
|
||||
return userMessage.asString(context)
|
||||
}
|
||||
}
|
||||
|
||||
data class PreciseLocationRequired(
|
||||
private val userMessage: StringValue = StringValue.StringResource(
|
||||
R.string.precise_location_required,
|
||||
)
|
||||
) : WgTunnelExceptions() {
|
||||
override fun getMessage(context: Context): String {
|
||||
return userMessage.asString(context)
|
||||
}
|
||||
}
|
||||
|
||||
data class FileExplorerRequired(
|
||||
private val userMessage: StringValue = StringValue.StringResource(
|
||||
R.string.error_no_file_explorer,
|
||||
)
|
||||
) : WgTunnelExceptions() {
|
||||
override fun getMessage(context: Context): String {
|
||||
return userMessage.asString(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 38 KiB |
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="#e8eaed"
|
||||
android:pathData="M440,520L200,520v-80h240v-240h80v240h240v80L520,520v240h-80v-240Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="#e8eaed"
|
||||
android:pathData="m256,760 l-56,-56 224,-224 -224,-224 56,-56 224,224 224,-224 56,56 -224,224 224,224 -56,56 -224,-224 -224,224Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="#e8eaed"
|
||||
android:pathData="M200,760h57l391,-391 -57,-57 -391,391v57ZM120,840v-170l528,-527q12,-11 26.5,-17t30.5,-6q16,0 31,6t26,18l55,56q12,11 17.5,26t5.5,30q0,16 -5.5,30.5T817,313L290,840L120,840ZM760,256 L704,200 760,256ZM619,341 L591,312 648,369 619,341Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="50dp"
|
||||
android:height="50dp"
|
||||
android:viewportWidth="50"
|
||||
android:viewportHeight="50">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M25,2c12.703,0 23,10.297 23,23S37.703,48 25,48S2,37.703 2,25S12.297,2 25,2zM32.934,34.375c0.423,-1.298 2.405,-14.234 2.65,-16.783c0.074,-0.772 -0.17,-1.285 -0.648,-1.514c-0.578,-0.278 -1.434,-0.139 -2.427,0.219c-1.362,0.491 -18.774,7.884 -19.78,8.312c-0.954,0.405 -1.856,0.847 -1.856,1.487c0,0.45 0.267,0.703 1.003,0.966c0.766,0.273 2.695,0.858 3.834,1.172c1.097,0.303 2.346,0.04 3.046,-0.395c0.742,-0.461 9.305,-6.191 9.92,-6.693c0.614,-0.502 1.104,0.141 0.602,0.644c-0.502,0.502 -6.38,6.207 -7.155,6.997c-0.941,0.959 -0.273,1.953 0.358,2.351c0.721,0.454 5.906,3.932 6.687,4.49c0.781,0.558 1.573,0.811 2.298,0.811C32.191,36.439 32.573,35.484 32.934,34.375z" />
|
||||
</vector>
|
||||
@@ -1,5 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_channel_background" />
|
||||
<foreground android:drawable="@mipmap/ic_channel_foreground" />
|
||||
</adaptive-icon>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 8.4 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 26 KiB |
@@ -0,0 +1,154 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="no_tunnels">Žádné tunely ještě nebyly přidány!</string>
|
||||
<string name="watcher_notification_text_paused">Monitorování změn ve stavu sítě: pozastaveno</string>
|
||||
<string name="notification_permission_required">Je vyžadováno oprávnění pro oznámení.</string>
|
||||
<string name="add_trusted_ssid">Přidat název důvěryhodné Wi-Fi</string>
|
||||
<string name="enable_auto_tunnel">Spustit automatické tunelování</string>
|
||||
<string name="tunnel_mobile_data">Tunelování na mobilních datech</string>
|
||||
<string name="one_tunnel_required">Alespoň jeden tunel je vyžadován pro použití této funkce</string>
|
||||
<string name="privacy_policy">Otevřít zásady soukromí</string>
|
||||
<string name="okay">OK</string>
|
||||
<string name="thank_you">Děkujeme za používání WG Tunnel!</string>
|
||||
<string name="add_tunnels_text">Přidat ze souboru nebo zipu</string>
|
||||
<string name="open_file">Otevřít soubor</string>
|
||||
<string name="add_from_qr">Přidat z QR kódu</string>
|
||||
<string name="qr_scan">QR skenování</string>
|
||||
<string name="tunnel_name">Název tunelu</string>
|
||||
<string name="add_tunnel">Přidat tunel</string>
|
||||
<string name="exclude">Vyloučit</string>
|
||||
<string name="include">Zahrnout</string>
|
||||
<string name="save_changes">Uložit</string>
|
||||
<string name="endpoint">Koncový bod</string>
|
||||
<string name="vpn_connection_failed">Připojení selhalo</string>
|
||||
<string name="always_on_vpn_support">Povolit trvalé připojení VPN</string>
|
||||
<string name="location_services_not_detected">Služby polohy nebyly detekovány</string>
|
||||
<string name="hint_search_packages">Hledat balíčky</string>
|
||||
<string name="attempt_connection">Pokus o připojení...</string>
|
||||
<string name="vpn_starting">VPN se spouští</string>
|
||||
<string name="vpn_on">VPN zapnuto</string>
|
||||
<string name="vpn_off">VPN vypnuto</string>
|
||||
<string name="default_vpn_on">Hlavní VPN zapnuto</string>
|
||||
<string name="default_vpn_off">Hlavní VPN vypnuto</string>
|
||||
<string name="create_import">Vytvořit od základu</string>
|
||||
<string name="turn_off_auto">Akce vyžaduje, aby bylo automatické tunelování vypnuté či pozastavené</string>
|
||||
<string name="add_peer">Přidat peer</string>
|
||||
<string name="comma_separated_list">seznam oddělený čárkami</string>
|
||||
<string name="random">(náhodné)</string>
|
||||
<string name="optional">(volitelné)</string>
|
||||
<string name="optional_no_recommend">(volitelné, nedoporučeno)</string>
|
||||
<string name="preshared_key">Předsdílený klíč</string>
|
||||
<string name="seconds">vteřin(y)</string>
|
||||
<string name="cancel">Zrušit</string>
|
||||
<string name="error_authentication_failed">Autentizace selhala</string>
|
||||
<string name="error_authorization_failed">Nepovedlo se autorizovat</string>
|
||||
<string name="enabled_app_shortcuts">Zapnout zkratky</string>
|
||||
<string name="export_configs">Exportovat konfigurace</string>
|
||||
<string name="export_configs_failed">Nepovedlo se exportovat konfigurace</string>
|
||||
<string name="location_services_required">Služby polohy vyžadovány</string>
|
||||
<string name="background_location_required">Oprávnění pro polohu na pozadí vyžadováno</string>
|
||||
<string name="precise_location_required">Oprávnění pro přesnou polohu vyžadováno</string>
|
||||
<string name="unknown_error">Došlo k neznámé chybě</string>
|
||||
<string name="exported_configs_message">Konfigurace exportovány do stažených souborů</string>
|
||||
<string name="tunnel_on_wifi">Tunelovat na nedůvěryhodné Wi-Fi</string>
|
||||
<string name="email_subject">WG Tunnel podpora</string>
|
||||
<string name="email_chooser">Poslat email…</string>
|
||||
<string name="use_kernel">Použít kernel modul</string>
|
||||
<string name="error_ssid_exists">SSID již existuje</string>
|
||||
<string name="error_root_denied">Oprávnění root zamítnuto</string>
|
||||
<string name="error_no_file_explorer">Žádný průzkumník souborů není nainstalován</string>
|
||||
<string name="error_invalid_code">Neplatný QR kód</string>
|
||||
<string name="error_none">Žádná chyba</string>
|
||||
<string name="auto_tunnel_title">Služba automatického tunelování</string>
|
||||
<string name="resume">Obnovit</string>
|
||||
<string name="active">aktivní</string>
|
||||
<string name="open_issue">Otevřít případ</string>
|
||||
<string name="incorrect_pin">PIN je nesprávný</string>
|
||||
<string name="create_pin">Vytvořte PIN</string>
|
||||
<string name="set_primary_tunnel">Nastavit jako hlavní tunel</string>
|
||||
<string name="use_tunnel_on_wifi_name">Použít tunel pro Wi-Fi</string>
|
||||
<string name="edit_tunnel">Upravit tunel</string>
|
||||
<string name="disabled">vypnuto</string>
|
||||
<string name="auto_tun_on">Obnovit automatické tunelování</string>
|
||||
<string name="auto_tun_off">Pozastavit automatické tunelování</string>
|
||||
<string name="version">Verze</string>
|
||||
<string name="mode">Mód</string>
|
||||
<string name="use_amnezia">"Použít Amnezia userspace "</string>
|
||||
<string name="junk_packet_count">Junk packet počet</string>
|
||||
<string name="junk_packet_minimum_size">Junk packet minimální velikost</string>
|
||||
<string name="junk_packet_maximum_size">Junk packet maximální velikost</string>
|
||||
<string name="init_packet_junk_size">Init packet junk velikost</string>
|
||||
<string name="response_packet_junk_size">Response packet junk velikost</string>
|
||||
<string name="unsure_how">pokud si nejste jisti, jak postupovat</string>
|
||||
<string name="see_the">Podívejte se na</string>
|
||||
<string name="getting_started_guide">začátečnickou příručku</string>
|
||||
<string name="error_file_format">Neplatný formát konfigurace</string>
|
||||
<string name="error_file_extension">Soubor není ve formátu .conf nebo .zip</string>
|
||||
<string name="turn_off_tunnel">Akce vyžaduje vypnutí tunelu</string>
|
||||
<string name="watcher_notification_text_active">Monitorování změn ve stavu sítě: aktivní</string>
|
||||
<string name="tunnel_on_ethernet">Tunelovat na ethernetu</string>
|
||||
<string name="tunnel_start_title">VPN připojeno</string>
|
||||
<string name="prominent_background_location_message">Tato funkce vyžaduje oprávnění pro přístup k poloze na pozadí pro zapnutí monitorování Wi-Fi SSID, i když je aplikace zavřená. Pro více detailů, podívejte se prosím na zásady soukromí umístěné v kategorii Podpora.</string>
|
||||
<string name="tunnel_start_text">Připojeno k tunelu</string>
|
||||
<string name="tunnels">Tunely</string>
|
||||
<string name="disable_auto_tunnel">Zastavit automatické tunelování</string>
|
||||
<string name="tunnel_all">Tunelovat všechny aplikace</string>
|
||||
<string name="config_changes_saved">Změny v konfiguraci uloženy.</string>
|
||||
<string name="icon">Ikona</string>
|
||||
<string name="no_thanks">Ne, děkuji</string>
|
||||
<string name="turn_on">Zapnout</string>
|
||||
<string name="map">Mapa</string>
|
||||
<string name="public_key">Veřejný klíč</string>
|
||||
<string name="addresses">Adresy</string>
|
||||
<string name="dns_servers">DNS servery</string>
|
||||
<string name="allowed_ips">Povolené IP adresy</string>
|
||||
<string name="name">Název</string>
|
||||
<string name="restart">Restartovat tunel</string>
|
||||
<string name="scanning_qr">Skenování QR</string>
|
||||
<string name="none">Žádné názvy důvěryhodných Wi-Fi</string>
|
||||
<string name="other">Ostatní</string>
|
||||
<string name="auto_tunneling">Automatické tunelování</string>
|
||||
<string name="turn_on_tunnel">Akce vyžaduje aktivní tunel</string>
|
||||
<string name="interface_">Rozhraní</string>
|
||||
<string name="done">Hotovo</string>
|
||||
<string name="rotate_keys">Rotovat klíče</string>
|
||||
<string name="private_key">Soukromý klíč</string>
|
||||
<string name="copy_public_key">Kopírovat veřejný klíč</string>
|
||||
<string name="base64_key">base64 klíč</string>
|
||||
<string name="docs_description">Přečíst si dokumentaci</string>
|
||||
<string name="discord_description">Přidat se ke komunitě</string>
|
||||
<string name="email_description">Poslat mi email</string>
|
||||
<string name="support_help_text">Pokud máte potíže, nápady pro zlepšení, nebo se chcete jen zapojit, následující prostředky jsou k dispozici:</string>
|
||||
<string name="location_services_missing_message">Aplikace nenašla žádné služby polohy zapnuté na Vašem zařízení. Dle Vašeho zařízení, tohle může způsobit, že funkce nedůvěryhodné Wi-Fi nedokáže přečíst jméno připojené Wi-Fi. Chcete i přesto pokračovat?</string>
|
||||
<string name="delete_tunnel">Smazat tunel</string>
|
||||
<string name="delete_tunnel_message">Jste si jisti, že chcete smazat tento tunel?</string>
|
||||
<string name="yes">Ano</string>
|
||||
<string name="pause">Pozastavit</string>
|
||||
<string name="paused">pozastaveno</string>
|
||||
<string name="tunneling_apps">Tunelování aplikací</string>
|
||||
<string name="all">vše</string>
|
||||
<string name="included">zahrnuto</string>
|
||||
<string name="excluded">vyloučeno</string>
|
||||
<string name="always_on_disabled">Trvalé VPN připojení se pokusilo spustit tunel, ale tato funkce je vypnutá v nastavení.</string>
|
||||
<string name="no_email_detected">Žádná emailová aplikace nebyla nalezena</string>
|
||||
<string name="no_browser_detected">Žádný prohlížeč nebyl nalezen</string>
|
||||
<string name="logs_saved">Logy uloženy do stažených souborů</string>
|
||||
<string name="read_logs">Přečíst si logy</string>
|
||||
<string name="config_parse_error">Nepovedlo se vložit konfiguraci</string>
|
||||
<string name="pin_created">PIN úspěšně vytvořen</string>
|
||||
<string name="enter_pin">Vložte Váš PIN</string>
|
||||
<string name="no_wifi_names_configured">Žádné názvy Wi-Fi nebyly nastaveny pro tento tunel</string>
|
||||
<string name="enable_app_lock">Zapnout zámek aplikace</string>
|
||||
<string name="restart_on_ping">Restartovat při selhání pingu</string>
|
||||
<string name="mobile_data_tunnel">Nastavit jako tunel pro mobilní data</string>
|
||||
<string name="general">Obecné</string>
|
||||
<string name="settings">Nastavení</string>
|
||||
<string name="support">Podpora</string>
|
||||
<string name="app_name">WG Tunnel</string>
|
||||
<string name="mtu">MTU</string>
|
||||
<string name="db_name">wg-tunnel-db</string>
|
||||
<string name="listen_port">Naslouchací port</string>
|
||||
<string name="auto">(automaticky)</string>
|
||||
<string name="kernel">Kernel</string>
|
||||
<string name="backend">Backend</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,171 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">WG Tunnel</string>
|
||||
<string name="error_file_extension">Datei ist keine .conf oder .zip</string>
|
||||
<string name="no_tunnels">Noch keine Tunnel hinzugefügt!</string>
|
||||
<string name="watcher_notification_text_active">Überwachung der Netzwerkänderungen: aktiv</string>
|
||||
<string name="tunnels">Tunnel</string>
|
||||
<string name="enable_auto_tunnel">Auto-Tunneln starten</string>
|
||||
<string name="tunnel_mobile_data">Tunnel für mobile Daten</string>
|
||||
<string name="one_tunnel_required">Mindestens ein Tunnel wird für diese Funktion benötigt</string>
|
||||
<string name="privacy_policy">Datenschutzbestimmungen anzeigen</string>
|
||||
<string name="disable_auto_tunnel">Auto-Tunneln stoppen</string>
|
||||
<string name="okay">Ok</string>
|
||||
<string name="tunnel_on_ethernet">Tunnel für Ethernet</string>
|
||||
<string name="attempt_connection">Verbindungsversuch..</string>
|
||||
<string name="auto_tunneling">Auto-Tunneln</string>
|
||||
<string name="default_vpn_off">Primärer VPN aus</string>
|
||||
<string name="turn_on_tunnel">Für diese Aktion muss ein aktiver Tunnel bestehen</string>
|
||||
<string name="watcher_notification_text_paused">Überwachung der Netzwerkänderungen: pausiert</string>
|
||||
<string name="tunnel_start_title">VPN verbunden</string>
|
||||
<string name="tunnel_start_text">Mit Tunnel verbunden</string>
|
||||
<string name="notification_permission_required">Benachrichtigungsberechtigung benötigt.</string>
|
||||
<string name="add_trusted_ssid">Vertrauenswürdigen WLAN-Namen hinzufügen</string>
|
||||
<string name="prominent_background_location_message">Diese Funktion erfordert die Erlaubnis zur Standortbestimmung im Hintergrund, um die Überwachung der WLAN SSID zu ermöglichen, auch wenn die Anwendung geschlossen ist. Weitere Einzelheiten in den Datenschutzbestimmungen, die auf dem Support-Bildschirm verlinkt sind.</string>
|
||||
<string name="prominent_background_location_title">Vereinbarung der Standortberechtigung im Hintergrund</string>
|
||||
<string name="thank_you">Danke fürs Benutzen von WG Tunnel!</string>
|
||||
<string name="trusted_ssid_empty_description">SSID eingeben</string>
|
||||
<string name="trusted_ssid_value_description">SSID bestätigen</string>
|
||||
<string name="add_tunnels_text">Von Datei oder ZIP hinzufügen</string>
|
||||
<string name="open_file">Datei öffnen</string>
|
||||
<string name="add_from_qr">Über QR-Code hinzufügen</string>
|
||||
<string name="qr_scan">Scanne QR</string>
|
||||
<string name="tunnel_name">Tunnel Name</string>
|
||||
<string name="add_tunnel">Tunnel hinzufügen</string>
|
||||
<string name="exclude">Ausschließen</string>
|
||||
<string name="include">Einschließen</string>
|
||||
<string name="tunnel_all">Alle Apps tunneln</string>
|
||||
<string name="config_changes_saved">Konfigurationsänderungen gespeichert.</string>
|
||||
<string name="save_changes">Speichern</string>
|
||||
<string name="icon">Symbol</string>
|
||||
<string name="no_thanks">Nein danke</string>
|
||||
<string name="turn_on">Einschalten</string>
|
||||
<string name="map">Karte</string>
|
||||
<string name="public_key">Öffentlicher Schlüssel</string>
|
||||
<string name="addresses">Adressen</string>
|
||||
<string name="dns_servers">DNS-Server</string>
|
||||
<string name="mtu">MTU</string>
|
||||
<string name="peer">Peer</string>
|
||||
<string name="allowed_ips">Erlaubte IPs</string>
|
||||
<string name="endpoint">Endpunkt</string>
|
||||
<string name="name">Name</string>
|
||||
<string name="restart">Tunnel neustarten</string>
|
||||
<string name="vpn_connection_failed">Verbindung fehlgeschlagen</string>
|
||||
<string name="always_on_vpn_support">Always-On VPN erlauben</string>
|
||||
<string name="location_services_not_detected">Standortdienste nicht erkannt</string>
|
||||
<string name="hint_search_packages">Pakete suchen</string>
|
||||
<string name="vpn_starting">VPN startet</string>
|
||||
<string name="db_name">wg-tunnel-db</string>
|
||||
<string name="scanning_qr">Scanne nach QR</string>
|
||||
<string name="none">Keine vertrauenswürdigen WLAN Namen</string>
|
||||
<string name="other">Sonstige</string>
|
||||
<string name="vpn_on">VPN an</string>
|
||||
<string name="vpn_off">VPN aus</string>
|
||||
<string name="default_vpn_on">Primärer VPN an</string>
|
||||
<string name="create_import">Von Grund auf neu erstellen</string>
|
||||
<string name="turn_off_auto">Für diese Aktion muss Auto-Tunneln ausgeschaltet oder pausiert sein</string>
|
||||
<string name="add_peer">Peer hinzufügen</string>
|
||||
<string name="done">Erledigt</string>
|
||||
<string name="rotate_keys">Schlüssel rotieren</string>
|
||||
<string name="private_key">Privater Schlüssel</string>
|
||||
<string name="copy_public_key">Öffentlichen Schlüssel kopieren</string>
|
||||
<string name="base64_key">base64-Schlüssel</string>
|
||||
<string name="comma_separated_list">Kommaseparierte Liste</string>
|
||||
<string name="delete_tunnel">Tunnel löschen</string>
|
||||
<string name="persistent_keepalive">Dauerhaftes Keepalive</string>
|
||||
<string name="background_location_required">Hintergrund Standortdienste erforderlich</string>
|
||||
<string name="enable_app_lock">App-Sperre aktivieren</string>
|
||||
<string name="discord_description">Tritt der Community bei</string>
|
||||
<string name="interface_">Schnittstelle</string>
|
||||
<string name="listen_port">Eingehender Port</string>
|
||||
<string name="random">(zufällig)</string>
|
||||
<string name="optional">(optional)</string>
|
||||
<string name="optional_no_recommend">(Optional, nicht empfohlen)</string>
|
||||
<string name="seconds">Sekunden</string>
|
||||
<string name="cancel">Abbrechen</string>
|
||||
<string name="preshared_key">Geteilter Schlüssel</string>
|
||||
<string name="enabled_app_shortcuts">App-Verknüpfungen aktivieren</string>
|
||||
<string name="exported_configs_message">Konfigurationen in Download Ordner exportiert</string>
|
||||
<string name="tunnel_on_wifi">Tunnel bei nicht vertrauenswürdigem WLAN</string>
|
||||
<string name="email_subject">WG Tunnel Unterstützung</string>
|
||||
<string name="docs_description">Dokumentation lesen</string>
|
||||
<string name="email_description">Sende mir eine E-Mail</string>
|
||||
<string name="support_help_text">Bei Fehlern oder Verbesserungsvorschlägen stehen folgende Ressourcen zur Verfügung:</string>
|
||||
<string name="error_root_denied">Root Shell verweigert</string>
|
||||
<string name="error_no_file_explorer">Kein Datei-Explorer installiert</string>
|
||||
<string name="location_services_missing_message">Die App erkennt keine auf deinem Gerät aktivierten Standortdienste. Je nach Gerät kann dies dazu führen, dass die Funktion \"Nicht vertrauenswürdiges WLAN\" den WLAN-Namen nicht lesen kann. Möchtest du trotzdem fortfahren?</string>
|
||||
<string name="auto_tunnel_title">Auto-Tunnel Service</string>
|
||||
<string name="delete_tunnel_message">Bist du sicher, dass du den Tunnel löschen möchtest?</string>
|
||||
<string name="yes">Ja</string>
|
||||
<string name="resume">Fortsetzen</string>
|
||||
<string name="pause">Pausieren</string>
|
||||
<string name="paused">Pausiert</string>
|
||||
<string name="active">Aktiv</string>
|
||||
<string name="go">Los</string>
|
||||
<string name="excluded">ausgeschlossen</string>
|
||||
<string name="all">Alle</string>
|
||||
<string name="always_on_disabled">Always-on VPN wollte einen Tunnel starten, aber dieses Feature ist in den Einstellungen deaktiviert.</string>
|
||||
<string name="no_browser_detected">Keinen Browser erkannt</string>
|
||||
<string name="open_issue">Issue öffnen</string>
|
||||
<string name="read_logs">Logs lesen</string>
|
||||
<string name="auto">(automatisch)</string>
|
||||
<string name="config_parse_error">Fehler beim Lesen der Konfiguration</string>
|
||||
<string name="incorrect_pin">PIN nicht korrekt</string>
|
||||
<string name="pin_created">PIN erfolgreich erstellt</string>
|
||||
<string name="enter_pin">Deine PIN eingeben</string>
|
||||
<string name="auto_off">Auto-Tunneln pausieren</string>
|
||||
<string name="auto_tun_on">Auto-Tunneln fortsetzen</string>
|
||||
<string name="auto_tun_off">Auto-Tunneln pausieren</string>
|
||||
<string name="version">Version</string>
|
||||
<string name="mode">Modus</string>
|
||||
<string name="userspace">Benutzerfläche</string>
|
||||
<string name="settings">Einstellungen</string>
|
||||
<string name="support">Unterstützung</string>
|
||||
<string name="watcher_channel_id">Wächterkanal</string>
|
||||
<string name="error_authentication_failed">Anmeldung fehlgeschlagen</string>
|
||||
<string name="export_configs">Konfigurationen exportieren</string>
|
||||
<string name="unknown_error">Unbekannter Fehler aufgetreten</string>
|
||||
<string name="email_chooser">Sende eine E-Mail…</string>
|
||||
<string name="error_authorization_failed">Autorisierung fehlgeschlagen</string>
|
||||
<string name="location_services_required">Standortdienste erforderlich</string>
|
||||
<string name="precise_location_required">Genauer Standort erforderlich</string>
|
||||
<string name="error_invalid_code">Ungültiger QR Code</string>
|
||||
<string name="error_none">Kein Fehler</string>
|
||||
<string name="tunneling_apps">Getunnelte Apps</string>
|
||||
<string name="included">eingeschlossen</string>
|
||||
<string name="no_email_detected">Keine E-Mail-App erkannt</string>
|
||||
<string name="logs_saved">Logs im Download Ordner gespeichert</string>
|
||||
<string name="create_pin">PIN erstellen</string>
|
||||
<string name="use_tunnel_on_wifi_name">Tunnel für WLAN-Namen verwenden</string>
|
||||
<string name="no_wifi_names_configured">Keine WLAN-Namen für diesen Tunnel konfiguriert</string>
|
||||
<string name="disabled">Deaktiviert</string>
|
||||
<string name="mobile_data_tunnel">Als Tunnel für Mobile Daten setzen</string>
|
||||
<string name="general">Allgemein</string>
|
||||
<string name="restart_on_ping">Neustart bei PING Fehler (Beta)</string>
|
||||
<string name="edit_tunnel">Tunnel bearbeiten</string>
|
||||
<string name="set_primary_tunnel">Als Primären Tunnel setzen</string>
|
||||
<string name="auto_on">Auto-Tunneln fortsetzen</string>
|
||||
<string name="vpn_channel_id">VPN Kanal</string>
|
||||
<string name="vpn_channel_name">VPN Benachrichtigungskanal</string>
|
||||
<string name="watcher_channel_name">Wächterbenachrichtigungskanal</string>
|
||||
<string name="turn_off_tunnel">Aktion erfordert deaktivierten Tunnel</string>
|
||||
<string name="kernel">Kernel</string>
|
||||
<string name="use_kernel">Kernelmodul verwenden</string>
|
||||
<string name="error_ssid_exists">SSID existiert bereits</string>
|
||||
<string name="use_amnezia">"Amnezia Benutzerumgebung benutzen "</string>
|
||||
<string name="junk_packet_count">Junk-Paket Anzahl</string>
|
||||
<string name="junk_packet_maximum_size">Junk-Paket maximale Grösse</string>
|
||||
<string name="init_packet_junk_size">Initial Junk-Paketgröße</string>
|
||||
<string name="backend">Backend</string>
|
||||
<string name="junk_packet_minimum_size">Junk-Paket minimale Grösse</string>
|
||||
<string name="response_packet_junk_size">Antwort Junk-Paketgröße</string>
|
||||
<string name="init_packet_magic_header">Initialpaket magic header</string>
|
||||
<string name="getting_started_guide">Startanleitung erhalten</string>
|
||||
<string name="transport_packet_magic_header">Transportpaket magic header</string>
|
||||
<string name="underload_packet_magic_header">Unterlastpaket magic header</string>
|
||||
<string name="see_the">Schaue das</string>
|
||||
<string name="unsure_how">Wenn du nicht sicher bist, wie du weiterverfahren sollst</string>
|
||||
<string name="export_configs_failed">Konfigurationsexport fehlgeschlagen</string>
|
||||
<string name="error_file_format">Ungültige Konfiguration Tunnel-Format</string>
|
||||
<string name="response_packet_magic_header">Antwortpaket magic header</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,160 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="map">Mapa</string>
|
||||
<string name="allowed_ips">Allowed IPs</string>
|
||||
<string name="email_chooser">Enviar un email…</string>
|
||||
<string name="go">ir</string>
|
||||
<string name="discord_description">Únete a la comunidad</string>
|
||||
<string name="kernel">Kernel</string>
|
||||
<string name="restart">Reiniciar túnel</string>
|
||||
<string name="vpn_connection_failed">Error de conexión</string>
|
||||
<string name="error_none">Sin error</string>
|
||||
<string name="other">Otros</string>
|
||||
<string name="add_peer">Añadir peer</string>
|
||||
<string name="done">Hecho</string>
|
||||
<string name="copy_public_key">Copiar public key</string>
|
||||
<string name="base64_key">clave base64</string>
|
||||
<string name="exported_configs_message">Configuración exportada a Descargas</string>
|
||||
<string name="tunnel_on_wifi">Túnel en Wi-Fi no de confianza</string>
|
||||
<string name="location_services_missing_message">La app no detecta activado el servicio de ubicación en tu dispositivo. Dependiendo del dispositivo, esto podría hacer que la característica de Wi-Fi no de confianza falle al leer el nombre Wi-Fi. ¿Quieres continuar de todas formas?</string>
|
||||
<string name="excluded">excluida(s)</string>
|
||||
<string name="all">todas</string>
|
||||
<string name="mobile_data_tunnel">Establecer como túnel en datos móviles</string>
|
||||
<string name="use_tunnel_on_wifi_name">Usar tunnel en nombre Wi-Fi</string>
|
||||
<string name="disabled">desactivado</string>
|
||||
<string name="version">Versión</string>
|
||||
<string name="userspace">Espacio del usuario</string>
|
||||
<string name="mode">Modo</string>
|
||||
<string name="support">Ayuda</string>
|
||||
<string name="private_key">Clave privada</string>
|
||||
<string name="trusted_ssid_value_description">Enviar SSID</string>
|
||||
<string name="trusted_ssid_empty_description">Introducir SSID</string>
|
||||
<string name="add_tunnels_text">Añadir desde archivo o zip</string>
|
||||
<string name="open_file">Abrir archivo</string>
|
||||
<string name="add_from_qr">Añadir mediante código QR</string>
|
||||
<string name="qr_scan">Escanear QR</string>
|
||||
<string name="tunnel_name">Nombre de túnel</string>
|
||||
<string name="add_tunnel">Añadir túnel</string>
|
||||
<string name="exclude">Excluir</string>
|
||||
<string name="include">Incluir</string>
|
||||
<string name="tunnel_all">Todas las apps por el túnel</string>
|
||||
<string name="config_changes_saved">Cambios de configuración guardados.</string>
|
||||
<string name="save_changes">Guardar</string>
|
||||
<string name="icon">Icono</string>
|
||||
<string name="no_thanks">No gracias</string>
|
||||
<string name="turn_on">Activar</string>
|
||||
<string name="mtu">MTU</string>
|
||||
<string name="dns_servers">DNS servers</string>
|
||||
<string name="addresses">Addresses</string>
|
||||
<string name="public_key">Public key</string>
|
||||
<string name="error_file_extension">No es un archivo .conf o .zip</string>
|
||||
<string name="turn_off_tunnel">Desactiva antes el túnel</string>
|
||||
<string name="no_tunnels">¡Ningún túnel añadido aún!</string>
|
||||
<string name="watcher_notification_text_paused">Monitoreando cambios de red: En pausa</string>
|
||||
<string name="watcher_notification_text_active">Monitoreando cambios de red: Activado</string>
|
||||
<string name="tunnel_start_title">VPN conectada</string>
|
||||
<string name="tunnel_start_text">Conectado al túnel</string>
|
||||
<string name="notification_permission_required">Necesita permiso de notificaciones.</string>
|
||||
<string name="add_trusted_ssid">Añadir nombres Wi-Fi de confianza</string>
|
||||
<string name="tunnels">Túneles</string>
|
||||
<string name="enable_auto_tunnel">Iniciar túnel-automático</string>
|
||||
<string name="disable_auto_tunnel">Parar túnel-automático</string>
|
||||
<string name="tunnel_mobile_data">Activar túnel en datos móviles</string>
|
||||
<string name="one_tunnel_required">Esta característica necesita ser usada en almenos por un túnel</string>
|
||||
<string name="privacy_policy">Ver Política de Privacidad</string>
|
||||
<string name="okay">OK</string>
|
||||
<string name="tunnel_on_ethernet">Túnel en ethernet</string>
|
||||
<string name="prominent_background_location_title">Divulgación de la ubicación en segundo plano</string>
|
||||
<string name="thank_you">¡Gracias por usar WG Tunnel!</string>
|
||||
<string name="endpoint">Endpoint</string>
|
||||
<string name="peer">Peer</string>
|
||||
<string name="name">Nombre</string>
|
||||
<string name="always_on_vpn_support">Permitir VPN siempre-activada</string>
|
||||
<string name="location_services_not_detected">Servicios de Ubicación No Detectados</string>
|
||||
<string name="hint_search_packages">Buscar paquetes</string>
|
||||
<string name="attempt_connection">Intentando conexión...</string>
|
||||
<string name="vpn_starting">Iniciando VPN</string>
|
||||
<string name="db_name">wg-tunnel-db</string>
|
||||
<string name="scanning_qr">Escaneando QR</string>
|
||||
<string name="none">Sin nombres Wi-Fi de confianza</string>
|
||||
<string name="auto_tunneling">Túnel-automático</string>
|
||||
<string name="vpn_on">VPN on</string>
|
||||
<string name="vpn_off">VPN off</string>
|
||||
<string name="default_vpn_on">VPN Principal on</string>
|
||||
<string name="default_vpn_off">VPN Principal off</string>
|
||||
<string name="create_import">Crear desde cero</string>
|
||||
<string name="turn_off_auto">La acción necesita que túnel-automático esté desactivado o en pausa</string>
|
||||
<string name="turn_on_tunnel">La acción necesita un túnel activado</string>
|
||||
<string name="rotate_keys">Rotar claves</string>
|
||||
<string name="comma_separated_list">lista separada por comas</string>
|
||||
<string name="random">(aleatorio)</string>
|
||||
<string name="optional">(opcional)</string>
|
||||
<string name="optional_no_recommend">(opcional, no recomendado)</string>
|
||||
<string name="seconds">segundos</string>
|
||||
<string name="cancel">Cancelar</string>
|
||||
<string name="error_authentication_failed">Fallo de autenticación</string>
|
||||
<string name="error_authorization_failed">Fallo de autorización</string>
|
||||
<string name="enabled_app_shortcuts">Habilitar acesos directos de app</string>
|
||||
<string name="export_configs">Exportar configuración</string>
|
||||
<string name="background_location_required">Se necesita ubicación en segundo plano</string>
|
||||
<string name="location_services_required">Se necesita servicio de ubicación</string>
|
||||
<string name="precise_location_required">Necesita ubicación precisa</string>
|
||||
<string name="unknown_error">Error desconocido</string>
|
||||
<string name="email_subject">Ayuda WG Tunnel</string>
|
||||
<string name="interface_">Interfaz</string>
|
||||
<string name="listen_port">Puerto de escucha</string>
|
||||
<string name="preshared_key">Clave previamente compartida</string>
|
||||
<string name="persistent_keepalive">Keepalive persistente</string>
|
||||
<string name="docs_description">Leer documentación</string>
|
||||
<string name="email_description">Envíame un email</string>
|
||||
<string name="support_help_text">Si tienes problemas, ideas para mejoras, o simlemente comprometerte, tienes disponibles los siguientes recursos:</string>
|
||||
<string name="use_kernel">Usar módulo del Kernel</string>
|
||||
<string name="error_ssid_exists">SSID existente</string>
|
||||
<string name="error_root_denied">Shell root denegado</string>
|
||||
<string name="error_no_file_explorer">Explorador de archivos no instalado</string>
|
||||
<string name="error_invalid_code">Código QR no valido</string>
|
||||
<string name="auto_tunnel_title">Servicio túnel-automático</string>
|
||||
<string name="delete_tunnel">Eliminar túnel</string>
|
||||
<string name="delete_tunnel_message">¿Estás seguro de que quieres eliminar este túnel?</string>
|
||||
<string name="yes">Sí</string>
|
||||
<string name="resume">Reanudar</string>
|
||||
<string name="pause">Pausar</string>
|
||||
<string name="active">activado</string>
|
||||
<string name="paused">en pausa</string>
|
||||
<string name="tunneling_apps">Apps por el túnel</string>
|
||||
<string name="included">incluida(s)</string>
|
||||
<string name="always_on_disabled">VPN siempre-activada ha intentado iniciar un túnel, pero está característica está deshabilitada en los ajustes.</string>
|
||||
<string name="no_email_detected">Ninguna app de email detectada</string>
|
||||
<string name="no_browser_detected">Ningún navegador detectado</string>
|
||||
<string name="logs_saved">Registros guardados en Descargas</string>
|
||||
<string name="open_issue">Abrir una incidencia</string>
|
||||
<string name="read_logs">Leer los registros</string>
|
||||
<string name="auto">(automático)</string>
|
||||
<string name="config_parse_error">Fallo al analizar la configuración</string>
|
||||
<string name="incorrect_pin">El pin no es correcto</string>
|
||||
<string name="pin_created">Pin creado con éxito</string>
|
||||
<string name="enter_pin">Introduce tu pin</string>
|
||||
<string name="create_pin">Crear pin</string>
|
||||
<string name="enable_app_lock">Activar el bloqueo de aplicaciones</string>
|
||||
<string name="restart_on_ping">Reiniciar al fallar ping (beta)</string>
|
||||
<string name="set_primary_tunnel">Establecer como túnel Principal</string>
|
||||
<string name="no_wifi_names_configured">No hay nombres Wi-Fi configurados para este túnel</string>
|
||||
<string name="general">General</string>
|
||||
<string name="edit_tunnel">Editar túnel</string>
|
||||
<string name="auto_on">Reanudar túnel-automático</string>
|
||||
<string name="auto_off">Pausar túnel-automático</string>
|
||||
<string name="auto_tun_on">Reanudar el túnel automático</string>
|
||||
<string name="auto_tun_off">Pausa del túnel automático</string>
|
||||
<string name="settings">Ajustes</string>
|
||||
<string name="app_name">WG Tunnel</string>
|
||||
<string name="vpn_channel_id">Canal VPN</string>
|
||||
<string name="vpn_channel_name">Canal de notificación VPN</string>
|
||||
<string name="watcher_channel_id">Canal del obvervador</string>
|
||||
<string name="watcher_channel_name">Canal de notificación del obvervador</string>
|
||||
<string name="prominent_background_location_message">La monitorización SSID Wi-Fi necesita de permiso de ubicación en segundo plano incluso si la app está cerrada. Mira el enlace a la Política de Privacidad en la pantalla de ayuda para más detalles.</string>
|
||||
<string name="export_configs_failed">Error al exportar la configuración</string>
|
||||
<string name="use_amnezia">"Utilizar el entorno de usuario de Amnezia "</string>
|
||||
<string name="junk_packet_count">Recuento de paquetes basura</string>
|
||||
<string name="backend">Backend</string>
|
||||
<string name="junk_packet_minimum_size">Tamaño mínimo del paquete basura</string>
|
||||
</resources>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user