Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot] 23d75ff0b7 build(deps): bump androidGradlePlugin from 8.8.0-rc01 to 8.9.0-alpha06
Bumps `androidGradlePlugin` from 8.8.0-rc01 to 8.9.0-alpha06.

Updates `com.android.application` from 8.8.0-rc01 to 8.9.0-alpha06

Updates `com.android.library` from 8.8.0-rc01 to 8.9.0-alpha06

---
updated-dependencies:
- dependency-name: com.android.application
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: com.android.library
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-17 13:57:38 +00:00
171 changed files with 2933 additions and 4457 deletions
-125
View File
@@ -1,125 +0,0 @@
name: build
on:
workflow_dispatch:
inputs:
build_type:
type: choice
description: "Build type"
required: true
default: debug
options:
- debug
- prerelease
- nightly
- release
secrets:
SIGNING_KEY_ALIAS:
required: false
SIGNING_KEY_PASSWORD:
required: false
SIGNING_STORE_PASSWORD:
required: false
SERVICE_ACCOUNT_JSON:
required: false
KEYSTORE:
required: false
workflow_call:
inputs:
build_type:
type: string
description: "Build type"
required: true
default: debug
secrets:
SIGNING_KEY_ALIAS:
required: false
SIGNING_KEY_PASSWORD:
required: false
SIGNING_STORE_PASSWORD:
required: false
SERVICE_ACCOUNT_JSON:
required: false
KEYSTORE:
required: false
env:
UPLOAD_DIR_ANDROID: android_artifacts
jobs:
build:
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/
outputs:
UPLOAD_DIR_ANDROID: ${{ env.UPLOAD_DIR_ANDROID }}
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
if: ${{ inputs.build_type != 'debug' }}
run: |
store_path=${{ env.KEY_STORE_LOCATION }}${{ env.KEY_STORE_FILE }}
echo "KEY_STORE_PATH=$store_path" >> $GITHUB_ENV
- name: Create service_account.json
if: ${{ inputs.build_type != 'debug' }}
id: createServiceAccount
run: echo '${{ secrets.ANDROID_SERVICE_ACCOUNT_JSON }}' > service_account.json
- name: Build Fdroid Release APK
if: ${{ inputs.build_type == 'release' }}
run: ./gradlew :app:assembleFdroidRelease --info
- name: Build Fdroid Prerelease APK
if: ${{ inputs.build_type == 'prerelease' }}
run: ./gradlew :app:assembleFdroidPrerelease --info
- name: Build Fdroid Nightly APK
if: ${{ inputs.build_type == 'nightly' }}
run: ./gradlew :app:assembleFdroidNightly --info
- name: Build Debug APK
if: ${{ inputs.build_type == 'debug' }}
run: ./gradlew :app:assembleFdroidDebug --stacktrace
# bump versionCode for nightly and prerelease builds
- name: Commit and push versionCode changes
if: ${{ inputs.build_type == 'nightly' || inputs.build_type == 'prerelease' }}
run: |
git config --global user.name 'GitHub Actions'
git config --global user.email 'actions@github.com'
git add versionCode.txt
git commit -m "Automated build update"
- name: Get release apk path
id: apk-path
run: echo "path=$(find . -regex '^.*/build/outputs/apk/fdroid/${{ inputs.build_type }}/.*\.apk$' -type f | head -1 | tail -c+2)" >> $GITHUB_OUTPUT
- name: Upload release apk
uses: actions/upload-artifact@v4
with:
name: ${{ env.UPLOAD_DIR_ANDROID }}
path: ${{github.workspace}}/${{ steps.apk-path.outputs.path }}
retention-days: 1
@@ -1,11 +1,11 @@
name: on-pr
name: ci-android
on:
workflow_dispatch:
pull_request:
jobs:
format_check:
format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -1,4 +1,4 @@
name: publish
name: release-android
on:
schedule:
@@ -31,8 +31,6 @@ on:
required: false
default: nightly
workflow_call:
env:
UPLOAD_DIR_ANDROID: android_artifacts
jobs:
check_commits:
@@ -55,22 +53,17 @@ jobs:
# This script checks for commits newer than 23 hours ago
NEW_COMMITS=$(git rev-list --count --after="$(date -Iseconds -d '23 hours ago')" ${{ github.sha }})
echo "new_commits=$NEW_COMMITS" >> $GITHUB_OUTPUT
build:
if: ${{ inputs.release_type != 'none' }}
uses: ./.github/workflows/build.yml
secrets: inherit
with:
build_type: ${{ inputs.release_type == '' && 'nightly' || inputs.release_type }}
publish:
needs:
- check_commits
- build
needs: check_commits
if: ${{ needs.check_commits.outputs.has_new_commits > 0 && inputs.release_type != 'none' }}
name: publish-github
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 needed for gh cli
GH_TOKEN: ${{ secrets.GH_TOKEN }}
@@ -78,10 +71,29 @@ jobs:
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: 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
id: decode_keystore
uses: timheuer/base64-to-file@v1.2
with:
fileName: ${{ env.KEY_STORE_FILE }}
fileDir: ${{ env.KEY_STORE_LOCATION }}
encodedString: ${{ secrets.KEYSTORE }}
# update latest tag
- name: Set latest tag
uses: rickstaa/action-create-tag@v1
@@ -108,12 +120,51 @@ jobs:
fromTag: "latest"
writeToFile: false # we won't write to file, just output
# 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
if: ${{ inputs.release_type != '' && inputs.release_type == 'release' }}
run: ./gradlew :app:assembleFdroidRelease -x test
- name: Build Fdroid Prerelease APK
if: ${{ inputs.release_type != '' && inputs.release_type == 'prerelease' }}
run: ./gradlew :app:assembleFdroidPrerelease -x test
- 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 == 'release' }}
run: echo "APK_PATH=$(find . -regex '^.*/build/outputs/apk/fdroid/release/.*\.apk$' -type f | head -1)" >> $GITHUB_ENV
- if: ${{ inputs.release_type != '' && inputs.release_type == 'prerelease' }}
run: echo "APK_PATH=$(find . -regex '^.*/build/outputs/apk/fdroid/prerelease/.*\.apk$' -type f | head -1)" >> $GITHUB_ENV
- name: Get version code
if: ${{ inputs.release_type == 'release' }}
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
- name: Commit and push versionCode changes
if: ${{ inputs.release_type == '' || inputs.release_type == 'nightly' || inputs.release_type == 'prerelease' }}
run: |
git config --global user.name 'GitHub Actions'
git config --global user.email 'actions@github.com'
git add versionCode.txt
git commit -m "Automated build update"
- name: Push changes
if: ${{ inputs.release_type == '' || inputs.release_type == 'nightly' || inputs.release_type == 'prerelease' }}
uses: ad-m/github-push-action@master
@@ -121,14 +172,25 @@ jobs:
github_token: ${{ secrets.GITHUB_TOKEN }}
branch: ${{ github.ref }}
- name: Make download dir
run: mkdir ${{ github.workspace }}/temp
# 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.4.3
with:
name: wgtunnel
path: ${{ env.APK_PATH }}
- name: Download artifacts
- name: Download APK from build
uses: actions/download-artifact@v4
with:
name: ${{ env.UPLOAD_DIR_ANDROID }}
path: ${{ github.workspace }}/temp
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
# Setup TAG_NAME, which is used as a general "name"
- if: github.event_name == 'workflow_dispatch'
@@ -159,9 +221,7 @@ jobs:
- name: Get checksum
id: checksum
run: |
file_path=$(find ${{ github.workspace }}/temp -type f -iname "*.apk" | tail -n1)
echo "checksum=$(apksigner verify -print-certs $file_path | grep -Po "(?<=SHA-256 digest:) .*" | tr -d "[:blank:]")" >> $GITHUB_OUTPUT
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
@@ -183,21 +243,7 @@ jobs:
draft: false
prerelease: ${{ inputs.release_type == 'prerelease' || inputs.release_type == '' || inputs.release_type == 'nightly' }}
make_latest: ${{ inputs.release_type == 'release' }}
files: |
${{ github.workspace }}/temp/*
publish-fdroid:
runs-on: ubuntu-latest
needs:
- build
if: inputs.release_type == 'release'
steps:
- name: Dispatch update for fdroid repo
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.ANDROID_PAT }}
repository: zaneschepke/fdroid
event-type: fdroid-update
files: ${{ github.workspace }}/${{ env.APK_PATH }}
publish-play:
if: ${{ inputs.track != 'none' && inputs.track != '' }}
+53 -68
View File
@@ -4,107 +4,79 @@ WG Tunnel
<div align="center">
An alternative Android client app for [WireGuard®](https://www.wireguard.com/)
and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/)
<br />
<br />
<a href="https://github.com/zaneschepke/wgtunnel/issues/new?assignees=zaneschepke&labels=bug&projects=&template=bug_report.md&title=%5BBUG%5D+-+Problem+with+app">Report a Bug</a>
·
<a href="https://github.com/zaneschepke/wgtunnel/issues/new?assignees=zaneschepke&labels=enhancement&projects=&template=feature_request.md&title=%5BFEATURE%5D+-+New+feature+request">Request a Feature</a>
·
<a href="https://github.com/zaneschepke/wgtunnel/discussions">Ask a Question</a>
</div>
<br/>
<div align="center">
[![Google Play](https://img.shields.io/badge/Google_Play-414141?style=for-the-badge&logo=google-play&logoColor=white)](https://play.google.com/store/apps/details?id=com.zaneschepke.wireguardautotunnel)
[![F-Droid](https://img.shields.io/static/v1?style=for-the-badge&message=F-Droid&color=1976D2&logo=F-Droid&logoColor=FFFFFF&label=)](https://f-droid.org/packages/com.zaneschepke.wireguardautotunnel/)
[![Personal](https://img.shields.io/static/v1?style=for-the-badge&message=Personal&color=1976D2&logo=F-Droid&logoColor=FFFFFF&label=)](https://github.com/zaneschepke/fdroid)
</div>
<div align="center">
[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?style=for-the-badge&logo=discord&logoColor=white)](https://discord.gg/rbRRNh6H7V)
[![Telegram](https://img.shields.io/badge/Telegram-2CA5E0?style=for-the-badge&logo=telegram&logoColor=white)](https://t.me/wgtunnel)
</div>
<details open="open">
<summary>Table of Contents</summary>
<div align="center">
- [About](#about)
- [Acknowledgements](#acknowledgements)
- [Screenshots](#screenshots)
- [Features](#features)
- [Building](#building)
- [Translation](#translation)
- [Contributing](#contributing)
</details>
[![Google Play](https://img.shields.io/badge/Google_Play-414141?style=for-the-badge&logo=google-play&logoColor=white)](https://play.google.com/store/apps/details?id=com.zaneschepke.wireguardautotunnel)
[![F-Droid](https://img.shields.io/static/v1?style=for-the-badge&message=F-Droid&color=1976D2&logo=F-Droid&logoColor=FFFFFF&label=)](https://f-droid.org/packages/com.zaneschepke.wireguardautotunnel/)
<div style="text-align: left;">
## About
Inspired by the official [wireguard-android](https://github.com/WireGuard/wireguard-android) app, WG Tunnel was created to address features and support missing from the official app. This app combines support for both [WireGuard®](https://www.wireguard.com/)
and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/), with its primary feature of auto-tunneling (on-demand tunneling).
</div>
<div style="text-align: left;">
## Acknowledgements
<div align="left">
Thank you to the following:
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.
- All of the users that have helped contribute to the project with ideas, translations, feedback, bug reports, testing, and donations.
- [WireGuard®](https://www.wireguard.com/) - © Jason A. Donenfeld (https://github.com/WireGuard/wireguard-android)
</div>
- [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) - Amnezia Team (https://github.com/amnezia-vpn/amneziawg-android)
<div align="center">
## Screenshots
</div>
<div style="display: flex; flex-wrap: wrap; justify-content: center; gap: 10px;">
<img label="Main" src="fastlane/metadata/android/en-US/images/phoneScreenshots/main_screen.png" width="200" />
<img label="Settings" src="fastlane/metadata/android/en-US/images/phoneScreenshots/settings_screen.png" width="200" />
<img label="Auto" src="fastlane/metadata/android/en-US/images/phoneScreenshots/auto_screen.png" width="200" />
<img label="Config" src="fastlane/metadata/android/en-US/images/phoneScreenshots/config_screen.png" width="200" />
</div>
<p float="center">
<img label="Main" style="padding-right:25px" src="fastlane/metadata/android/en-US/images/phoneScreenshots/main_screen.png" width="200" />
<img label="Config" style="padding-left:25px" src="fastlane/metadata/android/en-US/images/phoneScreenshots/config_screen.png" width="200" />
<img label="Settings" style="padding-left:25px" src="fastlane/metadata/android/en-US/images/phoneScreenshots/settings_screen.png" width="200" />
<img label="Support" style="padding-left:25px" src="fastlane/metadata/android/en-US/images/phoneScreenshots/support_screen.png" width="200" />
</p>
<div style="text-align: left;">
<div align="left">
## Inspiration
The original inspiration for this app came from the inconvenience of having to manually turn VPN off
and on while on different networks. This app was created to offer a free solution to this problem.
## Features
* Add tunnels via .conf file, zip, manual entry, clipboard, or QR code
* Auto-tunnel based on Wi-Fi SSID, ethernet, or mobile data
* Add tunnels via .conf file, zip, manual entry, or QR code
* Auto connect to tunnels based on Wi-Fi SSID, ethernet, or mobile data
* Split tunneling by application with search
* Support for kernel and userspace modes
* WireGuard support for kernel and userspace modes
* Amnezia support for userspace mode for DPI/censorship protection
* Pre/Post Up/Down scripts support for all modes on a rooted device
* Always-On VPN support
* Export tunnels to zip
* Export Amnezia and WireGuard tunnels to zip
* Quick tile support for tunnel toggling, auto-tunneling
* Shortcuts support for tunnel toggling, auto-tunneling
* Static shortcuts support for tunnel toggling, auto-tunneling
* Intent automation support for all tunnels
* In app VPN kill switch with LAN bypass
* Automatic auto-tunneling service and/or tunnel restart after reboot or app update
* Battery preservation measures
* Restart tunnel on ping failure
* Restart tunnel on ping failure (beta)
## Building
## Fdroid
```sh
git clone https://github.com/zaneschepke/wgtunnel
cd wgtunnel
```
Want updates faster?
```sh
./gradlew assembleDebug
```
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
@@ -114,6 +86,19 @@ Help translate WG Tunnel into your language
at [Hosted Weblate](https://hosted.weblate.org/engage/wg-tunnel/).\
[![Translation status](https://hosted.weblate.org/widgets/wg-tunnel/-/multi-auto.svg)](https://hosted.weblate.org/engage/wg-tunnel/)
## Building
```
$ git clone https://github.com/zaneschepke/wgtunnel
$ cd wgtunnel
```
And then build the app:
```
$ ./gradlew assembleDebug
```
## Contributing
Any contributions in the form of feedback, issues, code, or translations are welcome and much
+2 -9
View File
@@ -14,7 +14,7 @@ val versionCodeIncrement = with(getBuildTaskName().lowercase()) {
when {
this.contains(Constants.NIGHTLY) || this.contains(Constants.PRERELEASE) -> {
if (versionFile.exists()) {
versionFile.readText().trim().toInt() + 1
versionFile.readText().toInt() + 1
} else {
1
}
@@ -31,14 +31,6 @@ android {
generateLocaleConfig = true
}
// reproducibility
dependenciesInfo {
// Disables dependency metadata when building APKs.
includeInApk = false
// Disables dependency metadata when building Android App Bundles.
includeInBundle = false
}
defaultConfig {
applicationId = Constants.APP_ID
minSdk = Constants.MIN_SDK
@@ -219,6 +211,7 @@ dependencies {
// shortcuts
implementation(libs.androidx.core)
implementation(libs.androidx.core.google.shortcuts)
// splash
implementation(libs.androidx.core.splashscreen)
@@ -1,274 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 14,
"identityHash": "f2b260c389fb2e53216de40e4b1047f3",
"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_ping_enabled` INTEGER NOT NULL DEFAULT false, `is_amnezia_enabled` INTEGER NOT NULL DEFAULT false, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT false, `is_wifi_by_shell_enabled` INTEGER NOT NULL DEFAULT false, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT false, `is_vpn_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_lan_on_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `debounce_delay_seconds` INTEGER NOT NULL DEFAULT 3)",
"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": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isAmneziaEnabled",
"columnName": "is_amnezia_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isWildcardsEnabled",
"columnName": "is_wildcards_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isWifiNameByShellEnabled",
"columnName": "is_wifi_by_shell_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isStopOnNoInternetEnabled",
"columnName": "is_stop_on_no_internet_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isVpnKillSwitchEnabled",
"columnName": "is_vpn_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isKernelKillSwitchEnabled",
"columnName": "is_kernel_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isLanOnKillSwitchEnabled",
"columnName": "is_lan_on_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "debounceDelaySeconds",
"columnName": "debounce_delay_seconds",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "3"
}
],
"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 '', `is_Active` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `ping_interval` INTEGER DEFAULT null, `ping_cooldown` INTEGER DEFAULT null, `ping_ip` TEXT DEFAULT null, `is_ethernet_tunnel` INTEGER NOT NULL DEFAULT false)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "wgQuick",
"columnName": "wg_quick",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tunnelNetworks",
"columnName": "tunnel_networks",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isMobileDataTunnel",
"columnName": "is_mobile_data_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPrimaryTunnel",
"columnName": "is_primary_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "amQuick",
"columnName": "am_quick",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isActive",
"columnName": "is_Active",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "pingInterval",
"columnName": "ping_interval",
"affinity": "INTEGER",
"notNull": false,
"defaultValue": "null"
},
{
"fieldPath": "pingCooldown",
"columnName": "ping_cooldown",
"affinity": "INTEGER",
"notNull": false,
"defaultValue": "null"
},
{
"fieldPath": "pingIp",
"columnName": "ping_ip",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "null"
},
{
"fieldPath": "isEthernetTunnel",
"columnName": "is_ethernet_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_TunnelConfig_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f2b260c389fb2e53216de40e4b1047f3')"
]
}
}
+4 -2
View File
@@ -68,7 +68,6 @@
<activity
android:name=".ui.MainActivity"
android:exported="true"
android:windowSoftInputMode="adjustResize"
android:theme="@style/Theme.WireguardAutoTunnel"
android:configChanges="orientation|screenSize|keyboardHidden"
>
@@ -79,6 +78,9 @@
<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="com.journeyapps.barcodescanner.CaptureActivity"
@@ -164,7 +166,7 @@
tools:node="merge" />
<service
android:name=".service.foreground.TunnelForegroundService"
android:name=".service.foreground.TunnelBackgroundService"
android:exported="false"
android:persistent="true"
android:foregroundServiceType="systemExempted"
@@ -3,9 +3,6 @@ package com.zaneschepke.wireguardautotunnel
import android.app.Application
import android.os.StrictMode
import android.os.StrictMode.ThreadPolicy
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
@@ -55,7 +52,6 @@ class WireGuardAutoTunnel : Application() {
override fun onCreate() {
super.onCreate()
instance = this
ProcessLifecycleOwner.get().lifecycle.addObserver(AppLifecycleObserver())
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
StrictMode.setThreadPolicy(
@@ -78,9 +74,7 @@ class WireGuardAutoTunnel : Application() {
tunnelService.setBackendState(BackendState.SERVICE_ACTIVE, emptyList())
}
appStateRepository.getLocale()?.let {
withContext(mainDispatcher) {
LocaleUtil.changeLocale(it)
}
LocaleUtil.changeLocale(it)
}
}
}
@@ -92,25 +86,7 @@ class WireGuardAutoTunnel : Application() {
super.onTerminate()
}
class AppLifecycleObserver : DefaultLifecycleObserver {
override fun onStart(owner: LifecycleOwner) {
Timber.d("Application entered foreground")
foreground = true
}
override fun onPause(owner: LifecycleOwner) {
Timber.d("Application entered background")
foreground = false
}
}
companion object {
private var foreground = false
fun isForeground(): Boolean {
return foreground
}
lateinit var instance: WireGuardAutoTunnel
private set
}
@@ -11,7 +11,7 @@ import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
@Database(
entities = [Settings::class, TunnelConfig::class],
version = 14,
version = 13,
autoMigrations =
[
AutoMigration(from = 1, to = 2),
@@ -49,10 +49,6 @@ import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
from = 12,
to = 13,
),
AutoMigration(
from = 13,
to = 14,
),
],
exportSchema = true,
)
@@ -80,9 +80,4 @@ data class Settings(
defaultValue = "false",
)
val isLanOnKillSwitchEnabled: Boolean = false,
@ColumnInfo(
name = "debounce_delay_seconds",
defaultValue = "3",
)
val debounceDelaySeconds: Int = 3,
)
@@ -66,11 +66,7 @@ data class TunnelConfig(
) {
fun toAmConfig(): org.amnezia.awg.config.Config {
return configFromAmQuick(if (amQuick.isNotBlank()) amQuick else wgQuick)
}
fun toWgConfig(): Config {
return configFromWgQuick(wgQuick)
return configFromAmQuick(if (amQuick != "") amQuick else wgQuick)
}
companion object {
@@ -2,11 +2,9 @@ package com.zaneschepke.wireguardautotunnel.module
import android.content.Context
import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.logcatter.LogcatReader
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.notification.WireGuardNotification
import com.zaneschepke.wireguardautotunnel.service.shortcut.DynamicShortcutManager
import com.zaneschepke.wireguardautotunnel.service.shortcut.ShortcutManager
import com.zaneschepke.logcatter.LogcatReader
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -37,10 +35,4 @@ class AppModule {
fun provideNotificationService(@ApplicationContext context: Context): NotificationService {
return WireGuardNotification(context)
}
@Singleton
@Provides
fun provideShortcutManager(@ApplicationContext context: Context, @IoDispatcher ioDispatcher: CoroutineDispatcher): ShortcutManager {
return DynamicShortcutManager(context, ioDispatcher)
}
}
@@ -1,15 +0,0 @@
package com.zaneschepke.wireguardautotunnel.module
import javax.inject.Qualifier
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class Wifi
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class MobileData
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class Ethernet
@@ -7,21 +7,21 @@ import com.zaneschepke.wireguardautotunnel.service.network.WifiService
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.hilt.android.components.ServiceComponent
import dagger.hilt.android.scopes.ServiceScoped
@Module
@InstallIn(SingletonComponent::class)
@InstallIn(ServiceComponent::class)
abstract class ServiceModule {
@Binds
@ServiceScoped
abstract fun provideWifiService(wifiService: WifiService): NetworkService<WifiService>
@Binds
@Wifi
abstract fun provideWifiService(wifiService: WifiService): NetworkService
@ServiceScoped
abstract fun provideMobileDataService(mobileDataService: MobileDataService): NetworkService<MobileDataService>
@Binds
@MobileData
abstract fun provideMobileDataService(mobileDataService: MobileDataService): NetworkService
@Binds
@Ethernet
abstract fun provideEthernetService(ethernetService: EthernetService): NetworkService
@ServiceScoped
abstract fun provideEthernetService(ethernetService: EthernetService): NetworkService<EthernetService>
}
@@ -10,7 +10,6 @@ import com.wireguard.android.util.ToolsInstaller
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.WireGuardTunnel
@@ -79,9 +78,6 @@ class TunnelModule {
@IoDispatcher ioDispatcher: CoroutineDispatcher,
serviceManager: ServiceManager,
notificationService: NotificationService,
@Wifi wifiService: NetworkService,
@MobileData mobileDataService: NetworkService,
@Ethernet ethernetService: NetworkService,
): TunnelService {
return WireGuardTunnel(
amneziaBackend,
@@ -92,9 +88,6 @@ class TunnelModule {
ioDispatcher,
serviceManager,
notificationService,
wifiService,
mobileDataService,
ethernetService,
)
}
@@ -32,8 +32,6 @@ class AppUpdateReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action != Intent.ACTION_MY_PACKAGE_REPLACED) return
serviceManager.updateTunnelTile()
serviceManager.updateAutoTunnelTile()
applicationScope.launch {
val settings = appDataRepository.settings.getSettings()
if (settings.isAutoTunnelEnabled) {
@@ -42,7 +40,7 @@ class AppUpdateReceiver : BroadcastReceiver() {
}
if (!settings.isAutoTunnelEnabled) {
val tunnels = appDataRepository.tunnels.getAll().filter { it.isActive }
if (tunnels.isNotEmpty()) tunnelService.get().startTunnel(tunnels.first())
if (tunnels.isNotEmpty()) tunnelService.get().startTunnel(tunnels.first(), true)
}
}
}
@@ -32,8 +32,6 @@ class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (Intent.ACTION_BOOT_COMPLETED != intent.action) return
serviceManager.updateTunnelTile()
serviceManager.updateAutoTunnelTile()
applicationScope.launch {
with(appDataRepository.settings.getSettings()) {
if (isRestoreOnBootEnabled) {
@@ -41,7 +39,7 @@ class BootReceiver : BroadcastReceiver() {
val tunState = tunnelService.get().vpnState.value.status
if (activeTunnels.isNotEmpty() && tunState != TunnelState.UP) {
Timber.i("Starting previously active tunnel")
tunnelService.get().startTunnel(activeTunnels.first())
tunnelService.get().startTunnel(activeTunnels.first(), true)
}
if (isAutoTunnelEnabled) {
Timber.i("Starting watcher service from boot")
@@ -29,7 +29,7 @@ class ServiceManager
val autoTunnelActive = _autoTunnelActive.asStateFlow()
var autoTunnelService = CompletableDeferred<AutoTunnelService>()
var backgroundService = CompletableDeferred<TunnelForegroundService>()
var backgroundService = CompletableDeferred<TunnelBackgroundService>()
var autoTunnelTile = CompletableDeferred<AutoTunnelControlTile>()
var tunnelControlTile = CompletableDeferred<TunnelControlTile>()
@@ -59,10 +59,10 @@ class ServiceManager
}
}
suspend fun startBackgroundService(tunnelConfig: TunnelConfig) {
suspend fun startBackgroundService(tunnelConfig: TunnelConfig?) {
if (backgroundService.isCompleted) return
kotlin.runCatching {
startService(TunnelForegroundService::class.java, true)
startService(TunnelBackgroundService::class.java, true)
backgroundService.await()
backgroundService.getCompleted().start(tunnelConfig)
}.onFailure {
@@ -86,7 +86,7 @@ class ServiceManager
}
}
fun updateAutoTunnelTile() {
private fun updateAutoTunnelTile() {
if (autoTunnelTile.isCompleted) {
autoTunnelTile.getCompleted().updateTileState()
} else {
@@ -16,7 +16,7 @@ import kotlinx.coroutines.CompletableDeferred
import javax.inject.Inject
@AndroidEntryPoint
class TunnelForegroundService : LifecycleService() {
class TunnelBackgroundService : LifecycleService() {
@Inject
lateinit var notificationService: NotificationService
@@ -39,9 +39,9 @@ class TunnelForegroundService : LifecycleService() {
return super.onStartCommand(intent, flags, startId)
}
fun start(tunnelConfig: TunnelConfig) {
fun start(tunnelConfig: TunnelConfig?) {
ServiceCompat.startForeground(
this@TunnelForegroundService,
this,
NotificationService.KERNEL_SERVICE_NOTIFICATION_ID,
createNotification(tunnelConfig),
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
@@ -1,6 +1,7 @@
package com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel
import android.content.Intent
import android.net.NetworkCapabilities
import android.os.IBinder
import android.os.PowerManager
import androidx.core.app.ServiceCompat
@@ -9,18 +10,19 @@ import androidx.lifecycle.lifecycleScope
import com.wireguard.android.util.RootShell
import com.zaneschepke.wireguardautotunnel.R
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.AppShell
import com.zaneschepke.wireguardautotunnel.module.Ethernet
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.module.MainImmediateDispatcher
import com.zaneschepke.wireguardautotunnel.module.MobileData
import com.zaneschepke.wireguardautotunnel.module.Wifi
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model.AutoTunnelEvent
import com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model.AutoTunnelState
import com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model.NetworkState
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.NotificationAction
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
@@ -28,20 +30,26 @@ import com.zaneschepke.wireguardautotunnel.service.notification.WireGuardNotific
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
import com.zaneschepke.wireguardautotunnel.util.extensions.cancelWithMessage
import com.zaneschepke.wireguardautotunnel.util.extensions.getCurrentWifiName
import com.zaneschepke.wireguardautotunnel.util.extensions.isReachable
import com.zaneschepke.wireguardautotunnel.util.extensions.onNotRunning
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.net.InetAddress
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
import javax.inject.Provider
@@ -53,16 +61,13 @@ class AutoTunnelService : LifecycleService() {
lateinit var rootShell: Provider<RootShell>
@Inject
@Wifi
lateinit var wifiService: NetworkService
lateinit var wifiService: NetworkService<WifiService>
@Inject
@MobileData
lateinit var mobileDataService: NetworkService
lateinit var mobileDataService: NetworkService<MobileDataService>
@Inject
@Ethernet
lateinit var ethernetService: NetworkService
lateinit var ethernetService: NetworkService<EthernetService>
@Inject
lateinit var appDataRepository: Provider<AppDataRepository>
@@ -84,12 +89,14 @@ class AutoTunnelService : LifecycleService() {
@MainImmediateDispatcher
lateinit var mainImmediateDispatcher: CoroutineDispatcher
private val defaultState = AutoTunnelState()
private val autoTunnelStateFlow = MutableStateFlow(defaultState)
private val autoTunnelStateFlow = MutableStateFlow(AutoTunnelState())
private var wakeLock: PowerManager.WakeLock? = null
private val pingTunnelRestartActive = AtomicBoolean(false)
private var pingJob: Job? = null
override fun onCreate() {
super.onCreate()
serviceManager.autoTunnelService.complete(this)
@@ -121,6 +128,7 @@ class AutoTunnelService : LifecycleService() {
}
startAutoTunnelJob()
startAutoTunnelStateJob()
startPingStateJob()
}.onFailure {
Timber.e(it)
}
@@ -132,6 +140,7 @@ class AutoTunnelService : LifecycleService() {
}
override fun onDestroy() {
cancelAndResetPingJob()
serviceManager.autoTunnelService = CompletableDeferred()
super.onDestroy()
}
@@ -168,71 +177,124 @@ class AutoTunnelService : LifecycleService() {
}
}
private fun startPingJob() = lifecycleScope.launch {
watchForPingFailure()
}
private fun startPingStateJob() = lifecycleScope.launch {
autoTunnelStateFlow.collect {
if (it.isPingEnabled()) {
pingJob.onNotRunning { pingJob = startPingJob() }
} else {
if (!pingTunnelRestartActive.get()) cancelAndResetPingJob()
}
}
}
private suspend fun watchForPingFailure() {
withContext(ioDispatcher) {
Timber.i("Starting ping watcher")
runCatching {
do {
val vpnState = autoTunnelStateFlow.value.vpnState
if (vpnState.status.isUp() && !autoTunnelStateFlow.value.isNoConnectivity()) {
if (vpnState.tunnelConfig != null) {
val config = TunnelConfig.configFromWgQuick(vpnState.tunnelConfig.wgQuick)
val results = if (vpnState.tunnelConfig.pingIp != null) {
Timber.d("Pinging custom ip : ${vpnState.tunnelConfig.pingIp}")
listOf(InetAddress.getByName(vpnState.tunnelConfig.pingIp).isReachable(Constants.PING_TIMEOUT.toInt()))
} else {
Timber.d("Pinging all peers")
config.peers.map { peer ->
peer.isReachable()
}
}
Timber.i("Ping results reachable: $results")
if (results.contains(false)) {
Timber.i("Restarting VPN for ping failure")
val cooldown = vpnState.tunnelConfig.pingCooldown
pingTunnelRestartActive.set(true)
tunnelService.get().bounceTunnel()
pingTunnelRestartActive.set(false)
delay(cooldown ?: Constants.PING_COOLDOWN)
continue
}
}
}
delay(vpnState.tunnelConfig?.pingInterval ?: Constants.PING_INTERVAL)
} while (true)
}.onFailure {
Timber.e(it)
}
}
}
private fun startAutoTunnelStateJob() = lifecycleScope.launch(ioDispatcher) {
combine(
combineSettings(),
combineNetworkEventsJob(),
) { double, networkState ->
var wifiName: String? = null
if (networkState.wifiName == Constants.UNREADABLE_SSID && double.first.isTunnelOnWifiEnabled) {
wifiName = getWifiName(double.first)
}
val netState = wifiName?.let { networkState.copy(wifiName = it) } ?: networkState
AutoTunnelState(tunnelService.get().vpnState.value, netState, double.first, double.second)
AutoTunnelState(tunnelService.get().vpnState.value, networkState, double.first, double.second)
}.collect { state ->
Timber.d("Network state: ${state.networkState}")
autoTunnelStateFlow.update {
it.copy(vpnState = state.vpnState, networkState = state.networkState, settings = state.settings, tunnels = state.tunnels)
it.copy(state.vpnState, state.networkState, state.settings, state.tunnels)
}
}
}
private fun getWifiName(setting: Settings): String? {
return if (setting.isWifiNameByShellEnabled) {
rootShell.get().getCurrentWifiName()
} else if (wifiService.capabilities != null) {
WifiService.getNetworkName(wifiService.capabilities!!, this@AutoTunnelService)
} else {
null
}
private fun cancelAndResetPingJob() {
pingJob?.cancelWithMessage("Ping job canceled")
pingJob = null
}
@OptIn(FlowPreview::class)
private fun combineNetworkEventsJob(): Flow<NetworkState> {
return combine(
wifiService.status,
mobileDataService.status,
ethernetService.status,
wifiService.networkStatus,
mobileDataService.networkStatus,
ethernetService.networkStatus,
) { wifi, mobileData, ethernet ->
NetworkState(
wifi.available,
mobileData.available,
ethernet.available,
wifi.name,
wifi.isConnected,
mobileData.isConnected,
ethernet.isConnected,
when (wifi) {
is NetworkStatus.CapabilitiesChanged -> getWifiSSID(wifi.networkCapabilities)
is NetworkStatus.Available -> autoTunnelStateFlow.value.networkState.wifiName
is NetworkStatus.Unavailable -> null
},
)
}.distinctUntilChanged()
}.distinctUntilChanged().filterNot { it.isWifiConnected && it.wifiName == null }
}
private fun combineSettings(): Flow<Pair<Settings, TunnelConfigs>> {
return combine(
appDataRepository.get().settings.getSettingsFlow(),
appDataRepository.get().tunnels.getTunnelConfigsFlow().map { tunnels ->
// isActive is ignored for equality checks so user can manually toggle off tunnel with auto-tunnel
tunnels.map { it.copy(isActive = false) }
appDataRepository.get().tunnels.getTunnelConfigsFlow().distinctUntilChanged { old, new ->
old.map { it.isActive } != new.map { it.isActive }
},
) { settings, tunnels ->
Pair(settings, tunnels)
}.distinctUntilChanged()
}
@OptIn(FlowPreview::class)
private suspend fun getWifiSSID(networkCapabilities: NetworkCapabilities): String? {
return withContext(ioDispatcher) {
with(autoTunnelStateFlow.value.settings) {
if (isWifiNameByShellEnabled) return@withContext rootShell.get().getCurrentWifiName()
wifiService.getNetworkName(networkCapabilities)
}.also {
if (it?.contains(Constants.UNREADABLE_SSID) == true) {
Timber.w("SSID unreadable: missing permissions")
} else {
Timber.i("Detected valid SSID")
}
}
}
}
private fun startAutoTunnelJob() = lifecycleScope.launch(ioDispatcher) {
Timber.i("Starting auto-tunnel network event watcher")
val settings = appDataRepository.get().settings.getSettings()
val debounce = settings.debounceDelaySeconds * 1000L
Timber.d("Starting with debounce delay of: $debounce")
autoTunnelStateFlow.debounce(debounce).collect { watcherState ->
if (watcherState == defaultState) return@collect
autoTunnelStateFlow.collect { watcherState ->
Timber.d("New auto tunnel state emitted")
when (val event = watcherState.asAutoTunnelEvent()) {
is AutoTunnelEvent.Start -> tunnelService.get().startTunnel(
@@ -5,6 +5,7 @@ import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
import com.zaneschepke.wireguardautotunnel.util.extensions.isMatchingToWildcardList
import timber.log.Timber
data class AutoTunnelState(
val vpnState: VpnState = VpnState(),
@@ -81,6 +82,7 @@ data class AutoTunnelState(
}
private fun startOnUntrustedWifi(): Boolean {
Timber.d("Is tunnel on wifi enabled ${settings.isTunnelOnWifiEnabled}")
return isWifiActive() && settings.isTunnelOnWifiEnabled && vpnState.status.isDown() && !isCurrentSSIDTrusted()
}
@@ -5,8 +5,4 @@ data class NetworkState(
val isMobileDataConnected: Boolean = false,
val isEthernetConnected: Boolean = false,
val wifiName: String? = null,
) {
fun hasNoCapabilities(): Boolean {
return !isWifiConnected && !isMobileDataConnected && !isEthernetConnected
}
}
)
@@ -0,0 +1,116 @@
package com.zaneschepke.wireguardautotunnel.service.network
import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.net.wifi.WifiManager
import android.os.Build
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.map
import timber.log.Timber
abstract class BaseNetworkService<T : BaseNetworkService<T>>(
val context: Context,
networkCapability: Int,
) : NetworkService<T> {
private val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val wifiManager =
context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
fun checkHasCapability(networkCapability: Int): Boolean {
val network = connectivityManager.activeNetwork
val networkCapabilities = connectivityManager.getNetworkCapabilities(network)
return networkCapabilities?.hasTransport(networkCapability) == true
}
override val networkStatus =
callbackFlow {
if (!checkHasCapability(networkCapability)) {
trySend(NetworkStatus.Unavailable())
}
val networkStatusCallback =
when (Build.VERSION.SDK_INT) {
in Build.VERSION_CODES.S..Int.MAX_VALUE -> {
object :
ConnectivityManager.NetworkCallback(
FLAG_INCLUDE_LOCATION_INFO,
) {
override fun onAvailable(network: Network) {
trySend(NetworkStatus.Available(network))
}
override fun onLost(network: Network) {
trySend(NetworkStatus.Unavailable())
}
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
trySend(
NetworkStatus.CapabilitiesChanged(
network,
networkCapabilities,
),
)
}
}
}
else -> {
object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
trySend(NetworkStatus.Available(network))
}
override fun onLost(network: Network) {
trySend(NetworkStatus.Unavailable())
}
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
trySend(
NetworkStatus.CapabilitiesChanged(
network,
networkCapabilities,
),
)
}
}
}
}
val request =
NetworkRequest.Builder()
.addTransportType(networkCapability)
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
.build()
connectivityManager.registerNetworkCallback(request, networkStatusCallback)
awaitClose { connectivityManager.unregisterNetworkCallback(networkStatusCallback) }
}.catch {
Timber.e(it)
// conflate for backpressure
}.conflate()
}
inline fun <Result> Flow<NetworkStatus>.map(
crossinline onUnavailable: suspend () -> Result,
crossinline onAvailable: suspend (network: Network) -> Result,
crossinline onCapabilitiesChanged:
suspend (network: Network, networkCapabilities: NetworkCapabilities) -> Result,
): Flow<Result> = map { status ->
when (status) {
is NetworkStatus.Unavailable -> onUnavailable()
is NetworkStatus.Available -> onAvailable(status.network)
is NetworkStatus.CapabilitiesChanged ->
onCapabilitiesChanged(
status.network,
status.networkCapabilities,
)
}
}
@@ -1,67 +1,18 @@
package com.zaneschepke.wireguardautotunnel.service.network
import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import timber.log.Timber
import javax.inject.Inject
class EthernetService
@Inject
constructor(
@ApplicationContext context: Context,
) : NetworkService {
) :
BaseNetworkService<EthernetService>(context, NetworkCapabilities.TRANSPORT_ETHERNET) {
override var capabilities: NetworkCapabilities? = null
private val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
override val status = callbackFlow {
val networkStatusCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
trySend(NetworkStatus.Available(network))
}
override fun onLost(network: Network) {
trySend(NetworkStatus.Unavailable())
}
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
capabilities = networkCapabilities
trySend(
NetworkStatus.CapabilitiesChanged(
network,
networkCapabilities,
),
)
}
}
val request =
NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
.build()
connectivityManager.registerNetworkCallback(request, networkStatusCallback)
awaitClose { connectivityManager.unregisterNetworkCallback(networkStatusCallback) }
}.onStart {
// needed for services that are not yet available as it will impact later combine flows if we don't emit
emit(NetworkStatus.Unavailable())
}.catch {
Timber.e(it)
emit(NetworkStatus.Unavailable())
}.map {
when (it) {
is NetworkStatus.Available, is NetworkStatus.CapabilitiesChanged -> Status(true, null)
is NetworkStatus.Unavailable -> Status(false, null)
}
override fun isNetworkSecure(): Boolean {
return true
}
}
@@ -1,67 +1,17 @@
package com.zaneschepke.wireguardautotunnel.service.network
import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import timber.log.Timber
import javax.inject.Inject
class MobileDataService
@Inject
constructor(
@ApplicationContext context: Context,
) : NetworkService {
override var capabilities: NetworkCapabilities? = null
private val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
override val status = callbackFlow {
val networkStatusCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
trySend(NetworkStatus.Available(network))
}
override fun onLost(network: Network) {
trySend(NetworkStatus.Unavailable())
}
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
capabilities = networkCapabilities
trySend(
NetworkStatus.CapabilitiesChanged(
network,
networkCapabilities,
),
)
}
}
val request =
NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
.build()
connectivityManager.registerNetworkCallback(request, networkStatusCallback)
awaitClose { connectivityManager.unregisterNetworkCallback(networkStatusCallback) }
}.onStart {
// needed for services that are not yet available as it will impact later combine flows if we don't emit
emit(NetworkStatus.Unavailable())
}.catch {
Timber.e(it)
emit(NetworkStatus.Unavailable())
}.map {
when (it) {
is NetworkStatus.Available, is NetworkStatus.CapabilitiesChanged -> Status(true, null)
is NetworkStatus.Unavailable -> Status(false, null)
}
) :
BaseNetworkService<MobileDataService>(context, NetworkCapabilities.TRANSPORT_CELLULAR) {
override fun isNetworkSecure(): Boolean {
return false
}
}
@@ -1,28 +1,14 @@
package com.zaneschepke.wireguardautotunnel.service.network
import android.net.Network
import android.net.NetworkCapabilities
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
interface NetworkService {
val status: Flow<Status>
var capabilities: NetworkCapabilities?
}
inline fun <Result> Flow<NetworkStatus>.map(
crossinline onUnavailable: suspend () -> Result,
crossinline onAvailable: suspend (network: Network) -> Result,
crossinline onCapabilitiesChanged:
suspend (network: Network, networkCapabilities: NetworkCapabilities) -> Result,
): Flow<Result> = map { status ->
when (status) {
is NetworkStatus.Unavailable -> onUnavailable()
is NetworkStatus.Available -> onAvailable(status.network)
is NetworkStatus.CapabilitiesChanged ->
onCapabilitiesChanged(
status.network,
status.networkCapabilities,
)
interface NetworkService<T> {
fun getNetworkName(networkCapabilities: NetworkCapabilities): String? {
return null
}
fun isNetworkSecure(): Boolean
val networkStatus: Flow<NetworkStatus>
}
@@ -1,6 +0,0 @@
package com.zaneschepke.wireguardautotunnel.service.network
data class Status(
val available: Boolean,
val name: String?,
)
@@ -1,148 +1,32 @@
package com.zaneschepke.wireguardautotunnel.service.network
import android.content.Context
import android.net.ConnectivityManager
import android.net.ConnectivityManager.NetworkCallback.FLAG_INCLUDE_LOCATION_INFO
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.net.wifi.SupplicantState
import android.net.wifi.WifiManager
import android.os.Build
import com.wireguard.android.util.RootShell
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.module.AppShell
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.getCurrentWifiName
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.transform
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Provider
class WifiService
@Inject
constructor(
@ApplicationContext private val context: Context,
private val settingsRepository: SettingsRepository,
@AppShell private val rootShell: Provider<RootShell>,
) : NetworkService {
@ApplicationContext context: Context,
) :
BaseNetworkService<WifiService>(context, NetworkCapabilities.TRANSPORT_WIFI) {
override var capabilities: NetworkCapabilities? = null
val mutex = Mutex()
private var ssid: String? = null
private var available: Boolean = false
private val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
override val status = callbackFlow {
val networkStatusCallback =
when (Build.VERSION.SDK_INT) {
in Build.VERSION_CODES.S..Int.MAX_VALUE -> {
object :
ConnectivityManager.NetworkCallback(
FLAG_INCLUDE_LOCATION_INFO,
) {
override fun onAvailable(network: Network) {
trySend(NetworkStatus.Available(network))
}
override fun onLost(network: Network) {
trySend(NetworkStatus.Unavailable())
}
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
trySend(
NetworkStatus.CapabilitiesChanged(
network,
networkCapabilities,
),
)
}
}
}
else -> {
object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
trySend(NetworkStatus.Available(network))
}
override fun onLost(network: Network) {
trySend(NetworkStatus.Unavailable())
}
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
capabilities = networkCapabilities
trySend(
NetworkStatus.CapabilitiesChanged(
network,
networkCapabilities,
),
)
}
}
}
override fun getNetworkName(networkCapabilities: NetworkCapabilities): String? {
var ssid = networkCapabilities.getWifiName()
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S) {
val info = wifiManager.connectionInfo
if (info.supplicantState === SupplicantState.COMPLETED) {
ssid = info.ssid
}
val request =
NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
.build()
connectivityManager.registerNetworkCallback(request, networkStatusCallback)
awaitClose { connectivityManager.unregisterNetworkCallback(networkStatusCallback) }
}.onStart {
// needed for services that are not yet available as it will impact later combine flows if we don't emit
emit(NetworkStatus.Unavailable())
}.catch {
Timber.e(it)
emit(NetworkStatus.Unavailable())
}.transform {
when (it) {
is NetworkStatus.Available -> mutex.withLock {
available = true
}
is NetworkStatus.CapabilitiesChanged -> mutex.withLock {
if (available || ssid == null || ssid == Constants.UNREADABLE_SSID) {
available = false
Timber.d("Getting SSID from capabilities")
ssid = if (settingsRepository.getSettings().isWifiNameByShellEnabled) {
rootShell.get().getCurrentWifiName()
} else {
getNetworkName(it.networkCapabilities, context)
}
}
emit(Status(true, ssid))
}
is NetworkStatus.Unavailable -> emit(Status(false, null))
}
return ssid?.trim('"')
}
companion object {
fun getNetworkName(networkCapabilities: NetworkCapabilities, context: Context): String? {
var ssid = networkCapabilities.getWifiName()
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S) {
val wifiManager =
context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
@Suppress("DEPRECATION")
val info = wifiManager.connectionInfo
if (info.supplicantState === SupplicantState.COMPLETED) {
ssid = info.ssid
}
}
return ssid?.trim('"')
}
override fun isNetworkSecure(): Boolean {
// TODO
return false
}
}
@@ -19,7 +19,7 @@ interface NotificationService {
onlyAlertOnce: Boolean = true,
): Notification
fun createNotificationAction(notificationAction: NotificationAction): NotificationCompat.Action
fun createNotificationAction(action: NotificationAction): NotificationCompat.Action
fun remove(notificationId: Int)
@@ -14,7 +14,6 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver
import com.zaneschepke.wireguardautotunnel.ui.MainActivity
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
@@ -47,14 +46,6 @@ constructor(
addAction(it)
}
setContentTitle(title)
setContentIntent(
PendingIntent.getActivity(
context,
0,
Intent(context, MainActivity::class.java),
PendingIntent.FLAG_IMMUTABLE,
),
)
setContentText(description)
setOnlyAlertOnce(onlyAlertOnce)
setOngoing(onGoing)
@@ -93,7 +84,7 @@ constructor(
}
}
private fun NotificationChannels.asBuilder(): NotificationCompat.Builder {
fun NotificationChannels.asBuilder(): NotificationCompat.Builder {
return when (this) {
NotificationChannels.AUTO_TUNNEL -> {
NotificationCompat.Builder(
@@ -110,7 +101,7 @@ constructor(
}
}
private fun NotificationChannels.asChannel(): NotificationChannel {
fun NotificationChannels.asChannel(): NotificationChannel {
return when (this) {
NotificationChannels.VPN -> {
NotificationChannel(
@@ -1,79 +0,0 @@
package com.zaneschepke.wireguardautotunnel.service.shortcut
import android.content.Context
import android.content.Intent
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
class DynamicShortcutManager(private val context: Context, @IoDispatcher private val ioDispatcher: CoroutineDispatcher) : ShortcutManager {
override suspend fun addShortcuts() {
withContext(ioDispatcher) {
ShortcutManagerCompat.setDynamicShortcuts(context, createShortcuts())
}
}
override suspend fun removeShortcuts() {
withContext(ioDispatcher) {
ShortcutManagerCompat.removeDynamicShortcuts(context, createShortcuts().map { it.id })
}
}
private fun createShortcuts(): List<ShortcutInfoCompat> {
return listOf(
buildShortcut(
context.getString(R.string.vpn_off),
context.getString(R.string.vpn_off),
context.getString(R.string.vpn_off),
intent = Intent(context, ShortcutsActivity::class.java).apply {
putExtra("className", "WireGuardTunnelService")
action = ShortcutsActivity.Action.STOP.name
},
shortcutIcon = R.drawable.vpn_off,
),
buildShortcut(
context.getString(R.string.vpn_on),
context.getString(R.string.vpn_on),
context.getString(R.string.vpn_on),
intent = Intent(context, ShortcutsActivity::class.java).apply {
putExtra("className", "WireGuardTunnelService")
action = ShortcutsActivity.Action.START.name
},
shortcutIcon = R.drawable.vpn_on,
),
buildShortcut(
context.getString(R.string.start_auto),
context.getString(R.string.start_auto),
context.getString(R.string.start_auto),
intent = Intent(context, ShortcutsActivity::class.java).apply {
putExtra("className", "WireGuardConnectivityWatcherService")
action = ShortcutsActivity.Action.START.name
},
shortcutIcon = R.drawable.auto_play,
),
buildShortcut(
context.getString(R.string.stop_auto),
context.getString(R.string.stop_auto),
context.getString(R.string.stop_auto),
intent = Intent(context, ShortcutsActivity::class.java).apply {
putExtra("className", "WireGuardConnectivityWatcherService")
action = ShortcutsActivity.Action.STOP.name
},
shortcutIcon = R.drawable.auto_pause,
),
)
}
private fun buildShortcut(id: String, shortLabel: String, longLabel: String, intent: Intent, shortcutIcon: Int): ShortcutInfoCompat {
return ShortcutInfoCompat.Builder(context, id)
.setShortLabel(shortLabel)
.setLongLabel(longLabel)
.setIntent(intent)
.setIcon(IconCompat.createWithResource(context, shortcutIcon))
.build()
}
}
@@ -1,6 +0,0 @@
package com.zaneschepke.wireguardautotunnel.service.shortcut
interface ShortcutManager {
suspend fun addShortcuts()
suspend fun removeShortcuts()
}
@@ -45,7 +45,7 @@ class ShortcutsActivity : ComponentActivity() {
Timber.d("Shortcut action on name: ${tunnelConfig?.name}")
tunnelConfig?.let {
when (intent.action) {
Action.START.name -> tunnelService.get().startTunnel(it)
Action.START.name -> tunnelService.get().startTunnel(it, true)
Action.STOP.name -> tunnelService.get().stopTunnel()
else -> Unit
}
@@ -1,7 +1,5 @@
package com.zaneschepke.wireguardautotunnel.service.tile
import android.content.Intent
import android.os.IBinder
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
@@ -11,7 +9,6 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
@@ -80,17 +77,6 @@ class AutoTunnelControlTile : TileService() {
}
}
/* This works around an annoying unsolved frameworks bug some people are hitting. */
override fun onBind(intent: Intent): IBinder? {
var ret: IBinder? = null
try {
ret = super.onBind(intent)
} catch (_: Throwable) {
Timber.e("Failed to bind to TunnelControlTile")
}
return ret
}
private fun setUnavailable() {
kotlin.runCatching {
qsTile.state = Tile.STATE_UNAVAILABLE
@@ -1,10 +1,9 @@
package com.zaneschepke.wireguardautotunnel.service.tile
import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
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
@@ -13,8 +12,8 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Provider
@AndroidEntryPoint
class TunnelControlTile : TileService() {
@@ -22,7 +21,7 @@ class TunnelControlTile : TileService() {
lateinit var appDataRepository: AppDataRepository
@Inject
lateinit var tunnelService: TunnelService
lateinit var tunnelService: Provider<TunnelService>
@Inject
@ApplicationScope
@@ -43,20 +42,17 @@ class TunnelControlTile : TileService() {
override fun onStartListening() {
super.onStartListening()
Timber.d("Start listening called")
serviceManager.tunnelControlTile.complete(this)
applicationScope.launch {
if (appDataRepository.tunnels.getAll().isEmpty()) return@launch setUnavailable()
updateTileState()
}
}
fun updateTileState() = applicationScope.launch {
if (appDataRepository.tunnels.getAll().isEmpty()) return@launch setUnavailable()
with(tunnelService.vpnState.value) {
if (status.isUp() && tunnelConfig != null) return@launch updateTile(tunnelConfig.name, true)
}
appDataRepository.getStartTunnelConfig()?.let {
updateTile(it.name, false)
val lastActive = appDataRepository.getStartTunnelConfig()
lastActive?.let {
updateTile(it)
}
}
@@ -64,9 +60,14 @@ class TunnelControlTile : TileService() {
super.onClick()
unlockAndRun {
applicationScope.launch {
if (tunnelService.vpnState.value.status.isUp()) return@launch tunnelService.stopTunnel()
appDataRepository.getStartTunnelConfig()?.let {
tunnelService.startTunnel(it)
val lastActive = appDataRepository.getStartTunnelConfig()
lastActive?.let { tunnel ->
if (tunnel.isActive) {
tunnelService.get().stopTunnel()
} else {
tunnelService.get().startTunnel(tunnel, true)
}
updateTileState()
}
}
}
@@ -106,24 +107,13 @@ class TunnelControlTile : TileService() {
}
}
/* This works around an annoying unsolved frameworks bug some people are hitting. */
override fun onBind(intent: Intent): IBinder? {
var ret: IBinder? = null
try {
ret = super.onBind(intent)
} catch (_: Throwable) {
Timber.e("Failed to bind to TunnelControlTile")
}
return ret
}
private fun updateTile(name: String, active: Boolean) {
private fun updateTile(tunnelConfig: TunnelConfig?) {
kotlin.runCatching {
setTileDescription(name)
if (active) return setActive()
setInactive()
}.onFailure {
Timber.e(it)
tunnelConfig?.let {
setTileDescription(it.name)
if (it.isActive) return setActive()
setInactive()
}
}
}
}
@@ -6,7 +6,7 @@ import kotlinx.coroutines.flow.StateFlow
interface TunnelService : Tunnel, org.amnezia.awg.backend.Tunnel {
suspend fun startTunnel(tunnelConfig: TunnelConfig?)
suspend fun startTunnel(tunnelConfig: TunnelConfig?, background: Boolean = false)
suspend fun stopTunnel()
@@ -2,53 +2,37 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel
import com.wireguard.android.backend.Backend
import com.wireguard.android.backend.Tunnel.State
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.module.Ethernet
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.module.Kernel
import com.zaneschepke.wireguardautotunnel.module.MobileData
import com.zaneschepke.wireguardautotunnel.module.Wifi
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model.NetworkState
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService.Companion.VPN_NOTIFICATION_ID
import com.zaneschepke.wireguardautotunnel.service.notification.WireGuardNotification
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.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.service.notification.WireGuardNotification
import com.zaneschepke.wireguardautotunnel.util.StringValue
import com.zaneschepke.wireguardautotunnel.util.extensions.asAmBackendState
import com.zaneschepke.wireguardautotunnel.util.extensions.asBackendState
import com.zaneschepke.wireguardautotunnel.util.extensions.cancelWithMessage
import com.zaneschepke.wireguardautotunnel.util.extensions.isReachable
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.amnezia.awg.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationAction
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService.Companion.VPN_NOTIFICATION_ID
import timber.log.Timber
import java.net.InetAddress
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
import javax.inject.Provider
@@ -63,9 +47,6 @@ constructor(
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
private val serviceManager: ServiceManager,
private val notificationService: NotificationService,
@Wifi private val wifiService: NetworkService,
@MobileData private val mobileDataService: NetworkService,
@Ethernet private val ethernetService: NetworkService,
) : TunnelService {
private val _vpnState = MutableStateFlow(VpnState())
@@ -73,12 +54,9 @@ constructor(
private var statsJob: Job? = null
private var tunnelChangesJob: Job? = null
private var pingJob: Job? = null
private var networkJob: Job? = null
@get:Synchronized @set:Synchronized
private var isKernelBackend: Boolean? = null
private val isNetworkAvailable = AtomicBoolean(false)
private val tunnelControlMutex = Mutex()
@@ -105,96 +83,66 @@ constructor(
}
}
// TODO refactor duplicate
@OptIn(FlowPreview::class)
private fun combineNetworkEventsJob(): Flow<NetworkState> {
return combine(
wifiService.status,
mobileDataService.status,
ethernetService.status,
) { wifi, mobileData, ethernet ->
NetworkState(
wifi.available,
mobileData.available,
ethernet.available,
wifi.name,
)
}.distinctUntilChanged()
}
private suspend fun setState(tunnelConfig: TunnelConfig, tunnelState: TunnelState): Result<TunnelState> {
return runCatching {
when (val backend = backend()) {
is Backend -> backend.setState(this, tunnelState.toWgState(), tunnelConfig.toWgConfig()).let { TunnelState.from(it) }
is Backend -> backend.setState(this, tunnelState.toWgState(), TunnelConfig.configFromWgQuick(tunnelConfig.wgQuick)).let { TunnelState.from(it) }
is org.amnezia.awg.backend.Backend -> {
backend.setState(this, tunnelState.toAmState(), tunnelConfig.toAmConfig()).let {
val config = if (tunnelConfig.amQuick.isBlank()) {
TunnelConfig.configFromAmQuick(
tunnelConfig.wgQuick,
)
} else {
TunnelConfig.configFromAmQuick(tunnelConfig.amQuick)
}
backend.setState(this, tunnelState.toAmState(), config).let {
TunnelState.from(it)
}
}
else -> throw NotImplementedError()
}
}.onFailure {
// TODO add better error message to user, especially for kernel as exceptions contain no details
// TODO add better error message and comms to user
Timber.e(it)
}
}
private fun isTunnelAlreadyRunning(tunnelConfig: TunnelConfig): Boolean {
return with(_vpnState.value) {
this.tunnelConfig?.id == tunnelConfig.id && status.isUp().also {
if (it) Timber.w("Tunnel already running")
}
}
val isRunning = tunnelConfig == _vpnState.value.tunnelConfig && _vpnState.value.status.isUp()
if (isRunning) Timber.w("Tunnel already running")
return isRunning
}
override suspend fun startTunnel(tunnelConfig: TunnelConfig?) {
override suspend fun startTunnel(tunnelConfig: TunnelConfig?, background: Boolean) {
withContext(ioDispatcher) {
if (tunnelConfig == null || isTunnelAlreadyRunning(tunnelConfig)) return@withContext
onBeforeStart(tunnelConfig)
updateTunnelConfig(tunnelConfig) // need to update this here
appDataRepository.tunnels.save(tunnelConfig.copy(isActive = true))
withServiceActive {
setState(tunnelConfig, TunnelState.UP).onSuccess {
updateTunnelState(it, tunnelConfig)
startActiveTunnelJobs()
onBeforeStart()
tunnelControlMutex.withLock {
setState(tunnelConfig, TunnelState.UP).onSuccess {
updateTunnelState(it, tunnelConfig)
onTunnelStart(tunnelConfig)
}
}.onFailure {
onTunnelStop(tunnelConfig)
// TODO improve this with better statuses and handling
showTunnelStartFailed()
Timber.e(it)
}
}
}
}
private fun showTunnelStartFailed() {
if (WireGuardAutoTunnel.isForeground()) {
SnackbarController.showMessage(StringValue.StringResource(R.string.error_tunnel_start))
} else {
launchStartFailedNotification()
}
}
private fun launchStartFailedNotification() {
with(notificationService) {
val notification = createNotification(
WireGuardNotification.NotificationChannels.VPN,
title = context.getString(R.string.error_tunnel_start),
)
show(VPN_NOTIFICATION_ID, notification)
}
}
override suspend fun stopTunnel() {
withContext(ioDispatcher) {
if (_vpnState.value.status.isDown()) return@withContext
with(_vpnState.value) {
if (tunnelConfig == null) return@withContext
setState(tunnelConfig, TunnelState.DOWN).onSuccess {
onTunnelStop(tunnelConfig)
updateTunnelState(it, null)
}.onFailure {
clearJobsAndStats()
Timber.e(it)
tunnelControlMutex.withLock {
setState(tunnelConfig, TunnelState.DOWN).onSuccess {
onTunnelStop(tunnelConfig)
updateTunnelState(it, null)
}.onFailure {
Timber.e(it)
}
}
}
}
@@ -223,12 +171,10 @@ constructor(
}
override suspend fun bounceTunnel() {
with(_vpnState.value) {
if (tunnelConfig != null && status.isUp()) {
withServiceActive {
toggleTunnel(tunnelConfig)
toggleTunnel(tunnelConfig)
}
_vpnState.value.tunnelConfig?.let {
withServiceActive {
toggleTunnel(it)
toggleTunnel(it)
}
}
}
@@ -258,20 +204,42 @@ constructor(
}
}
private suspend fun onBeforeStart(tunnelConfig: TunnelConfig) {
private suspend fun onBeforeStart() {
with(_vpnState.value) {
if (status.isUp()) stopTunnel() else clearJobsAndStats()
serviceManager.startBackgroundService(tunnelConfig)
if (isKernelBackend == true) serviceManager.startBackgroundService(tunnelConfig)
}
}
private suspend fun onTunnelStart(tunnelConfig: TunnelConfig) {
startActiveTunnelJobs()
if (_vpnState.value.status.isUp()) {
appDataRepository.tunnels.save(tunnelConfig.copy(isActive = true))
}
if (isKernelBackend == false) launchUserspaceTunnelNotification()
}
private fun launchUserspaceTunnelNotification() {
with(notificationService) {
val notification = createNotification(
WireGuardNotification.NotificationChannels.VPN,
title = "${context.getString(R.string.tunnel_running)} - ${_vpnState.value.tunnelConfig?.name}",
actions = listOf(
notificationService.createNotificationAction(NotificationAction.TUNNEL_OFF),
),
)
show(VPN_NOTIFICATION_ID, notification)
}
}
private suspend fun onTunnelStop(tunnelConfig: TunnelConfig) {
runCatching {
appDataRepository.tunnels.save(tunnelConfig.copy(isActive = false))
appDataRepository.tunnels.save(tunnelConfig.copy(isActive = false))
if (isKernelBackend == true) {
serviceManager.stopBackgroundService()
} else {
notificationService.remove(VPN_NOTIFICATION_ID)
clearJobsAndStats()
}
clearJobsAndStats()
}
private fun clearJobsAndStats() {
@@ -312,24 +280,15 @@ constructor(
}
override fun cancelActiveTunnelJobs() {
statsJob?.cancelWithMessage("Tunnel stats job cancelled")
tunnelChangesJob?.cancelWithMessage("Tunnel changes job cancelled")
cancelPingJobs()
statsJob?.cancel()
tunnelChangesJob?.cancel()
}
override fun startActiveTunnelJobs() {
statsJob = startTunnelStatisticsJob()
tunnelChangesJob = startTunnelConfigChangesJob()
if (_vpnState.value.tunnelConfig?.isPingEnabled == true) {
startPingJobs()
}
}
private fun startPingJobs() {
cancelPingJobs()
pingJob = startPingJob()
networkJob = startNetworkJob()
}
override fun getName(): String {
return _vpnState.value.tunnelConfig?.name ?: ""
}
@@ -352,111 +311,22 @@ constructor(
}
}
private fun isQuickConfigChanged(config: TunnelConfig): Boolean {
return with(_vpnState.value) {
if (tunnelConfig == null) return false
config.wgQuick != tunnelConfig.wgQuick ||
config.amQuick != tunnelConfig.amQuick
}
}
private fun isPingConfigMatching(config: TunnelConfig): Boolean {
return with(_vpnState.value.tunnelConfig) {
if (this == null) return true
config.isPingEnabled == isPingEnabled &&
pingIp == config.pingIp &&
config.pingCooldown == pingCooldown &&
config.pingInterval == pingInterval
}
}
private fun handlePingConfigChanges() {
with(_vpnState.value.tunnelConfig) {
if (this == null) return
if (!isPingEnabled && pingJob?.isActive == true) {
cancelPingJobs()
return
}
restartPingJob()
}
}
private fun restartPingJob() {
cancelPingJobs()
startPingJobs()
}
private fun cancelPingJobs() {
pingJob?.cancelWithMessage("Ping job cancelled")
networkJob?.cancelWithMessage("Network job cancelled")
}
private fun startTunnelConfigChangesJob() = applicationScope.launch(ioDispatcher) {
tunnelConfigRepository.getTunnelConfigsFlow().collect { tunnels ->
tunnelConfigRepository.getTunnelConfigsFlow().collect {
with(_vpnState.value) {
if (tunnelConfig == null) return@collect
val storageConfig = tunnels.firstOrNull { it.id == tunnelConfig.id }
if (storageConfig == null) return@collect
val quickChanged = isQuickConfigChanged(storageConfig)
val pingMatching = isPingConfigMatching(storageConfig)
updateTunnelConfig(storageConfig)
if (quickChanged) bounceTunnel()
if (!pingMatching) handlePingConfigChanges()
}
}
}
private suspend fun pingTunnel(tunnelConfig: TunnelConfig): List<Boolean> {
return withContext(ioDispatcher) {
val config = tunnelConfig.toWgConfig()
if (tunnelConfig.pingIp != null) {
Timber.i("Pinging custom ip")
listOf(InetAddress.getByName(tunnelConfig.pingIp).isReachable(Constants.PING_TIMEOUT.toInt()))
} else {
Timber.i("Pinging all peers")
config.peers.map { peer ->
peer.isReachable()
if (status.isDown() || tunnelConfig == null) return@collect
val vpnConfigFromStorage = it.first { it.id == tunnelConfig.id }
val isRestartNeeded = vpnConfigFromStorage.wgQuick != tunnelConfig.wgQuick ||
vpnConfigFromStorage.amQuick != tunnelConfig.amQuick
updateTunnelConfig(vpnConfigFromStorage)
if (isRestartNeeded) {
Timber.d("Bouncing tunnel on config change")
bounceTunnel()
}
}
}
}
private fun startPingJob() = applicationScope.launch(ioDispatcher) {
do {
run {
with(_vpnState.value) {
if (status.isUp() && tunnelConfig != null && isNetworkAvailable.get()) {
val reachable = pingTunnel(tunnelConfig)
if (reachable.contains(false)) {
if (isNetworkAvailable.get()) {
Timber.i("Ping result: target was not reachable, bouncing the tunnel")
bounceTunnel()
delay(tunnelConfig.pingCooldown ?: Constants.PING_COOLDOWN)
} else {
Timber.i("Ping result: target was not reachable, but not network available")
}
return@run
} else {
Timber.i("Ping result: all ping targets were reached successfully")
}
}
delay(tunnelConfig?.pingInterval ?: Constants.PING_INTERVAL)
}
}
} while (true)
}
private fun startNetworkJob() = applicationScope.launch(ioDispatcher) {
combineNetworkEventsJob().collect {
Timber.d("New network state: $it")
if (!it.isWifiConnected && !it.isEthernetConnected && !it.isMobileDataConnected) {
isNetworkAvailable.set(false)
} else {
isNetworkAvailable.set(true)
}
}
}
override fun onStateChange(newState: Tunnel.State) {
_vpnState.update {
it.copy(status = TunnelState.from(newState))
@@ -1,15 +1,12 @@
package com.zaneschepke.wireguardautotunnel.ui
import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wireguard.android.backend.WgQuickBackend
import com.wireguard.android.util.RootShell
import com.wireguard.config.Config
import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
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.AppShell
@@ -19,20 +16,13 @@ import com.zaneschepke.wireguardautotunnel.service.tunnel.BackendState
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.ui.screens.tunneloptions.config.model.InterfaceProxy
import com.zaneschepke.wireguardautotunnel.ui.screens.tunneloptions.config.model.PeerProxy
import com.zaneschepke.wireguardautotunnel.ui.screens.tunneloptions.splittunnel.SplitTunnelApp
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
import com.zaneschepke.wireguardautotunnel.util.StringValue
import com.zaneschepke.wireguardautotunnel.util.extensions.getAllInternetCapablePackages
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
@@ -60,18 +50,6 @@ constructor(
private val logReader: LogReader,
) : ViewModel() {
private val _popBackStack = MutableSharedFlow<Boolean>()
val popBackStack = _popBackStack.asSharedFlow()
private val _isAppReady = MutableStateFlow(false)
val isAppReady = _isAppReady.asStateFlow()
private val _configurationChange = MutableStateFlow(false)
val configurationChange = _configurationChange.asStateFlow()
private val _splitTunnelApps = MutableStateFlow<List<SplitTunnelApp>>(emptyList())
val splitTunnelApps = _splitTunnelApps.asStateFlow()
val uiState =
combine(
appDataRepository.settings.getSettingsFlow(),
@@ -93,13 +71,17 @@ constructor(
AppUiState(),
)
private val _isAppReady = MutableStateFlow(false)
val isAppReady = _isAppReady.asStateFlow()
private val _configurationChange = MutableStateFlow(false)
val configurationChange = _configurationChange.asStateFlow()
init {
viewModelScope.launch {
initPin()
initServices()
launch {
initTunnel()
}
initTunnel()
appReadyCheck()
}
}
@@ -112,14 +94,12 @@ constructor(
}
private suspend fun initTunnel() {
withContext(ioDispatcher) {
if (tunnelService.get().getState() == TunnelState.UP) tunnelService.get().startActiveTunnelJobs()
val activeTunnels = appDataRepository.tunnels.getActive()
if (activeTunnels.isNotEmpty() &&
tunnelService.get().getState() == TunnelState.DOWN
) {
tunnelService.get().startTunnel(activeTunnels.first())
}
if (tunnelService.get().getState() == TunnelState.UP) tunnelService.get().startActiveTunnelJobs()
val activeTunnels = appDataRepository.tunnels.getActive()
if (activeTunnels.isNotEmpty() &&
tunnelService.get().getState() == TunnelState.DOWN
) {
tunnelService.get().startTunnel(activeTunnels.first())
}
}
@@ -136,10 +116,6 @@ constructor(
}
}
fun saveSettings(settings: Settings) = viewModelScope.launch {
appDataRepository.settings.save(settings)
}
fun onPinLockDisabled() = viewModelScope.launch(ioDispatcher) {
PinManager.clearPin()
appDataRepository.appState.setPinLockEnabled(false)
@@ -210,14 +186,18 @@ constructor(
private suspend fun handleVpnKillSwitchChange(enabled: Boolean) {
withContext(ioDispatcher) {
if (!enabled) return@withContext tunnelService.get().setBackendState(BackendState.SERVICE_ACTIVE, emptySet())
Timber.d("Starting kill switch")
val allowedIps = if (appDataRepository.settings.getSettings().isLanOnKillSwitchEnabled) {
TunnelConfig.IPV4_PUBLIC_NETWORKS
if (enabled) {
Timber.d("Starting kill switch")
val allowedIps = if (appDataRepository.settings.getSettings().isLanOnKillSwitchEnabled) {
TunnelConfig.IPV4_PUBLIC_NETWORKS
} else {
emptySet()
}
tunnelService.get().setBackendState(BackendState.KILL_SWITCH_ACTIVE, allowedIps)
} else {
emptySet()
Timber.d("Sending shutdown of kill switch")
tunnelService.get().setBackendState(BackendState.SERVICE_ACTIVE, emptySet())
}
tunnelService.get().setBackendState(BackendState.KILL_SWITCH_ACTIVE, allowedIps)
}
}
@@ -277,20 +257,7 @@ constructor(
}
}
suspend fun getEmitSplitTunnelApps(context: Context) {
withContext(ioDispatcher) {
val apps = context.getAllInternetCapablePackages().filter { it.applicationInfo != null }.map { pack ->
SplitTunnelApp(
context.packageManager.getApplicationIcon(pack.applicationInfo!!),
context.packageManager.getApplicationLabel(pack.applicationInfo!!).toString(),
pack.packageName,
)
}
_splitTunnelApps.emit(apps)
}
}
suspend fun requestRoot(): Result<Unit> {
private suspend fun requestRoot(): Result<Unit> {
return withContext(ioDispatcher) {
kotlin.runCatching {
rootShell.get().start()
@@ -300,121 +267,4 @@ constructor(
}
}
}
fun updateExistingTunnelConfig(
tunnelConfig: TunnelConfig,
tunnelName: String? = null,
peers: List<PeerProxy>? = null,
`interface`: InterfaceProxy? = null,
) = viewModelScope.launch {
runCatching {
val amConfig = tunnelConfig.toAmConfig()
val wgConfig = tunnelConfig.toWgConfig()
updateTunnelConfig(tunnelConfig, tunnelName, amConfig, wgConfig, peers, `interface`)
_popBackStack.emit(true)
SnackbarController.showMessage(StringValue.StringResource(R.string.config_changes_saved))
}.onFailure {
onConfigSaveError(it)
}
}
fun saveNewTunnel(tunnelName: String, peers: List<PeerProxy>, `interface`: InterfaceProxy) = viewModelScope.launch {
runCatching {
val config = buildConfigs(peers, `interface`)
appDataRepository.tunnels.save(
TunnelConfig(
name = tunnelName,
wgQuick = config.first.toWgQuickString(true),
amQuick = config.second.toAwgQuickString(true),
),
)
_popBackStack.emit(true)
SnackbarController.showMessage(StringValue.StringResource(R.string.config_changes_saved))
}.onFailure {
onConfigSaveError(it)
}
}
private fun onConfigSaveError(throwable: Throwable) {
Timber.e(throwable)
SnackbarController.showMessage(
throwable.message?.let { message ->
(StringValue.DynamicString(message))
} ?: StringValue.StringResource(R.string.unknown_error),
)
}
private suspend fun updateTunnelConfig(
tunnelConfig: TunnelConfig,
tunnelName: String? = null,
amConfig: org.amnezia.awg.config.Config,
wgConfig: Config,
peers: List<PeerProxy>? = null,
`interface`: InterfaceProxy? = null,
) {
val configs = rebuildConfigs(amConfig, wgConfig, peers, `interface`)
appDataRepository.tunnels.save(
tunnelConfig.copy(
name = tunnelName ?: tunnelConfig.name,
amQuick = configs.second.toAwgQuickString(true),
wgQuick = configs.first.toWgQuickString(true),
),
)
}
fun cleanUpUninstalledApps(tunnelConfig: TunnelConfig, packages: List<String>) = viewModelScope.launch(ioDispatcher) {
runCatching {
val amConfig = tunnelConfig.toAmConfig()
val wgConfig = tunnelConfig.toWgConfig()
val proxy = InterfaceProxy.from(amConfig.`interface`)
if (proxy.includedApplications.isEmpty() && proxy.excludedApplications.isEmpty()) return@launch
if (proxy.includedApplications.retainAll(packages.toSet()) || proxy.excludedApplications.retainAll(packages.toSet())) {
updateTunnelConfig(tunnelConfig, amConfig = amConfig, wgConfig = wgConfig, `interface` = proxy)
Timber.i("Removed split tunnel package for app that no longer exists on the device")
}
}.onFailure {
Timber.e(it)
}
}
fun bounceAutoTunnel() = viewModelScope.launch(ioDispatcher) {
serviceManager.stopAutoTunnel()
delay(1000L)
serviceManager.startAutoTunnel(true)
}
private suspend fun rebuildConfigs(
amConfig: org.amnezia.awg.config.Config,
wgConfig: Config,
peers: List<PeerProxy>? = null,
`interface`: InterfaceProxy? = null,
): Pair<Config, org.amnezia.awg.config.Config> {
return withContext(ioDispatcher) {
Pair(
Config.Builder().apply {
addPeers(peers?.map { it.toWgPeer() } ?: wgConfig.peers)
setInterface(`interface`?.toWgInterface() ?: wgConfig.`interface`)
}.build(),
org.amnezia.awg.config.Config.Builder().apply {
addPeers(peers?.map { it.toAmPeer() } ?: amConfig.peers)
setInterface(`interface`?.toAmInterface() ?: amConfig.`interface`)
}.build(),
)
}
}
private suspend fun buildConfigs(peers: List<PeerProxy>, `interface`: InterfaceProxy): Pair<Config, org.amnezia.awg.config.Config> {
return withContext(ioDispatcher) {
Pair(
Config.Builder().apply {
addPeers(peers.map { it.toWgPeer() })
setInterface(`interface`.toWgInterface())
}.build(),
org.amnezia.awg.config.Config.Builder().apply {
addPeers(peers.map { it.toAmPeer() })
setInterface(`interface`.toAmInterface())
}.build(),
)
}
}
}
@@ -1,20 +1,14 @@
package com.zaneschepke.wireguardautotunnel.ui
import android.content.Intent
import android.graphics.Color.TRANSPARENT
import android.os.Build
import android.os.Bundle
import androidx.activity.SystemBarStyle
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
@@ -35,21 +29,21 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.toRoute
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.service.shortcut.ShortcutManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.CustomSnackBar
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarControllerProvider
import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.options.OptionsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.pinlock.PinLockScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.scanner.ScannerScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
@@ -57,20 +51,14 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.Appear
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.display.DisplayScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.language.LanguageScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.AutoTunnelScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.advanced.AdvancedScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.disclosure.LocationDisclosureScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.killswitch.KillSwitchScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.logs.LogsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunneloptions.OptionsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunneloptions.config.ConfigScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunneloptions.splittunnel.SplitTunnelScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunneloptions.tunnelautotunnel.TunnelAutoTunnelScreen
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlin.system.exitProcess
@@ -83,18 +71,7 @@ class MainActivity : AppCompatActivity() {
@Inject
lateinit var tunnelService: TunnelService
@Inject
lateinit var shortcutManager: ShortcutManager
@OptIn(ExperimentalLayoutApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge(
statusBarStyle = SystemBarStyle.auto(TRANSPARENT, TRANSPARENT),
navigationBarStyle = SystemBarStyle.auto(TRANSPARENT, TRANSPARENT),
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
window.isNavigationBarContrastEnforced = false
}
super.onCreate(savedInstanceState)
val viewModel by viewModels<AppViewModel>()
@@ -119,10 +96,6 @@ class MainActivity : AppCompatActivity() {
}
}
LaunchedEffect(Unit) {
viewModel.getEmitSplitTunnelApps(this@MainActivity)
}
LaunchedEffect(appUiState.autoTunnelActive) {
requestAutoTunnelTileServiceUpdate()
}
@@ -131,17 +104,13 @@ class MainActivity : AppCompatActivity() {
LaunchedEffect(isAutoTunnelEnabled) {
this@MainActivity.requestAutoTunnelTileServiceUpdate()
}
LaunchedEffect(isShortcutsEnabled) {
if (!isShortcutsEnabled) return@LaunchedEffect shortcutManager.removeShortcuts()
shortcutManager.addShortcuts()
}
}
CompositionLocalProvider(LocalNavController provides navController) {
SnackbarControllerProvider { host ->
WireguardAutoTunnelTheme(theme = appUiState.generalState.theme) {
Scaffold(
contentWindowInsets = WindowInsets(0),
contentWindowInsets = WindowInsets(0.dp),
snackbarHost = {
SnackbarHost(host) { snackbarData: SnackbarData ->
CustomSnackBar(
@@ -215,24 +184,26 @@ class MainActivity : AppCompatActivity() {
composable<Route.Support> {
SupportScreen(appUiState, viewModel)
}
composable<Route.AutoTunnelAdvanced> {
AdvancedScreen(appUiState, viewModel)
}
composable<Route.Logs> {
LogsScreen()
}
composable<Route.Config> {
val args = it.toRoute<Route.Config>()
val config = appUiState.tunnels.firstOrNull { it.id == args.id }
ConfigScreen(config, viewModel)
ConfigScreen(
tunnelId = args.id,
)
}
composable<Route.TunnelOptions> {
val args = it.toRoute<Route.TunnelOptions>()
val config = appUiState.tunnels.first { it.id == args.id }
OptionsScreen(config)
composable<Route.Option> {
val args = it.toRoute<Route.Option>()
OptionsScreen(
tunnelId = args.id,
appUiState = appUiState,
)
}
composable<Route.Lock> {
PinLockScreen(viewModel)
PinLockScreen(
appViewModel = viewModel,
)
}
composable<Route.Scanner> {
ScannerScreen()
@@ -240,23 +211,6 @@ class MainActivity : AppCompatActivity() {
composable<Route.KillSwitch> {
KillSwitchScreen(appUiState, viewModel)
}
composable<Route.SplitTunnel> {
val args = it.toRoute<Route.SplitTunnel>()
val config = appUiState.tunnels.first { it.id == args.id }
SplitTunnelScreen(config, viewModel)
}
composable<Route.TunnelAutoTunnel> {
val args = it.toRoute<Route.TunnelOptions>()
val config = appUiState.tunnels.first { it.id == args.id }
TunnelAutoTunnelScreen(config, appUiState.settings)
}
}
BackHandler(enabled = true) {
lifecycleScope.launch {
if (!navController.popBackStack()) {
this@MainActivity.finish()
}
}
}
}
}
@@ -265,4 +219,9 @@ class MainActivity : AppCompatActivity() {
}
}
}
override fun onDestroy() {
super.onDestroy()
// save battery by not polling stats while app is closed
tunnelService.cancelActiveTunnelJobs()
}
}
@@ -12,9 +12,6 @@ sealed class Route {
@Serializable
data object AutoTunnel : Route()
@Serializable
data object AutoTunnelAdvanced : Route()
@Serializable
data object LocationDisclosure : Route()
@@ -34,7 +31,7 @@ sealed class Route {
data object Main : Route()
@Serializable
data class TunnelOptions(
data class Option(
val id: Int,
) : Route()
@@ -49,16 +46,6 @@ sealed class Route {
val id: Int,
) : Route()
@Serializable
data class SplitTunnel(
val id: Int,
) : Route()
@Serializable
data class TunnelAutoTunnel(
val id: Int,
) : Route()
@Serializable
data object Logs : Route()
}
@@ -55,13 +55,7 @@ fun ExpandingRowListItem(
modifier = Modifier.fillMaxWidth(13 / 20f),
) {
leading()
Text(
text,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onBackground,
)
Text(text, maxLines = 1, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.labelLarge)
}
trailing()
}
@@ -2,22 +2,36 @@ package com.zaneschepke.wireguardautotunnel.ui.common
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.CheckBox
import androidx.compose.material3.Icon
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
@Composable
fun SelectedLabel() {
Row(
modifier = Modifier.fillMaxSize(),
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically,
) {
val icon = Icons.Outlined.CheckBox
Icon(icon, icon.name)
Text(
stringResource(id = R.string.selected),
modifier =
Modifier.padding(
horizontal = 24.dp.scaledWidth(),
vertical = 16.dp.scaledHeight(),
),
color =
MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.labelSmall,
)
}
}
@@ -1,8 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.common.button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
@@ -16,13 +14,5 @@ fun ScaledSwitch(checked: Boolean, onClick: (checked: Boolean) -> Unit, enabled:
{ onClick(it) },
modifier.scale((52.dp.scaledHeight() / 52.dp)),
enabled = enabled,
colors = SwitchDefaults.colors().copy(
checkedThumbColor = MaterialTheme.colorScheme.background,
checkedIconColor = MaterialTheme.colorScheme.background,
uncheckedTrackColor = MaterialTheme.colorScheme.surface,
uncheckedBorderColor = MaterialTheme.colorScheme.outline,
uncheckedThumbColor = MaterialTheme.colorScheme.outline,
uncheckedIconColor = MaterialTheme.colorScheme.outline,
),
)
}
@@ -5,9 +5,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
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.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
@@ -19,10 +17,8 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
@Composable
fun SelectionItemButton(
@@ -34,8 +30,7 @@ fun SelectionItemButton(
) {
Card(
modifier =
Modifier
.clip(RoundedCornerShape(8.dp))
Modifier.clip(RoundedCornerShape(8.dp))
.clickable(
indication = if (ripple) ripple() else null,
interactionSource = remember { MutableInteractionSource() },
@@ -50,20 +45,15 @@ fun SelectionItemButton(
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start,
modifier = Modifier
.fillMaxSize()
.padding(end = 10.dp.scaledWidth()),
modifier = Modifier.fillMaxSize(),
) {
leading?.let {
it()
}
Text(
buttonText,
style = MaterialTheme.typography.labelMedium,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.fillMaxWidth(3 / 4f),
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
trailing?.let {
it()
@@ -52,7 +52,6 @@ fun SurfaceSelectionGroupButton(items: List<SelectionItem>) {
icon,
icon.name,
modifier = Modifier.size(iconSize),
tint = MaterialTheme.colorScheme.onSurface,
)
}
Column(
@@ -81,7 +80,7 @@ fun SurfaceSelectionGroupButton(items: List<SelectionItem>) {
}
}
}
if (index + 1 != items.size) HorizontalDivider(color = MaterialTheme.colorScheme.outline.copy(0.30f))
if (index + 1 != items.size) HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
}
}
}
@@ -3,7 +3,6 @@ package com.zaneschepke.wireguardautotunnel.ui.common.config
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -24,20 +23,19 @@ fun ConfigurationTextBox(
capitalization = KeyboardCapitalization.None,
imeAction = ImeAction.Done,
),
trailing: (@Composable () -> Unit)? = null,
trailing: @Composable () -> Unit = {},
interactionSource: MutableInteractionSource? = null,
) {
OutlinedTextField(
isError = isError,
textStyle = MaterialTheme.typography.labelLarge,
modifier = modifier,
value = value,
singleLine = true,
interactionSource = interactionSource,
onValueChange = { onValueChange(it) },
label = { Text(label, color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.labelMedium) },
label = { Text(label) },
maxLines = 1,
placeholder = { Text(hint, color = MaterialTheme.colorScheme.outline, style = MaterialTheme.typography.labelLarge) },
placeholder = { Text(hint) },
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
trailingIcon = trailing,
@@ -4,12 +4,12 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
@Composable
fun ConfigurationToggle(
@@ -38,11 +38,11 @@ fun ConfigurationToggle(
),
softWrap = true,
)
ScaledSwitch(
Switch(
modifier = modifier,
enabled = enabled,
checked = checked,
onClick = { onCheckChanged(it) },
onCheckedChange = { onCheckChanged(it) },
)
}
}
@@ -16,7 +16,6 @@ fun GroupLabel(title: String) {
Text(
title,
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onBackground,
)
}
}
@@ -4,14 +4,12 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.NavigationBarItemDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.Color
import androidx.navigation.NavController
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.compose.currentBackStackEntryAsState
@@ -59,13 +57,6 @@ fun BottomNavBar(navController: NavController, bottomNavItems: List<BottomNavIte
contentDescription = "${item.name} Icon",
)
},
colors = NavigationBarItemDefaults.colors().copy(
selectedIndicatorColor = Color.Transparent,
selectedIconColor = MaterialTheme.colorScheme.primary,
selectedTextColor = MaterialTheme.colorScheme.primary,
unselectedTextColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.55f),
unselectedIconColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.55f),
),
)
}
}
@@ -19,9 +19,7 @@ fun TopNavBar(title: String, trailing: @Composable () -> Unit = {}, showBack: Bo
},
navigationIcon = {
if (showBack) {
IconButton(onClick = {
navController.popBackStack()
}) {
IconButton(onClick = { navController.popBackStack() }) {
val icon = Icons.AutoMirrored.Outlined.ArrowBack
Icon(
imageVector = icon,
@@ -28,8 +28,8 @@ fun CustomTextField(
onValueChange: (value: String) -> Unit = {},
singleLine: Boolean = false,
placeholder: @Composable (() -> Unit)? = null,
keyboardOptions: KeyboardOptions = KeyboardOptions(),
keyboardActions: KeyboardActions = KeyboardActions(),
keyboardOptions: KeyboardOptions,
keyboardActions: KeyboardActions,
supportingText: @Composable (() -> Unit)? = null,
leading: @Composable (() -> Unit)? = null,
trailing: @Composable (() -> Unit)? = null,
@@ -0,0 +1,601 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.config
import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusGroup
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.ContentCopy
import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material.icons.rounded.Save
import androidx.compose.material3.FabPosition
import androidx.compose.material3.FloatingActionButton
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.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
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.platform.ClipboardManager
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
import com.zaneschepke.wireguardautotunnel.ui.screens.config.components.ApplicationSelectionDialog
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import kotlinx.coroutines.delay
@Composable
fun ConfigScreen(tunnelId: Int) {
val viewModel = hiltViewModel<ConfigViewModel, ConfigViewModel.ConfigViewModelFactory> { factory ->
factory.create(tunnelId)
}
val context = LocalContext.current
val snackbar = SnackbarController.current
val clipboardManager: ClipboardManager = LocalClipboardManager.current
val keyboardController = LocalSoftwareKeyboardController.current
val navController = LocalNavController.current
var showApplicationsDialog by remember { mutableStateOf(false) }
var showAuthPrompt by remember { mutableStateOf(false) }
var isAuthenticated by remember { mutableStateOf(false) }
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
var configType by remember { mutableStateOf<ConfigType?>(null) }
val derivedConfigType = remember {
derivedStateOf {
configType ?: if (!uiState.isAmneziaEnabled) ConfigType.WIREGUARD else ConfigType.AMNEZIA
}
}
val saved by viewModel.saved.collectAsStateWithLifecycle(null)
LaunchedEffect(saved) {
if (saved == true) {
navController.navigate(Route.Main)
}
}
LaunchedEffect(Unit) {
delay(2_000L)
viewModel.cleanUpUninstalledApps()
}
val keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() })
val keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done)
val fillMaxHeight = .85f
val fillMaxWidth = .85f
val screenPadding = 5.dp
val applicationButtonText =
buildAnnotatedString {
append(stringResource(id = R.string.tunneling_apps))
append(": ")
if (uiState.isAllApplicationsEnabled) {
append(stringResource(id = R.string.all))
} else {
append("${uiState.checkedPackageNames.size} ")
(
if (uiState.include) {
append(stringResource(id = R.string.included))
} else {
append(
stringResource(id = R.string.excluded),
)
}
)
}
}
if (showAuthPrompt) {
AuthorizationPrompt(
onSuccess = {
showAuthPrompt = false
isAuthenticated = true
},
onError = {
showAuthPrompt = false
snackbar.showMessage(
context.getString(R.string.error_authentication_failed),
)
},
onFailure = {
showAuthPrompt = false
snackbar.showMessage(
context.getString(R.string.error_authorization_failed),
)
},
)
}
if (showApplicationsDialog) {
ApplicationSelectionDialog(viewModel, uiState) {
showApplicationsDialog = false
}
}
Scaffold(
topBar = {
TopNavBar(stringResource(R.string.edit_tunnel))
},
floatingActionButtonPosition = FabPosition.End,
floatingActionButton = {
FloatingActionButton(
onClick = {
viewModel.onSaveAllChanges()
},
containerColor = MaterialTheme.colorScheme.primary,
shape = RoundedCornerShape(16.dp),
) {
Icon(
imageVector = Icons.Rounded.Save,
contentDescription = stringResource(id = R.string.save_changes),
tint = MaterialTheme.colorScheme.background,
)
}
},
) { padding ->
Column(Modifier.padding(padding)) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top,
modifier =
Modifier
.verticalScroll(rememberScrollState())
.weight(1f, true)
.fillMaxSize(),
) {
Surface(
tonalElevation = 2.dp,
shadowElevation = 2.dp,
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.surface,
modifier =
(
if (context.isRunningOnTv()) {
Modifier
.fillMaxHeight(fillMaxHeight)
.fillMaxWidth(fillMaxWidth)
} else {
Modifier.fillMaxWidth(fillMaxWidth)
}
)
.padding(bottom = 10.dp.scaledHeight()).padding(top = 24.dp.scaledHeight()),
) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top,
modifier =
Modifier
.padding(15.dp)
.focusGroup(),
) {
SectionTitle(
stringResource(R.string.interface_),
padding = screenPadding,
)
ConfigurationToggle(
stringResource(id = R.string.show_amnezia_properties),
checked = derivedConfigType.value == ConfigType.AMNEZIA,
onCheckChanged = { configType = if (it) ConfigType.AMNEZIA else ConfigType.WIREGUARD },
)
ConfigurationTextBox(
value = uiState.tunnelName,
onValueChange = viewModel::onTunnelNameChange,
keyboardActions = keyboardActions,
label = stringResource(R.string.name),
hint = stringResource(R.string.tunnel_name).lowercase(),
modifier =
Modifier
.fillMaxWidth(),
)
OutlinedTextField(
modifier =
Modifier
.fillMaxWidth()
.clickable { showAuthPrompt = true },
value = uiState.interfaceProxy.privateKey,
visualTransformation =
if ((tunnelId == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated) {
VisualTransformation.None
} else {
PasswordVisualTransformation()
},
enabled = (tunnelId == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated,
onValueChange = { value -> viewModel.onPrivateKeyChange(value) },
trailingIcon = {
IconButton(
modifier = Modifier.focusRequester(FocusRequester.Default),
onClick = { viewModel.generateKeyPair() },
) {
Icon(
Icons.Rounded.Refresh,
stringResource(R.string.rotate_keys),
tint = MaterialTheme.colorScheme.onSurface,
)
}
},
label = { Text(stringResource(R.string.private_key)) },
singleLine = true,
placeholder = { Text(stringResource(R.string.base64_key)) },
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
)
OutlinedTextField(
modifier =
Modifier
.fillMaxWidth()
.focusRequester(FocusRequester.Default),
value = uiState.interfaceProxy.publicKey,
enabled = false,
onValueChange = {},
trailingIcon = {
IconButton(
modifier = Modifier.focusRequester(FocusRequester.Default),
onClick = {
clipboardManager.setText(
AnnotatedString(uiState.interfaceProxy.publicKey),
)
},
) {
Icon(
Icons.Rounded.ContentCopy,
stringResource(R.string.copy_public_key),
tint = MaterialTheme.colorScheme.onSurface,
)
}
},
label = { Text(stringResource(R.string.public_key)) },
singleLine = true,
placeholder = { Text(stringResource(R.string.base64_key)) },
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
)
ConfigurationTextBox(
value = uiState.interfaceProxy.addresses,
onValueChange = viewModel::onAddressesChanged,
keyboardActions = keyboardActions,
label = stringResource(R.string.addresses),
hint = stringResource(R.string.comma_separated_list),
modifier =
Modifier
.fillMaxWidth()
.padding(end = 5.dp),
)
ConfigurationTextBox(
value = uiState.interfaceProxy.listenPort,
onValueChange = viewModel::onListenPortChanged,
keyboardActions = keyboardActions,
label = stringResource(R.string.listen_port),
hint = stringResource(R.string.random),
modifier = Modifier.fillMaxWidth(),
)
Row(modifier = Modifier.fillMaxWidth()) {
ConfigurationTextBox(
value = uiState.interfaceProxy.dnsServers,
onValueChange = viewModel::onDnsServersChanged,
keyboardActions = keyboardActions,
label = stringResource(R.string.dns_servers),
hint = stringResource(R.string.comma_separated_list),
modifier =
Modifier
.fillMaxWidth(3 / 5f)
.padding(end = 5.dp),
)
ConfigurationTextBox(
value = uiState.interfaceProxy.mtu,
onValueChange = viewModel::onMtuChanged,
keyboardActions = keyboardActions,
label = stringResource(R.string.mtu),
hint = stringResource(R.string.auto),
modifier = Modifier.width(IntrinsicSize.Min),
)
}
if (derivedConfigType.value == ConfigType.AMNEZIA) {
ConfigurationTextBox(
value = uiState.interfaceProxy.junkPacketCount,
onValueChange = viewModel::onJunkPacketCountChanged,
keyboardActions = keyboardActions,
label = stringResource(R.string.junk_packet_count),
hint = stringResource(R.string.junk_packet_count).lowercase(),
modifier =
Modifier
.fillMaxWidth(),
)
ConfigurationTextBox(
value = uiState.interfaceProxy.junkPacketMinSize,
onValueChange = viewModel::onJunkPacketMinSizeChanged,
keyboardActions = keyboardActions,
label = stringResource(R.string.junk_packet_minimum_size),
hint =
stringResource(
R.string.junk_packet_minimum_size,
).lowercase(),
modifier =
Modifier
.fillMaxWidth(),
)
ConfigurationTextBox(
value = uiState.interfaceProxy.junkPacketMaxSize,
onValueChange = viewModel::onJunkPacketMaxSizeChanged,
keyboardActions = keyboardActions,
label = stringResource(R.string.junk_packet_maximum_size),
hint =
stringResource(
R.string.junk_packet_maximum_size,
).lowercase(),
modifier =
Modifier
.fillMaxWidth(),
)
ConfigurationTextBox(
value = uiState.interfaceProxy.initPacketJunkSize,
onValueChange = viewModel::onInitPacketJunkSizeChanged,
keyboardActions = keyboardActions,
label = stringResource(R.string.init_packet_junk_size),
hint = stringResource(R.string.init_packet_junk_size).lowercase(),
modifier =
Modifier
.fillMaxWidth(),
)
ConfigurationTextBox(
value = uiState.interfaceProxy.responsePacketJunkSize,
onValueChange = viewModel::onResponsePacketJunkSize,
keyboardActions = keyboardActions,
label = stringResource(R.string.response_packet_junk_size),
hint =
stringResource(
R.string.response_packet_junk_size,
).lowercase(),
modifier =
Modifier
.fillMaxWidth(),
)
ConfigurationTextBox(
value = uiState.interfaceProxy.initPacketMagicHeader,
onValueChange = viewModel::onInitPacketMagicHeader,
keyboardActions = keyboardActions,
label = stringResource(R.string.init_packet_magic_header),
hint =
stringResource(
R.string.init_packet_magic_header,
).lowercase(),
modifier =
Modifier
.fillMaxWidth(),
)
ConfigurationTextBox(
value = uiState.interfaceProxy.responsePacketMagicHeader,
onValueChange = viewModel::onResponsePacketMagicHeader,
keyboardActions = keyboardActions,
label = stringResource(R.string.response_packet_magic_header),
hint =
stringResource(
R.string.response_packet_magic_header,
).lowercase(),
modifier =
Modifier
.fillMaxWidth(),
)
ConfigurationTextBox(
value = uiState.interfaceProxy.underloadPacketMagicHeader,
onValueChange = viewModel::onUnderloadPacketMagicHeader,
keyboardActions = keyboardActions,
label = stringResource(R.string.underload_packet_magic_header),
hint =
stringResource(
R.string.underload_packet_magic_header,
).lowercase(),
modifier =
Modifier
.fillMaxWidth(),
)
ConfigurationTextBox(
value = uiState.interfaceProxy.transportPacketMagicHeader,
onValueChange = viewModel::onTransportPacketMagicHeader,
keyboardActions = keyboardActions,
label = stringResource(R.string.transport_packet_magic_header),
hint =
stringResource(
R.string.transport_packet_magic_header,
).lowercase(),
modifier =
Modifier
.fillMaxWidth(),
)
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier =
Modifier
.fillMaxSize()
.padding(top = 5.dp),
horizontalArrangement = Arrangement.Center,
) {
TextButton(onClick = { showApplicationsDialog = true }) {
Text(applicationButtonText.text)
}
}
}
}
uiState.proxyPeers.forEachIndexed { index, peer ->
Surface(
tonalElevation = 2.dp,
shadowElevation = 2.dp,
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.surface,
modifier =
(
if (context.isRunningOnTv()) {
Modifier
.fillMaxHeight(fillMaxHeight)
.fillMaxWidth(fillMaxWidth)
} else {
Modifier.fillMaxWidth(fillMaxWidth)
}
)
.padding(top = 10.dp, bottom = 10.dp),
) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top,
modifier =
Modifier
.padding(horizontal = 15.dp)
.padding(bottom = 10.dp),
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier =
Modifier
.fillMaxWidth()
.padding(horizontal = 5.dp),
) {
SectionTitle(
stringResource(R.string.peer),
padding = screenPadding,
)
IconButton(onClick = { viewModel.onDeletePeer(index) }) {
val icon = Icons.Rounded.Delete
Icon(icon, icon.name)
}
}
ConfigurationTextBox(
value = peer.publicKey,
onValueChange = { value ->
viewModel.onPeerPublicKeyChange(index, value)
},
keyboardActions = keyboardActions,
label = stringResource(R.string.public_key),
hint = stringResource(R.string.base64_key),
modifier = Modifier.fillMaxWidth(),
)
ConfigurationTextBox(
value = peer.preSharedKey,
onValueChange = { value ->
viewModel.onPreSharedKeyChange(index, value)
},
keyboardActions = keyboardActions,
label = stringResource(R.string.preshared_key),
hint = stringResource(R.string.optional),
modifier = Modifier.fillMaxWidth(),
)
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = peer.persistentKeepalive,
enabled = true,
onValueChange = { value ->
viewModel.onPersistentKeepaliveChanged(index, value)
},
trailingIcon = {
Text(
stringResource(R.string.seconds),
modifier = Modifier.padding(end = 10.dp),
)
},
label = { Text(stringResource(R.string.persistent_keepalive)) },
singleLine = true,
placeholder = {
Text(stringResource(R.string.optional_no_recommend))
},
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
)
ConfigurationTextBox(
value = peer.endpoint,
onValueChange = { value ->
viewModel.onEndpointChange(index, value)
},
keyboardActions = keyboardActions,
label = stringResource(R.string.endpoint),
hint = stringResource(R.string.endpoint).lowercase(),
modifier = Modifier.fillMaxWidth(),
)
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = peer.allowedIps,
enabled = true,
onValueChange = { value ->
viewModel.onAllowedIpsChange(index, value)
},
label = { Text(stringResource(R.string.allowed_ips)) },
singleLine = true,
placeholder = {
Text(stringResource(R.string.comma_separated_list))
},
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
)
}
}
}
Row(
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically,
modifier =
Modifier
.fillMaxSize()
.padding(bottom = 140.dp),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) {
TextButton(onClick = { viewModel.addEmptyPeer() }) {
Text(stringResource(R.string.add_peer))
}
}
}
}
}
}
}
@@ -0,0 +1,81 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.config
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.extensions.Packages
data class ConfigUiState(
val proxyPeers: List<PeerProxy> = arrayListOf(PeerProxy()),
val interfaceProxy: InterfaceProxy = InterfaceProxy(),
val packages: Packages = emptyList(),
val checkedPackageNames: List<String> = emptyList(),
val include: Boolean = true,
val isAllApplicationsEnabled: Boolean = false,
val loading: Boolean = true,
val tunnel: TunnelConfig? = null,
var 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 {
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(tunnel: TunnelConfig): ConfigUiState {
val config = tunnel.toAmConfig()
return from(config).copy(
tunnelName = tunnel.name,
tunnel = tunnel,
isAmneziaEnabled = config.`interface`.junkPacketCount.isPresent,
)
}
}
}
@@ -0,0 +1,591 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.config
import android.Manifest
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.os.Build
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wireguard.config.Config
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.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.ui.screens.config.model.PeerProxy
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
import com.zaneschepke.wireguardautotunnel.util.StringValue
import com.zaneschepke.wireguardautotunnel.util.extensions.removeAt
import com.zaneschepke.wireguardautotunnel.util.extensions.update
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import timber.log.Timber
@HiltViewModel(assistedFactory = ConfigViewModel.ConfigViewModelFactory::class)
class ConfigViewModel
@AssistedInject
constructor(
private val appDataRepository: AppDataRepository,
@Assisted val id: Int,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : ViewModel() {
private val packageManager = WireGuardAutoTunnel.instance.packageManager
private val _saved = MutableSharedFlow<Boolean>()
val saved = _saved.asSharedFlow()
private val _uiState = MutableStateFlow(ConfigUiState())
val uiState = _uiState.onStart {
appDataRepository.tunnels.getById(id)?.let {
val packages = getQueriedPackages()
_uiState.value = ConfigUiState.from(it).copy(
packages = packages,
)
}
}.stateIn(
viewModelScope + ioDispatcher,
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
ConfigUiState(),
)
fun onTunnelNameChange(name: String) {
_uiState.update {
it.copy(tunnelName = name)
}
}
fun onIncludeChange(include: Boolean) {
_uiState.update {
it.copy(include = include)
}
}
fun cleanUpUninstalledApps() = viewModelScope.launch(ioDispatcher) {
uiState.value.tunnel?.let {
val config = it.toAmConfig()
val packages = getQueriedPackages()
val packageSet = packages.map { pack -> pack.packageName }.toSet()
val includedApps = config.`interface`.includedApplications.toMutableList()
val excludedApps = config.`interface`.excludedApplications.toMutableList()
if (includedApps.isEmpty() && excludedApps.isEmpty()) return@launch
if (includedApps.retainAll(packageSet) || excludedApps.retainAll(packageSet)) {
Timber.i("Removing split tunnel package name that no longer exists on the device")
_uiState.update { state ->
state.copy(
checkedPackageNames = if (_uiState.value.include) includedApps else excludedApps,
)
}
val wgQuick = buildConfig().toWgQuickString(true)
val amQuick = buildAmConfig().toAwgQuickString(true)
saveConfig(
it.copy(
amQuick = amQuick,
wgQuick = wgQuick,
),
)
}
}
}
fun onAddCheckedPackage(packageName: String) {
_uiState.update {
it.copy(
checkedPackageNames = it.checkedPackageNames + packageName,
)
}
}
fun onAllApplicationsChange(isAllApplicationsEnabled: Boolean) {
_uiState.update {
it.copy(isAllApplicationsEnabled = isAllApplicationsEnabled)
}
}
fun onRemoveCheckedPackage(packageName: String) {
_uiState.update {
it.copy(
checkedPackageNames = it.checkedPackageNames - packageName,
)
}
}
private fun getQueriedPackages(query: String = ""): List<PackageInfo> {
return getAllInternetCapablePackages().filter {
getPackageLabel(it).lowercase().contains(query.lowercase())
}
}
fun getPackageLabel(packageInfo: PackageInfo): String {
return packageInfo.applicationInfo?.loadLabel(packageManager).toString()
}
private fun getAllInternetCapablePackages(): List<PackageInfo> {
return getPackagesHoldingPermissions(arrayOf(Manifest.permission.INTERNET))
}
private fun getPackagesHoldingPermissions(permissions: Array<String>): List<PackageInfo> {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
packageManager.getPackagesHoldingPermissions(
permissions,
PackageManager.PackageInfoFlags.of(0L),
)
} else {
packageManager.getPackagesHoldingPermissions(permissions, 0)
}
}
private fun isAllApplicationsEnabled(): Boolean {
return _uiState.value.isAllApplicationsEnabled
}
private fun saveConfig(tunnelConfig: TunnelConfig) = viewModelScope.launch {
appDataRepository.tunnels.save(tunnelConfig)
}
private fun updateTunnelConfig(tunnelConfig: TunnelConfig?) = viewModelScope.launch {
if (tunnelConfig != null) {
saveConfig(tunnelConfig).join()
}
}
private fun buildPeerListFromProxyPeers(): List<Peer> {
return _uiState.value.proxyPeers.map {
val builder = Peer.Builder()
if (it.allowedIps.isNotEmpty()) builder.parseAllowedIPs(it.allowedIps.trim())
if (it.publicKey.isNotEmpty()) builder.parsePublicKey(it.publicKey.trim())
if (it.preSharedKey.isNotEmpty()) builder.parsePreSharedKey(it.preSharedKey.trim())
if (it.endpoint.isNotEmpty()) builder.parseEndpoint(it.endpoint.trim())
if (it.persistentKeepalive.isNotEmpty()) {
builder.parsePersistentKeepalive(it.persistentKeepalive.trim())
}
builder.build()
}
}
private fun 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.update {
it.copy(checkedPackageNames = emptyList())
}
}
private fun buildInterfaceListFromProxyInterface(): Interface {
val builder = Interface.Builder()
with(_uiState.value.interfaceProxy) {
builder.parsePrivateKey(this.privateKey.trim())
builder.parseAddresses(this.addresses.trim())
if (this.dnsServers.isNotEmpty()) {
builder.parseDnsServers(this.dnsServers.trim())
}
if (this.mtu.isNotEmpty()) {
builder.parseMtu(this.mtu.trim())
}
if (this.listenPort.isNotEmpty()) {
builder.parseListenPort(this.listenPort.trim())
}
if (isAllApplicationsEnabled()) emptyCheckedPackagesList()
if (_uiState.value.include) {
builder.includeApplications(
_uiState.value.checkedPackageNames,
)
}
if (!_uiState.value.include) {
builder.excludeApplications(
_uiState.value.checkedPackageNames,
)
}
}
return builder.build()
}
private fun buildAmInterfaceListFromProxyInterface(): org.amnezia.awg.config.Interface {
val builder = org.amnezia.awg.config.Interface.Builder()
with(_uiState.value.interfaceProxy) {
builder.parsePrivateKey(this.privateKey.trim())
builder.parseAddresses(this.addresses.trim())
if (this.dnsServers.isNotEmpty()) {
builder.parseDnsServers(this.dnsServers.trim())
}
if (this.mtu.isNotEmpty()) {
builder.parseMtu(this.mtu.trim())
}
if (this.listenPort.isNotEmpty()) {
builder.parseListenPort(this.listenPort.trim())
}
if (isAllApplicationsEnabled()) emptyCheckedPackagesList()
if (_uiState.value.include) {
builder.includeApplications(
_uiState.value.checkedPackageNames,
)
}
if (!_uiState.value.include) {
builder.excludeApplications(
_uiState.value.checkedPackageNames,
)
}
if (this.junkPacketCount.isNotEmpty()) {
builder.setJunkPacketCount(
this.junkPacketCount.trim().toInt(),
)
}
if (this.junkPacketMinSize.isNotEmpty()) {
builder.setJunkPacketMinSize(
this.junkPacketMinSize.trim().toInt(),
)
}
if (this.junkPacketMaxSize.isNotEmpty()) {
builder.setJunkPacketMaxSize(
this.junkPacketMaxSize.trim().toInt(),
)
}
if (this.initPacketJunkSize.isNotEmpty()) {
builder.setInitPacketJunkSize(
this.initPacketJunkSize.trim().toInt(),
)
}
if (this.responsePacketJunkSize.isNotEmpty()) {
builder.setResponsePacketJunkSize(
this.responsePacketJunkSize.trim().toInt(),
)
}
if (this.initPacketMagicHeader.isNotEmpty()) {
builder.setInitPacketMagicHeader(
this.initPacketMagicHeader.trim().toLong(),
)
}
if (this.responsePacketMagicHeader.isNotEmpty()) {
builder.setResponsePacketMagicHeader(
this.responsePacketMagicHeader.trim().toLong(),
)
}
if (this.transportPacketMagicHeader.isNotEmpty()) {
builder.setTransportPacketMagicHeader(
this.transportPacketMagicHeader.trim().toLong(),
)
}
if (this.underloadPacketMagicHeader.isNotEmpty()) {
builder.setUnderloadPacketMagicHeader(
this.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() = viewModelScope.launch {
kotlin.runCatching {
val wgQuick = buildConfig().toWgQuickString(true)
val amQuick = buildAmConfig().toAwgQuickString(true)
val tunnelConfig = uiState.value.tunnel?.copy(
name = _uiState.value.tunnelName,
amQuick = amQuick,
wgQuick = wgQuick,
) ?: TunnelConfig(
name = _uiState.value.tunnelName,
wgQuick = wgQuick,
amQuick = amQuick,
)
updateTunnelConfig(tunnelConfig)
SnackbarController.showMessage(
StringValue.StringResource(R.string.config_changes_saved),
)
_saved.emit(true)
}.onFailure {
Timber.e(it)
val message = it.message?.substringAfter(":", missingDelimiterValue = "")
val stringValue = if (message.isNullOrBlank()) {
StringValue.StringResource(R.string.unknown_error)
} else {
StringValue.DynamicString(message)
}
SnackbarController.showMessage(stringValue)
}
}
fun onPeerPublicKeyChange(index: Int, value: String) {
_uiState.update {
it.copy(
proxyPeers =
_uiState.value.proxyPeers.update(
index,
_uiState.value.proxyPeers[index].copy(publicKey = value),
),
)
}
}
fun onPreSharedKeyChange(index: Int, value: String) {
_uiState.update {
it.copy(
proxyPeers =
_uiState.value.proxyPeers.update(
index,
_uiState.value.proxyPeers[index].copy(preSharedKey = value),
),
)
}
}
fun onEndpointChange(index: Int, value: String) {
_uiState.update {
it.copy(
proxyPeers =
_uiState.value.proxyPeers.update(
index,
_uiState.value.proxyPeers[index].copy(endpoint = value),
),
)
}
}
fun onAllowedIpsChange(index: Int, value: String) {
_uiState.update {
it.copy(
proxyPeers =
_uiState.value.proxyPeers.update(
index,
_uiState.value.proxyPeers[index].copy(allowedIps = value),
),
)
}
}
fun onPersistentKeepaliveChanged(index: Int, value: String) {
_uiState.update {
it.copy(
proxyPeers =
_uiState.value.proxyPeers.update(
index,
_uiState.value.proxyPeers[index].copy(persistentKeepalive = value),
),
)
}
}
fun onDeletePeer(index: Int) {
_uiState.update {
it.copy(
proxyPeers = _uiState.value.proxyPeers.removeAt(index),
)
}
}
fun addEmptyPeer() {
_uiState.update {
it.copy(proxyPeers = _uiState.value.proxyPeers + PeerProxy())
}
}
fun generateKeyPair() {
val keyPair = KeyPair()
_uiState.update {
it.copy(
interfaceProxy =
it.interfaceProxy.copy(
privateKey = keyPair.privateKey.toBase64(),
publicKey = keyPair.publicKey.toBase64(),
),
)
}
}
fun onAddressesChanged(value: String) {
_uiState.update {
it.copy(
interfaceProxy = it.interfaceProxy.copy(addresses = value),
)
}
}
fun onListenPortChanged(value: String) {
_uiState.update {
it.copy(
interfaceProxy = it.interfaceProxy.copy(listenPort = value),
)
}
}
fun onDnsServersChanged(value: String) {
_uiState.update {
it.copy(
interfaceProxy = it.interfaceProxy.copy(dnsServers = value),
)
}
}
fun onMtuChanged(value: String) {
_uiState.update {
it.copy(interfaceProxy = it.interfaceProxy.copy(mtu = value))
}
}
private fun onInterfacePublicKeyChange(value: String) {
_uiState.update {
it.copy(
interfaceProxy = it.interfaceProxy.copy(publicKey = value),
)
}
}
fun onPrivateKeyChange(value: String) {
_uiState.update {
it.copy(
interfaceProxy = it.interfaceProxy.copy(privateKey = value),
)
}
if (NumberUtils.isValidKey(value)) {
val pair = KeyPair(Key.fromBase64(value))
onInterfacePublicKeyChange(pair.publicKey.toBase64())
} else {
onInterfacePublicKeyChange("")
}
}
fun emitQueriedPackages(query: String) {
val packages =
getAllInternetCapablePackages().filter {
getPackageLabel(it).lowercase().contains(query.lowercase())
}
_uiState.update { it.copy(packages = packages) }
}
fun onJunkPacketCountChanged(value: String) {
_uiState.update {
it.copy(
interfaceProxy = it.interfaceProxy.copy(junkPacketCount = value),
)
}
}
fun onJunkPacketMinSizeChanged(value: String) {
_uiState.update {
it.copy(
interfaceProxy = it.interfaceProxy.copy(junkPacketMinSize = value),
)
}
}
fun onJunkPacketMaxSizeChanged(value: String) {
_uiState.update {
it.copy(
interfaceProxy = it.interfaceProxy.copy(junkPacketMaxSize = value),
)
}
}
fun onInitPacketJunkSizeChanged(value: String) {
_uiState.update {
it.copy(
interfaceProxy = it.interfaceProxy.copy(initPacketJunkSize = value),
)
}
}
fun onResponsePacketJunkSize(value: String) {
_uiState.update {
it.copy(
interfaceProxy =
it.interfaceProxy.copy(
responsePacketJunkSize = value,
),
)
}
}
fun onInitPacketMagicHeader(value: String) {
_uiState.update {
it.copy(
interfaceProxy =
it.interfaceProxy.copy(
initPacketMagicHeader = value,
),
)
}
}
fun onResponsePacketMagicHeader(value: String) {
_uiState.update {
it.copy(
interfaceProxy =
it.interfaceProxy.copy(
responsePacketMagicHeader = value,
),
)
}
}
fun onTransportPacketMagicHeader(value: String) {
_uiState.update {
it.copy(
interfaceProxy =
it.interfaceProxy.copy(
transportPacketMagicHeader = value,
),
)
}
}
fun onUnderloadPacketMagicHeader(value: String) {
_uiState.update {
it.copy(
interfaceProxy =
it.interfaceProxy.copy(
underloadPacketMagicHeader = value,
),
)
}
}
@AssistedFactory
interface ConfigViewModelFactory {
fun create(id: Int): ConfigViewModel
}
}
@@ -0,0 +1,199 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.config.components
import android.content.pm.PackageInfo
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Android
import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.Checkbox
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.google.accompanist.drawablepainter.DrawablePainter
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.SearchBar
import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigUiState
import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ApplicationSelectionDialog(viewModel: ConfigViewModel, uiState: ConfigUiState, onDismiss: () -> Unit) {
val context = LocalContext.current
val licenseComparator = compareBy<PackageInfo> { viewModel.getPackageLabel(it) }
val sortedPackages = remember(uiState.packages, licenseComparator) {
uiState.packages.sortedWith(licenseComparator)
}
BasicAlertDialog(
onDismissRequest = { onDismiss() },
) {
Surface(
tonalElevation = 2.dp,
shadowElevation = 2.dp,
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.surface,
modifier =
Modifier
.fillMaxWidth()
.fillMaxHeight(if (uiState.isAllApplicationsEnabled) 1 / 5f else 4 / 5f),
) {
Column(
modifier =
Modifier
.fillMaxWidth(),
) {
Row(
modifier =
Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp, vertical = 7.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(stringResource(id = R.string.tunnel_all))
Switch(
checked = uiState.isAllApplicationsEnabled,
onCheckedChange = viewModel::onAllApplicationsChange,
)
}
if (!uiState.isAllApplicationsEnabled) {
Row(
modifier =
Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp, vertical = 7.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(stringResource(id = R.string.include))
Checkbox(
checked = uiState.include,
onCheckedChange = {
viewModel.onIncludeChange(!uiState.include)
},
)
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(stringResource(id = R.string.exclude))
Checkbox(
checked = !uiState.include,
onCheckedChange = {
viewModel.onIncludeChange(!uiState.include)
},
)
}
}
Row(
modifier =
Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp, vertical = 7.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
SearchBar(viewModel::emitQueriedPackages)
}
Spacer(Modifier.padding(5.dp))
LazyColumn(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top,
modifier = Modifier.fillMaxHeight(19 / 22f),
) {
items(sortedPackages, key = { it.packageName }) { pack ->
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier =
Modifier
.fillMaxSize()
.padding(5.dp).padding(end = 25.dp),
) {
Row(modifier = Modifier.fillMaxWidth().padding(start = 5.dp)) {
val drawable =
pack.applicationInfo?.loadIcon(context.packageManager)
val iconSize = 35.dp
if (drawable != null) {
Image(
painter = DrawablePainter(drawable),
stringResource(id = R.string.icon),
modifier = Modifier.size(iconSize),
)
} else {
val icon = Icons.Rounded.Android
Icon(
icon,
icon.name,
modifier = Modifier.size(iconSize),
)
}
Text(
viewModel.getPackageLabel(pack),
modifier = Modifier.padding(5.dp),
)
}
Checkbox(
modifier = Modifier.fillMaxSize(),
checked =
(
uiState.checkedPackageNames.contains(
pack.packageName,
)
),
onCheckedChange = {
if (it) {
viewModel.onAddCheckedPackage(pack.packageName)
} else {
viewModel.onRemoveCheckedPackage(pack.packageName)
}
},
)
}
}
}
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier =
Modifier
.fillMaxSize()
.padding(top = 5.dp),
horizontalArrangement = Arrangement.Center,
) {
TextButton(onClick = { onDismiss() }) {
Text(stringResource(R.string.done))
}
}
}
}
}
}
@@ -0,0 +1,121 @@
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 = listOf(
i.dnsServers.joinToString(", ").replace("/", "").trim(),
i.dnsSearchDomains.joinToString(", ").trim(),
).filter { it.length > 0 }.joinToString(", "),
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 + i.dnsSearchDomains).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 {
""
},
)
}
}
}
@@ -1,50 +1,14 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.tunneloptions.config.model
package com.zaneschepke.wireguardautotunnel.ui.screens.config.model
import com.wireguard.config.Peer
import com.zaneschepke.wireguardautotunnel.util.extensions.joinAndTrim
data class PeerProxy(
val publicKey: String = "",
val preSharedKey: String = "",
val persistentKeepalive: String = "",
val endpoint: String = "",
val allowedIps: String = ALL_IPS.joinAndTrim(),
val allowedIps: String = IPV4_WILDCARD.joinToString(", ").trim(),
) {
fun toWgPeer(): Peer {
return Peer.Builder().apply {
parsePublicKey(publicKey)
if (preSharedKey.isNotBlank()) parsePreSharedKey(preSharedKey)
if (persistentKeepalive.isNotBlank()) parsePersistentKeepalive(persistentKeepalive)
parseEndpoint(endpoint)
parseAllowedIPs(allowedIps)
}.build()
}
fun toAmPeer(): org.amnezia.awg.config.Peer {
return org.amnezia.awg.config.Peer.Builder().apply {
parsePublicKey(publicKey)
if (preSharedKey.isNotBlank()) parsePreSharedKey(preSharedKey)
if (persistentKeepalive.isNotBlank()) parsePersistentKeepalive(persistentKeepalive)
parseEndpoint(endpoint)
parseAllowedIPs(allowedIps)
}.build()
}
fun isLanExcluded(): Boolean {
return this.allowedIps.contains(IPV4_PUBLIC_NETWORKS.joinAndTrim())
}
fun includeLan(): PeerProxy {
return this.copy(
allowedIps = ALL_IPS.joinAndTrim(),
)
}
fun excludeLan(): PeerProxy {
return this.copy(
allowedIps = IPV4_PUBLIC_NETWORKS.joinAndTrim(),
)
}
companion object {
fun from(peer: Peer): PeerProxy {
return PeerProxy(
@@ -129,6 +93,6 @@ data class PeerProxy(
"200.0.0.0/5",
"208.0.0.0/4",
)
val ALL_IPS = setOf("0.0.0.0/0", "::/0")
val IPV4_WILDCARD = setOf("0.0.0.0/0")
}
}
@@ -35,6 +35,7 @@ import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.os.ConfigurationCompat
import androidx.hilt.navigation.compose.hiltViewModel
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
@@ -58,7 +59,6 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import java.text.Collator
import java.util.Locale
@OptIn(ExperimentalFoundationApi::class)
@Composable
@@ -74,7 +74,8 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) }
val isRunningOnTv = remember { context.isRunningOnTv() }
val collator = Collator.getInstance(Locale.getDefault())
val currentLocale = ConfigurationCompat.getLocales(context.resources.configuration)[0]
val collator = Collator.getInstance(currentLocale)
val sortedTunnels = remember(uiState.tunnels) {
uiState.tunnels.sortedWith(compareBy(collator) { it.name })
@@ -82,7 +83,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
val startAutoTunnel = withVpnPermission<Unit> { viewModel.onToggleAutoTunnel() }
val startTunnel = withVpnPermission<TunnelConfig> {
viewModel.onTunnelStart(it)
viewModel.onTunnelStart(it, uiState.settings.isKernelEnabled)
}
val autoTunnelToggleBattery = withIgnoreBatteryOpt(uiState.generalState.isBatteryOptimizationDisableShown) {
if (!uiState.generalState.isBatteryOptimizationDisableShown) viewModel.setBatteryOptimizeDisableShown()
@@ -129,7 +130,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) {
if (!checked) viewModel.onTunnelStop().also { return }
if (uiState.settings.isKernelEnabled) {
viewModel.onTunnelStart(tunnel)
viewModel.onTunnelStart(tunnel, uiState.settings.isKernelEnabled)
} else {
startTunnel.invoke(tunnel)
}
@@ -198,10 +199,10 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
)
LazyColumn(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(5.dp, Alignment.Top),
verticalArrangement = Arrangement.spacedBy(5.dp.scaledHeight(), Alignment.Top),
modifier =
Modifier
.fillMaxSize().padding(padding).padding(top = 24.dp.scaledHeight())
.fillMaxSize().padding(padding)
.overscroll(ScrollableDefaults.overscrollEffect())
.nestedScroll(nestedScrollConnection),
state = rememberLazyListState(0, uiState.tunnels.count()),
@@ -226,9 +227,8 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
) { tunnel ->
val expanded = uiState.generalState.isTunnelStatsExpanded
TunnelRowItem(
tunnel.id == uiState.vpnState.tunnelConfig?.id && (
uiState.vpnState.status.isUp() || (uiState.settings.isKernelEnabled && tunnel.isActive)
),
tunnel.id == uiState.vpnState.tunnelConfig?.id &&
uiState.vpnState.status.isUp(),
expanded,
selectedTunnel?.id == tunnel.id,
tunnel,
@@ -67,9 +67,9 @@ constructor(
appDataRepository.appState.setTunnelStatsExpanded(expanded)
}
fun onTunnelStart(tunnelConfig: TunnelConfig) = viewModelScope.launch {
fun onTunnelStart(tunnelConfig: TunnelConfig, background: Boolean) = viewModelScope.launch {
Timber.i("Starting tunnel ${tunnelConfig.name}")
tunnelService.get().startTunnel(tunnelConfig)
tunnelService.get().startTunnel(tunnelConfig, background)
}
fun onTunnelStop() = viewModelScope.launch {
@@ -86,6 +86,20 @@ constructor(
}
}
private fun generateQrCodeTunnelName(config: String): String {
var defaultName = generateQrCodeDefaultName(config)
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
}
private suspend fun makeTunnelNameUnique(name: String): String {
return withContext(ioDispatcher) {
val tunnels = appDataRepository.tunnels.getAll()
@@ -95,7 +95,7 @@ fun TunnelRowItem(
IconButton(
onClick = {
navController.navigate(
Route.TunnelOptions(tunnel.id),
Route.Option(tunnel.id),
)
},
) {
@@ -128,7 +128,7 @@ fun TunnelRowItem(
onClick = {
onHold()
navController.navigate(
Route.TunnelOptions(tunnel.id),
Route.Option(tunnel.id),
)
},
) {
@@ -21,21 +21,22 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.toThreeDecimalPlaceSt
@Composable
fun TunnelStatisticsRow(statistics: TunnelStatistics?, tunnelConfig: TunnelConfig) {
val config = TunnelConfig.configFromAmQuick(tunnelConfig.wgQuick)
config.peers.forEach { peer ->
Row(
modifier =
Modifier
.fillMaxWidth()
.padding(end = 10.dp, bottom = 10.dp, start = 45.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(5.dp, Alignment.Start),
) {
val peerId = peer.publicKey.toBase64().subSequence(0, 3).toString() + "***"
val peerRx = statistics?.peerStats(peer.publicKey)?.rxBytes ?: 0
val peerTx = statistics?.peerStats(peer.publicKey)?.txBytes ?: 0
Row(
modifier =
Modifier
.fillMaxWidth()
.padding(end = 10.dp, bottom = 10.dp, start = 45.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(30.dp, Alignment.Start),
) {
config.peers.forEach {
val peerId = it.publicKey.toBase64().subSequence(0, 3).toString() + "***"
val peerRx = statistics?.peerStats(it.publicKey)?.rxBytes ?: 0
val peerTx = statistics?.peerStats(it.publicKey)?.txBytes ?: 0
val peerTxMB = NumberUtils.bytesToMB(peerTx).toThreeDecimalPlaceString()
val peerRxMB = NumberUtils.bytesToMB(peerRx).toThreeDecimalPlaceString()
val handshake = statistics?.peerStats(peer.publicKey)?.latestHandshakeEpochMillis?.let {
val handshake = statistics?.peerStats(it.publicKey)?.latestHandshakeEpochMillis?.let {
if (it == 0L) {
stringResource(R.string.never)
} else {
@@ -55,11 +56,7 @@ fun TunnelStatisticsRow(statistics: TunnelStatistics?, tunnelConfig: TunnelConfi
Column(
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
Text(
stringResource(R.string.handshake) + ": $handshake",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.outline,
)
Text(stringResource(R.string.handshake) + ": $handshake", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.outline)
Text("rx: $peerRxMB MB", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.outline)
}
}
@@ -0,0 +1,290 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.options
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material.icons.outlined.NetworkPing
import androidx.compose.material.icons.outlined.PhoneAndroid
import androidx.compose.material.icons.outlined.Security
import androidx.compose.material.icons.outlined.SettingsEthernet
import androidx.compose.material.icons.outlined.Star
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.AppUiState
import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.common.config.SubmitConfigurationTextBox
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.components.TrustedNetworkTextBox
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.components.WildcardsLabel
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.isValidIpv4orIpv6Address
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
@Composable
fun OptionsScreen(optionsViewModel: OptionsViewModel = hiltViewModel(), appUiState: AppUiState, tunnelId: Int) {
val navController = LocalNavController.current
val config = appUiState.tunnels.first { it.id == tunnelId }
var currentText by remember { mutableStateOf("") }
LaunchedEffect(config.tunnelNetworks) {
currentText = ""
}
Scaffold(
topBar = {
TopNavBar(config.name, trailing = {
IconButton(onClick = {
navController.navigate(
Route.Config(config.id),
)
}) {
val icon = Icons.Outlined.Edit
Icon(
imageVector = icon,
contentDescription = icon.name,
)
}
})
},
) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top),
modifier =
Modifier
.fillMaxSize()
.padding(it)
.verticalScroll(rememberScrollState())
.padding(top = 24.dp.scaledHeight())
.padding(horizontal = 24.dp.scaledWidth()),
) {
GroupLabel(stringResource(R.string.auto_tunneling))
SurfaceSelectionGroupButton(
buildList {
addAll(
listOf(
SelectionItem(
Icons.Outlined.Star,
title = {
Text(
stringResource(R.string.primary_tunnel),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
description = {
Text(
stringResource(R.string.set_primary_tunnel),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.outline),
)
},
trailing = {
ScaledSwitch(
config.isPrimaryTunnel,
onClick = { optionsViewModel.onTogglePrimaryTunnel(config) },
)
},
onClick = { optionsViewModel.onTogglePrimaryTunnel(config) },
),
SelectionItem(
Icons.Outlined.PhoneAndroid,
title = {
Text(
stringResource(R.string.mobile_tunnel),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
description = {
Text(
stringResource(R.string.mobile_data_tunnel),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.outline),
)
},
trailing = {
ScaledSwitch(
config.isMobileDataTunnel,
onClick = { optionsViewModel.onToggleIsMobileDataTunnel(config) },
)
},
onClick = { optionsViewModel.onToggleIsMobileDataTunnel(config) },
),
SelectionItem(
Icons.Outlined.SettingsEthernet,
title = {
Text(
stringResource(R.string.ethernet_tunnel),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
description = {
Text(
stringResource(R.string.set_ethernet_tunnel),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.outline),
)
},
trailing = {
ScaledSwitch(
config.isEthernetTunnel,
onClick = { optionsViewModel.onToggleIsEthernetTunnel(config) },
)
},
onClick = { optionsViewModel.onToggleIsEthernetTunnel(config) },
),
SelectionItem(
Icons.Outlined.NetworkPing,
title = {
Text(
stringResource(R.string.restart_on_ping),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = {
ScaledSwitch(
checked = config.isPingEnabled,
onClick = { optionsViewModel.onToggleRestartOnPing(config) },
)
},
onClick = { optionsViewModel.onToggleRestartOnPing(config) },
),
),
)
if (config.isPingEnabled || appUiState.settings.isPingEnabled) {
add(
SelectionItem(
title = {},
description = {
SubmitConfigurationTextBox(
config.pingIp,
stringResource(R.string.set_custom_ping_ip),
stringResource(R.string.default_ping_ip),
isErrorValue = { !it.isNullOrBlank() && !it.isValidIpv4orIpv6Address() },
onSubmit = {
optionsViewModel.saveTunnelChanges(
config.copy(pingIp = it.ifBlank { null }),
)
},
)
fun isSecondsError(seconds: String?): Boolean {
return seconds?.let { value -> if (value.isBlank()) false else value.toLong() >= Long.MAX_VALUE / 1000 } ?: false
}
SubmitConfigurationTextBox(
config.pingInterval?.let { (it / 1000).toString() },
stringResource(R.string.set_custom_ping_internal),
"(${stringResource(R.string.optional_default)} ${Constants.PING_INTERVAL / 1000})",
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Done,
),
isErrorValue = ::isSecondsError,
onSubmit = {
optionsViewModel.saveTunnelChanges(
config.copy(pingInterval = if (it.isBlank()) null else it.toLong() * 1000),
)
},
)
SubmitConfigurationTextBox(
config.pingCooldown?.let { (it / 1000).toString() },
stringResource(R.string.set_custom_ping_cooldown),
"(${stringResource(R.string.optional_default)} ${Constants.PING_COOLDOWN / 1000})",
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
),
isErrorValue = ::isSecondsError,
onSubmit = {
optionsViewModel.saveTunnelChanges(
config.copy(pingCooldown = if (it.isBlank()) null else it.toLong() * 1000),
)
},
)
},
),
)
}
add(
SelectionItem(
title = {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp.scaledHeight()),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.weight(4f, false)
.fillMaxWidth(),
) {
val icon = Icons.Outlined.Security
Icon(
icon,
icon.name,
modifier = Modifier.size(iconSize),
)
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically),
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp.scaledWidth())
.padding(vertical = 6.dp.scaledHeight()),
) {
Text(
stringResource(R.string.use_tunnel_on_wifi_name),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
}
}
}
},
description = {
TrustedNetworkTextBox(
config.tunnelNetworks,
onDelete = { optionsViewModel.onDeleteRunSSID(it, config) },
currentText = currentText,
onSave = { optionsViewModel.onSaveRunSSID(it, config) },
onValueChange = { currentText = it },
supporting = {
if (appUiState.settings.isWildcardsEnabled) {
WildcardsLabel()
}
},
)
},
),
)
},
)
}
}
}
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.tunneloptions.tunnelautotunnel
package com.zaneschepke.wireguardautotunnel.ui.screens.options
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
@@ -12,7 +12,7 @@ import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class TunnelAutoTunnelViewModel
class OptionsViewModel
@Inject
constructor(
private val appDataRepository: AppDataRepository,
@@ -61,6 +61,23 @@ constructor(
}
}
fun onTogglePrimaryTunnel(tunnelConfig: TunnelConfig) = viewModelScope.launch {
appDataRepository.tunnels.updatePrimaryTunnel(
when (tunnelConfig.isPrimaryTunnel) {
true -> null
false -> tunnelConfig
},
)
}
fun onToggleRestartOnPing(tunnelConfig: TunnelConfig) = viewModelScope.launch {
appDataRepository.tunnels.save(
tunnelConfig.copy(
isPingEnabled = !tunnelConfig.isPingEnabled,
),
)
}
fun onToggleIsEthernetTunnel(tunnelConfig: TunnelConfig) = viewModelScope.launch {
if (tunnelConfig.isEthernetTunnel) {
appDataRepository.tunnels.updateEthernetTunnel(null)
@@ -5,11 +5,8 @@ import androidx.compose.foundation.focusable
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.fillMaxSize
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
@@ -49,6 +46,7 @@ import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavControll
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.ForwardButton
import com.zaneschepke.wireguardautotunnel.ui.theme.topPadding
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import com.zaneschepke.wireguardautotunnel.util.extensions.launchNotificationSettings
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
@@ -56,7 +54,6 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
import xyz.teamgravity.pin_lock_compose.PinManager
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel: AppViewModel, uiState: AppUiState) {
val context = LocalContext.current
@@ -95,8 +92,8 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
modifier =
Modifier
.verticalScroll(rememberScrollState())
.fillMaxSize().systemBarsPadding().imePadding()
.padding(top = 24.dp.scaledHeight())
.fillMaxSize()
.padding(top = topPadding)
.padding(bottom = 40.dp.scaledHeight())
.padding(horizontal = 24.dp.scaledWidth())
.then(
@@ -112,13 +109,6 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
},
),
) {
val onAutoTunnelClick = {
if (!uiState.generalState.isLocationDisclosureShown) {
navController.navigate(Route.LocationDisclosure)
} else {
navController.navigate(Route.AutoTunnel)
}
}
SurfaceSelectionGroupButton(
listOf(
SelectionItem(
@@ -131,10 +121,11 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
)
},
onClick = {
onAutoTunnelClick()
if (!uiState.generalState.isLocationDisclosureShown) return@SelectionItem navController.navigate(Route.LocationDisclosure)
navController.navigate(Route.AutoTunnel)
},
trailing = {
ForwardButton(Modifier.focusable()) { onAutoTunnelClick() }
ForwardButton(Modifier.focusable()) { navController.navigate(Route.AutoTunnel) }
},
),
),
@@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Contrast
import androidx.compose.material.icons.outlined.Notifications
import androidx.compose.material.icons.outlined.Translate
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
@@ -14,7 +13,6 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
@@ -24,14 +22,12 @@ import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelec
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.ForwardButton
import com.zaneschepke.wireguardautotunnel.util.extensions.launchNotificationSettings
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
@Composable
fun AppearanceScreen() {
val navController = LocalNavController.current
val context = LocalContext.current
Scaffold(
topBar = {
@@ -59,20 +55,6 @@ fun AppearanceScreen() {
),
),
)
SurfaceSelectionGroupButton(
listOf(
SelectionItem(
Icons.Outlined.Notifications,
title = { Text(stringResource(R.string.notifications), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
onClick = {
context.launchNotificationSettings()
},
trailing = {
ForwardButton { context.launchNotificationSettings() }
},
),
),
)
SurfaceSelectionGroupButton(
listOf(
SelectionItem(
@@ -60,10 +60,8 @@ fun LanguageScreen(appUiState: AppUiState, appViewModel: AppViewModel) {
appViewModel.onLocaleChange(LocaleUtil.OPTION_PHONE_LANGUAGE)
},
trailing = {
with(appUiState.generalState.locale) {
if (this == LocaleUtil.OPTION_PHONE_LANGUAGE || this == null) {
SelectedLabel()
}
if (appUiState.generalState.locale == LocaleUtil.OPTION_PHONE_LANGUAGE) {
SelectedLabel()
}
},
ripple = false,
@@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
@@ -17,8 +16,8 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.AirplanemodeActive
import androidx.compose.material.icons.outlined.Code
import androidx.compose.material.icons.outlined.Filter1
import androidx.compose.material.icons.outlined.NetworkPing
import androidx.compose.material.icons.outlined.Security
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.outlined.SettingsEthernet
import androidx.compose.material.icons.outlined.SignalCellular4Bar
import androidx.compose.material.icons.outlined.Wifi
@@ -43,16 +42,13 @@ import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.AppUiState
import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.components.TrustedNetworkTextBox
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.components.WildcardsLabel
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.BackgroundLocationDialog
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.ForwardButton
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.LearnMoreLinkLabel
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.LocationServicesDialog
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
@@ -66,7 +62,6 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
@Composable
fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltViewModel()) {
val context = LocalContext.current
val navController = LocalNavController.current
val fineLocationState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
var currentText by remember { mutableStateOf("") }
@@ -111,7 +106,7 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV
showLocationServicesAlertDialog,
onDismiss = { showLocationServicesAlertDialog = false },
onAttest = {
showLocationServicesAlertDialog = false
viewModel.onToggleTunnelOnWifi()
},
)
@@ -134,7 +129,6 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV
.fillMaxSize()
.padding(padding)
.verticalScroll(rememberScrollState())
.imePadding()
.padding(top = 24.dp.scaledHeight())
.padding(horizontal = 24.dp.scaledWidth()),
) {
@@ -315,6 +309,24 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV
viewModel.onToggleTunnelOnEthernet()
},
),
SelectionItem(
Icons.Outlined.NetworkPing,
title = {
Text(
stringResource(R.string.restart_on_ping),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = {
ScaledSwitch(
checked = uiState.settings.isPingEnabled,
onClick = { viewModel.onToggleRestartOnPing() },
)
},
onClick = {
viewModel.onToggleRestartOnPing()
},
),
SelectionItem(
Icons.Outlined.AirplanemodeActive,
title = {
@@ -341,25 +353,6 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV
),
),
)
SurfaceSelectionGroupButton(
listOf(
SelectionItem(
Icons.Outlined.Settings,
title = {
Text(
stringResource(R.string.advanced_settings),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
onClick = {
navController.navigate(Route.AutoTunnelAdvanced)
},
trailing = {
ForwardButton { navController.navigate(Route.AutoTunnelAdvanced) }
},
),
),
)
}
}
}
@@ -119,6 +119,16 @@ constructor(
}
}
fun onToggleRestartOnPing() = viewModelScope.launch {
with(settings.value) {
appDataRepository.settings.save(
copy(
isPingEnabled = !isPingEnabled,
),
)
}
}
fun onToggleStopOnNoInternet() = viewModelScope.launch {
with(settings.value) {
appDataRepository.settings.save(
@@ -1,121 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.advanced
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material.icons.outlined.PauseCircle
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.AppUiState
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
@Composable
fun AdvancedScreen(appUiState: AppUiState, appViewModel: AppViewModel) {
var isDropDownExpanded by remember {
mutableStateOf(false)
}
var selected by remember { mutableIntStateOf(appUiState.settings.debounceDelaySeconds) }
LaunchedEffect(selected) {
if (selected == appUiState.settings.debounceDelaySeconds) return@LaunchedEffect
appViewModel.saveSettings(appUiState.settings.copy(debounceDelaySeconds = selected))
if (appUiState.settings.isAutoTunnelEnabled) {
appViewModel.bounceAutoTunnel()
}
}
Scaffold(
topBar = {
TopNavBar(stringResource(R.string.advanced_settings))
},
) { padding ->
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top),
modifier =
Modifier
.fillMaxSize()
.padding(padding)
.verticalScroll(rememberScrollState())
.padding(top = 24.dp.scaledHeight())
.padding(horizontal = 24.dp.scaledWidth()),
) {
SurfaceSelectionGroupButton(
listOf(
SelectionItem(
Icons.Outlined.PauseCircle,
title = {
Text(
stringResource(R.string.debounce_delay),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
onClick = {
isDropDownExpanded = true
},
trailing = {
Row(
horizontalArrangement = Arrangement.spacedBy(5.dp, Alignment.CenterHorizontally),
verticalAlignment = Alignment.CenterVertically,
) {
Text(text = selected.toString(), style = MaterialTheme.typography.bodyMedium)
val icon = Icons.Default.ArrowDropDown
Icon(icon, icon.name)
}
DropdownMenu(
modifier = Modifier.height(250.dp.scaledHeight()),
scrollState = rememberScrollState(),
containerColor = MaterialTheme.colorScheme.surface,
expanded = isDropDownExpanded,
onDismissRequest = {
isDropDownExpanded = false
},
) {
(0..10).forEachIndexed { index, num ->
DropdownMenuItem(
text = {
Text(text = num.toString())
},
onClick = {
isDropDownExpanded = false
selected = num
},
)
}
}
},
),
),
)
}
}
}
@@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.LocationOn
import androidx.compose.material.icons.rounded.PermScanWifi
@@ -28,6 +27,7 @@ import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionIte
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.ForwardButton
import com.zaneschepke.wireguardautotunnel.ui.theme.topPadding
import com.zaneschepke.wireguardautotunnel.util.extensions.goFromRoot
import com.zaneschepke.wireguardautotunnel.util.extensions.launchAppSettings
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
@@ -46,8 +46,7 @@ fun LocationDisclosureScreen(appViewModel: AppViewModel, appUiState: AppUiState)
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top),
modifier =
Modifier.fillMaxSize().systemBarsPadding().padding(top = 24.dp.scaledHeight())
.padding(horizontal = 24.dp.scaledWidth()),
Modifier.fillMaxSize().padding(top = topPadding).padding(horizontal = 24.dp.scaledWidth()),
) {
val icon = Icons.Rounded.PermScanWifi
Icon(
@@ -4,7 +4,6 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
@@ -41,6 +40,7 @@ import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.common.label.VersionLabel
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.ForwardButton
import com.zaneschepke.wireguardautotunnel.ui.theme.topPadding
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import com.zaneschepke.wireguardautotunnel.util.extensions.launchSupportEmail
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
@@ -71,7 +71,7 @@ fun SupportScreen(appUiState: AppUiState, appViewModel: AppViewModel) {
modifier =
Modifier
.fillMaxSize()
.systemBarsPadding().padding(top = 24.dp.scaledHeight())
.padding(top = topPadding)
.verticalScroll(rememberScrollState())
.padding(horizontal = 24.dp.scaledWidth()),
) {
@@ -174,7 +174,7 @@ fun SupportScreen(appUiState: AppUiState, appViewModel: AppViewModel) {
title = {
Text(
stringResource(R.string.chat_description),
style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.onSurface),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = {
@@ -1,231 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.tunneloptions
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.CallSplit
import androidx.compose.material.icons.outlined.Bolt
import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material.icons.outlined.NetworkPing
import androidx.compose.material.icons.outlined.Star
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.common.config.SubmitConfigurationTextBox
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.ForwardButton
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.isValidIpv4orIpv6Address
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
import kotlin.text.isBlank
import kotlin.text.isNullOrBlank
import kotlin.text.toLong
@Composable
fun OptionsScreen(tunnelConfig: TunnelConfig, viewModel: TunnelOptionsViewModel = hiltViewModel()) {
val navController = LocalNavController.current
var currentText by remember { mutableStateOf("") }
LaunchedEffect(tunnelConfig.tunnelNetworks) {
currentText = ""
}
Scaffold(
topBar = {
TopNavBar(tunnelConfig.name)
},
) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top),
modifier =
Modifier
.fillMaxSize()
.padding(it)
.imePadding()
.verticalScroll(rememberScrollState())
.padding(top = 24.dp.scaledHeight())
.padding(horizontal = 24.dp.scaledWidth()),
) {
SurfaceSelectionGroupButton(
listOf(
SelectionItem(
Icons.Outlined.Star,
title = {
Text(
stringResource(R.string.primary_tunnel),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
description = {
Text(
stringResource(R.string.set_primary_tunnel),
style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline),
)
},
trailing = {
ScaledSwitch(
tunnelConfig.isPrimaryTunnel,
onClick = { viewModel.onTogglePrimaryTunnel(tunnelConfig) },
)
},
onClick = { viewModel.onTogglePrimaryTunnel(tunnelConfig) },
),
SelectionItem(
Icons.Outlined.Bolt,
title = {
Text(
stringResource(R.string.auto_tunneling),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
description = {
Text(
stringResource(R.string.tunnel_specific_settings),
style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline),
)
},
onClick = {
navController.navigate(Route.TunnelAutoTunnel(id = tunnelConfig.id))
},
trailing = {
ForwardButton { navController.navigate(Route.TunnelAutoTunnel(id = tunnelConfig.id)) }
},
),
SelectionItem(
Icons.Outlined.Edit,
title = {
Text(
stringResource(R.string.edit_tunnel),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
onClick = {
navController.navigate(Route.Config(id = tunnelConfig.id))
},
trailing = {
ForwardButton { navController.navigate(Route.Config(id = tunnelConfig.id)) }
},
),
SelectionItem(
Icons.AutoMirrored.Outlined.CallSplit,
title = {
Text(
stringResource(R.string.splt_tunneling),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
onClick = {
navController.navigate(Route.SplitTunnel(id = tunnelConfig.id))
},
trailing = {
ForwardButton { navController.navigate(Route.SplitTunnel(id = tunnelConfig.id)) }
},
),
),
)
SurfaceSelectionGroupButton(
buildList {
add(
SelectionItem(
Icons.Outlined.NetworkPing,
title = {
Text(
stringResource(R.string.restart_on_ping),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = {
ScaledSwitch(
checked = tunnelConfig.isPingEnabled,
onClick = { viewModel.onToggleRestartOnPing(tunnelConfig) },
)
},
onClick = { viewModel.onToggleRestartOnPing(tunnelConfig) },
),
)
if (tunnelConfig.isPingEnabled) {
add(
SelectionItem(
title = {},
description = {
SubmitConfigurationTextBox(
tunnelConfig.pingIp,
stringResource(R.string.set_custom_ping_ip),
stringResource(R.string.default_ping_ip),
isErrorValue = { !it.isNullOrBlank() && !it.isValidIpv4orIpv6Address() },
onSubmit = {
viewModel.saveTunnelChanges(
tunnelConfig.copy(pingIp = it.ifBlank { null }),
)
},
)
fun isSecondsError(seconds: String?): Boolean {
return seconds?.let { value -> if (value.isBlank()) false else value.toLong() >= Long.MAX_VALUE / 1000 }
?: false
}
SubmitConfigurationTextBox(
tunnelConfig.pingInterval?.let { (it / 1000).toString() },
stringResource(R.string.set_custom_ping_internal),
"(${stringResource(R.string.optional_default)} ${Constants.PING_INTERVAL / 1000})",
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Done,
),
isErrorValue = ::isSecondsError,
onSubmit = {
viewModel.saveTunnelChanges(
tunnelConfig.copy(pingInterval = if (it.isBlank()) null else it.toLong() * 1000),
)
},
)
SubmitConfigurationTextBox(
tunnelConfig.pingCooldown?.let { (it / 1000).toString() },
stringResource(R.string.set_custom_ping_cooldown),
"(${stringResource(R.string.optional_default)} ${Constants.PING_COOLDOWN / 1000})",
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
),
isErrorValue = ::isSecondsError,
onSubmit = {
viewModel.saveTunnelChanges(
tunnelConfig.copy(pingCooldown = if (it.isBlank()) null else it.toLong() * 1000),
)
},
)
},
),
)
}
},
)
}
}
}
@@ -1,37 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.tunneloptions
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class TunnelOptionsViewModel
@Inject
constructor(
private val appDataRepository: AppDataRepository,
) : ViewModel() {
fun onToggleRestartOnPing(tunnelConfig: TunnelConfig) = viewModelScope.launch {
appDataRepository.tunnels.save(
tunnelConfig.copy(
isPingEnabled = !tunnelConfig.isPingEnabled,
),
)
}
fun onTogglePrimaryTunnel(tunnelConfig: TunnelConfig) = viewModelScope.launch {
appDataRepository.tunnels.updatePrimaryTunnel(
when (tunnelConfig.isPrimaryTunnel) {
true -> null
false -> tunnelConfig
},
)
}
fun saveTunnelChanges(tunnelConfig: TunnelConfig) = viewModelScope.launch {
appDataRepository.tunnels.save(tunnelConfig)
}
}
@@ -1,763 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.tunneloptions.config
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusGroup
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
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.imePadding
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
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Save
import androidx.compose.material.icons.rounded.ContentCopy
import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material.icons.rounded.MoreVert
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
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.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.ui.screens.tunneloptions.config.model.InterfaceActions
import com.zaneschepke.wireguardautotunnel.ui.screens.tunneloptions.config.model.PeerActions
import com.zaneschepke.wireguardautotunnel.ui.screens.tunneloptions.config.model.InterfaceProxy
import com.zaneschepke.wireguardautotunnel.ui.screens.tunneloptions.config.model.PeerProxy
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
import org.amnezia.awg.crypto.KeyPair
@Composable
fun ConfigScreen(tunnelConfig: TunnelConfig?, appViewModel: AppViewModel) {
val context = LocalContext.current
val snackbar = SnackbarController.current
val clipboardManager: ClipboardManager = LocalClipboardManager.current
val keyboardController = LocalSoftwareKeyboardController.current
val navController = LocalNavController.current
var isInterfaceDropDownExpanded by remember {
mutableStateOf(false)
}
val popBackStack by appViewModel.popBackStack.collectAsStateWithLifecycle(false)
val configPair = Pair(tunnelConfig?.name ?: "", tunnelConfig?.toAmConfig())
var tunnelName by remember {
mutableStateOf(configPair.first)
}
var interfaceState by remember {
mutableStateOf(configPair.second?.let { InterfaceProxy.from(it.`interface`) } ?: InterfaceProxy())
}
var showAmneziaValues by remember {
mutableStateOf(configPair.second?.`interface`?.junkPacketCount?.isPresent == true)
}
var showScripts by remember {
mutableStateOf(false)
}
val peersState = remember {
(configPair.second?.peers?.map { PeerProxy.from(it) } ?: listOf(PeerProxy())).toMutableStateList()
}
var showAuthPrompt by remember { mutableStateOf(false) }
var isAuthenticated by remember { mutableStateOf(false) }
val keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() })
val keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done)
if (showAuthPrompt) {
AuthorizationPrompt(
onSuccess = {
showAuthPrompt = false
isAuthenticated = true
},
onError = {
showAuthPrompt = false
snackbar.showMessage(
context.getString(R.string.error_authentication_failed),
)
},
onFailure = {
showAuthPrompt = false
snackbar.showMessage(
context.getString(R.string.error_authorization_failed),
)
},
)
}
LaunchedEffect(popBackStack) {
if (popBackStack) navController.popBackStack()
}
Scaffold(
topBar = {
TopNavBar(stringResource(R.string.edit_tunnel), trailing = {
IconButton(onClick = {
tunnelConfig?.let {
appViewModel.updateExistingTunnelConfig(
it,
tunnelName,
peersState,
interfaceState,
)
} ?: appViewModel.saveNewTunnel(tunnelName, peersState, interfaceState)
}) {
val icon = Icons.Outlined.Save
Icon(
imageVector = icon,
contentDescription = icon.name,
)
}
})
},
) { padding ->
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top),
modifier =
Modifier
.fillMaxSize()
.padding(padding)
.imePadding()
.verticalScroll(rememberScrollState())
.padding(top = 24.dp.scaledHeight())
.padding(horizontal = 24.dp.scaledWidth()),
) {
Surface(
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.surface,
) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(5.dp, Alignment.Top),
modifier = Modifier
.padding(16.dp.scaledWidth())
.focusGroup(),
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier =
Modifier.fillMaxWidth(),
) {
GroupLabel(
stringResource(R.string.interface_),
)
Column {
IconButton(
modifier = Modifier.size(iconSize),
onClick = {
isInterfaceDropDownExpanded = true
},
) {
val icon = Icons.Rounded.MoreVert
Icon(icon, icon.name)
}
DropdownMenu(
containerColor = MaterialTheme.colorScheme.surface,
expanded = isInterfaceDropDownExpanded,
modifier = Modifier.shadow(12.dp).background(MaterialTheme.colorScheme.surface),
onDismissRequest = {
isInterfaceDropDownExpanded = false
},
) {
val isAmneziaCompatibilitySet = interfaceState.isAmneziaCompatibilityModeSet()
InterfaceActions.entries.forEach { action ->
DropdownMenuItem(
text = {
Text(
text = when (action) {
InterfaceActions.TOGGLE_SHOW_SCRIPTS -> if (showScripts) {
stringResource(R.string.hide_scripts)
} else {
stringResource(R.string.show_scripts)
}
InterfaceActions.TOGGLE_AMNEZIA_VALUES -> if (showAmneziaValues) {
stringResource(R.string.hide_amnezia_properties)
} else {
stringResource(R.string.show_amnezia_properties)
}
InterfaceActions.SET_AMNEZIA_COMPATIBILITY -> if (isAmneziaCompatibilitySet) {
stringResource(R.string.remove_amnezia_compatibility)
} else {
stringResource(R.string.enable_amnezia_compatibility)
}
},
)
},
onClick = {
isInterfaceDropDownExpanded = false
when (action) {
InterfaceActions.TOGGLE_AMNEZIA_VALUES -> showAmneziaValues = !showAmneziaValues
InterfaceActions.TOGGLE_SHOW_SCRIPTS -> showScripts = !showScripts
InterfaceActions.SET_AMNEZIA_COMPATIBILITY -> if (isAmneziaCompatibilitySet) {
showAmneziaValues = false
interfaceState = interfaceState.resetAmneziaProperties()
} else {
showAmneziaValues = true
interfaceState = interfaceState.toAmneziaCompatibilityConfig()
}
}
},
)
}
}
}
}
ConfigurationTextBox(
value = tunnelName,
onValueChange = { tunnelName = it },
keyboardActions = keyboardActions,
label = stringResource(R.string.name),
hint = stringResource(R.string.tunnel_name).lowercase(),
modifier =
Modifier
.fillMaxWidth(),
)
val privateKeyEnabled = (tunnelConfig == null) || isAuthenticated
OutlinedTextField(
textStyle = MaterialTheme.typography.labelLarge,
modifier =
Modifier
.fillMaxWidth()
.clickable { showAuthPrompt = true },
value = interfaceState.privateKey,
visualTransformation =
if (privateKeyEnabled) {
VisualTransformation.None
} else {
PasswordVisualTransformation()
},
enabled = privateKeyEnabled,
onValueChange = { interfaceState = interfaceState.copy(privateKey = it) },
trailingIcon = {
IconButton(
enabled = privateKeyEnabled,
modifier = Modifier.focusRequester(FocusRequester.Default).size(iconSize),
onClick = {
val keypair = KeyPair()
interfaceState = interfaceState.copy(
privateKey = keypair.privateKey.toBase64(),
publicKey = keypair.publicKey.toBase64(),
)
},
) {
Icon(
Icons.Rounded.Refresh,
stringResource(R.string.rotate_keys),
tint = if (privateKeyEnabled) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.outline,
)
}
},
label = { Text(stringResource(R.string.private_key)) },
singleLine = true,
placeholder = {
Text(
stringResource(R.string.base64_key),
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.outline,
)
},
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
)
OutlinedTextField(
textStyle = MaterialTheme.typography.labelLarge,
modifier =
Modifier
.fillMaxWidth()
.focusRequester(FocusRequester.Default),
value = interfaceState.publicKey,
enabled = false,
onValueChange = {
interfaceState = interfaceState.copy(publicKey = it)
},
trailingIcon = {
IconButton(
modifier = Modifier.focusRequester(FocusRequester.Default).size(iconSize),
onClick = {
clipboardManager.setText(
AnnotatedString(interfaceState.publicKey),
)
},
) {
Icon(
Icons.Rounded.ContentCopy,
stringResource(R.string.copy_public_key),
tint = MaterialTheme.colorScheme.onSurface,
)
}
},
label = { Text(stringResource(R.string.public_key)) },
singleLine = true,
placeholder = {
Text(
stringResource(R.string.base64_key),
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.outline,
)
},
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
)
ConfigurationTextBox(
value = interfaceState.addresses,
onValueChange = {
interfaceState = interfaceState.copy(addresses = it)
},
keyboardActions = keyboardActions,
label = stringResource(R.string.addresses),
hint = stringResource(R.string.comma_separated_list),
modifier =
Modifier
.fillMaxWidth()
.padding(end = 5.dp),
)
ConfigurationTextBox(
value = interfaceState.listenPort,
onValueChange = {
interfaceState = interfaceState.copy(listenPort = it)
},
keyboardActions = keyboardActions,
label = stringResource(R.string.listen_port),
hint = stringResource(R.string.random),
modifier = Modifier.fillMaxWidth(),
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(5.dp),
) {
ConfigurationTextBox(
value = interfaceState.dnsServers,
onValueChange = {
interfaceState = interfaceState.copy(dnsServers = it)
},
keyboardActions = keyboardActions,
label = stringResource(R.string.dns_servers),
hint = stringResource(R.string.comma_separated_list),
modifier =
Modifier
.fillMaxWidth(3 / 5f)
.padding(end = 5.dp),
)
ConfigurationTextBox(
value = interfaceState.mtu,
onValueChange = {
interfaceState = interfaceState.copy(mtu = it)
},
keyboardActions = keyboardActions,
label = stringResource(R.string.mtu),
hint = stringResource(R.string.auto),
modifier = Modifier.width(IntrinsicSize.Min),
)
}
if (showScripts) {
ConfigurationTextBox(
value = interfaceState.preUp,
onValueChange = {
interfaceState = interfaceState.copy(preUp = it)
},
keyboardActions = keyboardActions,
label = stringResource(R.string.pre_up),
hint = stringResource(R.string.comma_separated_list).lowercase(),
modifier = Modifier.fillMaxWidth(),
)
ConfigurationTextBox(
value = interfaceState.postUp,
onValueChange = {
interfaceState = interfaceState.copy(postUp = it)
},
keyboardActions = keyboardActions,
label = stringResource(R.string.post_up),
hint = stringResource(R.string.comma_separated_list).lowercase(),
modifier = Modifier.fillMaxWidth(),
)
ConfigurationTextBox(
value = interfaceState.preDown,
onValueChange = {
interfaceState = interfaceState.copy(preDown = it)
},
keyboardActions = keyboardActions,
label = stringResource(R.string.pre_down),
hint = stringResource(R.string.comma_separated_list).lowercase(),
modifier = Modifier.fillMaxWidth(),
)
ConfigurationTextBox(
value = interfaceState.postDown,
onValueChange = {
interfaceState = interfaceState.copy(postDown = it)
},
keyboardActions = keyboardActions,
label = stringResource(R.string.post_down),
hint = stringResource(R.string.comma_separated_list).lowercase(),
modifier = Modifier.fillMaxWidth(),
)
}
if (showAmneziaValues) {
ConfigurationTextBox(
value = interfaceState.junkPacketCount,
onValueChange = {
interfaceState = interfaceState.copy(junkPacketCount = it)
},
keyboardActions = keyboardActions,
label = stringResource(R.string.junk_packet_count),
hint = stringResource(R.string.junk_packet_count).lowercase(),
modifier =
Modifier
.fillMaxWidth(),
)
ConfigurationTextBox(
value = interfaceState.junkPacketMinSize,
onValueChange = {
interfaceState = interfaceState.copy(junkPacketMinSize = it)
},
keyboardActions = keyboardActions,
label = stringResource(R.string.junk_packet_minimum_size),
hint =
stringResource(
R.string.junk_packet_minimum_size,
).lowercase(),
modifier =
Modifier
.fillMaxWidth(),
)
ConfigurationTextBox(
value = interfaceState.junkPacketMaxSize,
onValueChange = {
interfaceState = interfaceState.copy(junkPacketMaxSize = it)
},
keyboardActions = keyboardActions,
label = stringResource(R.string.junk_packet_maximum_size),
hint =
stringResource(
R.string.junk_packet_maximum_size,
).lowercase(),
modifier =
Modifier
.fillMaxWidth(),
)
ConfigurationTextBox(
value = interfaceState.initPacketJunkSize,
onValueChange = {
interfaceState = interfaceState.copy(initPacketJunkSize = it)
},
keyboardActions = keyboardActions,
label = stringResource(R.string.init_packet_junk_size),
hint = stringResource(R.string.init_packet_junk_size).lowercase(),
modifier =
Modifier
.fillMaxWidth(),
)
ConfigurationTextBox(
value = interfaceState.responsePacketJunkSize,
onValueChange = {
interfaceState = interfaceState.copy(responsePacketJunkSize = it)
},
keyboardActions = keyboardActions,
label = stringResource(R.string.response_packet_junk_size),
hint =
stringResource(
R.string.response_packet_junk_size,
).lowercase(),
modifier =
Modifier
.fillMaxWidth(),
)
ConfigurationTextBox(
value = interfaceState.initPacketMagicHeader,
onValueChange = {
interfaceState = interfaceState.copy(initPacketMagicHeader = it)
},
keyboardActions = keyboardActions,
label = stringResource(R.string.init_packet_magic_header),
hint =
stringResource(
R.string.init_packet_magic_header,
).lowercase(),
modifier =
Modifier
.fillMaxWidth(),
)
ConfigurationTextBox(
value = interfaceState.responsePacketMagicHeader,
onValueChange = {
interfaceState = interfaceState.copy(responsePacketMagicHeader = it)
},
keyboardActions = keyboardActions,
label = stringResource(R.string.response_packet_magic_header),
hint =
stringResource(
R.string.response_packet_magic_header,
).lowercase(),
modifier =
Modifier
.fillMaxWidth(),
)
ConfigurationTextBox(
value = interfaceState.underloadPacketMagicHeader,
onValueChange = {
interfaceState = interfaceState.copy(underloadPacketMagicHeader = it)
},
keyboardActions = keyboardActions,
label = stringResource(R.string.underload_packet_magic_header),
hint =
stringResource(
R.string.underload_packet_magic_header,
).lowercase(),
modifier =
Modifier
.fillMaxWidth(),
)
ConfigurationTextBox(
value = interfaceState.transportPacketMagicHeader,
onValueChange = {
interfaceState = interfaceState.copy(transportPacketMagicHeader = it)
},
keyboardActions = keyboardActions,
label = stringResource(R.string.transport_packet_magic_header),
hint =
stringResource(
R.string.transport_packet_magic_header,
).lowercase(),
modifier =
Modifier
.fillMaxWidth(),
)
}
}
}
peersState.forEachIndexed { index, peer ->
var isPeerDropDownExpanded by remember {
mutableStateOf(false)
}
val isLanExcluded = peer.isLanExcluded()
Surface(
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.surface,
) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(10.dp, Alignment.Top),
modifier = Modifier
.padding(16.dp.scaledWidth())
.focusGroup(),
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier =
Modifier.fillMaxWidth(),
) {
GroupLabel(
stringResource(R.string.peer),
)
Row(
horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.End),
verticalAlignment = Alignment.CenterVertically,
) {
IconButton(
modifier = Modifier.size(iconSize),
onClick = {
// TODO make a dialog to confirm this
peersState.removeAt(index)
},
) {
val icon = Icons.Rounded.Delete
Icon(icon, icon.name)
}
Column {
IconButton(
modifier = Modifier.size(iconSize),
onClick = {
isPeerDropDownExpanded = true
},
) {
val icon = Icons.Rounded.MoreVert
Icon(icon, icon.name)
}
DropdownMenu(
containerColor = MaterialTheme.colorScheme.surface,
expanded = isPeerDropDownExpanded,
modifier = Modifier.shadow(12.dp).background(MaterialTheme.colorScheme.surface),
onDismissRequest = {
isPeerDropDownExpanded = false
},
) {
PeerActions.entries.forEach { action ->
DropdownMenuItem(
text = {
Text(
text = when (action) {
PeerActions.EXCLUDE_LAN -> if (isLanExcluded) {
stringResource(R.string.include_lan)
} else {
stringResource(R.string.exclude_lan)
}
},
)
},
onClick = {
isPeerDropDownExpanded = false
when (action) {
PeerActions.EXCLUDE_LAN -> if (isLanExcluded) {
peersState[index] = peer.includeLan()
} else {
peersState[index] = peer.excludeLan()
}
}
},
)
}
}
}
}
}
ConfigurationTextBox(
value = peer.publicKey,
onValueChange = { value ->
peersState[index] = peersState[index].copy(publicKey = value)
},
keyboardActions = keyboardActions,
label = stringResource(R.string.public_key),
hint = stringResource(R.string.base64_key),
modifier = Modifier.fillMaxWidth(),
)
ConfigurationTextBox(
value = peer.preSharedKey,
onValueChange = { value ->
peersState[index] = peersState[index].copy(preSharedKey = value)
},
keyboardActions = keyboardActions,
label = stringResource(R.string.preshared_key),
hint = stringResource(R.string.optional),
modifier = Modifier.fillMaxWidth(),
)
OutlinedTextField(
textStyle = MaterialTheme.typography.labelLarge,
modifier = Modifier.fillMaxWidth(),
value = peer.persistentKeepalive,
enabled = true,
onValueChange = { value ->
peersState[index] = peersState[index].copy(persistentKeepalive = value)
},
trailingIcon = {
Text(
stringResource(R.string.seconds),
modifier = Modifier.padding(end = 10.dp),
style = MaterialTheme.typography.labelMedium,
)
},
label = { Text(stringResource(R.string.persistent_keepalive), style = MaterialTheme.typography.labelMedium) },
singleLine = true,
placeholder = {
Text(stringResource(R.string.optional_no_recommend), style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.outline)
},
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
)
ConfigurationTextBox(
value = peer.endpoint,
onValueChange = { value ->
peersState[index] = peersState[index].copy(endpoint = value)
},
keyboardActions = keyboardActions,
label = stringResource(R.string.endpoint),
hint = stringResource(R.string.endpoint).lowercase(),
modifier = Modifier.fillMaxWidth(),
)
OutlinedTextField(
textStyle = MaterialTheme.typography.labelLarge,
modifier = Modifier.fillMaxWidth(),
value = peer.allowedIps,
enabled = true,
onValueChange = { value ->
peersState[index] = peersState[index].copy(allowedIps = value)
},
label = { Text(stringResource(R.string.allowed_ips)) },
singleLine = true,
placeholder = {
Text(stringResource(R.string.comma_separated_list), style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.outline)
},
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
)
}
}
}
Row(
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically,
modifier =
Modifier
.fillMaxSize()
.padding(bottom = 140.dp),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) {
TextButton(onClick = {
peersState.add(PeerProxy())
}) {
Text(stringResource(R.string.add_peer))
}
}
}
}
}
}
@@ -1,7 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.tunneloptions.config.model
enum class InterfaceActions {
TOGGLE_AMNEZIA_VALUES,
SET_AMNEZIA_COMPATIBILITY,
TOGGLE_SHOW_SCRIPTS,
}
@@ -1,224 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.tunneloptions.config.model
import com.wireguard.config.Interface
import com.zaneschepke.wireguardautotunnel.util.extensions.joinAndTrim
import com.zaneschepke.wireguardautotunnel.util.extensions.toTrimmedList
import kotlin.ranges.contains
data class InterfaceProxy(
val privateKey: String = "",
val publicKey: String = "",
val addresses: String = "",
val dnsServers: String = "",
val listenPort: String = "",
val mtu: String = "",
val includedApplications: MutableSet<String> = mutableSetOf(),
val excludedApplications: MutableSet<String> = mutableSetOf(),
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 = "",
val preUp: String = "",
val postUp: String = "",
val preDown: String = "",
val postDown: String = "",
) {
fun toWgInterface(): Interface {
return Interface.Builder().apply {
parseAddresses(addresses)
parsePrivateKey(privateKey)
if (dnsServers.isNotBlank()) parseDnsServers(dnsServers)
if (mtu.isNotBlank()) parseMtu(mtu)
if (listenPort.isNotBlank()) parseListenPort(listenPort)
includeApplications(includedApplications)
excludeApplications(excludedApplications)
preUp.toTrimmedList().forEach { parsePreUp(it) }
postUp.toTrimmedList().forEach { parsePostUp(it) }
preDown.toTrimmedList().forEach { parsePreDown(it) }
postDown.toTrimmedList().forEach { parsePostDown(it) }
}.build()
}
fun toAmneziaCompatibilityConfig(): InterfaceProxy {
return copy(
junkPacketCount = "4",
junkPacketMinSize = "40",
junkPacketMaxSize = "70",
initPacketJunkSize = "0",
responsePacketJunkSize = "0",
initPacketMagicHeader = "1",
responsePacketMagicHeader = "2",
underloadPacketMagicHeader = "3",
transportPacketMagicHeader = "4",
)
}
fun resetAmneziaProperties(): InterfaceProxy {
return copy(
junkPacketCount = "",
junkPacketMinSize = "",
junkPacketMaxSize = "",
initPacketJunkSize = "",
responsePacketJunkSize = "",
initPacketMagicHeader = "",
responsePacketMagicHeader = "",
underloadPacketMagicHeader = "",
transportPacketMagicHeader = "",
)
}
fun isAmneziaCompatibilityModeSet(): Boolean {
return junkPacketCount.toIntOrNull() in 3..<5 &&
junkPacketMinSize.toIntOrNull() == 40 &&
junkPacketMaxSize.toIntOrNull() == 70 &&
with(initPacketJunkSize.toIntOrNull()) { this == 0 || this == null } &&
with(responsePacketJunkSize.toIntOrNull()) { this == 0 || this == null } &&
initPacketMagicHeader.toLongOrNull() == 1L &&
responsePacketMagicHeader.toLongOrNull() == 2L &&
underloadPacketMagicHeader.toLongOrNull() == 3L &&
transportPacketMagicHeader.toLongOrNull() == 4L
}
fun toAmInterface(): org.amnezia.awg.config.Interface {
return org.amnezia.awg.config.Interface.Builder().apply {
parseAddresses(addresses)
parsePrivateKey(privateKey)
if (dnsServers.isNotBlank()) parseDnsServers(dnsServers)
if (mtu.isNotBlank()) parseMtu(mtu)
if (listenPort.isNotBlank()) parseListenPort(listenPort)
includeApplications(includedApplications)
excludeApplications(excludedApplications)
preUp.toTrimmedList().forEach { parsePreUp(it) }
postUp.toTrimmedList().forEach { parsePostUp(it) }
preDown.toTrimmedList().forEach { parsePreDown(it) }
postDown.toTrimmedList().forEach { parsePostDown(it) }
if (junkPacketCount.isNotBlank()) parseJunkPacketCount(junkPacketCount)
if (junkPacketMinSize.isNotBlank()) parseJunkPacketMinSize(junkPacketMinSize)
if (junkPacketMaxSize.isNotBlank()) parseJunkPacketMaxSize(junkPacketMaxSize)
if (initPacketJunkSize.isNotBlank()) parseInitPacketJunkSize(initPacketJunkSize)
if (responsePacketJunkSize.isNotBlank()) parseResponsePacketJunkSize(responsePacketJunkSize)
if (initPacketMagicHeader.isNotBlank()) parseInitPacketMagicHeader(initPacketMagicHeader)
if (responsePacketMagicHeader.isNotBlank()) parseResponsePacketMagicHeader(responsePacketMagicHeader)
if (underloadPacketMagicHeader.isNotBlank()) parseUnderloadPacketMagicHeader(underloadPacketMagicHeader)
if (transportPacketMagicHeader.isNotBlank()) parseTransportPacketMagicHeader(transportPacketMagicHeader)
}.build()
}
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 = listOf(
i.dnsServers.joinToString(", ").replace("/", "").trim(),
i.dnsSearchDomains.joinAndTrim(),
).filter { it.isNotEmpty() }.joinToString(", "),
listenPort =
if (i.listenPort.isPresent) {
i.listenPort.get().toString().trim()
} else {
""
},
mtu = if (i.mtu.isPresent) i.mtu.get().toString().trim() else "",
includedApplications = i.includedApplications.toMutableSet(),
excludedApplications = i.excludedApplications.toMutableSet(),
preUp = i.preUp.joinAndTrim(),
postUp = i.postUp.joinAndTrim(),
preDown = i.preDown.joinAndTrim(),
postDown = i.postDown.joinAndTrim(),
)
}
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 + i.dnsSearchDomains).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 "",
includedApplications = i.includedApplications.toMutableSet(),
excludedApplications = i.excludedApplications.toMutableSet(),
preUp = i.preUp.joinAndTrim(),
postUp = i.postUp.joinAndTrim(),
preDown = i.preDown.joinAndTrim(),
postDown = i.postDown.joinAndTrim(),
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 {
""
},
)
}
}
}
@@ -1,5 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.tunneloptions.config.model
enum class PeerActions {
EXCLUDE_LAN,
}
@@ -1,32 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.tunneloptions.splittunnel
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.AllInclusive
import androidx.compose.material.icons.filled.Remove
import androidx.compose.ui.graphics.vector.ImageVector
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.util.StringValue
enum class SplitOptions {
INCLUDE,
ALL,
EXCLUDE,
;
fun icon(): ImageVector {
return when (this) {
ALL -> Icons.Filled.AllInclusive
INCLUDE -> Icons.Filled.Add
EXCLUDE -> Icons.Filled.Remove
}
}
fun text(): StringValue {
return when (this) {
ALL -> StringValue.StringResource(R.string.all)
INCLUDE -> StringValue.StringResource(R.string.include)
EXCLUDE -> StringValue.StringResource(R.string.exclude)
}
}
}
@@ -1,9 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.tunneloptions.splittunnel
import android.graphics.drawable.Drawable
data class SplitTunnelApp(
val icon: Drawable,
val name: String,
val `package`: String,
)
@@ -1,268 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.tunneloptions.splittunnel
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
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.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Check
import androidx.compose.material.icons.outlined.Save
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MultiChoiceSegmentedButtonRow
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.accompanist.drawablepainter.rememberDrawablePainter
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.common.button.SelectionItemButton
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
import com.zaneschepke.wireguardautotunnel.ui.common.textbox.CustomTextField
import com.zaneschepke.wireguardautotunnel.ui.screens.tunneloptions.config.model.InterfaceProxy
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
import java.text.Collator
import java.util.Locale
@Composable
fun SplitTunnelScreen(tunnelConfig: TunnelConfig, viewModel: AppViewModel) {
val context = LocalContext.current
val navController = LocalNavController.current
val inputHeight = 45.dp
val collator = Collator.getInstance(Locale.getDefault())
val popBackStack by viewModel.popBackStack.collectAsStateWithLifecycle(false)
LaunchedEffect(popBackStack) {
if (popBackStack) navController.popBackStack()
}
val splitTunnelApps by viewModel.splitTunnelApps.collectAsStateWithLifecycle()
var proxyInterface by remember { mutableStateOf(InterfaceProxy()) }
var selectedSplitOption by remember { mutableStateOf(SplitOptions.ALL) }
val selectedPackages = remember { mutableStateListOf<String>() }
LaunchedEffect(Unit) {
proxyInterface = InterfaceProxy.from(tunnelConfig.toAmConfig().`interface`)
val pair = when {
proxyInterface.excludedApplications.isNotEmpty() -> Pair(SplitOptions.EXCLUDE, proxyInterface.excludedApplications)
proxyInterface.includedApplications.isNotEmpty() -> Pair(SplitOptions.INCLUDE, proxyInterface.includedApplications)
else -> Pair(SplitOptions.ALL, mutableSetOf())
}
selectedSplitOption = pair.first
selectedPackages.addAll(pair.second)
}
var query: String by remember { mutableStateOf("") }
val sortedPackages by remember {
derivedStateOf {
splitTunnelApps.sortedWith(compareBy(collator) { it.name }).filter { it.name.lowercase().contains(query.lowercase()) }.toMutableStateList()
}
}
LaunchedEffect(Unit) {
// clean up any split tunnel packages for apps that were uninstalled
viewModel.cleanUpUninstalledApps(tunnelConfig, splitTunnelApps.map { it.`package` })
}
Scaffold(
topBar = {
TopNavBar(stringResource(R.string.tunneling_apps), trailing = {
IconButton(onClick = {
proxyInterface.apply {
includedApplications.clear()
excludedApplications.clear()
}
when (selectedSplitOption) {
SplitOptions.INCLUDE -> proxyInterface.includedApplications.apply {
addAll(selectedPackages)
}
SplitOptions.EXCLUDE -> proxyInterface.excludedApplications.apply {
addAll(selectedPackages)
}
SplitOptions.ALL -> Unit
}
viewModel.updateExistingTunnelConfig(tunnelConfig, `interface` = proxyInterface)
}) {
val icon = Icons.Outlined.Save
Icon(
imageVector = icon,
contentDescription = icon.name,
)
}
})
},
) { padding ->
Column(
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.CenterVertically),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.padding(padding)
.padding(top = 24.dp.scaledHeight()),
) {
MultiChoiceSegmentedButtonRow(
modifier = Modifier.background(color = MaterialTheme.colorScheme.background).fillMaxWidth()
.padding(horizontal = 24.dp.scaledWidth()).height(inputHeight),
) {
SplitOptions.entries.forEachIndexed { index, entry ->
val active = selectedSplitOption == entry
SegmentedButton(
shape = SegmentedButtonDefaults.itemShape(index = index, count = SplitOptions.entries.size, baseShape = RoundedCornerShape(8.dp)),
icon = {
SegmentedButtonDefaults.Icon(active = active, activeContent = {
val icon = Icons.Outlined.Check
Icon(imageVector = icon, icon.name, tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(SegmentedButtonDefaults.IconSize))
}) {
Icon(
imageVector = entry.icon(),
contentDescription = entry.icon().name,
modifier = Modifier.size(SegmentedButtonDefaults.IconSize),
)
}
},
colors = SegmentedButtonDefaults.colors().copy(
activeContainerColor = MaterialTheme.colorScheme.surface,
inactiveContainerColor = MaterialTheme.colorScheme.background,
),
onCheckedChange = {
selectedSplitOption = entry
},
checked = active,
) {
Text(
entry.text().asString(context)
.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() },
color = MaterialTheme.colorScheme.onBackground,
style = MaterialTheme.typography.labelMedium,
)
}
}
}
if (selectedSplitOption != SplitOptions.ALL) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth(),
) {
CustomTextField(
textStyle = MaterialTheme.typography.labelMedium.copy(
color = MaterialTheme.colorScheme.onBackground,
),
value = query,
onValueChange = { input ->
query = input
},
interactionSource = remember { MutableInteractionSource() },
label = {},
leading = {
val icon = Icons.Outlined.Search
Icon(icon, icon.name)
},
containerColor = MaterialTheme.colorScheme.background,
modifier =
Modifier
.fillMaxWidth().height(inputHeight).padding(horizontal = 24.dp.scaledWidth()),
singleLine = true,
keyboardOptions =
KeyboardOptions(
capitalization = KeyboardCapitalization.None,
imeAction = ImeAction.Done,
),
keyboardActions = KeyboardActions(),
)
LazyColumn(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top,
contentPadding = PaddingValues(top = 10.dp),
) {
items(sortedPackages, key = { it.`package` }) { app ->
val checked = selectedPackages.contains(app.`package`)
val onClick = {
if (checked) selectedPackages.remove(app.`package`) else selectedPackages.add(app.`package`)
}
SelectionItemButton(
{
Image(
rememberDrawablePainter(app.icon),
app.name,
modifier =
Modifier
.padding(horizontal = 24.dp.scaledWidth())
.size(
iconSize,
),
)
},
buttonText = app.name,
onClick = {
onClick()
},
trailing = {
Row(
modifier = Modifier
.fillMaxWidth(),
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically,
) {
Checkbox(
checked = checked,
onCheckedChange = {
onClick()
},
)
}
},
)
}
}
}
}
}
}
}
@@ -1,174 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.tunneloptions.tunnelautotunnel
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.PhoneAndroid
import androidx.compose.material.icons.outlined.Security
import androidx.compose.material.icons.outlined.SettingsEthernet
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.components.TrustedNetworkTextBox
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.components.WildcardsLabel
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
@Composable
fun TunnelAutoTunnelScreen(tunnelConfig: TunnelConfig, settings: Settings, tunnelAutoTunnelViewModel: TunnelAutoTunnelViewModel = hiltViewModel()) {
var currentText by remember { mutableStateOf("") }
LaunchedEffect(tunnelConfig.tunnelNetworks) {
currentText = ""
}
Scaffold(
topBar = {
TopNavBar(tunnelConfig.name)
},
) { padding ->
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top),
modifier =
Modifier
.fillMaxSize()
.padding(padding)
.verticalScroll(rememberScrollState())
.padding(top = 24.dp.scaledHeight())
.padding(horizontal = 24.dp.scaledWidth()),
) {
SurfaceSelectionGroupButton(
buildList {
addAll(
listOf(
SelectionItem(
Icons.Outlined.PhoneAndroid,
title = {
Text(
stringResource(R.string.mobile_tunnel),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
description = {
Text(
stringResource(R.string.mobile_data_tunnel),
style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline),
)
},
trailing = {
ScaledSwitch(
tunnelConfig.isMobileDataTunnel,
onClick = { tunnelAutoTunnelViewModel.onToggleIsMobileDataTunnel(tunnelConfig) },
)
},
onClick = { tunnelAutoTunnelViewModel.onToggleIsMobileDataTunnel(tunnelConfig) },
),
SelectionItem(
Icons.Outlined.SettingsEthernet,
title = {
Text(
stringResource(R.string.ethernet_tunnel),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
description = {
Text(
stringResource(R.string.set_ethernet_tunnel),
style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline),
)
},
trailing = {
ScaledSwitch(
tunnelConfig.isEthernetTunnel,
onClick = { tunnelAutoTunnelViewModel.onToggleIsEthernetTunnel(tunnelConfig) },
)
},
onClick = { tunnelAutoTunnelViewModel.onToggleIsEthernetTunnel(tunnelConfig) },
),
),
)
add(
SelectionItem(
title = {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp.scaledHeight()),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.weight(4f, false)
.fillMaxWidth(),
) {
val icon = Icons.Outlined.Security
Icon(
icon,
icon.name,
modifier = Modifier.size(iconSize),
)
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically),
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp.scaledWidth())
.padding(vertical = 6.dp.scaledHeight()),
) {
Text(
stringResource(R.string.use_tunnel_on_wifi_name),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
}
}
}
},
description = {
TrustedNetworkTextBox(
tunnelConfig.tunnelNetworks,
onDelete = { tunnelAutoTunnelViewModel.onDeleteRunSSID(it, tunnelConfig) },
currentText = currentText,
onSave = { tunnelAutoTunnelViewModel.onSaveRunSSID(it, tunnelConfig) },
onValueChange = { currentText = it },
supporting = {
if (settings.isWildcardsEnabled) {
WildcardsLabel()
}
},
)
},
),
)
},
)
}
}
}
@@ -2,9 +2,8 @@ package com.zaneschepke.wireguardautotunnel.ui.theme
import androidx.compose.ui.graphics.Color
val OffWhite = Color(0xFFF2F2F4)
val CoolGray = Color(0xFF8D9D9F)
val LightGrey = Color(0xFFECEDEF)
val OffWhite = Color(0xFFE5E1E5)
val LightGrey = Color(0xFFCAC4D0)
val Aqua = Color(0xFF76BEBD)
val SilverTree = Color(0xFF6DB58B)
val Plantation = Color(0xFF264A49)
@@ -19,18 +18,14 @@ sealed class ThemeColors(
val primary: Color,
val secondary: Color,
val onSurface: Color,
val onBackground: Color,
val outline: Color,
) {
data object Light : ThemeColors(
background = LightGrey.copy(alpha = 0.95f),
background = LightGrey,
surface = OffWhite,
primary = Aqua,
secondary = LightGrey,
primary = Plantation,
secondary = OffWhite,
onSurface = BalticSea,
outline = Plantation.copy(alpha = .75f),
onBackground = BalticSea,
)
data object Dark : ThemeColors(
@@ -39,7 +34,5 @@ sealed class ThemeColors(
primary = Aqua,
secondary = Plantation,
onSurface = OffWhite,
outline = CoolGray,
onBackground = OffWhite,
)
}
@@ -4,3 +4,4 @@ import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
val iconSize = 24.dp.scaledHeight()
val topPadding = 80.dp.scaledHeight()
@@ -21,11 +21,9 @@ private val DarkColorScheme =
primary = ThemeColors.Dark.primary,
surface = ThemeColors.Dark.surface,
background = ThemeColors.Dark.background,
secondary = ThemeColors.Dark.secondary,
secondaryContainer = ThemeColors.Dark.secondary,
onSurface = ThemeColors.Dark.onSurface,
onSecondaryContainer = ThemeColors.Dark.primary,
outline = ThemeColors.Dark.outline,
onBackground = ThemeColors.Dark.onBackground,
)
private val LightColorScheme =
@@ -33,11 +31,9 @@ private val LightColorScheme =
primary = ThemeColors.Light.primary,
surface = ThemeColors.Light.surface,
background = ThemeColors.Light.background,
secondary = ThemeColors.Light.secondary,
secondaryContainer = ThemeColors.Light.secondary,
onSurface = ThemeColors.Light.onSurface,
onSecondaryContainer = ThemeColors.Light.primary,
outline = ThemeColors.Light.outline,
onBackground = ThemeColors.Light.onBackground,
)
enum class Theme {
@@ -74,18 +70,14 @@ fun WireguardAutoTunnelTheme(theme: Theme = Theme.AUTOMATIC, content: @Composabl
}
}
}
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
WindowCompat.setDecorFitsSystemWindows(window, false)
window.navigationBarColor = Color.Transparent.toArgb()
window.statusBarColor = Color.Transparent.toArgb()
WindowCompat.getInsetsController(window, window.decorView).apply {
isAppearanceLightStatusBars = !isDark
isAppearanceLightNavigationBars = !isDark
}
window.navigationBarColor = Color.Transparent.toArgb()
WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightStatusBars = !isDark
}
}
@@ -1,15 +1,12 @@
package com.zaneschepke.wireguardautotunnel.util.extensions
import android.Manifest
import android.content.ComponentName
import android.content.Context
import android.content.Context.POWER_SERVICE
import android.content.Intent
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.location.LocationManager
import android.net.Uri
import android.os.Build
import android.os.PowerManager
import android.provider.Settings
import android.service.quicksettings.TileService
@@ -171,6 +168,24 @@ fun Context.launchAppSettings() {
}
}
// fun Context.startTunnelBackground(tunnelId: Int) {
// sendBroadcast(
// Intent(this, BackgroundActionReceiver::class.java).apply {
// action = BackgroundActionReceiver.ACTION_CONNECT
// putExtra(BackgroundActionReceiver.TUNNEL_ID_EXTRA_KEY, tunnelId)
// },
// )
// }
//
// fun Context.stopTunnelBackground(tunnelId: Int) {
// sendBroadcast(
// Intent(this, BackgroundActionReceiver::class.java).apply {
// action = BackgroundActionReceiver.ACTION_DISCONNECT
// putExtra(BackgroundActionReceiver.TUNNEL_ID_EXTRA_KEY, tunnelId)
// },
// )
// }
fun Context.requestTunnelTileServiceStateUpdate() {
TileService.requestListeningState(
this,
@@ -184,15 +199,3 @@ fun Context.requestAutoTunnelTileServiceUpdate() {
ComponentName(this, AutoTunnelControlTile::class.java),
)
}
fun Context.getAllInternetCapablePackages(): List<PackageInfo> {
val permissions = arrayOf(Manifest.permission.INTERNET)
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
packageManager.getPackagesHoldingPermissions(
permissions,
PackageManager.PackageInfoFlags.of(0L),
)
} else {
packageManager.getPackagesHoldingPermissions(permissions, 0)
}
}
@@ -78,9 +78,15 @@ fun <T> CoroutineScope.asChannel(flow: Flow<T>): ReceiveChannel<T> = produce {
}
}
fun Job?.onNotRunning(callback: () -> Unit) {
if (this == null || this.isCompleted || this.isCompleted) {
callback.invoke()
}
}
fun Job.cancelWithMessage(message: String) {
kotlin.runCatching {
cancel()
this.cancel()
Timber.i(message)
}
}
@@ -51,17 +51,20 @@ fun String.replaceUnescapedChar(charToReplace: String, replacement: String): Str
this[matchResult.range.first - 1] != '\\' ||
(matchResult.range.first > 1 && this[matchResult.range.first - 2] == '\\')
) {
replacement
replacement.toString()
} else {
matchResult.value
}
}
}
fun Iterable<String>.joinAndTrim(): String {
return this.joinToString(", ").trim()
}
fun String.toTrimmedList(): List<String> {
return this.split(",").map { it.trim() }.filter { it.isNotEmpty() }
fun String.isCharacterEscaped(index: Int): Boolean {
if (index <= 0) return false
var backslashCount = 0
var currentIndex = index - 1
while (currentIndex >= 0 && this[currentIndex] == '\\') {
backslashCount++
currentIndex--
}
return backslashCount % 2 != 0
}
@@ -46,10 +46,11 @@ fun Peer.isReachable(): Boolean {
} else {
Constants.DEFAULT_PING_IP
}
Timber.d("Checking reachability of peer: $host")
Timber.i("Checking reachability of peer: $host")
val reachable =
InetAddress.getByName(host)
.isReachable(Constants.PING_TIMEOUT.toInt())
Timber.i("Result: reachable - $reachable")
return reachable
}
+14 -21
View File
@@ -12,7 +12,8 @@
<string name="tunnel_name">Název tunelu</string>
<string name="exclude">Vyloučit</string>
<string name="include">Zahrnout</string>
<string name="endpoint">Koncový bod</string>
<string name="save_changes">Uložit</string>
<string name="endpoint">Koncový bod</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>
@@ -62,8 +63,10 @@
<string name="tunnel_on_ethernet">Tunelovat na ethernetu</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="tunnels">Tunely</string>
<string name="config_changes_saved">Změny v konfiguraci uloženy.</string>
<string name="public_key">Veřejný klíč</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="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>
@@ -71,7 +74,8 @@
<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="rotate_keys">Rotovat klíče</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>
@@ -83,7 +87,9 @@
<string name="yes">Ano</string>
<string name="tunneling_apps">Tunelování aplikací</string>
<string name="all">vše</string>
<string name="no_email_detected">Žádná emailová aplikace nebyla nalezena</string>
<string name="included">zahrnuto</string>
<string name="excluded">vyloučeno</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="read_logs">Přečíst si logy</string>
<string name="pin_created">PIN úspěšně vytvořen</string>
@@ -95,21 +101,8 @@
<string name="support">Podpora</string>
<string name="app_name">WG Tunnel</string>
<string name="mtu">MTU</string>
<string name="listen_port">Naslouchací port</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="persistent_keepalive">Udržování spojení</string>
<string name="restart_at_boot">Restartovat při spuštění</string>
<string name="show_amnezia_properties">Zobrazit Amnezia možnosti</string>
<string name="language">Jazyk</string>
<string name="light">Světlé</string>
<string name="appearance">Vzhled</string>
<string name="display_theme">Téma zobrazení</string>
<string name="dark">Tmavé</string>
<string name="dynamic">Podle systému</string>
<string name="notifications">Oznámení</string>
<string name="kill_switch">Vypínač</string>
<string name="wifi_name_via_shell">Název WiFi přes shell</string>
<string name="use_root_shell_for_wifi">Použít root shell pro získání názvu WiFi</string>
<string name="add_from_clipboard">Přidat ze schránky</string>
</resources>
</resources>
+19 -42
View File
@@ -5,7 +5,7 @@
<string name="no_tunnels">Noch keine Tunnel hinzugefügt!</string>
<string name="tunnels">Tunnel</string>
<string name="tunnel_mobile_data">Tunnel für mobile Daten</string>
<string name="privacy_policy">Datenschutzbestimmungen anzeigen</string>
<string name="privacy_policy">Siehe Privacy Policy</string>
<string name="okay">Ok</string>
<string name="tunnel_on_ethernet">Tunnel für Ethernet</string>
<string name="auto_tunneling">Auto-Tunneln</string>
@@ -21,7 +21,10 @@
<string name="tunnel_name">Tunnel-Name</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="public_key">Öffentlicher Schlüssel</string>
<string name="addresses">Adressen</string>
<string name="dns_servers">DNS-Server</string>
@@ -33,10 +36,12 @@
<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="db_name">wg-tunnel-db</string>
<string name="vpn_on">VPN an</string>
<string name="vpn_off">VPN aus</string>
<string name="create_import">Von Grund auf neu erstellen</string>
<string name="add_peer">Gegenstelle hinzufügen</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>
@@ -60,14 +65,15 @@
<string name="email_description">Sende mir eine E-Mail</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 Dienst</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="excluded">ausgeschlossen</string>
<string name="all">alle</string>
<string name="no_browser_detected">Keinen Browser erkannt</string>
<string name="open_issue">Issue öffnen</string>
<string name="read_logs">Logeinträge lesen</string>
<string name="read_logs">Logs lesen</string>
<string name="auto">(automatisch)</string>
<string name="incorrect_pin">PIN nicht korrekt</string>
<string name="pin_created">PIN erfolgreich erstellt</string>
@@ -78,10 +84,11 @@
<string name="error_authentication_failed">Authentifizierung fehlgeschlagen</string>
<string name="export_configs">Konfigurationen exportieren</string>
<string name="unknown_error">Unbekannter Fehler aufgetreten</string>
<string name="email_chooser">Eine E-Mail senden</string>
<string name="email_chooser">Sende eine E-Mail…</string>
<string name="error_authorization_failed">Autorisierung fehlgeschlagen</string>
<string name="error_invalid_code">Ungültiger QR Code</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="create_pin">PIN erstellen</string>
<string name="use_tunnel_on_wifi_name">Tunnel für WLAN-Namen verwenden</string>
@@ -89,6 +96,7 @@
<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="vpn_channel_id">VPN Kanal</string>
<string name="vpn_channel_name">VPN Benachrichtigungskanal</string>
<string name="turn_off_tunnel">Aktion erfordert deaktivierten Tunnel</string>
<string name="kernel">Kernel</string>
@@ -115,22 +123,23 @@
<string name="chat_description">Tritt der Community bei</string>
<string name="set_custom_ping_cooldown">Ping-Neustart-Cooldown (sek)</string>
<string name="set_custom_ping_ip">Benutzerdefinierte Ping-IP einstellen</string>
<string name="default_ping_ip">(optional, Standardwert: Gegenstelle)</string>
<string name="default_ping_ip">(optional, Vorgabe ist peers)</string>
<string name="set_custom_ping_internal">Pingintervall (sek)</string>
<string name="always_on_message2">um sicherzustellen, dass Always-on VPN für alle anderen Apps ausgeschaltet ist und versuche es erneut</string>
<string name="tunnel_required">Feature erfordert mindestens einen Tunnel</string>
<string name="sec">Sek</string>
<string name="app_settings">App-Einstellungen</string>
<string name="background_location_message2">um sicherzustellen, dass diese Berechtigungen aktiviert sind</string>
<string name="background_location_message2">um sicherzustellen, dass diese Berechtigungen aktiviert sind.</string>
<string name="root_accepted">Root-Shell akzeptiert</string>
<string name="show_amnezia_properties">Amnezia-Eigenschaften anzeigen</string>
<string name="never">nie</string>
<string name="handshake">Handshake</string>
<string name="vpn_denied_dialog_title">Genehmigung verweigert</string>
<string name="logs">Logeinträge</string>
<string name="logs">Logs</string>
<string name="kernel_not_supported">Kernel nicht unterstützt</string>
<string name="trusted_wifi_names">Vertrauenswürdige WLAN Namen</string>
<string name="requires_app_relaunch">Diese Änderung erfordert einen Neustart der App. Möchten Sie fortfahren?</string>
<string name="selected">Ausgewählt</string>
<string name="use_root_shell_for_wifi">Root-Shell verwenden, um WLAN-Namen zu ermitteln</string>
<string name="light">Hell</string>
<string name="add_wifi_name">WLAN Namen hinzufügen</string>
@@ -160,36 +169,4 @@
<string name="use_wildcards">Wildcards für Namen verwenden</string>
<string name="stop_auto">Auto-Tunnel stoppen</string>
<string name="monitoring_state_changes">Überwache Statusänderungen</string>
<string name="stop_on_no_internet">Stoppen wenn keine Internetverbindung besteht</string>
<string name="ethernet_tunnel">Ethernet Tunnel</string>
<string name="set_ethernet_tunnel">Als Ethernet Tunnel setzten</string>
<string name="native_kill_switch">Nativer Notschalter</string>
<string name="vpn_kill_switch">VPN Notschalter</string>
<string name="kill_switch_options">Notschalteroptionen</string>
<string name="allow_lan_traffic">LAN Verkehr erlauben</string>
<string name="vpn_channel_description">Ein Kanal für VPN-Statusbenachrichtigungen</string>
<string name="auto_tunnel_channel_name">Auto-Tunnel-Benachrichtigungskanal</string>
<string name="auto_tunnel_channel_description">Ein Kanal für Auto-Tunnel Statusbenachrichtigungen</string>
<string name="stop">Stop</string>
<string name="splt_tunneling">Tunneling aufteilen</string>
<string name="tunnel_specific_settings">Tunnelspezifische Einstellungen</string>
<string name="show_scripts">Skripte anzeigen</string>
<string name="pre_up">Vor Aktivierung</string>
<string name="post_up">Nach Aktivierung</string>
<string name="pre_down">Vor Deaktivierung</string>
<string name="post_down">Nach Deaktivierung</string>
<string name="amnezia_kernel_message">Amnezia nicht verfügbar im Kernel-Modus</string>
<string name="enable_amnezia">Amnezia aktivieren</string>
<string name="wg_compat_mode">Wireguard Kompatibilitätsmodus</string>
<string name="quick_actions">Schnellaktionen</string>
<string name="stop_on_internet_loss">Stoppen, wenn die Internetverbindung getrennt wird</string>
<string name="bypass_lan_for_kill_switch">LAN umgehen für Notschalter</string>
<string name="advanced_settings">Erweiterte Einstellungen</string>
<string name="debounce_delay">Entprellverzögerung</string>
<string name="hide_scripts">Skripte verbergen</string>
<string name="enable_amnezia_compatibility">Amnezia Kompatibilität aktivieren</string>
<string name="remove_amnezia_compatibility">Amnezia Kompatibilität entfernen</string>
<string name="hide_amnezia_properties">Amnezia Eigenschaften verbergen</string>
<string name="include_lan">LAN einschließen</string>
<string name="exclude_lan">LAN ausschließen</string>
</resources>
</resources>
+12 -99
View File
@@ -4,10 +4,12 @@
<string name="email_chooser">Enviar un email…</string>
<string name="kernel">Kernel</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="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>
@@ -22,7 +24,10 @@
<string name="tunnel_name">Nombre de 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="mtu">MTU</string>
<string name="dns_servers">DNS servers</string>
<string name="addresses">Addresses</string>
@@ -32,7 +37,7 @@
<string name="no_tunnels">¡Ningún túnel añadido aún!</string>
<string name="tunnels">Túneles</string>
<string name="tunnel_mobile_data">Activar túnel en datos móviles</string>
<string name="privacy_policy">Ver política de privacidad</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>
@@ -43,6 +48,7 @@
<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="db_name">wg-tunnel-db</string>
<string name="auto_tunneling">Túnel-automático</string>
<string name="vpn_on">VPN on</string>
<string name="vpn_off">VPN off</string>
@@ -72,11 +78,12 @@
<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 de túnel-automático</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"></string>
<string name="tunneling_apps">Apps por el túnel</string>
<string name="included">incluida(s)</string>
<string name="no_email_detected">Ninguna app de email detectada</string>
<string name="no_browser_detected">Ningún navegador detectado</string>
<string name="open_issue">Abrir una incidencia</string>
@@ -92,104 +99,10 @@
<string name="edit_tunnel">Editar túnel</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="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="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="junk_packet_count">Recuento de paquetes basura</string>
<string name="junk_packet_minimum_size">Tamaño mínimo del paquete basura</string>
<string name="add_from_clipboard">Agregar desde el portapapeles</string>
<string name="app_settings">configuración de la aplicación</string>
<string name="optional_default">"opcional, por defecto: "</string>
<string name="show_amnezia_properties">Mostrar propiedades de Amnezia</string>
<string name="never">nunca</string>
<string name="sec">seg</string>
<string name="set_custom_ping_internal">Intervalo del Ping (seg)</string>
<string name="transport_packet_magic_header">Encabezado del paquete de transporte</string>
<string name="getting_started_guide">guía de inicio rápido</string>
<string name="always_on_message">Se ha denegado el permiso de conexión VPN. Por favor, compruebe el</string>
<string name="always_on_message2">para asegurarse de que el VPN Siempre encendido esté desactivada para todas las demás aplicaciones e inténtelo de nuevo</string>
<string name="response_packet_magic_header">Encabezado del paquete de respuesta</string>
<string name="junk_packet_maximum_size">Tamaño máximo del paquete basura</string>
<string name="init_packet_junk_size">Tamaño basura del paquete de inicialización</string>
<string name="unsure_how">si no está seguro de cómo continuar</string>
<string name="see_the">Ver la</string>
<string name="error_file_format">Formato de configuración de túnel no válido</string>
<string name="vpn_denied_dialog_title">Permiso denegado</string>
<string name="vpn_settings">Configuraciones VPN del sistema</string>
<string name="chat_description">Unirse a la comunidad</string>
<string name="tunnel_required">La función requiere al menos un túnel</string>
<string name="background_location_message">Permitir todo el tiempo que se requiera permiso de ubicación y/o ubicación precisa para esta función. Consultar</string>
<string name="response_packet_junk_size">Tamaño basura del paquete de respuesta</string>
<string name="init_packet_magic_header">Encabezado del paquete de inicialización</string>
<string name="background_location_message2">para asegurarse de que estos permisos estén habilitados</string>
<string name="root_accepted">Shell root permitido</string>
<string name="set_custom_ping_ip">Establecer ping ip personalizado</string>
<string name="default_ping_ip">(opcional, por defecto de los pares)</string>
<string name="set_custom_ping_cooldown">Tiempo de espera de reinicio del Ping (seg)</string>
<string name="restart_at_boot">Reiniciar al arrancar</string>
<string name="underload_packet_magic_header">Encabezado del paquete baja carga</string>
<string name="handshake">Protocolo de intercambio</string>
<string name="donate">Donar al proyecto</string>
<string name="appearance">Apariencia</string>
<string name="logs">Registros</string>
<string name="trusted_wifi_names">Nombres de wifis confiables</string>
<string name="use_wildcards">Usar comodines de nombre</string>
<string name="primary_tunnel">Túnel principal</string>
<string name="add_wifi_name">Añadir nombre de wifi</string>
<string name="on_demand_rules">Reglas del túnel bajo demanda</string>
<string name="kernel_not_supported">Kernel no compatible</string>
<string name="start_auto">Iniciar túnel automático</string>
<string name="stop_auto">Detener túnel automático</string>
<string name="local_logging">Registro local</string>
<string name="enable_local_logging">Habilitar registro local</string>
<string name="configuration_change">Cambio de configuración</string>
<string name="exclude_lan">Excluir LAN</string>
<string name="enable_amnezia_compatibility">Activar compatibilidad con Amnezia</string>
<string name="wifi_name_via_shell">Nombre del wifi a través del shell</string>
<string name="include_lan">Incluir LAN</string>
<string name="tunnel_specific_settings">Ajustes específicos del túnel</string>
<string name="hide_scripts">Ocultar scripts</string>
<string name="advanced_settings">Configuraciones avanzadas</string>
<string name="show_scripts">Mostrar scripts</string>
<string name="vpn_channel_description">Canal para notificaciones de estado de VPN</string>
<string name="auto_tunnel_channel_description">Canal para notificaciones de estado del túnel automático</string>
<string name="debounce_delay">Retardo de rebote</string>
<string name="hide_amnezia_properties">Ocultar propiedades de Amnezia</string>
<string name="skip">Saltar</string>
<string name="kill_switch">Interruptor de apagado</string>
<string name="automatic">Automático</string>
<string name="notifications">Notificaciones</string>
<string name="light">Claro</string>
<string name="dark">Oscuro</string>
<string name="requires_app_relaunch">Este cambio requiere un reinicio de la aplicación. ¿Desea continuar?</string>
<string name="use_root_shell_for_wifi">Utilizar el shell root para obtener el nombre del wifi</string>
<string name="tunnel_running">Túnel funcionando</string>
<string name="monitoring_state_changes">Monitorizando cambios de estado</string>
<string name="dynamic">Dinámico</string>
<string name="language">Idioma</string>
<string name="display_theme">Tema de la pantalla</string>
<string name="mobile_tunnel">Túnel de los datos móviles</string>
<string name="launch_app_settings">Iniciar la configuración de la aplicación</string>
<string name="learn_more">Aprender más</string>
<string name="wildcards_active">Comodines activos</string>
<string name="stop_on_no_internet">Detener cuando no hay internet</string>
<string name="stop_on_internet_loss">Detener túnel cuando se pierda el internet</string>
<string name="ethernet_tunnel">Túnel ethernet</string>
<string name="set_ethernet_tunnel">Establecer como túnel ethernet</string>
<string name="native_kill_switch">Interruptor de apagado nativo</string>
<string name="vpn_kill_switch">Interruptor de apagado VPN</string>
<string name="kill_switch_options">Opciones del interruptor de apagado</string>
<string name="allow_lan_traffic">Permitir tráfico LAN</string>
<string name="bypass_lan_for_kill_switch">Excluir LAN del interruptor de apagado</string>
<string name="auto_tunnel_channel_name">Canal de notificación del túnel automático</string>
<string name="stop">detener</string>
<string name="splt_tunneling">Túnel dividido</string>
<string name="pre_up">Pre up</string>
<string name="post_up">Post up</string>
<string name="pre_down">Pre down</string>
<string name="amnezia_kernel_message">Amnezia no disponible en modo kernel</string>
<string name="post_down">Post down</string>
<string name="enable_amnezia">Activar Amnezia</string>
<string name="wg_compat_mode">Modo compatibilidad de WG</string>
<string name="quick_actions">Acciones rápidas</string>
<string name="remove_amnezia_compatibility">Eliminar compatibilidad con Amnezia</string>
</resources>
</resources>
-2
View File
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+21 -12
View File
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="turn_off_tunnel">Cette action nécessite la désactivation du tunnel</string>
<string name="vpn_channel_id">Canal VPN</string>
<string name="turn_off_tunnel">Cette action nécessite la désactivation du tunnel</string>
<string name="no_tunnels">Aucun tunnel n\'a été ajouté pour le moment!</string>
<string name="tunnels">Tunnels</string>
<string name="tunnel_mobile_data">Tunnel sur données mobiles</string>
@@ -12,6 +13,9 @@
<string name="open_file">Fichier ouvert</string>
<string name="qr_scan">Scan du code QR</string>
<string name="exclude">Exclure</string>
<string name="tunnel_all">Tunneliser toutes les applications</string>
<string name="save_changes">Sauvegarder</string>
<string name="icon">Icône</string>
<string name="dns_servers">Serveurs DNS</string>
<string name="mtu">MTU</string>
<string name="peer">Pair</string>
@@ -25,6 +29,7 @@
<string name="vpn_off">VPN éteint</string>
<string name="turn_on_tunnel">Cette action nécessite un tunnel actif</string>
<string name="add_peer">Ajouter un pair</string>
<string name="done">Terminé</string>
<string name="rotate_keys">Rotation des clés</string>
<string name="error_file_extension">Le fichier n\'est pas un .conf ou .zip</string>
<string name="vpn_channel_name">Canal de notifications VPN</string>
@@ -38,17 +43,18 @@
<string name="config_changes_saved">Changements de la configuration sauvegardés.</string>
<string name="public_key">Clé publique</string>
<string name="addresses">Adresses</string>
<string name="vpn_on">VPN allumé</string>
<string name="db_name">wg-tunnel-db</string>
<string name="vpn_on">VPN allumé</string>
<string name="create_import">Créer à partir de zéro</string>
<string name="private_key">Clé privée</string>
<string name="interface_">Interface</string>
<string name="copy_public_key">Copier la clé publique</string>
<string name="base64_key">clé base64</string>
<string name="comma_separated_list">liste séparée par des virgules</string>
<string name="listen_port">Port d\'écoute</string>
<string name="listen_port">Écouter sur le port</string>
<string name="random">(aléatoire)</string>
<string name="optional_no_recommend">(optionnel, non recommandé)</string>
<string name="persistent_keepalive">Keepalive persistant</string>
<string name="persistent_keepalive">Keepalive persistent</string>
<string name="optional">(optionnel)</string>
<string name="preshared_key">Clé pré-partagée</string>
<string name="cancel">Annuler</string>
@@ -56,7 +62,7 @@
<string name="error_authentication_failed">Échec de l\'authentification</string>
<string name="error_authorization_failed">Autorisation échouée</string>
<string name="export_configs">Exporter les configs</string>
<string name="tunnel_on_wifi">Tunnel sur wifi non approuvé</string>
<string name="tunnel_on_wifi">Tunnel sur wifi non fiable</string>
<string name="email_chooser">Envoyer un mail…</string>
<string name="docs_description">Lire la documentation</string>
<string name="email_description">Envoyer un mail</string>
@@ -68,7 +74,9 @@
<string name="delete_tunnel">Supprimer un tunnel</string>
<string name="delete_tunnel_message">Êtes-vous sûr de vouloir supprimer ce tunnel?</string>
<string name="tunneling_apps">Applis de tunnel</string>
<string name="no_email_detected">Aucune application de mail détectée</string>
<string name="included">inclus</string>
<string name="excluded">exclu</string>
<string name="no_email_detected">Aucune appli de mail détectée</string>
<string name="no_browser_detected">Aucun navigateur détecté</string>
<string name="open_issue">Signaler un problème</string>
<string name="incorrect_pin">Code PIN incorrect</string>
@@ -92,17 +100,17 @@
<string name="app_settings">les réglages de l\'application</string>
<string name="background_location_message2">afin de s\'assurer que ces permissions soient actives.</string>
<string name="root_accepted">Accès au shell root autorisé</string>
<string name="set_custom_ping_ip">Personnaliser l\'ip à pinguer</string>
<string name="set_custom_ping_ip">Personnaliser l\'ip de ping</string>
<string name="optional_default">"optionnel, par défaut : "</string>
<string name="show_amnezia_properties">Voir les propriétés d\'Amnezia</string>
<string name="never">jamais</string>
<string name="handshake">handshake</string>
<string name="logs">Journaux</string>
<string name="error_invalid_code">Code QR invalide</string>
<string name="enabled_app_shortcuts">Activer les raccourcis</string>
<string name="enabled_app_shortcuts">Activer les raccourcis de l\'app</string>
<string name="unknown_error">Une erreur inconnue s\'est produite</string>
<string name="email_subject">Assistance WG Tunnel</string>
<string name="location_services_missing_message">L\'application détecte qu\'aucun service de localisation n\'est activé sur votre appareil. Selon votre appareil, cela peut empêcher la fonctionnalité « Wi-Fi approuvés » de lire le nom du Wi-Fi. Souhaitez-vous tout de même continuer?</string>
<string name="location_services_missing_message">L\'application détecte qu\'aucun service de localisation n\'est activé sur votre appareil. Selon votre appareil, cela peut empêcher la fonctionnalité « Wi-Fi de confiance » de lire le nom du Wi-Fi. Souhaitez-vous tout de même continuer?</string>
<string name="yes">Oui</string>
<string name="all">tout</string>
<string name="set_primary_tunnel">Définir comme tunnel principal</string>
@@ -124,7 +132,7 @@
<string name="always_on_message2">afin de s\'assurer que le VPN permanent est désactivé pour toutes les autres applis puis réessayer</string>
<string name="chat_description">Rejoindre la communauté</string>
<string name="background_location_message">L\'accès à la permission de localisation permanente et/ou la localisation précise est nécessaire pour cette fonctionnalité. Veuillez vérifier dans</string>
<string name="default_ping_ip">(facultatif, par défaut les pairs)</string>
<string name="default_ping_ip">(facultatif, par défaut aux pairs)</string>
<string name="set_custom_ping_internal">Intervalle de ping (sec)</string>
<string name="set_custom_ping_cooldown">Temps d\'attente avant redémarrage du ping (sec)</string>
<string name="sec">sec</string>
@@ -142,7 +150,8 @@
<string name="on_demand_rules">Règles de tunnel à la demande</string>
<string name="launch_app_settings">Ouvrir les paramètres de l\'appli</string>
<string name="display_theme">Thème d\'affichage</string>
<string name="trusted_wifi_names">Nom wifi approuvés</string>
<string name="selected">Sélectionné</string>
<string name="trusted_wifi_names">Nom wifi de confiance</string>
<string name="add_wifi_name">Ajouter un nom de wifi</string>
<string name="mobile_tunnel">Tunnel de données mobiles</string>
<string name="skip">Passer</string>
@@ -152,7 +161,7 @@
<string name="wifi_name_via_shell">Nom du Wifi via le shell</string>
<string name="use_root_shell_for_wifi">Utiliser un shell root pour obtenir le nom du wifi</string>
<string name="tunnel_running">Tunnel en cours d\'exécution</string>
<string name="monitoring_state_changes">Surveille les changements d’état</string>
<string name="monitoring_state_changes">Surveiller les changements d’état</string>
<string name="donate">Faire un don au projet</string>
<string name="local_logging">Journalisation locale</string>
<string name="enable_local_logging">Activer la journalisation locale</string>
+4
View File
@@ -26,6 +26,7 @@
<string name="create_import">Buat dari awal</string>
<string name="turn_on_tunnel">Tindakan memerlukan tunnel aktif</string>
<string name="add_peer">Tambahkan rekan</string>
<string name="done">Selesai</string>
<string name="interface_">Interface</string>
<string name="rotate_keys">Putar tombol</string>
<string name="private_key">Kunci privat</string>
@@ -56,6 +57,7 @@
<string name="auto_tunnel_title">Layanan Auto-tunnel</string>
<string name="yes">Ya</string>
<string name="tunneling_apps">Aplikasi tunneling</string>
<string name="included">termasuk</string>
<string name="incorrect_pin">Pin salah</string>
<string name="pin_created">Pin berhasil dibuat</string>
<string name="enter_pin">Masukkan pin Anda</string>
@@ -94,6 +96,7 @@
<string name="never">tidak pernah</string>
<string name="sec">dtk</string>
<string name="handshake">handshake</string>
<string name="vpn_channel_id">Saluran VPN</string>
<string name="vpn_channel_name">Notifikasi Saluran VPN</string>
<string name="prominent_background_location_message">Fitur ini memerlukan izin lokasi latar belakang untuk mengaktifkan pemantauan SSID Wi-Fi bahkan saat aplikasi ditutup. Untuk detail lebih lanjut, silakan lihat Kebijakan Privasi yang ditautkan di layar Dukungan.</string>
<string name="copy_public_key">Salin kunci publik</string>
@@ -104,6 +107,7 @@
<string name="delete_tunnel_message">Apakah Anda yakin ingin menghapus tunnel ini?</string>
<string name="no_email_detected">Tidak ada aplikasi email yang terdeteksi</string>
<string name="no_browser_detected">Tidak ada browser yang terdeteksi</string>
<string name="excluded">dikecualikan</string>
<string name="all">semua</string>
<string name="open_issue">Membuka masalah</string>
<string name="read_logs">Baca log</string>
+1 -58
View File
@@ -1,59 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="error_file_extension">Il file non è .conf o .zip</string>
<string name="allowed_ips">IP consentiti</string>
<string name="endpoint">Endpoint</string>
<string name="interface_">Interfaccia</string>
<string name="rotate_keys">Ruota chiavi</string>
<string name="private_key">Chiave privata</string>
<string name="base64_key">chiave base64</string>
<string name="comma_separated_list">elenco separato da virgola</string>
<string name="listen_port">Porta d\'ascolto</string>
<string name="error_authorization_failed">Autorizzazione fallita</string>
<string name="error_no_file_explorer">Nessun esploratore di file installato</string>
<string name="error_invalid_code">QR code non valido</string>
<string name="app_name">Tunnel WG</string>
<string name="vpn_channel_name">Canale di notifica VPN</string>
<string name="turn_off_tunnel">L\'operaz. richiede la disatt. del tunnel</string>
<string name="public_key">Chiave pubblica</string>
<string name="addresses">Indirizzi</string>
<string name="dns_servers">Server DNS</string>
<string name="enabled_app_shortcuts">Abilita le scorciatoie da app</string>
<string name="export_configs">Esporta configurazioni</string>
<string name="email_subject">Supporto di Tunnel WG</string>
<string name="email_chooser">Invia un email…</string>
<string name="docs_description">Read the docs</string>
<string name="email_description">Mandami un\'email (in inglese)</string>
<string name="no_tunnels">Ancora nessun tunnel aggiunto!</string>
<string name="peer">Peer</string>
<string name="mtu">MTU</string>
<string name="random">(casuale)</string>
<string name="optional">(opzionale)</string>
<string name="optional_no_recommend">(opzionale, non raccomandato)</string>
<string name="preshared_key">Chiave pre-condivisa</string>
<string name="error_authentication_failed">Autenticazione fallita</string>
<string name="tunnels">Tunnels</string>
<string name="tunnel_mobile_data">Tunnel su dati mobili</string>
<string name="privacy_policy">Guarda la policy sulla privacy</string>
<string name="okay">Okay</string>
<string name="tunnel_on_ethernet">Tunnel su ethernet</string>
<string name="name">Nome</string>
<string name="always_on_vpn_support">Permetti VPN sempre attiva</string>
<string name="location_services_not_detected">Servizi di localizzazione non rilevati</string>
<string name="hint_search_packages">Cerca pacchetti</string>
<string name="auto_tunneling">Tunnel automatico</string>
<string name="vpn_on">VPN on</string>
<string name="vpn_off">VPN off</string>
<string name="create_import">Crea da zero</string>
<string name="turn_on_tunnel">L\'operazione richiede un tunnel attivo</string>
<string name="add_peer">Aggiungi peer</string>
<string name="copy_public_key">Copia chiave pubblica</string>
<string name="seconds">secondi</string>
<string name="persistent_keepalive">Keepalive persistente</string>
<string name="cancel">Annulla</string>
<string name="unknown_error">Avvenuto errore sconosciuto</string>
<string name="tunnel_on_wifi">Tunnel su wifi non fidato</string>
<string name="use_kernel">Usa modulo kernel</string>
<string name="error_ssid_exists">L\'SSID esiste già</string>
<string name="error_root_denied">Shell di root negata</string>
</resources>
<resources></resources>
+2 -1
View File
@@ -2,5 +2,6 @@
<resources>
<string name="app_name">WG Tunnel</string>
<string name="error_file_extension">ファイルが.confまたは.zipではありません</string>
<string name="vpn_channel_id">VPNチャンネル</string>
<string name="vpn_channel_name">VPN通知チャンネル</string>
</resources>
</resources>
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+9 -1
View File
@@ -4,13 +4,15 @@
<string name="turn_off_tunnel">Actie vereist uitgeschakelde tunnel</string>
<string name="tunnel_mobile_data">Tunnel bij mobiele data</string>
<string name="app_name">WG Tunnel</string>
<string name="vpn_channel_name">VPN Notificatiekanaal</string>
<string name="vpn_channel_id">VPN Kanaal</string>
<string name="vpn_channel_name">VPN Notificatiekanaal</string>
<string name="tunnels">Tunnels</string>
<string name="privacy_policy">Bekijk privacybeleid</string>
<string name="tunnel_on_ethernet">Tunnel bij ethernet</string>
<string name="no_tunnels">Nog geen tunnels geconfigureerd!</string>
<string name="okay">Oke</string>
<string name="error_authentication_failed">Authenticatie mislukt</string>
<string name="icon">Icoon</string>
<string name="include">Meenemen</string>
<string name="addresses">Adres</string>
<string name="peer">Peer (extern systeem)</string>
@@ -34,14 +36,18 @@
<string name="add_from_qr">Toevoegen met QR code</string>
<string name="qr_scan">QR scannen</string>
<string name="exclude">Uitsluiten</string>
<string name="tunnel_all">Alle applicaties tunnelen</string>
<string name="config_changes_saved">Configuratiewijzigingen opgeslagen.</string>
<string name="comma_separated_list">Komma-gescheiden lijst</string>
<string name="open_file">Bestand openen</string>
<string name="public_key">Publieke sleutel</string>
<string name="tunnel_name">Tunnelnaam</string>
<string name="save_changes">Opslaan</string>
<string name="dns_servers">DNS servers</string>
<string name="mtu">MTU</string>
<string name="db_name">wg-tunnel-db</string>
<string name="vpn_on">VPN aan</string>
<string name="done">Gereed</string>
<string name="interface_">Interface</string>
<string name="create_import">Nieuw beginnen</string>
<string name="rotate_keys">Sleutels roteren</string>
@@ -56,6 +62,7 @@
<string name="preshared_key">Pre-shared key</string>
<string name="auto_tunnel_title">Auto-tunnel service</string>
<string name="error_invalid_code">Ongeldige QR code</string>
<string name="included">inclusief</string>
<string name="open_issue">Open een melding</string>
<string name="create_pin">Stel PIN in</string>
<string name="enable_app_lock">Schakel app-lock in</string>
@@ -83,6 +90,7 @@
<string name="junk_packet_count">Junk packetteller</string>
<string name="incorrect_pin">Foutieve PIN</string>
<string name="enter_pin">Vul PIN in</string>
<string name="excluded">exclusief</string>
<string name="pin_created">PIN aangemaakt</string>
<string name="use_tunnel_on_wifi_name">Gebruik tunnel bij WiFi naam</string>
<string name="error_file_format">Ongeldige tunnelconfiguratie</string>
+1 -192
View File
@@ -2,195 +2,4 @@
<resources>
<string name="tunnels">Tunele</string>
<string name="app_name">WG Tunnel</string>
<string name="unsure_how">jeśli nie masz pewności, jak postępować</string>
<string name="getting_started_guide">przewodnik wprowadzający</string>
<string name="peer">Peer</string>
<string name="background_location_message2">w celu upewnienia się, że uprawnienia te są włączone</string>
<string name="rotate_keys">Rotuj klucze</string>
<string name="tunnel_on_ethernet">Tunel przez sieć ethernetową</string>
<string name="public_key">Klucz publiczny</string>
<string name="addresses">Adresy</string>
<string name="mtu">MTU</string>
<string name="auto_tunneling">Autotunelowanie</string>
<string name="vpn_on">VPN włączono</string>
<string name="vpn_off">VPN wyłączono</string>
<string name="turn_on_tunnel">Czynność wymaga aktywnego tunelu</string>
<string name="interface_">Interfejs</string>
<string name="enabled_app_shortcuts">Włącz skróty aplikacji</string>
<string name="privacy_policy">Wyświetl politykę prywatności</string>
<string name="tunnel_mobile_data">Tunel przez mobilną transmisję danych</string>
<string name="hint_search_packages">Wyszukaj pakiety</string>
<string name="random">(losowy)</string>
<string name="pin_created">Kod PIN został pomyślnie utworzony</string>
<string name="enter_pin">Podaj swój kod PIN</string>
<string name="enable_app_lock">Włącz blokadę aplikacji</string>
<string name="response_packet_junk_size">Rozmiar śmieciowego pakietu odpowiedzi</string>
<string name="response_packet_magic_header">Nagłówek magicznego pakietu odpowiedzi</string>
<string name="transport_packet_magic_header">Nagłówek magicznego pakietu transportowego</string>
<string name="underload_packet_magic_header">Nagłówek magicznego pakietu niedociążenia</string>
<string name="see_the">Zobacz</string>
<string name="error_file_format">Nieprawidłowy format konfiguracji tunelu</string>
<string name="restart_at_boot">Uruchom ponownie przy rozruchu</string>
<string name="never">nigdy</string>
<string name="sec">sek.</string>
<string name="handshake">uzgadnianie</string>
<string name="show_amnezia_properties">Pokaż właściwości protokołu Amnezia</string>
<string name="seconds">sek.</string>
<string name="prominent_background_location_message">Ta funkcja wymaga pozwolenia na dostęp do lokalizacji w tle, aby włączyć monitorowanie SSID sieci Wi-Fi nawet wtedy, gdy aplikacja jest zamknięta. Więcej szczegółów znajdziesz w polityce prywatności znajdującej się na ekranie Obsługa.</string>
<string name="optional">(opcjonalnie)</string>
<string name="optional_no_recommend">(opcjonalnie, niezalecane)</string>
<string name="preshared_key">Klucz wstępnie udostępniony</string>
<string name="location_services_missing_message">Aplikacja nie wykrywa żadnych usług lokalizacyjnych włączonych na tym urządzeniu. W zależności od urządzenia może to spowodować, że funkcja niezaufanej sieci Wi-Fi nie będzie w stanie odczytać nazwy sieci Wi-Fi. Czy chcesz kontynuować mimo to?</string>
<string name="read_logs">Przeczytaj dzienniki</string>
<string name="support">Obsługa</string>
<string name="init_packet_magic_header">Nagłówek magicznego pakietu początkowego</string>
<string name="chat_description">Dołącz do społeczności</string>
<string name="cancel">Anuluj</string>
<string name="always_on_message2">w celu upewnienia się, że funkcja stałego VPN jest wyłączona dla wszystkich innych aplikacji, następnie spróbuj ponownie</string>
<string name="requires_app_relaunch">Ta zmiana wymaga ponownego uruchomienia aplikacji. Czy chcesz kontynuować?</string>
<string name="use_tunnel_on_wifi_name">Użyj tunelu na nazwę sieci Wi-Fi</string>
<string name="root_accepted">Zezwolono na powłokę użytkownika root</string>
<string name="tunneling_apps">Aplikacje tunelujące</string>
<string name="vpn_channel_name">Kanał powiadomień VPN</string>
<string name="create_import">Utwórz od nowa</string>
<string name="error_invalid_code">Nieprawidłowy kod QR</string>
<string name="always_on_message">Zezwolenie na połączenie VPN zostało odrzucone. Sprawdź</string>
<string name="persistent_keepalive">Trwałe utrzymywanie połączenia</string>
<string name="vpn_settings">Ustawienia systemowe VPN</string>
<string name="no_browser_detected">Nie wykryto przeglądarki</string>
<string name="configuration_change">Zmiana konfiguracji</string>
<string name="prominent_background_location_title">Ujawnienie lokalizacji w tle</string>
<string name="email_subject">Obsługa aplikacji WG Tunnel</string>
<string name="auto">(automatycznie)</string>
<string name="error_authorization_failed">Nie udało się autoryzować</string>
<string name="add_from_clipboard">Dodaj ze schowka</string>
<string name="delete_tunnel_message">Czy na pewno chcesz usunąć ten tunel?</string>
<string name="tunnel_on_wifi">Tunel przez niezaufaną sieć Wi-Fi</string>
<string name="dns_servers">Serwery DNS</string>
<string name="error_file_extension">Plik nie jest w formacie .conf lub .zip</string>
<string name="export_configs">Eksportuj konfiguracje</string>
<string name="copy_public_key">Skopiuj klucz publiczny</string>
<string name="restart_on_ping">Uruchom ponownie w przypadku niepowodzenia pingowania (beta)</string>
<string name="junk_packet_minimum_size">Minimalny rozmiar pakietu śmieciowego</string>
<string name="open_issue">Otwórz zagadnienie</string>
<string name="thank_you">Dziękujemy za korzystanie z aplikacji WG Tunnel!</string>
<string name="error_authentication_failed">Uwierzytelnianie nie powiodło się</string>
<string name="use_wildcards">Użyj symboli wieloznacznych nazw</string>
<string name="yes">Tak</string>
<string name="add_peer">Dodaj peera</string>
<string name="kernel">Jądro</string>
<string name="wildcards_active">Symbole wieloznaczne aktywne</string>
<string name="create_pin">Utwórz kod PIN</string>
<string name="junk_packet_maximum_size">Maksymalny rozmiar pakietu śmieciowego</string>
<string name="local_logging">Lokalne rejestrowanie</string>
<string name="monitoring_state_changes">Monitorowanie zmian stanu</string>
<string name="version">Wersja</string>
<string name="add_tunnels_text">Dodaj z pliku lub archiwum ZIP</string>
<string name="unknown_error">Wystąpił nieznany błąd</string>
<string name="start_auto">Uruchom autotunel</string>
<string name="location_services_not_detected">Usługi lokalizacyjne nie zostały wykryte</string>
<string name="background_location_message">Ta funkcja wymaga pozwolenia na określenie lokalizacji w dowolnym momencie i/lub dokładnej lokalizacji. Sprawdź</string>
<string name="auto_tunnel_title">Usługa autotunelu</string>
<string name="donate">Przekaż darowiznę na rzecz projektu</string>
<string name="trusted_ssid_value_description">Prześlij SSID</string>
<string name="all">wszystkie</string>
<string name="no_email_detected">Nie wykryto aplikacji e-mail</string>
<string name="turn_off_tunnel">Czynność wymaga wyłączenia tunelu</string>
<string name="add_from_qr">Dodaj z kodu QR</string>
<string name="qr_scan">Skanuj kod QR</string>
<string name="tunnel_name">Nazwa tunelu</string>
<string name="include">Uwzględnij</string>
<string name="config_changes_saved">Zmiany konfiguracji zostały zapisane.</string>
<string name="allowed_ips">Dozwolone adresy IP</string>
<string name="endpoint">Punkt końcowy</string>
<string name="name">Nazwa</string>
<string name="private_key">Klucz prywatny</string>
<string name="comma_separated_list">lista rozdzielona przecinkami</string>
<string name="listen_port">Port nasłuchu</string>
<string name="always_on_vpn_support">Zezwalaj na stały VPN</string>
<string name="base64_key">klucz base64</string>
<string name="use_kernel">Użyj modułu jądra</string>
<string name="error_ssid_exists">SSID już istnieje</string>
<string name="error_no_file_explorer">Eksplorator plików nie jest zainstalowany</string>
<string name="error_root_denied">Odmowa powłoki użytkownika root</string>
<string name="delete_tunnel">Usuń tunel</string>
<string name="incorrect_pin">Kod PIN jest nieprawidłowy</string>
<string name="edit_tunnel">Edytuj tunel</string>
<string name="settings">Ustawienia</string>
<string name="junk_packet_count">Liczba pakietów śmieciowych</string>
<string name="init_packet_junk_size">Rozmiar śmieciowego pakietu początkowego</string>
<string name="vpn_denied_dialog_title">Odmowa zezwolenia</string>
<string name="tunnel_required">Funkcja wymaga co najmniej jednego tunelu</string>
<string name="app_settings">ustawienia aplikacji</string>
<string name="set_custom_ping_ip">Ustaw niestandardowy adres IP pingowania</string>
<string name="default_ping_ip">(opcjonalnie, domyślnie do peerów)</string>
<string name="set_custom_ping_internal">Interwał pingowania (sek.)</string>
<string name="optional_default">"opcjonalnie, domyślnie: "</string>
<string name="logs">Dzienniki</string>
<string name="kill_switch">Wyłącznik awaryjny</string>
<string name="appearance">Wygląd</string>
<string name="notifications">Powiadomienia</string>
<string name="automatic">Automatyczny</string>
<string name="light">Jasny</string>
<string name="dark">Ciemny</string>
<string name="dynamic">Dynamiczny</string>
<string name="language">Język</string>
<string name="display_theme">Motyw wyświetlania</string>
<string name="trusted_wifi_names">Nazwy zaufanych sieci Wi-Fi</string>
<string name="add_wifi_name">Dodaj nazwę sieci Wi-Fi</string>
<string name="on_demand_rules">Zasady korzystania z tunelu na żądanie</string>
<string name="primary_tunnel">Tunel podstawowy</string>
<string name="mobile_tunnel">Tunel mobilnej transmisji danych</string>
<string name="skip">Pomiń</string>
<string name="launch_app_settings">Uruchom ustawienia aplikacji</string>
<string name="learn_more">Dowiedz się więcej</string>
<string name="wifi_name_via_shell">Nazwa sieci Wi-Fi poprzez powłokę</string>
<string name="use_root_shell_for_wifi">Użyj powłoki użytkownika root, aby uzyskać nazwę sieci Wi-Fi</string>
<string name="kernel_not_supported">Jądro nie jest obsługiwane</string>
<string name="stop_auto">Zatrzymaj autotunel</string>
<string name="tunnel_running">Tunel jest uruchomiony</string>
<string name="enable_local_logging">Włącz lokalne rejestrowanie</string>
<string name="email_chooser">Wyślij e-mail…</string>
<string name="set_custom_ping_cooldown">Czas odnowienia pingowania (sek.)</string>
<string name="open_file">Otwórz plik</string>
<string name="okay">OK</string>
<string name="no_tunnels">Nie dodano jeszcze żadnych tuneli!</string>
<string name="exclude">Wyklucz</string>
<string name="docs_description">Przeczytaj dokumentację</string>
<string name="email_description">Wyślij mi e-mail</string>
<string name="set_primary_tunnel">Ustaw jako tunel podstawowy</string>
<string name="mobile_data_tunnel">Ustaw jako tunel mobilnej transmisji danych</string>
<string name="vpn_channel_id">Kanał VPN</string>
<string name="stop_on_no_internet">Zatrzymaj, gdy nie ma Internetu</string>
<string name="stop_on_internet_loss">Zatrzymaj tunel przy utracie Internetu</string>
<string name="ethernet_tunnel">Tunel ethernetowy</string>
<string name="set_ethernet_tunnel">Ustaw jako tunel ethernetowy</string>
<string name="allow_lan_traffic">Zezwól na ruch LAN</string>
<string name="bypass_lan_for_kill_switch">Omiń LAN dla wyłącznika awaryjnego</string>
<string name="vpn_channel_description">Kanał powiadomień o stanie VPN</string>
<string name="auto_tunnel_channel_name">Kanał powiadomień autotunelu</string>
<string name="auto_tunnel_channel_description">Kanał powiadomień o stanie autotunelowania</string>
<string name="stop">zatrzymaj</string>
<string name="splt_tunneling">Tunelowanie rozdzielone</string>
<string name="tunnel_specific_settings">Ustawienia specyficzne dla tunelu</string>
<string name="pre_up">Przed aktywacją</string>
<string name="post_up">Po aktywacji</string>
<string name="pre_down">Przed dezaktywacją</string>
<string name="post_down">Po dezaktywacji</string>
<string name="amnezia_kernel_message">Protokół Amnezia niedostępny w trybie jądra</string>
<string name="enable_amnezia">Włącz protokół Amnezia</string>
<string name="wg_compat_mode">Tryb zgodności WG</string>
<string name="quick_actions">Szybkie czynności</string>
<string name="native_kill_switch">Natywny wyłącznik awaryjny</string>
<string name="vpn_kill_switch">Wyłącznik awaryjny VPN</string>
<string name="kill_switch_options">Opcje wyłącznika awaryjnego</string>
<string name="show_scripts">Pokaż skrypty</string>
<string name="hide_scripts">Ukryj skrypty</string>
<string name="debounce_delay">Opóźnienie odbicia</string>
<string name="enable_amnezia_compatibility">Włącz zgodność z protokołem Amnezia</string>
<string name="remove_amnezia_compatibility">Usuń zgodność z protokołem Amnezia</string>
<string name="exclude_lan">Wyklucz LAN</string>
<string name="include_lan">Uwzględnij LAN</string>
<string name="advanced_settings">Ustawienia zaawansowane</string>
<string name="hide_amnezia_properties">Ukryj właściwości protokołu Amnezia</string>
</resources>
</resources>
+9 -1
View File
@@ -10,6 +10,7 @@
<string name="background_location_message2">para garantir que essas permissões estejam ativadas.</string>
<string name="optional_no_recommend">(opcional, não recomendado)</string>
<string name="dns_servers">Servidores DNS</string>
<string name="save_changes">Gravar</string>
<string name="add_from_qr">Adicionar a partir de código QR</string>
<string name="tunnels">Túneis</string>
<string name="email_description">Me envie um email</string>
@@ -43,6 +44,7 @@
<string name="delete_tunnel_message">Tem certeza que quer apagar este túnel?</string>
<string name="yes">Sim</string>
<string name="tunneling_apps">Aplicações com túnel</string>
<string name="excluded">excluído</string>
<string name="all">todos</string>
<string name="auto">(automático)</string>
<string name="pin_created">Pin criado com sucesso</string>
@@ -76,6 +78,7 @@
<string name="vpn_on">VPN ligada</string>
<string name="vpn_off">VPN desligada</string>
<string name="add_peer">Adicionar par</string>
<string name="done">Feito</string>
<string name="interface_">Interface</string>
<string name="base64_key">Chave base64</string>
<string name="persistent_keepalive">Manter a conexão persistente (keepalive)</string>
@@ -84,6 +87,7 @@
<string name="error_ssid_exists">SSID já existe</string>
<string name="error_root_denied">Shell Root negado</string>
<string name="delete_tunnel">Apagar túnel</string>
<string name="included">incluso</string>
<string name="junk_packet_maximum_size">Tamanho máximo de pacote-lixo</string>
<string name="init_packet_junk_size">Tamanho de pacote-lixo inicial</string>
<string name="vpn_denied_dialog_title">Permissão negada</string>
@@ -93,17 +97,21 @@
<string name="auto_tunnel_title">Serviço de Auto-túnel</string>
<string name="error_no_file_explorer">Nenhum explorador de ficheiros instalado</string>
<string name="app_name">WG Tunnel</string>
<string name="vpn_channel_name">Canal de notificações VPN</string>
<string name="vpn_channel_id">Canal de VPN</string>
<string name="vpn_channel_name">Canal de notificações VPN</string>
<string name="prominent_background_location_message">Este recurso precisa de permissões de localização em segundo plano para ativar o monitoramento do SSID da rede Wi-Fi mesmo quando a aplicação está fechado. Para mais pormenores, por favor veja a Política de Privacidade no ecrã de Suporte.</string>
<string name="trusted_ssid_value_description">Envie o SSID</string>
<string name="add_tunnels_text">Adicionar a partir de ficheiro ou zip</string>
<string name="open_file">Abrir Ficheiro</string>
<string name="exclude">Excluir</string>
<string name="include">Incluir</string>
<string name="tunnel_all">Todas as aplicações pelo túnel</string>
<string name="config_changes_saved">Mudanças nas configurações gravadas.</string>
<string name="icon">Ícone</string>
<string name="public_key">Chave pública</string>
<string name="addresses">Endereços</string>
<string name="peer">Par</string>
<string name="db_name">wg-tunnel-db</string>
<string name="rotate_keys">Revezar chaves</string>
<string name="private_key">Chave privada</string>
<string name="copy_public_key">Copiar chave pública</string>

Some files were not shown because too many files have changed in this diff Show More