Compare commits

..

46 Commits

Author SHA1 Message Date
dependabot[bot] 285aa5fc7e build(deps): bump hiltAndroid from 2.54 to 2.55
Bumps `hiltAndroid` from 2.54 to 2.55.

Updates `com.google.dagger:hilt-android` from 2.54 to 2.55
- [Release notes](https://github.com/google/dagger/releases)
- [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md)
- [Commits](https://github.com/google/dagger/compare/dagger-2.54...dagger-2.55)

Updates `com.google.dagger:hilt-android-compiler` from 2.54 to 2.55
- [Release notes](https://github.com/google/dagger/releases)
- [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md)
- [Commits](https://github.com/google/dagger/compare/dagger-2.54...dagger-2.55)

Updates `com.google.dagger.hilt.android` from 2.54 to 2.55
- [Release notes](https://github.com/google/dagger/releases)
- [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md)
- [Commits](https://github.com/google/dagger/compare/dagger-2.54...dagger-2.55)

---
updated-dependencies:
- dependency-name: com.google.dagger:hilt-android
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: com.google.dagger:hilt-android-compiler
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: com.google.dagger.hilt.android
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-10 13:11:13 +00:00
Zane Schepke 7b7c8f6e8c ci: fix fdroid dispatch 2025-01-02 22:58:04 -05:00
Zane Schepke 7cbe3311c9 chore: bump version 2025-01-02 22:45:43 -05:00
Zane Schepke a5898d4ad1 fix: make sure ping shuts down 2025-01-02 17:10:32 -05:00
Zane Schepke 48c01aa0e3 Merge branch 'main' of https://github.com/zaneschepke/wgtunnel 2025-01-02 16:47:30 -05:00
Zane Schepke b1dc6c5d59 fix: make ping aware of network availability
add basic tunnel error messages
2025-01-02 16:47:23 -05:00
Weblate (bot) 2a7397edba Translations update from Hosted Weblate (#531)
Co-authored-by: Matthaiks <kitynska@gmail.com>
Co-authored-by: solokot <solokot@gmail.com>
Co-authored-by: Kachelkaiser <kachelkaiser@outlook.com>
Co-authored-by: CyanWolf <hydemr@pm.me>
Co-authored-by: Deleted User <noreply+48943@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
2025-01-02 16:35:43 -05:00
Zane Schepke 53c09267df fix: ethernet monitoring and auto tunnel override 2025-01-01 19:14:56 -05:00
Zane Schepke f772dc0f8a feat!: move ping from auto tunnel to tunnel
Ping feature will not be tunnel specific and work without auto tunneling being active
2025-01-01 18:16:26 -05:00
Zane Schepke ba064b267f fix: try to query ssid if we don't have a valid one yet 2025-01-01 02:38:59 -05:00
Zane Schepke 2c04b9d69c fix: active tunnel bug 2025-01-01 01:32:24 -05:00
Zane Schepke 48adaae0a0 docs: fix readme 2025-01-01 00:43:28 -05:00
Zane Schepke 8e49a4d343 docs: fix readme title 2025-01-01 00:41:51 -05:00
Zane Schepke 67f53915cc feat(lang): weblate localization changes (#530)
Co-authored-by: Matthaiks <kitynska@gmail.com>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: solokot <solokot@gmail.com>
Co-authored-by: Kachelkaiser <kachelkaiser@outlook.com>
Co-authored-by: L.y <lycidias@gmail.com>
2025-01-01 00:40:59 -05:00
Weblate (bot) 156c5478ae Translations update from Hosted Weblate (#522)
Co-authored-by: Zane Schepke <zanecschepke@gmail.com>
Co-authored-by: Matthaiks <kitynska@gmail.com>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
2025-01-01 00:28:11 -05:00
Zane Schepke 85319ba874 refactor: navigation 2025-01-01 00:08:33 -05:00
Zane Schepke ab858ab59e feat: config screen peer action to exclude private ips
closes #402
2024-12-31 22:31:31 -05:00
Zane Schepke 4196a543b2 fix: location disclosure screen navigation bug 2024-12-31 19:13:15 -05:00
Zane Schepke 02a8db0f9a feat: add setting for debounce delay tuning
closes #493
2024-12-31 19:02:44 -05:00
Zane Schepke eb7c6ca7ba ci: fix apk upload github release 2024-12-31 15:25:07 -05:00
Zane Schepke 8ffe145ade fix: android 9 crash bug 2024-12-31 14:19:57 -05:00
Zane Schepke 4bdc43deb3 ci: move versionCode bump to build 2024-12-31 00:47:09 -05:00
Zane Schepke 83c0ff497b ci: fix github publish 2024-12-31 00:27:29 -05:00
Zane Schepke c8ac40d370 fix: improve tile sync
#491
2024-12-31 00:17:34 -05:00
Zane Schepke 4dc91b5fae fix: create tunnel from scratch bug
closes #524
2024-12-28 23:46:21 -05:00
Zane Schepke 7a3627bf6a fix: remove unused shortcuts dep 2024-12-27 15:03:38 -05:00
Zane Schepke e86bba6a0e docs: move chats 2024-12-27 13:52:20 -05:00
Zane Schepke ac846bfdc9 docs: update screenshots and readme 2024-12-27 13:49:09 -05:00
Zane Schepke 11e385d350 feat: toggle to set amnezia/wg compatibility values
closes #469
closes #518
2024-12-27 02:52:44 -05:00
Zane Schepke b2e266fc9f fix: toggle state bug 2024-12-26 22:56:09 -05:00
Zane Schepke 7cbbf00e52 fix: search case sensitive
closes #515
2024-12-26 22:42:43 -05:00
Zane Schepke 6a0b2b678f fix: banner background color removed 2024-12-26 22:39:55 -05:00
Zane Schepke 1bfea142ad ci: fix fdroid publish 2024-12-26 22:34:00 -05:00
Zane Schepke 6fdf80e84d chore: remove crowdin 2024-12-26 22:17:11 -05:00
Zane Schepke b7ec9e2696 chore: fix language mapping 2024-12-26 20:36:30 -05:00
Zane Schepke 6502f553ac Update Crowdin configuration file 2024-12-26 20:27:52 -05:00
Zane Schepke 946ac71fb1 Update Crowdin configuration file 2024-12-26 19:51:16 -05:00
Zane Schepke dca8b50083 chore: crowdin yaml fix 2024-12-26 18:44:59 -05:00
Zane Schepke 1ab611aa7f chore: cleanup unused strings 2024-12-26 14:33:08 -05:00
Zane Schepke 40a2732ec9 Update Crowdin configuration file 2024-12-26 13:41:42 -05:00
Zane Schepke ffcb4f6ed8 fix: weblate conflicts (#519)
Co-authored-by: lateweb <weblate@techkoala.net>
Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Matthaiks <kitynska@gmail.com>
Co-authored-by: Tom <weblate.delicate088@passinbox.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: MouaisTe44 <r.craft.212121@gmail.com>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Julian Madsen <julian@sopyt.com>
Co-authored-by: tomo90 <ivanek.tomas@gmail.com>
Co-authored-by: solokot <solokot@gmail.com>
Co-authored-by: Marco <16669146+RodoMa92@users.noreply.github.com>
2024-12-26 13:32:29 -05:00
Zane Schepke 7b6bdec133 fix!: notification channel id should not be localized 2024-12-26 12:49:00 -05:00
Zane Schepke 6ece224592 fix: translation bug 2024-12-26 12:23:36 -05:00
Zane Schepke 66051e4a0a ci: minor refactor 2024-12-25 23:19:31 -05:00
Zane Schepke c8298297e2 ci: refactor build and publish 2024-12-25 22:43:26 -05:00
Zane Schepke 83bec24ca9 fix: reproducibility 2024-12-25 21:47:56 -05:00
130 changed files with 2338 additions and 942 deletions
+125
View File
@@ -0,0 +1,125 @@
name: build
on:
workflow_dispatch:
inputs:
build_type:
type: choice
description: "Build type"
required: true
default: debug
options:
- debug
- prerelease
- nightly
- release
secrets:
SIGNING_KEY_ALIAS:
required: false
SIGNING_KEY_PASSWORD:
required: false
SIGNING_STORE_PASSWORD:
required: false
SERVICE_ACCOUNT_JSON:
required: false
KEYSTORE:
required: false
workflow_call:
inputs:
build_type:
type: string
description: "Build type"
required: true
default: debug
secrets:
SIGNING_KEY_ALIAS:
required: false
SIGNING_KEY_PASSWORD:
required: false
SIGNING_STORE_PASSWORD:
required: false
SERVICE_ACCOUNT_JSON:
required: false
KEYSTORE:
required: false
env:
UPLOAD_DIR_ANDROID: android_artifacts
jobs:
build:
runs-on: ubuntu-latest
env:
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
KEY_STORE_FILE: 'android_keystore.jks'
KEY_STORE_LOCATION: ${{ github.workspace }}/app/keystore/
outputs:
UPLOAD_DIR_ANDROID: ${{ env.UPLOAD_DIR_ANDROID }}
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
cache: gradle
- name: Grant execute permission for gradlew
run: chmod +x gradlew
# Here we need to decode keystore.jks from base64 string and place it
# in the folder specified in the release signing configuration
- name: Decode Keystore
id: decode_keystore
uses: timheuer/base64-to-file@v1.2
with:
fileName: ${{ env.KEY_STORE_FILE }}
fileDir: ${{ env.KEY_STORE_LOCATION }}
encodedString: ${{ secrets.KEYSTORE }}
# create keystore path for gradle to read
- name: Create keystore path env var
if: ${{ inputs.build_type != 'debug' }}
run: |
store_path=${{ env.KEY_STORE_LOCATION }}${{ env.KEY_STORE_FILE }}
echo "KEY_STORE_PATH=$store_path" >> $GITHUB_ENV
- name: Create service_account.json
if: ${{ inputs.build_type != 'debug' }}
id: createServiceAccount
run: echo '${{ secrets.ANDROID_SERVICE_ACCOUNT_JSON }}' > service_account.json
- name: Build Fdroid Release APK
if: ${{ inputs.build_type == 'release' }}
run: ./gradlew :app:assembleFdroidRelease --info
- name: Build Fdroid Prerelease APK
if: ${{ inputs.build_type == 'prerelease' }}
run: ./gradlew :app:assembleFdroidPrerelease --info
- name: Build Fdroid Nightly APK
if: ${{ inputs.build_type == 'nightly' }}
run: ./gradlew :app:assembleFdroidNightly --info
- name: Build Debug APK
if: ${{ inputs.build_type == 'debug' }}
run: ./gradlew :app:assembleFdroidDebug --stacktrace
# bump versionCode for nightly and prerelease builds
- name: Commit and push versionCode changes
if: ${{ inputs.build_type == 'nightly' || inputs.build_type == 'prerelease' }}
run: |
git config --global user.name 'GitHub Actions'
git config --global user.email 'actions@github.com'
git add versionCode.txt
git commit -m "Automated build update"
- name: Get release apk path
id: apk-path
run: echo "path=$(find . -regex '^.*/build/outputs/apk/fdroid/${{ inputs.build_type }}/.*\.apk$' -type f | head -1 | tail -c+2)" >> $GITHUB_OUTPUT
- name: Upload release apk
uses: actions/upload-artifact@v4
with:
name: ${{ env.UPLOAD_DIR_ANDROID }}
path: ${{github.workspace}}/${{ steps.apk-path.outputs.path }}
retention-days: 1
@@ -1,11 +1,11 @@
name: ci-android
name: on-pr
on:
workflow_dispatch:
pull_request:
jobs:
format:
format_check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -1,4 +1,4 @@
name: release-android
name: publish
on:
schedule:
@@ -31,6 +31,8 @@ on:
required: false
default: nightly
workflow_call:
env:
UPLOAD_DIR_ANDROID: android_artifacts
jobs:
check_commits:
@@ -53,17 +55,22 @@ jobs:
# This script checks for commits newer than 23 hours ago
NEW_COMMITS=$(git rev-list --count --after="$(date -Iseconds -d '23 hours ago')" ${{ github.sha }})
echo "new_commits=$NEW_COMMITS" >> $GITHUB_OUTPUT
build:
needs: check_commits
if: ${{ inputs.release_type != 'none' }}
uses: ./.github/workflows/build.yml
secrets: inherit
with:
build_type: ${{ inputs.release_type == '' && 'nightly' || inputs.release_type }}
publish:
needs:
- check_commits
- build
if: ${{ needs.check_commits.outputs.has_new_commits > 0 && inputs.release_type != 'none' }}
name: Build Signed APK
name: publish-github
runs-on: ubuntu-latest
env:
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
KEY_STORE_FILE: 'android_keystore.jks'
KEY_STORE_LOCATION: ${{ github.workspace }}/app/keystore/
GH_USER: ${{ secrets.GH_USER }}
# GH needed for gh cli
GH_TOKEN: ${{ secrets.GH_TOKEN }}
@@ -71,29 +78,10 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
cache: gradle
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Install system dependencies
run: |
sudo apt update && sudo apt install -y gh apksigner
# Here we need to decode keystore.jks from base64 string and place it
# in the folder specified in the release signing configuration
- name: Decode Keystore
id: decode_keystore
uses: timheuer/base64-to-file@v1.2
with:
fileName: ${{ env.KEY_STORE_FILE }}
fileDir: ${{ env.KEY_STORE_LOCATION }}
encodedString: ${{ secrets.KEYSTORE }}
# update latest tag
- name: Set latest tag
uses: rickstaa/action-create-tag@v1
@@ -120,51 +108,12 @@ jobs:
fromTag: "latest"
writeToFile: false # we won't write to file, just output
# create keystore path for gradle to read
- name: Create keystore path env var
run: |
store_path=${{ env.KEY_STORE_LOCATION }}${{ env.KEY_STORE_FILE }}
echo "KEY_STORE_PATH=$store_path" >> $GITHUB_ENV
- name: Create service_account.json
id: createServiceAccount
run: echo '${{ secrets.SERVICE_ACCOUNT_JSON }}' > service_account.json
# Build and sign APK ("-x test" argument is used to skip tests)
# add fdroid flavor for apk upload
- name: Build Fdroid Release APK
if: ${{ inputs.release_type != '' && inputs.release_type == 'release' }}
run: ./gradlew :app:assembleFdroidRelease -x test
- name: Build Fdroid Prerelease APK
if: ${{ inputs.release_type != '' && inputs.release_type == 'prerelease' }}
run: ./gradlew :app:assembleFdroidPrerelease -x test
- name: Build Fdroid Nightly APK
if: ${{ inputs.release_type == '' || inputs.release_type == 'nightly' }}
run: ./gradlew :app:assembleFdroidNightly -x test
- if: ${{ inputs.release_type == '' || inputs.release_type == 'nightly' }}
run: echo "APK_PATH=$(find . -regex '^.*/build/outputs/apk/fdroid/nightly/.*\.apk$' -type f | head -1)" >> $GITHUB_ENV
- if: ${{ inputs.release_type != '' && inputs.release_type == 'release' }}
run: echo "APK_PATH=$(find . -regex '^.*/build/outputs/apk/fdroid/release/.*\.apk$' -type f | head -1)" >> $GITHUB_ENV
- if: ${{ inputs.release_type != '' && inputs.release_type == 'prerelease' }}
run: echo "APK_PATH=$(find . -regex '^.*/build/outputs/apk/fdroid/prerelease/.*\.apk$' -type f | head -1)" >> $GITHUB_ENV
- name: Get version code
if: ${{ inputs.release_type == 'release' }}
run: |
version_code=$(grep "VERSION_CODE" buildSrc/src/main/kotlin/Constants.kt | awk '{print $5}' | tr -d '\n')
echo "VERSION_CODE=$version_code" >> $GITHUB_ENV
- name: Commit and push versionCode changes
if: ${{ inputs.release_type == '' || inputs.release_type == 'nightly' || inputs.release_type == 'prerelease' }}
run: |
git config --global user.name 'GitHub Actions'
git config --global user.email 'actions@github.com'
git add versionCode.txt
git commit -m "Automated build update"
- name: Push changes
if: ${{ inputs.release_type == '' || inputs.release_type == 'nightly' || inputs.release_type == 'prerelease' }}
uses: ad-m/github-push-action@master
@@ -172,25 +121,14 @@ jobs:
github_token: ${{ secrets.GITHUB_TOKEN }}
branch: ${{ github.ref }}
# Save the APK after the Build job is complete to publish it as a Github release in the next job
- name: Upload APK
uses: actions/upload-artifact@v4.5.0
with:
name: wgtunnel
path: ${{ env.APK_PATH }}
- name: Make download dir
run: mkdir ${{ github.workspace }}/temp
- name: Download APK from build
- name: Download artifacts
uses: actions/download-artifact@v4
with:
name: wgtunnel
- name: Repository Dispatch for my F-Droid repo
uses: peter-evans/repository-dispatch@v3
if: ${{ inputs.release_type == 'release' }}
with:
token: ${{ secrets.PAT }}
repository: zaneschepke/fdroid
event-type: fdroid-update
name: ${{ env.UPLOAD_DIR_ANDROID }}
path: ${{ github.workspace }}/temp
# Setup TAG_NAME, which is used as a general "name"
- if: github.event_name == 'workflow_dispatch'
@@ -221,7 +159,9 @@ jobs:
- name: Get checksum
id: checksum
run: echo "checksum=$(apksigner verify -print-certs ${{ env.APK_PATH }} | grep -Po "(?<=SHA-256 digest:) .*" | tr -d "[:blank:]")" >> $GITHUB_OUTPUT
run: |
file_path=$(find ${{ github.workspace }}/temp -type f -iname "*.apk" | tail -n1)
echo "checksum=$(apksigner verify -print-certs $file_path | grep -Po "(?<=SHA-256 digest:) .*" | tr -d "[:blank:]")" >> $GITHUB_OUTPUT
- name: Create Release with Fastlane changelog notes
@@ -243,7 +183,21 @@ jobs:
draft: false
prerelease: ${{ inputs.release_type == 'prerelease' || inputs.release_type == '' || inputs.release_type == 'nightly' }}
make_latest: ${{ inputs.release_type == 'release' }}
files: ${{ github.workspace }}/${{ env.APK_PATH }}
files: |
${{ github.workspace }}/temp/*
publish-fdroid:
runs-on: ubuntu-latest
needs:
- build
if: inputs.release_type == 'release'
steps:
- name: Dispatch update for fdroid repo
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.PAT }}
repository: zaneschepke/fdroid
event-type: fdroid-update
publish-play:
if: ${{ inputs.track != 'none' && inputs.track != '' }}
+68 -53
View File
@@ -4,79 +4,107 @@ WG Tunnel
<div align="center">
An alternative Android client app for [WireGuard®](https://www.wireguard.com/)
and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/)
<br />
<br />
<a href="https://github.com/zaneschepke/wgtunnel/issues/new?assignees=zaneschepke&labels=bug&projects=&template=bug_report.md&title=%5BBUG%5D+-+Problem+with+app">Report a Bug</a>
·
<a href="https://github.com/zaneschepke/wgtunnel/issues/new?assignees=zaneschepke&labels=enhancement&projects=&template=feature_request.md&title=%5BFEATURE%5D+-+New+feature+request">Request a Feature</a>
·
<a href="https://github.com/zaneschepke/wgtunnel/discussions">Ask a Question</a>
</div>
<br/>
<div align="center">
[![Google Play](https://img.shields.io/badge/Google_Play-414141?style=for-the-badge&logo=google-play&logoColor=white)](https://play.google.com/store/apps/details?id=com.zaneschepke.wireguardautotunnel)
[![F-Droid](https://img.shields.io/static/v1?style=for-the-badge&message=F-Droid&color=1976D2&logo=F-Droid&logoColor=FFFFFF&label=)](https://f-droid.org/packages/com.zaneschepke.wireguardautotunnel/)
[![Personal](https://img.shields.io/static/v1?style=for-the-badge&message=Personal&color=1976D2&logo=F-Droid&logoColor=FFFFFF&label=)](https://github.com/zaneschepke/fdroid)
</div>
<div align="center">
[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?style=for-the-badge&logo=discord&logoColor=white)](https://discord.gg/rbRRNh6H7V)
[![Telegram](https://img.shields.io/badge/Telegram-2CA5E0?style=for-the-badge&logo=telegram&logoColor=white)](https://t.me/wgtunnel)
</div>
<div align="center">
<details open="open">
<summary>Table of Contents</summary>
- [About](#about)
- [Acknowledgements](#acknowledgements)
- [Screenshots](#screenshots)
- [Features](#features)
- [Building](#building)
- [Translation](#translation)
- [Contributing](#contributing)
[![Google Play](https://img.shields.io/badge/Google_Play-414141?style=for-the-badge&logo=google-play&logoColor=white)](https://play.google.com/store/apps/details?id=com.zaneschepke.wireguardautotunnel)
[![F-Droid](https://img.shields.io/static/v1?style=for-the-badge&message=F-Droid&color=1976D2&logo=F-Droid&logoColor=FFFFFF&label=)](https://f-droid.org/packages/com.zaneschepke.wireguardautotunnel/)
</details>
<div style="text-align: left;">
## About
Inspired by the official [wireguard-android](https://github.com/WireGuard/wireguard-android) app, WG Tunnel was created to address features and support missing from the official app. This app combines support for both [WireGuard®](https://www.wireguard.com/)
and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/), with its primary feature of auto-tunneling (on-demand tunneling).
</div>
<div style="text-align: left;">
<div align="left">
## Acknowledgements
This is an alternative Android Application for [WireGuard](https://www.wireguard.com/)
and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) with added
features. Built using the [wireguard-android](https://github.com/WireGuard/wireguard-android)
library and [Jetpack Compose](https://developer.android.com/jetpack/compose), this application was
inspired by the official [WireGuard Android](https://github.com/WireGuard/wireguard-android) app.
Thank you to the following:
</div>
- All of the users that have helped contribute to the project with ideas, translations, feedback, bug reports, testing, and donations.
- [WireGuard®](https://www.wireguard.com/) - © Jason A. Donenfeld (https://github.com/WireGuard/wireguard-android)
<div align="center">
- [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) - Amnezia Team (https://github.com/amnezia-vpn/amneziawg-android)
## Screenshots
<p float="center">
<img label="Main" style="padding-right:25px" src="fastlane/metadata/android/en-US/images/phoneScreenshots/main_screen.png" width="200" />
<img label="Config" style="padding-left:25px" src="fastlane/metadata/android/en-US/images/phoneScreenshots/config_screen.png" width="200" />
<img label="Settings" style="padding-left:25px" src="fastlane/metadata/android/en-US/images/phoneScreenshots/settings_screen.png" width="200" />
<img label="Support" style="padding-left:25px" src="fastlane/metadata/android/en-US/images/phoneScreenshots/support_screen.png" width="200" />
</p>
</div>
<div style="display: flex; flex-wrap: wrap; justify-content: center; gap: 10px;">
<img label="Main" src="fastlane/metadata/android/en-US/images/phoneScreenshots/main_screen.png" width="200" />
<img label="Settings" src="fastlane/metadata/android/en-US/images/phoneScreenshots/settings_screen.png" width="200" />
<img label="Auto" src="fastlane/metadata/android/en-US/images/phoneScreenshots/auto_screen.png" width="200" />
<img label="Config" src="fastlane/metadata/android/en-US/images/phoneScreenshots/config_screen.png" width="200" />
</div>
<div align="left">
## Inspiration
The original inspiration for this app came from the inconvenience of having to manually turn VPN off
and on while on different networks. This app was created to offer a free solution to this problem.
<div style="text-align: left;">
## Features
* Add tunnels via .conf file, zip, manual entry, or QR code
* Auto connect to tunnels based on Wi-Fi SSID, ethernet, or mobile data
* Add tunnels via .conf file, zip, manual entry, clipboard, or QR code
* Auto-tunnel based on Wi-Fi SSID, ethernet, or mobile data
* Split tunneling by application with search
* WireGuard support for kernel and userspace modes
* Support for kernel and userspace modes
* Amnezia support for userspace mode for DPI/censorship protection
* Pre/Post Up/Down scripts support for all modes on a rooted device
* Always-On VPN support
* Export Amnezia and WireGuard tunnels to zip
* Export tunnels to zip
* Quick tile support for tunnel toggling, auto-tunneling
* Static shortcuts support for tunnel toggling, auto-tunneling
* Shortcuts support for tunnel toggling, auto-tunneling
* Intent automation support for all tunnels
* In app VPN kill switch with LAN bypass
* Automatic auto-tunneling service and/or tunnel restart after reboot or app update
* Battery preservation measures
* Restart tunnel on ping failure (beta)
* Restart tunnel on ping failure
## Fdroid
## Building
Want updates faster?
```sh
git clone https://github.com/zaneschepke/wgtunnel
cd wgtunnel
```
Check out my personal [fdroid repository](https://github.com/zaneschepke/fdroid) to get updates the
moment they are released.
## Docs
Information about features, behaviors, and answers to common questions can be found in the
app [documentation](https://zaneschepke.com/wgtunnel-docs/overview.html).
The repository for these docs can be found [here](https://github.com/zaneschepke/wgtunnel-docs).
```sh
./gradlew assembleDebug
```
## Translation
@@ -86,19 +114,6 @@ Help translate WG Tunnel into your language
at [Hosted Weblate](https://hosted.weblate.org/engage/wg-tunnel/).\
[![Translation status](https://hosted.weblate.org/widgets/wg-tunnel/-/multi-auto.svg)](https://hosted.weblate.org/engage/wg-tunnel/)
## Building
```
$ git clone https://github.com/zaneschepke/wgtunnel
$ cd wgtunnel
```
And then build the app:
```
$ ./gradlew assembleDebug
```
## Contributing
Any contributions in the form of feedback, issues, code, or translations are welcome and much
+9 -2
View File
@@ -14,7 +14,7 @@ val versionCodeIncrement = with(getBuildTaskName().lowercase()) {
when {
this.contains(Constants.NIGHTLY) || this.contains(Constants.PRERELEASE) -> {
if (versionFile.exists()) {
versionFile.readText().toInt() + 1
versionFile.readText().trim().toInt() + 1
} else {
1
}
@@ -31,6 +31,14 @@ android {
generateLocaleConfig = true
}
// reproducibility
dependenciesInfo {
// Disables dependency metadata when building APKs.
includeInApk = false
// Disables dependency metadata when building Android App Bundles.
includeInBundle = false
}
defaultConfig {
applicationId = Constants.APP_ID
minSdk = Constants.MIN_SDK
@@ -211,7 +219,6 @@ dependencies {
// shortcuts
implementation(libs.androidx.core)
implementation(libs.androidx.core.google.shortcuts)
// splash
implementation(libs.androidx.core.splashscreen)
@@ -0,0 +1,274 @@
{
"formatVersion": 1,
"database": {
"version": 14,
"identityHash": "f2b260c389fb2e53216de40e4b1047f3",
"entities": [
{
"tableName": "Settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `is_amnezia_enabled` INTEGER NOT NULL DEFAULT false, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT false, `is_wifi_by_shell_enabled` INTEGER NOT NULL DEFAULT false, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT false, `is_vpn_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_lan_on_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `debounce_delay_seconds` INTEGER NOT NULL DEFAULT 3)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isAutoTunnelEnabled",
"columnName": "is_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnMobileDataEnabled",
"columnName": "is_tunnel_on_mobile_data_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "trustedNetworkSSIDs",
"columnName": "trusted_network_ssids",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isAlwaysOnVpnEnabled",
"columnName": "is_always_on_vpn_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnEthernetEnabled",
"columnName": "is_tunnel_on_ethernet_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isShortcutsEnabled",
"columnName": "is_shortcuts_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isTunnelOnWifiEnabled",
"columnName": "is_tunnel_on_wifi_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isKernelEnabled",
"columnName": "is_kernel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isRestoreOnBootEnabled",
"columnName": "is_restore_on_boot_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isMultiTunnelEnabled",
"columnName": "is_multi_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isAmneziaEnabled",
"columnName": "is_amnezia_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isWildcardsEnabled",
"columnName": "is_wildcards_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isWifiNameByShellEnabled",
"columnName": "is_wifi_by_shell_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isStopOnNoInternetEnabled",
"columnName": "is_stop_on_no_internet_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isVpnKillSwitchEnabled",
"columnName": "is_vpn_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isKernelKillSwitchEnabled",
"columnName": "is_kernel_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isLanOnKillSwitchEnabled",
"columnName": "is_lan_on_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "debounceDelaySeconds",
"columnName": "debounce_delay_seconds",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "3"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "TunnelConfig",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false, `is_primary_tunnel` INTEGER NOT NULL DEFAULT false, `am_quick` TEXT NOT NULL DEFAULT '', `is_Active` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `ping_interval` INTEGER DEFAULT null, `ping_cooldown` INTEGER DEFAULT null, `ping_ip` TEXT DEFAULT null, `is_ethernet_tunnel` INTEGER NOT NULL DEFAULT false)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "wgQuick",
"columnName": "wg_quick",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tunnelNetworks",
"columnName": "tunnel_networks",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isMobileDataTunnel",
"columnName": "is_mobile_data_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPrimaryTunnel",
"columnName": "is_primary_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "amQuick",
"columnName": "am_quick",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isActive",
"columnName": "is_Active",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "pingInterval",
"columnName": "ping_interval",
"affinity": "INTEGER",
"notNull": false,
"defaultValue": "null"
},
{
"fieldPath": "pingCooldown",
"columnName": "ping_cooldown",
"affinity": "INTEGER",
"notNull": false,
"defaultValue": "null"
},
{
"fieldPath": "pingIp",
"columnName": "ping_ip",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "null"
},
{
"fieldPath": "isEthernetTunnel",
"columnName": "is_ethernet_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_TunnelConfig_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f2b260c389fb2e53216de40e4b1047f3')"
]
}
}
+1 -1
View File
@@ -164,7 +164,7 @@
tools:node="merge" />
<service
android:name=".service.foreground.TunnelBackgroundService"
android:name=".service.foreground.TunnelForegroundService"
android:exported="false"
android:persistent="true"
android:foregroundServiceType="systemExempted"
@@ -3,6 +3,9 @@ package com.zaneschepke.wireguardautotunnel
import android.app.Application
import android.os.StrictMode
import android.os.StrictMode.ThreadPolicy
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
@@ -52,6 +55,7 @@ class WireGuardAutoTunnel : Application() {
override fun onCreate() {
super.onCreate()
instance = this
ProcessLifecycleOwner.get().lifecycle.addObserver(AppLifecycleObserver())
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
StrictMode.setThreadPolicy(
@@ -74,7 +78,9 @@ class WireGuardAutoTunnel : Application() {
tunnelService.setBackendState(BackendState.SERVICE_ACTIVE, emptyList())
}
appStateRepository.getLocale()?.let {
LocaleUtil.changeLocale(it)
withContext(mainDispatcher) {
LocaleUtil.changeLocale(it)
}
}
}
}
@@ -86,7 +92,25 @@ class WireGuardAutoTunnel : Application() {
super.onTerminate()
}
class AppLifecycleObserver : DefaultLifecycleObserver {
override fun onStart(owner: LifecycleOwner) {
Timber.d("Application entered foreground")
foreground = true
}
override fun onPause(owner: LifecycleOwner) {
Timber.d("Application entered background")
foreground = false
}
}
companion object {
private var foreground = false
fun isForeground(): Boolean {
return foreground
}
lateinit var instance: WireGuardAutoTunnel
private set
}
@@ -11,7 +11,7 @@ import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
@Database(
entities = [Settings::class, TunnelConfig::class],
version = 13,
version = 14,
autoMigrations =
[
AutoMigration(from = 1, to = 2),
@@ -49,6 +49,10 @@ import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
from = 12,
to = 13,
),
AutoMigration(
from = 13,
to = 14,
),
],
exportSchema = true,
)
@@ -80,4 +80,9 @@ data class Settings(
defaultValue = "false",
)
val isLanOnKillSwitchEnabled: Boolean = false,
@ColumnInfo(
name = "debounce_delay_seconds",
defaultValue = "3",
)
val debounceDelaySeconds: Int = 3,
)
@@ -66,7 +66,7 @@ data class TunnelConfig(
) {
fun toAmConfig(): org.amnezia.awg.config.Config {
return configFromAmQuick(if (amQuick != "") amQuick else wgQuick)
return configFromAmQuick(if (amQuick.isNotBlank()) amQuick else wgQuick)
}
fun toWgConfig(): Config {
@@ -7,25 +7,21 @@ import com.zaneschepke.wireguardautotunnel.service.network.WifiService
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ServiceComponent
import dagger.hilt.android.scopes.ServiceScoped
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(ServiceComponent::class)
@InstallIn(SingletonComponent::class)
abstract class ServiceModule {
@Binds
@Wifi
@ServiceScoped
abstract fun provideWifiService(wifiService: WifiService): NetworkService
@Binds
@MobileData
@ServiceScoped
abstract fun provideMobileDataService(mobileDataService: MobileDataService): NetworkService
@Binds
@Ethernet
@ServiceScoped
abstract fun provideEthernetService(ethernetService: EthernetService): NetworkService
}
@@ -10,6 +10,7 @@ import com.wireguard.android.util.ToolsInstaller
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.WireGuardTunnel
@@ -78,6 +79,9 @@ class TunnelModule {
@IoDispatcher ioDispatcher: CoroutineDispatcher,
serviceManager: ServiceManager,
notificationService: NotificationService,
@Wifi wifiService: NetworkService,
@MobileData mobileDataService: NetworkService,
@Ethernet ethernetService: NetworkService,
): TunnelService {
return WireGuardTunnel(
amneziaBackend,
@@ -88,6 +92,9 @@ class TunnelModule {
ioDispatcher,
serviceManager,
notificationService,
wifiService,
mobileDataService,
ethernetService,
)
}
@@ -32,6 +32,8 @@ class AppUpdateReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action != Intent.ACTION_MY_PACKAGE_REPLACED) return
serviceManager.updateTunnelTile()
serviceManager.updateAutoTunnelTile()
applicationScope.launch {
val settings = appDataRepository.settings.getSettings()
if (settings.isAutoTunnelEnabled) {
@@ -40,7 +42,7 @@ class AppUpdateReceiver : BroadcastReceiver() {
}
if (!settings.isAutoTunnelEnabled) {
val tunnels = appDataRepository.tunnels.getAll().filter { it.isActive }
if (tunnels.isNotEmpty()) tunnelService.get().startTunnel(tunnels.first(), true)
if (tunnels.isNotEmpty()) tunnelService.get().startTunnel(tunnels.first())
}
}
}
@@ -32,6 +32,8 @@ class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (Intent.ACTION_BOOT_COMPLETED != intent.action) return
serviceManager.updateTunnelTile()
serviceManager.updateAutoTunnelTile()
applicationScope.launch {
with(appDataRepository.settings.getSettings()) {
if (isRestoreOnBootEnabled) {
@@ -39,7 +41,7 @@ class BootReceiver : BroadcastReceiver() {
val tunState = tunnelService.get().vpnState.value.status
if (activeTunnels.isNotEmpty() && tunState != TunnelState.UP) {
Timber.i("Starting previously active tunnel")
tunnelService.get().startTunnel(activeTunnels.first(), true)
tunnelService.get().startTunnel(activeTunnels.first())
}
if (isAutoTunnelEnabled) {
Timber.i("Starting watcher service from boot")
@@ -29,7 +29,7 @@ class ServiceManager
val autoTunnelActive = _autoTunnelActive.asStateFlow()
var autoTunnelService = CompletableDeferred<AutoTunnelService>()
var backgroundService = CompletableDeferred<TunnelBackgroundService>()
var backgroundService = CompletableDeferred<TunnelForegroundService>()
var autoTunnelTile = CompletableDeferred<AutoTunnelControlTile>()
var tunnelControlTile = CompletableDeferred<TunnelControlTile>()
@@ -59,10 +59,10 @@ class ServiceManager
}
}
suspend fun startBackgroundService(tunnelConfig: TunnelConfig?) {
suspend fun startBackgroundService(tunnelConfig: TunnelConfig) {
if (backgroundService.isCompleted) return
kotlin.runCatching {
startService(TunnelBackgroundService::class.java, true)
startService(TunnelForegroundService::class.java, true)
backgroundService.await()
backgroundService.getCompleted().start(tunnelConfig)
}.onFailure {
@@ -86,7 +86,7 @@ class ServiceManager
}
}
private fun updateAutoTunnelTile() {
fun updateAutoTunnelTile() {
if (autoTunnelTile.isCompleted) {
autoTunnelTile.getCompleted().updateTileState()
} else {
@@ -16,7 +16,7 @@ import kotlinx.coroutines.CompletableDeferred
import javax.inject.Inject
@AndroidEntryPoint
class TunnelBackgroundService : LifecycleService() {
class TunnelForegroundService : LifecycleService() {
@Inject
lateinit var notificationService: NotificationService
@@ -39,9 +39,9 @@ class TunnelBackgroundService : LifecycleService() {
return super.onStartCommand(intent, flags, startId)
}
fun start(tunnelConfig: TunnelConfig?) {
fun start(tunnelConfig: TunnelConfig) {
ServiceCompat.startForeground(
this,
this@TunnelForegroundService,
NotificationService.KERNEL_SERVICE_NOTIFICATION_ID,
createNotification(tunnelConfig),
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
@@ -9,7 +9,6 @@ import androidx.lifecycle.lifecycleScope
import com.wireguard.android.util.RootShell
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.AppShell
import com.zaneschepke.wireguardautotunnel.module.Ethernet
@@ -22,34 +21,27 @@ import com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model.A
import com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model.AutoTunnelState
import com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model.NetworkState
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
import com.zaneschepke.wireguardautotunnel.service.network.WifiService
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationAction
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.notification.WireGuardNotification
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
import com.zaneschepke.wireguardautotunnel.util.extensions.cancelWithMessage
import com.zaneschepke.wireguardautotunnel.util.extensions.isReachable
import com.zaneschepke.wireguardautotunnel.util.extensions.onNotRunning
import com.zaneschepke.wireguardautotunnel.util.extensions.getCurrentWifiName
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.net.InetAddress
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
import javax.inject.Provider
@@ -98,10 +90,6 @@ class AutoTunnelService : LifecycleService() {
private var wakeLock: PowerManager.WakeLock? = null
private val pingTunnelRestartActive = AtomicBoolean(false)
private var pingJob: Job? = null
override fun onCreate() {
super.onCreate()
serviceManager.autoTunnelService.complete(this)
@@ -133,7 +121,6 @@ class AutoTunnelService : LifecycleService() {
}
startAutoTunnelJob()
startAutoTunnelStateJob()
startPingStateJob()
}.onFailure {
Timber.e(it)
}
@@ -145,7 +132,6 @@ class AutoTunnelService : LifecycleService() {
}
override fun onDestroy() {
cancelAndResetPingJob()
serviceManager.autoTunnelService = CompletableDeferred()
super.onDestroy()
}
@@ -182,65 +168,17 @@ class AutoTunnelService : LifecycleService() {
}
}
private fun startPingJob() = lifecycleScope.launch {
watchForPingFailure()
}
private fun startPingStateJob() = lifecycleScope.launch {
autoTunnelStateFlow.collect {
if (it == defaultState) return@collect
if (it.isPingEnabled()) {
pingJob.onNotRunning { pingJob = startPingJob() }
} else {
if (!pingTunnelRestartActive.get()) cancelAndResetPingJob()
}
}
}
private suspend fun watchForPingFailure() {
withContext(ioDispatcher) {
Timber.i("Starting ping watcher")
runCatching {
do {
val vpnState = autoTunnelStateFlow.value.vpnState
if (vpnState.status.isUp() && !autoTunnelStateFlow.value.isNoConnectivity()) {
if (vpnState.tunnelConfig != null) {
val config = TunnelConfig.configFromWgQuick(vpnState.tunnelConfig.wgQuick)
val results = if (vpnState.tunnelConfig.pingIp != null) {
Timber.d("Pinging custom ip : ${vpnState.tunnelConfig.pingIp}")
listOf(InetAddress.getByName(vpnState.tunnelConfig.pingIp).isReachable(Constants.PING_TIMEOUT.toInt()))
} else {
Timber.d("Pinging all peers")
config.peers.map { peer ->
peer.isReachable()
}
}
Timber.i("Ping results reachable: $results")
if (results.contains(false)) {
Timber.i("Restarting VPN for ping failure")
val cooldown = vpnState.tunnelConfig.pingCooldown
pingTunnelRestartActive.set(true)
tunnelService.get().bounceTunnel()
pingTunnelRestartActive.set(false)
delay(cooldown ?: Constants.PING_COOLDOWN)
continue
}
}
}
delay(vpnState.tunnelConfig?.pingInterval ?: Constants.PING_INTERVAL)
} while (true)
}.onFailure {
Timber.e(it)
}
}
}
private fun startAutoTunnelStateJob() = lifecycleScope.launch(ioDispatcher) {
combine(
combineSettings(),
combineNetworkEventsJob(),
) { double, networkState ->
AutoTunnelState(tunnelService.get().vpnState.value, networkState, double.first, double.second)
var wifiName: String? = null
if (networkState.wifiName == Constants.UNREADABLE_SSID && double.first.isTunnelOnWifiEnabled) {
wifiName = getWifiName(double.first)
}
val netState = wifiName?.let { networkState.copy(wifiName = it) } ?: networkState
AutoTunnelState(tunnelService.get().vpnState.value, netState, double.first, double.second)
}.collect { state ->
Timber.d("Network state: ${state.networkState}")
autoTunnelStateFlow.update {
@@ -249,9 +187,14 @@ class AutoTunnelService : LifecycleService() {
}
}
private fun cancelAndResetPingJob() {
pingJob?.cancelWithMessage("Ping job canceled")
pingJob = null
private fun getWifiName(setting: Settings): String? {
return if (setting.isWifiNameByShellEnabled) {
rootShell.get().getCurrentWifiName()
} else if (wifiService.capabilities != null) {
WifiService.getNetworkName(wifiService.capabilities!!, this@AutoTunnelService)
} else {
null
}
}
@OptIn(FlowPreview::class)
@@ -259,14 +202,15 @@ class AutoTunnelService : LifecycleService() {
return combine(
wifiService.status,
mobileDataService.status,
) { wifi, mobileData ->
ethernetService.status,
) { wifi, mobileData, ethernet ->
NetworkState(
wifi.available,
mobileData.available,
false,
ethernet.available,
wifi.name,
)
}.distinctUntilChanged().filterNot { it.isWifiConnected && it.wifiName == null }.debounce(500L)
}.distinctUntilChanged()
}
private fun combineSettings(): Flow<Pair<Settings, TunnelConfigs>> {
@@ -284,7 +228,10 @@ class AutoTunnelService : LifecycleService() {
@OptIn(FlowPreview::class)
private fun startAutoTunnelJob() = lifecycleScope.launch(ioDispatcher) {
Timber.i("Starting auto-tunnel network event watcher")
autoTunnelStateFlow.debounce(1000L).collect { watcherState ->
val settings = appDataRepository.get().settings.getSettings()
val debounce = settings.debounceDelaySeconds * 1000L
Timber.d("Starting with debounce delay of: $debounce")
autoTunnelStateFlow.debounce(debounce).collect { watcherState ->
if (watcherState == defaultState) return@collect
Timber.d("New auto tunnel state emitted")
when (val event = watcherState.asAutoTunnelEvent()) {
@@ -5,4 +5,8 @@ data class NetworkState(
val isMobileDataConnected: Boolean = false,
val isEthernetConnected: Boolean = false,
val wifiName: String? = null,
)
) {
fun hasNoCapabilities(): Boolean {
return !isWifiConnected && !isMobileDataConnected && !isEthernetConnected
}
}
@@ -20,6 +20,8 @@ constructor(
@ApplicationContext context: Context,
) : NetworkService {
override var capabilities: NetworkCapabilities? = null
private val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
@@ -32,6 +34,7 @@ constructor(
trySend(NetworkStatus.Unavailable())
}
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
capabilities = networkCapabilities
trySend(
NetworkStatus.CapabilitiesChanged(
network,
@@ -19,6 +19,9 @@ class MobileDataService
constructor(
@ApplicationContext context: Context,
) : NetworkService {
override var capabilities: NetworkCapabilities? = null
private val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
@@ -31,6 +34,7 @@ constructor(
trySend(NetworkStatus.Unavailable())
}
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
capabilities = networkCapabilities
trySend(
NetworkStatus.CapabilitiesChanged(
network,
@@ -7,6 +7,7 @@ import kotlinx.coroutines.flow.map
interface NetworkService {
val status: Flow<Status>
var capabilities: NetworkCapabilities?
}
inline fun <Result> Flow<NetworkStatus>.map(
@@ -12,6 +12,7 @@ import android.os.Build
import com.wireguard.android.util.RootShell
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.module.AppShell
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.getCurrentWifiName
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.channels.awaitClose
@@ -33,6 +34,8 @@ constructor(
@AppShell private val rootShell: Provider<RootShell>,
) : NetworkService {
override var capabilities: NetworkCapabilities? = null
val mutex = Mutex()
private var ssid: String? = null
@@ -79,6 +82,7 @@ constructor(
}
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
capabilities = networkCapabilities
trySend(
NetworkStatus.CapabilitiesChanged(
network,
@@ -110,10 +114,14 @@ constructor(
available = true
}
is NetworkStatus.CapabilitiesChanged -> mutex.withLock {
if (available) {
if (available || ssid == null || ssid == Constants.UNREADABLE_SSID) {
available = false
Timber.d("Getting SSID from capabilities")
ssid = getNetworkName(it.networkCapabilities)
ssid = if (settingsRepository.getSettings().isWifiNameByShellEnabled) {
rootShell.get().getCurrentWifiName()
} else {
getNetworkName(it.networkCapabilities, context)
}
}
emit(Status(true, ssid))
}
@@ -121,19 +129,20 @@ constructor(
}
}
private suspend fun getNetworkName(networkCapabilities: NetworkCapabilities): String? {
if (settingsRepository.getSettings().isWifiNameByShellEnabled) return rootShell.get().getCurrentWifiName()
var ssid = networkCapabilities.getWifiName()
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S) {
val wifiManager =
context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
companion object {
fun getNetworkName(networkCapabilities: NetworkCapabilities, context: Context): String? {
var ssid = networkCapabilities.getWifiName()
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S) {
val wifiManager =
context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
@Suppress("DEPRECATION")
val info = wifiManager.connectionInfo
if (info.supplicantState === SupplicantState.COMPLETED) {
ssid = info.ssid
@Suppress("DEPRECATION")
val info = wifiManager.connectionInfo
if (info.supplicantState === SupplicantState.COMPLETED) {
ssid = info.ssid
}
}
return ssid?.trim('"')
}
return ssid?.trim('"')
}
}
@@ -45,7 +45,7 @@ class ShortcutsActivity : ComponentActivity() {
Timber.d("Shortcut action on name: ${tunnelConfig?.name}")
tunnelConfig?.let {
when (intent.action) {
Action.START.name -> tunnelService.get().startTunnel(it, true)
Action.START.name -> tunnelService.get().startTunnel(it)
Action.STOP.name -> tunnelService.get().stopTunnel()
else -> Unit
}
@@ -1,5 +1,7 @@
package com.zaneschepke.wireguardautotunnel.service.tile
import android.content.Intent
import android.os.IBinder
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
@@ -9,6 +11,7 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
@@ -77,6 +80,17 @@ class AutoTunnelControlTile : TileService() {
}
}
/* This works around an annoying unsolved frameworks bug some people are hitting. */
override fun onBind(intent: Intent): IBinder? {
var ret: IBinder? = null
try {
ret = super.onBind(intent)
} catch (_: Throwable) {
Timber.e("Failed to bind to TunnelControlTile")
}
return ret
}
private fun setUnavailable() {
kotlin.runCatching {
qsTile.state = Tile.STATE_UNAVAILABLE
@@ -1,9 +1,10 @@
package com.zaneschepke.wireguardautotunnel.service.tile
import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
@@ -12,8 +13,8 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Provider
@AndroidEntryPoint
class TunnelControlTile : TileService() {
@@ -21,7 +22,7 @@ class TunnelControlTile : TileService() {
lateinit var appDataRepository: AppDataRepository
@Inject
lateinit var tunnelService: Provider<TunnelService>
lateinit var tunnelService: TunnelService
@Inject
@ApplicationScope
@@ -42,17 +43,20 @@ class TunnelControlTile : TileService() {
override fun onStartListening() {
super.onStartListening()
Timber.d("Start listening called")
serviceManager.tunnelControlTile.complete(this)
applicationScope.launch {
if (appDataRepository.tunnels.getAll().isEmpty()) return@launch setUnavailable()
updateTileState()
}
}
fun updateTileState() = applicationScope.launch {
val lastActive = appDataRepository.getStartTunnelConfig()
lastActive?.let {
updateTile(it)
if (appDataRepository.tunnels.getAll().isEmpty()) return@launch setUnavailable()
with(tunnelService.vpnState.value) {
if (status.isUp() && tunnelConfig != null) return@launch updateTile(tunnelConfig.name, true)
}
appDataRepository.getStartTunnelConfig()?.let {
updateTile(it.name, false)
}
}
@@ -60,14 +64,9 @@ class TunnelControlTile : TileService() {
super.onClick()
unlockAndRun {
applicationScope.launch {
val lastActive = appDataRepository.getStartTunnelConfig()
lastActive?.let { tunnel ->
if (tunnel.isActive) {
tunnelService.get().stopTunnel()
} else {
tunnelService.get().startTunnel(tunnel, true)
}
updateTileState()
if (tunnelService.vpnState.value.status.isUp()) return@launch tunnelService.stopTunnel()
appDataRepository.getStartTunnelConfig()?.let {
tunnelService.startTunnel(it)
}
}
}
@@ -107,13 +106,24 @@ class TunnelControlTile : TileService() {
}
}
private fun updateTile(tunnelConfig: TunnelConfig?) {
/* This works around an annoying unsolved frameworks bug some people are hitting. */
override fun onBind(intent: Intent): IBinder? {
var ret: IBinder? = null
try {
ret = super.onBind(intent)
} catch (_: Throwable) {
Timber.e("Failed to bind to TunnelControlTile")
}
return ret
}
private fun updateTile(name: String, active: Boolean) {
kotlin.runCatching {
tunnelConfig?.let {
setTileDescription(it.name)
if (it.isActive) return setActive()
setInactive()
}
setTileDescription(name)
if (active) return setActive()
setInactive()
}.onFailure {
Timber.e(it)
}
}
}
@@ -6,7 +6,7 @@ import kotlinx.coroutines.flow.StateFlow
interface TunnelService : Tunnel, org.amnezia.awg.backend.Tunnel {
suspend fun startTunnel(tunnelConfig: TunnelConfig?, background: Boolean = false)
suspend fun startTunnel(tunnelConfig: TunnelConfig?)
suspend fun stopTunnel()
@@ -2,30 +2,44 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel
import com.wireguard.android.backend.Backend
import com.wireguard.android.backend.Tunnel.State
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.module.Ethernet
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.module.Kernel
import com.zaneschepke.wireguardautotunnel.module.MobileData
import com.zaneschepke.wireguardautotunnel.module.Wifi
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationAction
import com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model.NetworkState
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService.Companion.VPN_NOTIFICATION_ID
import com.zaneschepke.wireguardautotunnel.service.notification.WireGuardNotification
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.AmneziaStatistics
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.WireGuardStatistics
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.service.notification.WireGuardNotification
import com.zaneschepke.wireguardautotunnel.util.StringValue
import com.zaneschepke.wireguardautotunnel.util.extensions.asAmBackendState
import com.zaneschepke.wireguardautotunnel.util.extensions.asBackendState
import com.zaneschepke.wireguardautotunnel.util.extensions.cancelWithMessage
import com.zaneschepke.wireguardautotunnel.util.extensions.isReachable
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
@@ -33,6 +47,8 @@ import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.amnezia.awg.backend.Tunnel
import timber.log.Timber
import java.net.InetAddress
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
import javax.inject.Provider
@@ -47,6 +63,9 @@ constructor(
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
private val serviceManager: ServiceManager,
private val notificationService: NotificationService,
@Wifi private val wifiService: NetworkService,
@MobileData private val mobileDataService: NetworkService,
@Ethernet private val ethernetService: NetworkService,
) : TunnelService {
private val _vpnState = MutableStateFlow(VpnState())
@@ -54,9 +73,12 @@ constructor(
private var statsJob: Job? = null
private var tunnelChangesJob: Job? = null
private var pingJob: Job? = null
private var networkJob: Job? = null
@get:Synchronized @set:Synchronized
private var isKernelBackend: Boolean? = null
private val isNetworkAvailable = AtomicBoolean(false)
private val tunnelControlMutex = Mutex()
@@ -83,19 +105,29 @@ constructor(
}
}
// TODO refactor duplicate
@OptIn(FlowPreview::class)
private fun combineNetworkEventsJob(): Flow<NetworkState> {
return combine(
wifiService.status,
mobileDataService.status,
ethernetService.status,
) { wifi, mobileData, ethernet ->
NetworkState(
wifi.available,
mobileData.available,
ethernet.available,
wifi.name,
)
}.distinctUntilChanged()
}
private suspend fun setState(tunnelConfig: TunnelConfig, tunnelState: TunnelState): Result<TunnelState> {
return runCatching {
when (val backend = backend()) {
is Backend -> backend.setState(this, tunnelState.toWgState(), TunnelConfig.configFromWgQuick(tunnelConfig.wgQuick)).let { TunnelState.from(it) }
is Backend -> backend.setState(this, tunnelState.toWgState(), tunnelConfig.toWgConfig()).let { TunnelState.from(it) }
is org.amnezia.awg.backend.Backend -> {
val config = if (tunnelConfig.amQuick.isBlank()) {
TunnelConfig.configFromAmQuick(
tunnelConfig.wgQuick,
)
} else {
TunnelConfig.configFromAmQuick(tunnelConfig.amQuick)
}
backend.setState(this, tunnelState.toAmState(), config).let {
backend.setState(this, tunnelState.toAmState(), tunnelConfig.toAmConfig()).let {
TunnelState.from(it)
}
}
@@ -108,41 +140,61 @@ constructor(
}
private fun isTunnelAlreadyRunning(tunnelConfig: TunnelConfig): Boolean {
val isRunning = tunnelConfig == _vpnState.value.tunnelConfig && _vpnState.value.status.isUp()
if (isRunning) Timber.w("Tunnel already running")
return isRunning
return with(_vpnState.value) {
this.tunnelConfig?.id == tunnelConfig.id && status.isUp().also {
if (it) Timber.w("Tunnel already running")
}
}
}
override suspend fun startTunnel(tunnelConfig: TunnelConfig?, background: Boolean) {
override suspend fun startTunnel(tunnelConfig: TunnelConfig?) {
withContext(ioDispatcher) {
if (tunnelConfig == null || isTunnelAlreadyRunning(tunnelConfig)) return@withContext
onBeforeStart(tunnelConfig)
updateTunnelConfig(tunnelConfig) // need to update this here
appDataRepository.tunnels.save(tunnelConfig.copy(isActive = true))
withServiceActive {
onBeforeStart(background)
tunnelControlMutex.withLock {
setState(tunnelConfig, TunnelState.UP).onSuccess {
updateTunnelState(it, tunnelConfig)
onTunnelStart(tunnelConfig, background)
}
setState(tunnelConfig, TunnelState.UP).onSuccess {
updateTunnelState(it, tunnelConfig)
startActiveTunnelJobs()
}.onFailure {
Timber.e(it)
onTunnelStop(tunnelConfig)
// TODO improve this with better statuses and handling
showTunnelStartFailed()
}
}
}
}
private fun showTunnelStartFailed() {
if (WireGuardAutoTunnel.isForeground()) {
SnackbarController.showMessage(StringValue.StringResource(R.string.error_tunnel_start))
} else {
launchStartFailedNotification()
}
}
private fun launchStartFailedNotification() {
with(notificationService) {
val notification = createNotification(
WireGuardNotification.NotificationChannels.VPN,
title = context.getString(R.string.error_tunnel_start),
)
show(VPN_NOTIFICATION_ID, notification)
}
}
override suspend fun stopTunnel() {
withContext(ioDispatcher) {
if (_vpnState.value.status.isDown()) return@withContext
with(_vpnState.value) {
if (tunnelConfig == null) return@withContext
tunnelControlMutex.withLock {
setState(tunnelConfig, TunnelState.DOWN).onSuccess {
onTunnelStop(tunnelConfig)
updateTunnelState(it, null)
}.onFailure {
Timber.e(it)
}
setState(tunnelConfig, TunnelState.DOWN).onSuccess {
onTunnelStop(tunnelConfig)
updateTunnelState(it, null)
}.onFailure {
clearJobsAndStats()
Timber.e(it)
}
}
}
@@ -171,10 +223,12 @@ constructor(
}
override suspend fun bounceTunnel() {
_vpnState.value.tunnelConfig?.let {
withServiceActive {
toggleTunnel(it)
toggleTunnel(it)
with(_vpnState.value) {
if (tunnelConfig != null && status.isUp()) {
withServiceActive {
toggleTunnel(tunnelConfig)
toggleTunnel(tunnelConfig)
}
}
}
}
@@ -204,31 +258,10 @@ constructor(
}
}
private suspend fun onBeforeStart(background: Boolean) {
private suspend fun onBeforeStart(tunnelConfig: TunnelConfig) {
with(_vpnState.value) {
if (status.isUp()) stopTunnel() else clearJobsAndStats()
if (isKernelBackend == true || background) serviceManager.startBackgroundService(tunnelConfig)
}
}
private suspend fun onTunnelStart(tunnelConfig: TunnelConfig, background: Boolean) {
startActiveTunnelJobs()
if (_vpnState.value.status.isUp()) {
appDataRepository.tunnels.save(tunnelConfig.copy(isActive = true))
}
if (isKernelBackend == false && !background) launchUserspaceTunnelNotification()
}
private fun launchUserspaceTunnelNotification() {
with(notificationService) {
val notification = createNotification(
WireGuardNotification.NotificationChannels.VPN,
title = "${context.getString(R.string.tunnel_running)} - ${_vpnState.value.tunnelConfig?.name}",
actions = listOf(
notificationService.createNotificationAction(NotificationAction.TUNNEL_OFF),
),
)
show(VPN_NOTIFICATION_ID, notification)
serviceManager.startBackgroundService(tunnelConfig)
}
}
@@ -279,15 +312,24 @@ constructor(
}
override fun cancelActiveTunnelJobs() {
statsJob?.cancel()
tunnelChangesJob?.cancel()
statsJob?.cancelWithMessage("Tunnel stats job cancelled")
tunnelChangesJob?.cancelWithMessage("Tunnel changes job cancelled")
cancelPingJobs()
}
override fun startActiveTunnelJobs() {
statsJob = startTunnelStatisticsJob()
tunnelChangesJob = startTunnelConfigChangesJob()
if (_vpnState.value.tunnelConfig?.isPingEnabled == true) {
startPingJobs()
}
}
private fun startPingJobs() {
cancelPingJobs()
pingJob = startPingJob()
networkJob = startNetworkJob()
}
override fun getName(): String {
return _vpnState.value.tunnelConfig?.name ?: ""
}
@@ -310,22 +352,111 @@ constructor(
}
}
private fun isQuickConfigChanged(config: TunnelConfig): Boolean {
return with(_vpnState.value) {
if (tunnelConfig == null) return false
config.wgQuick != tunnelConfig.wgQuick ||
config.amQuick != tunnelConfig.amQuick
}
}
private fun isPingConfigMatching(config: TunnelConfig): Boolean {
return with(_vpnState.value.tunnelConfig) {
if (this == null) return true
config.isPingEnabled == isPingEnabled &&
pingIp == config.pingIp &&
config.pingCooldown == pingCooldown &&
config.pingInterval == pingInterval
}
}
private fun handlePingConfigChanges() {
with(_vpnState.value.tunnelConfig) {
if (this == null) return
if (!isPingEnabled && pingJob?.isActive == true) {
cancelPingJobs()
return
}
restartPingJob()
}
}
private fun restartPingJob() {
cancelPingJobs()
startPingJobs()
}
private fun cancelPingJobs() {
pingJob?.cancelWithMessage("Ping job cancelled")
networkJob?.cancelWithMessage("Network job cancelled")
}
private fun startTunnelConfigChangesJob() = applicationScope.launch(ioDispatcher) {
tunnelConfigRepository.getTunnelConfigsFlow().collect {
tunnelConfigRepository.getTunnelConfigsFlow().collect { tunnels ->
with(_vpnState.value) {
if (status.isDown() || tunnelConfig == null) return@collect
val vpnConfigFromStorage = it.first { it.id == tunnelConfig.id }
val isRestartNeeded = vpnConfigFromStorage.wgQuick != tunnelConfig.wgQuick ||
vpnConfigFromStorage.amQuick != tunnelConfig.amQuick
updateTunnelConfig(vpnConfigFromStorage)
if (isRestartNeeded) {
Timber.d("Bouncing tunnel on config change")
bounceTunnel()
if (tunnelConfig == null) return@collect
val storageConfig = tunnels.firstOrNull { it.id == tunnelConfig.id }
if (storageConfig == null) return@collect
val quickChanged = isQuickConfigChanged(storageConfig)
val pingMatching = isPingConfigMatching(storageConfig)
updateTunnelConfig(storageConfig)
if (quickChanged) bounceTunnel()
if (!pingMatching) handlePingConfigChanges()
}
}
}
private suspend fun pingTunnel(tunnelConfig: TunnelConfig): List<Boolean> {
return withContext(ioDispatcher) {
val config = tunnelConfig.toWgConfig()
if (tunnelConfig.pingIp != null) {
Timber.i("Pinging custom ip")
listOf(InetAddress.getByName(tunnelConfig.pingIp).isReachable(Constants.PING_TIMEOUT.toInt()))
} else {
Timber.i("Pinging all peers")
config.peers.map { peer ->
peer.isReachable()
}
}
}
}
private fun startPingJob() = applicationScope.launch(ioDispatcher) {
do {
run {
with(_vpnState.value) {
if (status.isUp() && tunnelConfig != null && isNetworkAvailable.get()) {
val reachable = pingTunnel(tunnelConfig)
if (reachable.contains(false)) {
if (isNetworkAvailable.get()) {
Timber.i("Ping result: target was not reachable, bouncing the tunnel")
bounceTunnel()
delay(tunnelConfig.pingCooldown ?: Constants.PING_COOLDOWN)
} else {
Timber.i("Ping result: target was not reachable, but not network available")
}
return@run
} else {
Timber.i("Ping result: all ping targets were reached successfully")
}
}
delay(tunnelConfig?.pingInterval ?: Constants.PING_INTERVAL)
}
}
} while (true)
}
private fun startNetworkJob() = applicationScope.launch(ioDispatcher) {
combineNetworkEventsJob().collect {
Timber.d("New network state: $it")
if (!it.isWifiConnected && !it.isEthernetConnected && !it.isMobileDataConnected) {
isNetworkAvailable.set(false)
} else {
isNetworkAvailable.set(true)
}
}
}
override fun onStateChange(newState: Tunnel.State) {
_vpnState.update {
it.copy(status = TunnelState.from(newState))
@@ -9,6 +9,7 @@ import com.wireguard.config.Config
import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.AppShell
@@ -27,6 +28,7 @@ import com.zaneschepke.wireguardautotunnel.util.StringValue
import com.zaneschepke.wireguardautotunnel.util.extensions.getAllInternetCapablePackages
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
@@ -110,12 +112,14 @@ constructor(
}
private suspend fun initTunnel() {
if (tunnelService.get().getState() == TunnelState.UP) tunnelService.get().startActiveTunnelJobs()
val activeTunnels = appDataRepository.tunnels.getActive()
if (activeTunnels.isNotEmpty() &&
tunnelService.get().getState() == TunnelState.DOWN
) {
tunnelService.get().startTunnel(activeTunnels.first())
withContext(ioDispatcher) {
if (tunnelService.get().getState() == TunnelState.UP) tunnelService.get().startActiveTunnelJobs()
val activeTunnels = appDataRepository.tunnels.getActive()
if (activeTunnels.isNotEmpty() &&
tunnelService.get().getState() == TunnelState.DOWN
) {
tunnelService.get().startTunnel(activeTunnels.first())
}
}
}
@@ -132,6 +136,10 @@ constructor(
}
}
fun saveSettings(settings: Settings) = viewModelScope.launch {
appDataRepository.settings.save(settings)
}
fun onPinLockDisabled() = viewModelScope.launch(ioDispatcher) {
PinManager.clearPin()
appDataRepository.appState.setPinLockEnabled(false)
@@ -202,18 +210,14 @@ constructor(
private suspend fun handleVpnKillSwitchChange(enabled: Boolean) {
withContext(ioDispatcher) {
if (enabled) {
Timber.d("Starting kill switch")
val allowedIps = if (appDataRepository.settings.getSettings().isLanOnKillSwitchEnabled) {
TunnelConfig.IPV4_PUBLIC_NETWORKS
} else {
emptySet()
}
tunnelService.get().setBackendState(BackendState.KILL_SWITCH_ACTIVE, allowedIps)
if (!enabled) return@withContext tunnelService.get().setBackendState(BackendState.SERVICE_ACTIVE, emptySet())
Timber.d("Starting kill switch")
val allowedIps = if (appDataRepository.settings.getSettings().isLanOnKillSwitchEnabled) {
TunnelConfig.IPV4_PUBLIC_NETWORKS
} else {
Timber.d("Sending shutdown of kill switch")
tunnelService.get().setBackendState(BackendState.SERVICE_ACTIVE, emptySet())
emptySet()
}
tunnelService.get().setBackendState(BackendState.KILL_SWITCH_ACTIVE, allowedIps)
}
}
@@ -297,25 +301,67 @@ constructor(
}
}
fun saveConfigChanges(config: TunnelConfig, peers: List<PeerProxy>? = null, `interface`: InterfaceProxy? = null) = viewModelScope.launch(
ioDispatcher,
) {
fun updateExistingTunnelConfig(
tunnelConfig: TunnelConfig,
tunnelName: String? = null,
peers: List<PeerProxy>? = null,
`interface`: InterfaceProxy? = null,
) = viewModelScope.launch {
runCatching {
val amConfig = config.toAmConfig()
val wgConfig = config.toWgConfig()
rebuildConfigsAndSave(config, amConfig, wgConfig, peers, `interface`)
val amConfig = tunnelConfig.toAmConfig()
val wgConfig = tunnelConfig.toWgConfig()
updateTunnelConfig(tunnelConfig, tunnelName, amConfig, wgConfig, peers, `interface`)
_popBackStack.emit(true)
SnackbarController.showMessage(StringValue.StringResource(R.string.config_changes_saved))
}.onFailure {
Timber.e(it)
SnackbarController.showMessage(
it.message?.let { message ->
(StringValue.DynamicString(message))
} ?: StringValue.StringResource(R.string.unknown_error),
)
onConfigSaveError(it)
}
}
fun saveNewTunnel(tunnelName: String, peers: List<PeerProxy>, `interface`: InterfaceProxy) = viewModelScope.launch {
runCatching {
val config = buildConfigs(peers, `interface`)
appDataRepository.tunnels.save(
TunnelConfig(
name = tunnelName,
wgQuick = config.first.toWgQuickString(true),
amQuick = config.second.toAwgQuickString(true),
),
)
_popBackStack.emit(true)
SnackbarController.showMessage(StringValue.StringResource(R.string.config_changes_saved))
}.onFailure {
onConfigSaveError(it)
}
}
private fun onConfigSaveError(throwable: Throwable) {
Timber.e(throwable)
SnackbarController.showMessage(
throwable.message?.let { message ->
(StringValue.DynamicString(message))
} ?: StringValue.StringResource(R.string.unknown_error),
)
}
private suspend fun updateTunnelConfig(
tunnelConfig: TunnelConfig,
tunnelName: String? = null,
amConfig: org.amnezia.awg.config.Config,
wgConfig: Config,
peers: List<PeerProxy>? = null,
`interface`: InterfaceProxy? = null,
) {
val configs = rebuildConfigs(amConfig, wgConfig, peers, `interface`)
appDataRepository.tunnels.save(
tunnelConfig.copy(
name = tunnelName ?: tunnelConfig.name,
amQuick = configs.second.toAwgQuickString(true),
wgQuick = configs.first.toWgQuickString(true),
),
)
}
fun cleanUpUninstalledApps(tunnelConfig: TunnelConfig, packages: List<String>) = viewModelScope.launch(ioDispatcher) {
runCatching {
val amConfig = tunnelConfig.toAmConfig()
@@ -323,32 +369,52 @@ constructor(
val proxy = InterfaceProxy.from(amConfig.`interface`)
if (proxy.includedApplications.isEmpty() && proxy.excludedApplications.isEmpty()) return@launch
if (proxy.includedApplications.retainAll(packages.toSet()) || proxy.excludedApplications.retainAll(packages.toSet())) {
Timber.i("Removing split tunnel package for app that no longer exists on the device")
rebuildConfigsAndSave(tunnelConfig, amConfig, wgConfig, `interface` = proxy)
updateTunnelConfig(tunnelConfig, amConfig = amConfig, wgConfig = wgConfig, `interface` = proxy)
Timber.i("Removed split tunnel package for app that no longer exists on the device")
}
}.onFailure {
Timber.e(it)
}
}
private suspend fun rebuildConfigsAndSave(
config: TunnelConfig,
fun bounceAutoTunnel() = viewModelScope.launch(ioDispatcher) {
serviceManager.stopAutoTunnel()
delay(1000L)
serviceManager.startAutoTunnel(true)
}
private suspend fun rebuildConfigs(
amConfig: org.amnezia.awg.config.Config,
wgConfig: Config,
peers: List<PeerProxy>? = null,
`interface`: InterfaceProxy? = null,
) {
appDataRepository.tunnels.save(
config.copy(
wgQuick = Config.Builder().apply {
): Pair<Config, org.amnezia.awg.config.Config> {
return withContext(ioDispatcher) {
Pair(
Config.Builder().apply {
addPeers(peers?.map { it.toWgPeer() } ?: wgConfig.peers)
setInterface(`interface`?.toWgInterface() ?: wgConfig.`interface`)
}.build().toWgQuickString(true),
amQuick = org.amnezia.awg.config.Config.Builder().apply {
}.build(),
org.amnezia.awg.config.Config.Builder().apply {
addPeers(peers?.map { it.toAmPeer() } ?: amConfig.peers)
setInterface(`interface`?.toAmInterface() ?: amConfig.`interface`)
}.build().toAwgQuickString(true),
),
)
}.build(),
)
}
}
private suspend fun buildConfigs(peers: List<PeerProxy>, `interface`: InterfaceProxy): Pair<Config, org.amnezia.awg.config.Config> {
return withContext(ioDispatcher) {
Pair(
Config.Builder().apply {
addPeers(peers.map { it.toWgPeer() })
setInterface(`interface`.toWgInterface())
}.build(),
org.amnezia.awg.config.Config.Builder().apply {
addPeers(peers.map { it.toAmPeer() })
setInterface(`interface`.toAmInterface())
}.build(),
)
}
}
}
@@ -5,6 +5,7 @@ import android.graphics.Color.TRANSPARENT
import android.os.Build
import android.os.Bundle
import androidx.activity.SystemBarStyle
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
@@ -34,6 +35,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
@@ -55,6 +57,7 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.Appear
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.display.DisplayScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.language.LanguageScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.AutoTunnelScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.advanced.AdvancedScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.disclosure.LocationDisclosureScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.killswitch.KillSwitchScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
@@ -67,6 +70,7 @@ import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlin.system.exitProcess
@@ -211,28 +215,24 @@ class MainActivity : AppCompatActivity() {
composable<Route.Support> {
SupportScreen(appUiState, viewModel)
}
composable<Route.AutoTunnelAdvanced> {
AdvancedScreen(appUiState, viewModel)
}
composable<Route.Logs> {
LogsScreen()
}
composable<Route.Config> {
val args = it.toRoute<Route.Config>()
ConfigScreen(
appUiState,
tunnelId = args.id,
appViewModel = viewModel,
)
val config = appUiState.tunnels.firstOrNull { it.id == args.id }
ConfigScreen(config, viewModel)
}
composable<Route.TunnelOptions> {
val args = it.toRoute<Route.TunnelOptions>()
OptionsScreen(
tunnelId = args.id,
appUiState = appUiState,
)
val config = appUiState.tunnels.first { it.id == args.id }
OptionsScreen(config)
}
composable<Route.Lock> {
PinLockScreen(
appViewModel = viewModel,
)
PinLockScreen(viewModel)
}
composable<Route.Scanner> {
ScannerScreen()
@@ -242,11 +242,20 @@ class MainActivity : AppCompatActivity() {
}
composable<Route.SplitTunnel> {
val args = it.toRoute<Route.SplitTunnel>()
SplitTunnelScreen(appUiState, args.id, viewModel)
val config = appUiState.tunnels.first { it.id == args.id }
SplitTunnelScreen(config, viewModel)
}
composable<Route.TunnelAutoTunnel> {
val args = it.toRoute<Route.SplitTunnel>()
TunnelAutoTunnelScreen(appUiState, args.id)
val args = it.toRoute<Route.TunnelOptions>()
val config = appUiState.tunnels.first { it.id == args.id }
TunnelAutoTunnelScreen(config, appUiState.settings)
}
}
BackHandler(enabled = true) {
lifecycleScope.launch {
if (!navController.popBackStack()) {
this@MainActivity.finish()
}
}
}
}
@@ -256,9 +265,4 @@ class MainActivity : AppCompatActivity() {
}
}
}
override fun onDestroy() {
super.onDestroy()
// save battery by not polling stats while app is closed
tunnelService.cancelActiveTunnelJobs()
}
}
@@ -12,6 +12,9 @@ sealed class Route {
@Serializable
data object AutoTunnel : Route()
@Serializable
data object AutoTunnelAdvanced : Route()
@Serializable
data object LocationDisclosure : Route()
@@ -19,7 +19,9 @@ fun TopNavBar(title: String, trailing: @Composable () -> Unit = {}, showBack: Bo
},
navigationIcon = {
if (showBack) {
IconButton(onClick = { navController.popBackStack() }) {
IconButton(onClick = {
navController.popBackStack()
}) {
val icon = Icons.AutoMirrored.Outlined.ArrowBack
Icon(
imageVector = icon,
@@ -82,7 +82,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
val startAutoTunnel = withVpnPermission<Unit> { viewModel.onToggleAutoTunnel() }
val startTunnel = withVpnPermission<TunnelConfig> {
viewModel.onTunnelStart(it, uiState.settings.isKernelEnabled)
viewModel.onTunnelStart(it)
}
val autoTunnelToggleBattery = withIgnoreBatteryOpt(uiState.generalState.isBatteryOptimizationDisableShown) {
if (!uiState.generalState.isBatteryOptimizationDisableShown) viewModel.setBatteryOptimizeDisableShown()
@@ -129,7 +129,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) {
if (!checked) viewModel.onTunnelStop().also { return }
if (uiState.settings.isKernelEnabled) {
viewModel.onTunnelStart(tunnel, uiState.settings.isKernelEnabled)
viewModel.onTunnelStart(tunnel)
} else {
startTunnel.invoke(tunnel)
}
@@ -226,8 +226,9 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
) { tunnel ->
val expanded = uiState.generalState.isTunnelStatsExpanded
TunnelRowItem(
tunnel.id == uiState.vpnState.tunnelConfig?.id &&
uiState.vpnState.status.isUp(),
tunnel.id == uiState.vpnState.tunnelConfig?.id && (
uiState.vpnState.status.isUp() || (uiState.settings.isKernelEnabled && tunnel.isActive)
),
expanded,
selectedTunnel?.id == tunnel.id,
tunnel,
@@ -67,9 +67,9 @@ constructor(
appDataRepository.appState.setTunnelStatsExpanded(expanded)
}
fun onTunnelStart(tunnelConfig: TunnelConfig, background: Boolean) = viewModelScope.launch {
fun onTunnelStart(tunnelConfig: TunnelConfig) = viewModelScope.launch {
Timber.i("Starting tunnel ${tunnelConfig.name}")
tunnelService.get().startTunnel(tunnelConfig, background)
tunnelService.get().startTunnel(tunnelConfig)
}
fun onTunnelStop() = viewModelScope.launch {
@@ -86,20 +86,6 @@ constructor(
}
}
private fun generateQrCodeTunnelName(config: String): String {
var defaultName = generateQrCodeDefaultName(config)
val lines = config.lines().toMutableList()
val linesIterator = lines.iterator()
while (linesIterator.hasNext()) {
val next = linesIterator.next()
if (next.contains(Constants.QR_CODE_NAME_PROPERTY)) {
defaultName = next.substringAfter(Constants.QR_CODE_NAME_PROPERTY).trim()
break
}
}
return defaultName
}
private suspend fun makeTunnelNameUnique(name: String): String {
return withContext(ioDispatcher) {
val tunnels = appDataRepository.tunnels.getAll()
@@ -112,6 +112,13 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
},
),
) {
val onAutoTunnelClick = {
if (!uiState.generalState.isLocationDisclosureShown) {
navController.navigate(Route.LocationDisclosure)
} else {
navController.navigate(Route.AutoTunnel)
}
}
SurfaceSelectionGroupButton(
listOf(
SelectionItem(
@@ -124,11 +131,10 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
)
},
onClick = {
if (!uiState.generalState.isLocationDisclosureShown) return@SelectionItem navController.navigate(Route.LocationDisclosure)
navController.navigate(Route.AutoTunnel)
onAutoTunnelClick()
},
trailing = {
ForwardButton(Modifier.focusable()) { navController.navigate(Route.AutoTunnel) }
ForwardButton(Modifier.focusable()) { onAutoTunnelClick() }
},
),
),
@@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
@@ -16,8 +17,8 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.AirplanemodeActive
import androidx.compose.material.icons.outlined.Code
import androidx.compose.material.icons.outlined.Filter1
import androidx.compose.material.icons.outlined.NetworkPing
import androidx.compose.material.icons.outlined.Security
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.outlined.SettingsEthernet
import androidx.compose.material.icons.outlined.SignalCellular4Bar
import androidx.compose.material.icons.outlined.Wifi
@@ -42,13 +43,16 @@ import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.AppUiState
import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.components.TrustedNetworkTextBox
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.components.WildcardsLabel
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.BackgroundLocationDialog
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.ForwardButton
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.LearnMoreLinkLabel
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.LocationServicesDialog
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
@@ -62,6 +66,7 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
@Composable
fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltViewModel()) {
val context = LocalContext.current
val navController = LocalNavController.current
val fineLocationState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
var currentText by remember { mutableStateOf("") }
@@ -129,6 +134,7 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV
.fillMaxSize()
.padding(padding)
.verticalScroll(rememberScrollState())
.imePadding()
.padding(top = 24.dp.scaledHeight())
.padding(horizontal = 24.dp.scaledWidth()),
) {
@@ -309,24 +315,6 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV
viewModel.onToggleTunnelOnEthernet()
},
),
SelectionItem(
Icons.Outlined.NetworkPing,
title = {
Text(
stringResource(R.string.restart_on_ping),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = {
ScaledSwitch(
checked = uiState.settings.isPingEnabled,
onClick = { viewModel.onToggleRestartOnPing() },
)
},
onClick = {
viewModel.onToggleRestartOnPing()
},
),
SelectionItem(
Icons.Outlined.AirplanemodeActive,
title = {
@@ -353,6 +341,25 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV
),
),
)
SurfaceSelectionGroupButton(
listOf(
SelectionItem(
Icons.Outlined.Settings,
title = {
Text(
stringResource(R.string.advanced_settings),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
onClick = {
navController.navigate(Route.AutoTunnelAdvanced)
},
trailing = {
ForwardButton { navController.navigate(Route.AutoTunnelAdvanced) }
},
),
),
)
}
}
}
@@ -119,16 +119,6 @@ constructor(
}
}
fun onToggleRestartOnPing() = viewModelScope.launch {
with(settings.value) {
appDataRepository.settings.save(
copy(
isPingEnabled = !isPingEnabled,
),
)
}
}
fun onToggleStopOnNoInternet() = viewModelScope.launch {
with(settings.value) {
appDataRepository.settings.save(
@@ -0,0 +1,121 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.advanced
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material.icons.outlined.PauseCircle
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.AppUiState
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
@Composable
fun AdvancedScreen(appUiState: AppUiState, appViewModel: AppViewModel) {
var isDropDownExpanded by remember {
mutableStateOf(false)
}
var selected by remember { mutableIntStateOf(appUiState.settings.debounceDelaySeconds) }
LaunchedEffect(selected) {
if (selected == appUiState.settings.debounceDelaySeconds) return@LaunchedEffect
appViewModel.saveSettings(appUiState.settings.copy(debounceDelaySeconds = selected))
if (appUiState.settings.isAutoTunnelEnabled) {
appViewModel.bounceAutoTunnel()
}
}
Scaffold(
topBar = {
TopNavBar(stringResource(R.string.advanced_settings))
},
) { padding ->
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top),
modifier =
Modifier
.fillMaxSize()
.padding(padding)
.verticalScroll(rememberScrollState())
.padding(top = 24.dp.scaledHeight())
.padding(horizontal = 24.dp.scaledWidth()),
) {
SurfaceSelectionGroupButton(
listOf(
SelectionItem(
Icons.Outlined.PauseCircle,
title = {
Text(
stringResource(R.string.debounce_delay),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
onClick = {
isDropDownExpanded = true
},
trailing = {
Row(
horizontalArrangement = Arrangement.spacedBy(5.dp, Alignment.CenterHorizontally),
verticalAlignment = Alignment.CenterVertically,
) {
Text(text = selected.toString(), style = MaterialTheme.typography.bodyMedium)
val icon = Icons.Default.ArrowDropDown
Icon(icon, icon.name)
}
DropdownMenu(
modifier = Modifier.height(250.dp.scaledHeight()),
scrollState = rememberScrollState(),
containerColor = MaterialTheme.colorScheme.surface,
expanded = isDropDownExpanded,
onDismissRequest = {
isDropDownExpanded = false
},
) {
(0..10).forEachIndexed { index, num ->
DropdownMenuItem(
text = {
Text(text = num.toString())
},
onClick = {
isDropDownExpanded = false
selected = num
},
)
}
}
},
),
),
)
}
}
}
@@ -3,13 +3,16 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.tunneloptions
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.CallSplit
import androidx.compose.material.icons.outlined.Bolt
import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material.icons.outlined.NetworkPing
import androidx.compose.material.icons.outlined.Star
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
@@ -23,33 +26,40 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.AppUiState
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.common.config.SubmitConfigurationTextBox
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.ForwardButton
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.isValidIpv4orIpv6Address
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
import kotlin.text.isBlank
import kotlin.text.isNullOrBlank
import kotlin.text.toLong
@Composable
fun OptionsScreen(tunnelOptionsViewModel: TunnelOptionsViewModel = hiltViewModel(), appUiState: AppUiState, tunnelId: Int) {
fun OptionsScreen(tunnelConfig: TunnelConfig, viewModel: TunnelOptionsViewModel = hiltViewModel()) {
val navController = LocalNavController.current
val config = remember { appUiState.tunnels.first { it.id == tunnelId } }
var currentText by remember { mutableStateOf("") }
LaunchedEffect(config.tunnelNetworks) {
LaunchedEffect(tunnelConfig.tunnelNetworks) {
currentText = ""
}
Scaffold(
topBar = {
TopNavBar(config.name)
TopNavBar(tunnelConfig.name)
},
) {
Column(
@@ -59,6 +69,7 @@ fun OptionsScreen(tunnelOptionsViewModel: TunnelOptionsViewModel = hiltViewModel
Modifier
.fillMaxSize()
.padding(it)
.imePadding()
.verticalScroll(rememberScrollState())
.padding(top = 24.dp.scaledHeight())
.padding(horizontal = 24.dp.scaledWidth()),
@@ -81,11 +92,11 @@ fun OptionsScreen(tunnelOptionsViewModel: TunnelOptionsViewModel = hiltViewModel
},
trailing = {
ScaledSwitch(
config.isPrimaryTunnel,
onClick = { tunnelOptionsViewModel.onTogglePrimaryTunnel(config) },
tunnelConfig.isPrimaryTunnel,
onClick = { viewModel.onTogglePrimaryTunnel(tunnelConfig) },
)
},
onClick = { tunnelOptionsViewModel.onTogglePrimaryTunnel(config) },
onClick = { viewModel.onTogglePrimaryTunnel(tunnelConfig) },
),
SelectionItem(
Icons.Outlined.Bolt,
@@ -102,10 +113,10 @@ fun OptionsScreen(tunnelOptionsViewModel: TunnelOptionsViewModel = hiltViewModel
)
},
onClick = {
navController.navigate(Route.TunnelAutoTunnel(id = tunnelId))
navController.navigate(Route.TunnelAutoTunnel(id = tunnelConfig.id))
},
trailing = {
ForwardButton { navController.navigate(Route.TunnelAutoTunnel(id = tunnelId)) }
ForwardButton { navController.navigate(Route.TunnelAutoTunnel(id = tunnelConfig.id)) }
},
),
SelectionItem(
@@ -117,10 +128,10 @@ fun OptionsScreen(tunnelOptionsViewModel: TunnelOptionsViewModel = hiltViewModel
)
},
onClick = {
navController.navigate(Route.Config(id = tunnelId))
navController.navigate(Route.Config(id = tunnelConfig.id))
},
trailing = {
ForwardButton { navController.navigate(Route.Config(id = tunnelId)) }
ForwardButton { navController.navigate(Route.Config(id = tunnelConfig.id)) }
},
),
SelectionItem(
@@ -132,15 +143,89 @@ fun OptionsScreen(tunnelOptionsViewModel: TunnelOptionsViewModel = hiltViewModel
)
},
onClick = {
navController.navigate(Route.SplitTunnel(id = tunnelId))
navController.navigate(Route.SplitTunnel(id = tunnelConfig.id))
},
trailing = {
ForwardButton { navController.navigate(Route.SplitTunnel(id = tunnelId)) }
ForwardButton { navController.navigate(Route.SplitTunnel(id = tunnelConfig.id)) }
},
),
),
)
// GroupLabel(stringResource(R.string.quick_actions))
SurfaceSelectionGroupButton(
buildList {
add(
SelectionItem(
Icons.Outlined.NetworkPing,
title = {
Text(
stringResource(R.string.restart_on_ping),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = {
ScaledSwitch(
checked = tunnelConfig.isPingEnabled,
onClick = { viewModel.onToggleRestartOnPing(tunnelConfig) },
)
},
onClick = { viewModel.onToggleRestartOnPing(tunnelConfig) },
),
)
if (tunnelConfig.isPingEnabled) {
add(
SelectionItem(
title = {},
description = {
SubmitConfigurationTextBox(
tunnelConfig.pingIp,
stringResource(R.string.set_custom_ping_ip),
stringResource(R.string.default_ping_ip),
isErrorValue = { !it.isNullOrBlank() && !it.isValidIpv4orIpv6Address() },
onSubmit = {
viewModel.saveTunnelChanges(
tunnelConfig.copy(pingIp = it.ifBlank { null }),
)
},
)
fun isSecondsError(seconds: String?): Boolean {
return seconds?.let { value -> if (value.isBlank()) false else value.toLong() >= Long.MAX_VALUE / 1000 }
?: false
}
SubmitConfigurationTextBox(
tunnelConfig.pingInterval?.let { (it / 1000).toString() },
stringResource(R.string.set_custom_ping_internal),
"(${stringResource(R.string.optional_default)} ${Constants.PING_INTERVAL / 1000})",
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Done,
),
isErrorValue = ::isSecondsError,
onSubmit = {
viewModel.saveTunnelChanges(
tunnelConfig.copy(pingInterval = if (it.isBlank()) null else it.toLong() * 1000),
)
},
)
SubmitConfigurationTextBox(
tunnelConfig.pingCooldown?.let { (it / 1000).toString() },
stringResource(R.string.set_custom_ping_cooldown),
"(${stringResource(R.string.optional_default)} ${Constants.PING_COOLDOWN / 1000})",
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
),
isErrorValue = ::isSecondsError,
onSubmit = {
viewModel.saveTunnelChanges(
tunnelConfig.copy(pingCooldown = if (it.isBlank()) null else it.toLong() * 1000),
)
},
)
},
),
)
}
},
)
}
}
}
@@ -2,11 +2,8 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.tunneloptions
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.util.StringValue
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
@@ -17,50 +14,14 @@ class TunnelOptionsViewModel
constructor(
private val appDataRepository: AppDataRepository,
) : ViewModel() {
fun onDeleteRunSSID(ssid: String, tunnelConfig: TunnelConfig) = viewModelScope.launch {
fun onToggleRestartOnPing(tunnelConfig: TunnelConfig) = viewModelScope.launch {
appDataRepository.tunnels.save(
tunnelConfig =
tunnelConfig.copy(
tunnelNetworks = (tunnelConfig.tunnelNetworks - ssid).toMutableList(),
isPingEnabled = !tunnelConfig.isPingEnabled,
),
)
}
fun saveTunnelChanges(tunnelConfig: TunnelConfig) = viewModelScope.launch {
appDataRepository.tunnels.save(tunnelConfig)
}
fun onSaveRunSSID(ssid: String, tunnelConfig: TunnelConfig) = viewModelScope.launch {
if (ssid.isBlank()) return@launch
val trimmed = ssid.trim()
val tunnelsWithName = appDataRepository.tunnels.findByTunnelNetworksName(trimmed)
if (!tunnelConfig.tunnelNetworks.contains(trimmed) &&
tunnelsWithName.isEmpty()
) {
saveTunnelChanges(
tunnelConfig.copy(
tunnelNetworks = (tunnelConfig.tunnelNetworks + ssid).toMutableList(),
),
)
} else {
SnackbarController.showMessage(
StringValue.StringResource(
R.string.error_ssid_exists,
),
)
}
}
fun onToggleIsMobileDataTunnel(tunnelConfig: TunnelConfig) = viewModelScope.launch {
if (tunnelConfig.isMobileDataTunnel) {
appDataRepository.tunnels.updateMobileDataTunnel(null)
} else {
appDataRepository.tunnels.updateMobileDataTunnel(tunnelConfig)
}
}
fun onTogglePrimaryTunnel(tunnelConfig: TunnelConfig) = viewModelScope.launch {
appDataRepository.tunnels.updatePrimaryTunnel(
when (tunnelConfig.isPrimaryTunnel) {
@@ -70,19 +31,7 @@ constructor(
)
}
fun onToggleRestartOnPing(tunnelConfig: TunnelConfig) = viewModelScope.launch {
appDataRepository.tunnels.save(
tunnelConfig.copy(
isPingEnabled = !tunnelConfig.isPingEnabled,
),
)
}
fun onToggleIsEthernetTunnel(tunnelConfig: TunnelConfig) = viewModelScope.launch {
if (tunnelConfig.isEthernetTunnel) {
appDataRepository.tunnels.updateEthernetTunnel(null)
} else {
appDataRepository.tunnels.updateEthernetTunnel(tunnelConfig)
}
fun saveTunnelChanges(tunnelConfig: TunnelConfig) = viewModelScope.launch {
appDataRepository.tunnels.save(tunnelConfig)
}
}
@@ -1,5 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.tunneloptions.config
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusGroup
import androidx.compose.foundation.layout.Arrangement
@@ -8,7 +9,9 @@ import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -19,7 +22,10 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Save
import androidx.compose.material.icons.rounded.ContentCopy
import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material.icons.rounded.MoreVert
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
@@ -30,15 +36,14 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.ClipboardManager
@@ -53,56 +58,49 @@ import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.AppUiState
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.ui.screens.tunneloptions.config.model.InterfaceActions
import com.zaneschepke.wireguardautotunnel.ui.screens.tunneloptions.config.model.PeerActions
import com.zaneschepke.wireguardautotunnel.ui.screens.tunneloptions.config.model.InterfaceProxy
import com.zaneschepke.wireguardautotunnel.ui.screens.tunneloptions.config.model.PeerProxy
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
import kotlinx.coroutines.launch
import org.amnezia.awg.crypto.KeyPair
@Composable
fun ConfigScreen(appUiState: AppUiState, appViewModel: AppViewModel, tunnelId: Int) {
fun ConfigScreen(tunnelConfig: TunnelConfig?, appViewModel: AppViewModel) {
val context = LocalContext.current
val snackbar = SnackbarController.current
val clipboardManager: ClipboardManager = LocalClipboardManager.current
val keyboardController = LocalSoftwareKeyboardController.current
val navController = LocalNavController.current
val scope = rememberCoroutineScope()
var isInterfaceDropDownExpanded by remember {
mutableStateOf(false)
}
val popBackStack by appViewModel.popBackStack.collectAsStateWithLifecycle(false)
val tunnelConfig by remember {
derivedStateOf {
appUiState.tunnels.first { it.id == tunnelId }
}
}
val configPair by remember {
derivedStateOf {
Pair(tunnelConfig.name, tunnelConfig.toAmConfig())
}
}
val configPair = Pair(tunnelConfig?.name ?: "", tunnelConfig?.toAmConfig())
var tunnelName by remember {
mutableStateOf(configPair.first)
}
var interfaceState by remember {
mutableStateOf(InterfaceProxy.from(configPair.second.`interface`))
mutableStateOf(configPair.second?.let { InterfaceProxy.from(it.`interface`) } ?: InterfaceProxy())
}
var showAmneziaValues by remember {
mutableStateOf(configPair.second.`interface`.junkPacketCount.isPresent)
mutableStateOf(configPair.second?.`interface`?.junkPacketCount?.isPresent == true)
}
var showScripts by remember {
@@ -110,7 +108,7 @@ fun ConfigScreen(appUiState: AppUiState, appViewModel: AppViewModel, tunnelId: I
}
val peersState = remember {
configPair.second.peers.map { PeerProxy.from(it) }.toMutableStateList()
(configPair.second?.peers?.map { PeerProxy.from(it) } ?: listOf(PeerProxy())).toMutableStateList()
}
var showAuthPrompt by remember { mutableStateOf(false) }
@@ -148,13 +146,14 @@ fun ConfigScreen(appUiState: AppUiState, appViewModel: AppViewModel, tunnelId: I
topBar = {
TopNavBar(stringResource(R.string.edit_tunnel), trailing = {
IconButton(onClick = {
appViewModel.saveConfigChanges(
tunnelConfig.copy(
name = tunnelName,
),
peers = peersState,
`interface` = interfaceState,
)
tunnelConfig?.let {
appViewModel.updateExistingTunnelConfig(
it,
tunnelName,
peersState,
interfaceState,
)
} ?: appViewModel.saveNewTunnel(tunnelName, peersState, interfaceState)
}) {
val icon = Icons.Outlined.Save
Icon(
@@ -172,6 +171,7 @@ fun ConfigScreen(appUiState: AppUiState, appViewModel: AppViewModel, tunnelId: I
Modifier
.fillMaxSize()
.padding(padding)
.imePadding()
.verticalScroll(rememberScrollState())
.padding(top = 24.dp.scaledHeight())
.padding(horizontal = 24.dp.scaledWidth()),
@@ -187,35 +187,77 @@ fun ConfigScreen(appUiState: AppUiState, appViewModel: AppViewModel, tunnelId: I
.padding(16.dp.scaledWidth())
.focusGroup(),
) {
GroupLabel(
stringResource(R.string.interface_),
)
ConfigurationToggle(
stringResource(id = R.string.show_amnezia_properties),
checked = showAmneziaValues,
onCheckChanged = {
if (appUiState.settings.isKernelEnabled) {
snackbar.showMessage(context.getString(R.string.amnezia_kernel_message))
} else {
showAmneziaValues = it
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier =
Modifier.fillMaxWidth(),
) {
GroupLabel(
stringResource(R.string.interface_),
)
Column {
IconButton(
modifier = Modifier.size(iconSize),
onClick = {
isInterfaceDropDownExpanded = true
},
) {
val icon = Icons.Rounded.MoreVert
Icon(icon, icon.name)
}
},
)
ConfigurationToggle(
stringResource(id = R.string.show_scripts),
checked = showScripts,
onCheckChanged = { checked ->
if (appUiState.settings.isKernelEnabled) {
showScripts = checked
} else {
scope.launch {
appViewModel.requestRoot().onSuccess {
showScripts = checked
}
DropdownMenu(
containerColor = MaterialTheme.colorScheme.surface,
expanded = isInterfaceDropDownExpanded,
modifier = Modifier.shadow(12.dp).background(MaterialTheme.colorScheme.surface),
onDismissRequest = {
isInterfaceDropDownExpanded = false
},
) {
val isAmneziaCompatibilitySet = interfaceState.isAmneziaCompatibilityModeSet()
InterfaceActions.entries.forEach { action ->
DropdownMenuItem(
text = {
Text(
text = when (action) {
InterfaceActions.TOGGLE_SHOW_SCRIPTS -> if (showScripts) {
stringResource(R.string.hide_scripts)
} else {
stringResource(R.string.show_scripts)
}
InterfaceActions.TOGGLE_AMNEZIA_VALUES -> if (showAmneziaValues) {
stringResource(R.string.hide_amnezia_properties)
} else {
stringResource(R.string.show_amnezia_properties)
}
InterfaceActions.SET_AMNEZIA_COMPATIBILITY -> if (isAmneziaCompatibilitySet) {
stringResource(R.string.remove_amnezia_compatibility)
} else {
stringResource(R.string.enable_amnezia_compatibility)
}
},
)
},
onClick = {
isInterfaceDropDownExpanded = false
when (action) {
InterfaceActions.TOGGLE_AMNEZIA_VALUES -> showAmneziaValues = !showAmneziaValues
InterfaceActions.TOGGLE_SHOW_SCRIPTS -> showScripts = !showScripts
InterfaceActions.SET_AMNEZIA_COMPATIBILITY -> if (isAmneziaCompatibilitySet) {
showAmneziaValues = false
interfaceState = interfaceState.resetAmneziaProperties()
} else {
showAmneziaValues = true
interfaceState = interfaceState.toAmneziaCompatibilityConfig()
}
}
},
)
}
}
},
)
}
}
ConfigurationTextBox(
value = tunnelName,
onValueChange = { tunnelName = it },
@@ -226,6 +268,7 @@ fun ConfigScreen(appUiState: AppUiState, appViewModel: AppViewModel, tunnelId: I
Modifier
.fillMaxWidth(),
)
val privateKeyEnabled = (tunnelConfig == null) || isAuthenticated
OutlinedTextField(
textStyle = MaterialTheme.typography.labelLarge,
modifier =
@@ -234,17 +277,17 @@ fun ConfigScreen(appUiState: AppUiState, appViewModel: AppViewModel, tunnelId: I
.clickable { showAuthPrompt = true },
value = interfaceState.privateKey,
visualTransformation =
if ((tunnelId == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated) {
if (privateKeyEnabled) {
VisualTransformation.None
} else {
PasswordVisualTransformation()
},
enabled = (tunnelId == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated,
enabled = privateKeyEnabled,
onValueChange = { interfaceState = interfaceState.copy(privateKey = it) },
trailingIcon = {
IconButton(
enabled = isAuthenticated,
modifier = Modifier.focusRequester(FocusRequester.Default),
enabled = privateKeyEnabled,
modifier = Modifier.focusRequester(FocusRequester.Default).size(iconSize),
onClick = {
val keypair = KeyPair()
interfaceState = interfaceState.copy(
@@ -256,7 +299,7 @@ fun ConfigScreen(appUiState: AppUiState, appViewModel: AppViewModel, tunnelId: I
Icon(
Icons.Rounded.Refresh,
stringResource(R.string.rotate_keys),
tint = if (isAuthenticated) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.outline,
tint = if (privateKeyEnabled) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.outline,
)
}
},
@@ -285,7 +328,7 @@ fun ConfigScreen(appUiState: AppUiState, appViewModel: AppViewModel, tunnelId: I
},
trailingIcon = {
IconButton(
modifier = Modifier.focusRequester(FocusRequester.Default),
modifier = Modifier.focusRequester(FocusRequester.Default).size(iconSize),
onClick = {
clipboardManager.setText(
AnnotatedString(interfaceState.publicKey),
@@ -538,13 +581,17 @@ fun ConfigScreen(appUiState: AppUiState, appViewModel: AppViewModel, tunnelId: I
}
}
peersState.forEachIndexed { index, peer ->
var isPeerDropDownExpanded by remember {
mutableStateOf(false)
}
val isLanExcluded = peer.isLanExcluded()
Surface(
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.surface,
) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(5.dp, Alignment.Top),
verticalArrangement = Arrangement.spacedBy(10.dp, Alignment.Top),
modifier = Modifier
.padding(16.dp.scaledWidth())
.focusGroup(),
@@ -558,11 +605,65 @@ fun ConfigScreen(appUiState: AppUiState, appViewModel: AppViewModel, tunnelId: I
GroupLabel(
stringResource(R.string.peer),
)
IconButton(onClick = {
peersState.removeAt(index)
}) {
val icon = Icons.Rounded.Delete
Icon(icon, icon.name)
Row(
horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.End),
verticalAlignment = Alignment.CenterVertically,
) {
IconButton(
modifier = Modifier.size(iconSize),
onClick = {
// TODO make a dialog to confirm this
peersState.removeAt(index)
},
) {
val icon = Icons.Rounded.Delete
Icon(icon, icon.name)
}
Column {
IconButton(
modifier = Modifier.size(iconSize),
onClick = {
isPeerDropDownExpanded = true
},
) {
val icon = Icons.Rounded.MoreVert
Icon(icon, icon.name)
}
DropdownMenu(
containerColor = MaterialTheme.colorScheme.surface,
expanded = isPeerDropDownExpanded,
modifier = Modifier.shadow(12.dp).background(MaterialTheme.colorScheme.surface),
onDismissRequest = {
isPeerDropDownExpanded = false
},
) {
PeerActions.entries.forEach { action ->
DropdownMenuItem(
text = {
Text(
text = when (action) {
PeerActions.EXCLUDE_LAN -> if (isLanExcluded) {
stringResource(R.string.include_lan)
} else {
stringResource(R.string.exclude_lan)
}
},
)
},
onClick = {
isPeerDropDownExpanded = false
when (action) {
PeerActions.EXCLUDE_LAN -> if (isLanExcluded) {
peersState[index] = peer.includeLan()
} else {
peersState[index] = peer.excludeLan()
}
}
},
)
}
}
}
}
}
@@ -0,0 +1,7 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.tunneloptions.config.model
enum class InterfaceActions {
TOGGLE_AMNEZIA_VALUES,
SET_AMNEZIA_COMPATIBILITY,
TOGGLE_SHOW_SCRIPTS,
}
@@ -3,6 +3,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.tunneloptions.config.mode
import com.wireguard.config.Interface
import com.zaneschepke.wireguardautotunnel.util.extensions.joinAndTrim
import com.zaneschepke.wireguardautotunnel.util.extensions.toTrimmedList
import kotlin.ranges.contains
data class InterfaceProxy(
val privateKey: String = "",
@@ -44,6 +45,46 @@ data class InterfaceProxy(
}.build()
}
fun toAmneziaCompatibilityConfig(): InterfaceProxy {
return copy(
junkPacketCount = "4",
junkPacketMinSize = "40",
junkPacketMaxSize = "70",
initPacketJunkSize = "0",
responsePacketJunkSize = "0",
initPacketMagicHeader = "1",
responsePacketMagicHeader = "2",
underloadPacketMagicHeader = "3",
transportPacketMagicHeader = "4",
)
}
fun resetAmneziaProperties(): InterfaceProxy {
return copy(
junkPacketCount = "",
junkPacketMinSize = "",
junkPacketMaxSize = "",
initPacketJunkSize = "",
responsePacketJunkSize = "",
initPacketMagicHeader = "",
responsePacketMagicHeader = "",
underloadPacketMagicHeader = "",
transportPacketMagicHeader = "",
)
}
fun isAmneziaCompatibilityModeSet(): Boolean {
return junkPacketCount.toIntOrNull() in 3..<5 &&
junkPacketMinSize.toIntOrNull() == 40 &&
junkPacketMaxSize.toIntOrNull() == 70 &&
with(initPacketJunkSize.toIntOrNull()) { this == 0 || this == null } &&
with(responsePacketJunkSize.toIntOrNull()) { this == 0 || this == null } &&
initPacketMagicHeader.toLongOrNull() == 1L &&
responsePacketMagicHeader.toLongOrNull() == 2L &&
underloadPacketMagicHeader.toLongOrNull() == 3L &&
transportPacketMagicHeader.toLongOrNull() == 4L
}
fun toAmInterface(): org.amnezia.awg.config.Interface {
return org.amnezia.awg.config.Interface.Builder().apply {
parseAddresses(addresses)
@@ -0,0 +1,5 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.tunneloptions.config.model
enum class PeerActions {
EXCLUDE_LAN,
}
@@ -8,7 +8,7 @@ data class PeerProxy(
val preSharedKey: String = "",
val persistentKeepalive: String = "",
val endpoint: String = "",
val allowedIps: String = IPV4_WILDCARD.joinAndTrim(),
val allowedIps: String = ALL_IPS.joinAndTrim(),
) {
fun toWgPeer(): Peer {
return Peer.Builder().apply {
@@ -29,6 +29,22 @@ data class PeerProxy(
}.build()
}
fun isLanExcluded(): Boolean {
return this.allowedIps.contains(IPV4_PUBLIC_NETWORKS.joinAndTrim())
}
fun includeLan(): PeerProxy {
return this.copy(
allowedIps = ALL_IPS.joinAndTrim(),
)
}
fun excludeLan(): PeerProxy {
return this.copy(
allowedIps = IPV4_PUBLIC_NETWORKS.joinAndTrim(),
)
}
companion object {
fun from(peer: Peer): PeerProxy {
return PeerProxy(
@@ -113,6 +129,6 @@ data class PeerProxy(
"200.0.0.0/5",
"208.0.0.0/4",
)
val IPV4_WILDCARD = setOf("0.0.0.0/0")
val ALL_IPS = setOf("0.0.0.0/0", "::/0")
}
}
@@ -48,7 +48,7 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.accompanist.drawablepainter.rememberDrawablePainter
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.AppUiState
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.common.button.SelectionItemButton
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
@@ -62,7 +62,7 @@ import java.text.Collator
import java.util.Locale
@Composable
fun SplitTunnelScreen(appUiState: AppUiState, tunnelId: Int, viewModel: AppViewModel) {
fun SplitTunnelScreen(tunnelConfig: TunnelConfig, viewModel: AppViewModel) {
val context = LocalContext.current
val navController = LocalNavController.current
@@ -76,8 +76,6 @@ fun SplitTunnelScreen(appUiState: AppUiState, tunnelId: Int, viewModel: AppViewM
if (popBackStack) navController.popBackStack()
}
val config by remember { derivedStateOf { appUiState.tunnels.first { it.id == tunnelId } } }
val splitTunnelApps by viewModel.splitTunnelApps.collectAsStateWithLifecycle()
var proxyInterface by remember { mutableStateOf(InterfaceProxy()) }
@@ -87,7 +85,7 @@ fun SplitTunnelScreen(appUiState: AppUiState, tunnelId: Int, viewModel: AppViewM
val selectedPackages = remember { mutableStateListOf<String>() }
LaunchedEffect(Unit) {
proxyInterface = InterfaceProxy.from(config.toWgConfig().`interface`)
proxyInterface = InterfaceProxy.from(tunnelConfig.toAmConfig().`interface`)
val pair = when {
proxyInterface.excludedApplications.isNotEmpty() -> Pair(SplitOptions.EXCLUDE, proxyInterface.excludedApplications)
proxyInterface.includedApplications.isNotEmpty() -> Pair(SplitOptions.INCLUDE, proxyInterface.includedApplications)
@@ -101,13 +99,13 @@ fun SplitTunnelScreen(appUiState: AppUiState, tunnelId: Int, viewModel: AppViewM
val sortedPackages by remember {
derivedStateOf {
splitTunnelApps.sortedWith(compareBy(collator) { it.name }).filter { it.name.contains(query) }.toMutableStateList()
splitTunnelApps.sortedWith(compareBy(collator) { it.name }).filter { it.name.lowercase().contains(query.lowercase()) }.toMutableStateList()
}
}
LaunchedEffect(Unit) {
// clean up any split tunnel packages for apps that were uninstalled
viewModel.cleanUpUninstalledApps(config, splitTunnelApps.map { it.`package` })
viewModel.cleanUpUninstalledApps(tunnelConfig, splitTunnelApps.map { it.`package` })
}
Scaffold(
@@ -127,7 +125,7 @@ fun SplitTunnelScreen(appUiState: AppUiState, tunnelId: Int, viewModel: AppViewM
}
SplitOptions.ALL -> Unit
}
viewModel.saveConfigChanges(config, `interface` = proxyInterface)
viewModel.updateExistingTunnelConfig(tunnelConfig, `interface` = proxyInterface)
}) {
val icon = Icons.Outlined.Save
Icon(
@@ -8,10 +8,8 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.NetworkPing
import androidx.compose.material.icons.outlined.PhoneAndroid
import androidx.compose.material.icons.outlined.Security
import androidx.compose.material.icons.outlined.SettingsEthernet
@@ -28,37 +26,31 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.AppUiState
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.common.config.SubmitConfigurationTextBox
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.components.TrustedNetworkTextBox
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.components.WildcardsLabel
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.isValidIpv4orIpv6Address
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
@Composable
fun TunnelAutoTunnelScreen(appUiState: AppUiState, tunnelId: Int, tunnelAutoTunnelViewModel: TunnelAutoTunnelViewModel = hiltViewModel()) {
val config = remember { appUiState.tunnels.first { it.id == tunnelId } }
fun TunnelAutoTunnelScreen(tunnelConfig: TunnelConfig, settings: Settings, tunnelAutoTunnelViewModel: TunnelAutoTunnelViewModel = hiltViewModel()) {
var currentText by remember { mutableStateOf("") }
LaunchedEffect(config.tunnelNetworks) {
LaunchedEffect(tunnelConfig.tunnelNetworks) {
currentText = ""
}
Scaffold(
topBar = {
TopNavBar(config.name)
TopNavBar(tunnelConfig.name)
},
) { padding ->
Column(
@@ -92,11 +84,11 @@ fun TunnelAutoTunnelScreen(appUiState: AppUiState, tunnelId: Int, tunnelAutoTunn
},
trailing = {
ScaledSwitch(
config.isMobileDataTunnel,
onClick = { tunnelAutoTunnelViewModel.onToggleIsMobileDataTunnel(config) },
tunnelConfig.isMobileDataTunnel,
onClick = { tunnelAutoTunnelViewModel.onToggleIsMobileDataTunnel(tunnelConfig) },
)
},
onClick = { tunnelAutoTunnelViewModel.onToggleIsMobileDataTunnel(config) },
onClick = { tunnelAutoTunnelViewModel.onToggleIsMobileDataTunnel(tunnelConfig) },
),
SelectionItem(
Icons.Outlined.SettingsEthernet,
@@ -114,82 +106,14 @@ fun TunnelAutoTunnelScreen(appUiState: AppUiState, tunnelId: Int, tunnelAutoTunn
},
trailing = {
ScaledSwitch(
config.isEthernetTunnel,
onClick = { tunnelAutoTunnelViewModel.onToggleIsEthernetTunnel(config) },
tunnelConfig.isEthernetTunnel,
onClick = { tunnelAutoTunnelViewModel.onToggleIsEthernetTunnel(tunnelConfig) },
)
},
onClick = { tunnelAutoTunnelViewModel.onToggleIsEthernetTunnel(config) },
),
SelectionItem(
Icons.Outlined.NetworkPing,
title = {
Text(
stringResource(R.string.restart_on_ping),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = {
ScaledSwitch(
checked = config.isPingEnabled,
onClick = { tunnelAutoTunnelViewModel.onToggleRestartOnPing(config) },
)
},
onClick = { tunnelAutoTunnelViewModel.onToggleRestartOnPing(config) },
onClick = { tunnelAutoTunnelViewModel.onToggleIsEthernetTunnel(tunnelConfig) },
),
),
)
if (config.isPingEnabled || appUiState.settings.isPingEnabled) {
add(
SelectionItem(
title = {},
description = {
SubmitConfigurationTextBox(
config.pingIp,
stringResource(R.string.set_custom_ping_ip),
stringResource(R.string.default_ping_ip),
isErrorValue = { !it.isNullOrBlank() && !it.isValidIpv4orIpv6Address() },
onSubmit = {
tunnelAutoTunnelViewModel.saveTunnelChanges(
config.copy(pingIp = it.ifBlank { null }),
)
},
)
fun isSecondsError(seconds: String?): Boolean {
return seconds?.let { value -> if (value.isBlank()) false else value.toLong() >= Long.MAX_VALUE / 1000 } ?: false
}
SubmitConfigurationTextBox(
config.pingInterval?.let { (it / 1000).toString() },
stringResource(R.string.set_custom_ping_internal),
"(${stringResource(R.string.optional_default)} ${Constants.PING_INTERVAL / 1000})",
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Done,
),
isErrorValue = ::isSecondsError,
onSubmit = {
tunnelAutoTunnelViewModel.saveTunnelChanges(
config.copy(pingInterval = if (it.isBlank()) null else it.toLong() * 1000),
)
},
)
SubmitConfigurationTextBox(
config.pingCooldown?.let { (it / 1000).toString() },
stringResource(R.string.set_custom_ping_cooldown),
"(${stringResource(R.string.optional_default)} ${Constants.PING_COOLDOWN / 1000})",
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
),
isErrorValue = ::isSecondsError,
onSubmit = {
tunnelAutoTunnelViewModel.saveTunnelChanges(
config.copy(pingCooldown = if (it.isBlank()) null else it.toLong() * 1000),
)
},
)
},
),
)
}
add(
SelectionItem(
title = {
@@ -229,13 +153,13 @@ fun TunnelAutoTunnelScreen(appUiState: AppUiState, tunnelId: Int, tunnelAutoTunn
},
description = {
TrustedNetworkTextBox(
config.tunnelNetworks,
onDelete = { tunnelAutoTunnelViewModel.onDeleteRunSSID(it, config) },
tunnelConfig.tunnelNetworks,
onDelete = { tunnelAutoTunnelViewModel.onDeleteRunSSID(it, tunnelConfig) },
currentText = currentText,
onSave = { tunnelAutoTunnelViewModel.onSaveRunSSID(it, config) },
onSave = { tunnelAutoTunnelViewModel.onSaveRunSSID(it, tunnelConfig) },
onValueChange = { currentText = it },
supporting = {
if (appUiState.settings.isWildcardsEnabled) {
if (settings.isWildcardsEnabled) {
WildcardsLabel()
}
},
@@ -61,23 +61,6 @@ constructor(
}
}
fun onTogglePrimaryTunnel(tunnelConfig: TunnelConfig) = viewModelScope.launch {
appDataRepository.tunnels.updatePrimaryTunnel(
when (tunnelConfig.isPrimaryTunnel) {
true -> null
false -> tunnelConfig
},
)
}
fun onToggleRestartOnPing(tunnelConfig: TunnelConfig) = viewModelScope.launch {
appDataRepository.tunnels.save(
tunnelConfig.copy(
isPingEnabled = !tunnelConfig.isPingEnabled,
),
)
}
fun onToggleIsEthernetTunnel(tunnelConfig: TunnelConfig) = viewModelScope.launch {
if (tunnelConfig.isEthernetTunnel) {
appDataRepository.tunnels.updateEthernetTunnel(null)
@@ -78,15 +78,9 @@ fun <T> CoroutineScope.asChannel(flow: Flow<T>): ReceiveChannel<T> = produce {
}
}
fun Job?.onNotRunning(callback: () -> Unit) {
if (this == null || this.isCompleted || this.isCompleted) {
callback.invoke()
}
}
fun Job.cancelWithMessage(message: String) {
kotlin.runCatching {
this.cancel()
cancel()
Timber.i(message)
}
}
@@ -46,11 +46,10 @@ fun Peer.isReachable(): Boolean {
} else {
Constants.DEFAULT_PING_IP
}
Timber.i("Checking reachability of peer: $host")
Timber.d("Checking reachability of peer: $host")
val reachable =
InetAddress.getByName(host)
.isReachable(Constants.PING_TIMEOUT.toInt())
Timber.i("Result: reachable - $reachable")
return reachable
}
+21 -14
View File
@@ -12,8 +12,7 @@
<string name="tunnel_name">Název tunelu</string>
<string name="exclude">Vyloučit</string>
<string name="include">Zahrnout</string>
<string name="save_changes">Uložit</string>
<string name="endpoint">Koncový bod</string>
<string name="endpoint">Koncový bod</string>
<string name="always_on_vpn_support">Povolit trvalé připojení VPN</string>
<string name="location_services_not_detected">Služby polohy nebyly detekovány</string>
<string name="hint_search_packages">Hledat balíčky</string>
@@ -63,10 +62,8 @@
<string name="tunnel_on_ethernet">Tunelovat na ethernetu</string>
<string name="prominent_background_location_message">Tato funkce vyžaduje oprávnění pro přístup k poloze na pozadí pro zapnutí monitorování Wi-Fi SSID, i když je aplikace zavřená. Pro více detailů, podívejte se prosím na zásady soukromí umístěné v kategorii Podpora.</string>
<string name="tunnels">Tunely</string>
<string name="tunnel_all">Tunelovat všechny aplikace</string>
<string name="config_changes_saved">Změny v konfiguraci uloženy.</string>
<string name="icon">Ikona</string>
<string name="public_key">Veřejný klíč</string>
<string name="config_changes_saved">Změny v konfiguraci uloženy.</string>
<string name="public_key">Veřejný klíč</string>
<string name="addresses">Adresy</string>
<string name="dns_servers">DNS servery</string>
<string name="allowed_ips">Povolené IP adresy</string>
@@ -74,8 +71,7 @@
<string name="auto_tunneling">Automatické tunelování</string>
<string name="turn_on_tunnel">Akce vyžaduje aktivní tunel</string>
<string name="interface_">Rozhraní</string>
<string name="done">Hotovo</string>
<string name="rotate_keys">Rotovat klíče</string>
<string name="rotate_keys">Rotovat klíče</string>
<string name="private_key">Soukromý klíč</string>
<string name="copy_public_key">Kopírovat veřejný klíč</string>
<string name="base64_key">base64 klíč</string>
@@ -87,9 +83,7 @@
<string name="yes">Ano</string>
<string name="tunneling_apps">Tunelování aplikací</string>
<string name="all">vše</string>
<string name="included">zahrnuto</string>
<string name="excluded">vyloučeno</string>
<string name="no_email_detected">Žádná emailová aplikace nebyla nalezena</string>
<string name="no_email_detected">Žádná emailová aplikace nebyla nalezena</string>
<string name="no_browser_detected">Žádný prohlížeč nebyl nalezen</string>
<string name="read_logs">Přečíst si logy</string>
<string name="pin_created">PIN úspěšně vytvořen</string>
@@ -101,8 +95,21 @@
<string name="support">Podpora</string>
<string name="app_name">WG Tunnel</string>
<string name="mtu">MTU</string>
<string name="db_name">wg-tunnel-db</string>
<string name="listen_port">Naslouchací port</string>
<string name="listen_port">Naslouchací port</string>
<string name="auto">(automaticky)</string>
<string name="kernel">Kernel</string>
</resources>
<string name="persistent_keepalive">Udržování spojení</string>
<string name="restart_at_boot">Restartovat při spuštění</string>
<string name="show_amnezia_properties">Zobrazit Amnezia možnosti</string>
<string name="language">Jazyk</string>
<string name="light">Světlé</string>
<string name="appearance">Vzhled</string>
<string name="display_theme">Téma zobrazení</string>
<string name="dark">Tmavé</string>
<string name="dynamic">Podle systému</string>
<string name="notifications">Oznámení</string>
<string name="kill_switch">Vypínač</string>
<string name="wifi_name_via_shell">Název WiFi přes shell</string>
<string name="use_root_shell_for_wifi">Použít root shell pro získání názvu WiFi</string>
<string name="add_from_clipboard">Přidat ze schránky</string>
</resources>
+42 -19
View File
@@ -5,7 +5,7 @@
<string name="no_tunnels">Noch keine Tunnel hinzugefügt!</string>
<string name="tunnels">Tunnel</string>
<string name="tunnel_mobile_data">Tunnel für mobile Daten</string>
<string name="privacy_policy">Siehe Privacy Policy</string>
<string name="privacy_policy">Datenschutzbestimmungen anzeigen</string>
<string name="okay">Ok</string>
<string name="tunnel_on_ethernet">Tunnel für Ethernet</string>
<string name="auto_tunneling">Auto-Tunneln</string>
@@ -21,10 +21,7 @@
<string name="tunnel_name">Tunnel-Name</string>
<string name="exclude">Ausschließen</string>
<string name="include">Einschließen</string>
<string name="tunnel_all">Alle Apps tunneln</string>
<string name="config_changes_saved">Konfigurationsänderungen gespeichert.</string>
<string name="save_changes">Speichern</string>
<string name="icon">Symbol</string>
<string name="public_key">Öffentlicher Schlüssel</string>
<string name="addresses">Adressen</string>
<string name="dns_servers">DNS-Server</string>
@@ -36,12 +33,10 @@
<string name="always_on_vpn_support">Always-On VPN erlauben</string>
<string name="location_services_not_detected">Standortdienste nicht erkannt</string>
<string name="hint_search_packages">Pakete suchen</string>
<string name="db_name">wg-tunnel-db</string>
<string name="vpn_on">VPN an</string>
<string name="vpn_off">VPN aus</string>
<string name="create_import">Von Grund auf neu erstellen</string>
<string name="add_peer">Peer hinzufügen</string>
<string name="done">Erledigt</string>
<string name="add_peer">Gegenstelle hinzufügen</string>
<string name="rotate_keys">Schlüssel rotieren</string>
<string name="private_key">Privater Schlüssel</string>
<string name="copy_public_key">Öffentlichen Schlüssel kopieren</string>
@@ -65,15 +60,14 @@
<string name="email_description">Sende mir eine E-Mail</string>
<string name="error_root_denied">Root Shell verweigert</string>
<string name="error_no_file_explorer">Kein Datei-Explorer installiert</string>
<string name="location_services_missing_message">Die App erkennt keine auf deinem Gerät aktivierten Standortdienste. Je nach Gerät kann dies dazu führen, dass die Funktion \"Nicht vertrauenswürdiges WLAN\" den WLAN-Namen nicht lesen kann. Möchtest du trotzdem fortfahren?</string>
<string name="auto_tunnel_title">Auto-Tunnel Service</string>
<string name="location_services_missing_message">Die App erkennt keine auf deinem Gerät aktivierten Standortdienste. Je nach Gerät kann dies dazu führen, dass die Funktion nicht vertrauenswürdiges WLAN den WLAN-Namen nicht lesen kann. Möchtest du trotzdem fortfahren?</string>
<string name="auto_tunnel_title">Auto-Tunnel Dienst</string>
<string name="delete_tunnel_message">Bist du sicher, dass du den Tunnel löschen möchtest?</string>
<string name="yes">Ja</string>
<string name="excluded">ausgeschlossen</string>
<string name="all">alle</string>
<string name="no_browser_detected">Keinen Browser erkannt</string>
<string name="open_issue">Issue öffnen</string>
<string name="read_logs">Logs lesen</string>
<string name="read_logs">Logeinträge lesen</string>
<string name="auto">(automatisch)</string>
<string name="incorrect_pin">PIN nicht korrekt</string>
<string name="pin_created">PIN erfolgreich erstellt</string>
@@ -84,11 +78,10 @@
<string name="error_authentication_failed">Authentifizierung fehlgeschlagen</string>
<string name="export_configs">Konfigurationen exportieren</string>
<string name="unknown_error">Unbekannter Fehler aufgetreten</string>
<string name="email_chooser">Sende eine E-Mail…</string>
<string name="email_chooser">Eine E-Mail senden</string>
<string name="error_authorization_failed">Autorisierung fehlgeschlagen</string>
<string name="error_invalid_code">Ungültiger QR Code</string>
<string name="tunneling_apps">Getunnelte Apps</string>
<string name="included">eingeschlossen</string>
<string name="no_email_detected">Keine E-Mail-App erkannt</string>
<string name="create_pin">PIN erstellen</string>
<string name="use_tunnel_on_wifi_name">Tunnel für WLAN-Namen verwenden</string>
@@ -96,7 +89,6 @@
<string name="restart_on_ping">Neustart bei PING Fehler (Beta)</string>
<string name="edit_tunnel">Tunnel bearbeiten</string>
<string name="set_primary_tunnel">Als Primären Tunnel setzen</string>
<string name="vpn_channel_id">VPN Kanal</string>
<string name="vpn_channel_name">VPN Benachrichtigungskanal</string>
<string name="turn_off_tunnel">Aktion erfordert deaktivierten Tunnel</string>
<string name="kernel">Kernel</string>
@@ -123,23 +115,22 @@
<string name="chat_description">Tritt der Community bei</string>
<string name="set_custom_ping_cooldown">Ping-Neustart-Cooldown (sek)</string>
<string name="set_custom_ping_ip">Benutzerdefinierte Ping-IP einstellen</string>
<string name="default_ping_ip">(optional, Vorgabe ist peers)</string>
<string name="default_ping_ip">(optional, Standardwert: Gegenstelle)</string>
<string name="set_custom_ping_internal">Pingintervall (sek)</string>
<string name="always_on_message2">um sicherzustellen, dass Always-on VPN für alle anderen Apps ausgeschaltet ist und versuche es erneut</string>
<string name="tunnel_required">Feature erfordert mindestens einen Tunnel</string>
<string name="sec">Sek</string>
<string name="app_settings">App-Einstellungen</string>
<string name="background_location_message2">um sicherzustellen, dass diese Berechtigungen aktiviert sind.</string>
<string name="background_location_message2">um sicherzustellen, dass diese Berechtigungen aktiviert sind</string>
<string name="root_accepted">Root-Shell akzeptiert</string>
<string name="show_amnezia_properties">Amnezia-Eigenschaften anzeigen</string>
<string name="never">nie</string>
<string name="handshake">Handshake</string>
<string name="vpn_denied_dialog_title">Genehmigung verweigert</string>
<string name="logs">Logs</string>
<string name="logs">Logeinträge</string>
<string name="kernel_not_supported">Kernel nicht unterstützt</string>
<string name="trusted_wifi_names">Vertrauenswürdige WLAN Namen</string>
<string name="requires_app_relaunch">Diese Änderung erfordert einen Neustart der App. Möchten Sie fortfahren?</string>
<string name="selected">Ausgewählt</string>
<string name="use_root_shell_for_wifi">Root-Shell verwenden, um WLAN-Namen zu ermitteln</string>
<string name="light">Hell</string>
<string name="add_wifi_name">WLAN Namen hinzufügen</string>
@@ -169,4 +160,36 @@
<string name="use_wildcards">Wildcards für Namen verwenden</string>
<string name="stop_auto">Auto-Tunnel stoppen</string>
<string name="monitoring_state_changes">Überwache Statusänderungen</string>
</resources>
<string name="stop_on_no_internet">Stoppen wenn keine Internetverbindung besteht</string>
<string name="ethernet_tunnel">Ethernet Tunnel</string>
<string name="set_ethernet_tunnel">Als Ethernet Tunnel setzten</string>
<string name="native_kill_switch">Nativer Notschalter</string>
<string name="vpn_kill_switch">VPN Notschalter</string>
<string name="kill_switch_options">Notschalteroptionen</string>
<string name="allow_lan_traffic">LAN Verkehr erlauben</string>
<string name="vpn_channel_description">Ein Kanal für VPN-Statusbenachrichtigungen</string>
<string name="auto_tunnel_channel_name">Auto-Tunnel-Benachrichtigungskanal</string>
<string name="auto_tunnel_channel_description">Ein Kanal für Auto-Tunnel Statusbenachrichtigungen</string>
<string name="stop">Stop</string>
<string name="splt_tunneling">Tunneling aufteilen</string>
<string name="tunnel_specific_settings">Tunnelspezifische Einstellungen</string>
<string name="show_scripts">Skripte anzeigen</string>
<string name="pre_up">Vor Aktivierung</string>
<string name="post_up">Nach Aktivierung</string>
<string name="pre_down">Vor Deaktivierung</string>
<string name="post_down">Nach Deaktivierung</string>
<string name="amnezia_kernel_message">Amnezia nicht verfügbar im Kernel-Modus</string>
<string name="enable_amnezia">Amnezia aktivieren</string>
<string name="wg_compat_mode">Wireguard Kompatibilitätsmodus</string>
<string name="quick_actions">Schnellaktionen</string>
<string name="stop_on_internet_loss">Stoppen, wenn die Internetverbindung getrennt wird</string>
<string name="bypass_lan_for_kill_switch">LAN umgehen für Notschalter</string>
<string name="advanced_settings">Erweiterte Einstellungen</string>
<string name="debounce_delay">Entprellverzögerung</string>
<string name="hide_scripts">Skripte verbergen</string>
<string name="enable_amnezia_compatibility">Amnezia Kompatibilität aktivieren</string>
<string name="remove_amnezia_compatibility">Amnezia Kompatibilität entfernen</string>
<string name="hide_amnezia_properties">Amnezia Eigenschaften verbergen</string>
<string name="include_lan">LAN einschließen</string>
<string name="exclude_lan">LAN ausschließen</string>
</resources>
+99 -12
View File
@@ -4,12 +4,10 @@
<string name="email_chooser">Enviar un email…</string>
<string name="kernel">Kernel</string>
<string name="add_peer">Añadir peer</string>
<string name="done">Hecho</string>
<string name="copy_public_key">Copiar public key</string>
<string name="base64_key">clave base64</string>
<string name="tunnel_on_wifi">Túnel en Wi-Fi no de confianza</string>
<string name="location_services_missing_message">La app no detecta activado el servicio de ubicación en tu dispositivo. Dependiendo del dispositivo, esto podría hacer que la característica de Wi-Fi no de confianza falle al leer el nombre Wi-Fi. ¿Quieres continuar de todas formas?</string>
<string name="excluded">excluida(s)</string>
<string name="all">todas</string>
<string name="mobile_data_tunnel">Establecer como túnel en datos móviles</string>
<string name="use_tunnel_on_wifi_name">Usar tunnel en nombre Wi-Fi</string>
@@ -24,10 +22,7 @@
<string name="tunnel_name">Nombre de túnel</string>
<string name="exclude">Excluir</string>
<string name="include">Incluir</string>
<string name="tunnel_all">Todas las apps por el túnel</string>
<string name="config_changes_saved">Cambios de configuración guardados.</string>
<string name="save_changes">Guardar</string>
<string name="icon">Icono</string>
<string name="mtu">MTU</string>
<string name="dns_servers">DNS servers</string>
<string name="addresses">Addresses</string>
@@ -37,7 +32,7 @@
<string name="no_tunnels">¡Ningún túnel añadido aún!</string>
<string name="tunnels">Túneles</string>
<string name="tunnel_mobile_data">Activar túnel en datos móviles</string>
<string name="privacy_policy">Ver Política de Privacidad</string>
<string name="privacy_policy">Ver política de privacidad</string>
<string name="okay">OK</string>
<string name="tunnel_on_ethernet">Túnel en ethernet</string>
<string name="prominent_background_location_title">Divulgación de la ubicación en segundo plano</string>
@@ -48,7 +43,6 @@
<string name="always_on_vpn_support">Permitir VPN siempre-activada</string>
<string name="location_services_not_detected">Servicios de Ubicación No Detectados</string>
<string name="hint_search_packages">Buscar paquetes</string>
<string name="db_name">wg-tunnel-db</string>
<string name="auto_tunneling">Túnel-automático</string>
<string name="vpn_on">VPN on</string>
<string name="vpn_off">VPN off</string>
@@ -78,12 +72,11 @@
<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="auto_tunnel_title">Servicio de túnel-automático</string>
<string name="delete_tunnel">Eliminar túnel</string>
<string name="delete_tunnel_message">¿Estás seguro de que quieres eliminar este túnel?</string>
<string name="yes"></string>
<string name="tunneling_apps">Apps por el túnel</string>
<string name="included">incluida(s)</string>
<string name="no_email_detected">Ninguna app de email detectada</string>
<string name="no_browser_detected">Ningún navegador detectado</string>
<string name="open_issue">Abrir una incidencia</string>
@@ -99,10 +92,104 @@
<string name="edit_tunnel">Editar túnel</string>
<string name="settings">Ajustes</string>
<string name="app_name">WG Tunnel</string>
<string name="vpn_channel_id">Canal VPN</string>
<string name="vpn_channel_name">Canal de notificación VPN</string>
<string name="prominent_background_location_message">La monitorización SSID Wi-Fi necesita de permiso de ubicación en segundo plano incluso si la app está cerrada. Mira el enlace a la Política de Privacidad en la pantalla de ayuda para más detalles.</string>
<string name="prominent_background_location_message">La monitorización SSID Wi-Fi necesita de permiso de ubicación en segundo plano incluso si la app está cerrada. Mira el enlace a la Política de Privacidad en la pantalla de ayuda para más detalles.</string>
<string name="junk_packet_count">Recuento de paquetes basura</string>
<string name="junk_packet_minimum_size">Tamaño mínimo del paquete basura</string>
<string name="add_from_clipboard">Agregar desde el portapapeles</string>
</resources>
<string name="app_settings">configuración de la aplicación</string>
<string name="optional_default">"opcional, por defecto: "</string>
<string name="show_amnezia_properties">Mostrar propiedades de Amnezia</string>
<string name="never">nunca</string>
<string name="sec">seg</string>
<string name="set_custom_ping_internal">Intervalo del Ping (seg)</string>
<string name="transport_packet_magic_header">Encabezado del paquete de transporte</string>
<string name="getting_started_guide">guía de inicio rápido</string>
<string name="always_on_message">Se ha denegado el permiso de conexión VPN. Por favor, compruebe el</string>
<string name="always_on_message2">para asegurarse de que el VPN Siempre encendido esté desactivada para todas las demás aplicaciones e inténtelo de nuevo</string>
<string name="response_packet_magic_header">Encabezado del paquete de respuesta</string>
<string name="junk_packet_maximum_size">Tamaño máximo del paquete basura</string>
<string name="init_packet_junk_size">Tamaño basura del paquete de inicialización</string>
<string name="unsure_how">si no está seguro de cómo continuar</string>
<string name="see_the">Ver la</string>
<string name="error_file_format">Formato de configuración de túnel no válido</string>
<string name="vpn_denied_dialog_title">Permiso denegado</string>
<string name="vpn_settings">Configuraciones VPN del sistema</string>
<string name="chat_description">Unirse a la comunidad</string>
<string name="tunnel_required">La función requiere al menos un túnel</string>
<string name="background_location_message">Permitir todo el tiempo que se requiera permiso de ubicación y/o ubicación precisa para esta función. Consultar</string>
<string name="response_packet_junk_size">Tamaño basura del paquete de respuesta</string>
<string name="init_packet_magic_header">Encabezado del paquete de inicialización</string>
<string name="background_location_message2">para asegurarse de que estos permisos estén habilitados</string>
<string name="root_accepted">Shell root permitido</string>
<string name="set_custom_ping_ip">Establecer ping ip personalizado</string>
<string name="default_ping_ip">(opcional, por defecto de los pares)</string>
<string name="set_custom_ping_cooldown">Tiempo de espera de reinicio del Ping (seg)</string>
<string name="restart_at_boot">Reiniciar al arrancar</string>
<string name="underload_packet_magic_header">Encabezado del paquete baja carga</string>
<string name="handshake">Protocolo de intercambio</string>
<string name="donate">Donar al proyecto</string>
<string name="appearance">Apariencia</string>
<string name="logs">Registros</string>
<string name="trusted_wifi_names">Nombres de wifis confiables</string>
<string name="use_wildcards">Usar comodines de nombre</string>
<string name="primary_tunnel">Túnel principal</string>
<string name="add_wifi_name">Añadir nombre de wifi</string>
<string name="on_demand_rules">Reglas del túnel bajo demanda</string>
<string name="kernel_not_supported">Kernel no compatible</string>
<string name="start_auto">Iniciar túnel automático</string>
<string name="stop_auto">Detener túnel automático</string>
<string name="local_logging">Registro local</string>
<string name="enable_local_logging">Habilitar registro local</string>
<string name="configuration_change">Cambio de configuración</string>
<string name="exclude_lan">Excluir LAN</string>
<string name="enable_amnezia_compatibility">Activar compatibilidad con Amnezia</string>
<string name="wifi_name_via_shell">Nombre del wifi a través del shell</string>
<string name="include_lan">Incluir LAN</string>
<string name="tunnel_specific_settings">Ajustes específicos del túnel</string>
<string name="hide_scripts">Ocultar scripts</string>
<string name="advanced_settings">Configuraciones avanzadas</string>
<string name="show_scripts">Mostrar scripts</string>
<string name="vpn_channel_description">Canal para notificaciones de estado de VPN</string>
<string name="auto_tunnel_channel_description">Canal para notificaciones de estado del túnel automático</string>
<string name="debounce_delay">Retardo de rebote</string>
<string name="hide_amnezia_properties">Ocultar propiedades de Amnezia</string>
<string name="skip">Saltar</string>
<string name="kill_switch">Interruptor de apagado</string>
<string name="automatic">Automático</string>
<string name="notifications">Notificaciones</string>
<string name="light">Claro</string>
<string name="dark">Oscuro</string>
<string name="requires_app_relaunch">Este cambio requiere un reinicio de la aplicación. ¿Desea continuar?</string>
<string name="use_root_shell_for_wifi">Utilizar el shell root para obtener el nombre del wifi</string>
<string name="tunnel_running">Túnel funcionando</string>
<string name="monitoring_state_changes">Monitorizando cambios de estado</string>
<string name="dynamic">Dinámico</string>
<string name="language">Idioma</string>
<string name="display_theme">Tema de la pantalla</string>
<string name="mobile_tunnel">Túnel de los datos móviles</string>
<string name="launch_app_settings">Iniciar la configuración de la aplicación</string>
<string name="learn_more">Aprender más</string>
<string name="wildcards_active">Comodines activos</string>
<string name="stop_on_no_internet">Detener cuando no hay internet</string>
<string name="stop_on_internet_loss">Detener túnel cuando se pierda el internet</string>
<string name="ethernet_tunnel">Túnel ethernet</string>
<string name="set_ethernet_tunnel">Establecer como túnel ethernet</string>
<string name="native_kill_switch">Interruptor de apagado nativo</string>
<string name="vpn_kill_switch">Interruptor de apagado VPN</string>
<string name="kill_switch_options">Opciones del interruptor de apagado</string>
<string name="allow_lan_traffic">Permitir tráfico LAN</string>
<string name="bypass_lan_for_kill_switch">Excluir LAN del interruptor de apagado</string>
<string name="auto_tunnel_channel_name">Canal de notificación del túnel automático</string>
<string name="stop">detener</string>
<string name="splt_tunneling">Túnel dividido</string>
<string name="pre_up">Pre up</string>
<string name="post_up">Post up</string>
<string name="pre_down">Pre down</string>
<string name="amnezia_kernel_message">Amnezia no disponible en modo kernel</string>
<string name="post_down">Post down</string>
<string name="enable_amnezia">Activar Amnezia</string>
<string name="wg_compat_mode">Modo compatibilidad de WG</string>
<string name="quick_actions">Acciones rápidas</string>
<string name="remove_amnezia_compatibility">Eliminar compatibilidad con Amnezia</string>
</resources>
+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+12 -21
View File
@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="vpn_channel_id">Canal VPN</string>
<string name="turn_off_tunnel">Cette action nécessite la désactivation du tunnel</string>
<string name="turn_off_tunnel">Cette action nécessite la désactivation du tunnel</string>
<string name="no_tunnels">Aucun tunnel n\'a été ajouté pour le moment!</string>
<string name="tunnels">Tunnels</string>
<string name="tunnel_mobile_data">Tunnel sur données mobiles</string>
@@ -13,9 +12,6 @@
<string name="open_file">Fichier ouvert</string>
<string name="qr_scan">Scan du code QR</string>
<string name="exclude">Exclure</string>
<string name="tunnel_all">Tunneliser toutes les applications</string>
<string name="save_changes">Sauvegarder</string>
<string name="icon">Icône</string>
<string name="dns_servers">Serveurs DNS</string>
<string name="mtu">MTU</string>
<string name="peer">Pair</string>
@@ -29,7 +25,6 @@
<string name="vpn_off">VPN éteint</string>
<string name="turn_on_tunnel">Cette action nécessite un tunnel actif</string>
<string name="add_peer">Ajouter un pair</string>
<string name="done">Terminé</string>
<string name="rotate_keys">Rotation des clés</string>
<string name="error_file_extension">Le fichier n\'est pas un .conf ou .zip</string>
<string name="vpn_channel_name">Canal de notifications VPN</string>
@@ -43,18 +38,17 @@
<string name="config_changes_saved">Changements de la configuration sauvegardés.</string>
<string name="public_key">Clé publique</string>
<string name="addresses">Adresses</string>
<string name="db_name">wg-tunnel-db</string>
<string name="vpn_on">VPN allumé</string>
<string name="vpn_on">VPN allumé</string>
<string name="create_import">Créer à partir de zéro</string>
<string name="private_key">Clé privée</string>
<string name="interface_">Interface</string>
<string name="copy_public_key">Copier la clé publique</string>
<string name="base64_key">clé base64</string>
<string name="comma_separated_list">liste séparée par des virgules</string>
<string name="listen_port">Écouter sur le port</string>
<string name="listen_port">Port d\'écoute</string>
<string name="random">(aléatoire)</string>
<string name="optional_no_recommend">(optionnel, non recommandé)</string>
<string name="persistent_keepalive">Keepalive persistent</string>
<string name="persistent_keepalive">Keepalive persistant</string>
<string name="optional">(optionnel)</string>
<string name="preshared_key">Clé pré-partagée</string>
<string name="cancel">Annuler</string>
@@ -62,7 +56,7 @@
<string name="error_authentication_failed">Échec de l\'authentification</string>
<string name="error_authorization_failed">Autorisation échouée</string>
<string name="export_configs">Exporter les configs</string>
<string name="tunnel_on_wifi">Tunnel sur wifi non fiable</string>
<string name="tunnel_on_wifi">Tunnel sur wifi non approuvé</string>
<string name="email_chooser">Envoyer un mail…</string>
<string name="docs_description">Lire la documentation</string>
<string name="email_description">Envoyer un mail</string>
@@ -74,9 +68,7 @@
<string name="delete_tunnel">Supprimer un tunnel</string>
<string name="delete_tunnel_message">Êtes-vous sûr de vouloir supprimer ce tunnel?</string>
<string name="tunneling_apps">Applis de tunnel</string>
<string name="included">inclus</string>
<string name="excluded">exclu</string>
<string name="no_email_detected">Aucune appli de mail détectée</string>
<string name="no_email_detected">Aucune application de mail détectée</string>
<string name="no_browser_detected">Aucun navigateur détecté</string>
<string name="open_issue">Signaler un problème</string>
<string name="incorrect_pin">Code PIN incorrect</string>
@@ -100,17 +92,17 @@
<string name="app_settings">les réglages de l\'application</string>
<string name="background_location_message2">afin de s\'assurer que ces permissions soient actives.</string>
<string name="root_accepted">Accès au shell root autorisé</string>
<string name="set_custom_ping_ip">Personnaliser l\'ip de ping</string>
<string name="set_custom_ping_ip">Personnaliser l\'ip à pinguer</string>
<string name="optional_default">"optionnel, par défaut : "</string>
<string name="show_amnezia_properties">Voir les propriétés d\'Amnezia</string>
<string name="never">jamais</string>
<string name="handshake">handshake</string>
<string name="logs">Journaux</string>
<string name="error_invalid_code">Code QR invalide</string>
<string name="enabled_app_shortcuts">Activer les raccourcis de l\'app</string>
<string name="enabled_app_shortcuts">Activer les raccourcis</string>
<string name="unknown_error">Une erreur inconnue s\'est produite</string>
<string name="email_subject">Assistance WG Tunnel</string>
<string name="location_services_missing_message">L\'application détecte qu\'aucun service de localisation n\'est activé sur votre appareil. Selon votre appareil, cela peut empêcher la fonctionnalité « Wi-Fi de confiance » de lire le nom du Wi-Fi. Souhaitez-vous tout de même continuer?</string>
<string name="location_services_missing_message">L\'application détecte qu\'aucun service de localisation n\'est activé sur votre appareil. Selon votre appareil, cela peut empêcher la fonctionnalité « Wi-Fi approuvés » de lire le nom du Wi-Fi. Souhaitez-vous tout de même continuer?</string>
<string name="yes">Oui</string>
<string name="all">tout</string>
<string name="set_primary_tunnel">Définir comme tunnel principal</string>
@@ -132,7 +124,7 @@
<string name="always_on_message2">afin de s\'assurer que le VPN permanent est désactivé pour toutes les autres applis puis réessayer</string>
<string name="chat_description">Rejoindre la communauté</string>
<string name="background_location_message">L\'accès à la permission de localisation permanente et/ou la localisation précise est nécessaire pour cette fonctionnalité. Veuillez vérifier dans</string>
<string name="default_ping_ip">(facultatif, par défaut aux pairs)</string>
<string name="default_ping_ip">(facultatif, par défaut les pairs)</string>
<string name="set_custom_ping_internal">Intervalle de ping (sec)</string>
<string name="set_custom_ping_cooldown">Temps d\'attente avant redémarrage du ping (sec)</string>
<string name="sec">sec</string>
@@ -150,8 +142,7 @@
<string name="on_demand_rules">Règles de tunnel à la demande</string>
<string name="launch_app_settings">Ouvrir les paramètres de l\'appli</string>
<string name="display_theme">Thème d\'affichage</string>
<string name="selected">Sélectionné</string>
<string name="trusted_wifi_names">Nom wifi de confiance</string>
<string name="trusted_wifi_names">Nom wifi approuvés</string>
<string name="add_wifi_name">Ajouter un nom de wifi</string>
<string name="mobile_tunnel">Tunnel de données mobiles</string>
<string name="skip">Passer</string>
@@ -161,7 +152,7 @@
<string name="wifi_name_via_shell">Nom du Wifi via le shell</string>
<string name="use_root_shell_for_wifi">Utiliser un shell root pour obtenir le nom du wifi</string>
<string name="tunnel_running">Tunnel en cours d\'exécution</string>
<string name="monitoring_state_changes">Surveiller les changements d’état</string>
<string name="monitoring_state_changes">Surveille les changements d’état</string>
<string name="donate">Faire un don au projet</string>
<string name="local_logging">Journalisation locale</string>
<string name="enable_local_logging">Activer la journalisation locale</string>
-4
View File
@@ -26,7 +26,6 @@
<string name="create_import">Buat dari awal</string>
<string name="turn_on_tunnel">Tindakan memerlukan tunnel aktif</string>
<string name="add_peer">Tambahkan rekan</string>
<string name="done">Selesai</string>
<string name="interface_">Interface</string>
<string name="rotate_keys">Putar tombol</string>
<string name="private_key">Kunci privat</string>
@@ -57,7 +56,6 @@
<string name="auto_tunnel_title">Layanan Auto-tunnel</string>
<string name="yes">Ya</string>
<string name="tunneling_apps">Aplikasi tunneling</string>
<string name="included">termasuk</string>
<string name="incorrect_pin">Pin salah</string>
<string name="pin_created">Pin berhasil dibuat</string>
<string name="enter_pin">Masukkan pin Anda</string>
@@ -96,7 +94,6 @@
<string name="never">tidak pernah</string>
<string name="sec">dtk</string>
<string name="handshake">handshake</string>
<string name="vpn_channel_id">Saluran VPN</string>
<string name="vpn_channel_name">Notifikasi Saluran VPN</string>
<string name="prominent_background_location_message">Fitur ini memerlukan izin lokasi latar belakang untuk mengaktifkan pemantauan SSID Wi-Fi bahkan saat aplikasi ditutup. Untuk detail lebih lanjut, silakan lihat Kebijakan Privasi yang ditautkan di layar Dukungan.</string>
<string name="copy_public_key">Salin kunci publik</string>
@@ -107,7 +104,6 @@
<string name="delete_tunnel_message">Apakah Anda yakin ingin menghapus tunnel ini?</string>
<string name="no_email_detected">Tidak ada aplikasi email yang terdeteksi</string>
<string name="no_browser_detected">Tidak ada browser yang terdeteksi</string>
<string name="excluded">dikecualikan</string>
<string name="all">semua</string>
<string name="open_issue">Membuka masalah</string>
<string name="read_logs">Baca log</string>
+58 -1
View File
@@ -1,2 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="error_file_extension">Il file non è .conf o .zip</string>
<string name="allowed_ips">IP consentiti</string>
<string name="endpoint">Endpoint</string>
<string name="interface_">Interfaccia</string>
<string name="rotate_keys">Ruota chiavi</string>
<string name="private_key">Chiave privata</string>
<string name="base64_key">chiave base64</string>
<string name="comma_separated_list">elenco separato da virgola</string>
<string name="listen_port">Porta d\'ascolto</string>
<string name="error_authorization_failed">Autorizzazione fallita</string>
<string name="error_no_file_explorer">Nessun esploratore di file installato</string>
<string name="error_invalid_code">QR code non valido</string>
<string name="app_name">Tunnel WG</string>
<string name="vpn_channel_name">Canale di notifica VPN</string>
<string name="turn_off_tunnel">L\'operaz. richiede la disatt. del tunnel</string>
<string name="public_key">Chiave pubblica</string>
<string name="addresses">Indirizzi</string>
<string name="dns_servers">Server DNS</string>
<string name="enabled_app_shortcuts">Abilita le scorciatoie da app</string>
<string name="export_configs">Esporta configurazioni</string>
<string name="email_subject">Supporto di Tunnel WG</string>
<string name="email_chooser">Invia un email…</string>
<string name="docs_description">Read the docs</string>
<string name="email_description">Mandami un\'email (in inglese)</string>
<string name="no_tunnels">Ancora nessun tunnel aggiunto!</string>
<string name="peer">Peer</string>
<string name="mtu">MTU</string>
<string name="random">(casuale)</string>
<string name="optional">(opzionale)</string>
<string name="optional_no_recommend">(opzionale, non raccomandato)</string>
<string name="preshared_key">Chiave pre-condivisa</string>
<string name="error_authentication_failed">Autenticazione fallita</string>
<string name="tunnels">Tunnels</string>
<string name="tunnel_mobile_data">Tunnel su dati mobili</string>
<string name="privacy_policy">Guarda la policy sulla privacy</string>
<string name="okay">Okay</string>
<string name="tunnel_on_ethernet">Tunnel su ethernet</string>
<string name="name">Nome</string>
<string name="always_on_vpn_support">Permetti VPN sempre attiva</string>
<string name="location_services_not_detected">Servizi di localizzazione non rilevati</string>
<string name="hint_search_packages">Cerca pacchetti</string>
<string name="auto_tunneling">Tunnel automatico</string>
<string name="vpn_on">VPN on</string>
<string name="vpn_off">VPN off</string>
<string name="create_import">Crea da zero</string>
<string name="turn_on_tunnel">L\'operazione richiede un tunnel attivo</string>
<string name="add_peer">Aggiungi peer</string>
<string name="copy_public_key">Copia chiave pubblica</string>
<string name="seconds">secondi</string>
<string name="persistent_keepalive">Keepalive persistente</string>
<string name="cancel">Annulla</string>
<string name="unknown_error">Avvenuto errore sconosciuto</string>
<string name="tunnel_on_wifi">Tunnel su wifi non fidato</string>
<string name="use_kernel">Usa modulo kernel</string>
<string name="error_ssid_exists">L\'SSID esiste già</string>
<string name="error_root_denied">Shell di root negata</string>
</resources>
+1 -2
View File
@@ -2,6 +2,5 @@
<resources>
<string name="app_name">WG Tunnel</string>
<string name="error_file_extension">ファイルが.confまたは.zipではありません</string>
<string name="vpn_channel_id">VPNチャンネル</string>
<string name="vpn_channel_name">VPN通知チャンネル</string>
</resources>
</resources>
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+1 -9
View File
@@ -4,15 +4,13 @@
<string name="turn_off_tunnel">Actie vereist uitgeschakelde tunnel</string>
<string name="tunnel_mobile_data">Tunnel bij mobiele data</string>
<string name="app_name">WG Tunnel</string>
<string name="vpn_channel_id">VPN Kanaal</string>
<string name="vpn_channel_name">VPN Notificatiekanaal</string>
<string name="vpn_channel_name">VPN Notificatiekanaal</string>
<string name="tunnels">Tunnels</string>
<string name="privacy_policy">Bekijk privacybeleid</string>
<string name="tunnel_on_ethernet">Tunnel bij ethernet</string>
<string name="no_tunnels">Nog geen tunnels geconfigureerd!</string>
<string name="okay">Oke</string>
<string name="error_authentication_failed">Authenticatie mislukt</string>
<string name="icon">Icoon</string>
<string name="include">Meenemen</string>
<string name="addresses">Adres</string>
<string name="peer">Peer (extern systeem)</string>
@@ -36,18 +34,14 @@
<string name="add_from_qr">Toevoegen met QR code</string>
<string name="qr_scan">QR scannen</string>
<string name="exclude">Uitsluiten</string>
<string name="tunnel_all">Alle applicaties tunnelen</string>
<string name="config_changes_saved">Configuratiewijzigingen opgeslagen.</string>
<string name="comma_separated_list">Komma-gescheiden lijst</string>
<string name="open_file">Bestand openen</string>
<string name="public_key">Publieke sleutel</string>
<string name="tunnel_name">Tunnelnaam</string>
<string name="save_changes">Opslaan</string>
<string name="dns_servers">DNS servers</string>
<string name="mtu">MTU</string>
<string name="db_name">wg-tunnel-db</string>
<string name="vpn_on">VPN aan</string>
<string name="done">Gereed</string>
<string name="interface_">Interface</string>
<string name="create_import">Nieuw beginnen</string>
<string name="rotate_keys">Sleutels roteren</string>
@@ -62,7 +56,6 @@
<string name="preshared_key">Pre-shared key</string>
<string name="auto_tunnel_title">Auto-tunnel service</string>
<string name="error_invalid_code">Ongeldige QR code</string>
<string name="included">inclusief</string>
<string name="open_issue">Open een melding</string>
<string name="create_pin">Stel PIN in</string>
<string name="enable_app_lock">Schakel app-lock in</string>
@@ -90,7 +83,6 @@
<string name="junk_packet_count">Junk packetteller</string>
<string name="incorrect_pin">Foutieve PIN</string>
<string name="enter_pin">Vul PIN in</string>
<string name="excluded">exclusief</string>
<string name="pin_created">PIN aangemaakt</string>
<string name="use_tunnel_on_wifi_name">Gebruik tunnel bij WiFi naam</string>
<string name="error_file_format">Ongeldige tunnelconfiguratie</string>
+192 -1
View File
@@ -2,4 +2,195 @@
<resources>
<string name="tunnels">Tunele</string>
<string name="app_name">WG Tunnel</string>
</resources>
<string name="unsure_how">jeśli nie masz pewności, jak postępować</string>
<string name="getting_started_guide">przewodnik wprowadzający</string>
<string name="peer">Peer</string>
<string name="background_location_message2">w celu upewnienia się, że uprawnienia te są włączone</string>
<string name="rotate_keys">Rotuj klucze</string>
<string name="tunnel_on_ethernet">Tunel przez sieć ethernetową</string>
<string name="public_key">Klucz publiczny</string>
<string name="addresses">Adresy</string>
<string name="mtu">MTU</string>
<string name="auto_tunneling">Autotunelowanie</string>
<string name="vpn_on">VPN włączono</string>
<string name="vpn_off">VPN wyłączono</string>
<string name="turn_on_tunnel">Czynność wymaga aktywnego tunelu</string>
<string name="interface_">Interfejs</string>
<string name="enabled_app_shortcuts">Włącz skróty aplikacji</string>
<string name="privacy_policy">Wyświetl politykę prywatności</string>
<string name="tunnel_mobile_data">Tunel przez mobilną transmisję danych</string>
<string name="hint_search_packages">Wyszukaj pakiety</string>
<string name="random">(losowy)</string>
<string name="pin_created">Kod PIN został pomyślnie utworzony</string>
<string name="enter_pin">Podaj swój kod PIN</string>
<string name="enable_app_lock">Włącz blokadę aplikacji</string>
<string name="response_packet_junk_size">Rozmiar śmieciowego pakietu odpowiedzi</string>
<string name="response_packet_magic_header">Nagłówek magicznego pakietu odpowiedzi</string>
<string name="transport_packet_magic_header">Nagłówek magicznego pakietu transportowego</string>
<string name="underload_packet_magic_header">Nagłówek magicznego pakietu niedociążenia</string>
<string name="see_the">Zobacz</string>
<string name="error_file_format">Nieprawidłowy format konfiguracji tunelu</string>
<string name="restart_at_boot">Uruchom ponownie przy rozruchu</string>
<string name="never">nigdy</string>
<string name="sec">sek.</string>
<string name="handshake">uzgadnianie</string>
<string name="show_amnezia_properties">Pokaż właściwości protokołu Amnezia</string>
<string name="seconds">sek.</string>
<string name="prominent_background_location_message">Ta funkcja wymaga pozwolenia na dostęp do lokalizacji w tle, aby włączyć monitorowanie SSID sieci Wi-Fi nawet wtedy, gdy aplikacja jest zamknięta. Więcej szczegółów znajdziesz w polityce prywatności znajdującej się na ekranie Obsługa.</string>
<string name="optional">(opcjonalnie)</string>
<string name="optional_no_recommend">(opcjonalnie, niezalecane)</string>
<string name="preshared_key">Klucz wstępnie udostępniony</string>
<string name="location_services_missing_message">Aplikacja nie wykrywa żadnych usług lokalizacyjnych włączonych na tym urządzeniu. W zależności od urządzenia może to spowodować, że funkcja niezaufanej sieci Wi-Fi nie będzie w stanie odczytać nazwy sieci Wi-Fi. Czy chcesz kontynuować mimo to?</string>
<string name="read_logs">Przeczytaj dzienniki</string>
<string name="support">Obsługa</string>
<string name="init_packet_magic_header">Nagłówek magicznego pakietu początkowego</string>
<string name="chat_description">Dołącz do społeczności</string>
<string name="cancel">Anuluj</string>
<string name="always_on_message2">w celu upewnienia się, że funkcja stałego VPN jest wyłączona dla wszystkich innych aplikacji, następnie spróbuj ponownie</string>
<string name="requires_app_relaunch">Ta zmiana wymaga ponownego uruchomienia aplikacji. Czy chcesz kontynuować?</string>
<string name="use_tunnel_on_wifi_name">Użyj tunelu na nazwę sieci Wi-Fi</string>
<string name="root_accepted">Zezwolono na powłokę użytkownika root</string>
<string name="tunneling_apps">Aplikacje tunelujące</string>
<string name="vpn_channel_name">Kanał powiadomień VPN</string>
<string name="create_import">Utwórz od nowa</string>
<string name="error_invalid_code">Nieprawidłowy kod QR</string>
<string name="always_on_message">Zezwolenie na połączenie VPN zostało odrzucone. Sprawdź</string>
<string name="persistent_keepalive">Trwałe utrzymywanie połączenia</string>
<string name="vpn_settings">Ustawienia systemowe VPN</string>
<string name="no_browser_detected">Nie wykryto przeglądarki</string>
<string name="configuration_change">Zmiana konfiguracji</string>
<string name="prominent_background_location_title">Ujawnienie lokalizacji w tle</string>
<string name="email_subject">Obsługa aplikacji WG Tunnel</string>
<string name="auto">(automatycznie)</string>
<string name="error_authorization_failed">Nie udało się autoryzować</string>
<string name="add_from_clipboard">Dodaj ze schowka</string>
<string name="delete_tunnel_message">Czy na pewno chcesz usunąć ten tunel?</string>
<string name="tunnel_on_wifi">Tunel przez niezaufaną sieć Wi-Fi</string>
<string name="dns_servers">Serwery DNS</string>
<string name="error_file_extension">Plik nie jest w formacie .conf lub .zip</string>
<string name="export_configs">Eksportuj konfiguracje</string>
<string name="copy_public_key">Skopiuj klucz publiczny</string>
<string name="restart_on_ping">Uruchom ponownie w przypadku niepowodzenia pingowania (beta)</string>
<string name="junk_packet_minimum_size">Minimalny rozmiar pakietu śmieciowego</string>
<string name="open_issue">Otwórz zagadnienie</string>
<string name="thank_you">Dziękujemy za korzystanie z aplikacji WG Tunnel!</string>
<string name="error_authentication_failed">Uwierzytelnianie nie powiodło się</string>
<string name="use_wildcards">Użyj symboli wieloznacznych nazw</string>
<string name="yes">Tak</string>
<string name="add_peer">Dodaj peera</string>
<string name="kernel">Jądro</string>
<string name="wildcards_active">Symbole wieloznaczne aktywne</string>
<string name="create_pin">Utwórz kod PIN</string>
<string name="junk_packet_maximum_size">Maksymalny rozmiar pakietu śmieciowego</string>
<string name="local_logging">Lokalne rejestrowanie</string>
<string name="monitoring_state_changes">Monitorowanie zmian stanu</string>
<string name="version">Wersja</string>
<string name="add_tunnels_text">Dodaj z pliku lub archiwum ZIP</string>
<string name="unknown_error">Wystąpił nieznany błąd</string>
<string name="start_auto">Uruchom autotunel</string>
<string name="location_services_not_detected">Usługi lokalizacyjne nie zostały wykryte</string>
<string name="background_location_message">Ta funkcja wymaga pozwolenia na określenie lokalizacji w dowolnym momencie i/lub dokładnej lokalizacji. Sprawdź</string>
<string name="auto_tunnel_title">Usługa autotunelu</string>
<string name="donate">Przekaż darowiznę na rzecz projektu</string>
<string name="trusted_ssid_value_description">Prześlij SSID</string>
<string name="all">wszystkie</string>
<string name="no_email_detected">Nie wykryto aplikacji e-mail</string>
<string name="turn_off_tunnel">Czynność wymaga wyłączenia tunelu</string>
<string name="add_from_qr">Dodaj z kodu QR</string>
<string name="qr_scan">Skanuj kod QR</string>
<string name="tunnel_name">Nazwa tunelu</string>
<string name="include">Uwzględnij</string>
<string name="config_changes_saved">Zmiany konfiguracji zostały zapisane.</string>
<string name="allowed_ips">Dozwolone adresy IP</string>
<string name="endpoint">Punkt końcowy</string>
<string name="name">Nazwa</string>
<string name="private_key">Klucz prywatny</string>
<string name="comma_separated_list">lista rozdzielona przecinkami</string>
<string name="listen_port">Port nasłuchu</string>
<string name="always_on_vpn_support">Zezwalaj na stały VPN</string>
<string name="base64_key">klucz base64</string>
<string name="use_kernel">Użyj modułu jądra</string>
<string name="error_ssid_exists">SSID już istnieje</string>
<string name="error_no_file_explorer">Eksplorator plików nie jest zainstalowany</string>
<string name="error_root_denied">Odmowa powłoki użytkownika root</string>
<string name="delete_tunnel">Usuń tunel</string>
<string name="incorrect_pin">Kod PIN jest nieprawidłowy</string>
<string name="edit_tunnel">Edytuj tunel</string>
<string name="settings">Ustawienia</string>
<string name="junk_packet_count">Liczba pakietów śmieciowych</string>
<string name="init_packet_junk_size">Rozmiar śmieciowego pakietu początkowego</string>
<string name="vpn_denied_dialog_title">Odmowa zezwolenia</string>
<string name="tunnel_required">Funkcja wymaga co najmniej jednego tunelu</string>
<string name="app_settings">ustawienia aplikacji</string>
<string name="set_custom_ping_ip">Ustaw niestandardowy adres IP pingowania</string>
<string name="default_ping_ip">(opcjonalnie, domyślnie do peerów)</string>
<string name="set_custom_ping_internal">Interwał pingowania (sek.)</string>
<string name="optional_default">"opcjonalnie, domyślnie: "</string>
<string name="logs">Dzienniki</string>
<string name="kill_switch">Wyłącznik awaryjny</string>
<string name="appearance">Wygląd</string>
<string name="notifications">Powiadomienia</string>
<string name="automatic">Automatyczny</string>
<string name="light">Jasny</string>
<string name="dark">Ciemny</string>
<string name="dynamic">Dynamiczny</string>
<string name="language">Język</string>
<string name="display_theme">Motyw wyświetlania</string>
<string name="trusted_wifi_names">Nazwy zaufanych sieci Wi-Fi</string>
<string name="add_wifi_name">Dodaj nazwę sieci Wi-Fi</string>
<string name="on_demand_rules">Zasady korzystania z tunelu na żądanie</string>
<string name="primary_tunnel">Tunel podstawowy</string>
<string name="mobile_tunnel">Tunel mobilnej transmisji danych</string>
<string name="skip">Pomiń</string>
<string name="launch_app_settings">Uruchom ustawienia aplikacji</string>
<string name="learn_more">Dowiedz się więcej</string>
<string name="wifi_name_via_shell">Nazwa sieci Wi-Fi poprzez powłokę</string>
<string name="use_root_shell_for_wifi">Użyj powłoki użytkownika root, aby uzyskać nazwę sieci Wi-Fi</string>
<string name="kernel_not_supported">Jądro nie jest obsługiwane</string>
<string name="stop_auto">Zatrzymaj autotunel</string>
<string name="tunnel_running">Tunel jest uruchomiony</string>
<string name="enable_local_logging">Włącz lokalne rejestrowanie</string>
<string name="email_chooser">Wyślij e-mail…</string>
<string name="set_custom_ping_cooldown">Czas odnowienia pingowania (sek.)</string>
<string name="open_file">Otwórz plik</string>
<string name="okay">OK</string>
<string name="no_tunnels">Nie dodano jeszcze żadnych tuneli!</string>
<string name="exclude">Wyklucz</string>
<string name="docs_description">Przeczytaj dokumentację</string>
<string name="email_description">Wyślij mi e-mail</string>
<string name="set_primary_tunnel">Ustaw jako tunel podstawowy</string>
<string name="mobile_data_tunnel">Ustaw jako tunel mobilnej transmisji danych</string>
<string name="vpn_channel_id">Kanał VPN</string>
<string name="stop_on_no_internet">Zatrzymaj, gdy nie ma Internetu</string>
<string name="stop_on_internet_loss">Zatrzymaj tunel przy utracie Internetu</string>
<string name="ethernet_tunnel">Tunel ethernetowy</string>
<string name="set_ethernet_tunnel">Ustaw jako tunel ethernetowy</string>
<string name="allow_lan_traffic">Zezwól na ruch LAN</string>
<string name="bypass_lan_for_kill_switch">Omiń LAN dla wyłącznika awaryjnego</string>
<string name="vpn_channel_description">Kanał powiadomień o stanie VPN</string>
<string name="auto_tunnel_channel_name">Kanał powiadomień autotunelu</string>
<string name="auto_tunnel_channel_description">Kanał powiadomień o stanie autotunelowania</string>
<string name="stop">zatrzymaj</string>
<string name="splt_tunneling">Tunelowanie rozdzielone</string>
<string name="tunnel_specific_settings">Ustawienia specyficzne dla tunelu</string>
<string name="pre_up">Przed aktywacją</string>
<string name="post_up">Po aktywacji</string>
<string name="pre_down">Przed dezaktywacją</string>
<string name="post_down">Po dezaktywacji</string>
<string name="amnezia_kernel_message">Protokół Amnezia niedostępny w trybie jądra</string>
<string name="enable_amnezia">Włącz protokół Amnezia</string>
<string name="wg_compat_mode">Tryb zgodności WG</string>
<string name="quick_actions">Szybkie czynności</string>
<string name="native_kill_switch">Natywny wyłącznik awaryjny</string>
<string name="vpn_kill_switch">Wyłącznik awaryjny VPN</string>
<string name="kill_switch_options">Opcje wyłącznika awaryjnego</string>
<string name="show_scripts">Pokaż skrypty</string>
<string name="hide_scripts">Ukryj skrypty</string>
<string name="debounce_delay">Opóźnienie odbicia</string>
<string name="enable_amnezia_compatibility">Włącz zgodność z protokołem Amnezia</string>
<string name="remove_amnezia_compatibility">Usuń zgodność z protokołem Amnezia</string>
<string name="exclude_lan">Wyklucz LAN</string>
<string name="include_lan">Uwzględnij LAN</string>
<string name="advanced_settings">Ustawienia zaawansowane</string>
<string name="hide_amnezia_properties">Ukryj właściwości protokołu Amnezia</string>
</resources>
+1 -9
View File
@@ -10,7 +10,6 @@
<string name="background_location_message2">para garantir que essas permissões estejam ativadas.</string>
<string name="optional_no_recommend">(opcional, não recomendado)</string>
<string name="dns_servers">Servidores DNS</string>
<string name="save_changes">Gravar</string>
<string name="add_from_qr">Adicionar a partir de código QR</string>
<string name="tunnels">Túneis</string>
<string name="email_description">Me envie um email</string>
@@ -44,7 +43,6 @@
<string name="delete_tunnel_message">Tem certeza que quer apagar este túnel?</string>
<string name="yes">Sim</string>
<string name="tunneling_apps">Aplicações com túnel</string>
<string name="excluded">excluído</string>
<string name="all">todos</string>
<string name="auto">(automático)</string>
<string name="pin_created">Pin criado com sucesso</string>
@@ -78,7 +76,6 @@
<string name="vpn_on">VPN ligada</string>
<string name="vpn_off">VPN desligada</string>
<string name="add_peer">Adicionar par</string>
<string name="done">Feito</string>
<string name="interface_">Interface</string>
<string name="base64_key">Chave base64</string>
<string name="persistent_keepalive">Manter a conexão persistente (keepalive)</string>
@@ -87,7 +84,6 @@
<string name="error_ssid_exists">SSID já existe</string>
<string name="error_root_denied">Shell Root negado</string>
<string name="delete_tunnel">Apagar túnel</string>
<string name="included">incluso</string>
<string name="junk_packet_maximum_size">Tamanho máximo de pacote-lixo</string>
<string name="init_packet_junk_size">Tamanho de pacote-lixo inicial</string>
<string name="vpn_denied_dialog_title">Permissão negada</string>
@@ -97,21 +93,17 @@
<string name="auto_tunnel_title">Serviço de Auto-túnel</string>
<string name="error_no_file_explorer">Nenhum explorador de ficheiros instalado</string>
<string name="app_name">WG Tunnel</string>
<string name="vpn_channel_id">Canal de VPN</string>
<string name="vpn_channel_name">Canal de notificações VPN</string>
<string name="vpn_channel_name">Canal de notificações VPN</string>
<string name="prominent_background_location_message">Este recurso precisa de permissões de localização em segundo plano para ativar o monitoramento do SSID da rede Wi-Fi mesmo quando a aplicação está fechado. Para mais pormenores, por favor veja a Política de Privacidade no ecrã de Suporte.</string>
<string name="trusted_ssid_value_description">Envie o SSID</string>
<string name="add_tunnels_text">Adicionar a partir de ficheiro ou zip</string>
<string name="open_file">Abrir Ficheiro</string>
<string name="exclude">Excluir</string>
<string name="include">Incluir</string>
<string name="tunnel_all">Todas as aplicações pelo túnel</string>
<string name="config_changes_saved">Mudanças nas configurações gravadas.</string>
<string name="icon">Ícone</string>
<string name="public_key">Chave pública</string>
<string name="addresses">Endereços</string>
<string name="peer">Par</string>
<string name="db_name">wg-tunnel-db</string>
<string name="rotate_keys">Revezar chaves</string>
<string name="private_key">Chave privada</string>
<string name="copy_public_key">Copiar chave pública</string>
+1 -9
View File
@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="tunnel_on_ethernet">Túnel na ethernet</string>
<string name="save_changes">Gravar</string>
<string name="public_key">Chave pública</string>
<string name="addresses">Endereços</string>
<string name="dns_servers">Servidores DNS</string>
@@ -18,7 +17,6 @@
<string name="error_no_file_explorer">Nenhum explorador de ficheiros instalado</string>
<string name="error_invalid_code">Código QR inválido</string>
<string name="auto_tunnel_title">Serviço de Auto-túnel</string>
<string name="excluded">excluído</string>
<string name="all">todos</string>
<string name="enter_pin">Digite o seu pin</string>
<string name="use_tunnel_on_wifi_name">Usar túnel em wifi com nome</string>
@@ -44,8 +42,6 @@
<string name="open_file">Abrir Ficheiro</string>
<string name="add_from_qr">Adicionar a partir de código QR</string>
<string name="add_tunnels_text">Adicionar a partir de ficheiro ou zip</string>
<string name="tunnel_all">Todas as aplicações pelo túnel</string>
<string name="icon">Ícone</string>
<string name="qr_scan">Escanear o código QR</string>
<string name="tunnel_name">Nome do Túnel</string>
<string name="config_changes_saved">Mudanças nas configurações gravadas.</string>
@@ -62,7 +58,6 @@
<string name="vpn_off">VPN desligada</string>
<string name="listen_port">Porta de escuta</string>
<string name="turn_on_tunnel">Esta ação precisa um túnel ativo</string>
<string name="done">Feito</string>
<string name="add_peer">Adicionar par</string>
<string name="interface_">Interface</string>
<string name="copy_public_key">Copiar chave pública</string>
@@ -87,7 +82,6 @@
<string name="docs_description">Ler a documentação</string>
<string name="error_root_denied">Shell Root negado</string>
<string name="location_services_missing_message">A aplicação não detetou o serviço de localização ativado no seu dispositivo. Dependendo do dispositivo, isto pode causar que a função de Wi-Fi não confiável falhe em ler o nome do Wi-Fi. Quer continuar mesmo assim?</string>
<string name="included">incluso</string>
<string name="open_issue">Abrir um problema</string>
<string name="tunneling_apps">Aplicações com túnel</string>
<string name="no_email_detected">Nenhuma aplicação de email detetado</string>
@@ -108,9 +102,7 @@
<string name="see_the">Veja o</string>
<string name="getting_started_guide">guia de início rápido</string>
<string name="error_file_format">Formato de configuração inválido</string>
<string name="vpn_channel_id">Canal de VPN</string>
<string name="vpn_channel_name">Canal de notificações VPN</string>
<string name="db_name">wg-tunnel-db</string>
<string name="vpn_channel_name">Canal de notificações VPN</string>
<string name="set_custom_ping_ip">Definir ip ping personalizado</string>
<string name="vpn_denied_dialog_title">Permissão negada</string>
<string name="vpn_settings">Configurações do sistema VPN</string>
+41 -18
View File
@@ -7,17 +7,14 @@
<string name="name">Имя</string>
<string name="peer">Пир</string>
<string name="privacy_policy">Посмотреть политику конфиденциальности</string>
<string name="icon">Иконка</string>
<string name="add_from_qr">Добавить из QR</string>
<string name="qr_scan">Сканировать QR</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="addresses">Адреса</string>
<string name="dns_servers">DNS-серверы</string>
<string name="allowed_ips">Разрешённые IP</string>
@@ -30,11 +27,10 @@
<string name="optional">(необязательно)</string>
<string name="optional_no_recommend">(необязательно, не рекомендуется)</string>
<string name="tunneling_apps">Туннелируемые приложения</string>
<string name="excluded">исключено</string>
<string name="all">все</string>
<string name="no_email_detected">Приложение для отправки почты не обнаружено</string>
<string name="no_email_detected">Приложение для отправки почты не найдено</string>
<string name="enter_pin">Введите PIN-код</string>
<string name="enable_app_lock">Включить блокировку приложения</string>
<string name="enable_app_lock">Защита приложения</string>
<string name="restart_on_ping">Перезапуск при сбое пинга (бета)</string>
<string name="settings">Настройки</string>
<string name="support">Поддержка</string>
@@ -47,11 +43,9 @@
<string name="cancel">Отмена</string>
<string name="docs_description">Посмотреть документацию</string>
<string name="email_chooser">Отправить письмо…</string>
<string name="included">включено</string>
<string name="auto_tunnel_title">Сервис автотуннелирования</string>
<string name="create_import">Создать с нуля</string>
<string name="private_key">Закрытый ключ</string>
<string name="db_name">wg-tunnel-db</string>
<string name="base64_key">Ключ в base64</string>
<string name="error_no_file_explorer">Не найден файловый менеджер</string>
<string name="delete_tunnel_message">Вы действительно хотите удалить этот туннель?</string>
@@ -61,7 +55,6 @@
<string name="error_file_extension">Файл не имеет расширение .conf или .zip</string>
<string name="random">(случайно)</string>
<string name="app_name">WG Tunnel</string>
<string name="vpn_channel_id">Канал VPN</string>
<string name="vpn_channel_name">Канал уведомлений VPN</string>
<string name="tunnels">Туннели</string>
<string name="okay">Хорошо</string>
@@ -69,7 +62,7 @@
<string name="thank_you">Благодарим вас за использование WG Tunnel!</string>
<string name="trusted_ssid_value_description">Отправить SSID</string>
<string name="add_tunnels_text">Добавить из файла или архива</string>
<string name="location_services_not_detected">Сервисы местоположения не обнаружены</string>
<string name="location_services_not_detected">Сервисы местоположения не найдены</string>
<string name="turn_on_tunnel">Действие требует наличие активного туннеля</string>
<string name="add_peer">Добавить пира</string>
<string name="rotate_keys">Обновить ключи</string>
@@ -88,7 +81,6 @@
<string name="yes">Да</string>
<string name="use_tunnel_on_wifi_name">Использовать туннель в сети Wi-Fi</string>
<string name="prominent_background_location_message">Эта функция требует фоновый доступ к местоположению для отслеживания имён сетей Wi-Fi, даже когда приложение закрыто. Для получения дополнительной информации, прочтите политику конфиденциальности на экране поддержки.</string>
<string name="done">Готово</string>
<string name="copy_public_key">Копировать открытый ключ</string>
<string name="error_authorization_failed">Не удалось пройти аутентификацию</string>
<string name="enabled_app_shortcuts">Включить ярлыки приложения</string>
@@ -104,7 +96,7 @@
<string name="email_subject">Поддержка WG Tunnel</string>
<string name="email_description">Отправить письмо автору</string>
<string name="use_kernel">Использовать модуль режима ядра</string>
<string name="no_browser_detected">Браузер не обнаружен</string>
<string name="no_browser_detected">Браузер не найден</string>
<string name="read_logs">Посмотреть журнал</string>
<string name="auto">(авто)</string>
<string name="set_primary_tunnel">Назначить как главный туннель</string>
@@ -122,7 +114,7 @@
<string name="restart_at_boot">Перезапуск при загрузке</string>
<string name="default_ping_ip">(необязательно, по умолчанию для пиров)</string>
<string name="app_settings">настройки приложения</string>
<string name="background_location_message2">, чтобы убедиться, что эти разрешения предоставлены.</string>
<string name="background_location_message2">, чтобы убедиться, что эти разрешения предоставлены</string>
<string name="always_on_message2">, чтобы убедиться, что функция «Постоянный VPN» отключена для всех других приложений, и повторите попытку</string>
<string name="background_location_message">Разрешать всё время, пока для работы этой функции требуется доступ на определение местоположения и/или точное местоположение. Смотрите</string>
<string name="vpn_settings">Системные настройки VPN</string>
@@ -155,10 +147,9 @@
<string name="use_wildcards">Использовать подстановочные знаки в имени</string>
<string name="appearance">Внешний вид</string>
<string name="notifications">Уведомления</string>
<string name="kill_switch">Экстренный разрыв соединения</string>
<string name="kill_switch">Экстренное отключение</string>
<string name="dark">Тёмная</string>
<string name="display_theme">Тема</string>
<string name="selected">Выбрано</string>
<string name="add_wifi_name">Добавить сеть Wi-Fi</string>
<string name="launch_app_settings">Настройки запуска приложения</string>
<string name="on_demand_rules">Правила туннеля по запросу</string>
@@ -169,4 +160,36 @@
<string name="monitoring_state_changes">Отслеживание изменений состояния</string>
<string name="enable_local_logging">Включить ведение журнала</string>
<string name="add_from_clipboard">Добавить из буфера обмена</string>
</resources>
<string name="auto_tunnel_channel_name">Канал уведомлений автотуннеля</string>
<string name="show_scripts">Показать сценарии</string>
<string name="amnezia_kernel_message">Amnezia недоступна в режиме ядра</string>
<string name="enable_amnezia">Использовать Amnezia</string>
<string name="wg_compat_mode">Режим совместимости WG</string>
<string name="quick_actions">Быстрые действия</string>
<string name="stop_on_no_internet">Остановить без интернета</string>
<string name="stop_on_internet_loss">Остановить туннель при потере интернета</string>
<string name="ethernet_tunnel">Туннель для Ethernet</string>
<string name="set_ethernet_tunnel">Назначить как туннель для Ethernet</string>
<string name="native_kill_switch">Штатное экстренное отключение</string>
<string name="vpn_kill_switch">Экстренное отключение VPN</string>
<string name="kill_switch_options">Настройка экстренного отключения</string>
<string name="allow_lan_traffic">Разрешать трафик LAN</string>
<string name="bypass_lan_for_kill_switch">Обход LAN при экстренном отключении</string>
<string name="vpn_channel_description">Канал уведомлений о состоянии VPN</string>
<string name="auto_tunnel_channel_description">Канал уведомлений о состоянии автотуннеля</string>
<string name="stop">стоп</string>
<string name="splt_tunneling">Раздельное туннелирование</string>
<string name="tunnel_specific_settings">Специальные настройки туннеля</string>
<string name="pre_up">До активации</string>
<string name="post_up">После активации</string>
<string name="pre_down">До деактивации</string>
<string name="post_down">После деактивации</string>
<string name="debounce_delay">Задержка отбоя</string>
<string name="remove_amnezia_compatibility">Отключить совместимость с Amnezia</string>
<string name="exclude_lan">Исключить локальную сеть</string>
<string name="hide_scripts">Скрыть сценарии</string>
<string name="hide_amnezia_properties">Скрыть настройки Amnezia</string>
<string name="advanced_settings">Дополнительные настройки</string>
<string name="enable_amnezia_compatibility">Включить совместимость с Amnezia</string>
<string name="include_lan">Включить локальную сеть</string>
</resources>
+5 -19
View File
@@ -1,12 +1,8 @@
<?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="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 .conf veya .zip değil</string>
<string name="vpn_channel_name">VPN Bildirim Kanalı</string>
<string name="error_file_extension">Dosya .conf veya .zip değil</string>
<string name="turn_off_tunnel">İşlem için tünelin kapalı olması gerekiyor</string>
<string name="no_tunnels">Henüz tünel eklenmedi!</string>
<string name="tunnels">Tüneller</string>
@@ -25,10 +21,7 @@
<string name="tunnel_name">Tünel Adı</string>
<string name="exclude">Hariç tut</string>
<string name="include">Dahil et</string>
<string name="tunnel_all">Tüm uygulamaları tünelle</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="public_key">Genel anahtar</string>
<string name="addresses">Adresler</string>
<string name="dns_servers">DNS sunucuları</string>
@@ -40,14 +33,12 @@
<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">Paketlerde ara</string>
<string name="db_name">wg-tunnel-db</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="create_import">Sıfırdan oluştur</string>
<string name="turn_on_tunnel">İşlem için aktif tünel gerekiyor</string>
<string name="add_peer">Eş ekle</string>
<string name="done">Tamam</string>
<string name="interface_">Arayüz</string>
<string name="rotate_keys">Anahtarları döndür</string>
<string name="private_key">Özel anahtar</string>
@@ -68,8 +59,7 @@
<string name="export_configs">Yapılandırmaları dışa aktar</string>
<string name="unknown_error">Bilinmeyen bir hata oluştu</string>
<string name="tunnel_on_wifi">Güvenilmeyen wifi\'da 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_subject">WG Tunnel Desteği</string>
<string name="email_chooser">E-posta gönder…</string>
<string name="docs_description">Belgeleri oku</string>
<string name="email_description">Bana e-posta gönder</string>
@@ -84,8 +74,6 @@
<string name="delete_tunnel_message">Bu tüneli silmek istediğinizden emin misiniz?</string>
<string name="yes">Evet</string>
<string name="tunneling_apps">Tünellenen uygulamalar</string>
<string name="included">dahil</string>
<string name="excluded">hariç</string>
<string name="all">tümü</string>
<string name="no_email_detected">E-posta uygulaması algılanmadı</string>
<string name="no_browser_detected">Tarayıcı algılanmadı</string>
@@ -115,11 +103,9 @@
<string name="response_packet_magic_header">Yanıt paketi sihirli başlığı</string>
<string name="transport_packet_magic_header">Taşıma paketi sihirli başlığı</string>
<string name="underload_packet_magic_header">Düşük yük paketi sihirli başlığı</string>
<string name="telegram_url" translatable="false">https://t.me/wgtunnel</string>
<string name="unsure_how">nasıl devam edeceğinizden emin değilseniz</string>
<string name="unsure_how">nasıl devam edeceğinizden emin değilseniz</string>
<string name="see_the">Bakın:</string>
<string name="getting_started_url" translatable="false">https://zaneschepke.com/wgtunnel-docs/getting-started.html</string>
<string name="getting_started_guide">başlangıç kılavuzu</string>
<string name="getting_started_guide">başlangıç kılavuzu</string>
<string name="error_file_format">Geçersiz tünel yapılandırma formatı</string>
<string name="restart_at_boot">Önyüklemede yeniden başlat</string>
</resources>
+3 -11
View File
@@ -11,9 +11,7 @@
<string name="qr_scan">Сканувати QR</string>
<string name="tunnel_name">Ім\'я тунелю</string>
<string name="include">Включити</string>
<string name="tunnel_all">Тунель для всіх додатків</string>
<string name="config_changes_saved">Зміни налаштувань збережено.</string>
<string name="icon">Іконка</string>
<string name="turn_on_tunnel">Дія потребує активного тунелю</string>
<string name="rotate_keys">Оновити ключі</string>
<string name="private_key">Закритий ключ</string>
@@ -32,8 +30,7 @@
<string name="getting_started_guide">інструкція щодо початку роботи</string>
<string name="error_file_format">некоректний формат конфігурації тунелю</string>
<string name="vpn_channel_name">Канал сповіщення VPN</string>
<string name="vpn_channel_id">Канал VPN</string>
<string name="error_file_extension">Файл не є .conf або .zip файлом</string>
<string name="error_file_extension">Файл не є .conf або .zip файлом</string>
<string name="turn_off_tunnel">Дія потребує вимкнення тунелю</string>
<string name="tunnel_mobile_data">Тунелювати мобільні дані</string>
<string name="privacy_policy">Переглянути політику конфіденційності</string>
@@ -43,7 +40,6 @@
<string name="open_file">Відкрити файл</string>
<string name="exclude">Виключити</string>
<string name="add_from_qr">Додати з QR коду</string>
<string name="save_changes">Зберегти</string>
<string name="public_key">Публічний ключ</string>
<string name="addresses">Адреса</string>
<string name="allowed_ips">Дозволені IP</string>
@@ -55,13 +51,11 @@
<string name="name">Ім\'я</string>
<string name="always_on_vpn_support">Дозволили Always-ON VPN</string>
<string name="location_services_not_detected">Сервіси місце знаходження не знайдено</string>
<string name="db_name">wg-tunnel-db</string>
<string name="auto_tunneling">Авто-тунелювання</string>
<string name="auto_tunneling">Авто-тунелювання</string>
<string name="vpn_on">VPN увімк.</string>
<string name="vpn_off">VPN вимк.</string>
<string name="create_import">Створити з нуля</string>
<string name="add_peer">Додати peer</string>
<string name="done">Готово</string>
<string name="interface_">Інтерфейс</string>
<string name="copy_public_key">Копіювати відкритий ключ</string>
<string name="listen_port">Слухати порт</string>
@@ -83,11 +77,9 @@
<string name="auto_tunnel_title">Сервіс авто-тунелювання</string>
<string name="delete_tunnel">Видалити тунель</string>
<string name="location_services_missing_message">Додаток не знайшов служб місце знаходження на вашому пристрої. На деяких пристроях це може привести до неможливості визначення назви мережі Wi-Fi і помилок функції недовірених Wi-Fi мереж. Все рівно хочете продовжити?</string>
<string name="included">включено</string>
<string name="delete_tunnel_message">Ви дійсно хочете видалити цей тунель?</string>
<string name="yes">Так</string>
<string name="tunneling_apps">Тунельовані додатки</string>
<string name="excluded">виключено</string>
<string name="auto">(авто)</string>
<string name="all">всі</string>
<string name="no_email_detected">Програми для надсилання email не знайдено</string>
@@ -115,4 +107,4 @@
<string name="response_packet_magic_header">Заголовок пакету відповіді</string>
<string name="unsure_how">, якщо не впевнені що робити далі</string>
<string name="see_the">Дивіться</string>
</resources>
</resources>
+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+17 -26
View File
@@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="turn_off_tunnel">请关闭连接再操作</string>
<string name="no_tunnels">还没有添加隧道!</string>
<string name="tunnels">连接列表</string>
<string name="turn_off_tunnel">关闭连接再操作</string>
<string name="no_tunnels">还没有添加隧道!</string>
<string name="tunnels">隧道列表</string>
<string name="privacy_policy">查看隐私政策</string>
<string name="tunnel_mobile_data">允许隧道使用手机数据流量</string>
<string name="tunnel_mobile_data">允许隧道使用数据流量</string>
<string name="okay"></string>
<string name="tunnel_on_ethernet">在局域网中使用隧道</string>
<string name="error_file_extension">文件类型不是 .conf 或 .zip</string>
@@ -14,8 +14,6 @@
<string name="qr_scan">扫描二维码</string>
<string name="tunnel_name">隧道名称</string>
<string name="include">包含</string>
<string name="tunnel_all">全局代理</string>
<string name="icon">图标</string>
<string name="public_key">公钥</string>
<string name="addresses">地址</string>
<string name="dns_servers">DNS 服务器</string>
@@ -23,11 +21,9 @@
<string name="allowed_ips">允许的 IP</string>
<string name="name">名称</string>
<string name="peer">节点</string>
<string name="always_on_vpn_support">允许VPN始终在线</string>
<string name="always_on_vpn_support">允许 VPN 始终在线</string>
<string name="location_services_not_detected">定位服务未开启</string>
<string name="hint_search_packages">查找软件包</string>
<string name="db_name">wg-tunnel 数据库</string>
<string name="done">完成</string>
<string name="rotate_keys">轮换秘钥</string>
<string name="create_import">手动创建</string>
<string name="private_key">私钥</string>
@@ -35,7 +31,7 @@
<string name="optional">(可选)</string>
<string name="optional_no_recommend">(可选,不建议)</string>
<string name="error_authorization_failed">验证失败</string>
<string name="enabled_app_shortcuts">创建快捷方式到桌面</string>
<string name="enabled_app_shortcuts">创建桌面快捷方式</string>
<string name="export_configs">导出设置</string>
<string name="docs_description">阅读文档</string>
<string name="email_description">给作者发邮件</string>
@@ -52,7 +48,7 @@
<string name="base64_key">Base64 编码</string>
<string name="use_kernel">使用内核模块</string>
<string name="endpoint">端点</string>
<string name="thank_you">谢谢使用 WG Tunnel!</string>
<string name="thank_you">谢谢使用 WG Tunnel!</string>
<string name="prominent_background_location_message">此功能是在应用关闭时,后台自动扫描 Wi-Fi SSID,需要开启后台位置信息访问权限。更多信息,请在支持页面查看隐私政策。</string>
<string name="vpn_on">VPN 已连接</string>
<string name="vpn_off">VPN 已关闭</string>
@@ -67,21 +63,18 @@
<string name="error_ssid_exists">SSID 已经存在</string>
<string name="open_file">打开文件</string>
<string name="config_changes_saved">设置已保存。</string>
<string name="save_changes">保存</string>
<string name="interface_">接口</string>
<string name="email_subject">WG Tunnel 支持</string>
<string name="auto_tunnel_title">自动连接服务</string>
<string name="delete_tunnel">删除隧道</string>
<string name="delete_tunnel_message">确定删除这个隧道吗?</string>
<string name="location_services_missing_message">此应用不会在的设备上检测任何已开启的定位服务。根据不同的设备,可能会导致无法获得不可信 WiFi 的名称。想继续吗?</string>
<string name="location_services_missing_message">此应用不会在的设备上检测任何已开启的定位服务。根据不同的设备,可能会导致无法获得不可信 WiFi 的名称。想继续吗?</string>
<string name="yes"></string>
<string name="tunneling_apps">正使用隧道的应用</string>
<string name="included">已包含</string>
<string name="excluded">排除</string>
<string name="tunneling_apps">使用隧道的应用</string>
<string name="all">全部</string>
<string name="no_email_detected">未安装邮件应用</string>
<string name="enable_app_lock">锁定应用</string>
<string name="use_tunnel_on_wifi_name">在指定的wifi上使用此隧道</string>
<string name="use_tunnel_on_wifi_name">在指定的 WiFi 上使用此隧道</string>
<string name="version">版本</string>
<string name="settings">设置</string>
<string name="support">支持</string>
@@ -91,13 +84,12 @@
<string name="root_accepted">已获取 root 权限</string>
<string name="default_ping_ip">(可选,默认选择节点)</string>
<string name="set_custom_ping_internal">Ping 间隔(秒)</string>
<string name="optional_default">"可选,默认: "</string>
<string name="optional_default">"可选,默认 "</string>
<string name="set_custom_ping_cooldown">Ping 重启间隔(秒)</string>
<string name="show_amnezia_properties">显示 Amnezia 属性</string>
<string name="no_browser_detected">没有安装浏览器</string>
<string name="incorrect_pin">密码不正确</string>
<string name="set_custom_ping_ip">自定义 Ping 的目标 ip</string>
<string name="vpn_channel_id">VPN 频道</string>
<string name="junk_packet_count">无效包计数</string>
<string name="app_name">WG Tunnel</string>
<string name="vpn_channel_name">VPN 通知频道</string>
@@ -108,7 +100,7 @@
<string name="enter_pin">输入密码</string>
<string name="create_pin">创建密码</string>
<string name="set_primary_tunnel">设置为主隧道</string>
<string name="mobile_data_tunnel">允许使用手机数据流量</string>
<string name="mobile_data_tunnel">允许使用数据流量</string>
<string name="init_packet_junk_size">初始化无效包大小</string>
<string name="junk_packet_maximum_size">无效包最大值</string>
<string name="response_packet_magic_header">响应包的 magic header</string>
@@ -141,18 +133,17 @@
<string name="language">语言</string>
<string name="add_wifi_name">添加 WiFi SSID</string>
<string name="primary_tunnel">主隧道</string>
<string name="mobile_tunnel">允许使用移动数据</string>
<string name="mobile_tunnel">使用数据流量</string>
<string name="add_from_clipboard">从剪贴板添加</string>
<string name="transport_packet_magic_header">传输包的 magic header</string>
<string name="underload_packet_magic_header">欠载数据包 magic header</string>
<string name="restart_at_boot">开机时重新启动</string>
<string name="background_location_message">需要允许所有时间位置权限和/或精确位置才能使用此功能。请参阅</string>
<string name="learn_more">了解更多</string>
<string name="unsure_how">如果不确定如何进行</string>
<string name="see_the"></string>
<string name="unsure_how">如果不确定如何进行</string>
<string name="see_the">请查阅</string>
<string name="getting_started_guide">入门指南</string>
<string name="selected">已选择</string>
<string name="on_demand_rules">按需隧道规则</string>
<string name="on_demand_rules">自定义隧道规则</string>
<string name="skip">取消</string>
<string name="launch_app_settings">打开应用设置</string>
<string name="use_wildcards">使用 SSID 通配符</string>
@@ -165,7 +156,7 @@
<string name="stop_auto">停止自动隧道</string>
<string name="tunnel_running">隧道运行中</string>
<string name="donate">捐赠</string>
<string name="local_logging">开启日志</string>
<string name="local_logging">日志</string>
<string name="enable_local_logging">开启本地日志</string>
<string name="configuration_change">配置更改</string>
<string name="requires_app_relaunch">此更改需要重新启动应用程序。您是否要继续?</string>
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_banner_background">#1D1A20</color>
</resources>
<color name="ic_banner_background">#1D1A20</color>
</resources>
+21 -18
View File
@@ -1,6 +1,6 @@
<resources>
<string name="app_name">WG Tunnel</string>
<string name="vpn_channel_id">VPN Channel</string>
<string name="vpn_channel_id" translatable="false">VPN Channel</string>
<string name="vpn_channel_name">VPN Notification Channel</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>
@@ -26,11 +26,8 @@
<string name="tunnel_name">Tunnel Name</string>
<string name="exclude">Exclude</string>
<string name="include">Include</string>
<string name="tunnel_all">Tunnel all applications</string>
<string name="config_changes_saved">Configuration changes saved.</string>
<string name="save_changes">Save</string>
<string name="icon">Icon</string>
<string name="public_key">Public key</string>
<string name="config_changes_saved">Configuration changes saved.</string>
<string name="public_key">Public key</string>
<string name="addresses">Addresses</string>
<string name="dns_servers">DNS servers</string>
<string name="mtu">MTU</string>
@@ -41,15 +38,14 @@
<string name="always_on_vpn_support">Allow Always-On VPN </string>
<string name="location_services_not_detected">Location Services Not Detected</string>
<string name="hint_search_packages">Search packages</string>
<string name="db_name">wg-tunnel-db</string>
<string name="db_name" translatable="false">wg-tunnel-db</string>
<string name="auto_tunneling">Auto-tunneling</string>
<string name="vpn_on">VPN on</string>
<string name="vpn_off">VPN off</string>
<string name="create_import">Create from scratch</string>
<string name="turn_on_tunnel">Action requires active tunnel</string>
<string name="add_peer">Add peer</string>
<string name="done">Done</string>
<string name="interface_">Interface</string>
<string name="interface_">Interface</string>
<string name="rotate_keys">Rotate keys</string>
<string name="private_key">Private key</string>
<string name="copy_public_key">Copy public key</string>
@@ -85,9 +81,7 @@
<string name="delete_tunnel_message">Are you sure you would like to delete this tunnel?</string>
<string name="yes">Yes</string>
<string name="tunneling_apps">Tunneling apps</string>
<string name="included">included</string>
<string name="excluded">excluded</string>
<string name="all">all</string>
<string name="all">all</string>
<string name="no_email_detected">No email app detected</string>
<string name="no_browser_detected">No browser detected</string>
<string name="open_issue">Open an issue</string>
@@ -152,8 +146,7 @@
<string name="dynamic">Dynamic</string>
<string name="language">Language</string>
<string name="display_theme">Display theme</string>
<string name="selected">Selected</string>
<string name="trusted_wifi_names">Trusted wifi names</string>
<string name="trusted_wifi_names">Trusted wifi names</string>
<string name="add_wifi_name">Add wifi name</string>
<string name="on_demand_rules">On demand tunnel rules</string>
<string name="primary_tunnel">Primary tunnel</string>
@@ -186,18 +179,28 @@
<string name="allow_lan_traffic">Allow LAN traffic</string>
<string name="bypass_lan_for_kill_switch">Bypass LAN for kill switch</string>
<string name="vpn_channel_description">A channel for VPN state notifications</string>
<string name="auto_tunnel_channel_id">Auto-tunnel Channel</string>
<string name="auto_tunnel_channel_id" translatable="false">Auto-tunnel Channel</string>
<string name="auto_tunnel_channel_name">Auto-tunnel Notification Channel</string>
<string name="auto_tunnel_channel_description">A channel for auto-tunnel state notifications</string>
<string name="stop">stop</string>
<string name="config">Config</string>
<string name="splt_tunneling">Split tunneling</string>
<string name="tunnel_specific_settings">Tunnel specific settings</string>
<string name="quick_actions">Quick actions</string>
<string name="show_scripts">Show scripts</string>
<string name="show_scripts">Show scripts</string>
<string name="pre_up">Pre up</string>
<string name="post_up">Post up</string>
<string name="pre_down">Pre down</string>
<string name="post_down">Post down</string>
<string name="amnezia_kernel_message">Amnezia unavailable in kernel mode</string>
<string name="enable_amnezia">Enable Amnezia</string>
<string name="wg_compat_mode">WG compatibility mode</string>
<string name="quick_actions">Quick actions</string>
<string name="advanced_settings">Advanced settings</string>
<string name="debounce_delay">Debounce delay</string>
<string name="hide_amnezia_properties">Hide Amnezia properties</string>
<string name="hide_scripts">Hide scripts</string>
<string name="enable_amnezia_compatibility">Enable Amnezia compatibility</string>
<string name="remove_amnezia_compatibility">Remove Amnezia compatibility</string>
<string name="exclude_lan">Exclude LAN</string>
<string name="include_lan">Include LAN</string>
<string name="error_tunnel_start">Failed to starting tunnel</string>
</resources>
+2 -2
View File
@@ -1,7 +1,7 @@
object Constants {
const val VERSION_NAME = "3.6.5"
const val VERSION_NAME = "3.6.6"
const val JVM_TARGET = "17"
const val VERSION_CODE = 36500
const val VERSION_CODE = 36600
const val TARGET_SDK = 35
const val MIN_SDK = 26
const val APP_ID = "com.zaneschepke.wireguardautotunnel"
@@ -0,0 +1,6 @@
Was ist neu?
- VPN-Notschalter mit LAN-Bypass hinzugefügt
- Verbesserte automatische Tunnelgeschwindigkeit und Zuverlässigkeit
- Verbesserte Kachelsynchronisation
- Verschiedene Fehlerbehebungen und Verbesserungen
@@ -0,0 +1,3 @@
Was ist neu?
- Fehler beim Umschalten des Kernelmodus behoben
- Fehler beim Absturz der Benachrichtigung behoben
@@ -0,0 +1,6 @@
Was ist neu?
- Verbesserungen der Zuverlässigkeit des automatischen Tunnels
- Behebung von Problemen beim Speichern von geteilten Tunneln und Leistungsverbesserungen
- Kernel-Modus-Fehler behoben
- Kachel-Fehler behoben
- Verschiedene andere Korrekturen und Verbesserungen
@@ -0,0 +1,6 @@
What's new:
- Ping feature now works independent of auto tunnel
- Added convenience action for Amnezia compatibility
- Added convenience action for excluding LAN from tunnel
- Added debounce delay tuning option for auto tunnel
- Many bug fixes and improvements
Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 112 KiB

@@ -0,0 +1,5 @@
Novedades:
- Interruptor de apagado VPN añadido con exclusión LAN
- Velocidad y fiabilidad mejoradas del túnel automático
- Sincronización de mosaicos mejorada
- Varias correcciones de errores y mejoras
@@ -0,0 +1,3 @@
Novedades:
- Error de conmutación del modo kernel corregido
- Se ha corregido un error de bloqueo de notificación
@@ -0,0 +1,6 @@
Novedades:
- Mejoras en la fiabilidad del túnel automático
- Corrección de guardado de túnel dividido y mejoras de rendimiento
- Corrección de errores en modo kernel
- Corrección de errores en mosaicos
- Otras correcciones y mejoras
@@ -1,14 +1,14 @@
Fonctionnalités
- Ajoutez des tunnels à l'aide d'un fichier .conf, zip, par entrée manuelle ou par code QR
- Connexion automatique au VPN en fonction du SSID de votre Wi-Fi, de l'usage d'Ethernet ou de l'usage des données mobiles
- Division des tunnels en fonction de l'application avec recherche
- Prise en charge de WireGuard pour les modes noyau et espace utilisateur
- Prise en charge d'Amnezia pour la protection contre l'inspection profonde de paquets et la censure
- Prise en charge du VPN permanent
- Export des tunnels Amnezia et WireGuard en zip
- Prise en charge des réglages rapides pour l'activation du VPN
- Prise en charge des raccourcis statiques pour le tunnel principal permettant l'intégration automatisée.
- Prise en charge de l'automatisation des intentions pour tous les tunnels
- Redémarrage automatique des services après redémarrage du téléphone
- Mesures de préservation de la batterie
- Permet l'ajout de tunnels à l'aide d'un fichier .conf, zip, par entrée manuelle ou par code QR
- Se connecte automatiquement au VPN en fonction du SSID de votre Wi-Fi, de l'usage d'Ethernet ou de l'usage des données mobiles
- Divise les tunnels en fonction de l'application avec recherche
- Prend en charge les modes noyau et espace utilisateur de WireGuard
- Prend en charge Amnezia pour la protection contre l'inspection profonde de paquets ou contre la censure
- Prend en charge le VPN permanent
- Permet l'export des tunnels Amnezia et WireGuard en zip
- Permet l'activation du VPN depuis la barre de réglages rapide
- Prend en charge les raccourcis statiques pour le tunnel principal permettant l'intégration automatisée.
- Prend en charge l'automatisation des intentions pour tous les tunnels
- Redémarre automatiquement les services après un redémarrage du téléphone
- Préserve la batterie
@@ -1 +1 @@
Une appli alternative pour clients VPN WireGuard avec plus de fonctionnalités
Une alternative pour clients VPN WireGuard avec fonctionnalités supplémentaires
@@ -0,0 +1,14 @@
Funksjoner
- Legg til via .conf filer, zip, manuelt, eller QR kode
- Koble til VPN automatisk etter Wi-Fi SSID, ethernet, eller mobilnettverk
- Split tunneling av dine programmer, med søk
- WireGuard støtte for kernel
- Amnezia støtte for DPI/sensur beskyttelse
- Alltid-På VPN
- Eksporter Amnezia and WireGuard tunneler til zip
- Snarveisbryter or VPN
- Statisk snarvei for automatisk integrasjon av hovedtunnel
- Støtte for automatisering av alle tunneler
- Automatisk restart av tunnel etter restart
- Batterisparende innstillinger
@@ -0,0 +1 @@
En alternativ VPN klient for Wireguard med mange funsjoner
@@ -0,0 +1 @@
WG Tunnel
@@ -1,3 +1,3 @@
Usprawnienia:
- naprawiono błąd uprawnień na Android 9
Ulepszenia:
- Naprawiono < błąd uprawnień Androida 9
- Inne optymalizacje
@@ -0,0 +1,5 @@
Ulepszenia:
- Dodanie statystyk tunelu do ekranu głównego
- Ulepszenie nawigacji AndroidTV na ekranie ustawień
- Usunięcie wibracji powiadomień
- Różne inne poprawki błędów
@@ -0,0 +1,5 @@
Ulepszenia:
- Dodano obsługę automatycznego tunelowania tylko dla danych mobilnych
- Ulepszono interfejs użytkownika ekranu pomocy technicznej
- Zaktualizowano łącza do zasobów
- Różne inne poprawki błędów
@@ -0,0 +1,5 @@
Ulepszenia:
- Dodano podstawową obsługę jądra protokołu WireGuard
- Ulepszony przepływ ujawniania lokalizacji
- Naprawiono błąd uprawnień automatycznego tunelowania
- Różne inne poprawki błędów interfejsu użytkownika
@@ -0,0 +1,2 @@
Poprawki:
- Uprawnienia do pierwszego planu w systemie Android 14
@@ -0,0 +1,7 @@
Ulepszenia:
- Refaktoryzacja zarządzania stanem
- Ulepszenie nawigacji AndroidTV
- Ulepszenie wydajności automatycznego tunelowania
- Ulepszenie nawigacji
- Funkcja pauzy automatycznego tunelowania
- Wiele poprawek błędów
@@ -0,0 +1,7 @@
Ulepszenia:
- Refaktoryzacja zarządzania stanem
- Ulepszenie nawigacji AndroidTV
- Ulepszenie wydajności automatycznego tunelowania
- Ulepszenie nawigacji
- Funkcja pauzy automatycznego tunelowania
- Wiele poprawek błędów
@@ -0,0 +1,8 @@
Ulepszenia:
- Refaktoryzacja zarządzania stanem
- Poprawa nawigacji AndroidTV
- Poprawa wydajności automatycznego tunelowania
- Poprawa nawigacji
- Funkcja pauzy automatycznego tunelowania
- Naprawiono autotunelowanie uruchamiania pierwszego planu
- Wiele poprawek błędów
@@ -0,0 +1,7 @@
Ulepszenia:
- Dodano potwierdzenie usunięcia tunelu
- Dodano uprawnienia w tle dla trybu oszczędzania energii
Poprawki:
- Błąd wyłączania tunelu zamrożonego
- Błąd w polu odbiorcy wiadomości e-mail
- Edycja konfiguracji pustego DNS
@@ -0,0 +1,3 @@
Ulepszenia:
- Naprawiono niezapisywanie konfiguracji tworzenia
- Podniesiono wersje
@@ -0,0 +1,2 @@
Co nowego:
- To jest wersja testowa CI
@@ -0,0 +1,5 @@
Co nowego:
- Automatyczne uruchamianie po ponownym uruchomieniu w trybie jądra stałego VPN
- Obsługa ikon adaptacyjnych motywów
- Naprawiono ikony powiadomień, ikonę kafelka
- Naprawiono ikony AndroidTV

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