Compare commits

..

58 Commits

Author SHA1 Message Date
Zane Schepke d44baa84a8 feat: improved imports
Added a feature where you can now add a commented "# Name = " property to QR code configs to import them with a name. If there is no name configured, app will use the first peer's host address as a name.

Improved imports so they no longer replace an existing config if that config has the same name. Instead, they will import with a (number) appended for config name duplicates.

Closes #68

Fixed a bug where the initial state of auto tunneling may not be correct and cause unexpected behavior.

Fixed a bug where Amnezia imports were not working when being imported as a zip.

Improved/refactored error handling.
2024-05-11 20:42:24 -04:00
Zane Schepke cb1b8ee7d6 add version metadata 2024-05-11 00:18:58 -04:00
Zane Schepke 4153351fc4 Merge branch 'main' of https://github.com/zaneschepke/wgtunnel 2024-05-10 23:52:43 -04:00
dependabot[bot] 48e6f341cb build(deps): bump actions/upload-artifact from 4.3.1 to 4.3.3 (#179)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-10 23:52:03 -04:00
Zane Schepke d531adede5 remove comments 2024-05-10 23:49:38 -04:00
Zane Schepke 2df1bb07ab change auto tunnel to not watch vpn state 2024-05-10 23:46:50 -04:00
Zane Schepke a5e60c3fbe add amnezia import/export 2024-05-10 21:42:59 -04:00
Weblate (bot) e4af481402 Translations update from Hosted Weblate (#206)
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
2024-05-10 12:34:10 -04:00
Weblate (bot) 77b3fc8360 Translations update from Hosted Weblate (#204)
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
2024-05-09 15:26:47 -04:00
Zane Schepke 4fd908f271 fix: notification workflow links 2024-05-08 17:30:00 -04:00
maskedeken 632da245ab fix kernel mode switch not work (#196) 2024-05-08 17:25:28 -04:00
Weblate (bot) 04f22cb92d Translations update from Hosted Weblate (#200)
Co-authored-by: Gabriel Franz <gabrielfranz31@gmail.com>
2024-05-08 13:13:58 -04:00
Weblate (bot) 31194d8b88 Translations update from Hosted Weblate (#194)
Co-authored-by: Mat1RX <ladved@gmail.com>
2024-05-05 21:39:09 -04:00
Zane Schepke 421bf418d1 fix: fastlane metadata 2024-05-05 21:36:44 -04:00
Zane Schepke e84d7e354c feat: add amnezia side-by-side 2024-05-05 00:49:31 -04:00
Zane Schepke 681b066d99 Update issue-workflow.yml 2024-05-03 22:54:35 -04:00
Zane Schepke ad53fca928 Create publish-workflow.yml 2024-05-03 22:53:42 -04:00
Zane Schepke f7e4b7e8ef Update issue-workflow.yml 2024-05-03 22:42:44 -04:00
Zane Schepke b04e8e7f60 Update issue-workflow.yml 2024-05-03 22:38:37 -04:00
Zane Schepke cbee5cfd1b Create issue-workflow.yml 2024-05-03 22:26:05 -04:00
Weblate (bot) 440fe6ceda Translations update from Hosted Weblate (#182)
Co-authored-by: Dominik Thalhammer <dominik@thalhammer.it>
2024-04-29 00:00:28 -04:00
Zane Schepke 27def018bd ci: fix spacing 2024-04-27 21:50:13 -04:00
Zane Schepke 16979dbb2b Update ic_channel.xml 2024-04-26 01:50:08 -04:00
Hosted Weblate 5d8190628d Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/
2024-04-26 01:50:08 -04:00
Anonymous 6d597a235d Translated using Weblate (Spanish)
Currently translated at 100.0% (159 of 159 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/es/
2024-04-26 01:49:49 -04:00
Anonymous 61fd2f01b9 Translated using Weblate (German)
Currently translated at 49.0% (78 of 159 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/de/
2024-04-26 01:49:08 -04:00
Anonymous 914a641977 Translated using Weblate (Russian)
Currently translated at 3.1% (5 of 159 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/ru/
2024-04-26 01:49:08 -04:00
Anonymous c97a3afbc4 Translated using Weblate (Turkish)
Currently translated at 100.0% (159 of 159 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/tr/
2024-04-26 01:49:08 -04:00
gallegonovato fadc2d1562 Translated using Weblate (Spanish)
Currently translated at 100.0% (159 of 159 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/es/
2024-04-26 01:49:08 -04:00
Languages add-on 106ab76b82 Added translation using Weblate (Spanish) 2024-04-26 01:48:39 -04:00
Weblate (bot) 48b3f60b37 Translations update from Hosted Weblate (#177)
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Gabriel Franz <gabrielfranz31@gmail.com>
Co-authored-by: Saratoga79 <ordizi79@gmail.com>
2024-04-24 03:49:57 -04:00
WINZORT e7f5cee6dd Add fastlane tr (#173) 2024-04-22 23:21:19 -04:00
Zane Schepke 5e01fd6c85 Merge pull request #175 from zaneschepke/i18n
Translations update from Hosted Weblate
2024-04-22 23:18:46 -04:00
Dominik Thalhammer dcbd72c6f6 Translated using Weblate (German)
Currently translated at 40.9% (9 of 22 strings)

Translation: WG Tunnel/fastlane
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/fastlane/de/
2024-04-22 13:18:21 +02:00
Languages add-on f27aa1452a Added translation using Weblate (German) 2024-04-22 07:52:45 +02:00
Languages add-on 82c3521c58 Added translation using Weblate (Russian) 2024-04-21 06:19:53 +02:00
Zane Schepke c6535ffe7e chore: update chats 2024-04-20 18:36:40 -04:00
Zane Schepke be5fe94ce6 chore: cleanup unused strings 2024-04-20 17:42:14 -04:00
Zane Schepke 1e905c1cfb chore: update README.md 2024-04-20 17:09:36 -04:00
Zane Schepke a549c111aa Merge pull request #170 from yurtpage/i18n
i18n
2024-04-20 16:28:15 -04:00
Zane Schepke 5ab344044e chore: add Weblate details to README 2024-04-20 14:30:21 -04:00
Zane Schepke 7030f68548 chore: update README 2024-04-20 13:57:33 -04:00
Zane Schepke ecd51b6fe5 chore: add code of conduct 2024-04-20 13:52:28 -04:00
Yurt Page 3e4f8e0791 fastlane i18n ru
Signed-off-by: Yurt Page <yurtpage@gmail.com>
2024-04-19 18:28:58 +03:00
Zane Schepke e940a0dbc5 Merge pull request #167 from mikropsoft/main
Improve tr locales
2024-04-18 22:12:21 -04:00
WINZORT 8e233475df Update strings.xml 2024-04-17 17:41:49 +03:00
WINZORT 24789826ad Update strings.xml 2024-04-17 17:38:00 +03:00
WINZORT 787e0d1a71 Update strings.xml 2024-04-17 17:37:22 +03:00
WINZORT 914ef53ef0 Improve tr locales 2024-04-17 17:23:27 +03:00
Zane Schepke a569974beb add Turkish localization 2024-04-16 16:13:41 -04:00
Zane Schepke 87bc89b6f1 Merge branch 'main' of https://github.com/zaneschepke/wgtunnel 2024-04-16 00:50:43 -04:00
Zane Schepke c343220e96 Merge pull request #156 from mikropsoft/main
Add tr locales
2024-04-16 00:41:14 -04:00
Zane Schepke 5447ec73f7 fix: stop tunnel regression
Fixes regression where tunnel is stuck in on state after x amount of toggles. Closes #163

Adds obfuscation of potentially sensitive data from logs.
Closes #160

Adds hiding of FAB on scroll to allow users to toggle tunnels when they have many tunnel configs.
Closes #161
2024-04-16 00:14:06 -04:00
Zane Schepke a2b8eb5b0b ci: update release wording 2024-04-06 11:06:19 -04:00
Zane Schepke e37777e662 fix: improve auto-tunnel reliability
Improved service manager stop service command to improve reliability
Closes #148

Added assets required by Google Play for AndroidTV to fix Google Play release

Added apk fingerprint hash to release to allow verification of apk signature via apksigner

Improve tile sync when tiles are first added

Bump versions
2024-04-06 10:43:00 -04:00
WINZORT ee3fcabcf1 Add tr locales 2024-04-06 15:39:46 +03:00
Zane Schepke 2a8895ffbc docs: update readme, add security 2024-03-30 23:16:19 -04:00
Zane Schepke b1fdb5b9b2 feat: auto-tunneling flexibility
Added tunnel settings feature where users can configure a tunnel to be used on certain SSID or with mobile data.
Closes #50

Added feature where if a tunnel was active when phone restarted, the app will start that tunnel on boot.

Removed automatic auto-tunnel toggling/override from the tunnel tile and app shortcuts as it can cause undesirable behavior.

Added second tile to control auto-tunneling pause/resume state from a tile.

Added two additional static shortcuts to be able to control auto-tunneling pause/resume state from shortcuts.

Fixed bug where crashes can happen from serializing and deserializing tunnel configs by removing the need for serialization of tunnel configs.

Refactored logic of watcher and tunnel service to make state more predictable.
#127

Fixed bug where rapidly toggling tunnels can cause crashes.
Closes #145

Improved how tunnels are manually toggled from one to another.

Improved logic/storage around primary tunnel behavior.

Fixes issue where info level logs were not populating on release builds.

Increase allowed name length displayed in UI.
Closes #143

Fixes bug where androidTV could crash in certain situations.

Bump versions.

Updated screenshots.
2024-03-30 00:00:35 -04:00
144 changed files with 3093 additions and 1053 deletions
+22
View File
@@ -0,0 +1,22 @@
# Contributor Code of Conduct
## Pledge
We as individuals involved in this project, pledge to participate in this
community in a respectful, constructive, and civil manner as we work towards a common goal
of delivering free, open source, and value adding software for all.
## Standard
The standard for this community is the Golden Rule.
> “Do unto others as you would have them do unto you.”
## Scope
This Code of Conduct applies to all spaces related to WG Tunnel.
## Incidents or Concerns
For any incidents or concerns, reach out to Zane at
<support@zaneschepke.com>.
+20
View File
@@ -0,0 +1,20 @@
name: Issue Updates Workflow
on:
issues:
types: [opened, closed, reopened]
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Send Telegram Message
run: |
msg_text='${{ github.actor }} updated an issue:
status: ${{ github.event.issue.state }} - #${{ github.event.issue.number }} ${{ github.event.issue.title }}
https://github.com/zaneschepke/wgtunnel/issues/${{ github.event.issue.number }}'
curl -s -X POST 'https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN }}/sendMessage' \
-d "chat_id=${{ secrets.TELEGRAM_TO }}&text=${msg_text}&message_thread_id=${{ secrets.TELEGRAM_TOPIC }}"
+27 -2
View File
@@ -1,4 +1,3 @@
# name of the workflow
name: Android CI Tag Deployment (Pre-release) name: Android CI Tag Deployment (Pre-release)
on: on:
@@ -68,7 +67,7 @@ jobs:
echo "VERSION_CODE=$version_code" >> $GITHUB_ENV echo "VERSION_CODE=$version_code" >> $GITHUB_ENV
# Save the APK after the Build job is complete to publish it as a Github release in the next job # Save the APK after the Build job is complete to publish it as a Github release in the next job
- name: Upload APK - name: Upload APK
uses: actions/upload-artifact@v4.3.1 uses: actions/upload-artifact@v4.3.3
with: with:
name: wgtunnel name: wgtunnel
path: ${{ steps.apk-path.outputs.path }} path: ${{ steps.apk-path.outputs.path }}
@@ -89,6 +88,32 @@ jobs:
draft: false draft: false
prerelease: true prerelease: true
files: ${{ github.workspace }}/${{ steps.apk-path.outputs.path }} files: ${{ github.workspace }}/${{ steps.apk-path.outputs.path }}
- name: Install apksigner
run: |
sudo apt-get update
sudo apt-get install -y apksigner
- name: Get checksum
id: checksum
run: echo "checksum=$(apksigner verify -print-certs ${{ steps.apk-path.outputs.path }} | grep -Po "(?<=SHA-256 digest:) .*" | tr -d "[:blank:]")" >> $GITHUB_OUTPUT
- name: Append checksum
id: append_checksum
uses: softprops/action-gh-release@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
body: |
SHA256 fingerprint:
```${{ steps.checksum.outputs.checksum }}```
tag_name: ${{ github.ref_name }}
name: ${{ github.ref_name }}
draft: false
prerelease: true
append_body: true
- name: Deploy with fastlane - name: Deploy with fastlane
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1
with: with:
+21
View File
@@ -0,0 +1,21 @@
name: Release Updates Workflow
on:
release:
types: [published]
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Send Telegram Message
run: |
msg_text='${{ github.actor }} published a new release:
Release: ${{ github.event.release.tag_name }}
${{ github.event.release.body }}
https://github.com/zaneschepke/wgtunnel/releases/tag/${{ github.event.release.tag_name }}'
curl -s -X POST 'https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN }}/sendMessage' \
-d "chat_id=${{ secrets.TELEGRAM_TO }}&text=${msg_text}&message_thread_id=${{ secrets.TELEGRAM_TOPIC }}"
+27 -1
View File
@@ -69,7 +69,7 @@ jobs:
echo "VERSION_CODE=$version_code" >> $GITHUB_ENV echo "VERSION_CODE=$version_code" >> $GITHUB_ENV
# Save the APK after the Build job is complete to publish it as a Github release in the next job # Save the APK after the Build job is complete to publish it as a Github release in the next job
- name: Upload APK - name: Upload APK
uses: actions/upload-artifact@v4.3.1 uses: actions/upload-artifact@v4.3.3
with: with:
name: wgtunnel name: wgtunnel
path: ${{ steps.apk-path.outputs.path }} path: ${{ steps.apk-path.outputs.path }}
@@ -95,6 +95,32 @@ jobs:
draft: false draft: false
prerelease: false prerelease: false
files: ${{ github.workspace }}/${{ steps.apk-path.outputs.path }} files: ${{ github.workspace }}/${{ steps.apk-path.outputs.path }}
- name: Install apksigner
run: |
sudo apt-get update
sudo apt-get install -y apksigner
- name: Get checksum
id: checksum
run: echo "checksum=$(apksigner verify -print-certs ${{ steps.apk-path.outputs.path }} | grep -Po "(?<=SHA-256 digest:) .*" | tr -d "[:blank:]")" >> $GITHUB_OUTPUT
- name: Append checksum
id: append_checksum
uses: softprops/action-gh-release@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
body: |
SHA256 fingerprint:
```${{ steps.checksum.outputs.checksum }}```
tag_name: ${{ github.ref_name }}
name: ${{ github.ref_name }}
draft: false
prerelease: false
append_body: true
- name: Deploy with fastlane - name: Deploy with fastlane
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1
with: with:
+27 -9
View File
@@ -4,8 +4,9 @@ WG Tunnel
<div align="center"> <div align="center">
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?style=for-the-badge&logo=discord&logoColor=white)](https://discord.gg/rbRRNh6H7V)
[![Discord Chat](https://img.shields.io/discord/1108285024631001111.svg)](https://discord.gg/rbRRNh6H7V) [![X Community](https://img.shields.io/badge/X-000000?style=for-the-badge&logo=x&logoColor=white)](https://twitter.com/i/communities/1780655267685736818)
[![Telegram](https://img.shields.io/badge/Telegram-2CA5E0?style=for-the-badge&logo=telegram&logoColor=white)](https://t.me/wgtunnel)
</div> </div>
@@ -21,7 +22,7 @@ WG Tunnel
<div align="left"> <div align="left">
This is an alternative Android Application for [WireGuard](https://www.wireguard.com/) with added This is an alternative Android Application for [WireGuard](https://www.wireguard.com/) and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) with added
features. Built using the [wireguard-android](https://github.com/WireGuard/wireguard-android) 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 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. inspired by the official [WireGuard Android](https://github.com/WireGuard/wireguard-android) app.
@@ -49,24 +50,41 @@ and on while on different networks. This app was created to offer a free solutio
## Features ## Features
* Add tunnels via .conf file, zip, manual entry, or QR code * Add tunnels via .conf file, zip, manual entry, or QR code
* Auto connect to VPN based on Wi-Fi SSID, ethernet, or mobile data * Auto connect to tunnels based on Wi-Fi SSID, ethernet, or mobile data
* Split tunneling by application with search * Split tunneling by application with search
* WireGuard support for kernel and userspace modes * WireGuard support for kernel and userspace modes
* Amnezia support for userspace mode for DPI/censorship protection
* Always-On VPN support * Always-On VPN support
* Export tunnels to zip * Export Amnezia and WireGuard tunnels to zip
* Quick tile support for VPN toggling * Quick tile support for tunnel toggling, auto-tunneling
* Static shortcuts support for primary tunnel for automation integration * Static shortcuts support for tunnel toggling, auto-tunneling
* Intent automation support for all tunnels * Intent automation support for all tunnels
* Automatic service restart after reboot * Automatic auto-tunneling service restart after reboot
* Automatic tunnel restart after reboot
* Battery preservation measures * Battery preservation measures
* Restart tunnel on ping failure (beta)
## Docs (WIP) ## Docs
Basic documentation of the feature and behaviors of this app can be Basic documentation of the feature and behaviors of this app can be
found [here](https://zaneschepke.com/wgtunnel-docs/overview.html). found [here](https://zaneschepke.com/wgtunnel-docs/overview.html).
The repository for these docs can be found [here](https://github.com/zaneschepke/wgtunnel-docs). The repository for these docs can be found [here](https://github.com/zaneschepke/wgtunnel-docs).
## Contributing
Any contributions in the form of feedback, issues, code, or translations are welcome and much appreciated!
Please read the [code of conduct](https://github.com/zaneschepke/wgtunnel?tab=coc-ov-file#contributor-code-of-conduct) before contributing.
## Translation
This app is using [Weblate](https://weblate.org) to assist with translations.
Help translate WG Tunnel into your language at [Hosted Weblate](https://hosted.weblate.org/engage/wg-tunnel/).\
[![Translation status](https://hosted.weblate.org/widgets/wg-tunnel/-/multi-auto.svg)](https://hosted.weblate.org/engage/wg-tunnel/)
## Building ## Building
``` ```
+5
View File
@@ -0,0 +1,5 @@
# Security Policy
## Reporting a Vulnerability
Please report security issues to `support@zaneschepke.com`
+8 -2
View File
@@ -12,6 +12,10 @@ android {
namespace = Constants.APP_ID namespace = Constants.APP_ID
compileSdk = Constants.TARGET_SDK compileSdk = Constants.TARGET_SDK
androidResources {
generateLocaleConfig = true
}
defaultConfig { defaultConfig {
applicationId = Constants.APP_ID applicationId = Constants.APP_ID
minSdk = Constants.MIN_SDK minSdk = Constants.MIN_SDK
@@ -25,8 +29,6 @@ android {
getByName("debug").assets.srcDirs(files("$projectDir/schemas")) // Room getByName("debug").assets.srcDirs(files("$projectDir/schemas")) // Room
} }
resourceConfigurations.addAll(listOf("en"))
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { useSupportLibrary = true } vectorDrawables { useSupportLibrary = true }
} }
@@ -137,6 +139,7 @@ dependencies {
implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.runtime.ktx)
// helpers for implementing LifecycleOwner in a Service // helpers for implementing LifecycleOwner in a Service
implementation(libs.androidx.lifecycle.service) implementation(libs.androidx.lifecycle.service)
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)
@@ -160,6 +163,7 @@ dependencies {
// get tunnel lib from github packages or mavenLocal // get tunnel lib from github packages or mavenLocal
implementation(libs.tunnel) implementation(libs.tunnel)
implementation(libs.amneziawg.android)
coreLibraryDesugaring(libs.desugar.jdk.libs) coreLibraryDesugaring(libs.desugar.jdk.libs)
// logging // logging
@@ -170,6 +174,8 @@ dependencies {
implementation(libs.androidx.navigation.compose) implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.hilt.navigation.compose) implementation(libs.androidx.hilt.navigation.compose)
implementation(libs.zaneschepke.multifab)
// hilt // hilt
implementation(libs.hilt.android) implementation(libs.hilt.android)
ksp(libs.hilt.android.compiler) ksp(libs.hilt.android.compiler)
@@ -0,0 +1,190 @@
{
"formatVersion": 1,
"database": {
"version": 8,
"identityHash": "b4d4a7c489f6b2f0d3aa4fa6f37b4935",
"entities": [
{
"tableName": "Settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_auto_tunnel_paused` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `is_amnezia_enabled` INTEGER NOT NULL DEFAULT false)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isAutoTunnelEnabled",
"columnName": "is_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnMobileDataEnabled",
"columnName": "is_tunnel_on_mobile_data_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "trustedNetworkSSIDs",
"columnName": "trusted_network_ssids",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isAlwaysOnVpnEnabled",
"columnName": "is_always_on_vpn_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnEthernetEnabled",
"columnName": "is_tunnel_on_ethernet_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isShortcutsEnabled",
"columnName": "is_shortcuts_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isTunnelOnWifiEnabled",
"columnName": "is_tunnel_on_wifi_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isKernelEnabled",
"columnName": "is_kernel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isRestoreOnBootEnabled",
"columnName": "is_restore_on_boot_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isMultiTunnelEnabled",
"columnName": "is_multi_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isAutoTunnelPaused",
"columnName": "is_auto_tunnel_paused",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isAmneziaEnabled",
"columnName": "is_amnezia_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "TunnelConfig",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false, `is_primary_tunnel` INTEGER NOT NULL DEFAULT false, `am_quick` TEXT NOT NULL DEFAULT '')",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "wgQuick",
"columnName": "wg_quick",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tunnelNetworks",
"columnName": "tunnel_networks",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isMobileDataTunnel",
"columnName": "is_mobile_data_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPrimaryTunnel",
"columnName": "is_primary_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "amQuick",
"columnName": "am_quick",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_TunnelConfig_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b4d4a7c489f6b2f0d3aa4fa6f37b4935')"
]
}
}
+1 -1
View File
@@ -52,7 +52,7 @@
<application <application
android:name=".WireGuardAutoTunnel" android:name=".WireGuardAutoTunnel"
android:allowBackup="true" android:allowBackup="true"
android:banner="@mipmap/ic_banner" android:banner="@drawable/ic_banner"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:enableOnBackInvokedCallback="true" android:enableOnBackInvokedCallback="true"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
@@ -2,13 +2,14 @@ package com.zaneschepke.wireguardautotunnel
import android.app.Application import android.app.Application
import android.content.ComponentName import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.service.quicksettings.TileService import android.service.quicksettings.TileService
import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelControlTile import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelControlTile
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
import timber.log.Timber import timber.log.Timber
import xyz.teamgravity.pin_lock_compose.PinManager import xyz.teamgravity.pin_lock_compose.PinManager
@@ -21,7 +22,16 @@ class WireGuardAutoTunnel : Application() {
PinManager.initialize(this) PinManager.initialize(this)
} }
override fun onLowMemory() {
super.onLowMemory()
applicationScope.cancel("onLowMemory() called by system")
applicationScope = MainScope()
}
companion object { companion object {
var applicationScope = MainScope()
lateinit var instance: WireGuardAutoTunnel lateinit var instance: WireGuardAutoTunnel
private set private set
@@ -29,16 +39,16 @@ class WireGuardAutoTunnel : Application() {
return instance.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK) return instance.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
} }
fun requestTunnelTileServiceStateUpdate(context: Context) { fun requestTunnelTileServiceStateUpdate() {
TileService.requestListeningState( TileService.requestListeningState(
context, instance,
ComponentName(instance, TunnelControlTile::class.java), ComponentName(instance, TunnelControlTile::class.java),
) )
} }
fun requestAutoTunnelTileServiceUpdate(context: Context) { fun requestAutoTunnelTileServiceUpdate() {
TileService.requestListeningState( TileService.requestListeningState(
context, instance,
ComponentName(instance, AutoTunnelControlTile::class.java), ComponentName(instance, AutoTunnelControlTile::class.java),
) )
} }
@@ -6,12 +6,12 @@ import androidx.room.DeleteColumn
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.room.TypeConverters import androidx.room.TypeConverters
import androidx.room.migration.AutoMigrationSpec import androidx.room.migration.AutoMigrationSpec
import com.zaneschepke.wireguardautotunnel.data.model.Settings import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
@Database( @Database(
entities = [Settings::class, TunnelConfig::class], entities = [Settings::class, TunnelConfig::class],
version = 7, version = 8,
autoMigrations = autoMigrations =
[ [
AutoMigration(from = 1, to = 2), AutoMigration(from = 1, to = 2),
@@ -33,6 +33,7 @@ import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
to = 7, to = 7,
spec = RemoveLegacySettingColumnsMigration::class, spec = RemoveLegacySettingColumnsMigration::class,
), ),
AutoMigration(7, 8)
], ],
exportSchema = true, exportSchema = true,
) )
@@ -5,7 +5,7 @@ import androidx.room.Delete
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import com.zaneschepke.wireguardautotunnel.data.model.Settings import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@Dao @Dao
@@ -5,7 +5,7 @@ import androidx.room.Delete
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@@ -20,6 +20,9 @@ interface TunnelConfigDao {
@Query("SELECT * FROM TunnelConfig WHERE id=:id") @Query("SELECT * FROM TunnelConfig WHERE id=:id")
suspend fun getById(id: Long): TunnelConfig? suspend fun getById(id: Long): TunnelConfig?
@Query("SELECT * FROM TunnelConfig WHERE name=:name")
suspend fun getByName(name: String) : TunnelConfig?
@Query("SELECT * FROM TunnelConfig") @Query("SELECT * FROM TunnelConfig")
suspend fun getAll(): TunnelConfigs suspend fun getAll(): TunnelConfigs
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.data.model package com.zaneschepke.wireguardautotunnel.data.domain
data class GeneralState( data class GeneralState(
val locationDisclosureShown: Boolean = LOCATION_DISCLOSURE_SHOWN_DEFAULT, val locationDisclosureShown: Boolean = LOCATION_DISCLOSURE_SHOWN_DEFAULT,
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.data.model package com.zaneschepke.wireguardautotunnel.data.domain
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
@@ -50,4 +50,9 @@ data class Settings(
defaultValue = "false", defaultValue = "false",
) )
val isPingEnabled: Boolean = false, val isPingEnabled: Boolean = false,
@ColumnInfo(
name = "is_amnezia_enabled",
defaultValue = "false",
)
val isAmneziaEnabled: Boolean = false,
) )
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.data.model package com.zaneschepke.wireguardautotunnel.data.domain
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
@@ -27,12 +27,25 @@ data class TunnelConfig(
defaultValue = "false", defaultValue = "false",
) )
val isPrimaryTunnel: Boolean = false, val isPrimaryTunnel: Boolean = false,
@ColumnInfo(
name = "am_quick",
defaultValue = "",
)
val amQuick: String = AM_QUICK_DEFAULT,
) { ) {
companion object { companion object {
fun configFromQuick(wgQuick: String): Config { fun configFromWgQuick(wgQuick: String): Config {
val inputStream: InputStream = wgQuick.byteInputStream() val inputStream: InputStream = wgQuick.byteInputStream()
val reader = inputStream.bufferedReader(Charsets.UTF_8) return inputStream.bufferedReader(Charsets.UTF_8).use {
return Config.parse(reader) Config.parse(it)
}
} }
fun configFromAmQuick(amQuick: String) : org.amnezia.awg.config.Config {
val inputStream: InputStream = amQuick.byteInputStream()
return inputStream.bufferedReader(Charsets.UTF_8).use {
org.amnezia.awg.config.Config.parse(it)
}
}
const val AM_QUICK_DEFAULT = ""
} }
} }
@@ -1,6 +1,6 @@
package com.zaneschepke.wireguardautotunnel.data.repository package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
interface AppDataRepository { interface AppDataRepository {
suspend fun getPrimaryOrFirstTunnel(): TunnelConfig? suspend fun getPrimaryOrFirstTunnel(): TunnelConfig?
@@ -1,6 +1,6 @@
package com.zaneschepke.wireguardautotunnel.data.repository package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import javax.inject.Inject import javax.inject.Inject
class AppDataRoomRepository @Inject constructor( class AppDataRoomRepository @Inject constructor(
@@ -1,6 +1,6 @@
package com.zaneschepke.wireguardautotunnel.data.repository package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.model.GeneralState import com.zaneschepke.wireguardautotunnel.data.domain.GeneralState
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface AppStateRepository { interface AppStateRepository {
@@ -1,7 +1,7 @@
package com.zaneschepke.wireguardautotunnel.data.repository package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
import com.zaneschepke.wireguardautotunnel.data.model.GeneralState import com.zaneschepke.wireguardautotunnel.data.domain.GeneralState
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import timber.log.Timber import timber.log.Timber
@@ -1,7 +1,7 @@
package com.zaneschepke.wireguardautotunnel.data.repository package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.SettingsDao import com.zaneschepke.wireguardautotunnel.data.SettingsDao
import com.zaneschepke.wireguardautotunnel.data.model.Settings import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
class RoomSettingsRepository(private val settingsDoa: SettingsDao) : SettingsRepository { class RoomSettingsRepository(private val settingsDoa: SettingsDao) : SettingsRepository {
@@ -1,7 +1,7 @@
package com.zaneschepke.wireguardautotunnel.data.repository package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@@ -54,6 +54,10 @@ class RoomTunnelConfigRepository(private val tunnelConfigDao: TunnelConfigDao) :
return tunnelConfigDao.count().toInt() return tunnelConfigDao.count().toInt()
} }
override suspend fun findByTunnelName(name: String): TunnelConfig? {
return tunnelConfigDao.getByName(name)
}
override suspend fun findByTunnelNetworksName(name: String): TunnelConfigs { override suspend fun findByTunnelNetworksName(name: String): TunnelConfigs {
return tunnelConfigDao.findByTunnelNetworkName(name) return tunnelConfigDao.findByTunnelNetworkName(name)
} }
@@ -1,6 +1,6 @@
package com.zaneschepke.wireguardautotunnel.data.repository package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.model.Settings import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface SettingsRepository { interface SettingsRepository {
@@ -1,6 +1,6 @@
package com.zaneschepke.wireguardautotunnel.data.repository package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@@ -22,6 +22,8 @@ interface TunnelConfigRepository {
suspend fun count(): Int suspend fun count(): Int
suspend fun findByTunnelName(name : String) : TunnelConfig?
suspend fun findByTunnelNetworksName(name: String): TunnelConfigs suspend fun findByTunnelNetworksName(name: String): TunnelConfigs
suspend fun findByMobileDataTunnel(): TunnelConfigs suspend fun findByMobileDataTunnel(): TunnelConfigs
@@ -40,14 +40,21 @@ class TunnelModule {
return WgQuickBackend(context, rootShell, ToolsInstaller(context, rootShell)) return WgQuickBackend(context, rootShell, ToolsInstaller(context, rootShell))
} }
@Provides
@Singleton
fun provideAmneziaBackend(@ApplicationContext context: Context) : org.amnezia.awg.backend.Backend {
return org.amnezia.awg.backend.GoBackend(context)
}
@Provides @Provides
@Singleton @Singleton
fun provideVpnService( fun provideVpnService(
amneziaBackend: org.amnezia.awg.backend.Backend,
@Userspace userspaceBackend: Backend, @Userspace userspaceBackend: Backend,
@Kernel kernelBackend: Backend, @Kernel kernelBackend: Backend,
appDataRepository: AppDataRepository appDataRepository: AppDataRepository
): VpnService { ): VpnService {
return WireGuardTunnel(userspaceBackend, kernelBackend, appDataRepository) return WireGuardTunnel(amneziaBackend,userspaceBackend, kernelBackend, appDataRepository)
} }
@Provides @Provides
@@ -24,7 +24,7 @@ class NotificationActionReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) = goAsync { override fun onReceive(context: Context, intent: Intent?) = goAsync {
try { try {
//TODO fix for manual start changes when enabled //TODO fix for manual start changes when enabled
serviceManager.stopVpnService(context) serviceManager.stopVpnServiceForeground(context)
delay(Constants.TOGGLE_TUNNEL_DELAY) delay(Constants.TOGGLE_TUNNEL_DELAY)
serviceManager.startVpnServiceForeground(context) serviceManager.startVpnServiceForeground(context)
} catch (e: Exception) { } catch (e: Exception) {
@@ -3,5 +3,6 @@ package com.zaneschepke.wireguardautotunnel.service.foreground
enum class Action { enum class Action {
START, START,
START_FOREGROUND, START_FOREGROUND,
STOP STOP,
STOP_FOREGROUND
} }
@@ -24,8 +24,7 @@ open class ForegroundService : LifecycleService() {
when (action) { when (action) {
Action.START.name, Action.START.name,
Action.START_FOREGROUND.name -> startService(intent.extras) Action.START_FOREGROUND.name -> startService(intent.extras)
Action.STOP.name, Action.STOP_FOREGROUND.name -> stopService()
Action.STOP.name -> stopService(intent.extras)
Constants.ALWAYS_ON_VPN_ACTION -> { Constants.ALWAYS_ON_VPN_ACTION -> {
Timber.i("Always-on VPN starting service") Timber.i("Always-on VPN starting service")
startService(intent.extras) startService(intent.extras)
@@ -38,29 +37,19 @@ open class ForegroundService : LifecycleService() {
"with a null intent. It has been probably restarted by the system.", "with a null intent. It has been probably restarted by the system.",
) )
} }
// by returning this we make sure the service is restarted if the system kills the service
return START_STICKY return START_STICKY
} }
override fun onDestroy() {
super.onDestroy()
Timber.d("The service has been destroyed")
}
protected open fun startService(extras: Bundle?) { protected open fun startService(extras: Bundle?) {
if (isServiceStarted) return if (isServiceStarted) return
Timber.d("Starting ${this.javaClass.simpleName}") Timber.d("Starting ${this.javaClass.simpleName}")
isServiceStarted = true isServiceStarted = true
} }
protected open fun stopService(extras: Bundle?) { protected open fun stopService() {
Timber.d("Stopping ${this.javaClass.simpleName}") Timber.d("Stopping ${this.javaClass.simpleName}")
try { stopForeground(STOP_FOREGROUND_REMOVE)
stopForeground(STOP_FOREGROUND_REMOVE) stopSelf()
stopSelf()
} catch (e: Exception) {
Timber.e(e)
}
isServiceStarted = false isServiceStarted = false
} }
} }
@@ -23,15 +23,8 @@ class ServiceManager(private val appDataRepository: AppDataRepository) {
intent.component?.javaClass intent.component?.javaClass
try { try {
when (action) { when (action) {
Action.START_FOREGROUND -> { Action.START_FOREGROUND, Action.STOP_FOREGROUND -> context.startForegroundService(intent)
context.startForegroundService(intent) Action.START, Action.STOP -> context.startService(intent)
}
Action.START -> {
context.startService(intent)
}
Action.STOP -> context.startService(intent)
} }
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e.message) Timber.e(e.message)
@@ -52,9 +45,19 @@ class ServiceManager(private val appDataRepository: AppDataRepository) {
) )
} }
suspend fun stopVpnServiceForeground(context: Context, isManualStop: Boolean = false) {
if (isManualStop) onManualStop()
Timber.i("Stopping vpn service")
actionOnService(
Action.STOP_FOREGROUND,
context,
WireGuardTunnelService::class.java,
)
}
suspend fun stopVpnService(context: Context, isManualStop: Boolean = false) { suspend fun stopVpnService(context: Context, isManualStop: Boolean = false) {
if (isManualStop) onManualStop() if (isManualStop) onManualStop()
Timber.d("Stopping vpn service action") Timber.i("Stopping vpn service")
actionOnService( actionOnService(
Action.STOP, Action.STOP,
context, context,
@@ -1,6 +0,0 @@
package com.zaneschepke.wireguardautotunnel.service.foreground
enum class ServiceState {
STARTED,
STOPPED,
}
@@ -1,84 +1,63 @@
package com.zaneschepke.wireguardautotunnel.service.foreground package com.zaneschepke.wireguardautotunnel.service.foreground
import com.wireguard.android.backend.Tunnel import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.data.model.Settings
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
data class WatcherState( data class WatcherState(
val isWifiConnected: Boolean = false, val isWifiConnected: Boolean = false,
val config: TunnelConfig? = null,
val vpnStatus: Tunnel.State = Tunnel.State.DOWN,
val isEthernetConnected: Boolean = false, val isEthernetConnected: Boolean = false,
val isMobileDataConnected: Boolean = false, val isMobileDataConnected: Boolean = false,
val currentNetworkSSID: String = "", val currentNetworkSSID: String = "",
val settings: Settings = Settings() val settings: Settings = Settings()
) { ) {
private fun isVpnConnected() = vpnStatus == Tunnel.State.UP
fun isEthernetConditionMet(): Boolean { fun isEthernetConditionMet(): Boolean {
return (isEthernetConnected && return (isEthernetConnected &&
settings.isTunnelOnEthernetEnabled && settings.isTunnelOnEthernetEnabled)
!isVpnConnected())
} }
fun isMobileDataConditionMet(): Boolean { fun isMobileDataConditionMet(): Boolean {
return (!isEthernetConnected && return (!isEthernetConnected &&
settings.isTunnelOnMobileDataEnabled && settings.isTunnelOnMobileDataEnabled &&
!isWifiConnected && !isWifiConnected &&
isMobileDataConnected && isMobileDataConnected)
!isVpnConnected())
} }
fun isTunnelNotMobileDataPreferredConditionMet(): Boolean { fun isTunnelOnMobileDataPreferredConditionMet(): Boolean {
return (!isEthernetConnected && return (!isEthernetConnected &&
settings.isTunnelOnMobileDataEnabled && settings.isTunnelOnMobileDataEnabled &&
!isWifiConnected && !isWifiConnected &&
isMobileDataConnected && isMobileDataConnected)
config?.isMobileDataTunnel == false && isVpnConnected())
} }
fun isTunnelOffOnMobileDataConditionMet(): Boolean { fun isTunnelOffOnMobileDataConditionMet(): Boolean {
return (!isEthernetConnected && return (!isEthernetConnected &&
!settings.isTunnelOnMobileDataEnabled && !settings.isTunnelOnMobileDataEnabled &&
isMobileDataConnected && isMobileDataConnected &&
!isWifiConnected && !isWifiConnected)
isVpnConnected())
} }
fun isUntrustedWifiConditionMet(): Boolean { fun isUntrustedWifiConditionMet(): Boolean {
return (!isEthernetConnected && return (!isEthernetConnected &&
isWifiConnected && isWifiConnected &&
!settings.trustedNetworkSSIDs.contains(currentNetworkSSID) && !settings.trustedNetworkSSIDs.contains(currentNetworkSSID) &&
settings.isTunnelOnWifiEnabled settings.isTunnelOnWifiEnabled)
&& !isVpnConnected())
}
fun isTunnelNotWifiNamePreferredMet(ssid: String): Boolean {
return (!isEthernetConnected &&
isWifiConnected &&
!settings.trustedNetworkSSIDs.contains(currentNetworkSSID) &&
settings.isTunnelOnWifiEnabled && config?.tunnelNetworks?.contains(ssid) == false && isVpnConnected())
} }
fun isTrustedWifiConditionMet(): Boolean { fun isTrustedWifiConditionMet(): Boolean {
return (!isEthernetConnected && return (!isEthernetConnected &&
(isWifiConnected && (isWifiConnected &&
settings.trustedNetworkSSIDs.contains(currentNetworkSSID)) && settings.trustedNetworkSSIDs.contains(currentNetworkSSID)))
(isVpnConnected()))
} }
fun isTunnelOffOnWifiConditionMet(): Boolean { fun isTunnelOffOnWifiConditionMet(): Boolean {
return (!isEthernetConnected && return (!isEthernetConnected &&
(isWifiConnected && (isWifiConnected &&
!settings.isTunnelOnWifiEnabled && !settings.isTunnelOnWifiEnabled))
(isVpnConnected())))
} }
fun isTunnelOffOnNoConnectivityMet(): Boolean { fun isTunnelOffOnNoConnectivityMet(): Boolean {
return (!isEthernetConnected && return (!isEthernetConnected &&
!isWifiConnected && !isWifiConnected &&
!isMobileDataConnected && !isMobileDataConnected)
(isVpnConnected()))
} }
} }
@@ -5,9 +5,8 @@ import android.os.Bundle
import android.os.PowerManager import android.os.PowerManager
import androidx.core.app.ServiceCompat import androidx.core.app.ServiceCompat
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.service.network.EthernetService import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
@@ -15,14 +14,17 @@ import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
import com.zaneschepke.wireguardautotunnel.service.network.NetworkStatus import com.zaneschepke.wireguardautotunnel.service.network.NetworkStatus
import com.zaneschepke.wireguardautotunnel.service.network.WifiService import com.zaneschepke.wireguardautotunnel.service.network.WifiService
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import java.net.InetAddress import java.net.InetAddress
@@ -56,7 +58,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
private val networkEventsFlow = MutableStateFlow(WatcherState()) private val networkEventsFlow = MutableStateFlow(WatcherState())
private lateinit var watcherJob: Job private var watcherJob: Job? = null
private var wakeLock: PowerManager.WakeLock? = null private var wakeLock: PowerManager.WakeLock? = null
private val tag = this.javaClass.name private val tag = this.javaClass.name
@@ -86,8 +88,8 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
} }
} }
override fun stopService(extras: Bundle?) { override fun stopService() {
super.stopService(extras) super.stopService()
wakeLock?.let { wakeLock?.let {
if (it.isHeld) { if (it.isHeld) {
it.release() it.release()
@@ -134,8 +136,10 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
} }
private fun cancelWatcherJob() { private fun cancelWatcherJob() {
if (this::watcherJob.isInitialized) { try {
watcherJob.cancel() watcherJob?.cancel()
} catch (e : CancellationException) {
Timber.i("Watcher job cancelled")
} }
} }
@@ -159,10 +163,6 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
watchForEthernetConnectivityChanges() watchForEthernetConnectivityChanges()
} }
} }
launch {
Timber.i("Starting vpn state watcher")
watchForVpnConnectivityChanges()
}
launch { launch {
Timber.i("Starting settings watcher") Timber.i("Starting settings watcher")
watchForSettingsChanges() watchForSettingsChanges()
@@ -182,29 +182,32 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
} }
private suspend fun watchForMobileDataConnectivityChanges() { private suspend fun watchForMobileDataConnectivityChanges() {
mobileDataService.networkStatus.collect { mobileDataService.networkStatus.collect { status ->
when (it) { when (status) {
is NetworkStatus.Available -> { is NetworkStatus.Available -> {
Timber.i("Gained Mobile data connection") Timber.i("Gained Mobile data connection")
networkEventsFlow.value = networkEventsFlow.update {
networkEventsFlow.value.copy( it.copy(
isMobileDataConnected = true, isMobileDataConnected = true,
) )
}
} }
is NetworkStatus.CapabilitiesChanged -> { is NetworkStatus.CapabilitiesChanged -> {
networkEventsFlow.value = networkEventsFlow.update {
networkEventsFlow.value.copy( it.copy(
isMobileDataConnected = true, isMobileDataConnected = true,
) )
}
Timber.i("Mobile data capabilities changed") Timber.i("Mobile data capabilities changed")
} }
is NetworkStatus.Unavailable -> { is NetworkStatus.Unavailable -> {
networkEventsFlow.value = networkEventsFlow.update {
networkEventsFlow.value.copy( it.copy(
isMobileDataConnected = false, isMobileDataConnected = false,
) )
}
Timber.i("Lost mobile data connection") Timber.i("Lost mobile data connection")
} }
} }
@@ -214,15 +217,15 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
private suspend fun watchForPingFailure() { private suspend fun watchForPingFailure() {
try { try {
do { do {
if (vpnService.vpnState.value.status == Tunnel.State.UP) { if (vpnService.vpnState.value.status == TunnelState.UP) {
val tunnelConfig = vpnService.vpnState.value.tunnelConfig val tunnelConfig = vpnService.vpnState.value.tunnelConfig
tunnelConfig?.let { tunnelConfig?.let {
val config = TunnelConfig.configFromQuick(it.wgQuick) val config = TunnelConfig.configFromWgQuick(it.wgQuick)
val results = config.peers.map { peer -> val results = config.peers.map { peer ->
val host = if (peer.endpoint.isPresent && val host = if (peer.endpoint.isPresent &&
peer.endpoint.get().resolved.isPresent) peer.endpoint.get().resolved.isPresent)
peer.endpoint.get().resolved.get().host peer.endpoint.get().resolved.get().host
else Constants.BACKUP_PING_HOST else Constants.DEFAULT_PING_IP
Timber.i("Checking reachability of: $host") Timber.i("Checking reachability of: $host")
val reachable = InetAddress.getByName(host) val reachable = InetAddress.getByName(host)
.isReachable(Constants.PING_TIMEOUT.toInt()) .isReachable(Constants.PING_TIMEOUT.toInt())
@@ -231,9 +234,9 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
} }
if (results.contains(false)) { if (results.contains(false)) {
Timber.i("Restarting VPN for ping failure") Timber.i("Restarting VPN for ping failure")
serviceManager.stopVpnService(this) serviceManager.stopVpnServiceForeground(this)
delay(Constants.VPN_RESTART_DELAY) delay(Constants.VPN_RESTART_DELAY)
serviceManager.startVpnServiceForeground(this) serviceManager.startVpnServiceForeground(this, it.id)
delay(Constants.PING_COOLDOWN) delay(Constants.PING_COOLDOWN)
} }
} }
@@ -246,54 +249,48 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
} }
private suspend fun watchForSettingsChanges() { private suspend fun watchForSettingsChanges() {
appDataRepository.settings.getSettingsFlow().collect { appDataRepository.settings.getSettingsFlow().collect { settings ->
if (networkEventsFlow.value.settings.isAutoTunnelPaused != it.isAutoTunnelPaused) { if (networkEventsFlow.value.settings.isAutoTunnelPaused != settings.isAutoTunnelPaused) {
when (it.isAutoTunnelPaused) { when (settings.isAutoTunnelPaused) {
true -> launchWatcherPausedNotification() true -> launchWatcherPausedNotification()
false -> launchWatcherNotification() false -> launchWatcherNotification()
} }
} }
networkEventsFlow.value = networkEventsFlow.update {
networkEventsFlow.value.copy( it.copy(
settings = it, settings = settings,
)
}
}
private suspend fun watchForVpnConnectivityChanges() {
vpnService.vpnState.collect {
networkEventsFlow.value =
networkEventsFlow.value.copy(
vpnStatus = it.status,
config = it.tunnelConfig,
) )
}
} }
} }
private suspend fun watchForEthernetConnectivityChanges() { private suspend fun watchForEthernetConnectivityChanges() {
ethernetService.networkStatus.collect { ethernetService.networkStatus.collect { status ->
when (it) { when (status) {
is NetworkStatus.Available -> { is NetworkStatus.Available -> {
Timber.i("Gained Ethernet connection") Timber.i("Gained Ethernet connection")
networkEventsFlow.value = networkEventsFlow.update {
networkEventsFlow.value.copy( it.copy(
isEthernetConnected = true, isEthernetConnected = true,
) )
}
} }
is NetworkStatus.CapabilitiesChanged -> { is NetworkStatus.CapabilitiesChanged -> {
Timber.i("Ethernet capabilities changed") Timber.i("Ethernet capabilities changed")
networkEventsFlow.value = networkEventsFlow.update {
networkEventsFlow.value.copy( it.copy(
isEthernetConnected = true, isEthernetConnected = true,
) )
}
} }
is NetworkStatus.Unavailable -> { is NetworkStatus.Unavailable -> {
networkEventsFlow.value = networkEventsFlow.update {
networkEventsFlow.value.copy( it.copy(
isEthernetConnected = false, isEthernetConnected = false,
) )
}
Timber.i("Lost Ethernet connection") Timber.i("Lost Ethernet connection")
} }
} }
@@ -301,38 +298,43 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
} }
private suspend fun watchForWifiConnectivityChanges() { private suspend fun watchForWifiConnectivityChanges() {
wifiService.networkStatus.collect { wifiService.networkStatus.collect { status ->
when (it) { when (status) {
is NetworkStatus.Available -> { is NetworkStatus.Available -> {
Timber.i("Gained Wi-Fi connection") Timber.i("Gained Wi-Fi connection")
networkEventsFlow.value = networkEventsFlow.update {
networkEventsFlow.value.copy( it.copy(
isWifiConnected = true, isWifiConnected = true,
) )
}
} }
is NetworkStatus.CapabilitiesChanged -> { is NetworkStatus.CapabilitiesChanged -> {
Timber.i("Wifi capabilities changed") Timber.i("Wifi capabilities changed")
networkEventsFlow.value = networkEventsFlow.update {
networkEventsFlow.value.copy( it.copy(
isWifiConnected = true, isWifiConnected = true,
) )
val ssid = wifiService.getNetworkName(it.networkCapabilities) }
ssid?.let { val ssid = wifiService.getNetworkName(status.networkCapabilities)
Timber.i("Detected SSID: $ssid") ssid?.let { name ->
appDataRepository.appState.setCurrentSsid(ssid) if(name.contains(Constants.UNREADABLE_SSID)) {
networkEventsFlow.value = Timber.w("SSID unreadable: missing permissions")
networkEventsFlow.value.copy( } else Timber.i("Detected valid SSID")
currentNetworkSSID = ssid, appDataRepository.appState.setCurrentSsid(name)
networkEventsFlow.update {
it.copy(
currentNetworkSSID = name,
) )
}
} ?: Timber.w("Failed to read ssid") } ?: Timber.w("Failed to read ssid")
} }
is NetworkStatus.Unavailable -> { is NetworkStatus.Unavailable -> {
networkEventsFlow.value = networkEventsFlow.update {
networkEventsFlow.value.copy( it.copy(
isWifiConnected = false, isWifiConnected = false,
) )
}
Timber.i("Lost Wi-Fi connection") Timber.i("Lost Wi-Fi connection")
} }
} }
@@ -347,72 +349,74 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
return appDataRepository.tunnels.findByTunnelNetworksName(ssid).firstOrNull() return appDataRepository.tunnels.findByTunnelNetworksName(ssid).firstOrNull()
} }
private fun isTunnelDown() : Boolean {
return vpnService.vpnState.value.status == TunnelState.DOWN
}
private suspend fun manageVpn() { private suspend fun manageVpn() {
networkEventsFlow.collectLatest { watcherState -> networkEventsFlow.collectLatest { watcherState ->
val autoTunnel = "Auto-tunnel watcher" val autoTunnel = "Auto-tunnel watcher"
if (!watcherState.settings.isAutoTunnelPaused) { if (!watcherState.settings.isAutoTunnelPaused) {
//delay for rapid network state changes and then collect latest //delay for rapid network state changes and then collect latest
delay(Constants.WATCHER_COLLECTION_DELAY) delay(Constants.WATCHER_COLLECTION_DELAY)
val tunnelConfig = vpnService.vpnState.value.tunnelConfig
when { when {
watcherState.isEthernetConditionMet() -> { watcherState.isEthernetConditionMet() -> {
Timber.i("$autoTunnel - tunnel on on ethernet condition met") Timber.i("$autoTunnel - tunnel on on ethernet condition met")
serviceManager.startVpnServiceForeground(this) if(isTunnelDown()) serviceManager.startVpnServiceForeground(this)
} }
watcherState.isMobileDataConditionMet() -> { watcherState.isMobileDataConditionMet() -> {
Timber.i("$autoTunnel - tunnel on on mobile data condition met") Timber.i("$autoTunnel - tunnel on on mobile data condition met")
serviceManager.startVpnServiceForeground(this, getMobileDataTunnel()?.id) if(isTunnelDown()) serviceManager.startVpnServiceForeground(this, getMobileDataTunnel()?.id)
} }
watcherState.isTunnelNotMobileDataPreferredConditionMet() -> { watcherState.isTunnelOnMobileDataPreferredConditionMet() -> {
getMobileDataTunnel()?.let { if(tunnelConfig?.isMobileDataTunnel == false) {
Timber.i("$autoTunnel - tunnel connected on mobile data is not preferred condition met, switching to preferred") getMobileDataTunnel()?.let {
serviceManager.startVpnServiceForeground( Timber.i("$autoTunnel - tunnel connected on mobile data is not preferred condition met, switching to preferred")
this, if(isTunnelDown()) serviceManager.startVpnServiceForeground(
getMobileDataTunnel()?.id, this,
) getMobileDataTunnel()?.id,
)
}
} }
} }
watcherState.isTunnelOffOnMobileDataConditionMet() -> { watcherState.isTunnelOffOnMobileDataConditionMet() -> {
Timber.i("$autoTunnel - tunnel off on mobile data met, turning vpn off") Timber.i("$autoTunnel - tunnel off on mobile data met, turning vpn off")
serviceManager.stopVpnService(this) if(!isTunnelDown()) serviceManager.stopVpnServiceForeground(this)
}
watcherState.isTunnelNotWifiNamePreferredMet(watcherState.currentNetworkSSID) -> {
Timber.i("$autoTunnel - tunnel on ssid not associated with current tunnel condition met")
getSsidTunnel(watcherState.currentNetworkSSID)?.let {
Timber.i("Found tunnel associated with this SSID, bringing tunnel up")
serviceManager.startVpnServiceForeground(this, it.id)
} ?: suspend {
Timber.i("No tunnel associated with this SSID, using defaults")
if (appDataRepository.getPrimaryOrFirstTunnel()?.name != vpnService.name) {
serviceManager.startVpnServiceForeground(this)
}
}.invoke()
} }
watcherState.isUntrustedWifiConditionMet() -> { watcherState.isUntrustedWifiConditionMet() -> {
Timber.i("$autoTunnel - tunnel on untrusted wifi condition met") if(tunnelConfig?.tunnelNetworks?.contains(watcherState.currentNetworkSSID) == false ||
serviceManager.startVpnServiceForeground( tunnelConfig == null) {
this, Timber.i("$autoTunnel - tunnel on ssid not associated with current tunnel condition met")
getSsidTunnel(watcherState.currentNetworkSSID)?.id, getSsidTunnel(watcherState.currentNetworkSSID)?.let {
) Timber.i("Found tunnel associated with this SSID, bringing tunnel up")
if(isTunnelDown()) serviceManager.startVpnServiceForeground(this, it.id)
} ?: suspend {
Timber.i("No tunnel associated with this SSID, using defaults")
if (appDataRepository.getPrimaryOrFirstTunnel()?.name != vpnService.name) {
if(isTunnelDown()) serviceManager.startVpnServiceForeground(this)
}
}.invoke()
}
} }
watcherState.isTrustedWifiConditionMet() -> { watcherState.isTrustedWifiConditionMet() -> {
Timber.i("$autoTunnel - tunnel off on trusted wifi condition met, turning vpn off") Timber.i("$autoTunnel - tunnel off on trusted wifi condition met, turning vpn off")
serviceManager.stopVpnService(this) if(!isTunnelDown()) serviceManager.stopVpnServiceForeground(this)
} }
watcherState.isTunnelOffOnWifiConditionMet() -> { watcherState.isTunnelOffOnWifiConditionMet() -> {
Timber.i("$autoTunnel - tunnel off on wifi condition met, turning vpn off") Timber.i("$autoTunnel - tunnel off on wifi condition met, turning vpn off")
serviceManager.stopVpnService(this) if(!isTunnelDown()) serviceManager.stopVpnServiceForeground(this)
} }
watcherState.isTunnelOffOnNoConnectivityMet() -> { watcherState.isTunnelOffOnNoConnectivityMet() -> {
Timber.i("$autoTunnel - tunnel off on no connectivity met, turning vpn off") Timber.i("$autoTunnel - tunnel off on no connectivity met, turning vpn off")
serviceManager.stopVpnService(this) if(!isTunnelDown()) serviceManager.stopVpnServiceForeground(this)
} }
else -> { else -> {
@@ -5,21 +5,23 @@ import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.core.app.ServiceCompat import androidx.core.app.ServiceCompat
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.handshakeStatus import com.zaneschepke.wireguardautotunnel.util.handshakeStatus
import com.zaneschepke.wireguardautotunnel.util.mapPeerStats import com.zaneschepke.wireguardautotunnel.util.mapPeerStats
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@@ -35,7 +37,7 @@ class WireGuardTunnelService : ForegroundService() {
@Inject @Inject
lateinit var notificationService: NotificationService lateinit var notificationService: NotificationService
private lateinit var job: Job private var job: Job? = null
private var didShowConnected = false private var didShowConnected = false
@@ -56,7 +58,7 @@ class WireGuardTunnelService : ForegroundService() {
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
launch { launch {
val tunnelId = extras?.getInt(Constants.TUNNEL_EXTRA_KEY) val tunnelId = extras?.getInt(Constants.TUNNEL_EXTRA_KEY)
if (vpnService.getState() == Tunnel.State.UP) { if (vpnService.getState() == TunnelState.UP) {
vpnService.stopTunnel() vpnService.stopTunnel()
} }
vpnService.startTunnel( vpnService.startTunnel(
@@ -75,7 +77,7 @@ class WireGuardTunnelService : ForegroundService() {
private suspend fun handshakeNotifications() { private suspend fun handshakeNotifications() {
var tunnelName: String? = null var tunnelName: String? = null
vpnService.vpnState.collect { state -> vpnService.vpnState.collect { state ->
state.statistics state.statistics
?.mapPeerStats() ?.mapPeerStats()
?.map { it.value?.handshakeStatus() } ?.map { it.value?.handshakeStatus() }
.let { statuses -> .let { statuses ->
@@ -100,7 +102,7 @@ class WireGuardTunnelService : ForegroundService() {
else -> {} else -> {}
} }
} }
if (state.status == Tunnel.State.UP && state.tunnelConfig?.name != tunnelName) { if (state.status == TunnelState.UP && state.tunnelConfig?.name != tunnelName) {
tunnelName = state.tunnelConfig?.name tunnelName = state.tunnelConfig?.name
launchVpnNotification( launchVpnNotification(
getString(R.string.tunnel_start_title), getString(R.string.tunnel_start_title),
@@ -117,8 +119,8 @@ class WireGuardTunnelService : ForegroundService() {
) )
} }
override fun stopService(extras: Bundle?) { override fun stopService() {
super.stopService(extras) super.stopService()
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
vpnService.stopTunnel() vpnService.stopTunnel()
didShowConnected = false didShowConnected = false
@@ -177,8 +179,10 @@ class WireGuardTunnelService : ForegroundService() {
} }
private fun cancelJob() { private fun cancelJob() {
if (this::job.isInitialized) { try {
job.cancel() job?.cancel()
} catch (e : CancellationException) {
Timber.i("Tunnel job cancelled")
} }
} }
} }
@@ -2,7 +2,7 @@ package com.zaneschepke.wireguardautotunnel.service.shortcut
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.lifecycle.lifecycleScope import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.Action import com.zaneschepke.wireguardautotunnel.service.foreground.Action
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
@@ -24,7 +24,7 @@ class ShortcutsActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
lifecycleScope.launch(Dispatchers.Main) { WireGuardAutoTunnel.applicationScope.launch(Dispatchers.IO) {
val settings = appDataRepository.settings.getSettings() val settings = appDataRepository.settings.getSettings()
if (settings.isShortcutsEnabled) { if (settings.isShortcutsEnabled) {
when (intent.getStringExtra(CLASS_NAME_EXTRA_KEY)) { when (intent.getStringExtra(CLASS_NAME_EXTRA_KEY)) {
@@ -40,7 +40,7 @@ class ShortcutsActivity : ComponentActivity() {
this@ShortcutsActivity, tunnelConfig?.id, isManualStart = true, this@ShortcutsActivity, tunnelConfig?.id, isManualStart = true,
) )
Action.STOP.name -> serviceManager.stopVpnService( Action.STOP.name -> serviceManager.stopVpnServiceForeground(
this@ShortcutsActivity, this@ShortcutsActivity,
isManualStop = true, isManualStop = true,
) )
@@ -4,7 +4,7 @@ import android.os.Build
import android.service.quicksettings.Tile import android.service.quicksettings.Tile
import android.service.quicksettings.TileService import android.service.quicksettings.TileService
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@@ -53,6 +53,11 @@ class AutoTunnelControlTile : TileService() {
} }
} }
override fun onTileAdded() {
super.onTileAdded()
onStartListening()
}
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
scope.cancel() scope.cancel()
@@ -3,10 +3,10 @@ package com.zaneschepke.wireguardautotunnel.service.tile
import android.os.Build import android.os.Build
import android.service.quicksettings.Tile import android.service.quicksettings.Tile
import android.service.quicksettings.TileService import android.service.quicksettings.TileService
import com.wireguard.android.backend.Tunnel import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -38,12 +38,12 @@ class TunnelControlTile : TileService() {
scope.launch { scope.launch {
vpnService.vpnState.collect { it -> vpnService.vpnState.collect { it ->
when (it.status) { when (it.status) {
Tunnel.State.UP -> { TunnelState.UP -> {
setActive() setActive()
it.tunnelConfig?.name?.let { name -> setTileDescription(name) } it.tunnelConfig?.name?.let { name -> setTileDescription(name) }
} }
Tunnel.State.DOWN -> { TunnelState.DOWN -> {
setInactive() setInactive()
val config = appDataRepository.getStartTunnelConfig()?.also { config -> val config = appDataRepository.getStartTunnelConfig()?.also { config ->
manualStartConfig = config manualStartConfig = config
@@ -52,7 +52,6 @@ class TunnelControlTile : TileService() {
setTileDescription(it.name) setTileDescription(it.name)
} ?: setUnavailable() } ?: setUnavailable()
} }
else -> setInactive() else -> setInactive()
} }
} }
@@ -69,13 +68,21 @@ class TunnelControlTile : TileService() {
scope.cancel() scope.cancel()
} }
override fun onTileAdded() {
super.onTileAdded()
onStartListening()
}
override fun onClick() { override fun onClick() {
super.onClick() super.onClick()
unlockAndRun { unlockAndRun {
scope.launch { scope.launch {
try { try {
if (vpnService.getState() == Tunnel.State.UP) { if (vpnService.getState() == TunnelState.UP) {
serviceManager.stopVpnService(this@TunnelControlTile, isManualStop = true) serviceManager.stopVpnServiceForeground(
this@TunnelControlTile,
isManualStop = true,
)
} else { } else {
serviceManager.startVpnServiceForeground( serviceManager.startVpnServiceForeground(
this@TunnelControlTile, manualStartConfig?.id, isManualStart = true, this@TunnelControlTile, manualStartConfig?.id, isManualStart = true,
@@ -0,0 +1,42 @@
package com.zaneschepke.wireguardautotunnel.service.tunnel
import com.wireguard.android.backend.Tunnel
enum class TunnelState {
UP,
DOWN,
TOGGLE;
fun toWgState() : Tunnel.State {
return when(this) {
UP -> Tunnel.State.UP
DOWN -> Tunnel.State.DOWN
TOGGLE -> Tunnel.State.TOGGLE
}
}
fun toAmState() : org.amnezia.awg.backend.Tunnel.State {
return when(this) {
UP -> org.amnezia.awg.backend.Tunnel.State.UP
DOWN -> org.amnezia.awg.backend.Tunnel.State.DOWN
TOGGLE -> org.amnezia.awg.backend.Tunnel.State.TOGGLE
}
}
companion object {
fun from(state: Tunnel.State) : TunnelState {
return when(state) {
Tunnel.State.DOWN -> DOWN
Tunnel.State.TOGGLE -> TOGGLE
Tunnel.State.UP -> UP
}
}
fun from(state: org.amnezia.awg.backend.Tunnel.State) : TunnelState {
return when(state) {
org.amnezia.awg.backend.Tunnel.State.DOWN -> DOWN
org.amnezia.awg.backend.Tunnel.State.TOGGLE -> TOGGLE
org.amnezia.awg.backend.Tunnel.State.UP -> UP
}
}
}
}
@@ -1,15 +1,15 @@
package com.zaneschepke.wireguardautotunnel.service.tunnel package com.zaneschepke.wireguardautotunnel.service.tunnel
import com.wireguard.android.backend.Tunnel import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
interface VpnService : Tunnel { interface VpnService : Tunnel, org.amnezia.awg.backend.Tunnel {
suspend fun startTunnel(tunnelConfig: TunnelConfig? = null): Tunnel.State suspend fun startTunnel(tunnelConfig: TunnelConfig? = null): TunnelState
suspend fun stopTunnel() suspend fun stopTunnel()
val vpnState: StateFlow<VpnState> val vpnState: StateFlow<VpnState>
fun getState(): Tunnel.State fun getState(): TunnelState
} }
@@ -1,11 +1,10 @@
package com.zaneschepke.wireguardautotunnel.service.tunnel package com.zaneschepke.wireguardautotunnel.service.tunnel
import com.wireguard.android.backend.Statistics import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.wireguard.android.backend.Tunnel import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
data class VpnState( data class VpnState(
val status: Tunnel.State = Tunnel.State.DOWN, val status: TunnelState = TunnelState.DOWN,
val tunnelConfig: TunnelConfig? = null, val tunnelConfig: TunnelConfig? = null,
val statistics: Statistics? = null val statistics: TunnelStatistics? = null
) )
@@ -2,14 +2,17 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel
import com.wireguard.android.backend.Backend import com.wireguard.android.backend.Backend
import com.wireguard.android.backend.BackendException import com.wireguard.android.backend.BackendException
import com.wireguard.android.backend.Statistics
import com.wireguard.android.backend.Tunnel.State import com.wireguard.android.backend.Tunnel.State
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.Kernel import com.zaneschepke.wireguardautotunnel.module.Kernel
import com.zaneschepke.wireguardautotunnel.module.Userspace import com.zaneschepke.wireguardautotunnel.module.Userspace
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.AmneziaStatistics
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.WireGuardStatistics
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -18,12 +21,14 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.amnezia.awg.backend.Tunnel
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
class WireGuardTunnel class WireGuardTunnel
@Inject @Inject
constructor( constructor(
private val userspaceAmneziaBackend : org.amnezia.awg.backend.Backend,
@Userspace private val userspaceBackend: Backend, @Userspace private val userspaceBackend: Backend,
@Kernel private val kernelBackend: Backend, @Kernel private val kernelBackend: Backend,
private val appDataRepository: AppDataRepository, private val appDataRepository: AppDataRepository,
@@ -33,50 +38,74 @@ constructor(
private val scope = CoroutineScope(Dispatchers.IO) private val scope = CoroutineScope(Dispatchers.IO)
private lateinit var statsJob: Job private var statsJob: Job? = null
private var backend: Backend = userspaceBackend private var backend: Backend = userspaceBackend
private var backendIsUserspace = true private var backendIsWgUserspace = true
private var backendIsAmneziaUserspace = false
init { init {
scope.launch { scope.launch {
appDataRepository.settings.getSettingsFlow().collect { appDataRepository.settings.getSettingsFlow().collect {
if (it.isKernelEnabled && backendIsUserspace) { if (it.isKernelEnabled && (backendIsWgUserspace || backendIsAmneziaUserspace)) {
Timber.d("Setting kernel backend") Timber.d("Setting kernel backend")
backend = kernelBackend backend = kernelBackend
backendIsUserspace = false backendIsWgUserspace = false
} else if (!it.isKernelEnabled && !backendIsUserspace) { backendIsAmneziaUserspace = false
Timber.d("Setting userspace backend") } else if (!it.isKernelEnabled && !it.isAmneziaEnabled && !backendIsWgUserspace) {
Timber.d("Setting WireGuard userspace backend")
backend = userspaceBackend backend = userspaceBackend
backendIsUserspace = true backendIsWgUserspace = true
backendIsAmneziaUserspace = false
} else if (it.isAmneziaEnabled && !backendIsAmneziaUserspace) {
Timber.d("Setting Amnezia userspace backend")
backendIsAmneziaUserspace = true
backendIsWgUserspace = false
} }
} }
} }
} }
override suspend fun startTunnel(tunnelConfig: TunnelConfig?): State { private fun setState(tunnelConfig: TunnelConfig?, tunnelState: TunnelState) : TunnelState {
return if(backendIsAmneziaUserspace) {
Timber.i("Using Amnezia backend")
val config = tunnelConfig?.let {
if(it.amQuick != "") TunnelConfig.configFromAmQuick(it.amQuick) else {
Timber.w("Using backwards compatible wg config, amnezia specific config not found.")
TunnelConfig.configFromAmQuick(it.wgQuick)
}
}
val state = userspaceAmneziaBackend.setState(this, tunnelState.toAmState(), config)
TunnelState.from(state)
} else {
Timber.i("Using Wg backend")
val wgConfig = tunnelConfig?.let { TunnelConfig.configFromWgQuick(it.wgQuick) }
val state = backend.setState(
this,
tunnelState.toWgState(),
wgConfig,
)
TunnelState.from(state)
}
}
override suspend fun startTunnel(tunnelConfig: TunnelConfig?): TunnelState {
return try { return try {
//TODO we need better error handling here //TODO we need better error handling here
val config = tunnelConfig ?: appDataRepository.getPrimaryOrFirstTunnel() val config = tunnelConfig ?: appDataRepository.getPrimaryOrFirstTunnel()
if (config != null) { if (config != null) {
emitTunnelConfig(config) emitTunnelConfig(config)
val wgConfig = TunnelConfig.configFromQuick(config.wgQuick) setState(config, TunnelState.UP)
val state =
backend.setState(
this,
State.UP,
wgConfig,
)
state
} else throw Exception("No tunnels") } else throw Exception("No tunnels")
} catch (e: BackendException) { } catch (e: BackendException) {
Timber.e("Failed to start tunnel with error: ${e.message}") Timber.e("Failed to start tunnel with error: ${e.message}")
State.DOWN TunnelState.from(State.DOWN)
} }
} }
private fun emitTunnelState(state: State) { private fun emitTunnelState(state : TunnelState) {
_vpnState.tryEmit( _vpnState.tryEmit(
_vpnState.value.copy( _vpnState.value.copy(
status = state, status = state,
@@ -84,7 +113,7 @@ constructor(
) )
} }
private fun emitBackendStatistics(statistics: Statistics) { private fun emitBackendStatistics(statistics: TunnelStatistics) {
_vpnState.tryEmit( _vpnState.tryEmit(
_vpnState.value.copy( _vpnState.value.copy(
statistics = statistics, statistics = statistics,
@@ -100,43 +129,65 @@ constructor(
) )
} }
private fun resetVpnState() {
_vpnState.tryEmit(VpnState())
}
override suspend fun stopTunnel() { override suspend fun stopTunnel() {
try { try {
if (getState() == State.UP) { if (getState() == TunnelState.UP) {
val state = backend.setState(this, State.DOWN, null) val state = setState(null, TunnelState.DOWN)
resetVpnState()
emitTunnelState(state) emitTunnelState(state)
} }
} catch (e: BackendException) { } catch (e: BackendException) {
Timber.e("Failed to stop tunnel with error: ${e.message}") Timber.e("Failed to stop wireguard tunnel with error: ${e.message}")
} catch (e: org.amnezia.awg.backend.BackendException) {
Timber.e("Failed to stop amnezia tunnel with error: ${e.message}")
} }
} }
override fun getState(): State { override fun getState(): TunnelState {
return backend.getState(this) return if(backendIsAmneziaUserspace) TunnelState.from(userspaceAmneziaBackend.getState(this))
else TunnelState.from(backend.getState(this))
} }
override fun getName(): String { override fun getName(): String {
return _vpnState.value.tunnelConfig?.name ?: "" return _vpnState.value.tunnelConfig?.name ?: ""
} }
override fun onStateChange(state: State) {
override fun onStateChange(newState: Tunnel.State) {
handleStateChange(TunnelState.from(newState))
}
private fun handleStateChange(state: TunnelState) {
val tunnel = this val tunnel = this
emitTunnelState(state) emitTunnelState(state)
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(WireGuardAutoTunnel.instance) WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
if (state == State.UP) { if (state == TunnelState.UP) {
statsJob = statsJob =
scope.launch { scope.launch {
while (true) { while (true) {
val statistics = backend.getStatistics(tunnel) if(backendIsAmneziaUserspace) {
emitBackendStatistics(statistics) emitBackendStatistics(AmneziaStatistics(userspaceAmneziaBackend.getStatistics(tunnel)))
} else {
emitBackendStatistics(WireGuardStatistics(backend.getStatistics(tunnel)))
}
delay(Constants.VPN_STATISTIC_CHECK_INTERVAL) delay(Constants.VPN_STATISTIC_CHECK_INTERVAL)
} }
} }
} }
if (state == State.DOWN) { if (state == TunnelState.DOWN) {
if (this::statsJob.isInitialized) { try {
statsJob.cancel() statsJob?.cancel()
} catch (e : CancellationException) {
Timber.i("Stats job cancelled")
} }
} }
} }
override fun onStateChange(state: State) {
handleStateChange(TunnelState.from(state))
}
} }
@@ -0,0 +1,34 @@
package com.zaneschepke.wireguardautotunnel.service.tunnel.statistics
import org.amnezia.awg.backend.Statistics
import org.amnezia.awg.crypto.Key
class AmneziaStatistics(private val statistics: Statistics) : TunnelStatistics() {
override fun peerStats(peer: Key): PeerStats? {
val key = Key.fromBase64(peer.toBase64())
val stats = statistics.peer(key)
return stats?.let {
PeerStats(
rxBytes = stats.rxBytes,
txBytes = stats.txBytes,
latestHandshakeEpochMillis = stats.latestHandshakeEpochMillis
)
}
}
override fun isTunnelStale(): Boolean {
return statistics.isStale
}
override fun getPeers(): Array<Key> {
return statistics.peers()
}
override fun rx(): Long {
return statistics.totalRx()
}
override fun tx(): Long {
return statistics.totalTx()
}
}
@@ -0,0 +1,18 @@
package com.zaneschepke.wireguardautotunnel.service.tunnel.statistics
import org.amnezia.awg.crypto.Key
abstract class TunnelStatistics {
@JvmRecord
data class PeerStats(val rxBytes: Long, val txBytes: Long, val latestHandshakeEpochMillis: Long)
abstract fun peerStats(peer: Key): PeerStats?
abstract fun isTunnelStale() : Boolean
abstract fun getPeers(): Array<Key>
abstract fun rx() : Long
abstract fun tx() : Long
}
@@ -0,0 +1,36 @@
package com.zaneschepke.wireguardautotunnel.service.tunnel.statistics
import com.wireguard.android.backend.Statistics
import org.amnezia.awg.crypto.Key
class WireGuardStatistics(private val statistics: Statistics) : TunnelStatistics() {
override fun peerStats(peer: Key): PeerStats? {
val key = com.wireguard.crypto.Key.fromBase64(peer.toBase64())
val peerStats = statistics.peer(key)
return peerStats?.let {
PeerStats(
txBytes = peerStats.txBytes,
rxBytes = peerStats.rxBytes,
latestHandshakeEpochMillis = peerStats.latestHandshakeEpochMillis
)
}
}
override fun isTunnelStale(): Boolean {
return statistics.isStale
}
override fun getPeers(): Array<Key> {
return statistics.peers().map {
Key.fromBase64(it.toBase64())
}.toTypedArray()
}
override fun rx(): Long {
return statistics.totalRx()
}
override fun tx(): Long {
return statistics.totalTx()
}
}
@@ -1,7 +1,7 @@
package com.zaneschepke.wireguardautotunnel.ui package com.zaneschepke.wireguardautotunnel.ui
import android.app.Application
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.widget.Toast import android.widget.Toast
@@ -19,6 +19,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import java.time.Instant import java.time.Instant
@@ -27,9 +28,7 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class AppViewModel class AppViewModel
@Inject @Inject
constructor( constructor() : ViewModel() {
private val application: Application,
) : ViewModel() {
val vpnIntent: Intent? = GoBackend.VpnService.prepare(WireGuardAutoTunnel.instance) val vpnIntent: Intent? = GoBackend.VpnService.prepare(WireGuardAutoTunnel.instance)
@@ -49,68 +48,78 @@ constructor(
} }
private fun requestPermissions() { private fun requestPermissions() {
_appUiState.value = _appUiState.value.copy( _appUiState.update {
requestPermissions = true, it.copy(
) requestPermissions = true
)
}
} }
fun permissionsRequested() { fun permissionsRequested() {
_appUiState.value = _appUiState.value.copy( _appUiState.update {
requestPermissions = false, it.copy(
) requestPermissions = false
)
}
} }
fun openWebPage(url: String) { fun openWebPage(url: String, context : Context) {
try { try {
val webpage: Uri = Uri.parse(url) val webpage: Uri = Uri.parse(url)
val intent = Intent(Intent.ACTION_VIEW, webpage).apply { val intent = Intent(Intent.ACTION_VIEW, webpage).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
} }
application.startActivity(intent) context.startActivity(intent)
} catch (e: ActivityNotFoundException) { } catch (e: ActivityNotFoundException) {
Timber.e(e) Timber.e(e)
showSnackbarMessage(application.getString(R.string.no_browser_detected)) showSnackbarMessage(context.getString(R.string.no_browser_detected))
} }
} }
fun onVpnPermissionAccepted() { fun onVpnPermissionAccepted() {
_appUiState.value = _appUiState.value.copy( _appUiState.update {
vpnPermissionAccepted = true, it.copy(
) vpnPermissionAccepted = true
)
}
} }
fun launchEmail() { fun launchEmail(context: Context) {
try { try {
val intent = val intent =
Intent(Intent.ACTION_SENDTO).apply { Intent(Intent.ACTION_SENDTO).apply {
type = Constants.EMAIL_MIME_TYPE type = Constants.EMAIL_MIME_TYPE
putExtra(Intent.EXTRA_EMAIL, arrayOf(application.getString(R.string.my_email))) putExtra(Intent.EXTRA_EMAIL, arrayOf(context.getString(R.string.my_email)))
putExtra(Intent.EXTRA_SUBJECT, application.getString(R.string.email_subject)) putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.email_subject))
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
} }
application.startActivity( context.startActivity(
Intent.createChooser(intent, application.getString(R.string.email_chooser)).apply { Intent.createChooser(intent, context.getString(R.string.email_chooser)).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}, },
) )
} catch (e: ActivityNotFoundException) { } catch (e: ActivityNotFoundException) {
Timber.e(e) Timber.e(e)
showSnackbarMessage(application.getString(R.string.no_email_detected)) showSnackbarMessage(context.getString(R.string.no_email_detected))
} }
} }
fun showSnackbarMessage(message: String) { fun showSnackbarMessage(message: String) {
_appUiState.value = _appUiState.value.copy( _appUiState.update {
snackbarMessage = message, it.copy(
snackbarMessageConsumed = false, snackbarMessage = message,
) snackbarMessageConsumed = false,
)
}
} }
fun snackbarMessageConsumed() { fun snackbarMessageConsumed() {
_appUiState.value = _appUiState.value.copy( _appUiState.update {
snackbarMessage = "", it.copy(
snackbarMessageConsumed = true, snackbarMessage = "",
) snackbarMessageConsumed = true,
)
}
} }
val logs = mutableStateListOf<LogMessage>() val logs = mutableStateListOf<LogMessage>()
@@ -118,12 +127,12 @@ constructor(
fun readLogCatOutput() = fun readLogCatOutput() =
viewModelScope.launch(viewModelScope.coroutineContext + Dispatchers.IO) { viewModelScope.launch(viewModelScope.coroutineContext + Dispatchers.IO) {
launch { launch {
Logcatter.logs { Logcatter.logs(callback = {
logs.add(it) logs.add(it)
if (logs.size > Constants.LOG_BUFFER_SIZE) { if (logs.size > Constants.LOG_BUFFER_SIZE) {
logs.removeRange(0, (logs.size - Constants.LOG_BUFFER_SIZE).toInt()) logs.removeRange(0, (logs.size - Constants.LOG_BUFFER_SIZE).toInt())
} }
} })
} }
} }
@@ -132,17 +141,19 @@ constructor(
Logcatter.clear() Logcatter.clear()
} }
fun saveLogsToFile() { fun saveLogsToFile(context: Context) {
val fileName = "${Constants.BASE_LOG_FILE_NAME}-${Instant.now().epochSecond}.txt" val fileName = "${Constants.BASE_LOG_FILE_NAME}-${Instant.now().epochSecond}.txt"
val content = logs.joinToString(separator = "\n") val content = logs.joinToString(separator = "\n")
FileUtils.saveFileToDownloads(application.applicationContext, content, fileName) FileUtils.saveFileToDownloads(context.applicationContext, content, fileName)
Toast.makeText(application, application.getString(R.string.logs_saved), Toast.LENGTH_SHORT) Toast.makeText(context, context.getString(R.string.logs_saved), Toast.LENGTH_SHORT)
.show() .show()
} }
fun setNotificationPermissionAccepted(accepted: Boolean) { fun setNotificationPermissionAccepted(accepted: Boolean) {
_appUiState.value = _appUiState.value.copy( _appUiState.update {
notificationPermissionAccepted = accepted, it.copy(
) notificationPermissionAccepted = accepted,
)
}
} }
} }
@@ -32,10 +32,12 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState import com.google.accompanist.permissions.rememberPermissionState
@@ -47,6 +49,7 @@ import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar
import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.options.OptionsScreen import com.zaneschepke.wireguardautotunnel.ui.screens.options.OptionsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.pinlock.PinLockScreen import com.zaneschepke.wireguardautotunnel.ui.screens.pinlock.PinLockScreen
@@ -84,7 +87,7 @@ class MainActivity : AppCompatActivity() {
// load preferences into memory and init data // load preferences into memory and init data
lifecycleScope.launch { lifecycleScope.launch {
dataStoreManager.init() dataStoreManager.init()
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(this@MainActivity) WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
val settings = settingsRepository.getSettings() val settings = settingsRepository.getSettings()
if (settings.isAutoTunnelEnabled) { if (settings.isAutoTunnelEnabled) {
serviceManager.startWatcherService(application.applicationContext) serviceManager.startWatcherService(application.applicationContext)
@@ -138,7 +141,11 @@ class MainActivity : AppCompatActivity() {
return@LaunchedEffect notificationPermissionState.launchPermissionRequest() return@LaunchedEffect notificationPermissionState.launchPermissionRequest()
} }
if (!appUiState.vpnPermissionAccepted) { if (!appUiState.vpnPermissionAccepted) {
return@LaunchedEffect vpnActivityResultState.launch(appViewModel.vpnIntent) return@LaunchedEffect appViewModel.vpnIntent?.let {
vpnActivityResultState.launch(
it
)
}!!
} }
} }
} }
@@ -232,14 +239,28 @@ class MainActivity : AppCompatActivity() {
composable(Screen.Support.Logs.route) { composable(Screen.Support.Logs.route) {
LogsScreen(appViewModel) LogsScreen(appViewModel)
} }
composable("${Screen.Config.route}/{id}") { //TODO fix navigation for amnezia
composable("${Screen.Config.route}/{id}?configType={configType}", arguments =
listOf(
navArgument("id") {
type = NavType.StringType
defaultValue = "0"
},
navArgument("configType") {
type = NavType.StringType
defaultValue = ConfigType.WIREGUARD.name
}
)
) {
val id = it.arguments?.getString("id") val id = it.arguments?.getString("id")
val configType = ConfigType.valueOf( it.arguments?.getString("configType") ?: ConfigType.WIREGUARD.name)
if (!id.isNullOrBlank()) { if (!id.isNullOrBlank()) {
ConfigScreen( ConfigScreen(
navController = navController, navController = navController,
tunnelId = id, tunnelId = id,
appViewModel = appViewModel, appViewModel = appViewModel,
focusRequester = focusRequester, focusRequester = focusRequester,
configType = configType
) )
} }
} }
@@ -4,13 +4,15 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Home import androidx.compose.material.icons.rounded.Home
import androidx.compose.material.icons.rounded.QuestionMark import androidx.compose.material.icons.rounded.QuestionMark
import androidx.compose.material.icons.rounded.Settings import androidx.compose.material.icons.rounded.Settings
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
sealed class Screen(val route: String) { sealed class Screen(val route: String) {
data object Main : Screen("main") { data object Main : Screen("main") {
val navItem = val navItem =
BottomNavItem( BottomNavItem(
name = "Tunnels", name = WireGuardAutoTunnel.instance.getString(R.string.tunnels),
route = route, route = route,
icon = Icons.Rounded.Home, icon = Icons.Rounded.Home,
) )
@@ -19,7 +21,7 @@ sealed class Screen(val route: String) {
data object Settings : Screen("settings") { data object Settings : Screen("settings") {
val navItem = val navItem =
BottomNavItem( BottomNavItem(
name = "Settings", name = WireGuardAutoTunnel.instance.getString(R.string.settings),
route = route, route = route,
icon = Icons.Rounded.Settings, icon = Icons.Rounded.Settings,
) )
@@ -28,7 +30,7 @@ sealed class Screen(val route: String) {
data object Support : Screen("support") { data object Support : Screen("support") {
val navItem = val navItem =
BottomNavItem( BottomNavItem(
name = "Support", name = WireGuardAutoTunnel.instance.getString(R.string.support),
route = route, route = route,
icon = Icons.Rounded.QuestionMark, icon = Icons.Rounded.QuestionMark,
) )
@@ -17,7 +17,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.wireguard.android.backend.Statistics import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.util.NumberUtils import com.zaneschepke.wireguardautotunnel.util.NumberUtils
import com.zaneschepke.wireguardautotunnel.util.toThreeDecimalPlaceString import com.zaneschepke.wireguardautotunnel.util.toThreeDecimalPlaceString
@@ -30,7 +30,7 @@ fun RowListItem(
onClick: () -> Unit, onClick: () -> Unit,
rowButton: @Composable () -> Unit, rowButton: @Composable () -> Unit,
expanded: Boolean, expanded: Boolean,
statistics: Statistics? statistics: TunnelStatistics?
) { ) {
Box( Box(
modifier = modifier =
@@ -59,7 +59,7 @@ fun RowListItem(
rowButton() rowButton()
} }
if (expanded) { if (expanded) {
statistics?.peers()?.forEach { statistics?.getPeers()?.forEach {
Row( Row(
modifier = modifier =
Modifier Modifier
@@ -69,9 +69,9 @@ fun RowListItem(
horizontalArrangement = Arrangement.SpaceEvenly, horizontalArrangement = Arrangement.SpaceEvenly,
) { ) {
//TODO change these to string resources //TODO change these to string resources
val handshakeEpoch = statistics.peer(it)!!.latestHandshakeEpochMillis val handshakeEpoch = statistics.peerStats(it)!!.latestHandshakeEpochMillis
val peerTx = statistics.peer(it)!!.txBytes val peerTx = statistics.peerStats(it)!!.txBytes
val peerRx = statistics.peer(it)!!.rxBytes val peerRx = statistics.peerStats(it)!!.rxBytes
val peerId = it.toBase64().subSequence(0, 3).toString() + "***" val peerId = it.toBase64().subSequence(0, 3).toString() + "***"
val handshakeSec = val handshakeSec =
NumberUtils.getSecondsBetweenTimestampAndNow(handshakeEpoch) NumberUtils.getSecondsBetweenTimestampAndNow(handshakeEpoch)
@@ -28,7 +28,12 @@ fun ConfigurationToggle(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
) { ) {
Text(label, textAlign = TextAlign.Start) Text(label, textAlign = TextAlign.Start, modifier = Modifier
.weight(
weight = 1.0f,
fill = false,
),
softWrap = true)
Switch( Switch(
modifier = modifier, modifier = modifier,
enabled = enabled, enabled = enabled,
@@ -1,30 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.models
import com.wireguard.config.Interface
data class InterfaceProxy(
var privateKey: String = "",
var publicKey: String = "",
var addresses: String = "",
var dnsServers: String = "",
var listenPort: String = "",
var mtu: String = ""
) {
companion object {
fun from(i: Interface): InterfaceProxy {
return InterfaceProxy(
publicKey = i.keyPair.publicKey.toBase64().trim(),
privateKey = i.keyPair.privateKey.toBase64().trim(),
addresses = i.addresses.joinToString(", ").trim(),
dnsServers = i.dnsServers.joinToString(", ").replace("/", "").trim(),
listenPort =
if (i.listenPort.isPresent) {
i.listenPort.get().toString().trim()
} else {
""
},
mtu = if (i.mtu.isPresent) i.mtu.get().toString().trim() else "",
)
}
}
}
@@ -79,9 +79,9 @@ import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.Event import com.zaneschepke.wireguardautotunnel.util.getMessage
import com.zaneschepke.wireguardautotunnel.util.Result
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@@ -94,7 +94,8 @@ fun ConfigScreen(
focusRequester: FocusRequester, focusRequester: FocusRequester,
navController: NavController, navController: NavController,
appViewModel: AppViewModel, appViewModel: AppViewModel,
tunnelId: String tunnelId: String,
configType: ConfigType
) { ) {
val context = LocalContext.current val context = LocalContext.current
val clipboardManager: ClipboardManager = LocalClipboardManager.current val clipboardManager: ClipboardManager = LocalClipboardManager.current
@@ -148,11 +149,11 @@ fun ConfigScreen(
}, },
onError = { onError = {
showAuthPrompt = false showAuthPrompt = false
appViewModel.showSnackbarMessage(Event.Error.AuthenticationFailed.message) appViewModel.showSnackbarMessage(context.getString(R.string.error_authentication_failed))
}, },
onFailure = { onFailure = {
showAuthPrompt = false showAuthPrompt = false
appViewModel.showSnackbarMessage(Event.Error.AuthorizationFailed.message) appViewModel.showSnackbarMessage(context.getString(R.string.error_authorization_failed))
}, },
) )
} }
@@ -319,15 +320,11 @@ fun ConfigScreen(
} }
}, },
onClick = { onClick = {
viewModel.onSaveAllChanges().let { viewModel.onSaveAllChanges(configType).onSuccess {
when (it) { appViewModel.showSnackbarMessage(context.getString(R.string.config_changes_saved))
is Result.Success -> { navController.navigate(Screen.Main.route)
appViewModel.showSnackbarMessage(it.data.message) }.onFailure {
navController.navigate(Screen.Main.route) appViewModel.showSnackbarMessage(it.getMessage(context))
}
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
}
} }
}, },
containerColor = fobColor, containerColor = fobColor,
@@ -486,6 +483,98 @@ fun ConfigScreen(
modifier = Modifier.width(IntrinsicSize.Min), modifier = Modifier.width(IntrinsicSize.Min),
) )
} }
if(configType == ConfigType.AMNEZIA) {
ConfigurationTextBox(
value = uiState.interfaceProxy.junkPacketCount,
onValueChange = { value -> viewModel.onJunkPacketCountChanged(value) },
keyboardActions = keyboardActions,
label = stringResource(R.string.junk_packet_count),
hint = stringResource(R.string.junk_packet_count).lowercase(),
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
)
ConfigurationTextBox(
value = uiState.interfaceProxy.junkPacketMinSize,
onValueChange = { value -> viewModel.onJunkPacketMinSizeChanged(value) },
keyboardActions = keyboardActions,
label = stringResource(R.string.junk_packet_minimum_size),
hint = stringResource(R.string.junk_packet_minimum_size).lowercase(),
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
)
ConfigurationTextBox(
value = uiState.interfaceProxy.junkPacketMaxSize,
onValueChange = { value -> viewModel.onJunkPacketMaxSizeChanged(value) },
keyboardActions = keyboardActions,
label = stringResource(R.string.junk_packet_maximum_size),
hint = stringResource(R.string.junk_packet_maximum_size).lowercase(),
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
)
ConfigurationTextBox(
value = uiState.interfaceProxy.initPacketJunkSize,
onValueChange = { value -> viewModel.onInitPacketJunkSizeChanged(value) },
keyboardActions = keyboardActions,
label = stringResource(R.string.init_packet_junk_size),
hint = stringResource(R.string.init_packet_junk_size).lowercase(),
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
)
ConfigurationTextBox(
value = uiState.interfaceProxy.responsePacketJunkSize,
onValueChange = { value -> viewModel.onResponsePacketJunkSize(value) },
keyboardActions = keyboardActions,
label = stringResource(R.string.response_packet_junk_size),
hint = stringResource(R.string.response_packet_junk_size).lowercase(),
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
)
ConfigurationTextBox(
value = uiState.interfaceProxy.initPacketMagicHeader,
onValueChange = { value -> viewModel.onInitPacketMagicHeader(value) },
keyboardActions = keyboardActions,
label = stringResource(R.string.init_packet_magic_header),
hint = stringResource(R.string.init_packet_magic_header).lowercase(),
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
)
ConfigurationTextBox(
value = uiState.interfaceProxy.responsePacketMagicHeader,
onValueChange = { value -> viewModel.onResponsePacketMagicHeader(value) },
keyboardActions = keyboardActions,
label = stringResource(R.string.response_packet_magic_header),
hint = stringResource(R.string.response_packet_magic_header).lowercase(),
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
)
ConfigurationTextBox(
value = uiState.interfaceProxy.underloadPacketMagicHeader,
onValueChange = { value -> viewModel.onUnderloadPacketMagicHeader(value) },
keyboardActions = keyboardActions,
label = stringResource(R.string.underload_packet_magic_header),
hint = stringResource(R.string.underload_packet_magic_header).lowercase(),
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
)
ConfigurationTextBox(
value = uiState.interfaceProxy.transportPacketMagicHeader,
onValueChange = { value -> viewModel.onTransportPacketMagicHeader(value) },
keyboardActions = keyboardActions,
label = stringResource(R.string.transport_packet_magic_header),
hint = stringResource(R.string.transport_packet_magic_header).lowercase(),
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
)
}
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier modifier = Modifier
@@ -1,8 +1,9 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.config package com.zaneschepke.wireguardautotunnel.ui.screens.config
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig import com.wireguard.config.Config
import com.zaneschepke.wireguardautotunnel.ui.models.InterfaceProxy import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.ui.models.PeerProxy import com.zaneschepke.wireguardautotunnel.ui.screens.config.model.InterfaceProxy
import com.zaneschepke.wireguardautotunnel.ui.screens.config.model.PeerProxy
import com.zaneschepke.wireguardautotunnel.util.Packages import com.zaneschepke.wireguardautotunnel.util.Packages
data class ConfigUiState( data class ConfigUiState(
@@ -14,5 +15,58 @@ data class ConfigUiState(
val isAllApplicationsEnabled: Boolean = false, val isAllApplicationsEnabled: Boolean = false,
val loading: Boolean = true, val loading: Boolean = true,
val tunnel: TunnelConfig? = null, val tunnel: TunnelConfig? = null,
val tunnelName: String = "" val tunnelName: String = "",
) val isAmneziaEnabled: Boolean = false
) {
companion object {
fun from(config : Config) : ConfigUiState {
val proxyPeers = config.peers.map { PeerProxy.from(it) }
val proxyInterface = InterfaceProxy.from(config.`interface`)
var include = true
var isAllApplicationsEnabled = false
val checkedPackages =
if (config.`interface`.includedApplications.isNotEmpty()) {
config.`interface`.includedApplications
} else if (config.`interface`.excludedApplications.isNotEmpty()) {
include = false
config.`interface`.excludedApplications
} else {
isAllApplicationsEnabled = true
emptySet()
}
return ConfigUiState(
proxyPeers,
proxyInterface,
emptyList(),
checkedPackages.toList(),
include,
isAllApplicationsEnabled,
)
}
fun from(config: org.amnezia.awg.config.Config) : ConfigUiState {
//TODO update with new values
val proxyPeers = config.peers.map { PeerProxy.from(it) }
val proxyInterface = InterfaceProxy.from(config.`interface`)
var include = true
var isAllApplicationsEnabled = false
val checkedPackages =
if (config.`interface`.includedApplications.isNotEmpty()) {
config.`interface`.includedApplications
} else if (config.`interface`.excludedApplications.isNotEmpty()) {
include = false
config.`interface`.excludedApplications
} else {
isAllApplicationsEnabled = true
emptySet()
}
return ConfigUiState(
proxyPeers,
proxyInterface,
emptyList(),
checkedPackages.toList(),
include,
isAllApplicationsEnabled,
)
}
}
}
@@ -1,7 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.config package com.zaneschepke.wireguardautotunnel.ui.screens.config
import android.Manifest import android.Manifest
import android.app.Application
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build import android.os.Build
@@ -12,21 +11,24 @@ import com.wireguard.config.Interface
import com.wireguard.config.Peer import com.wireguard.config.Peer
import com.wireguard.crypto.Key import com.wireguard.crypto.Key
import com.wireguard.crypto.KeyPair import com.wireguard.crypto.KeyPair
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.ui.models.InterfaceProxy import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.ui.models.PeerProxy import com.zaneschepke.wireguardautotunnel.ui.screens.config.model.PeerProxy
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.Event
import com.zaneschepke.wireguardautotunnel.util.NumberUtils import com.zaneschepke.wireguardautotunnel.util.NumberUtils
import com.zaneschepke.wireguardautotunnel.util.Result import com.zaneschepke.wireguardautotunnel.util.StringValue
import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions
import com.zaneschepke.wireguardautotunnel.util.removeAt import com.zaneschepke.wireguardautotunnel.util.removeAt
import com.zaneschepke.wireguardautotunnel.util.update import com.zaneschepke.wireguardautotunnel.util.update
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@@ -35,11 +37,11 @@ import javax.inject.Inject
class ConfigViewModel class ConfigViewModel
@Inject @Inject
constructor( constructor(
private val application: Application, private val settingsRepository: SettingsRepository,
private val appDataRepository: AppDataRepository private val appDataRepository: AppDataRepository
) : ViewModel() { ) : ViewModel() {
private val packageManager = application.packageManager private val packageManager = WireGuardAutoTunnel.instance.packageManager
private val _uiState = MutableStateFlow(ConfigUiState()) private val _uiState = MutableStateFlow(ConfigUiState())
val uiState = _uiState.asStateFlow() val uiState = _uiState.asStateFlow()
@@ -52,32 +54,17 @@ constructor(
val tunnelConfig = val tunnelConfig =
appDataRepository.tunnels.getAll() appDataRepository.tunnels.getAll()
.firstOrNull { it.id.toString() == tunnelId } .firstOrNull { it.id.toString() == tunnelId }
val isAmneziaEnabled = settingsRepository.getSettings().isAmneziaEnabled
if (tunnelConfig != null) { if (tunnelConfig != null) {
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick) (if(isAmneziaEnabled) {
val proxyPeers = config.peers.map { PeerProxy.from(it) } val amConfig = if(tunnelConfig.amQuick == "") tunnelConfig.wgQuick else tunnelConfig.amQuick
val proxyInterface = InterfaceProxy.from(config.`interface`) ConfigUiState.from(TunnelConfig.configFromAmQuick(amConfig))
var include = true } else ConfigUiState.from(TunnelConfig.configFromWgQuick(tunnelConfig.wgQuick))).copy(
var isAllApplicationsEnabled = false packages = packages,
val checkedPackages = loading = false,
if (config.`interface`.includedApplications.isNotEmpty()) { tunnel = tunnelConfig,
config.`interface`.includedApplications tunnelName = tunnelConfig.name,
} else if (config.`interface`.excludedApplications.isNotEmpty()) { isAmneziaEnabled = isAmneziaEnabled
include = false
config.`interface`.excludedApplications
} else {
isAllApplicationsEnabled = true
emptySet()
}
ConfigUiState(
proxyPeers,
proxyInterface,
packages,
checkedPackages.toList(),
include,
isAllApplicationsEnabled,
false,
tunnelConfig,
tunnelConfig.name,
) )
} else { } else {
ConfigUiState(loading = false, packages = packages) ConfigUiState(loading = false, packages = packages)
@@ -121,7 +108,7 @@ constructor(
} }
fun getPackageLabel(packageInfo: PackageInfo): String { fun getPackageLabel(packageInfo: PackageInfo): String {
return packageInfo.applicationInfo.loadLabel(application.packageManager).toString() return packageInfo.applicationInfo.loadLabel(packageManager).toString()
} }
private fun getAllInternetCapablePackages(): List<PackageInfo> { private fun getAllInternetCapablePackages(): List<PackageInfo> {
@@ -150,7 +137,7 @@ constructor(
viewModelScope.launch { viewModelScope.launch {
if (tunnelConfig != null) { if (tunnelConfig != null) {
saveConfig(tunnelConfig).join() saveConfig(tunnelConfig).join()
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(application) WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
} }
} }
@@ -168,6 +155,20 @@ constructor(
} }
} }
private fun buildAmPeerListFromProxyPeers(): List<org.amnezia.awg.config.Peer> {
return _uiState.value.proxyPeers.map {
val builder = org.amnezia.awg.config.Peer.Builder()
if (it.allowedIps.isNotEmpty()) builder.parseAllowedIPs(it.allowedIps.trim())
if (it.publicKey.isNotEmpty()) builder.parsePublicKey(it.publicKey.trim())
if (it.preSharedKey.isNotEmpty()) builder.parsePreSharedKey(it.preSharedKey.trim())
if (it.endpoint.isNotEmpty()) builder.parseEndpoint(it.endpoint.trim())
if (it.persistentKeepalive.isNotEmpty()) {
builder.parsePersistentKeepalive(it.persistentKeepalive.trim())
}
builder.build()
}
}
private fun emptyCheckedPackagesList() { private fun emptyCheckedPackagesList() {
_uiState.value = _uiState.value.copy(checkedPackageNames = emptyList()) _uiState.value = _uiState.value.copy(checkedPackageNames = emptyList())
} }
@@ -190,147 +191,226 @@ constructor(
return builder.build() return builder.build()
} }
fun onSaveAllChanges(): Result<Event> { private fun buildAmInterfaceListFromProxyInterface(): org.amnezia.awg.config.Interface {
val builder = org.amnezia.awg.config.Interface.Builder()
builder.parsePrivateKey(_uiState.value.interfaceProxy.privateKey.trim())
builder.parseAddresses(_uiState.value.interfaceProxy.addresses.trim())
if (_uiState.value.interfaceProxy.dnsServers.isNotEmpty()) {
builder.parseDnsServers(_uiState.value.interfaceProxy.dnsServers.trim())
}
if (_uiState.value.interfaceProxy.mtu.isNotEmpty())
builder.parseMtu(_uiState.value.interfaceProxy.mtu.trim())
if (_uiState.value.interfaceProxy.listenPort.isNotEmpty()) {
builder.parseListenPort(_uiState.value.interfaceProxy.listenPort.trim())
}
if (isAllApplicationsEnabled()) emptyCheckedPackagesList()
if (_uiState.value.include) builder.includeApplications(_uiState.value.checkedPackageNames)
if (!_uiState.value.include) builder.excludeApplications(_uiState.value.checkedPackageNames)
if(_uiState.value.interfaceProxy.junkPacketCount.isNotEmpty()) {
builder.setJunkPacketCount(_uiState.value.interfaceProxy.junkPacketCount.trim().toInt())
}
if(_uiState.value.interfaceProxy.junkPacketMinSize.isNotEmpty()) {
builder.setJunkPacketMinSize(_uiState.value.interfaceProxy.junkPacketMinSize.trim().toInt())
}
if(_uiState.value.interfaceProxy.junkPacketMaxSize.isNotEmpty()) {
builder.setJunkPacketMaxSize(_uiState.value.interfaceProxy.junkPacketMaxSize.trim().toInt())
}
if(_uiState.value.interfaceProxy.initPacketJunkSize.isNotEmpty()) {
builder.setInitPacketJunkSize(_uiState.value.interfaceProxy.initPacketJunkSize.trim().toInt())
}
if(_uiState.value.interfaceProxy.responsePacketJunkSize.isNotEmpty()) {
builder.setResponsePacketJunkSize(_uiState.value.interfaceProxy.responsePacketJunkSize.trim().toInt())
}
if(_uiState.value.interfaceProxy.initPacketMagicHeader.isNotEmpty()) {
builder.setInitPacketMagicHeader(_uiState.value.interfaceProxy.initPacketMagicHeader.trim().toLong())
}
if(_uiState.value.interfaceProxy.responsePacketMagicHeader.isNotEmpty()) {
builder.setResponsePacketMagicHeader(_uiState.value.interfaceProxy.responsePacketMagicHeader.trim().toLong())
}
if(_uiState.value.interfaceProxy.transportPacketMagicHeader.isNotEmpty()) {
builder.setTransportPacketMagicHeader(_uiState.value.interfaceProxy.transportPacketMagicHeader.trim().toLong())
}
if(_uiState.value.interfaceProxy.underloadPacketMagicHeader.isNotEmpty()) {
builder.setUnderloadPacketMagicHeader(_uiState.value.interfaceProxy.underloadPacketMagicHeader.trim().toLong())
}
return builder.build()
}
private fun buildConfig() : Config {
val peerList = buildPeerListFromProxyPeers()
val wgInterface = buildInterfaceListFromProxyInterface()
return Config.Builder().addPeers(peerList).setInterface(wgInterface).build()
}
private fun buildAmConfig() : org.amnezia.awg.config.Config {
val peerList = buildAmPeerListFromProxyPeers()
val amInterface = buildAmInterfaceListFromProxyInterface()
return org.amnezia.awg.config.Config.Builder().addPeers(peerList).setInterface(amInterface).build()
}
fun onSaveAllChanges(configType: ConfigType): Result<Unit> {
return try { return try {
val peerList = buildPeerListFromProxyPeers() val wgQuick = buildConfig().toWgQuickString()
val wgInterface = buildInterfaceListFromProxyInterface() val amQuick = if(configType == ConfigType.AMNEZIA) {
val config = Config.Builder().addPeers(peerList).setInterface(wgInterface).build() buildAmConfig().toAwgQuickString()
} else TunnelConfig.AM_QUICK_DEFAULT
val tunnelConfig = when (uiState.value.tunnel) { val tunnelConfig = when (uiState.value.tunnel) {
null -> TunnelConfig( null -> TunnelConfig(
name = _uiState.value.tunnelName, name = _uiState.value.tunnelName,
wgQuick = config.toWgQuickString(), wgQuick = wgQuick,
amQuick = amQuick
) )
else -> uiState.value.tunnel!!.copy( else -> uiState.value.tunnel!!.copy(
name = _uiState.value.tunnelName, name = _uiState.value.tunnelName,
wgQuick = config.toWgQuickString(), wgQuick = wgQuick,
amQuick = amQuick
) )
} }
updateTunnelConfig(tunnelConfig) updateTunnelConfig(tunnelConfig)
Result.Success(Event.Message.ConfigSaved) Result.success(Unit)
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e) Timber.e(e)
val message = e.message?.substringAfter(":", missingDelimiterValue = "") val message = e.message?.substringAfter(":", missingDelimiterValue = "")
Result.Error(Event.Error.ConfigParseError(message ?: "")) val stringValue = message?.let {
StringValue.DynamicString(message)
} ?: StringValue.StringResource(R.string.unknown_error)
Result.failure(WgTunnelExceptions.ConfigParseError(stringValue))
} }
} }
fun onPeerPublicKeyChange(index: Int, value: String) { fun onPeerPublicKeyChange(index: Int, value: String) {
_uiState.value = _uiState.update {
_uiState.value.copy( it.copy(
proxyPeers = proxyPeers =
_uiState.value.proxyPeers.update( _uiState.value.proxyPeers.update(
index, index,
_uiState.value.proxyPeers[index].copy(publicKey = value), _uiState.value.proxyPeers[index].copy(publicKey = value),
), ),
) )
}
} }
fun onPreSharedKeyChange(index: Int, value: String) { fun onPreSharedKeyChange(index: Int, value: String) {
_uiState.value = _uiState.update {
_uiState.value.copy( it.copy(
proxyPeers = proxyPeers =
_uiState.value.proxyPeers.update( _uiState.value.proxyPeers.update(
index, index,
_uiState.value.proxyPeers[index].copy(preSharedKey = value), _uiState.value.proxyPeers[index].copy(preSharedKey = value),
), ),
) )
}
} }
fun onEndpointChange(index: Int, value: String) { fun onEndpointChange(index: Int, value: String) {
_uiState.value = _uiState.update {
_uiState.value.copy( it.copy(
proxyPeers = proxyPeers =
_uiState.value.proxyPeers.update( _uiState.value.proxyPeers.update(
index, index,
_uiState.value.proxyPeers[index].copy(endpoint = value), _uiState.value.proxyPeers[index].copy(endpoint = value),
), ),
) )
}
} }
fun onAllowedIpsChange(index: Int, value: String) { fun onAllowedIpsChange(index: Int, value: String) {
_uiState.value = _uiState.update {
_uiState.value.copy( it.copy(
proxyPeers = proxyPeers =
_uiState.value.proxyPeers.update( _uiState.value.proxyPeers.update(
index, index,
_uiState.value.proxyPeers[index].copy(allowedIps = value), _uiState.value.proxyPeers[index].copy(allowedIps = value),
), ),
) )
}
} }
fun onPersistentKeepaliveChanged(index: Int, value: String) { fun onPersistentKeepaliveChanged(index: Int, value: String) {
_uiState.value = _uiState.update {
_uiState.value.copy( it.copy(
proxyPeers = proxyPeers =
_uiState.value.proxyPeers.update( _uiState.value.proxyPeers.update(
index, index,
_uiState.value.proxyPeers[index].copy(persistentKeepalive = value), _uiState.value.proxyPeers[index].copy(persistentKeepalive = value),
), ),
) )
}
} }
fun onDeletePeer(index: Int) { fun onDeletePeer(index: Int) {
_uiState.value = _uiState.update {
_uiState.value.copy( it.copy(
proxyPeers = _uiState.value.proxyPeers.removeAt(index), proxyPeers = _uiState.value.proxyPeers.removeAt(index),
) )
}
} }
fun addEmptyPeer() { fun addEmptyPeer() {
_uiState.value = _uiState.value.copy(proxyPeers = _uiState.value.proxyPeers + PeerProxy()) _uiState.update {
it.copy(proxyPeers = _uiState.value.proxyPeers + PeerProxy())
}
} }
fun generateKeyPair() { fun generateKeyPair() {
val keyPair = KeyPair() val keyPair = KeyPair()
_uiState.value = _uiState.update {
_uiState.value.copy( it.copy(
interfaceProxy = interfaceProxy =
_uiState.value.interfaceProxy.copy( _uiState.value.interfaceProxy.copy(
privateKey = keyPair.privateKey.toBase64(), privateKey = keyPair.privateKey.toBase64(),
publicKey = keyPair.publicKey.toBase64(), publicKey = keyPair.publicKey.toBase64(),
), ),
) )
}
} }
fun onAddressesChanged(value: String) { fun onAddressesChanged(value: String) {
_uiState.value = _uiState.update {
_uiState.value.copy( it.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(addresses = value), interfaceProxy = _uiState.value.interfaceProxy.copy(addresses = value),
) )
}
} }
fun onListenPortChanged(value: String) { fun onListenPortChanged(value: String) {
_uiState.value = _uiState.update {
_uiState.value.copy( it.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(listenPort = value), interfaceProxy = _uiState.value.interfaceProxy.copy(listenPort = value),
) )
}
} }
fun onDnsServersChanged(value: String) { fun onDnsServersChanged(value: String) {
_uiState.value = _uiState.update {
_uiState.value.copy( it.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(dnsServers = value), interfaceProxy = _uiState.value.interfaceProxy.copy(dnsServers = value),
) )
}
} }
fun onMtuChanged(value: String) { fun onMtuChanged(value: String) {
_uiState.value = _uiState.update {
_uiState.value.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(mtu = value)) it.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(mtu = value))
}
} }
private fun onInterfacePublicKeyChange(value: String) { private fun onInterfacePublicKeyChange(value: String) {
_uiState.value = _uiState.update {
_uiState.value.copy( it.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(publicKey = value), interfaceProxy = _uiState.value.interfaceProxy.copy(publicKey = value),
) )
}
} }
fun onPrivateKeyChange(value: String) { fun onPrivateKeyChange(value: String) {
_uiState.value = _uiState.update {
_uiState.value.copy( it.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(privateKey = value), interfaceProxy = _uiState.value.interfaceProxy.copy(privateKey = value),
) )
}
if (NumberUtils.isValidKey(value)) { if (NumberUtils.isValidKey(value)) {
val pair = KeyPair(Key.fromBase64(value)) val pair = KeyPair(Key.fromBase64(value))
onInterfacePublicKeyChange(pair.publicKey.toBase64()) onInterfacePublicKeyChange(pair.publicKey.toBase64())
@@ -344,6 +424,77 @@ constructor(
getAllInternetCapablePackages().filter { getAllInternetCapablePackages().filter {
getPackageLabel(it).lowercase().contains(query.lowercase()) getPackageLabel(it).lowercase().contains(query.lowercase())
} }
_uiState.value = _uiState.value.copy(packages = packages) _uiState.update { it.copy(packages = packages) }
}
fun onJunkPacketCountChanged(value: String) {
_uiState.update {
it.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(junkPacketCount = value)
)
}
}
fun onJunkPacketMinSizeChanged(value: String) {
_uiState.update {
it.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(junkPacketMinSize = value)
)
}
}
fun onJunkPacketMaxSizeChanged(value: String) {
_uiState.update {
it.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(junkPacketMaxSize = value)
)
}
}
fun onInitPacketJunkSizeChanged(value: String) {
_uiState.update {
it.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(initPacketJunkSize = value)
)
}
}
fun onResponsePacketJunkSize(value: String) {
_uiState.update {
it.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(responsePacketJunkSize = value)
)
}
}
fun onInitPacketMagicHeader(value: String) {
_uiState.update {
it.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(initPacketMagicHeader = value)
)
}
}
fun onResponsePacketMagicHeader(value: String) {
_uiState.update {
it.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(responsePacketMagicHeader = value)
)
}
}
fun onTransportPacketMagicHeader(value: String) {
_uiState.update {
it.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(transportPacketMagicHeader = value)
)
}
}
fun onUnderloadPacketMagicHeader(value: String) {
_uiState.update {
it.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(underloadPacketMagicHeader = value)
)
}
} }
} }
@@ -0,0 +1,63 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.config.model
import com.wireguard.config.Interface
data class InterfaceProxy(
val privateKey: String = "",
val publicKey: String = "",
val addresses: String = "",
val dnsServers: String = "",
val listenPort: String = "",
val mtu: String = "",
val junkPacketCount: String = "",
val junkPacketMinSize: String = "",
val junkPacketMaxSize: String = "",
val initPacketJunkSize: String = "",
val responsePacketJunkSize: String = "",
val initPacketMagicHeader: String = "",
val responsePacketMagicHeader: String = "",
val underloadPacketMagicHeader: String = "",
val transportPacketMagicHeader: String = "",
) {
companion object {
fun from(i: Interface): InterfaceProxy {
return InterfaceProxy(
publicKey = i.keyPair.publicKey.toBase64().trim(),
privateKey = i.keyPair.privateKey.toBase64().trim(),
addresses = i.addresses.joinToString(", ").trim(),
dnsServers = i.dnsServers.joinToString(", ").replace("/", "").trim(),
listenPort =
if (i.listenPort.isPresent) {
i.listenPort.get().toString().trim()
} else {
""
},
mtu = if (i.mtu.isPresent) i.mtu.get().toString().trim() else "",
)
}
fun from(i: org.amnezia.awg.config.Interface) : InterfaceProxy {
return InterfaceProxy(
publicKey = i.keyPair.publicKey.toBase64().trim(),
privateKey = i.keyPair.privateKey.toBase64().trim(),
addresses = i.addresses.joinToString(", ").trim(),
dnsServers = i.dnsServers.joinToString(", ").replace("/", "").trim(),
listenPort =
if (i.listenPort.isPresent) {
i.listenPort.get().toString().trim()
} else {
""
},
mtu = if (i.mtu.isPresent) i.mtu.get().toString().trim() else "",
junkPacketCount = if(i.junkPacketCount.isPresent) i.junkPacketCount.get().toString() else "",
junkPacketMinSize = if(i.junkPacketMinSize.isPresent) i.junkPacketMinSize.get().toString() else "",
junkPacketMaxSize = if(i.junkPacketMaxSize.isPresent) i.junkPacketMaxSize.get().toString() else "",
initPacketJunkSize = if(i.initPacketJunkSize.isPresent) i.initPacketJunkSize.get().toString() else "",
responsePacketJunkSize = if(i.responsePacketJunkSize.isPresent) i.responsePacketJunkSize.get().toString() else "",
initPacketMagicHeader = if(i.initPacketMagicHeader.isPresent) i.initPacketMagicHeader.get().toString() else "",
responsePacketMagicHeader = if(i.responsePacketMagicHeader.isPresent) i.responsePacketMagicHeader.get().toString() else "",
transportPacketMagicHeader = if(i.transportPacketMagicHeader.isPresent) i.transportPacketMagicHeader.get().toString() else "",
underloadPacketMagicHeader = if(i.underloadPacketMagicHeader.isPresent) i.underloadPacketMagicHeader.get().toString() else "",
)
}
}
}
@@ -1,13 +1,13 @@
package com.zaneschepke.wireguardautotunnel.ui.models package com.zaneschepke.wireguardautotunnel.ui.screens.config.model
import com.wireguard.config.Peer import com.wireguard.config.Peer
data class PeerProxy( data class PeerProxy(
var publicKey: String = "", val publicKey: String = "",
var preSharedKey: String = "", val preSharedKey: String = "",
var persistentKeepalive: String = "", val persistentKeepalive: String = "",
var endpoint: String = "", val endpoint: String = "",
var allowedIps: String = IPV4_WILDCARD.joinToString(", ").trim() val allowedIps: String = IPV4_WILDCARD.joinToString(", ").trim()
) { ) {
companion object { companion object {
fun from(peer: Peer): PeerProxy { fun from(peer: Peer): PeerProxy {
@@ -35,6 +35,31 @@ data class PeerProxy(
) )
} }
fun from(peer: org.amnezia.awg.config.Peer) : PeerProxy {
return PeerProxy(
publicKey = peer.publicKey.toBase64(),
preSharedKey =
if (peer.preSharedKey.isPresent) {
peer.preSharedKey.get().toBase64().trim()
} else {
""
},
persistentKeepalive =
if (peer.persistentKeepalive.isPresent) {
peer.persistentKeepalive.get().toString().trim()
} else {
""
},
endpoint =
if (peer.endpoint.isPresent) {
peer.endpoint.get().toString().trim()
} else {
""
},
allowedIps = peer.allowedIps.joinToString(", ").trim(),
)
}
val IPV4_PUBLIC_NETWORKS = val IPV4_PUBLIC_NETWORKS =
setOf( setOf(
"0.0.0.0/5", "0.0.0.0/5",
@@ -0,0 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main
enum class ConfigType {
AMNEZIA,
WIREGUARD
}
@@ -29,11 +29,11 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.overscroll import androidx.compose.foundation.overscroll
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Create import androidx.compose.material.icons.filled.Create
import androidx.compose.material.icons.filled.FileOpen import androidx.compose.material.icons.filled.FileOpen
import androidx.compose.material.icons.filled.QrCode import androidx.compose.material.icons.filled.QrCode
import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.Bolt import androidx.compose.material.icons.rounded.Bolt
import androidx.compose.material.icons.rounded.Circle import androidx.compose.material.icons.rounded.Circle
import androidx.compose.material.icons.rounded.CopyAll import androidx.compose.material.icons.rounded.CopyAll
@@ -45,7 +45,6 @@ import androidx.compose.material.icons.rounded.Star
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FabPosition import androidx.compose.material3.FabPosition
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@@ -71,25 +70,36 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController import androidx.navigation.NavController
import com.iamageo.multifablibrary.FabIcon
import com.iamageo.multifablibrary.FabOption
import com.iamageo.multifablibrary.MultiFabItem
import com.iamageo.multifablibrary.MultiFloatingActionButton
import com.journeyapps.barcodescanner.ScanContract import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanOptions import com.journeyapps.barcodescanner.ScanOptions
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.CaptureActivityPortrait import com.zaneschepke.wireguardautotunnel.ui.CaptureActivityPortrait
import com.zaneschepke.wireguardautotunnel.ui.Screen import com.zaneschepke.wireguardautotunnel.ui.Screen
@@ -98,8 +108,7 @@ import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
import com.zaneschepke.wireguardautotunnel.ui.theme.corn import com.zaneschepke.wireguardautotunnel.ui.theme.corn
import com.zaneschepke.wireguardautotunnel.ui.theme.mint import com.zaneschepke.wireguardautotunnel.ui.theme.mint
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.Event import com.zaneschepke.wireguardautotunnel.util.getMessage
import com.zaneschepke.wireguardautotunnel.util.Result
import com.zaneschepke.wireguardautotunnel.util.handshakeStatus import com.zaneschepke.wireguardautotunnel.util.handshakeStatus
import com.zaneschepke.wireguardautotunnel.util.mapPeerStats import com.zaneschepke.wireguardautotunnel.util.mapPeerStats
import com.zaneschepke.wireguardautotunnel.util.truncateWithEllipsis import com.zaneschepke.wireguardautotunnel.util.truncateWithEllipsis
@@ -123,6 +132,26 @@ fun MainScreen(
val sheetState = rememberModalBottomSheetState() val sheetState = rememberModalBottomSheetState()
var showBottomSheet by remember { mutableStateOf(false) } var showBottomSheet by remember { mutableStateOf(false) }
var configType by remember { mutableStateOf(ConfigType.WIREGUARD) }
// Nested scroll for control FAB
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
// Hide FAB
if (available.y < -1) {
isVisible.value = false
}
// Show FAB
if (available.y > 1) {
isVisible.value = true
}
return Offset.Zero
}
}
}
var showDeleteTunnelAlertDialog by remember { mutableStateOf(false) } var showDeleteTunnelAlertDialog by remember { mutableStateOf(false) }
var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) } var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) }
@@ -164,7 +193,7 @@ fun MainScreen(
name.startsWith(Constants.ANDROID_TV_EXPLORER_STUB) name.startsWith(Constants.ANDROID_TV_EXPLORER_STUB)
} }
) { ) {
appViewModel.showSnackbarMessage(Event.Error.FileExplorerRequired.message) appViewModel.showSnackbarMessage(context.getString(R.string.error_no_file_explorer))
} }
return intent return intent
} }
@@ -172,11 +201,8 @@ fun MainScreen(
) { data -> ) { data ->
if (data == null) return@rememberLauncherForActivityResult if (data == null) return@rememberLauncherForActivityResult
scope.launch { scope.launch {
viewModel.onTunnelFileSelected(data).let { viewModel.onTunnelFileSelected(data, configType, context).onFailure {
when (it) { appViewModel.showSnackbarMessage(it.getMessage(context))
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
is Result.Success -> {}
}
} }
} }
} }
@@ -186,11 +212,8 @@ fun MainScreen(
onResult = { onResult = {
if (it.contents != null) { if (it.contents != null) {
scope.launch { scope.launch {
viewModel.onTunnelQrResult(it.contents).let { result -> viewModel.onTunnelQrResult(it.contents, configType).onFailure {
when (result) { appViewModel.showSnackbarMessage(it.getMessage(context))
is Result.Success -> {}
is Result.Error -> appViewModel.showSnackbarMessage(result.error.message)
}
} }
} }
} }
@@ -203,7 +226,7 @@ fun MainScreen(
confirmButton = { confirmButton = {
TextButton( TextButton(
onClick = { onClick = {
selectedTunnel?.let { viewModel.onDelete(it) } selectedTunnel?.let { viewModel.onDelete(it, context) }
showDeleteTunnelAlertDialog = false showDeleteTunnelAlertDialog = false
selectedTunnel = null selectedTunnel = null
}, },
@@ -223,7 +246,7 @@ fun MainScreen(
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) { fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) {
if (appViewModel.isRequiredPermissionGranted()) { if (appViewModel.isRequiredPermissionGranted()) {
if (checked) viewModel.onTunnelStart(tunnel) else viewModel.onTunnelStop() if (checked) viewModel.onTunnelStart(tunnel, context) else viewModel.onTunnelStop(context)
} }
} }
@@ -231,14 +254,30 @@ fun MainScreen(
return LoadingScreen() return LoadingScreen()
} }
fun launchQrScanner() {
val scanOptions = ScanOptions()
scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE)
scanOptions.setOrientationLocked(true)
scanOptions.setPrompt(
context.getString(R.string.scanning_qr),
)
scanOptions.setBeepEnabled(false)
scanOptions.captureActivity =
CaptureActivityPortrait::class.java
scanLauncher.launch(scanOptions)
}
Scaffold( Scaffold(
modifier = modifier =
Modifier.pointerInput(Unit) { Modifier.pointerInput(Unit) {
detectTapGestures( if(uiState.tunnels.isNotEmpty()) {
onTap = { detectTapGestures(
selectedTunnel = null onTap = {
}, selectedTunnel = null
) },
)
}
}, },
floatingActionButtonPosition = FabPosition.End, floatingActionButtonPosition = FabPosition.End,
floatingActionButton = { floatingActionButton = {
@@ -250,7 +289,7 @@ fun MainScreen(
val secondaryColor = MaterialTheme.colorScheme.secondary val secondaryColor = MaterialTheme.colorScheme.secondary
val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp) val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
var fobColor by remember { mutableStateOf(secondaryColor) } var fobColor by remember { mutableStateOf(secondaryColor) }
FloatingActionButton( MultiFloatingActionButton(
modifier = modifier =
(if ( (if (
WireGuardAutoTunnel.isRunningOnAndroidTv() && WireGuardAutoTunnel.isRunningOnAndroidTv() &&
@@ -263,29 +302,45 @@ fun MainScreen(
fobColor = if (it.isFocused) hoverColor else secondaryColor fobColor = if (it.isFocused) hoverColor else secondaryColor
} }
}, },
onClick = { showBottomSheet = true }, fabIcon = FabIcon(
containerColor = fobColor, iconRes = R.drawable.add,
iconResAfterRotate = R.drawable.close,
iconRotate = 180f
),
fabOption = FabOption(
iconTint = MaterialTheme.colorScheme.background,
backgroundTint = MaterialTheme.colorScheme.primary,
),
itemsMultiFab = listOf(
MultiFabItem(
label = {
Text(
stringResource(id = R.string.amnezia),
color = Color.White,
textAlign = TextAlign.Center,
modifier = Modifier.padding(end = 10.dp)
)
},
icon = R.drawable.add,
value = ConfigType.AMNEZIA.name,
),
MultiFabItem(
label = {
Text(stringResource(id = R.string.wireguard), color = Color.White, textAlign = TextAlign.Center, modifier = Modifier.padding(end = 10.dp))
},
icon = R.drawable.add,
value = ConfigType.WIREGUARD.name
),
),
onFabItemClicked = {
showBottomSheet = true
configType = ConfigType.valueOf(it.value)
},
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
) { )
Icon(
imageVector = Icons.Rounded.Add,
contentDescription = stringResource(id = R.string.add_tunnel),
tint = Color.DarkGray,
)
}
} }
}, },
) { ) {
AnimatedVisibility(uiState.tunnels.isEmpty(), exit = fadeOut(), enter = fadeIn()) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier
.fillMaxSize(),
) {
Text(text = stringResource(R.string.no_tunnels), fontStyle = FontStyle.Italic)
}
}
if (showBottomSheet) { if (showBottomSheet) {
ModalBottomSheet( ModalBottomSheet(
onDismissRequest = { showBottomSheet = false }, onDismissRequest = { showBottomSheet = false },
@@ -321,16 +376,7 @@ fun MainScreen(
.clickable { .clickable {
scope.launch { scope.launch {
showBottomSheet = false showBottomSheet = false
val scanOptions = ScanOptions() launchQrScanner()
scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE)
scanOptions.setOrientationLocked(true)
scanOptions.setPrompt(
context.getString(R.string.scanning_qr),
)
scanOptions.setBeepEnabled(false)
scanOptions.captureActivity =
CaptureActivityPortrait::class.java
scanLauncher.launch(scanOptions)
} }
} }
.padding(10.dp), .padding(10.dp),
@@ -354,7 +400,7 @@ fun MainScreen(
.clickable { .clickable {
showBottomSheet = false showBottomSheet = false
navController.navigate( navController.navigate(
"${Screen.Config.route}/${Constants.MANUAL_TUNNEL_CONFIG_ID}", "${Screen.Config.route}/${Constants.MANUAL_TUNNEL_CONFIG_ID}?configType=${configType}",
) )
} }
.padding(10.dp), .padding(10.dp),
@@ -377,13 +423,47 @@ fun MainScreen(
verticalArrangement = Arrangement.Top, verticalArrangement = Arrangement.Top,
modifier = modifier =
Modifier Modifier
.fillMaxWidth() .fillMaxSize()
.overscroll(ScrollableDefaults.overscrollEffect()), .overscroll(ScrollableDefaults.overscrollEffect())
.nestedScroll(nestedScrollConnection),
state = rememberLazyListState(0, uiState.tunnels.count()), state = rememberLazyListState(0, uiState.tunnels.count()),
userScrollEnabled = true, userScrollEnabled = true,
reverseLayout = false, reverseLayout = false,
flingBehavior = ScrollableDefaults.flingBehavior(), flingBehavior = ScrollableDefaults.flingBehavior(),
) { ) {
item {
val gettingStarted = buildAnnotatedString {
append(stringResource(id = R.string.see_the))
append(" ")
pushStringAnnotation(tag = "gettingStarted", annotation = stringResource(id = R.string.getting_started_url))
withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.primary)) {
append(stringResource(id = R.string.getting_started_guide))
}
pop()
append(" ")
append(stringResource(R.string.unsure_how))
append(".")
}
AnimatedVisibility(
uiState.tunnels.isEmpty(), exit = fadeOut(), enter = fadeIn()) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier.padding(top = 100.dp)
) {
Text(text = stringResource(R.string.no_tunnels), fontStyle = FontStyle.Italic)
ClickableText(
modifier = Modifier.padding(vertical = 10.dp, horizontal = 24.dp),
text = gettingStarted,
style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center),
) {
gettingStarted.getStringAnnotations(tag = "gettingStarted", it, it).firstOrNull()?.let { annotation ->
appViewModel.openWebPage(annotation.item, context)
}
}
}
}
}
item { item {
if (uiState.settings.isAutoTunnelEnabled) { if (uiState.settings.isAutoTunnelEnabled) {
val autoTunnelingLabel = buildAnnotatedString { val autoTunnelingLabel = buildAnnotatedString {
@@ -439,7 +519,7 @@ fun MainScreen(
val leadingIconColor = val leadingIconColor =
(if ( (if (
uiState.vpnState.tunnelConfig?.name == tunnel.name && uiState.vpnState.tunnelConfig?.name == tunnel.name &&
uiState.vpnState.status == Tunnel.State.UP uiState.vpnState.status == TunnelState.UP
) { ) {
uiState.vpnState.statistics uiState.vpnState.statistics
?.mapPeerStats() ?.mapPeerStats()
@@ -485,10 +565,10 @@ fun MainScreen(
text = tunnel.name.truncateWithEllipsis(Constants.ALLOWED_DISPLAY_NAME_LENGTH), text = tunnel.name.truncateWithEllipsis(Constants.ALLOWED_DISPLAY_NAME_LENGTH),
onHold = { onHold = {
if ( if (
(uiState.vpnState.status == Tunnel.State.UP) && (uiState.vpnState.status == TunnelState.UP) &&
(tunnel.name == uiState.vpnState.tunnelConfig?.name) (tunnel.name == uiState.vpnState.tunnelConfig?.name)
) { ) {
appViewModel.showSnackbarMessage(Event.Message.TunnelOffAction.message) appViewModel.showSnackbarMessage(context.getString(R.string.turn_off_tunnel))
return@RowListItem return@RowListItem
} }
haptic.performHapticFeedback(HapticFeedbackType.LongPress) haptic.performHapticFeedback(HapticFeedbackType.LongPress)
@@ -497,7 +577,7 @@ fun MainScreen(
onClick = { onClick = {
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) { if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
if ( if (
uiState.vpnState.status == Tunnel.State.UP && uiState.vpnState.status == TunnelState.UP &&
(uiState.vpnState.tunnelConfig?.name == tunnel.name) (uiState.vpnState.tunnelConfig?.name == tunnel.name)
) { ) {
expanded.value = !expanded.value expanded.value = !expanded.value
@@ -522,7 +602,7 @@ fun MainScreen(
!uiState.settings.isAutoTunnelPaused !uiState.settings.isAutoTunnelPaused
) { ) {
appViewModel.showSnackbarMessage( appViewModel.showSnackbarMessage(
Event.Message.AutoTunnelOffAction.message, context.getString(R.string.turn_off_tunnel),
) )
} else { } else {
navController.navigate( navController.navigate(
@@ -555,7 +635,7 @@ fun MainScreen(
} else { } else {
val checked by remember { val checked by remember {
derivedStateOf { derivedStateOf {
(uiState.vpnState.status == Tunnel.State.UP && (uiState.vpnState.status == TunnelState.UP &&
tunnel.name == uiState.vpnState.tunnelConfig?.name) tunnel.name == uiState.vpnState.tunnelConfig?.name)
} }
} }
@@ -577,7 +657,7 @@ fun MainScreen(
onClick = { onClick = {
if (uiState.settings.isAutoTunnelEnabled) { if (uiState.settings.isAutoTunnelEnabled) {
appViewModel.showSnackbarMessage( appViewModel.showSnackbarMessage(
Event.Message.AutoTunnelOffAction.message, context.getString(R.string.turn_off_auto),
) )
} else { } else {
selectedTunnel = tunnel selectedTunnel = tunnel
@@ -597,13 +677,13 @@ fun MainScreen(
modifier = Modifier.focusRequester(focusRequester), modifier = Modifier.focusRequester(focusRequester),
onClick = { onClick = {
if ( if (
uiState.vpnState.status == Tunnel.State.UP && uiState.vpnState.status == TunnelState.UP &&
(uiState.vpnState.tunnelConfig?.name == tunnel.name) (uiState.vpnState.tunnelConfig?.name == tunnel.name)
) { ) {
expanded.value = !expanded.value expanded.value = !expanded.value
} else { } else {
appViewModel.showSnackbarMessage( appViewModel.showSnackbarMessage(
Event.Message.TunnelOnAction.message, context.getString(R.string.turn_on_tunnel),
) )
} }
}, },
@@ -620,11 +700,11 @@ fun MainScreen(
IconButton( IconButton(
onClick = { onClick = {
if ( if (
uiState.vpnState.status == Tunnel.State.UP && uiState.vpnState.status == TunnelState.UP &&
tunnel.name == uiState.vpnState.tunnelConfig?.name tunnel.name == uiState.vpnState.tunnelConfig?.name
) { ) {
appViewModel.showSnackbarMessage( appViewModel.showSnackbarMessage(
Event.Message.TunnelOffAction.message, context.getString(R.string.turn_off_tunnel),
) )
} else { } else {
selectedTunnel = tunnel selectedTunnel = tunnel
@@ -1,6 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main package com.zaneschepke.wireguardautotunnel.ui.screens.main
import com.zaneschepke.wireguardautotunnel.data.model.Settings import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
@@ -1,6 +1,5 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main package com.zaneschepke.wireguardautotunnel.ui.screens.main
import android.app.Application
import android.content.Context import android.content.Context
import android.database.Cursor import android.database.Cursor
import android.net.Uri import android.net.Uri
@@ -9,15 +8,15 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.wireguard.config.Config import com.wireguard.config.Config
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.model.Settings import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.Event
import com.zaneschepke.wireguardautotunnel.util.NumberUtils import com.zaneschepke.wireguardautotunnel.util.NumberUtils
import com.zaneschepke.wireguardautotunnel.util.Result import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions
import com.zaneschepke.wireguardautotunnel.util.toWgQuickString
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
@@ -34,7 +33,6 @@ import javax.inject.Inject
class MainViewModel class MainViewModel
@Inject @Inject
constructor( constructor(
private val application: Application,
private val appDataRepository: AppDataRepository, private val appDataRepository: AppDataRepository,
private val serviceManager: ServiceManager, private val serviceManager: ServiceManager,
val vpnService: VpnService val vpnService: VpnService
@@ -54,21 +52,21 @@ constructor(
MainUiState(), MainUiState(),
) )
private fun stopWatcherService() = private fun stopWatcherService(context: Context) =
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
serviceManager.stopWatcherService(application.applicationContext) serviceManager.stopWatcherService(context)
} }
fun onDelete(tunnel: TunnelConfig) { fun onDelete(tunnel: TunnelConfig, context: Context) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val settings = appDataRepository.settings.getSettings() val settings = appDataRepository.settings.getSettings()
val isPrimary = tunnel.isPrimaryTunnel val isPrimary = tunnel.isPrimaryTunnel
if (appDataRepository.tunnels.count() == 1 || isPrimary) { if (appDataRepository.tunnels.count() == 1 || isPrimary) {
stopWatcherService() stopWatcherService(context)
resetTunnelSetting(settings) resetTunnelSetting(settings)
} }
appDataRepository.tunnels.delete(tunnel) appDataRepository.tunnels.delete(tunnel)
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(application) WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
} }
} }
@@ -81,80 +79,133 @@ constructor(
) )
} }
fun onTunnelStart(tunnelConfig: TunnelConfig) = fun onTunnelStart(tunnelConfig: TunnelConfig, context: Context) =
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
Timber.d("On start called!") Timber.d("On start called!")
serviceManager.startVpnService( serviceManager.startVpnService(
application.applicationContext, context,
tunnelConfig.id, tunnelConfig.id,
isManualStart = true, isManualStart = true,
) )
} }
fun onTunnelStop() = fun onTunnelStop(context: Context) =
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
Timber.i("Stopping active tunnel") Timber.i("Stopping active tunnel")
serviceManager.stopVpnService(application.applicationContext, isManualStop = true) serviceManager.stopVpnService(context, isManualStop = true)
} }
private fun validateConfigString(config: String) { private fun validateConfigString(config: String, configType: ConfigType) {
TunnelConfig.configFromQuick(config) when(configType) {
ConfigType.AMNEZIA -> TunnelConfig.configFromAmQuick(config)
ConfigType.WIREGUARD -> TunnelConfig.configFromWgQuick(config)
}
} }
suspend fun onTunnelQrResult(result: String): Result<Unit> { private fun generateQrCodeDefaultName(config : String, configType: ConfigType) : String {
return try { return try {
validateConfigString(result) when(configType) {
val tunnelConfig = ConfigType.AMNEZIA -> {
TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = result) TunnelConfig.configFromAmQuick(config).peers[0].endpoint.get().host
}
ConfigType.WIREGUARD -> {
TunnelConfig.configFromWgQuick(config).peers[0].endpoint.get().host
}
}
} catch (e : Exception) {
Timber.e(e)
NumberUtils.generateRandomTunnelName()
}
}
private fun generateQrCodeTunnelName(config : String, configType: ConfigType) : String {
var defaultName = generateQrCodeDefaultName(config, configType)
val lines = config.lines().toMutableList()
val linesIterator = lines.iterator()
while(linesIterator.hasNext()) {
val next = linesIterator.next()
if(next.contains(Constants.QR_CODE_NAME_PROPERTY)) {
defaultName = next.substringAfter(Constants.QR_CODE_NAME_PROPERTY).trim()
break
}
}
return defaultName
}
suspend fun onTunnelQrResult(result: String, configType: ConfigType): Result<Unit> {
return try {
validateConfigString(result, configType)
val tunnelName = makeTunnelNameUnique(generateQrCodeTunnelName(result, configType))
val tunnelConfig = when(configType) {
ConfigType.AMNEZIA ->{
TunnelConfig(name = tunnelName, amQuick = result,
wgQuick = TunnelConfig.configFromAmQuick(result).toWgQuickString())
}
ConfigType.WIREGUARD -> TunnelConfig(name = tunnelName, wgQuick = result)
}
addTunnel(tunnelConfig) addTunnel(tunnelConfig)
Result.Success(Unit) Result.success(Unit)
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e) Timber.e(e)
Result.Error(Event.Error.InvalidQrCode) Result.failure(WgTunnelExceptions.InvalidQrCode())
} }
} }
private suspend fun saveTunnelConfigFromStream(stream: InputStream, fileName: String) { private suspend fun makeTunnelNameUnique(name : String) : String {
val bufferReader = stream.bufferedReader(charset = Charsets.UTF_8) val tunnels = appDataRepository.tunnels.getAll()
val config = Config.parse(bufferReader) var tunnelName = name
val tunnelName = getNameFromFileName(fileName) var num = 1
addTunnel(TunnelConfig(name = tunnelName, wgQuick = config.toWgQuickString())) while (tunnels.any { it.name == tunnelName }) {
withContext(Dispatchers.IO) { stream.close() } tunnelName = name + "(${num})"
num++
}
return tunnelName
} }
private fun getInputStreamFromUri(uri: Uri): InputStream? { private suspend fun saveTunnelConfigFromStream(stream: InputStream, fileName: String, type: ConfigType) {
return application.applicationContext.contentResolver.openInputStream(uri) var amQuick : String? = null
} val wgQuick = stream.use {
when(type) {
suspend fun onTunnelFileSelected(uri: Uri): Result<Unit> { ConfigType.AMNEZIA -> {
try { val config = org.amnezia.awg.config.Config.parse(it)
if (isValidUriContentScheme(uri)) { amQuick = config.toAwgQuickString()
val fileName = getFileName(application.applicationContext, uri) config.toWgQuickString()
when (getFileExtensionFromFileName(fileName)) { }
Constants.CONF_FILE_EXTENSION -> ConfigType.WIREGUARD -> {
saveTunnelFromConfUri(fileName, uri).let { Config.parse(it).toWgQuickString()
when (it) { }
is Result.Error -> return Result.Error(Event.Error.FileReadFailed) }
is Result.Success -> return it }
} val tunnelName = makeTunnelNameUnique(getNameFromFileName(fileName))
} addTunnel(TunnelConfig(name = tunnelName, wgQuick = wgQuick, amQuick = amQuick ?: TunnelConfig.AM_QUICK_DEFAULT))
}
Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri)
else -> return Result.Error(Event.Error.InvalidFileExtension) private fun getInputStreamFromUri(uri: Uri, context: Context): InputStream? {
return context.applicationContext.contentResolver.openInputStream(uri)
}
suspend fun onTunnelFileSelected(uri: Uri, configType: ConfigType, context: Context): Result<Unit> {
return try {
if (isValidUriContentScheme(uri)) {
val fileName = getFileName(context, uri)
return when (getFileExtensionFromFileName(fileName)) {
Constants.CONF_FILE_EXTENSION ->
saveTunnelFromConfUri(fileName, uri, configType, context)
Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri, configType, context)
else -> Result.failure(WgTunnelExceptions.InvalidFileExtension())
} }
return Result.Success(Unit)
} else { } else {
return Result.Error(Event.Error.InvalidFileExtension) Result.failure(WgTunnelExceptions.InvalidFileExtension())
} }
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e) Timber.e(e)
return Result.Error(Event.Error.FileReadFailed) Result.failure(WgTunnelExceptions.FileReadFailed())
} }
} }
private suspend fun saveTunnelsFromZipUri(uri: Uri) { private suspend fun saveTunnelsFromZipUri(uri: Uri, configType: ConfigType, context: Context) : Result<Unit> {
ZipInputStream(getInputStreamFromUri(uri)).use { zip -> return ZipInputStream(getInputStreamFromUri(uri, context)).use { zip ->
generateSequence { zip.nextEntry } generateSequence { zip.nextEntry }
.filterNot { .filterNot {
it.isDirectory || it.isDirectory ||
@@ -162,40 +213,57 @@ constructor(
} }
.forEach { .forEach {
val name = getNameFromFileName(it.name) val name = getNameFromFileName(it.name)
val config = Config.parse(zip) withContext(viewModelScope.coroutineContext + Dispatchers.IO) {
viewModelScope.launch(Dispatchers.IO) { try {
addTunnel(TunnelConfig(name = name, wgQuick = config.toWgQuickString())) var amQuick : String? = null
val wgQuick =
when(configType) {
ConfigType.AMNEZIA -> {
val config = org.amnezia.awg.config.Config.parse(zip)
amQuick = config.toAwgQuickString()
config.toWgQuickString()
}
ConfigType.WIREGUARD -> {
Config.parse(zip).toWgQuickString()
}
}
addTunnel(TunnelConfig(name = makeTunnelNameUnique(name), wgQuick = wgQuick, amQuick = amQuick ?: TunnelConfig.AM_QUICK_DEFAULT))
Result.success(Unit)
} catch (e : Exception) {
Result.failure(WgTunnelExceptions.FileReadFailed())
}
} }
} }
Result.success(Unit)
} }
} }
private suspend fun saveTunnelFromConfUri(name: String, uri: Uri): Result<Unit> { private suspend fun saveTunnelFromConfUri(name: String, uri: Uri, configType: ConfigType, context: Context): Result<Unit> {
val stream = getInputStreamFromUri(uri) val stream = getInputStreamFromUri(uri, context)
return if (stream != null) { return if (stream != null) {
saveTunnelConfigFromStream(stream, name) saveTunnelConfigFromStream(stream, name, configType)
Result.Success(Unit) Result.success(Unit)
} else { } else {
Result.Error(Event.Error.FileReadFailed) Result.failure(WgTunnelExceptions.FileReadFailed())
} }
} }
private suspend fun addTunnel(tunnelConfig: TunnelConfig) { private suspend fun addTunnel(tunnelConfig: TunnelConfig) {
val firstTunnel = appDataRepository.tunnels.count() == 0 val firstTunnel = appDataRepository.tunnels.count() == 0
saveTunnel(tunnelConfig) saveTunnel(tunnelConfig)
if (firstTunnel) WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(application) if (firstTunnel) WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
} }
fun pauseAutoTunneling() = fun pauseAutoTunneling() =
viewModelScope.launch { viewModelScope.launch {
appDataRepository.settings.save(uiState.value.settings.copy(isAutoTunnelPaused = true)) appDataRepository.settings.save(uiState.value.settings.copy(isAutoTunnelPaused = true))
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate(application) WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate()
} }
fun resumeAutoTunneling() = fun resumeAutoTunneling() =
viewModelScope.launch { viewModelScope.launch {
appDataRepository.settings.save(uiState.value.settings.copy(isAutoTunnelPaused = false)) appDataRepository.settings.save(uiState.value.settings.copy(isAutoTunnelPaused = false))
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate(application) WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate()
} }
private suspend fun saveTunnel(tunnelConfig: TunnelConfig) { private suspend fun saveTunnel(tunnelConfig: TunnelConfig) {
@@ -239,12 +307,12 @@ constructor(
return fileName.substring(0, fileName.lastIndexOf('.')) return fileName.substring(0, fileName.lastIndexOf('.'))
} }
private fun getFileExtensionFromFileName(fileName: String): String { private fun getFileExtensionFromFileName(fileName: String): String? {
return try { return try {
fileName.substring(fileName.lastIndexOf('.')) fileName.substring(fileName.lastIndexOf('.'))
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e) Timber.e(e)
"" null
} }
} }
@@ -1,5 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.options package com.zaneschepke.wireguardautotunnel.ui.screens.options
import android.annotation.SuppressLint
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -7,7 +8,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
@@ -24,9 +24,10 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -38,16 +39,23 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController import androidx.navigation.NavController
import com.iamageo.multifablibrary.FabIcon
import com.iamageo.multifablibrary.FabOption
import com.iamageo.multifablibrary.MultiFabItem
import com.iamageo.multifablibrary.MultiFloatingActionButton
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
@@ -55,11 +63,13 @@ import com.zaneschepke.wireguardautotunnel.ui.Screen
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.Result import com.zaneschepke.wireguardautotunnel.util.getMessage
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@OptIn(ExperimentalLayoutApi::class) @OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
fun OptionsScreen( fun OptionsScreen(
@@ -71,6 +81,8 @@ fun OptionsScreen(
) { ) {
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
val uiState by optionsViewModel.uiState.collectAsStateWithLifecycle() val uiState by optionsViewModel.uiState.collectAsStateWithLifecycle()
val context = LocalContext.current
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
@@ -90,196 +102,236 @@ fun OptionsScreen(
fun saveTrustedSSID() { fun saveTrustedSSID() {
if (currentText.isNotEmpty()) { if (currentText.isNotEmpty()) {
scope.launch { scope.launch {
optionsViewModel.onSaveRunSSID(currentText).let { optionsViewModel.onSaveRunSSID(currentText).onSuccess {
when (it) { currentText = ""
is Result.Success -> currentText = "" }.onFailure {
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message) appViewModel.showSnackbarMessage(it.getMessage(context))
}
} }
} }
} }
} }
Column( Scaffold(
horizontalAlignment = Alignment.CenterHorizontally, floatingActionButton = {
verticalArrangement = Arrangement.Top, val secondaryColor = MaterialTheme.colorScheme.secondary
modifier = val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
Modifier var fobColor by remember { mutableStateOf(secondaryColor) }
.fillMaxSize() MultiFloatingActionButton(
.verticalScroll(scrollState) modifier =
.clickable( (if (
indication = null, WireGuardAutoTunnel.isRunningOnAndroidTv()
interactionSource = interactionSource,
) {
focusManager.clearFocus()
},
) {
Surface(
tonalElevation = 2.dp,
shadowElevation = 2.dp,
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.surface,
modifier =
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
Modifier
.height(IntrinsicSize.Min)
.fillMaxWidth(fillMaxWidth)
.padding(top = 10.dp)
} else {
Modifier
.fillMaxWidth(fillMaxWidth)
.padding(top = 20.dp)
})
.padding(bottom = 10.dp),
) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top,
modifier = Modifier.padding(15.dp),
) {
SectionTitle(
title = stringResource(id = R.string.general),
padding = screenPadding,
) )
ConfigurationToggle( Modifier.focusRequester(focusRequester)
stringResource(R.string.set_primary_tunnel), else Modifier)
enabled = true, .onFocusChanged {
checked = uiState.isDefaultTunnel, if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
modifier = Modifier fobColor = if (it.isFocused) hoverColor else secondaryColor
.focusRequester(focusRequester), }
padding = screenPadding, },
onCheckChanged = { optionsViewModel.onTogglePrimaryTunnel() }, fabIcon = FabIcon(
) iconRes = R.drawable.edit,
Row( iconResAfterRotate = R.drawable.close,
verticalAlignment = Alignment.CenterVertically, iconRotate = 180f
modifier = Modifier ),
.fillMaxSize() fabOption = FabOption(
.padding(top = 5.dp), iconTint = MaterialTheme.colorScheme.background,
horizontalArrangement = Arrangement.Center, backgroundTint = MaterialTheme.colorScheme.primary,
) { ),
TextButton( itemsMultiFab = listOf(
onClick = { MultiFabItem(
navController.navigate( label = {
"${Screen.Config.route}/${tunnelId}", Text(
stringResource(id = R.string.amnezia),
color = Color.White,
textAlign = TextAlign.Center,
modifier = Modifier.padding(end = 10.dp)
) )
}, },
) { icon = R.drawable.edit,
Text(stringResource(R.string.edit_tunnel)) value = ConfigType.AMNEZIA.name,
} ),
MultiFabItem(
label = {
Text(stringResource(id = R.string.wireguard), color = Color.White, textAlign = TextAlign.Center, modifier = Modifier.padding(end = 10.dp))
},
icon = R.drawable.edit,
value = ConfigType.WIREGUARD.name
),
),
onFabItemClicked = {
val configType = ConfigType.valueOf(it.value)
navController.navigate(
"${Screen.Config.route}/${tunnelId}?configType=${configType.name}",
)
},
shape = RoundedCornerShape(16.dp),
)
}
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top,
modifier =
Modifier
.fillMaxSize()
.verticalScroll(scrollState)
.clickable(
indication = null,
interactionSource = interactionSource,
) {
focusManager.clearFocus()
},
) {
Surface(
tonalElevation = 2.dp,
shadowElevation = 2.dp,
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.surface,
modifier =
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
Modifier
.height(IntrinsicSize.Min)
.fillMaxWidth(fillMaxWidth)
.padding(top = 10.dp)
} else {
Modifier
.fillMaxWidth(fillMaxWidth)
.padding(top = 20.dp)
})
.padding(bottom = 10.dp),
) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top,
modifier = Modifier.padding(15.dp),
) {
SectionTitle(
title = stringResource(id = R.string.general),
padding = screenPadding,
)
ConfigurationToggle(
stringResource(R.string.set_primary_tunnel),
enabled = true,
checked = uiState.isDefaultTunnel,
modifier = Modifier
.focusRequester(focusRequester),
padding = screenPadding,
onCheckChanged = { optionsViewModel.onTogglePrimaryTunnel() },
)
} }
} }
} Surface(
Surface( tonalElevation = 2.dp,
tonalElevation = 2.dp, shadowElevation = 2.dp,
shadowElevation = 2.dp, shape = RoundedCornerShape(12.dp),
shape = RoundedCornerShape(12.dp), color = MaterialTheme.colorScheme.surface,
color = MaterialTheme.colorScheme.surface, modifier =
modifier = (if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { Modifier
Modifier .height(IntrinsicSize.Min)
.height(IntrinsicSize.Min) .fillMaxWidth(fillMaxWidth)
.fillMaxWidth(fillMaxWidth) .padding(top = 10.dp)
.padding(top = 10.dp) } else {
} else { Modifier
Modifier .fillMaxWidth(fillMaxWidth)
.fillMaxWidth(fillMaxWidth) .padding(top = 20.dp)
.padding(top = 20.dp) })
}) .padding(bottom = 10.dp),
.padding(bottom = 10.dp),
) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top,
modifier = Modifier.padding(15.dp),
) { ) {
SectionTitle( Column(
title = stringResource(id = R.string.auto_tunneling), horizontalAlignment = Alignment.Start,
padding = screenPadding, verticalArrangement = Arrangement.Top,
) modifier = Modifier.padding(15.dp),
ConfigurationToggle( ) {
stringResource(R.string.mobile_data_tunnel), SectionTitle(
enabled = true, title = stringResource(id = R.string.auto_tunneling),
checked = uiState.tunnel?.isMobileDataTunnel == true, padding = screenPadding,
padding = screenPadding,
onCheckChanged = { optionsViewModel.onToggleIsMobileDataTunnel() },
)
Column {
FlowRow(
modifier = Modifier
.padding(screenPadding)
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(5.dp),
) {
uiState.tunnel?.tunnelNetworks?.forEach { ssid ->
ClickableIconButton(
onClick = {
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
focusRequester.requestFocus()
optionsViewModel.onDeleteRunSSID(ssid)
}
},
onIconClick = {
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) focusRequester.requestFocus()
optionsViewModel.onDeleteRunSSID(ssid)
},
text = ssid,
icon = Icons.Filled.Close,
enabled = true,
)
}
if (uiState.tunnel == null || uiState.tunnel?.tunnelNetworks?.isEmpty() == true) {
Text(
stringResource(R.string.no_wifi_names_configured),
fontStyle = FontStyle.Italic,
color = Color.Gray,
)
}
}
OutlinedTextField(
enabled = true,
value = currentText,
onValueChange = { currentText = it },
label = { Text(stringResource(id = R.string.use_tunnel_on_wifi_name)) },
modifier =
Modifier
.padding(
start = screenPadding,
top = 5.dp,
bottom = 10.dp,
),
maxLines = 1,
keyboardOptions =
KeyboardOptions(
capitalization = KeyboardCapitalization.None,
imeAction = ImeAction.Done,
),
keyboardActions = KeyboardActions(onDone = { saveTrustedSSID() }),
trailingIcon = {
if (currentText != "") {
IconButton(onClick = { saveTrustedSSID() }) {
Icon(
imageVector = Icons.Outlined.Add,
contentDescription =
if (currentText == "") {
stringResource(
id =
R.string
.trusted_ssid_empty_description,
)
} else {
stringResource(
id =
R.string
.trusted_ssid_value_description,
)
},
tint = MaterialTheme.colorScheme.primary,
)
}
}
},
) )
ConfigurationToggle(
stringResource(R.string.mobile_data_tunnel),
enabled = true,
checked = uiState.tunnel?.isMobileDataTunnel == true,
padding = screenPadding,
onCheckChanged = { optionsViewModel.onToggleIsMobileDataTunnel() },
)
Column {
FlowRow(
modifier = Modifier
.padding(screenPadding)
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(5.dp),
) {
uiState.tunnel?.tunnelNetworks?.forEach { ssid ->
ClickableIconButton(
onClick = {
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
focusRequester.requestFocus()
optionsViewModel.onDeleteRunSSID(ssid)
}
},
onIconClick = {
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) focusRequester.requestFocus()
optionsViewModel.onDeleteRunSSID(ssid)
},
text = ssid,
icon = Icons.Filled.Close,
enabled = true,
)
}
if (uiState.tunnel == null || uiState.tunnel?.tunnelNetworks?.isEmpty() == true) {
Text(
stringResource(R.string.no_wifi_names_configured),
fontStyle = FontStyle.Italic,
color = Color.Gray,
)
}
}
OutlinedTextField(
enabled = true,
value = currentText,
onValueChange = { currentText = it },
label = { Text(stringResource(id = R.string.use_tunnel_on_wifi_name)) },
modifier =
Modifier
.padding(
start = screenPadding,
top = 5.dp,
bottom = 10.dp,
),
maxLines = 1,
keyboardOptions =
KeyboardOptions(
capitalization = KeyboardCapitalization.None,
imeAction = ImeAction.Done,
),
keyboardActions = KeyboardActions(onDone = { saveTrustedSSID() }),
trailingIcon = {
if (currentText != "") {
IconButton(onClick = { saveTrustedSSID() }) {
Icon(
imageVector = Icons.Outlined.Add,
contentDescription =
if (currentText == "") {
stringResource(
id =
R.string
.trusted_ssid_empty_description,
)
} else {
stringResource(
id =
R.string
.trusted_ssid_value_description,
)
},
tint = MaterialTheme.colorScheme.primary,
)
}
}
},
)
}
} }
} }
} }
@@ -1,6 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.options package com.zaneschepke.wireguardautotunnel.ui.screens.options
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
data class OptionsUiState( data class OptionsUiState(
val id: String? = null, val id: String? = null,
@@ -4,17 +4,17 @@ import androidx.compose.ui.util.fastFirstOrNull
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.Event import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions
import com.zaneschepke.wireguardautotunnel.util.Result
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import javax.inject.Inject import javax.inject.Inject
@@ -43,9 +43,11 @@ constructor(
) )
fun init(tunnelId: String) { fun init(tunnelId: String) {
_optionState.value = _optionState.value.copy( _optionState.update {
id = tunnelId, it.copy(
) id = tunnelId
)
}
} }
fun onDeleteRunSSID(ssid: String) = viewModelScope.launch(Dispatchers.IO) { fun onDeleteRunSSID(ssid: String) = viewModelScope.launch(Dispatchers.IO) {
@@ -73,9 +75,9 @@ constructor(
tunnelsWithName.isEmpty()) { tunnelsWithName.isEmpty()) {
uiState.value.tunnel?.tunnelNetworks?.add(trimmed) uiState.value.tunnel?.tunnelNetworks?.add(trimmed)
saveTunnel(uiState.value.tunnel) saveTunnel(uiState.value.tunnel)
Result.Success(Unit) Result.success(Unit)
} else { } else {
Result.Error(Event.Error.SsidConflict) Result.failure(WgTunnelExceptions.SsidConflict())
} }
} }
@@ -95,7 +97,7 @@ constructor(
false -> uiState.value.tunnel false -> uiState.value.tunnel
}, },
) )
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(WireGuardAutoTunnel.instance) WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
} }
} }
} }
@@ -70,19 +70,19 @@ import androidx.navigation.NavController
import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState import com.google.accompanist.permissions.rememberPermissionState
import com.wireguard.android.backend.Tunnel
import com.wireguard.android.backend.WgQuickBackend import com.wireguard.android.backend.WgQuickBackend
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.Screen import com.zaneschepke.wireguardautotunnel.ui.Screen
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
import com.zaneschepke.wireguardautotunnel.util.Event
import com.zaneschepke.wireguardautotunnel.util.FileUtils import com.zaneschepke.wireguardautotunnel.util.FileUtils
import com.zaneschepke.wireguardautotunnel.util.Result import com.zaneschepke.wireguardautotunnel.util.getMessage
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
@@ -130,16 +130,28 @@ fun SettingsScreen(
fun exportAllConfigs() { fun exportAllConfigs() {
try { try {
val files = uiState.tunnels.map { File(context.cacheDir, "${it.name}.conf") } val wgFiles = uiState.tunnels.map { config ->
files.forEachIndexed { index, file -> val file = File(context.cacheDir, "${config.name}-wg.conf")
file.outputStream().use { it.write(uiState.tunnels[index].wgQuick.toByteArray()) } file.outputStream().use {
it.write(config.wgQuick.toByteArray())
}
file
}
val amFiles = uiState.tunnels.mapNotNull { config -> if(config.amQuick != TunnelConfig.AM_QUICK_DEFAULT) {
val file = File(context.cacheDir, "${config.name}-am.conf")
file.outputStream().use {
it.write(config.amQuick.toByteArray())
}
file
} else null }
FileUtils.saveFilesToZip(context, wgFiles + amFiles).onFailure {
appViewModel.showSnackbarMessage(it.getMessage(context))
}.onSuccess {
didExportFiles = true
appViewModel.showSnackbarMessage(context.getString(R.string.exported_configs_message))
} }
FileUtils.saveFilesToZip(context, files)
didExportFiles = true
appViewModel.showSnackbarMessage(Event.Message.ConfigsExported.message)
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e) Timber.e(e)
appViewModel.showSnackbarMessage(Event.Error.Exception(e).message)
} }
} }
@@ -160,7 +172,7 @@ fun SettingsScreen(
fun handleAutoTunnelToggle() { fun handleAutoTunnelToggle() {
if (uiState.isBatteryOptimizeDisableShown || isBatteryOptimizationsDisabled()) { if (uiState.isBatteryOptimizeDisableShown || isBatteryOptimizationsDisabled()) {
if (appViewModel.isRequiredPermissionGranted()) { if (appViewModel.isRequiredPermissionGranted()) {
viewModel.onToggleAutoTunnel() viewModel.onToggleAutoTunnel(context)
} }
} else { } else {
requestBatteryOptimizationsDisabled() requestBatteryOptimizationsDisabled()
@@ -169,11 +181,10 @@ fun SettingsScreen(
fun saveTrustedSSID() { fun saveTrustedSSID() {
if (currentText.isNotEmpty()) { if (currentText.isNotEmpty()) {
viewModel.onSaveTrustedSSID(currentText).let { viewModel.onSaveTrustedSSID(currentText).onSuccess {
when (it) { currentText = ""
is Result.Success -> currentText = "" }.onFailure {
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message) appViewModel.showSnackbarMessage(it.getMessage(context))
}
} }
} }
} }
@@ -307,11 +318,11 @@ fun SettingsScreen(
}, },
onError = { _ -> onError = { _ ->
showAuthPrompt = false showAuthPrompt = false
appViewModel.showSnackbarMessage(Event.Error.AuthenticationFailed.message) appViewModel.showSnackbarMessage(context.getString(R.string.error_authentication_failed))
}, },
onFailure = { onFailure = {
showAuthPrompt = false showAuthPrompt = false
appViewModel.showSnackbarMessage(Event.Error.AuthorizationFailed.message) appViewModel.showSnackbarMessage(context.getString(R.string.error_authorization_failed))
}, },
) )
} }
@@ -519,12 +530,12 @@ fun SettingsScreen(
when (false) { when (false) {
isBackgroundLocationGranted -> isBackgroundLocationGranted ->
appViewModel.showSnackbarMessage( appViewModel.showSnackbarMessage(
Event.Error.BackgroundLocationRequired.message, context.getString(R.string.background_location_required),
) )
fineLocationState.status.isGranted -> fineLocationState.status.isGranted ->
appViewModel.showSnackbarMessage( appViewModel.showSnackbarMessage(
Event.Error.PreciseLocationRequired.message, context.getString(R.string.precise_location_required),
) )
viewModel.isLocationEnabled(context) -> viewModel.isLocationEnabled(context) ->
@@ -550,39 +561,48 @@ fun SettingsScreen(
} }
} }
} }
if (WgQuickBackend.hasKernelSupport()) { Surface(
Surface( tonalElevation = 2.dp,
tonalElevation = 2.dp, shadowElevation = 2.dp,
shadowElevation = 2.dp, shape = RoundedCornerShape(12.dp),
shape = RoundedCornerShape(12.dp), color = MaterialTheme.colorScheme.surface,
color = MaterialTheme.colorScheme.surface, modifier = Modifier
modifier = Modifier .fillMaxWidth(fillMaxWidth)
.fillMaxWidth(fillMaxWidth) .padding(vertical = 10.dp),
.padding(vertical = 10.dp), ) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top,
modifier = Modifier.padding(15.dp),
) { ) {
Column( SectionTitle(
horizontalAlignment = Alignment.Start, title = stringResource(id = R.string.backend),
verticalArrangement = Arrangement.Top, padding = screenPadding,
modifier = Modifier.padding(15.dp), )
) { ConfigurationToggle(
SectionTitle( stringResource(R.string.use_amnezia),
title = stringResource(id = R.string.kernel), enabled =
padding = screenPadding, !(uiState.settings.isAutoTunnelEnabled ||
) uiState.settings.isAlwaysOnVpnEnabled ||
(uiState.vpnState.status == TunnelState.UP) || uiState.settings.isKernelEnabled),
checked = uiState.settings.isAmneziaEnabled,
padding = screenPadding,
onCheckChanged = {
viewModel.onToggleAmnezia()
},
)
if (WgQuickBackend.hasKernelSupport()) {
ConfigurationToggle( ConfigurationToggle(
stringResource(R.string.use_kernel), stringResource(R.string.use_kernel),
enabled = enabled =
!(uiState.settings.isAutoTunnelEnabled || !(uiState.settings.isAutoTunnelEnabled ||
uiState.settings.isAlwaysOnVpnEnabled || uiState.settings.isAlwaysOnVpnEnabled ||
(uiState.vpnState.status == Tunnel.State.UP)), (uiState.vpnState.status == TunnelState.UP)),
checked = uiState.settings.isKernelEnabled, checked = uiState.settings.isKernelEnabled,
padding = screenPadding, padding = screenPadding,
onCheckChanged = { onCheckChanged = {
viewModel.onToggleKernelMode().let { viewModel.onToggleKernelMode().onFailure {
when (it) { appViewModel.showSnackbarMessage(it.getMessage(context))
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
is Result.Success -> {}
}
} }
}, },
) )
@@ -1,7 +1,7 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings package com.zaneschepke.wireguardautotunnel.ui.screens.settings
import com.zaneschepke.wireguardautotunnel.data.model.Settings import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
data class SettingsUiState( data class SettingsUiState(
@@ -1,6 +1,5 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings package com.zaneschepke.wireguardautotunnel.ui.screens.settings
import android.app.Application
import android.content.Context import android.content.Context
import android.location.LocationManager import android.location.LocationManager
import androidx.core.location.LocationManagerCompat import androidx.core.location.LocationManagerCompat
@@ -8,13 +7,12 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.wireguard.android.util.RootShell import com.wireguard.android.util.RootShell
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.model.Settings import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.Event import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions
import com.zaneschepke.wireguardautotunnel.util.Result
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
@@ -27,7 +25,6 @@ import javax.inject.Inject
class SettingsViewModel class SettingsViewModel
@Inject @Inject
constructor( constructor(
private val application: Application,
private val appDataRepository: AppDataRepository, private val appDataRepository: AppDataRepository,
private val serviceManager: ServiceManager, private val serviceManager: ServiceManager,
private val rootShell: RootShell, private val rootShell: RootShell,
@@ -60,9 +57,9 @@ constructor(
return if (!uiState.value.settings.trustedNetworkSSIDs.contains(trimmed)) { return if (!uiState.value.settings.trustedNetworkSSIDs.contains(trimmed)) {
uiState.value.settings.trustedNetworkSSIDs.add(trimmed) uiState.value.settings.trustedNetworkSSIDs.add(trimmed)
saveSettings(uiState.value.settings) saveSettings(uiState.value.settings)
Result.Success(Unit) Result.success(Unit)
} else { } else {
Result.Error(Event.Error.SsidConflict) Result.failure(WgTunnelExceptions.SsidConflict())
} }
} }
@@ -93,15 +90,15 @@ constructor(
) )
} }
fun onToggleAutoTunnel() = fun onToggleAutoTunnel(context: Context) =
viewModelScope.launch { viewModelScope.launch {
val isAutoTunnelEnabled = uiState.value.settings.isAutoTunnelEnabled val isAutoTunnelEnabled = uiState.value.settings.isAutoTunnelEnabled
var isAutoTunnelPaused = uiState.value.settings.isAutoTunnelPaused var isAutoTunnelPaused = uiState.value.settings.isAutoTunnelPaused
if (isAutoTunnelEnabled) { if (isAutoTunnelEnabled) {
serviceManager.stopWatcherService(application) serviceManager.stopWatcherService(context)
} else { } else {
serviceManager.startWatcherService(application) serviceManager.startWatcherService(context)
isAutoTunnelPaused = false isAutoTunnelPaused = false
} }
saveSettings( saveSettings(
@@ -110,7 +107,7 @@ constructor(
isAutoTunnelPaused = isAutoTunnelPaused, isAutoTunnelPaused = isAutoTunnelPaused,
), ),
) )
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate(application) WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate()
} }
fun onToggleAlwaysOnVPN() = fun onToggleAlwaysOnVPN() =
@@ -162,21 +159,42 @@ constructor(
) )
} }
fun onToggleAmnezia() = viewModelScope.launch {
if(uiState.value.settings.isKernelEnabled) {
saveKernelMode(false)
}
saveAmneziaMode(!uiState.value.settings.isAmneziaEnabled)
}
private fun saveAmneziaMode(on: Boolean) {
saveSettings(
uiState.value.settings.copy(
isAmneziaEnabled = on
)
)
}
fun onToggleKernelMode(): Result<Unit> { fun onToggleKernelMode(): Result<Unit> {
if (!uiState.value.settings.isKernelEnabled) { if (!uiState.value.settings.isKernelEnabled) {
try { try {
rootShell.start() rootShell.start()
Timber.i("Root shell accepted!") Timber.i("Root shell accepted!")
saveKernelMode(on = true) saveSettings(
uiState.value.settings.copy(
isKernelEnabled = true,
isAmneziaEnabled = false,
),
)
} catch (e: RootShell.RootShellException) { } catch (e: RootShell.RootShellException) {
Timber.e(e) Timber.e(e)
saveKernelMode(on = false) saveKernelMode(on = false)
return Result.Error(Event.Error.RootDenied) return Result.failure(WgTunnelExceptions.RootDenied())
} }
} else { } else {
saveKernelMode(on = false) saveKernelMode(on = false)
} }
return Result.Success(Unit) return Result.success(Unit)
} }
fun onToggleRestartOnPing() = viewModelScope.launch { fun onToggleRestartOnPing() = viewModelScope.launch {
@@ -37,6 +37,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextDecoration
@@ -106,7 +107,7 @@ fun SupportScreen(
modifier = Modifier.padding(bottom = 20.dp), modifier = Modifier.padding(bottom = 20.dp),
) )
TextButton( TextButton(
onClick = { appViewModel.openWebPage(context.resources.getString(R.string.docs_url)) }, onClick = { appViewModel.openWebPage(context.resources.getString(R.string.docs_url), context) },
modifier = Modifier modifier = Modifier
.padding(vertical = 5.dp) .padding(vertical = 5.dp)
.focusRequester(focusRequester), .focusRequester(focusRequester),
@@ -122,7 +123,13 @@ fun SupportScreen(
Text( Text(
stringResource(id = R.string.docs_description), stringResource(id = R.string.docs_description),
textAlign = TextAlign.Justify, textAlign = TextAlign.Justify,
modifier = Modifier.padding(start = 10.dp), modifier = Modifier
.padding(start = 10.dp)
.weight(
weight = 1.0f,
fill = false,
),
softWrap = true
) )
} }
Icon( Icon(
@@ -136,7 +143,7 @@ fun SupportScreen(
color = MaterialTheme.colorScheme.onBackground, color = MaterialTheme.colorScheme.onBackground,
) )
TextButton( TextButton(
onClick = { appViewModel.openWebPage(context.resources.getString(R.string.discord_url)) }, onClick = { appViewModel.openWebPage(context.resources.getString(R.string.telegram_url), context) },
modifier = Modifier.padding(vertical = 5.dp), modifier = Modifier.padding(vertical = 5.dp),
) { ) {
Row( Row(
@@ -145,7 +152,7 @@ fun SupportScreen(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) { ) {
Row { Row {
val icon = ImageVector.vectorResource(R.drawable.discord) val icon = ImageVector.vectorResource(R.drawable.telegram)
Icon( Icon(
icon, icon,
icon.name, icon.name,
@@ -168,7 +175,7 @@ fun SupportScreen(
color = MaterialTheme.colorScheme.onBackground, color = MaterialTheme.colorScheme.onBackground,
) )
TextButton( TextButton(
onClick = { appViewModel.openWebPage(context.resources.getString(R.string.github_url)) }, onClick = { appViewModel.openWebPage(context.resources.getString(R.string.github_url), context) },
modifier = Modifier.padding(vertical = 5.dp), modifier = Modifier.padding(vertical = 5.dp),
) { ) {
Row( Row(
@@ -200,7 +207,7 @@ fun SupportScreen(
color = MaterialTheme.colorScheme.onBackground, color = MaterialTheme.colorScheme.onBackground,
) )
TextButton( TextButton(
onClick = { appViewModel.launchEmail() }, onClick = { appViewModel.launchEmail(context) },
modifier = Modifier.padding(vertical = 5.dp), modifier = Modifier.padding(vertical = 5.dp),
) { ) {
Row( Row(
@@ -262,7 +269,7 @@ fun SupportScreen(
fontSize = 16.sp, fontSize = 16.sp,
modifier = modifier =
Modifier.clickable { Modifier.clickable {
appViewModel.openWebPage(context.resources.getString(R.string.privacy_policy_url)) appViewModel.openWebPage(context.resources.getString(R.string.privacy_policy_url), context)
}, },
) )
Row( Row(
@@ -270,8 +277,21 @@ fun SupportScreen(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(25.dp), modifier = Modifier.padding(25.dp),
) { ) {
Text("Version: ${BuildConfig.VERSION_NAME}", modifier = Modifier.focusable()) val version = buildAnnotatedString {
Text("Mode: ${if (uiState.settings.isKernelEnabled) "Kernel" else "Userspace"}") append(stringResource(id = R.string.version))
append(": ")
append(BuildConfig.VERSION_NAME)
}
val mode = buildAnnotatedString {
append(stringResource(R.string.mode))
append(": ")
when(uiState.settings.isKernelEnabled){
true -> append(stringResource(id = R.string.kernel))
false -> append(stringResource(id = R.string.userspace))
}
}
Text(version.text, modifier = Modifier.focusable())
Text(mode.text)
} }
} }
} }
@@ -1,5 +1,5 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.support package com.zaneschepke.wireguardautotunnel.ui.screens.support
import com.zaneschepke.wireguardautotunnel.data.model.Settings import com.zaneschepke.wireguardautotunnel.data.domain.Settings
data class SupportUiState(val settings: Settings = Settings()) data class SupportUiState(val settings: Settings = Settings())
@@ -27,6 +27,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.ClipboardManager import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -43,6 +44,8 @@ fun LogsScreen(appViewModel: AppViewModel) {
appViewModel.logs appViewModel.logs
} }
val context = LocalContext.current
val lazyColumnListState = rememberLazyListState() val lazyColumnListState = rememberLazyListState()
val clipboardManager: ClipboardManager = LocalClipboardManager.current val clipboardManager: ClipboardManager = LocalClipboardManager.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@@ -57,7 +60,7 @@ fun LogsScreen(appViewModel: AppViewModel) {
floatingActionButton = { floatingActionButton = {
FloatingActionButton( FloatingActionButton(
onClick = { onClick = {
appViewModel.saveLogsToFile() appViewModel.saveLogsToFile(context)
}, },
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
containerColor = MaterialTheme.colorScheme.primary, containerColor = MaterialTheme.colorScheme.primary,
@@ -2,7 +2,7 @@ package com.zaneschepke.wireguardautotunnel.util
object Constants { object Constants {
const val BASE_LOG_FILE_NAME = "wgtunnel-logs" const val BASE_LOG_FILE_NAME = "wg_tunnel_logs"
const val LOG_BUFFER_SIZE = 3_000L const val LOG_BUFFER_SIZE = 3_000L
const val MANUAL_TUNNEL_CONFIG_ID = "0" const val MANUAL_TUNNEL_CONFIG_ID = "0"
@@ -25,7 +25,7 @@ object Constants {
const val SUBSCRIPTION_TIMEOUT = 5_000L const val SUBSCRIPTION_TIMEOUT = 5_000L
const val FOCUS_REQUEST_DELAY = 500L const val FOCUS_REQUEST_DELAY = 500L
const val BACKUP_PING_HOST = "1.1.1.1" const val DEFAULT_PING_IP = "1.1.1.1"
const val PING_TIMEOUT = 5_000L const val PING_TIMEOUT = 5_000L
const val VPN_RESTART_DELAY = 1_000L const val VPN_RESTART_DELAY = 1_000L
const val PING_INTERVAL = 60_000L const val PING_INTERVAL = 60_000L
@@ -35,4 +35,9 @@ object Constants {
const val TUNNEL_EXTRA_KEY = "tunnelId" const val TUNNEL_EXTRA_KEY = "tunnelId"
const val UNREADABLE_SSID = "<unknown ssid>"
val amneziaProperties = listOf("Jc", "Jmin", "Jmax", "S1", "S2", "H1", "H2", "H3", "H4")
const val QR_CODE_NAME_PROPERTY = "# Name ="
} }
@@ -1,117 +0,0 @@
package com.zaneschepke.wireguardautotunnel.util
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
sealed class Event {
abstract val message: String
sealed class Error : Event() {
data object None : Error() {
override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.error_none)
}
data object SsidConflict : Error() {
override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.error_ssid_exists)
}
data class ConfigParseError(val appendedMessage: String) : Error() {
override val message: String =
WireGuardAutoTunnel.instance.getString(R.string.config_parse_error) + (
if (appendedMessage != "") ": ${appendedMessage.trim()}" else "")
}
data object RootDenied : Error() {
override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.error_root_denied)
}
data class General(val customMessage: String) : Error() {
override val message: String
get() = customMessage
}
data class Exception(val exception: kotlin.Exception) : Error() {
override val message: String
get() =
exception.message
?: WireGuardAutoTunnel.instance.getString(R.string.unknown_error)
}
data object InvalidQrCode : Error() {
override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.error_invalid_code)
}
data object InvalidFileExtension : Error() {
override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.error_file_extension)
}
data object FileReadFailed : Error() {
override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.error_file_extension)
}
data object AuthenticationFailed : Error() {
override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.error_authentication_failed)
}
data object AuthorizationFailed : Error() {
override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.error_authorization_failed)
}
data object BackgroundLocationRequired : Error() {
override val message: String
get() =
WireGuardAutoTunnel.instance.getString(R.string.background_location_required)
}
data object LocationServicesRequired : Error() {
override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.location_services_required)
}
data object PreciseLocationRequired : Error() {
override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.precise_location_required)
}
data object FileExplorerRequired : Error() {
override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.error_no_file_explorer)
}
}
sealed class Message : Event() {
data object ConfigSaved : Message() {
override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.config_changes_saved)
}
data object ConfigsExported : Message() {
override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.exported_configs_message)
}
data object TunnelOffAction : Message() {
override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.turn_off_tunnel)
}
data object TunnelOnAction : Message() {
override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.turn_on_tunnel)
}
data object AutoTunnelOffAction : Message() {
override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.turn_off_auto)
}
}
}
@@ -1,16 +1,17 @@
package com.zaneschepke.wireguardautotunnel.util package com.zaneschepke.wireguardautotunnel.util
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import com.wireguard.android.backend.Statistics import com.zaneschepke.wireguardautotunnel.R
import com.wireguard.android.backend.Statistics.PeerStats import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.wireguard.crypto.Key
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.amnezia.awg.config.Config
import java.math.BigDecimal import java.math.BigDecimal
import java.text.DecimalFormat import java.text.DecimalFormat
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
@@ -50,15 +51,15 @@ typealias TunnelConfigs = List<TunnelConfig>
typealias Packages = List<PackageInfo> typealias Packages = List<PackageInfo>
fun Statistics.mapPeerStats(): Map<Key, PeerStats?> { fun TunnelStatistics.mapPeerStats(): Map<org.amnezia.awg.crypto.Key, TunnelStatistics.PeerStats?> {
return this.peers().associateWith { key -> (this.peer(key)) } return this.getPeers().associateWith { key -> (this.peerStats(key)) }
} }
fun PeerStats.latestHandshakeSeconds(): Long? { fun TunnelStatistics.PeerStats.latestHandshakeSeconds(): Long? {
return NumberUtils.getSecondsBetweenTimestampAndNow(this.latestHandshakeEpochMillis) return NumberUtils.getSecondsBetweenTimestampAndNow(this.latestHandshakeEpochMillis)
} }
fun PeerStats.handshakeStatus(): HandshakeStatus { fun TunnelStatistics.PeerStats.handshakeStatus(): HandshakeStatus {
// TODO add never connected status after duration // TODO add never connected status after duration
return this.latestHandshakeSeconds().let { return this.latestHandshakeSeconds().let {
when { when {
@@ -71,3 +72,25 @@ fun PeerStats.handshakeStatus(): HandshakeStatus {
} }
} }
} }
fun Config.toWgQuickString() : String {
val amQuick = toAwgQuickString()
val lines = amQuick.lines().toMutableList()
val linesIterator = lines.iterator()
while(linesIterator.hasNext()) {
val next = linesIterator.next()
Constants.amneziaProperties.forEach {
if(next.startsWith(it, ignoreCase = true)) {
linesIterator.remove()
}
}
}
return lines.joinToString(System.lineSeparator())
}
fun Throwable.getMessage(context: Context) : String {
return when(this) {
is WgTunnelExceptions -> this.getMessage(context)
else -> this.message ?: StringValue.StringResource(R.string.unknown_error).asString(context)
}
}
@@ -6,6 +6,7 @@ import android.os.Build
import android.os.Environment import android.os.Environment
import android.provider.MediaStore import android.provider.MediaStore
import android.provider.MediaStore.MediaColumns import android.provider.MediaStore.MediaColumns
import timber.log.Timber
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.OutputStream import java.io.OutputStream
@@ -70,21 +71,27 @@ object FileUtils {
} }
} }
fun saveFilesToZip(context: Context, files: List<File>) { fun saveFilesToZip(context: Context, files: List<File>) : Result<Unit> {
val zipOutputStream = return try {
createDownloadsFileOutputStream( val zipOutputStream =
context, createDownloadsFileOutputStream(
"wg-export_${Instant.now().epochSecond}.zip", context,
ZIP_FILE_MIME_TYPE, "wg-export_${Instant.now().epochSecond}.zip",
) ZIP_FILE_MIME_TYPE,
ZipOutputStream(zipOutputStream).use { zos -> )
files.forEach { file -> ZipOutputStream(zipOutputStream).use { zos ->
val entry = ZipEntry(file.name) files.forEach { file ->
zos.putNextEntry(entry) val entry = ZipEntry(file.name)
if (file.isFile) { zos.putNextEntry(entry)
file.inputStream().use { fis -> fis.copyTo(zos) } if (file.isFile) {
file.inputStream().use { fis -> fis.copyTo(zos) }
}
} }
return Result.success(Unit)
} }
} catch (e : Exception) {
Timber.e(e)
Result.failure(WgTunnelExceptions.ConfigExportFailed())
} }
} }
} }
@@ -1,16 +0,0 @@
package com.zaneschepke.wireguardautotunnel.util
import timber.log.Timber
sealed class Result<T> {
class Success<T>(val data: T) : Result<T>()
class Error<T>(val error: Event.Error) : Result<T>() {
init {
when (this.error) {
is Event.Error.Exception -> Timber.e(this.error.exception)
else -> Timber.e(this.error.message)
}
}
}
}
@@ -0,0 +1,124 @@
package com.zaneschepke.wireguardautotunnel.util
import android.content.Context
import com.zaneschepke.wireguardautotunnel.R
sealed class WgTunnelExceptions : Exception() {
abstract fun getMessage(context: Context) : String
data class General(private val userMessage : StringValue) : WgTunnelExceptions() {
override fun getMessage(context: Context) : String {
return userMessage.asString(context)
}
}
data class SsidConflict(private val userMessage : StringValue = StringValue.StringResource(R.string.error_ssid_exists)) : WgTunnelExceptions() {
override fun getMessage(context: Context) : String {
return userMessage.asString(context)
}
}
data class ConfigExportFailed(private val userMessage : StringValue = StringValue.StringResource(R.string.export_configs_failed)) : WgTunnelExceptions() {
override fun getMessage(context: Context) : String {
return userMessage.asString(context)
}
}
data class ConfigParseError(private val appendMessage : StringValue = StringValue.Empty) : WgTunnelExceptions() {
override fun getMessage(context: Context) : String {
return StringValue.StringResource(R.string.config_parse_error).asString(context) + (
if (appendMessage != StringValue.Empty) ": ${appendMessage.asString(context)}" else "")
}
}
data class RootDenied(private val userMessage : StringValue = StringValue.StringResource(R.string.error_root_denied)) : WgTunnelExceptions() {
override fun getMessage(context: Context) : String {
return userMessage.asString(context)
}
}
data class InvalidQrCode(private val userMessage : StringValue = StringValue.StringResource(R.string.error_invalid_code)) : WgTunnelExceptions() {
override fun getMessage(context: Context) : String {
return userMessage.asString(context)
}
}
data class InvalidFileExtension(private val userMessage : StringValue = StringValue.StringResource(R.string.error_file_extension)) : WgTunnelExceptions() {
override fun getMessage(context: Context) : String {
return userMessage.asString(context)
}
}
data class FileReadFailed(private val userMessage : StringValue = StringValue.StringResource(R.string.error_file_format)) : WgTunnelExceptions() {
override fun getMessage(context: Context) : String {
return userMessage.asString(context)
}
}
data class AuthenticationFailed(private val userMessage : StringValue = StringValue.StringResource(R.string.error_authentication_failed)) : WgTunnelExceptions() {
override fun getMessage(context: Context) : String {
return userMessage.asString(context)
}
}
data class AuthorizationFailed(private val userMessage : StringValue = StringValue.StringResource(R.string.error_authorization_failed)) : WgTunnelExceptions() {
override fun getMessage(context: Context) : String {
return userMessage.asString(context)
}
}
data class BackgroundLocationRequired(private val userMessage : StringValue = StringValue.StringResource(R.string.background_location_required)) : WgTunnelExceptions() {
override fun getMessage(context: Context) : String {
return userMessage.asString(context)
}
}
data class LocationServicesRequired(private val userMessage : StringValue = StringValue.StringResource(R.string.location_services_required)) : WgTunnelExceptions() {
override fun getMessage(context: Context) : String {
return userMessage.asString(context)
}
}
data class PreciseLocationRequired(private val userMessage : StringValue = StringValue.StringResource(R.string.precise_location_required)) : WgTunnelExceptions() {
override fun getMessage(context: Context) : String {
return userMessage.asString(context)
}
}
data class FileExplorerRequired (private val userMessage : StringValue = StringValue.StringResource(R.string.error_no_file_explorer)) : WgTunnelExceptions() {
override fun getMessage(context: Context) : String {
return userMessage.asString(context)
}
}
// sealed class Message : Event() {
// data object ConfigSaved : Message() {
// override val message: String
// get() = WireGuardAutoTunnel.instance.getString(R.string.config_changes_saved)
// }
//
// data object ConfigsExported : Message() {
// override val message: String
// get() = WireGuardAutoTunnel.instance.getString(R.string.exported_configs_message)
// }
//
// data object TunnelOffAction : Message() {
// override val message: String
// get() = WireGuardAutoTunnel.instance.getString(R.string.turn_off_tunnel)
// }
//
// data object TunnelOnAction : Message() {
// override val message: String
// get() = WireGuardAutoTunnel.instance.getString(R.string.turn_on_tunnel)
// }
//
// data object AutoTunnelOffAction : Message() {
// override val message: String
// get() = WireGuardAutoTunnel.instance.getString(R.string.turn_off_auto)
// }
// }
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

+9
View File
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M440,520L200,520v-80h240v-240h80v240h240v80L520,520v240h-80v-240Z"
android:fillColor="#e8eaed"/>
</vector>
+9
View File
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="m256,760 l-56,-56 224,-224 -224,-224 56,-56 224,224 224,-224 56,56 -224,224 224,224 -56,56 -224,-224 -224,224Z"
android:fillColor="#e8eaed"/>
</vector>
+9
View File
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M200,760h57l391,-391 -57,-57 -391,391v57ZM120,840v-170l528,-527q12,-11 26.5,-17t30.5,-6q16,0 31,6t26,18l55,56q12,11 17.5,26t5.5,30q0,16 -5.5,30.5T817,313L290,840L120,840ZM760,256 L704,200 760,256ZM619,341 L591,312 648,369 619,341Z"
android:fillColor="#e8eaed"/>
</vector>
+9
View File
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="50dp"
android:height="50dp"
android:viewportWidth="50"
android:viewportHeight="50">
<path
android:fillColor="#FF000000"
android:pathData="M25,2c12.703,0 23,10.297 23,23S37.703,48 25,48S2,37.703 2,25S12.297,2 25,2zM32.934,34.375c0.423,-1.298 2.405,-14.234 2.65,-16.783c0.074,-0.772 -0.17,-1.285 -0.648,-1.514c-0.578,-0.278 -1.434,-0.139 -2.427,0.219c-1.362,0.491 -18.774,7.884 -19.78,8.312c-0.954,0.405 -1.856,0.847 -1.856,1.487c0,0.45 0.267,0.703 1.003,0.966c0.766,0.273 2.695,0.858 3.834,1.172c1.097,0.303 2.346,0.04 3.046,-0.395c0.742,-0.461 9.305,-6.191 9.92,-6.693c0.614,-0.502 1.104,0.141 0.602,0.644c-0.502,0.502 -6.38,6.207 -7.155,6.997c-0.941,0.959 -0.273,1.953 0.358,2.351c0.721,0.454 5.906,3.932 6.687,4.49c0.781,0.558 1.573,0.811 2.298,0.811C32.191,36.439 32.573,35.484 32.934,34.375z"/>
</vector>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_channel_background" />
</adaptive-icon>
+1
View File
@@ -0,0 +1 @@
unqualifiedResLocale=en-US
+164
View File
@@ -0,0 +1,164 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">WG Tunnel</string>
<string name="error_file_extension">Dies ist keine .conf oder .zip Datei</string>
<string name="no_tunnels">Noch keine Tunnel hinzugefügt!</string>
<string name="watcher_notification_text_active">Überwachung der Netzwerkänderungen: aktiv</string>
<string name="tunnels">Tunnel</string>
<string name="enable_auto_tunnel">Starte automatisches Verbinden</string>
<string name="tunnel_mobile_data">Verbinden im Mobilnetzwerk</string>
<string name="one_tunnel_required">Mindestens eine Verbindung wird für diese Funktion benötigt</string>
<string name="privacy_policy">Siehe Privacy Policy</string>
<string name="disable_auto_tunnel">Stoppe automatisches Verbinden</string>
<string name="okay">Okay</string>
<string name="tunnel_on_ethernet">Verbindung über Ethernet</string>
<string name="search_icon">Suchsymbol</string>
<string name="attempt_connection">Versuche zu verbinden..</string>
<string name="auto_tunneling">Automatisches verbinden</string>
<string name="default_vpn_off">Primärer VPN aus</string>
<string name="turn_on_tunnel">Für diese Altion muss eine Verbindung bestehen</string>
<string name="watcher_notification_text_paused">Überwachung der Netzwerkänderungen: pausiert</string>
<string name="tunnel_start_title">VPN verbunden</string>
<string name="tunnel_start_text">Mit Tunnel verbunden</string>
<string name="notification_permission_required">Berechtigung für die Benachrichtigung benötigt.</string>
<string name="add_trusted_ssid">Vertrauenswürdiger WiFi-Name hinzugügen</string>
<string name="prominent_background_location_message">Diese Funktion benötigt Standortberechtigung im Hintergrund um die WiFi SSIDs auch wenn die Applikation geschlossen ist zu überwachen. Für mehr Infos, siehe Privacy Policy auf der Hilfeseite.</string>
<string name="prominent_background_location_title">Vereinbarung der Standortberechtigung im Hintergrund</string>
<string name="thank_you">Danke fürs benutzen von WG Tunnel!</string>
<string name="trusted_ssid_empty_description">SSID eingeben</string>
<string name="trusted_ssid_value_description">SSID bestätigen</string>
<string name="add_tunnels_text">Von Datei oder ZIP hinzufügen</string>
<string name="open_file">Datei geöffnet</string>
<string name="add_from_qr">Von QR-Code hinzufügen</string>
<string name="qr_scan">Scanne QR</string>
<string name="tunnel_name">Verbindungsname</string>
<string name="add_tunnel">Verbindung hinzufügen</string>
<string name="exclude">Ausgenommen</string>
<string name="include">Eingeschlossen</string>
<string name="tunnel_all">Verbinde alle Apps</string>
<string name="config_changes_saved">Änderungen der Konfiguration gespeichert.</string>
<string name="save_changes">Speichern</string>
<string name="icon">Symbol</string>
<string name="no_thanks">Nein danke</string>
<string name="turn_on">Einschalten</string>
<string name="map">Karte</string>
<string name="public_key">Öffentlicher Schlüssel</string>
<string name="addresses">Adressen</string>
<string name="dns_servers">DNS-Server</string>
<string name="mtu">MTU</string>
<string name="peer">Peer</string>
<string name="allowed_ips">Erlaubte IPs</string>
<string name="endpoint">Endpunkt</string>
<string name="name">Name</string>
<string name="restart">Verbindung neustarten</string>
<string name="vpn_connection_failed">Verbindung fehlgeschlagen</string>
<string name="always_on_vpn_support">Erlaube Always-On VPN</string>
<string name="location_services_not_detected">Standortdienste Nicht Erkannt</string>
<string name="hint_search_packages">Suche packete</string>
<string name="clear_icon">Lösche Symbol</string>
<string name="vpn_starting">VPN startet</string>
<string name="db_name">wg-tunnel-db</string>
<string name="scanning_qr">Scanne nach QR</string>
<string name="none">Keine vertrauenswürdigen WiFi Namen</string>
<string name="other">Andere</string>
<string name="vpn_on">VPN an</string>
<string name="vpn_off">VPN aus</string>
<string name="default_vpn_on">Primärer VPN an</string>
<string name="create_import">Starte von Grund auf neu</string>
<string name="turn_off_auto">Für diese Aktion muss automatisches Verbinden ausgeschaltet oder pausiert sein</string>
<string name="add_peer">Peer hinzufügen</string>
<string name="done">Erledigt</string>
<string name="rotate_keys">Schlüssel ändern</string>
<string name="private_key">Privater Schlüssel</string>
<string name="copy_public_key">Öffentlicher Schlüssel kopieren</string>
<string name="base64_key">base64-Schlüssel</string>
<string name="comma_separated_list">Kommaseparierte Liste</string>
<string name="delete_tunnel">Tunnel löschen</string>
<string name="persistent_keepalive">Dauerhaftes Keepalive</string>
<string name="background_location_required">Hintergrund Standortdienste erforderlich</string>
<string name="enable_app_lock">App Sperre aktiviert</string>
<string name="discord_description">Tritt der Community bei</string>
<string name="interface_">Schnittstelle</string>
<string name="listen_port">Eingehender Port</string>
<string name="random">(zufällig)</string>
<string name="optional">(nicht erforderlich)</string>
<string name="optional_no_recommend">(nicht erforderlich, aber empfohlen)</string>
<string name="seconds">Sekunden</string>
<string name="cancel">Abbrechen</string>
<string name="preshared_key">Geteilter Schlüssel</string>
<string name="enabled_app_shortcuts">Aktiviere App Verknüpfungen</string>
<string name="exported_configs_message">Konfigurationen in den Download Ordner exportiert</string>
<string name="tunnel_on_wifi">Tunnel bei nicht vertrauenswürdigem Wifi</string>
<string name="email_subject">WG Tunnel Unterstützung</string>
<string name="docs_description">Lese die Dokumentation</string>
<string name="email_description">Sende mir eine E-Mail</string>
<string name="support_help_text">Bei Fehlern oder Verbesserungsvorschlägen stehen folgende Ressourcen zur Verfügung:</string>
<string name="error_root_denied">Root Shell verboten</string>
<string name="error_no_file_explorer">Kein Datei-Explorer installiert</string>
<string name="location_services_missing_message">Die App konnte keine aktivierten Standortdienste auf deinem Gerät erkennen. Dies kann auf manchen Geräten ein Auslesen des aktuellen WIFI-Names für die \"Nicht vertrauenswürdiges WIFI\" Funktion verhindern. Möchtest du trotzdem fortfahren ?</string>
<string name="auto_tunnel_title">Auto-Tunnel Service</string>
<string name="delete_tunnel_message">Bist du sicher, dass du den Tunnel löschen möchtest?</string>
<string name="yes">Ja</string>
<string name="resume">Fortsetzen</string>
<string name="pause">Pausieren</string>
<string name="paused">Pausiert</string>
<string name="active">Aktiv</string>
<string name="go">Gehe</string>
<string name="excluded">Ausgeschlossen</string>
<string name="all">Alle</string>
<string name="always_on_disabled">Always-on VPN wollte eine Tunnel starten, aber dieses Feature ist in den Einstellungen deaktiviert.</string>
<string name="no_browser_detected">Kein Browser erkannt</string>
<string name="open_issue">Öffne ein Issue</string>
<string name="read_logs">Lese die Logs</string>
<string name="auto">(automatisch)</string>
<string name="config_parse_error">Fehler beim lesen der Konfiguration</string>
<string name="incorrect_pin">PIN is nicht korrekt</string>
<string name="pin_created">PIN erfolgreich angelegt</string>
<string name="enter_pin">Gib deine PIN ein</string>
<string name="auto_off">Auto-Tunnel pausieren</string>
<string name="auto_tun_on">Auto-Tunnel fortsetzen</string>
<string name="auto_tun_off">Auto-Tunnel pausieren</string>
<string name="version">Version</string>
<string name="mode">Modus</string>
<string name="userspace">Userspace</string>
<string name="settings">Einstellungen</string>
<string name="support">Unterstützung</string>
<string name="watcher_channel_id">Beobachter Kanal</string>
<string name="error_authentication_failed">Anmeldung fehlgeschlagen</string>
<string name="export_configs">Exportiere Konfigurationen</string>
<string name="unknown_error">Ein unbekannter Fehler ist aufgetreten</string>
<string name="email_chooser">Sende eine E-Mail…</string>
<string name="error_authorization_failed">Fehler bei der Authorisierung</string>
<string name="location_services_required">Standortdienste erforderlich</string>
<string name="precise_location_required">Genauer Standort erforderlich</string>
<string name="error_invalid_code">Fehlerhafter QR code</string>
<string name="error_none">Kein Fehler</string>
<string name="tunneling_apps">Tunnel Anwendungen</string>
<string name="included">Hinzugefügt</string>
<string name="no_email_detected">Keine E-Mail Anwendung erkannt</string>
<string name="logs_saved">Logs im Download Ordner gespeichert</string>
<string name="create_pin">Erstelle eine PIN</string>
<string name="use_tunnel_on_wifi_name">Tunnel in Wifi-Namen verwenden</string>
<string name="no_wifi_names_configured">Keine Wifi-Namen für diesen Tunnel konfiguriert</string>
<string name="disabled">Deaktiviert</string>
<string name="mobile_data_tunnel">Als Tunnel für Mobile Daten setzen</string>
<string name="general">Allgemein</string>
<string name="restart_on_ping">Neustart bei PING Fehler (Beta)</string>
<string name="edit_tunnel">Tunnel bearbeiten</string>
<string name="set_primary_tunnel">Als Primären Tunnel setzen</string>
<string name="auto_on">Auto-Tunnel fortsetzen</string>
<string name="vpn_channel_id">VPN Kanal</string>
<string name="vpn_channel_name">VPN Benachrichtigungskanal</string>
<string name="watcher_channel_name">Beobachter Benachrichtigungskanal</string>
<string name="turn_off_tunnel">Aktion erfordert deaktivierten Tunnel</string>
<string name="kernel">Kernel</string>
<string name="use_kernel">Verwende das Kernel Modul</string>
<string name="error_ssid_exists">SSID existiert bereits</string>
<string name="use_amnezia">"Benutze Amnezia Benuzterumgebung "</string>
<string name="junk_packet_count">Müll Packet Anzahk</string>
<string name="junk_packet_maximum_size">Müll Packet maximale Grösse</string>
<string name="init_packet_junk_size">Erstes Packet Müllgrösse</string>
<string name="backend">Backend</string>
<string name="junk_packet_minimum_size">Müll Packet minimale Grösse</string>
<string name="response_packet_junk_size">Antwortpaket Müllgrösse</string>
</resources>
+157
View File
@@ -0,0 +1,157 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="map">Mapa</string>
<string name="allowed_ips">Allowed IPs</string>
<string name="email_chooser">Enviar un email…</string>
<string name="go">ir</string>
<string name="discord_description">Únete a la comunidad</string>
<string name="kernel">Kernel</string>
<string name="restart">Reiniciar túnel</string>
<string name="vpn_connection_failed">Error de conexión</string>
<string name="error_none">Sin error</string>
<string name="other">Otros</string>
<string name="add_peer">Añadir peer</string>
<string name="done">Hecho</string>
<string name="copy_public_key">Copiar public key</string>
<string name="base64_key">clave base64</string>
<string name="exported_configs_message">Configuración exportada a Descargas</string>
<string name="tunnel_on_wifi">Túnel en Wi-Fi no de confianza</string>
<string name="location_services_missing_message">La app no detecta activado el servicio de ubicación en tu dispositivo. Dependiendo del dispositivo, esto podría hacer que la característica de Wi-Fi no de confianza falle al leer el nombre Wi-Fi. ¿Quieres continuar de todas formas?</string>
<string name="excluded">excluida(s)</string>
<string name="all">todas</string>
<string name="mobile_data_tunnel">Establecer como túnel en datos móviles</string>
<string name="use_tunnel_on_wifi_name">Usar tunnel en nombre Wi-Fi</string>
<string name="disabled">desactivado</string>
<string name="version">Versión</string>
<string name="userspace">Espacio del usuario</string>
<string name="mode">Modo</string>
<string name="support">Ayuda</string>
<string name="private_key">Clave privada</string>
<string name="trusted_ssid_value_description">Enviar SSID</string>
<string name="trusted_ssid_empty_description">Introducir SSID</string>
<string name="add_tunnels_text">Añadir desde archivo o zip</string>
<string name="open_file">Abrir archivo</string>
<string name="add_from_qr">Añadir mediante código QR</string>
<string name="qr_scan">Escanear QR</string>
<string name="tunnel_name">Nombre de túnel</string>
<string name="add_tunnel">Añadir túnel</string>
<string name="exclude">Excluir</string>
<string name="include">Incluir</string>
<string name="tunnel_all">Todas las apps por el túnel</string>
<string name="config_changes_saved">Cambios de configuración guardados.</string>
<string name="save_changes">Guardar</string>
<string name="icon">Icono</string>
<string name="no_thanks">No gracias</string>
<string name="turn_on">Activar</string>
<string name="mtu">MTU</string>
<string name="dns_servers">DNS servers</string>
<string name="addresses">Addresses</string>
<string name="public_key">Public key</string>
<string name="error_file_extension">No es un archivo .conf o .zip</string>
<string name="turn_off_tunnel">Desactiva antes el túnel</string>
<string name="no_tunnels">¡Ningún túnel añadido aún!</string>
<string name="watcher_notification_text_paused">Monitoreando cambios de red: En pausa</string>
<string name="watcher_notification_text_active">Monitoreando cambios de red: Activado</string>
<string name="tunnel_start_title">VPN conectada</string>
<string name="tunnel_start_text">Conectado al túnel</string>
<string name="notification_permission_required">Necesita permiso de notificaciones.</string>
<string name="add_trusted_ssid">Añadir nombres Wi-Fi de confianza</string>
<string name="tunnels">Túneles</string>
<string name="enable_auto_tunnel">Iniciar túnel-automático</string>
<string name="disable_auto_tunnel">Parar túnel-automático</string>
<string name="tunnel_mobile_data">Activar túnel en datos móviles</string>
<string name="one_tunnel_required">Esta característica necesita ser usada en almenos por un túnel</string>
<string name="privacy_policy">Ver Política de Privacidad</string>
<string name="okay">OK</string>
<string name="tunnel_on_ethernet">Túnel en ethernet</string>
<string name="prominent_background_location_title">Divulgación de la ubicación en segundo plano</string>
<string name="thank_you">¡Gracias por usar WG Tunnel!</string>
<string name="endpoint">Endpoint</string>
<string name="peer">Peer</string>
<string name="name">Nombre</string>
<string name="always_on_vpn_support">Permitir VPN siempre-activada</string>
<string name="location_services_not_detected">Servicios de Ubicación No Detectados</string>
<string name="hint_search_packages">Buscar paquetes</string>
<string name="clear_icon">Icono claro</string>
<string name="attempt_connection">Intentando conexión...</string>
<string name="vpn_starting">Iniciando VPN</string>
<string name="db_name">wg-tunnel-db</string>
<string name="scanning_qr">Escaneando QR</string>
<string name="none">Sin nombres Wi-Fi de confianza</string>
<string name="auto_tunneling">Túnel-automático</string>
<string name="vpn_on">VPN on</string>
<string name="vpn_off">VPN off</string>
<string name="default_vpn_on">VPN Principal on</string>
<string name="default_vpn_off">VPN Principal off</string>
<string name="create_import">Crear desde cero</string>
<string name="turn_off_auto">La acción necesita que túnel-automático esté desactivado o en pausa</string>
<string name="turn_on_tunnel">La acción necesita un túnel activado</string>
<string name="rotate_keys">Rotar claves</string>
<string name="comma_separated_list">lista separada por comas</string>
<string name="random">(aleatorio)</string>
<string name="optional">(opcional)</string>
<string name="optional_no_recommend">(opcional, no recomendado)</string>
<string name="seconds">segundos</string>
<string name="cancel">Cancelar</string>
<string name="error_authentication_failed">Fallo de autenticación</string>
<string name="error_authorization_failed">Fallo de autorización</string>
<string name="enabled_app_shortcuts">Habilitar acesos directos de app</string>
<string name="export_configs">Exportar configuración</string>
<string name="background_location_required">Se necesita ubicación en segundo plano</string>
<string name="location_services_required">Se necesita servicio de ubicación</string>
<string name="precise_location_required">Necesita ubicación precisa</string>
<string name="unknown_error">Error desconocido</string>
<string name="email_subject">Ayuda WG Tunnel</string>
<string name="interface_">Interfaz</string>
<string name="listen_port">Puerto de escucha</string>
<string name="preshared_key">Clave previamente compartida</string>
<string name="persistent_keepalive">Keepalive persistente</string>
<string name="search_icon">Icono de búsqueda</string>
<string name="docs_description">Leer documentación</string>
<string name="email_description">Envíame un email</string>
<string name="support_help_text">Si tienes problemas, ideas para mejoras, o simlemente comprometerte, tienes disponibles los siguientes recursos:</string>
<string name="use_kernel">Usar módulo del Kernel</string>
<string name="error_ssid_exists">SSID existente</string>
<string name="error_root_denied">Shell root denegado</string>
<string name="error_no_file_explorer">Explorador de archivos no instalado</string>
<string name="error_invalid_code">Código QR no valido</string>
<string name="auto_tunnel_title">Servicio túnel-automático</string>
<string name="delete_tunnel">Eliminar túnel</string>
<string name="delete_tunnel_message">¿Estás seguro de que quieres eliminar este túnel?</string>
<string name="yes"></string>
<string name="resume">Reanudar</string>
<string name="pause">Pausar</string>
<string name="active">activado</string>
<string name="paused">en pausa</string>
<string name="tunneling_apps">Apps por el túnel</string>
<string name="included">incluida(s)</string>
<string name="always_on_disabled">VPN siempre-activada ha intentado iniciar un túnel, pero está característica está deshabilitada en los ajustes.</string>
<string name="no_email_detected">Ninguna app de email detectada</string>
<string name="no_browser_detected">Ningún navegador detectado</string>
<string name="logs_saved">Registros guardados en Descargas</string>
<string name="open_issue">Abrir una incidencia</string>
<string name="read_logs">Leer los registros</string>
<string name="auto">(automático)</string>
<string name="config_parse_error">Fallo al analizar la configuración</string>
<string name="incorrect_pin">El pin no es correcto</string>
<string name="pin_created">Pin creado con éxito</string>
<string name="enter_pin">Introduce tu pin</string>
<string name="create_pin">Crear pin</string>
<string name="enable_app_lock">Bloqueo de app activado</string>
<string name="restart_on_ping">Reiniciar al fallar ping (beta)</string>
<string name="set_primary_tunnel">Establecer como túnel Principal</string>
<string name="no_wifi_names_configured">No hay nombres Wi-Fi configurados para este túnel</string>
<string name="general">General</string>
<string name="edit_tunnel">Editar túnel</string>
<string name="auto_on">Reanudar túnel-automático</string>
<string name="auto_off">Pausar túnel-automático</string>
<string name="auto_tun_on">Reanudar el túnel automático</string>
<string name="auto_tun_off">Pausa del túnel automático</string>
<string name="settings">Ajustes</string>
<string name="app_name">WG Tunnel</string>
<string name="vpn_channel_id">Canal VPN</string>
<string name="vpn_channel_name">Canal de notificación VPN</string>
<string name="watcher_channel_id">Canal del obvervador</string>
<string name="watcher_channel_name">Canal de notificación del obvervador</string>
<string name="prominent_background_location_message">La monitorización SSID Wi-Fi necesita de permiso de ubicación en segundo plano incluso si la app está cerrada. Mira el enlace a la Política de Privacidad en la pantalla de ayuda para más detalles.</string>
</resources>
+39
View File
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="turn_off_tunnel">Действие требует отключения туннеля</string>
<string name="add_tunnel">Добавить туннель</string>
<string name="mtu">MTU</string>
<string name="tunnel_name">Имя туннеля</string>
<string name="public_key">Публичный ключ</string>
<string name="name">Имя</string>
<string name="peer">Пир</string>
<string name="privacy_policy">Посмотреть политику конфиденциальности</string>
<string name="icon">Иконка</string>
<string name="turn_on">Включить</string>
<string name="add_from_qr">Добавить из QR</string>
<string name="qr_scan">Сканер QR</string>
<string name="auto_tunneling">Авто-туннелирование</string>
<string name="no_tunnels">Туннели еще не добавлены!</string>
<string name="open_file">Открыть файл</string>
<string name="exclude">Исключить</string>
<string name="include">Включить</string>
<string name="tunnel_all">Туннель для всех приложений</string>
<string name="config_changes_saved">Изменения конфигурации сохранены.</string>
<string name="save_changes">Сохранить</string>
<string name="no_thanks">Нет, спасибо</string>
<string name="map">Карта</string>
<string name="addresses">Адреса</string>
<string name="dns_servers">DNS сервера</string>
<string name="allowed_ips">Разрешенные IP</string>
<string name="endpoint">Конечная точка</string>
<string name="restart">Перезагрузить туннель</string>
<string name="vpn_connection_failed">Ошибка соединения</string>
<string name="always_on_vpn_support">Разрешить постоянный VPN</string>
<string name="hint_search_packages">Поиск приложений</string>
<string name="other">Другое</string>
<string name="vpn_on">VPN вкл.</string>
<string name="vpn_off">VPN откл.</string>
<string name="interface_">Интерфейс</string>
<string name="optional">(необязательно)</string>
<string name="optional_no_recommend">(необязательно, не рекомендуется)</string>
</resources>
+162
View File
@@ -0,0 +1,162 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">WG Tunnel</string>
<string name="vpn_channel_id">VPN Kanalı</string>
<string name="vpn_channel_name">VPN Bildirim Kanalı</string>
<string name="watcher_channel_id">İzleyici Kanalı</string>
<string name="watcher_channel_name">İzleyici Bildirim Kanalı</string>
<string name="github_url" translatable="false">https://github.com/zaneschepke/wgtunnel/issues</string>
<string name="docs_url" translatable="false">https://zaneschepke.com/wgtunnel-docs/overview.html</string>
<string name="privacy_policy_url" translatable="false">https://zaneschepke.com/wgtunnel-docs/privacypolicy.html</string>
<string name="error_file_extension">Dosya bir .conf veya .zip değil</string>
<string name="turn_off_tunnel">Eylem tünelin kapalı olmasını gerektiriyor</string>
<string name="no_tunnels">Henüz bir tünel ekli değil!</string>
<string name="discord_url" translatable="false">https://discord.gg/rbRRNh6H7V</string>
<string name="watcher_notification_text_active">Ağ durumu değişikliklerini izleme: etkin</string>
<string name="watcher_notification_text_paused">Ağ durumu değişikliklerini izleme: duraklatıldı</string>
<string name="tunnel_start_title">VPN bağlandı</string>
<string name="tunnel_start_text">Tünele bağlı</string>
<string name="notification_permission_required">Bildirim izni gerekli.</string>
<string name="add_trusted_ssid">Güvenilir Wi-Fi adı ekle</string>
<string name="tunnels">Tüneller</string>
<string name="enable_auto_tunnel">Otomatik tünellemeyi başlat</string>
<string name="disable_auto_tunnel">Otomatik tünellemeyi durdur</string>
<string name="tunnel_mobile_data">Mobil veri üzerinde tünel</string>
<string name="one_tunnel_required">Bu özelliği kullanabilmek için en az bir tünel gereklidir.</string>
<string name="privacy_policy">Gizlilik Politikası</string>
<string name="okay">Tamam</string>
<string name="tunnel_on_ethernet">Ethernet üzerinde tünel</string>
<string name="prominent_background_location_message">Bu özellik, uygulama kapalıyken bile Wi-Fi SSID izlemesini etkinleştirmek için arka plan konumu izni gerektirir. Daha fazla bilgi için lütfen Destek ekranında bağlantısı verilen Gizlilik Politikasına bakın.</string>
<string name="prominent_background_location_title">Arka Plan Konum Açıklaması</string>
<string name="thank_you">WG Tunnel\'i kullandığınız için teşekkür ederiz!</string>
<string name="trusted_ssid_empty_description">SSID\'yi gir</string>
<string name="trusted_ssid_value_description">SSID\'yi gönder</string>
<string name="add_tunnels_text">Dosyadan veya ZIP\'ten ekle</string>
<string name="open_file">Dosya Aç</string>
<string name="add_from_qr">QR kodundan ekle</string>
<string name="qr_scan">QR kodu tarat</string>
<string name="tunnel_name">Tünel adı</string>
<string name="add_tunnel">Tünel ekle</string>
<string name="exclude">Hariç tut</string>
<string name="include">Dahil et</string>
<string name="tunnel_all">Tüm uygulamaları dahil et</string>
<string name="config_changes_saved">Yapılandırma değişiklikleri kaydedildi.</string>
<string name="save_changes">Kaydet</string>
<string name="icon">Simge</string>
<string name="no_thanks">Hayır, teşekkürler</string>
<string name="turn_on"></string>
<string name="map">Harita</string>
<string name="public_key">Genel anahtar</string>
<string name="addresses">Adresler</string>
<string name="dns_servers">DNS sunucuları</string>
<string name="mtu">MTU</string>
<string name="peer">Eş (peer)</string>
<string name="allowed_ips">İzin verilen IP\'ler</string>
<string name="endpoint">Uç nokta (endpoint)</string>
<string name="name">Ad</string>
<string name="restart">Tüneli Yeniden Başlat</string>
<string name="vpn_connection_failed">Bağlantı kurulamadı</string>
<string name="always_on_vpn_support">Her Zaman Açık VPN\'e İzin Ver</string>
<string name="location_services_not_detected">Konum Hizmetleri Algılanmadı</string>
<string name="hint_search_packages">Uygulama arayın</string>
<string name="clear_icon">Simgeyi Temizle</string>
<string name="search_icon">Simge Ara</string>
<string name="attempt_connection">Bağlantı kurulmaya çalışılıyor..</string>
<string name="vpn_starting">VPN başlatılıyor</string>
<string name="db_name">wg-tunnel-db</string>
<string name="scanning_qr">QR taranıyor</string>
<string name="none">Güvenilir Wi-Fi adı yok</string>
<string name="other">Diğer</string>
<string name="auto_tunneling">Otomatik tünelleme</string>
<string name="vpn_on">VPN açık</string>
<string name="vpn_off">VPN kapalı</string>
<string name="default_vpn_on">Birincil VPN açık</string>
<string name="default_vpn_off">Birincil VPN kapalı</string>
<string name="create_import">Sıfırdan oluştur</string>
<string name="turn_off_auto">Eylem, otomatik tünelin devre dışı bırakılmasını veya duraklatılmasını gerektirir</string>
<string name="turn_on_tunnel">Eylem aktif tünel gerektirir</string>
<string name="add_peer">Eş (peer) ekle</string>
<string name="done">Tamam</string>
<string name="interface_">Arayüz</string>
<string name="rotate_keys">Tuşları döndür</string>
<string name="private_key">Özel anahtar</string>
<string name="copy_public_key">Genel anahtarı kopyala</string>
<string name="base64_key">Base64 anahtarı</string>
<string name="comma_separated_list">Virgül ile ayrılmış liste ekleyin</string>
<string name="listen_port">Bağlantı noktası</string>
<string name="random">(Rastgele)</string>
<string name="optional">(İsteğe bağlı)</string>
<string name="optional_no_recommend">(İsteğe bağlı, önerilmez)</string>
<string name="preshared_key">Ön paylaşımlı anahtar</string>
<string name="seconds">saniye</string>
<string name="persistent_keepalive">Kalıcı olarak canlı tutma</string>
<string name="cancel">İptal</string>
<string name="error_authentication_failed">Kimlik doğrulama başarısız oldu</string>
<string name="error_authorization_failed">Yetkilendirilemedi</string>
<string name="enabled_app_shortcuts">Uygulama kısayollarını etkinleştir</string>
<string name="export_configs">Yapılandırmaları dışa aktar</string>
<string name="location_services_required">Gerekli konum hizmetleri</string>
<string name="background_location_required">Arka plan konumu gerekli</string>
<string name="precise_location_required">Kesin konum gerekli</string>
<string name="unknown_error">Bilinmeyen bir hata oluştu</string>
<string name="exported_configs_message">Yapılandırmalar indirilenler\'e aktarıldı</string>
<string name="tunnel_on_wifi">Güvenilmeyen kablosuz internet bağlantısında tünel</string>
<string name="my_email" translatable="false">support@zaneschepke.com</string>
<string name="email_subject">WG Tunnel Desteği</string>
<string name="email_chooser">Bir e-posta gönderin…</string>
<string name="go">git</string>
<string name="docs_description">Belgeleri oku</string>
<string name="discord_description">Topluluğa katıl</string>
<string name="email_description">Bana e-posta gönder</string>
<string name="support_help_text">Sorun yaşıyorsanız, iyileştirme fikirleriniz varsa veya sadece etkileşimde bulunmak istiyorsanız, aşağıdaki kaynaklar mevcuttur:</string>
<string name="kernel">Çekirdek</string>
<string name="use_kernel">Çekirdek modülünü kullan</string>
<string name="error_ssid_exists">SSID zaten var</string>
<string name="error_root_denied">Kök kabuğu reddedildi</string>
<string name="error_no_file_explorer">Yüklü dosya gezgini yok</string>
<string name="error_invalid_code">Geçersiz QR kodu</string>
<string name="error_none">Hata yok</string>
<string name="location_services_missing_message">Uygulama, cihazınızda etkinleştirilen herhangi bir konum hizmeti algılamıyor. Cihaza bağlı olarak bu, güvenilmeyen Wi-Fi özelliğinin Wi-Fi adını okuyamamasına neden olabilir. Yine de devam etmek ister misiniz?</string>
<string name="auto_tunnel_title">Otomatik Tünel Hizmeti</string>
<string name="delete_tunnel">Tüneli sil</string>
<string name="delete_tunnel_message">Bu tüneli silmek istediğinizden emin misiniz?</string>
<string name="yes">Evet</string>
<string name="resume">Devam et</string>
<string name="pause">Duraklat</string>
<string name="paused">duraklatıldı</string>
<string name="active">aktif</string>
<string name="tunneling_apps">Tünellenen uygulamalar</string>
<string name="included">dahil edildi</string>
<string name="excluded">hariç tutuldu</string>
<string name="all">tümü</string>
<string name="always_on_disabled">Her zaman açık VPN bir tünel başlatmaya çalıştı, ancak bu özellik ayarlarda devre dışı bırakıldı.</string>
<string name="no_email_detected">E-posta uygulaması algılanmadı</string>
<string name="no_browser_detected">Tarayıcı algılanmadı</string>
<string name="logs_saved">İndirilenlere kaydedilen günlükler</string>
<string name="open_issue">Bir sorun bildir</string>
<string name="read_logs">Günlükleri oku</string>
<string name="auto">(Otomatik)</string>
<string name="config_parse_error">Yapılandırma kaydedilemedi</string>
<string name="incorrect_pin">PIN kodu yanlış</string>
<string name="pin_created">Pin başarıyla oluşturuldu</string>
<string name="enter_pin">PIN kodunuzu girin</string>
<string name="create_pin">PIN kodu oluştur</string>
<string name="enable_app_lock">Uygulama kilidini etkinleştir</string>
<string name="restart_on_ping">Ping başarısız olduğunda yeniden başlat (beta)</string>
<string name="mobile_data_tunnel">Mobil veri tüneli olarak ayarla</string>
<string name="set_primary_tunnel">Birincil tünel olarak ayarla</string>
<string name="use_tunnel_on_wifi_name">Wi-Fi adında tünel kullan</string>
<string name="no_wifi_names_configured">Bu tünel için yapılandırılmış Wi-Fi adı yok</string>
<string name="general">Genel</string>
<string name="edit_tunnel">Tüneli düzenle</string>
<string name="disabled">Devre dışı</string>
<string name="auto_on">Otomatik ayarlamayı sürdür</string>
<string name="auto_off">Otomatik ayarlamayı duraklat</string>
<string name="auto_tun_on">Otomatik tüneli sürdür</string>
<string name="auto_tun_off">Otomatik tüneli duraklat</string>
<string name="version">Sürüm</string>
<string name="mode">Mod</string>
<string name="userspace">Kullanıcı alanı</string>
<string name="settings">Ayarlar</string>
<string name="support">Destek</string>
</resources>
-7
View File
@@ -1,11 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black_background">#FF1c1b20</color> <color name="black_background">#FF1c1b20</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources> </resources>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_channel_background">#1D1A20</color>
</resources>
+28 -38
View File
@@ -4,32 +4,24 @@
<string name="vpn_channel_name">VPN Notification Channel</string> <string name="vpn_channel_name">VPN Notification Channel</string>
<string name="watcher_channel_id">Watcher Channel</string> <string name="watcher_channel_id">Watcher Channel</string>
<string name="watcher_channel_name">Watcher Notification Channel</string> <string name="watcher_channel_name">Watcher Notification Channel</string>
<string name="foreground_file">FOREGROUND_FILE</string>
<string name="github_url" translatable="false">https://github.com/zaneschepke/wgtunnel/issues</string> <string name="github_url" translatable="false">https://github.com/zaneschepke/wgtunnel/issues</string>
<string name="docs_url" translatable="false">https://zaneschepke.com/wgtunnel-docs/overview.html</string> <string name="docs_url" translatable="false">https://zaneschepke.com/wgtunnel-docs/overview.html</string>
<string name="privacy_policy_url" translatable="false">https://zaneschepke.com/wgtunnel-docs/privacypolicy.html</string> <string name="privacy_policy_url" translatable="false">https://zaneschepke.com/wgtunnel-docs/privacypolicy.html</string>
<string name="error_file_extension">File is not a .conf or .zip</string> <string name="error_file_extension">File is not a .conf or .zip</string>
<string name="turn_off_tunnel">Action requires tunnel off</string> <string name="turn_off_tunnel">Action requires tunnel off</string>
<string name="no_tunnels">No tunnels added yet!</string> <string name="no_tunnels">No tunnels added yet!</string>
<string name="tunnel_exists">Tunnel name already exists</string>
<string name="discord_url" translatable="false">https://discord.gg/rbRRNh6H7V</string> <string name="discord_url" translatable="false">https://discord.gg/rbRRNh6H7V</string>
<string name="watcher_notification_title">Watcher Service</string>
<string name="watcher_notification_text_active">Monitoring network state changes: active</string> <string name="watcher_notification_text_active">Monitoring network state changes: active</string>
<string name="watcher_notification_text_paused">Monitoring network state changes: paused</string> <string name="watcher_notification_text_paused">Monitoring network state changes: paused</string>
<string name="tunnel_start_title">VPN connected</string> <string name="tunnel_start_title">VPN connected</string>
<string name="tunnel_start_text">Connected to tunnel</string> <string name="tunnel_start_text">Connected to tunnel</string>
<string name="vpn_permission_required">VPN permission is required for the app to work properly. If this permission is not launching, please disable \"Always-on VPN\" in your phone settings for the official WireGuard mobile app and try again.</string>
<string name="notification_permission_required">Notifications permission required.</string> <string name="notification_permission_required">Notifications permission required.</string>
<string name="open_settings">Open Settings</string>
<string name="add_trusted_ssid">Add trusted wifi name</string> <string name="add_trusted_ssid">Add trusted wifi name</string>
<string name="tunnels">Tunnels</string> <string name="tunnels">Tunnels</string>
<string name="enable_auto_tunnel">Start auto-tunneling</string> <string name="enable_auto_tunnel">Start auto-tunneling</string>
<string name="disable_auto_tunnel">Stop auto-tunneling</string> <string name="disable_auto_tunnel">Stop auto-tunneling</string>
<string name="tunnel_mobile_data">Tunnel on mobile data</string> <string name="tunnel_mobile_data">Tunnel on mobile data</string>
<string name="background_location_reason">\"Allow all the time\" location permission is required for retrieving Wi-Fi SSID in the background. Permission is needed for this feature.</string>
<string name="location_permission_reason">Location permission is required for this feature to work properly.</string>
<string name="one_tunnel_required">At least one tunnel required to use this feature</string> <string name="one_tunnel_required">At least one tunnel required to use this feature</string>
<string name="retry">"Retry"</string>
<string name="privacy_policy">View Privacy Policy</string> <string name="privacy_policy">View Privacy Policy</string>
<string name="okay">Okay</string> <string name="okay">Okay</string>
<string name="tunnel_on_ethernet">Tunnel on ethernet</string> <string name="tunnel_on_ethernet">Tunnel on ethernet</string>
@@ -38,12 +30,10 @@
<string name="thank_you">Thank you for using WG Tunnel!</string> <string name="thank_you">Thank you for using WG Tunnel!</string>
<string name="trusted_ssid_empty_description">Enter SSID</string> <string name="trusted_ssid_empty_description">Enter SSID</string>
<string name="trusted_ssid_value_description">Submit SSID</string> <string name="trusted_ssid_value_description">Submit SSID</string>
<string name="config_validation" translatable="false">[Interface]</string>
<string name="add_tunnels_text">Add from file or zip</string> <string name="add_tunnels_text">Add from file or zip</string>
<string name="open_file">File Open</string> <string name="open_file">File Open</string>
<string name="add_from_qr">Add from QR code</string> <string name="add_from_qr">Add from QR code</string>
<string name="qr_scan">QR Scan</string> <string name="qr_scan">QR Scan</string>
<string name="tunnel_edit">Tunnel Edit</string>
<string name="tunnel_name">Tunnel Name</string> <string name="tunnel_name">Tunnel Name</string>
<string name="add_tunnel">Add Tunnel</string> <string name="add_tunnel">Add Tunnel</string>
<string name="exclude">Exclude</string> <string name="exclude">Exclude</string>
@@ -55,34 +45,18 @@
<string name="no_thanks">No thanks</string> <string name="no_thanks">No thanks</string>
<string name="turn_on">Turn on</string> <string name="turn_on">Turn on</string>
<string name="map">Map</string> <string name="map">Map</string>
<string name="bad_config">Bad config. Please try again.</string>
<string name="config_interface">Interface</string>
<string name="public_key">Public key</string> <string name="public_key">Public key</string>
<string name="barcode_downloading">Waiting for the Barcode UI module to be downloaded.</string>
<string name="barcode_downloading_message">Barcode module downloading. Try again.</string>
<string name="addresses">Addresses</string> <string name="addresses">Addresses</string>
<string name="dns_servers">DNS servers</string> <string name="dns_servers">DNS servers</string>
<string name="mtu">MTU</string> <string name="mtu">MTU</string>
<string name="peer">Peer</string> <string name="peer">Peer</string>
<string name="allowed_ips">Allowed IPs</string> <string name="allowed_ips">Allowed IPs</string>
<string name="endpoint">Endpoint</string> <string name="endpoint">Endpoint</string>
<string name="transfer">Transfer</string>
<string name="last_handshake">Last handshake</string>
<string name="name">Name</string> <string name="name">Name</string>
<string name="restart">Restart Tunnel</string> <string name="restart">Restart Tunnel</string>
<string name="vpn_connection_failed">Connection failed</string> <string name="vpn_connection_failed">Connection failed</string>
<string name="failed_connection_to">Failed connection to -</string>
<string name="initial_connection_failure_message">Attempting to connect to server after 30 seconds of no response.</string>
<string name="lost_connection_failure_message">Attempting to reconnect to server after more than one minute of no response.</string>
<string name="always_on_vpn_support">Allow Always-On VPN </string> <string name="always_on_vpn_support">Allow Always-On VPN </string>
<string name="select_tunnel_message">Please select a tunnel first</string>
<string name="location_services_not_detected">Location Services Not Detected</string> <string name="location_services_not_detected">Location Services Not Detected</string>
<string name="check_again">Check again</string>
<string name="detecting_location_services_disabled">Detecting Location Services disabled</string>
<string name="precise_location_message">This feature requires precise location to access Wi-Fi SSID name. Please enable precise location here or in the app settings.</string>
<string name="request">Request</string>
<string name="toggle_vpn">Toggle VPN</string>
<string name="no_tunnel_available">No tunnels available</string>
<string name="hint_search_packages">Search packages</string> <string name="hint_search_packages">Search packages</string>
<string name="clear_icon">Clear Icon</string> <string name="clear_icon">Clear Icon</string>
<string name="search_icon">Search Icon</string> <string name="search_icon">Search Icon</string>
@@ -90,13 +64,9 @@
<string name="vpn_starting">VPN starting</string> <string name="vpn_starting">VPN starting</string>
<string name="db_name">wg-tunnel-db</string> <string name="db_name">wg-tunnel-db</string>
<string name="scanning_qr">Scanning for QR</string> <string name="scanning_qr">Scanning for QR</string>
<string name="qr_result_failed">QR scan failed</string>
<string name="none">No trusted wifi names</string> <string name="none">No trusted wifi names</string>
<string name="never">Never</string>
<string name="stream_failed">Failed to open file stream.</string>
<string name="other">Other</string> <string name="other">Other</string>
<string name="auto_tunneling">Auto-tunneling</string> <string name="auto_tunneling">Auto-tunneling</string>
<string name="select_tunnel">Select tunnel to use</string>
<string name="vpn_on">VPN on</string> <string name="vpn_on">VPN on</string>
<string name="vpn_off">VPN off</string> <string name="vpn_off">VPN off</string>
<string name="default_vpn_on">Primary VPN on</string> <string name="default_vpn_on">Primary VPN on</string>
@@ -124,7 +94,7 @@
<string name="error_authorization_failed">Failed to authorize</string> <string name="error_authorization_failed">Failed to authorize</string>
<string name="enabled_app_shortcuts">Enable app shortcuts</string> <string name="enabled_app_shortcuts">Enable app shortcuts</string>
<string name="export_configs">Export configs</string> <string name="export_configs">Export configs</string>
<string name="battery_saver">Battery saver (beta)</string> <string name="export_configs_failed">Failed to export configs</string>
<string name="location_services_required">Location services required</string> <string name="location_services_required">Location services required</string>
<string name="background_location_required">Background location required</string> <string name="background_location_required">Background location required</string>
<string name="precise_location_required">Precise location required</string> <string name="precise_location_required">Precise location required</string>
@@ -135,20 +105,16 @@
<string name="email_subject">WG Tunnel Support</string> <string name="email_subject">WG Tunnel Support</string>
<string name="email_chooser">Send an email…</string> <string name="email_chooser">Send an email…</string>
<string name="go">go</string> <string name="go">go</string>
<string name="docs_description">Read the docs (WIP)</string> <string name="docs_description">Read the docs</string>
<string name="discord_description">Join the community</string> <string name="discord_description">Join the community</string>
<string name="email_description">Send me an email</string> <string name="email_description">Send me an email</string>
<string name="support_help_text">If you are experiencing issues, have improvement ideas, or just want to engage, the following resources are available:</string> <string name="support_help_text">If you are experiencing issues, have improvement ideas, or just want to engage, the following resources are available:</string>
<string name="kernel">Kernel</string>
<string name="use_kernel">Use kernel module</string> <string name="use_kernel">Use kernel module</string>
<string name="error_ssid_exists">SSID already exists</string> <string name="error_ssid_exists">SSID already exists</string>
<string name="error_root_denied">Root shell denied</string> <string name="error_root_denied">Root shell denied</string>
<string name="error_no_file_explorer">No file explorer installed</string> <string name="error_no_file_explorer">No file explorer installed</string>
<string name="error_no_scan">No code scanned</string>
<string name="error_invalid_code">Invalid QR code</string> <string name="error_invalid_code">Invalid QR code</string>
<string name="error_none">No error</string> <string name="error_none">No error</string>
<string name="error_file_read">Failed to read file</string>
<string name="location_service_missing">Location Services Not Detected</string>
<string name="location_services_missing_message">The app is not detecting any location services enabled on your device. Depending on the device, this could cause the untrusted wifi feature to fail to read the wifi name. Would you like to continue anyways?</string> <string name="location_services_missing_message">The app is not detecting any location services enabled on your device. Depending on the device, this could cause the untrusted wifi feature to fail to read the wifi name. Would you like to continue anyways?</string>
<string name="auto_tunnel_title">Auto-tunnel Service</string> <string name="auto_tunnel_title">Auto-tunnel Service</string>
<string name="delete_tunnel">Delete tunnel</string> <string name="delete_tunnel">Delete tunnel</string>
@@ -174,7 +140,7 @@
<string name="pin_created">Pin successfully created</string> <string name="pin_created">Pin successfully created</string>
<string name="enter_pin">Enter your pin</string> <string name="enter_pin">Enter your pin</string>
<string name="create_pin">Create pin</string> <string name="create_pin">Create pin</string>
<string name="enable_app_lock">Enabled app lock</string> <string name="enable_app_lock">Enable app lock</string>
<string name="restart_on_ping">Restart on ping fail (beta)</string> <string name="restart_on_ping">Restart on ping fail (beta)</string>
<string name="mobile_data_tunnel">Set as mobile data tunnel</string> <string name="mobile_data_tunnel">Set as mobile data tunnel</string>
<string name="set_primary_tunnel">Set as primary tunnel</string> <string name="set_primary_tunnel">Set as primary tunnel</string>
@@ -182,10 +148,34 @@
<string name="no_wifi_names_configured">No wifi names configured for this tunnel</string> <string name="no_wifi_names_configured">No wifi names configured for this tunnel</string>
<string name="general">General</string> <string name="general">General</string>
<string name="edit_tunnel">Edit tunnel</string> <string name="edit_tunnel">Edit tunnel</string>
<string name="tunnel">Tunnel</string>
<string name="disabled">disabled</string> <string name="disabled">disabled</string>
<string name="auto_on">Resume auto tun</string> <string name="auto_on">Resume auto tun</string>
<string name="auto_off">Pause auto tun</string> <string name="auto_off">Pause auto tun</string>
<string name="auto_tun_on">Resume auto-tunnel</string> <string name="auto_tun_on">Resume auto-tunnel</string>
<string name="auto_tun_off">Pause auto-tunnel</string> <string name="auto_tun_off">Pause auto-tunnel</string>
<string name="version">Version</string>
<string name="mode">Mode</string>
<string name="userspace">Userspace</string>
<string name="settings">Settings</string>
<string name="support">Support</string>
<string name="backend">Backend</string>
<string name="kernel">Kernel</string>
<string name="use_amnezia">"Use Amnezia userspace "</string>
<string name="junk_packet_count">Junk packet count</string>
<string name="junk_packet_minimum_size">Junk packet minimum size</string>
<string name="junk_packet_maximum_size">Junk packet maximum size</string>
<string name="init_packet_junk_size">Init packet junk size</string>
<string name="response_packet_junk_size">Response packet junk size</string>
<string name="init_packet_magic_header">Init packet magic header</string>
<string name="response_packet_magic_header">Response packet magic header</string>
<string name="transport_packet_magic_header">Transport packet magic header</string>
<string name="underload_packet_magic_header">Underload packet magic header</string>
<string name="telegram_url" translatable="false">https://t.me/wgtunnel</string>
<string name="unsure_how">if you are unsure how to proceed</string>
<string name="see_the">See the</string>
<string name="getting_started_url" translatable="false">https://zaneschepke.com/wgtunnel-docs/getting-started.html</string>
<string name="getting_started_guide">getting started guide</string>
<string name="amnezia" translatable="false">Amnezia</string>
<string name="wireguard" translatable="false">WireGuard</string>
<string name="error_file_format">Invalid tunnel config format</string>
</resources> </resources>
+2 -2
View File
@@ -1,7 +1,7 @@
object Constants { object Constants {
const val VERSION_NAME = "3.4.0" const val VERSION_NAME = "3.4.4"
const val JVM_TARGET = "17" const val JVM_TARGET = "17"
const val VERSION_CODE = 34000 const val VERSION_CODE = 34400
const val TARGET_SDK = 34 const val TARGET_SDK = 34
const val MIN_SDK = 26 const val MIN_SDK = 26
const val APP_ID = "com.zaneschepke.wireguardautotunnel" const val APP_ID = "com.zaneschepke.wireguardautotunnel"
@@ -0,0 +1,3 @@
Verbesserungen:
- Berechtigungsfehler auf Android Versionen vor 9 behoben
- Andere Optimierungen
@@ -0,0 +1,5 @@
Verbesserungen:
- Tunnelstatistiken zum Hauptbildschirm hinzugefügt
- Navigation der Einstellungen auf AndroidTV verbessert
- Vibration von Benachrichtigungen entfernt
- Verschiedene andere Fehlerbehebungen
@@ -0,0 +1,5 @@
Verbesserungen:
- Unterstützung für Tunnelung nur bei Verwendung von Mobilen Daten
- Verbesserungen der Support Oberfläche
- Aktualisierung der Ressourcenlinks
- Verschiedene andere Fehlerbehebungen
@@ -0,0 +1,5 @@
Verbesserungen:
- Grundlegende Unterstützung für WireGuard Kernelmodus hinzugefügt
- Verbesserung des Positionsanfrage
- Behebung eines Berechtigungsfehlers bei Auto-Tunneln
- Verschiedene andere GUI Fehlerbehebungen
@@ -0,0 +1,2 @@
Fehlerbehebungen:
- Android 14 Vordergrundberechtigungen
@@ -0,0 +1,7 @@
Verbesserungen:
- Refaktorierung der Zustandsverwaltung
- Verbesserung der AndroidTV Navigation
- Verbesserung der Auto-Tunnel Effizienz
- Navigation verbessert
- Auto-Tunnel Pausefunktion
- Viele Fehlerbehebungen
@@ -0,0 +1,7 @@
Verbesserungen:
- Refaktorisierung des Zustandsmanagements
- Verbesserung der AndroidTV Navigation
- Verbesserung der Auto-Tunnel Effizienz
- Verbesserung der Navigation
- Auto-Tunnel Pause Funktion
- Viele Fehlerbehebungen
@@ -0,0 +1,8 @@
Verbesserungen:
- Refaktorisierung des Zustandsmanagements
- Verbesserung der AndroidTV Navigation
- Verbesserung der Auto-Tunnel Effizienz
- Verbesserung der Navigation
- Auto-Tunnel Pause Funktion
- Reparatur des Auto-Tunnel starts im Vordergrund
- Viele Fehlerbehebungen
@@ -0,0 +1,7 @@
Verbesserungen:
- Bestätigung beim Tunnel löschen hinzugefügt
- Hintergrundberechtigung für Energiesparmodus hinzugefügt
Fehlerbehebungen:
- Einfrieren der App bei Tunnel deaktivierung
- Fehler im E-Mail Empfängerfeld
- Konfigurationsbearbeitung mit leerem DNS Feld
@@ -0,0 +1,3 @@
Verbesserungen:
- Erstellte Konfiguration wurde nicht gespeichert
- Version angehoben
@@ -0,0 +1,2 @@
Was ist neu:
- Dies ist eine CI Test version

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