mirror of
https://github.com/wgtunnel/android.git
synced 2026-07-03 14:07:49 +02:00
Compare commits
78 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0a9773d202 | |||
| 3cb4480a65 | |||
| a7f3255a76 | |||
| 7d7b99f448 | |||
| 74e9e462bb | |||
| 619e3c1cde | |||
| 77f8a8215b | |||
| 8772036dd7 | |||
| 63625ccbd7 | |||
| 9ac7ae77b3 | |||
| e062fbb34d | |||
| 16d5586433 | |||
| 48a3ad64f4 | |||
| e5796d641d | |||
| daf5eebdd2 | |||
| 4c725491f4 | |||
| 7529c11172 | |||
| 83f530df42 | |||
| 8083ab9526 | |||
| 7d1312da0f | |||
| d4dbc43c70 | |||
| 294f2624c7 | |||
| 0603cb2fdd | |||
| 48ddbcbb0e | |||
| e6c3e3f5b3 | |||
| 0d75699b40 | |||
| 5c98aab9e0 | |||
| a1e3489ba2 | |||
| bcd19b5494 | |||
| 160a6ca84d | |||
| aaf7ebd326 | |||
| b8c75a45e4 | |||
| ac17a09e19 | |||
| c51a7ee393 | |||
| c534516e33 | |||
| 9c999cc62c | |||
| cc3c865211 | |||
| 8648a67fdc | |||
| 9ee1fa69ed | |||
| 379ffdcbbf | |||
| 6e3c1324b2 | |||
| 660bea0104 | |||
| 2b8610fa8a | |||
| 944034ac74 | |||
| 9f394aeffb | |||
| 6c3c6891eb | |||
| af1848f12d | |||
| 96cffdfa7d | |||
| afebd975ea | |||
| 588a2a18bd | |||
| 221b38a119 | |||
| 0008d8b9bb | |||
| 9f85638b9a | |||
| fe54c9cd0e | |||
| 554499f9de | |||
| 12c9b52653 | |||
| 03712a6c1d | |||
| 5f03a97fcc | |||
| 6788b05fa0 | |||
| 9494853dee | |||
| 01695c3286 | |||
| bb6e45ed92 | |||
| 63e257f419 | |||
| e145cd95e1 | |||
| 7e790acbfe | |||
| 334aaa1c2b | |||
| 6201671dd0 | |||
| 517d90c3bf | |||
| d78443e7fa | |||
| 40d0466c14 | |||
| 5220c1a10c | |||
| 0e4e421628 | |||
| abdbf74755 | |||
| 5bc49eec50 | |||
| c7040b8081 | |||
| 26ecfec3fc | |||
| 5408cf3954 | |||
| 22c4a303fc |
@@ -70,16 +70,16 @@ jobs:
|
||||
outputs:
|
||||
UPLOAD_DIR_ANDROID: ${{ env.UPLOAD_DIR_ANDROID }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up JDK 17
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
java-version: '21'
|
||||
cache: gradle
|
||||
|
||||
- name: Grant execute permission for gradlew
|
||||
|
||||
@@ -72,15 +72,15 @@ jobs:
|
||||
outputs:
|
||||
UPLOAD_DIR_ANDROID: ${{ env.UPLOAD_DIR_ANDROID }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
- name: Set up JDK 17
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
java-version: '21'
|
||||
cache: gradle
|
||||
- name: Grant execute permission for gradlew
|
||||
run: chmod +x gradlew
|
||||
@@ -112,6 +112,9 @@ jobs:
|
||||
./gradlew :app:assemble${flavor^}Debug --stacktrace
|
||||
;;
|
||||
esac
|
||||
env:
|
||||
GITHUB_SHA: ${{ github.sha }}
|
||||
GITHUB_RUN_NUMBER: ${{ github.run_number }}
|
||||
- name: Get release apk path
|
||||
id: apk-path
|
||||
run: echo "path=$(find . -regex '^.*/build/outputs/apk/${{ inputs.flavor }}/${{ inputs.build_type }}/.*\.apk$' -type f | head -1 | tail -c+2)" >> $GITHUB_OUTPUT
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
has_new_commits: ${{ steps.check.outputs.new_commits }}
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
- name: Check for new commits
|
||||
id: check
|
||||
env:
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
|
||||
@@ -1,25 +1,30 @@
|
||||
name: on-pr
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
format_check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up JDK 17
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Verify Gradle Wrapper
|
||||
uses: gradle/actions/wrapper-validation@v6
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
java-version: '21'
|
||||
cache: gradle
|
||||
|
||||
- name: Grant execute permission for gradlew
|
||||
run: chmod +x gradlew
|
||||
|
||||
- name: Run ktfmt
|
||||
run: ./gradlew ktfmtCheck
|
||||
run: ./gradlew ktfmtCheck
|
||||
@@ -78,7 +78,7 @@ jobs:
|
||||
name: publish-github
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ github.event_name == 'push' && github.ref || 'master' }}
|
||||
- name: Install system dependencies
|
||||
@@ -187,57 +187,61 @@ jobs:
|
||||
repository: wgtunnel/fdroid
|
||||
event-type: fdroid-update
|
||||
|
||||
build-google-aab:
|
||||
if: >-
|
||||
${{
|
||||
github.event_name == 'push' ||
|
||||
inputs.track != 'none'
|
||||
}}
|
||||
uses: ./.github/workflows/build-aab.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
build_type: release
|
||||
flavor: google
|
||||
|
||||
publish-play:
|
||||
if: ${{ github.event_name == 'push' || inputs.track != 'none' }}
|
||||
name: Publish to Google Play
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
|
||||
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
|
||||
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
|
||||
KEY_STORE_FILE: 'android_keystore.jks'
|
||||
KEY_STORE_LOCATION: ${{ github.workspace }}/app/keystore/
|
||||
|
||||
needs: build-google-aab
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v5
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
cache: gradle
|
||||
ref: ${{ github.event_name == 'push' && github.ref || 'master' }}
|
||||
|
||||
- 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@v2.0
|
||||
- name: Download AAB artifact
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
fileName: ${{ env.KEY_STORE_FILE }}
|
||||
fileDir: ${{ env.KEY_STORE_LOCATION }}
|
||||
encodedString: ${{ secrets.KEYSTORE }}
|
||||
name: google-play-aab
|
||||
path: ${{ github.workspace }}/aab
|
||||
|
||||
# create keystore path for gradle to read
|
||||
- name: Create keystore path env var
|
||||
- name: Find exact AAB file path
|
||||
id: find-aab
|
||||
run: |
|
||||
store_path=${{ env.KEY_STORE_LOCATION }}${{ env.KEY_STORE_FILE }}
|
||||
echo "KEY_STORE_PATH=$store_path" >> $GITHUB_ENV
|
||||
AAB_PATH=$(find "${{ github.workspace }}/aab" -name "*.aab" -type f | head -1)
|
||||
if [ -z "$AAB_PATH" ]; then
|
||||
echo "ERROR: No .aab file found after download!"
|
||||
find "${{ github.workspace }}/aab" -type f
|
||||
exit 1
|
||||
fi
|
||||
echo "Found AAB: $AAB_PATH"
|
||||
echo "aab_path=$AAB_PATH" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create service_account.json
|
||||
id: createServiceAccount
|
||||
run: echo '${{ secrets.SERVICE_ACCOUNT_JSON }}' > service_account.json
|
||||
|
||||
- name: Deploy with fastlane
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: '3.2' # Not needed with a .ruby-version file
|
||||
ruby-version: '3.4'
|
||||
bundler-cache: true
|
||||
|
||||
- name: Distribute app to Prod track 🚀
|
||||
- name: Upload to Google Play
|
||||
run: |
|
||||
track=${{ github.event_name == 'push' && 'production' || inputs.track }}
|
||||
(cd ${{ github.workspace }} && bundle install && bundle exec fastlane $track --verbose)
|
||||
bundle exec fastlane run upload_to_play_store \
|
||||
track:"$track" \
|
||||
aab:"${{ steps.find-aab.outputs.aab_path }}" \
|
||||
json_key:"service_account.json" \
|
||||
package_name:"com.zaneschepke.wireguardautotunnel" \
|
||||
skip_upload_apk:true
|
||||
@@ -1,3 +1,4 @@
|
||||
source "https://rubygems.org"
|
||||
|
||||
gem "fastlane"
|
||||
gem "fastlane"
|
||||
gem "multi_json"
|
||||
@@ -49,8 +49,8 @@ and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/)
|
||||
|
||||
## About
|
||||
|
||||
WG Tunnel is an alternative Android client for WireGuard and AmneziaWG, inspired by the official WireGuard Android app. It fills gaps in the official client by adding advanced features like auto-tunneling (on-demand VPN activation), while seamlessly supporting both protocols across app modes—including Kernel (for direct WireGuard kernel integration; AmneziaWG not supported), VPN (standard system-level tunneling), Lockdown (a custom kill switch for leak prevention), and Proxy (built-in HTTP/SOCKS5 forwarding)—for enhanced privacy, censorship resistance, and flexibility.
|
||||
|
||||
WG Tunnel is an alternative Android client for WireGuard and AmneziaWG, inspired by the official WireGuard Android app. It fills gaps in the official client by adding advanced features like auto-tunneling, AmneziaWG support, different app modes like **Lockdown** (a custom kill switch for leak prevention), and **Local Proxy** (expose a tunnel over a local SOCKS5/HTTP proxy server) for enhanced privacy, censorship resistance, and flexibility.
|
||||
|
||||
</div>
|
||||
|
||||
<div style="text-align: left;">
|
||||
@@ -67,21 +67,18 @@ WG Tunnel is an alternative Android client for WireGuard and AmneziaWG, inspired
|
||||
|
||||
## Features
|
||||
|
||||
- **Tunnel Import Methods**: Easily add tunnels using .conf files, ZIP archives, manual entry, or QR code scanning.
|
||||
- **Auto-Tunneling**: Automatically activate tunnels based on Wi-Fi SSID, Ethernet connections, or mobile data networks.
|
||||
- **Split Tunneling**: Flexible support for routing specific apps or traffic through the VPN.
|
||||
- **WireGuard Modes**: Full compatibility with WireGuard in both kernel and userspace implementations.
|
||||
- **AmneziaWG Integration**: Userspace mode for AmneziaWG, providing robust censorship evasion.
|
||||
- **Always-On VPN**: Ensures continuous protection with Android's Always-On VPN feature.
|
||||
- **Quick Controls**: Quick Settings tile and home screen shortcuts for easy VPN toggling.
|
||||
- **Automation Support**: Intent-based automation for controlling tunnels.
|
||||
- **Auto-Restore**: Seamlessly restores auto-tunneling and active tunnels after device restarts or app updates.
|
||||
- **Proxying Options**: Built-in HTTP and SOCKS5 proxy support within tunnels.
|
||||
- **Lockdown Mode**: Custom kill switch for maximum leak prevention and security.
|
||||
- **Dynamic DNS Handling**: Detects and updates DNS changes without tunnel restarts.
|
||||
- **Monitoring Tools**: Advanced tunnel monitoring features for tunnel performance monitoring.
|
||||
- **Android TV Support**: Android TV support for secure streaming and browsing.
|
||||
- **Advanced DNS**: DNS over HTTPS support for tunnel endpoint resolutions.
|
||||
- **Auto-Tunneling:** Automatically activate tunnels based on your device's active network details.
|
||||
- **Deferred Endpoint Bootstrapping:** Safely resolves endpoints and updates peers after the tunnel is up for better reliability and leak protection on startup.
|
||||
- **Handshake Monitoring:** Real-time handshake monitoring for instant tunnel health feedback.
|
||||
- **AmneziaWG Support:** Full support for AmneziaWG 2.0, providing robust censorship protection.
|
||||
- **Split Tunneling:** Flexible support for routing specific apps or traffic through the VPN.
|
||||
- **Local Proxy Mode:** Expose WireGuard tunnels over a local SOCKS5 or HTTP proxy to browsers or firewall apps (like AdGuard).
|
||||
- **Lockdown Mode:** Advanced in-app kill switch that blocks all traffic while the tunnel is down.
|
||||
- **Quick Controls:** Quick Settings tile and home screen shortcuts for easy toggling.
|
||||
- **Remote Control Support:** Intent-based automation for controlling tunnels and auto-tunneling from automation apps (like Tasker).
|
||||
- **Dynamic DNS Handling:** Automatically detect and update endpoints on server IP changes without requiring a restart.
|
||||
- **IPv6 Endpoints:** Automatically upgrade to IPv6 endpoints or fall back to IPv4 based on network conditions without requiring a restart.
|
||||
- **Android TV Support:** Full support for nearly all features on Android TV.
|
||||
|
||||
## Building
|
||||
|
||||
|
||||
+38
-32
@@ -1,6 +1,5 @@
|
||||
import com.android.build.api.dsl.ApplicationExtension
|
||||
import com.android.build.api.variant.FilterConfiguration
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
@@ -23,13 +22,6 @@ licensee {
|
||||
ignoreDependencies("com.github.topjohnwu.libsu")
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget = JvmTarget.JVM_17
|
||||
freeCompilerArgs = listOf("-XXLanguage:+PropertyParamAnnotationDefaultTargetMode")
|
||||
}
|
||||
}
|
||||
|
||||
configure<ApplicationExtension> {
|
||||
namespace = Constants.APP_ID
|
||||
compileSdk = Constants.TARGET_SDK
|
||||
@@ -46,10 +38,11 @@ configure<ApplicationExtension> {
|
||||
|
||||
splits {
|
||||
abi {
|
||||
isEnable = !project.hasProperty("noSplits")
|
||||
val noSplits = providers.gradleProperty("noSplits").isPresent
|
||||
isEnable = !noSplits
|
||||
reset()
|
||||
include("armeabi-v7a", "arm64-v8a")
|
||||
isUniversalApk = !project.hasProperty("noSplits")
|
||||
isUniversalApk = !noSplits
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,14 +50,17 @@ configure<ApplicationExtension> {
|
||||
applicationId = Constants.APP_ID
|
||||
minSdk = Constants.MIN_SDK
|
||||
targetSdk = Constants.TARGET_SDK
|
||||
versionCode = computeVersionCode()
|
||||
versionName = computeVersionName()
|
||||
versionCode = Constants.VERSION_CODE
|
||||
versionName = Constants.VERSION_NAME
|
||||
|
||||
experimentalProperties["android.experimental.disableGitVersion"] = true
|
||||
|
||||
sourceSets {
|
||||
getByName("debug").assets.directories += "$projectDir/schemas"
|
||||
}
|
||||
|
||||
val languagesArray = buildLanguagesArray(languageList())
|
||||
val languagesProvider = project.languageListProvider()
|
||||
val languagesArray = buildLanguagesArray(languagesProvider.get())
|
||||
buildConfigField("String[]", "LANGUAGES", "new String[]{ $languagesArray }")
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
@@ -134,8 +130,6 @@ configure<ApplicationExtension> {
|
||||
|
||||
compileOptions {
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
@@ -148,31 +142,42 @@ configure<ApplicationExtension> {
|
||||
|
||||
androidComponents {
|
||||
onVariants { variant ->
|
||||
val isNightly = project.isNightlyBuild()
|
||||
|
||||
val abiNameMap =
|
||||
mapOf(
|
||||
"armeabi-v7a" to "armv7",
|
||||
"arm64-v8a" to "arm64",
|
||||
"x86" to "x86",
|
||||
"x86_64" to "x64",
|
||||
)
|
||||
if (isNightly) {
|
||||
variant.outputs.forEach { output ->
|
||||
|
||||
output.versionCode.set(
|
||||
output.versionCode.get() + project.getVersionCodeIncrement()
|
||||
)
|
||||
|
||||
val currentVersion = output.versionName.get()
|
||||
val nextVersion = bumpToNextPatchVersion(currentVersion)
|
||||
val gitHash = project.getGitCommitHash()
|
||||
|
||||
output.versionName.set("$nextVersion-nightly+git.$gitHash")
|
||||
}
|
||||
}
|
||||
|
||||
val abiNameMap = mapOf(
|
||||
"armeabi-v7a" to "armv7",
|
||||
"arm64-v8a" to "arm64",
|
||||
"x86" to "x86",
|
||||
"x86_64" to "x64",
|
||||
)
|
||||
|
||||
variant.outputs.forEach { output ->
|
||||
val abi = output.filters.find { it.filterType == FilterConfiguration.FilterType.ABI }?.identifier
|
||||
|
||||
val flavorName = variant.productFlavors.joinToString("-") { it.second }
|
||||
|
||||
val versionName = output.versionName.get()
|
||||
|
||||
val baseFileName = "${Constants.APP_NAME}-${flavorName}-v${versionName}"
|
||||
|
||||
val outputFileName =
|
||||
if (!abi.isNullOrEmpty()) {
|
||||
val shortAbiName = abiNameMap.getOrDefault(abi, abi)
|
||||
"${baseFileName}-${shortAbiName}.apk"
|
||||
} else {
|
||||
"${baseFileName}.apk"
|
||||
}
|
||||
val outputFileName = if (!abi.isNullOrEmpty()) {
|
||||
val shortAbiName = abiNameMap.getOrDefault(abi, abi)
|
||||
"${baseFileName}-${shortAbiName}.apk"
|
||||
} else {
|
||||
"${baseFileName}.apk"
|
||||
}
|
||||
|
||||
output.outputFileName.set(outputFileName)
|
||||
}
|
||||
@@ -225,6 +230,7 @@ dependencies {
|
||||
// UI utilities
|
||||
implementation(libs.bundles.ui.utilities)
|
||||
implementation(libs.lottie.compose)
|
||||
implementation(libs.sonner)
|
||||
|
||||
// Misc utilities
|
||||
implementation(libs.bundles.misc.utilities)
|
||||
|
||||
@@ -53,7 +53,6 @@
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.App.Start"
|
||||
tools:targetApi="tiramisu">
|
||||
@@ -74,6 +73,13 @@
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="wg" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<action android:name="android.intent.action.SHOW_APP_INFO" />
|
||||
@@ -169,9 +175,9 @@
|
||||
tools:node="remove" />
|
||||
</provider>
|
||||
<service
|
||||
android:name=".core.service.tile.TunnelControlTile"
|
||||
android:name=".service.tile.TunnelControlTile"
|
||||
android:exported="true"
|
||||
android:icon="@drawable/ic_notification"
|
||||
android:icon="@drawable/ic_qs_logo"
|
||||
android:label="@string/tunnel_control"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
<meta-data
|
||||
@@ -186,9 +192,9 @@
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service
|
||||
android:name=".core.service.tile.AutoTunnelControlTile"
|
||||
android:name=".service.tile.AutoTunnelControlTile"
|
||||
android:exported="true"
|
||||
android:icon="@drawable/ic_notification"
|
||||
android:icon="@drawable/ic_qs_logo"
|
||||
android:label="@string/auto_tunnel"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
<meta-data
|
||||
@@ -203,7 +209,7 @@
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service
|
||||
android:name=".core.service.autotunnel.AutoTunnelService"
|
||||
android:name=".service.autotunnel.AutoTunnelService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="specialUse"
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
package com.zaneschepke.wireguardautotunnel
|
||||
|
||||
import ProxySettingsScreen
|
||||
import android.Manifest
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.net.VpnService
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.SystemBarStyle
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
@@ -19,17 +21,29 @@ import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.CheckCircleOutline
|
||||
import androidx.compose.material.icons.outlined.ErrorOutline
|
||||
import androidx.compose.material.icons.outlined.FavoriteBorder
|
||||
import androidx.compose.material.icons.outlined.Info
|
||||
import androidx.compose.material.icons.outlined.WarningAmber
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -38,20 +52,19 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.LinkAnnotation
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextLinkStyles
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.withLink
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.zIndex
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
|
||||
@@ -59,6 +72,10 @@ import androidx.navigation3.runtime.entryProvider
|
||||
import androidx.navigation3.runtime.rememberNavBackStack
|
||||
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
||||
import androidx.navigation3.ui.NavDisplay
|
||||
import com.dokar.sonner.TextToastAction
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.dokar.sonner.Toaster
|
||||
import com.dokar.sonner.rememberToasterState
|
||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||
@@ -69,11 +86,9 @@ import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
|
||||
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.banner.AppAlertBanner
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.LocalNetworkPermissionDialog
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.VpnDeniedDialog
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.CustomSnackBar
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarInfo
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarType
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.rememberCustomSnackbarState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.SecureRoute
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.Tab
|
||||
@@ -110,19 +125,29 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.settings.ipv6.IPv6
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.sort.SortScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.splittunnel.SplitTunnelScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.AlertRed
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.Heart
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.OffWhite
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.Straw
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
|
||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.installApk
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.restartApp
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
|
||||
import com.zaneschepke.wireguardautotunnel.util.permission.LocalNetworkPermissionHelper
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.ConfigEditViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SplitTunnelViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelViewModel
|
||||
import de.raphaelebner.roomdatabasebackup.core.OnCompleteListener.Companion.EXIT_CODE_ERROR
|
||||
import de.raphaelebner.roomdatabasebackup.core.OnCompleteListener.Companion.EXIT_CODE_ERROR_DECRYPTION_ERROR
|
||||
import de.raphaelebner.roomdatabasebackup.core.OnCompleteListener.Companion.EXIT_CODE_ERROR_RESTORE_BACKUP_IS_ENCRYPTED
|
||||
import de.raphaelebner.roomdatabasebackup.core.OnCompleteListener.Companion.EXIT_CODE_ERROR_WRONG_DECRYPTION_PASSWORD
|
||||
import de.raphaelebner.roomdatabasebackup.core.RoomBackup
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlinx.coroutines.awaitCancellation
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
@@ -132,6 +157,7 @@ import org.koin.androidx.compose.koinViewModel
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
import org.koin.core.parameter.parametersOf
|
||||
import org.orbitmvi.orbit.compose.collectAsState
|
||||
import timber.log.Timber
|
||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
@@ -155,9 +181,10 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
handleIncomingIntent(intent)
|
||||
roomBackup = RoomBackup(this).database(appDatabase).enableLogDebug(true).maxFileCount(5)
|
||||
|
||||
roomBackup = RoomBackup(this)
|
||||
handleConfigFileIntent(intent)
|
||||
handleWgDeepLinkIntent(intent)
|
||||
|
||||
installSplashScreen().apply {
|
||||
setKeepOnScreenCondition { !viewModel.container.stateFlow.value.isAppLoaded }
|
||||
@@ -175,12 +202,74 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
val snackbarState = rememberCustomSnackbarState()
|
||||
val toaster = rememberToasterState()
|
||||
var showVpnPermissionDialog by remember { mutableStateOf(false) }
|
||||
var vpnPermissionDenied by remember { mutableStateOf(false) }
|
||||
var requestingTunnelMode by remember {
|
||||
mutableStateOf<Pair<TunnelMode?, TunnelConfig?>>(Pair(null, null))
|
||||
}
|
||||
var showLocalNetworkRationale by remember { mutableStateOf(false) }
|
||||
var hasPromptedLocalNetwork by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val localNetworkPermissionLauncher =
|
||||
rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.RequestPermission()
|
||||
) { isGranted ->
|
||||
if (!isGranted) {
|
||||
val canAskAgain =
|
||||
ActivityCompat.shouldShowRequestPermissionRationale(
|
||||
this,
|
||||
Manifest.permission.ACCESS_LOCAL_NETWORK,
|
||||
)
|
||||
|
||||
if (!canAskAgain) {
|
||||
val intent =
|
||||
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||
data = Uri.fromParts("package", packageName, null)
|
||||
}
|
||||
startActivity(intent)
|
||||
} else {
|
||||
toaster.show(
|
||||
message =
|
||||
context.getString(R.string.local_network_permission_denied),
|
||||
type = ToastType.Warning,
|
||||
duration = 6000.milliseconds,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(uiState.isAppLoaded) {
|
||||
if (
|
||||
uiState.isAppLoaded &&
|
||||
!hasPromptedLocalNetwork &&
|
||||
LocalNetworkPermissionHelper.shouldRequestPermission() &&
|
||||
!LocalNetworkPermissionHelper.isPermissionGranted(context)
|
||||
) {
|
||||
hasPromptedLocalNetwork = true
|
||||
showLocalNetworkRationale = true
|
||||
}
|
||||
}
|
||||
|
||||
if (showLocalNetworkRationale) {
|
||||
LocalNetworkPermissionDialog(
|
||||
onDismiss = {
|
||||
showLocalNetworkRationale = false
|
||||
toaster.show(
|
||||
message = context.getString(R.string.local_network_permission_denied),
|
||||
type = ToastType.Warning,
|
||||
duration = 6000.milliseconds,
|
||||
)
|
||||
},
|
||||
onContinue = {
|
||||
showLocalNetworkRationale = false
|
||||
|
||||
localNetworkPermissionLauncher.launch(
|
||||
Manifest.permission.ACCESS_LOCAL_NETWORK
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
val startingStack = buildList {
|
||||
add(Route.Tunnels)
|
||||
@@ -232,22 +321,18 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
is GlobalSideEffect.Snackbar -> {
|
||||
scope.launch {
|
||||
snackbarState.showSnackbar(
|
||||
SnackbarInfo(
|
||||
message =
|
||||
buildAnnotatedString {
|
||||
append(sideEffect.message.asString(context))
|
||||
},
|
||||
type = sideEffect.type ?: SnackbarType.INFO,
|
||||
durationMs = sideEffect.durationMs ?: 4000L,
|
||||
)
|
||||
)
|
||||
when (sideEffect.type) {
|
||||
ToastType.Warning,
|
||||
ToastType.Error -> toaster.dismissAll()
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
is GlobalSideEffect.Toast ->
|
||||
scope.launch { context.showToast(sideEffect.message.asString(context)) }
|
||||
toaster.show(
|
||||
message = sideEffect.message.asString(context),
|
||||
type = sideEffect.type,
|
||||
duration = (sideEffect.durationMs ?: 4000L).milliseconds,
|
||||
)
|
||||
}
|
||||
|
||||
is GlobalSideEffect.LaunchUrl -> context.openWebUrl(sideEffect.url)
|
||||
is GlobalSideEffect.InstallApk -> context.installApk(sideEffect.apk)
|
||||
@@ -275,49 +360,37 @@ class MainActivity : AppCompatActivity() {
|
||||
},
|
||||
)
|
||||
|
||||
val annotatedMessage = buildAnnotatedString {
|
||||
append(context.getString(R.string.donation_prompt_prefix))
|
||||
append(" ")
|
||||
withLink(
|
||||
LinkAnnotation.Clickable(
|
||||
tag = context.getString(R.string.support),
|
||||
styles =
|
||||
TextLinkStyles(
|
||||
style =
|
||||
SpanStyle(
|
||||
textDecoration = TextDecoration.Underline,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
focusedStyle =
|
||||
SpanStyle(
|
||||
textDecoration = TextDecoration.Underline,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
background =
|
||||
MaterialTheme.colorScheme.primary.copy(
|
||||
alpha = 0.2f
|
||||
),
|
||||
),
|
||||
),
|
||||
) {
|
||||
snackbarState.dismissCurrent()
|
||||
navController.push(Route.Donate)
|
||||
}
|
||||
) {
|
||||
append(context.getString(R.string.donation_prompt_link))
|
||||
}
|
||||
append(" ")
|
||||
append(context.getString(R.string.donation_prompt_suffix))
|
||||
uiState.pendingWgImportUrl?.let { url ->
|
||||
val host = Uri.parse(url).host ?: url
|
||||
InfoDialog(
|
||||
onDismiss = { viewModel.dismissWgImport() },
|
||||
onAttest = { viewModel.importFromUrl(url) },
|
||||
title = stringResource(R.string.add_from_url),
|
||||
body = { Text(stringResource(R.string.wg_url_confirm_message, host)) },
|
||||
confirmText = stringResource(R.string.okay),
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (uiState.shouldShowDonationSnackbar && !uiState.alreadyDonated) {
|
||||
viewModel.setShouldShowDonationSnackbar(false)
|
||||
snackbarState.showSnackbar(
|
||||
SnackbarInfo(
|
||||
message = annotatedMessage,
|
||||
type = SnackbarType.THANK_YOU,
|
||||
durationMs = 30_000L,
|
||||
)
|
||||
toaster.show(
|
||||
message =
|
||||
context.getString(R.string.donation_prompt_prefix) +
|
||||
" " +
|
||||
context.getString(R.string.donation_prompt_link) +
|
||||
" " +
|
||||
context.getString(R.string.donation_prompt_suffix),
|
||||
type = ToastType.Normal,
|
||||
duration = 30_000L.milliseconds,
|
||||
action =
|
||||
TextToastAction(
|
||||
text = context.getString(R.string.donate_title),
|
||||
onClick = { toastId ->
|
||||
toaster.dismiss(toastId)
|
||||
navController.push(Route.Donate)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -378,25 +451,6 @@ class MainActivity : AppCompatActivity() {
|
||||
)
|
||||
}
|
||||
Scaffold(
|
||||
snackbarHost = {
|
||||
snackbarState.SnackbarHost(
|
||||
modifier =
|
||||
Modifier.align(Alignment.BottomCenter)
|
||||
.padding(bottom = 80.dp)
|
||||
) { info ->
|
||||
CustomSnackBar(
|
||||
message = info.message,
|
||||
type = info.type,
|
||||
onDismiss = { snackbarState.dismissCurrent() },
|
||||
containerColor =
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(
|
||||
2.dp
|
||||
),
|
||||
modifier =
|
||||
Modifier.wrapContentHeight(align = Alignment.Top),
|
||||
)
|
||||
}
|
||||
},
|
||||
topBar = { DynamicTopAppBar(navState) },
|
||||
bottomBar = {
|
||||
if (navState.showBottomItems) {
|
||||
@@ -548,6 +602,70 @@ class MainActivity : AppCompatActivity() {
|
||||
)
|
||||
}
|
||||
}
|
||||
Toaster(
|
||||
state = toaster,
|
||||
alignment = Alignment.BottomCenter,
|
||||
offset = IntOffset(0, -220),
|
||||
richColors = true,
|
||||
background = {
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp),
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp),
|
||||
)
|
||||
)
|
||||
},
|
||||
elevation = 1.dp,
|
||||
shadowAmbientColor =
|
||||
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.08f),
|
||||
shadowSpotColor =
|
||||
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
|
||||
border = {
|
||||
BorderStroke(
|
||||
0.dp,
|
||||
androidx.compose.ui.graphics.Color.Transparent,
|
||||
)
|
||||
},
|
||||
actionSlot = { toast ->
|
||||
(toast.action as? TextToastAction)?.let { action ->
|
||||
TextButton(
|
||||
onClick = { action.onClick(toast) },
|
||||
colors =
|
||||
ButtonDefaults.textButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.primary
|
||||
),
|
||||
contentPadding = PaddingValues(horizontal = 12.dp),
|
||||
) {
|
||||
Text(text = action.text, fontWeight = FontWeight.Medium)
|
||||
}
|
||||
}
|
||||
},
|
||||
iconSlot = { toast ->
|
||||
val (icon, color) =
|
||||
when (toast.type) {
|
||||
ToastType.Success ->
|
||||
Icons.Outlined.CheckCircleOutline to SilverTree
|
||||
ToastType.Error ->
|
||||
Icons.Outlined.ErrorOutline to AlertRed
|
||||
ToastType.Warning ->
|
||||
Icons.Outlined.WarningAmber to Straw
|
||||
ToastType.Info ->
|
||||
Icons.Outlined.Info to
|
||||
MaterialTheme.colorScheme.onSurface
|
||||
ToastType.Normal ->
|
||||
Icons.Outlined.FavoriteBorder to Heart
|
||||
}
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = color,
|
||||
modifier = Modifier.padding(end = 12.dp),
|
||||
)
|
||||
},
|
||||
contentColor = { MaterialTheme.colorScheme.onSurface },
|
||||
shape = { RoundedCornerShape(16.dp) },
|
||||
showCloseButton = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -555,55 +673,14 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
fun performBackup() = lifecycleScope.launch {
|
||||
roomBackup
|
||||
.database(appDatabase)
|
||||
.backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG)
|
||||
.enableLogDebug(true)
|
||||
.maxFileCount(5)
|
||||
.apply {
|
||||
onCompleteListener { success, _, _ ->
|
||||
lifecycleScope.launch {
|
||||
if (success) {
|
||||
showToast(
|
||||
getString(
|
||||
R.string.backup_success,
|
||||
getString(R.string.restarting_app),
|
||||
)
|
||||
)
|
||||
restartApp()
|
||||
} else {
|
||||
showToast(R.string.backup_failed)
|
||||
}
|
||||
}
|
||||
}
|
||||
private fun handleWgDeepLinkIntent(intent: Intent) {
|
||||
if (intent.action == Intent.ACTION_VIEW) {
|
||||
val uri = intent.data ?: return
|
||||
if (uri.scheme == "wg") {
|
||||
val httpsUrl = uri.toString().replaceFirst("wg://", "https://")
|
||||
viewModel.promptWgImport(httpsUrl)
|
||||
}
|
||||
.backup()
|
||||
}
|
||||
|
||||
fun performRestore() = lifecycleScope.launch {
|
||||
roomBackup
|
||||
.database(appDatabase)
|
||||
.enableLogDebug(true)
|
||||
.backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG)
|
||||
.apply {
|
||||
onCompleteListener { success, _, _ ->
|
||||
lifecycleScope.launch {
|
||||
if (success) {
|
||||
showToast(
|
||||
getString(
|
||||
R.string.restore_success,
|
||||
getString(R.string.restarting_app),
|
||||
)
|
||||
)
|
||||
restartApp()
|
||||
} else {
|
||||
showToast(R.string.restore_failed)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.restore()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
@@ -611,21 +688,105 @@ class MainActivity : AppCompatActivity() {
|
||||
networkMonitor.checkPermissionsAndUpdateState()
|
||||
}
|
||||
|
||||
fun performBackup(encrypt: Boolean = false, password: String? = null) {
|
||||
roomBackup
|
||||
.backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG)
|
||||
.apply {
|
||||
if (encrypt && !password.isNullOrBlank()) {
|
||||
backupIsEncrypted(true)
|
||||
customEncryptPassword(password)
|
||||
}
|
||||
}
|
||||
.onCompleteListener { success, _, _ ->
|
||||
lifecycleScope.launch {
|
||||
val sideEffect =
|
||||
if (success) {
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.backup_success),
|
||||
ToastType.Success,
|
||||
)
|
||||
} else {
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.backup_failed),
|
||||
ToastType.Error,
|
||||
)
|
||||
}
|
||||
viewModel.postSideEffect(sideEffect)
|
||||
}
|
||||
}
|
||||
.backup()
|
||||
}
|
||||
|
||||
fun performRestore(encrypt: Boolean = false, password: String? = null) {
|
||||
roomBackup
|
||||
.backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG)
|
||||
.apply {
|
||||
if (encrypt && !password.isNullOrBlank()) {
|
||||
backupIsEncrypted(true)
|
||||
customEncryptPassword(password)
|
||||
}
|
||||
}
|
||||
.onCompleteListener { success, message, exitCode ->
|
||||
lifecycleScope.launch {
|
||||
if (success) {
|
||||
viewModel.postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.restore_success),
|
||||
ToastType.Success,
|
||||
)
|
||||
)
|
||||
roomBackup.restartApp(Intent(this@MainActivity, MainActivity::class.java))
|
||||
} else {
|
||||
Timber.w("Restore failed, exitCode=$exitCode, message=$message")
|
||||
|
||||
val errorMessage =
|
||||
when (exitCode) {
|
||||
EXIT_CODE_ERROR_WRONG_DECRYPTION_PASSWORD ->
|
||||
getString(R.string.restore_failed_wrong_password)
|
||||
|
||||
EXIT_CODE_ERROR,
|
||||
EXIT_CODE_ERROR_DECRYPTION_ERROR,
|
||||
EXIT_CODE_ERROR_RESTORE_BACKUP_IS_ENCRYPTED ->
|
||||
getString(R.string.restore_failed_invalid_file)
|
||||
|
||||
else -> getString(R.string.restore_failed)
|
||||
}
|
||||
|
||||
viewModel.postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.DynamicString(errorMessage),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.restore()
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
setIntent(intent)
|
||||
handleIncomingIntent(intent)
|
||||
handleConfigFileIntent(intent)
|
||||
handleWgDeepLinkIntent(intent)
|
||||
}
|
||||
|
||||
private fun handleIncomingIntent(intent: Intent?) {
|
||||
private fun handleConfigFileIntent(intent: Intent?) {
|
||||
intent ?: return
|
||||
|
||||
when (intent.action) {
|
||||
Intent.ACTION_VIEW,
|
||||
Intent.ACTION_EDIT,
|
||||
Intent.ACTION_SEND -> {
|
||||
val uri: Uri? = intent.data
|
||||
uri?.let { viewModel.importFromUri(it) }
|
||||
val uri: Uri? = intent.data ?: return
|
||||
val name = uri?.lastPathSegment?.lowercase() ?: return
|
||||
if (
|
||||
!name.endsWith(FileUtils.CONF_FILE_EXTENSION) &&
|
||||
!name.endsWith(FileUtils.ZIP_FILE_EXTENSION)
|
||||
) {
|
||||
Timber.d("Ignoring non-config URI in handleIncomingIntent: $uri")
|
||||
return
|
||||
}
|
||||
viewModel.importFromUri(uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,8 @@ import com.zaneschepke.tunnel.backend.Backend
|
||||
import com.zaneschepke.tunnel.di.tunnelModule
|
||||
import com.zaneschepke.tunnel.service.VpnService
|
||||
import com.zaneschepke.wireguardautotunnel.core.event.TunnelEventDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.AppBoostrapCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.tile.AutoTunnelTileRefresher
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.tile.TunnelTileRefresher
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
||||
import com.zaneschepke.wireguardautotunnel.di.Dispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.di.Scope
|
||||
@@ -21,6 +18,9 @@ import com.zaneschepke.wireguardautotunnel.di.dispatchersModule
|
||||
import com.zaneschepke.wireguardautotunnel.di.networkModule
|
||||
import com.zaneschepke.wireguardautotunnel.di.tunnelBackendProviderModule
|
||||
import com.zaneschepke.wireguardautotunnel.di.workerModule
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelTileRefresher
|
||||
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelTileRefresher
|
||||
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -51,6 +51,13 @@ class WireGuardAutoTunnel : Application(), KoinComponent {
|
||||
|
||||
private val backend: Backend by inject()
|
||||
|
||||
private val alwaysOnCallback =
|
||||
object : VpnService.AlwaysOnCallback {
|
||||
override fun alwaysOnTriggered() {
|
||||
applicationScope.launch { tunnelCoordinator.startDefault() }
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(KoinViewModelScopeApi::class)
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
@@ -71,11 +78,10 @@ class WireGuardAutoTunnel : Application(), KoinComponent {
|
||||
lazyModules(networkModule)
|
||||
}
|
||||
instance = this
|
||||
|
||||
notificationService.createAllChannels()
|
||||
|
||||
// Sync tiles
|
||||
AutoTunnelTileRefresher.refresh(this)
|
||||
TunnelTileRefresher.refresh(this)
|
||||
syncTiles()
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Timber.plant(Timber.DebugTree())
|
||||
@@ -87,13 +93,7 @@ class WireGuardAutoTunnel : Application(), KoinComponent {
|
||||
Timber.plant(ReleaseTree())
|
||||
}
|
||||
|
||||
backend.setAlwaysOnCallback(
|
||||
object : VpnService.AlwaysOnCallback {
|
||||
override fun alwaysOnTriggered() {
|
||||
applicationScope.launch { tunnelCoordinator.startDefault() }
|
||||
}
|
||||
}
|
||||
)
|
||||
backend.setAlwaysOnCallback(alwaysOnCallback)
|
||||
|
||||
val dispatcher = get<TunnelEventDispatcher>()
|
||||
val coordinator = get<TunnelCoordinator>()
|
||||
@@ -111,6 +111,11 @@ class WireGuardAutoTunnel : Application(), KoinComponent {
|
||||
applicationScope.launch(ioDispatcher) { boostrapCoordinator.bootstrap() }
|
||||
}
|
||||
|
||||
private fun syncTiles() {
|
||||
AutoTunnelTileRefresher.refresh(this)
|
||||
TunnelTileRefresher.refresh(this)
|
||||
}
|
||||
|
||||
companion object {
|
||||
lateinit var instance: WireGuardAutoTunnel
|
||||
private set
|
||||
|
||||
+1
-1
@@ -3,11 +3,11 @@ package com.zaneschepke.wireguardautotunnel.core.broadcast
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.AutoTunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.di.Scope
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.component.KoinComponent
|
||||
|
||||
+160
-14
@@ -1,29 +1,41 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.event
|
||||
|
||||
import android.content.Context
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.tunnel.event.TunnelEvent
|
||||
import com.zaneschepke.tunnel.model.BackendMode
|
||||
import com.zaneschepke.tunnel.state.BackendStatus
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.TunnelNotificationLine
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.TunnelNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.lifecyle.AppVisibilityObserver
|
||||
import com.zaneschepke.wireguardautotunnel.notification.TunnelNotificationLine
|
||||
import com.zaneschepke.wireguardautotunnel.notification.TunnelNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class TunnelEventDispatcher(
|
||||
private val notificationManager: TunnelNotificationService,
|
||||
private val tunnelRepository: TunnelRepository,
|
||||
private val context: Context,
|
||||
private val appVisibilityObserver: AppVisibilityObserver,
|
||||
private val globalEffectRepository: GlobalEffectRepository,
|
||||
) {
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
fun bind(
|
||||
scope: CoroutineScope,
|
||||
providerEvents: Flow<TunnelEvent>,
|
||||
@@ -32,54 +44,174 @@ class TunnelEventDispatcher(
|
||||
tunnelDisplayStates: StateFlow<Map<Int, DisplayTunnelState>>,
|
||||
) {
|
||||
|
||||
// informational events
|
||||
// Informational events from tunnel backend
|
||||
providerEvents
|
||||
.distinctUntilChanged()
|
||||
.onEach { event ->
|
||||
when (event) {
|
||||
is TunnelEvent.FallbackToIpv4 -> {
|
||||
val name = getTunnelName(event.tunnelId)
|
||||
notificationManager.showIpv4Fallback(name)
|
||||
showOrNotify(
|
||||
scope = scope,
|
||||
foregroundAction = {
|
||||
globalEffectRepository.post(
|
||||
GlobalSideEffect.Snackbar(
|
||||
message =
|
||||
StringValue.DynamicString(
|
||||
context.getString(
|
||||
R.string.notification_ipv4_fallback_message,
|
||||
name,
|
||||
)
|
||||
),
|
||||
type = ToastType.Info,
|
||||
)
|
||||
)
|
||||
},
|
||||
backgroundAction = { notificationManager.showIpv4Fallback(name) },
|
||||
)
|
||||
}
|
||||
|
||||
is TunnelEvent.RecoveredToIpv6 -> {
|
||||
val name = getTunnelName(event.tunnelId)
|
||||
notificationManager.showIpv6Recovery(name)
|
||||
showOrNotify(
|
||||
scope = scope,
|
||||
foregroundAction = {
|
||||
globalEffectRepository.post(
|
||||
GlobalSideEffect.Snackbar(
|
||||
message =
|
||||
StringValue.DynamicString(
|
||||
context.getString(
|
||||
R.string.notification_ipv6_recovery_message,
|
||||
name,
|
||||
)
|
||||
),
|
||||
type = ToastType.Success,
|
||||
)
|
||||
)
|
||||
},
|
||||
backgroundAction = { notificationManager.showIpv6Recovery(name) },
|
||||
)
|
||||
}
|
||||
|
||||
is TunnelEvent.DynamicDnsUpdate -> {
|
||||
val name = getTunnelName(event.tunnelId)
|
||||
notificationManager.showDynamicDnsUpdate(name)
|
||||
showOrNotify(
|
||||
scope = scope,
|
||||
foregroundAction = {
|
||||
globalEffectRepository.post(
|
||||
GlobalSideEffect.Snackbar(
|
||||
message =
|
||||
StringValue.DynamicString(
|
||||
context.getString(
|
||||
R.string.notification_dynamic_dns_message,
|
||||
name,
|
||||
)
|
||||
),
|
||||
type = ToastType.Info,
|
||||
)
|
||||
)
|
||||
},
|
||||
backgroundAction = { notificationManager.showDynamicDnsUpdate(name) },
|
||||
)
|
||||
}
|
||||
|
||||
is TunnelEvent.NoRootShellAccess -> {
|
||||
notificationManager.showRootShellAccess()
|
||||
showOrNotify(
|
||||
scope = scope,
|
||||
foregroundAction = {
|
||||
globalEffectRepository.post(
|
||||
GlobalSideEffect.Snackbar(
|
||||
message =
|
||||
StringValue.DynamicString(
|
||||
context.getString(R.string.error_root_denied)
|
||||
),
|
||||
type = ToastType.Error,
|
||||
)
|
||||
)
|
||||
},
|
||||
backgroundAction = { notificationManager.showRootShellAccess() },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.launchIn(scope)
|
||||
|
||||
// errors from the coordinator
|
||||
// Errors from our tunnel coordinator
|
||||
coordinatorErrors
|
||||
.distinctUntilChanged()
|
||||
.onEach { error ->
|
||||
when (error) {
|
||||
is TunnelErrorEvent.VpnPermissionDenied -> {
|
||||
notificationManager.showVpnRequired()
|
||||
showOrNotify(
|
||||
scope = scope,
|
||||
foregroundAction = {
|
||||
globalEffectRepository.post(
|
||||
GlobalSideEffect.Snackbar(
|
||||
message =
|
||||
StringValue.DynamicString(
|
||||
context.getString(R.string.vpn_permission_required)
|
||||
),
|
||||
type = ToastType.Error,
|
||||
)
|
||||
)
|
||||
},
|
||||
backgroundAction = { notificationManager.showVpnRequired() },
|
||||
)
|
||||
}
|
||||
|
||||
is TunnelErrorEvent.InternalFailure -> {
|
||||
notificationManager.showError(error.message)
|
||||
showOrNotify(
|
||||
scope = scope,
|
||||
foregroundAction = {
|
||||
globalEffectRepository.post(
|
||||
GlobalSideEffect.Snackbar(
|
||||
message = StringValue.DynamicString(error.message),
|
||||
type = ToastType.Error,
|
||||
)
|
||||
)
|
||||
},
|
||||
backgroundAction = { notificationManager.showError(error.message) },
|
||||
)
|
||||
}
|
||||
|
||||
is TunnelErrorEvent.Socks5PortUnavailable -> {
|
||||
val name = getTunnelName(error.tunnelId)
|
||||
notificationManager.showSocks5PortUnavailable(error.port, name)
|
||||
val message =
|
||||
context.getString(R.string.error_socks5_port_unavailable, error.port)
|
||||
|
||||
showOrNotify(
|
||||
scope = scope,
|
||||
foregroundAction = {
|
||||
globalEffectRepository.post(
|
||||
GlobalSideEffect.Snackbar(
|
||||
message = StringValue.DynamicString(message),
|
||||
type = ToastType.Error,
|
||||
)
|
||||
)
|
||||
},
|
||||
backgroundAction = {
|
||||
notificationManager.showSocks5PortUnavailable(error.port, name)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
is TunnelErrorEvent.HttpPortUnavailable -> {
|
||||
val name = getTunnelName(error.tunnelId)
|
||||
notificationManager.showHttpPortUnavailable(error.port, name)
|
||||
val message =
|
||||
context.getString(R.string.error_http_port_unavailable, error.port)
|
||||
|
||||
showOrNotify(
|
||||
scope = scope,
|
||||
foregroundAction = {
|
||||
globalEffectRepository.post(
|
||||
GlobalSideEffect.Snackbar(
|
||||
message = StringValue.DynamicString(message),
|
||||
type = ToastType.Error,
|
||||
)
|
||||
)
|
||||
},
|
||||
backgroundAction = {
|
||||
notificationManager.showHttpPortUnavailable(error.port, name)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,6 +245,7 @@ class TunnelEventDispatcher(
|
||||
.associateBy { it.id }
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.debounce(500.milliseconds) // give the service notification time to display
|
||||
.onEach { vpnLines -> notificationManager.updateVpnPersistentNotification(vpnLines) }
|
||||
.launchIn(scope)
|
||||
|
||||
@@ -140,12 +273,25 @@ class TunnelEventDispatcher(
|
||||
.associateBy { it.id }
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.debounce(500.milliseconds) // give the service notification time to display
|
||||
.onEach { proxyLines ->
|
||||
notificationManager.updateProxyPersistentNotification(proxyLines)
|
||||
}
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
private fun showOrNotify(
|
||||
scope: CoroutineScope,
|
||||
foregroundAction: suspend () -> Unit,
|
||||
backgroundAction: () -> Unit,
|
||||
) {
|
||||
if (appVisibilityObserver.isForeground.value) {
|
||||
scope.launch { foregroundAction() }
|
||||
} else {
|
||||
backgroundAction()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getTunnelName(tunnelId: Int): String {
|
||||
return tunnelRepository.getById(tunnelId)?.name ?: context.getString(R.string.unknown)
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.orchestration
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelStateHolder
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.autotunnel.AutoTunnelStateHolder
|
||||
|
||||
class AutoTunnelCoordinator(
|
||||
private val repository: AutoTunnelSettingsRepository,
|
||||
|
||||
+38
-7
@@ -2,7 +2,6 @@ package com.zaneschepke.wireguardautotunnel.core.orchestration
|
||||
|
||||
import com.zaneschepke.tunnel.model.BackendMode
|
||||
import com.zaneschepke.wireguardautotunnel.core.event.TunnelErrorEvent
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.RoomDnsSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelActionSource
|
||||
@@ -17,6 +16,7 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepos
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.ProxySettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -47,6 +47,9 @@ class TunnelCoordinator(
|
||||
scope: CoroutineScope,
|
||||
) {
|
||||
|
||||
private val _userOverrideFlow = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
|
||||
val userOverrideFlow = _userOverrideFlow.asSharedFlow()
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
val tunnelDisplayStates: StateFlow<Map<Int, DisplayTunnelState>> =
|
||||
tunnelProvider.backendStatus
|
||||
@@ -107,15 +110,34 @@ class TunnelCoordinator(
|
||||
) = tunnelMutex.withLock {
|
||||
// wait for app to be bootstrapped
|
||||
bootstrapCoordinator.isReady.first { it }
|
||||
|
||||
if (source == TunnelActionSource.USER) {
|
||||
_userOverrideFlow.tryEmit(Unit)
|
||||
}
|
||||
|
||||
// enforce single tunnel, for now
|
||||
if (backendStatus.value.activeTunnels.isNotEmpty()) {
|
||||
stopActiveTunnelsInternal(source)
|
||||
}
|
||||
|
||||
startTunnelInternal(config, source)
|
||||
}
|
||||
|
||||
suspend fun stopTunnel(id: Int, source: TunnelActionSource = TunnelActionSource.USER) =
|
||||
tunnelMutex.withLock {
|
||||
if (source == TunnelActionSource.USER) {
|
||||
_userOverrideFlow.tryEmit(Unit)
|
||||
}
|
||||
stopTunnelInternal(id, source)
|
||||
}
|
||||
|
||||
suspend fun stopActiveTunnels() = tunnelMutex.withLock { stopActiveTunnelsInternal() }
|
||||
suspend fun stopActiveTunnels(source: TunnelActionSource = TunnelActionSource.USER) =
|
||||
tunnelMutex.withLock {
|
||||
if (source == TunnelActionSource.USER) {
|
||||
_userOverrideFlow.tryEmit(Unit)
|
||||
}
|
||||
stopActiveTunnelsInternal(source)
|
||||
}
|
||||
|
||||
private suspend fun startTunnelInternal(
|
||||
tunnelConfig: TunnelConfig,
|
||||
@@ -167,9 +189,6 @@ class TunnelCoordinator(
|
||||
}
|
||||
}
|
||||
|
||||
// TODO for now, enforce single tunnel until multi-tunneling is implement
|
||||
stopActiveTunnelsInternal()
|
||||
|
||||
tunnelProvider
|
||||
.startTunnel(
|
||||
tunnel =
|
||||
@@ -193,6 +212,10 @@ class TunnelCoordinator(
|
||||
|
||||
suspend fun toggleTunnels(source: TunnelActionSource = TunnelActionSource.USER) =
|
||||
tunnelMutex.withLock {
|
||||
if (source == TunnelActionSource.USER) {
|
||||
_userOverrideFlow.tryEmit(Unit)
|
||||
}
|
||||
|
||||
val active = tunnelProvider.backendStatus.value.activeTunnels
|
||||
if (active.isNotEmpty()) {
|
||||
lastActiveTunnels = active.keys.toList()
|
||||
@@ -201,7 +224,7 @@ class TunnelCoordinator(
|
||||
_actions.emit(TunnelActionEvent.Stopped(tunnelId = id, source = source))
|
||||
}
|
||||
|
||||
stopActiveTunnelsInternal()
|
||||
stopActiveTunnelsInternal(source)
|
||||
return@withLock
|
||||
}
|
||||
|
||||
@@ -226,7 +249,15 @@ class TunnelCoordinator(
|
||||
.onFailure { _errors.emit(TunnelErrorEvent.from(it, id)) }
|
||||
}
|
||||
|
||||
private suspend fun stopActiveTunnelsInternal() {
|
||||
private suspend fun stopActiveTunnelsInternal(
|
||||
source: TunnelActionSource = TunnelActionSource.USER
|
||||
) {
|
||||
val active = tunnelProvider.backendStatus.value.activeTunnels
|
||||
|
||||
active.keys.forEach { id ->
|
||||
_actions.emit(TunnelActionEvent.Stopped(tunnelId = id, source = source))
|
||||
}
|
||||
|
||||
tunnelProvider.stopActiveTunnels()
|
||||
}
|
||||
}
|
||||
|
||||
+3
-4
@@ -19,9 +19,8 @@ class ShortcutsActivity : ComponentActivity() {
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
applicationScope.launch {
|
||||
shortcutCoordinator.handle(intent)
|
||||
finish()
|
||||
}
|
||||
finish()
|
||||
|
||||
applicationScope.launch { shortcutCoordinator.handle(intent) }
|
||||
}
|
||||
}
|
||||
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.zaneschepke.tunnel.ApplicationProvider
|
||||
import com.zaneschepke.tunnel.model.BackendMode
|
||||
import com.zaneschepke.tunnel.state.BackendStatus
|
||||
import com.zaneschepke.wireguardautotunnel.MainActivity
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.notification.AndroidNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.PROXY_GROUP_KEY
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.VPN_GROUP_KEY
|
||||
import com.zaneschepke.wireguardautotunnel.notification.TunnelNotificationLine
|
||||
import com.zaneschepke.wireguardautotunnel.notification.TunnelNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelTileRefresher
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
class AndroidApplicationProvider(
|
||||
private val notificationService: NotificationService,
|
||||
private val tunnelNotificationService: TunnelNotificationService,
|
||||
private val tunnelRepository: TunnelRepository,
|
||||
) : ApplicationProvider {
|
||||
|
||||
private val context: Context = notificationService.context
|
||||
|
||||
override fun refreshTile(context: Context) {
|
||||
TunnelTileRefresher.refresh(context)
|
||||
}
|
||||
|
||||
override fun createVpnConfigurePendingIntent(context: Context): PendingIntent {
|
||||
return PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
Intent(context, MainActivity::class.java).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
},
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
|
||||
)
|
||||
}
|
||||
|
||||
override val vpnInitNotification: Notification
|
||||
get() =
|
||||
notificationService.createNotification(
|
||||
channel = AndroidNotificationService.NotificationChannels.Tunnel.VPN,
|
||||
title = context.getString(R.string.initializing),
|
||||
onGoing = true,
|
||||
groupKey = VPN_GROUP_KEY,
|
||||
)
|
||||
|
||||
override val proxyInitNotification: Notification
|
||||
get() =
|
||||
notificationService.createNotification(
|
||||
channel = AndroidNotificationService.NotificationChannels.Tunnel.Proxy,
|
||||
title = context.getString(R.string.initializing),
|
||||
onGoing = true,
|
||||
groupKey = PROXY_GROUP_KEY,
|
||||
)
|
||||
|
||||
override val vpnNotificationId: Int
|
||||
get() = NotificationService.VPN_NOTIFICATION_ID
|
||||
|
||||
override val proxyNotificationId: Int
|
||||
get() = NotificationService.PROXY_NOTIFICATION_ID
|
||||
|
||||
override suspend fun buildVpnPersistentNotification(
|
||||
currentStatus: BackendStatus
|
||||
): Notification {
|
||||
val lines = computeVpnNotificationLines(currentStatus)
|
||||
return tunnelNotificationService.buildVpnPersistentNotification(lines)
|
||||
}
|
||||
|
||||
override suspend fun buildProxyPersistentNotification(
|
||||
currentStatus: BackendStatus
|
||||
): Notification {
|
||||
val lines = computeProxyNotificationLines(currentStatus)
|
||||
return tunnelNotificationService.buildProxyPersistentNotification(lines)
|
||||
}
|
||||
|
||||
private suspend fun computeVpnNotificationLines(
|
||||
status: BackendStatus
|
||||
): Map<Int, TunnelNotificationLine> {
|
||||
val activeTunnels = status.activeTunnels
|
||||
val allTunnels = tunnelRepository.userTunnelsFlow.first()
|
||||
return activeTunnels
|
||||
.mapNotNull { (id, activeTunnel) ->
|
||||
val mode = activeTunnel.mode ?: return@mapNotNull null
|
||||
if (mode !is BackendMode.Vpn && mode !is BackendMode.Proxy.KillSwitchPrimary)
|
||||
return@mapNotNull null
|
||||
val tunnel = allTunnels.find { it.id == id } ?: return@mapNotNull null
|
||||
val displayState = DisplayTunnelState.from(activeTunnel)
|
||||
TunnelNotificationLine(id, tunnel.name, displayState)
|
||||
}
|
||||
.associateBy { it.id }
|
||||
}
|
||||
|
||||
private suspend fun computeProxyNotificationLines(
|
||||
status: BackendStatus
|
||||
): Map<Int, TunnelNotificationLine> {
|
||||
val activeTunnels = status.activeTunnels
|
||||
val allTunnels = tunnelRepository.userTunnelsFlow.first()
|
||||
return activeTunnels
|
||||
.mapNotNull { (id, activeTunnel) ->
|
||||
val mode = activeTunnel.mode ?: return@mapNotNull null
|
||||
if (mode !is BackendMode.Proxy.Standard) return@mapNotNull null
|
||||
val tunnel = allTunnels.find { it.id == id } ?: return@mapNotNull null
|
||||
val displayState = DisplayTunnelState.from(activeTunnel)
|
||||
TunnelNotificationLine(id, tunnel.name, displayState)
|
||||
}
|
||||
.associateBy { it.id }
|
||||
}
|
||||
}
|
||||
-9
@@ -4,14 +4,11 @@ import com.zaneschepke.tunnel.Tunnel
|
||||
import com.zaneschepke.tunnel.backend.Backend
|
||||
import com.zaneschepke.tunnel.model.BackendMode
|
||||
import com.zaneschepke.tunnel.state.BackendStatus
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.LockdownSettings
|
||||
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
@@ -52,10 +49,4 @@ class TunnelBackendProvider(
|
||||
override suspend fun disableLockDown(): Result<Unit> {
|
||||
return backend.disableKillSwitch()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val localErrorEvents = MutableSharedFlow<Pair<String?, BackendCoreException>>()
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val localMessageEvents = MutableSharedFlow<Pair<String?, BackendMessage>>()
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@ import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkerParameters
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelStateHolder
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.autotunnel.AutoTunnelStateHolder
|
||||
import java.util.concurrent.TimeUnit
|
||||
import timber.log.Timber
|
||||
|
||||
|
||||
@@ -6,14 +6,14 @@ import android.os.StrictMode
|
||||
import com.zaneschepke.logcatter.LogReader
|
||||
import com.zaneschepke.logcatter.LogcatReader
|
||||
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.AndroidNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelStateHolder
|
||||
import com.zaneschepke.wireguardautotunnel.core.shortcut.DynamicShortcutManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.SelectedTunnelsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.notification.AndroidNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.autotunnel.AutoTunnelStateHolder
|
||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||
import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AutoTunnelViewModel
|
||||
|
||||
@@ -1,25 +1,19 @@
|
||||
package com.zaneschepke.wireguardautotunnel.di
|
||||
|
||||
import android.app.Notification
|
||||
import android.content.Context
|
||||
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
|
||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||
import com.zaneschepke.networkmonitor.StableNetworkEngine
|
||||
import com.zaneschepke.tunnel.NotificationProvider
|
||||
import com.zaneschepke.tunnel.backend.RootShell
|
||||
import com.zaneschepke.tunnel.ApplicationProvider
|
||||
import com.zaneschepke.tunnel.util.RootShell
|
||||
import com.zaneschepke.tunnel.util.RootShellException
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.event.TunnelEventDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.AndroidNotificationService.NotificationChannels
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.AndroidTunnelNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.PROXY_GROUP_KEY
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.VPN_GROUP_KEY
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.TunnelNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.tile.TunnelTileRefresher
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.AndroidApplicationProvider
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelBackendProvider
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.lifecyle.AppVisibilityObserver
|
||||
import com.zaneschepke.wireguardautotunnel.notification.AndroidTunnelNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.notification.TunnelNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.to
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -36,40 +30,15 @@ import timber.log.Timber
|
||||
|
||||
val tunnelBackendProviderModule = module {
|
||||
single<TunnelNotificationService> { AndroidTunnelNotificationService(get()) }
|
||||
single { AppVisibilityObserver() }
|
||||
singleOf(::TunnelEventDispatcher)
|
||||
|
||||
single<NotificationProvider> {
|
||||
val notificationService = get<NotificationService>()
|
||||
val context = androidContext()
|
||||
object : NotificationProvider {
|
||||
override val vpnInitNotification: Notification
|
||||
get() =
|
||||
notificationService.createNotification(
|
||||
channel = NotificationChannels.Tunnel.VPN,
|
||||
title = context.getString(R.string.initializing),
|
||||
onGoing = true,
|
||||
groupKey = VPN_GROUP_KEY,
|
||||
)
|
||||
|
||||
override val proxyInitNotification: Notification
|
||||
get() =
|
||||
notificationService.createNotification(
|
||||
channel = NotificationChannels.Tunnel.Proxy,
|
||||
title = context.getString(R.string.initializing),
|
||||
onGoing = true,
|
||||
groupKey = PROXY_GROUP_KEY,
|
||||
)
|
||||
|
||||
override val vpnNotificationId: Int
|
||||
get() = NotificationService.VPN_NOTIFICATION_ID
|
||||
|
||||
override val proxyNotificationId: Int
|
||||
get() = NotificationService.PROXY_NOTIFICATION_ID
|
||||
|
||||
override fun refreshTile(context: Context) {
|
||||
TunnelTileRefresher.refresh(context)
|
||||
}
|
||||
}
|
||||
single<ApplicationProvider> {
|
||||
AndroidApplicationProvider(
|
||||
notificationService = get(),
|
||||
tunnelNotificationService = get(),
|
||||
tunnelRepository = get(),
|
||||
)
|
||||
}
|
||||
|
||||
single {
|
||||
|
||||
@@ -7,4 +7,6 @@ sealed interface AutoTunnelEvent {
|
||||
data class Sync(val start: Set<TunnelConfig>, val stop: Set<Int>) : AutoTunnelEvent
|
||||
|
||||
data object DoNothing : AutoTunnelEvent
|
||||
|
||||
data object StopAllDueToNoInternet : AutoTunnelEvent
|
||||
}
|
||||
|
||||
+7
-2
@@ -4,7 +4,12 @@ import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelActionSource
|
||||
|
||||
sealed interface TunnelActionEvent {
|
||||
|
||||
data class Started(val tunnelId: Int, val source: TunnelActionSource) : TunnelActionEvent
|
||||
val source: TunnelActionSource
|
||||
val tunnelId: Int
|
||||
|
||||
data class Stopped(val tunnelId: Int, val source: TunnelActionSource) : TunnelActionEvent
|
||||
data class Started(override val tunnelId: Int, override val source: TunnelActionSource) :
|
||||
TunnelActionEvent
|
||||
|
||||
data class Stopped(override val tunnelId: Int, override val source: TunnelActionSource) :
|
||||
TunnelActionEvent
|
||||
}
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ import kotlinx.coroutines.flow.asSharedFlow
|
||||
class GlobalEffectRepository {
|
||||
|
||||
private val _globalEffectFlow =
|
||||
MutableSharedFlow<GlobalSideEffect>(replay = 0, extraBufferCapacity = 1)
|
||||
MutableSharedFlow<GlobalSideEffect>(replay = 0, extraBufferCapacity = 0)
|
||||
val flow = _globalEffectFlow.asSharedFlow()
|
||||
|
||||
suspend fun post(effect: GlobalSideEffect) {
|
||||
|
||||
+2
-4
@@ -1,8 +1,8 @@
|
||||
package com.zaneschepke.wireguardautotunnel.domain.sideeffect
|
||||
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarType
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import java.io.File
|
||||
|
||||
@@ -10,14 +10,12 @@ sealed class GlobalSideEffect {
|
||||
|
||||
data class Snackbar(
|
||||
val message: StringValue,
|
||||
val type: SnackbarType? = null,
|
||||
val type: ToastType,
|
||||
val actionLabel: String? = null,
|
||||
val onAction: (() -> Unit)? = null,
|
||||
val durationMs: Long? = null,
|
||||
) : GlobalSideEffect()
|
||||
|
||||
data class Toast(val message: StringValue) : GlobalSideEffect()
|
||||
|
||||
data object PopBackStack : GlobalSideEffect()
|
||||
|
||||
data class LaunchUrl(val url: String) : GlobalSideEffect()
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package com.zaneschepke.wireguardautotunnel.lifecyle
|
||||
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
class AppVisibilityObserver : DefaultLifecycleObserver {
|
||||
|
||||
private val _isForeground = MutableStateFlow(false)
|
||||
val isForeground: StateFlow<Boolean> = _isForeground.asStateFlow()
|
||||
|
||||
init {
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
||||
}
|
||||
|
||||
override fun onStart(owner: LifecycleOwner) {
|
||||
_isForeground.value = true
|
||||
}
|
||||
|
||||
override fun onStop(owner: LifecycleOwner) {
|
||||
_isForeground.value = false
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.notification
|
||||
package com.zaneschepke.wireguardautotunnel.notification
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Notification
|
||||
@@ -17,8 +17,8 @@ import androidx.core.app.NotificationManagerCompat.IMPORTANCE_HIGH
|
||||
import com.zaneschepke.wireguardautotunnel.MainActivity
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.broadcast.NotificationActionReceiver
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.EXTRA_ID
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.EXTRA_ID
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
|
||||
class AndroidNotificationService(override val context: Context) : NotificationService {
|
||||
+130
-25
@@ -1,21 +1,116 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.notification
|
||||
package com.zaneschepke.wireguardautotunnel.notification
|
||||
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.AndroidNotificationService.NotificationChannels
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.PROXY_GROUP_KEY
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.PROXY_NOTIFICATION_ID
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.TUNNEL_ERROR_NOTIFICATION_ID
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.TUNNEL_MESSAGES_NOTIFICATION_ID
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.VPN_GROUP_KEY
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.VPN_NOTIFICATION_ID
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||
import com.zaneschepke.wireguardautotunnel.notification.AndroidNotificationService.NotificationChannels
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.PROXY_GROUP_KEY
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.PROXY_NOTIFICATION_ID
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.TUNNEL_ERROR_NOTIFICATION_ID
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.TUNNEL_MESSAGES_NOTIFICATION_ID
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.VPN_GROUP_KEY
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.VPN_NOTIFICATION_ID
|
||||
|
||||
class AndroidTunnelNotificationService(private val notificationService: NotificationService) :
|
||||
TunnelNotificationService {
|
||||
|
||||
private val context = notificationService.context
|
||||
|
||||
private fun createGroupNotification(
|
||||
tunnelNotificationLines: Map<Int, TunnelNotificationLine>,
|
||||
channel: NotificationChannels.Tunnel,
|
||||
groupKey: String,
|
||||
): android.app.Notification {
|
||||
val title =
|
||||
if (tunnelNotificationLines.size == 1) {
|
||||
val name = tunnelNotificationLines.values.first().name
|
||||
when (channel) {
|
||||
is NotificationChannels.Tunnel.VPN ->
|
||||
"${context.getString(R.string.vpn)} • $name"
|
||||
is NotificationChannels.Tunnel.Proxy ->
|
||||
"${context.getString(R.string.proxy)} • $name"
|
||||
}
|
||||
} else {
|
||||
when (channel) {
|
||||
is NotificationChannels.Tunnel.VPN -> context.getString(R.string.vpn)
|
||||
is NotificationChannels.Tunnel.Proxy -> context.getString(R.string.proxy)
|
||||
}
|
||||
}
|
||||
|
||||
val formattedLines =
|
||||
tunnelNotificationLines.values.map { line ->
|
||||
val status = line.displayState.asLocalizedString(context)
|
||||
|
||||
if (tunnelNotificationLines.size == 1) {
|
||||
status
|
||||
} else {
|
||||
context.getString(R.string.notification_tunnel_status_format, line.name, status)
|
||||
}
|
||||
}
|
||||
val description = formattedLines.joinToString("\n")
|
||||
|
||||
val actions =
|
||||
if (tunnelNotificationLines.size == 1) {
|
||||
val tunnelId = tunnelNotificationLines.keys.first()
|
||||
listOf(
|
||||
notificationService.createNotificationAction(
|
||||
notificationAction = NotificationAction.TUNNEL_OFF,
|
||||
extraId = tunnelId,
|
||||
)
|
||||
)
|
||||
} else {
|
||||
listOf(
|
||||
notificationService.createNotificationAction(
|
||||
notificationAction = NotificationAction.STOP_ALL,
|
||||
extraId = null,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val style =
|
||||
if (tunnelNotificationLines.size > 1) {
|
||||
NotificationCompat.InboxStyle()
|
||||
.setBigContentTitle(title)
|
||||
.setSummaryText(
|
||||
"${tunnelNotificationLines.size} ${context.getString(R.string.tunnels).lowercase()}"
|
||||
)
|
||||
.also { inbox -> formattedLines.forEach { inbox.addLine(it) } }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
return notificationService.createNotification(
|
||||
channel = channel,
|
||||
title = title,
|
||||
description = description,
|
||||
actions = actions,
|
||||
onGoing = true,
|
||||
onlyAlertOnce = true,
|
||||
groupKey = groupKey,
|
||||
style = style,
|
||||
)
|
||||
}
|
||||
|
||||
override fun buildVpnPersistentNotification(
|
||||
tunnelNotificationLines: Map<Int, TunnelNotificationLine>
|
||||
): android.app.Notification {
|
||||
return createGroupNotification(
|
||||
tunnelNotificationLines,
|
||||
NotificationChannels.Tunnel.VPN,
|
||||
VPN_GROUP_KEY,
|
||||
)
|
||||
}
|
||||
|
||||
override fun buildProxyPersistentNotification(
|
||||
tunnelNotificationLines: Map<Int, TunnelNotificationLine>
|
||||
): android.app.Notification {
|
||||
return createGroupNotification(
|
||||
tunnelNotificationLines,
|
||||
NotificationChannels.Tunnel.Proxy,
|
||||
PROXY_GROUP_KEY,
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateGroupNotification(
|
||||
tunnelNotificationLines: Map<Int, TunnelNotificationLine>,
|
||||
notificationId: Int,
|
||||
@@ -88,26 +183,36 @@ class AndroidTunnelNotificationService(private val notificationService: Notifica
|
||||
notificationService.show(notificationId, notification)
|
||||
}
|
||||
|
||||
override fun updateProxyPersistentNotification(
|
||||
tunnelNotificationLines: Map<Int, TunnelNotificationLine>
|
||||
) {
|
||||
updateGroupNotification(
|
||||
tunnelNotificationLines = tunnelNotificationLines,
|
||||
notificationId = PROXY_NOTIFICATION_ID,
|
||||
channel = NotificationChannels.Tunnel.Proxy,
|
||||
groupKey = PROXY_GROUP_KEY,
|
||||
)
|
||||
}
|
||||
|
||||
override fun updateVpnPersistentNotification(
|
||||
tunnelNotificationLines: Map<Int, TunnelNotificationLine>
|
||||
) {
|
||||
updateGroupNotification(
|
||||
tunnelNotificationLines = tunnelNotificationLines,
|
||||
notificationId = VPN_NOTIFICATION_ID,
|
||||
channel = NotificationChannels.Tunnel.VPN,
|
||||
groupKey = VPN_GROUP_KEY,
|
||||
)
|
||||
if (tunnelNotificationLines.isEmpty()) {
|
||||
notificationService.remove(VPN_NOTIFICATION_ID)
|
||||
return
|
||||
}
|
||||
val notification =
|
||||
createGroupNotification(
|
||||
tunnelNotificationLines,
|
||||
NotificationChannels.Tunnel.VPN,
|
||||
VPN_GROUP_KEY,
|
||||
)
|
||||
notificationService.show(VPN_NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
override fun updateProxyPersistentNotification(
|
||||
tunnelNotificationLines: Map<Int, TunnelNotificationLine>
|
||||
) {
|
||||
if (tunnelNotificationLines.isEmpty()) {
|
||||
notificationService.remove(PROXY_NOTIFICATION_ID)
|
||||
return
|
||||
}
|
||||
val notification =
|
||||
createGroupNotification(
|
||||
tunnelNotificationLines,
|
||||
NotificationChannels.Tunnel.Proxy,
|
||||
PROXY_GROUP_KEY,
|
||||
)
|
||||
notificationService.show(PROXY_NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
override fun showIpv4Fallback(tunnelName: String) {
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.notification
|
||||
package com.zaneschepke.wireguardautotunnel.notification
|
||||
|
||||
import android.app.Notification
|
||||
import android.content.Context
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.AndroidNotificationService.NotificationChannels
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||
import com.zaneschepke.wireguardautotunnel.notification.AndroidNotificationService.NotificationChannels
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
|
||||
interface NotificationService {
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.notification
|
||||
package com.zaneschepke.wireguardautotunnel.notification
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState
|
||||
|
||||
+11
-1
@@ -1,4 +1,6 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.notification
|
||||
package com.zaneschepke.wireguardautotunnel.notification
|
||||
|
||||
import android.app.Notification
|
||||
|
||||
interface TunnelNotificationService {
|
||||
|
||||
@@ -6,6 +8,14 @@ interface TunnelNotificationService {
|
||||
|
||||
fun updateVpnPersistentNotification(tunnelNotificationLines: Map<Int, TunnelNotificationLine>)
|
||||
|
||||
fun buildVpnPersistentNotification(
|
||||
tunnelNotificationLines: Map<Int, TunnelNotificationLine>
|
||||
): Notification
|
||||
|
||||
fun buildProxyPersistentNotification(
|
||||
tunnelNotificationLines: Map<Int, TunnelNotificationLine>
|
||||
): Notification
|
||||
|
||||
fun showIpv4Fallback(tunnelName: String)
|
||||
|
||||
fun showIpv6Recovery(tunnelName: String)
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service
|
||||
package com.zaneschepke.wireguardautotunnel.service
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.VpnService
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelService
|
||||
import com.zaneschepke.wireguardautotunnel.service.autotunnel.AutoTunnelService
|
||||
|
||||
class ServiceManager(private val context: Context) {
|
||||
|
||||
+13
-6
@@ -1,4 +1,4 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.autotunnel
|
||||
package com.zaneschepke.wireguardautotunnel.service.autotunnel
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
@@ -17,6 +17,7 @@ class AutoTunnelEngine {
|
||||
}
|
||||
}
|
||||
Decision.None -> AutoTunnelEvent.DoNothing
|
||||
is Decision.StopDueToNoInternet -> AutoTunnelEvent.StopAllDueToNoInternet
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,13 +28,17 @@ class AutoTunnelEngine {
|
||||
|
||||
val activeTunnelIds = backend.activeTunnels.keys.toSet()
|
||||
|
||||
val desiredTunnels = resolveDesiredTunnels(state).map { it.id }.toSet()
|
||||
|
||||
// stop condition overrides everything
|
||||
if (!network.hasInternet() && settings.isStopOnNoInternetEnabled) {
|
||||
return Decision.Sync(start = emptySet(), stop = activeTunnelIds)
|
||||
if (!network.hasInternet()) {
|
||||
return if (settings.isStopOnNoInternetEnabled) {
|
||||
Decision.StopDueToNoInternet
|
||||
} else {
|
||||
// keep tunnel state neutral on no internet otherwise
|
||||
Decision.None
|
||||
}
|
||||
}
|
||||
|
||||
val desiredTunnels = resolveDesiredTunnels(state).map { it.id }.toSet()
|
||||
|
||||
val toStart = desiredTunnels - activeTunnelIds
|
||||
val toStop = activeTunnelIds - desiredTunnels
|
||||
|
||||
@@ -96,5 +101,7 @@ class AutoTunnelEngine {
|
||||
data class Sync(val start: Set<TunnelConfig>, val stop: Set<Int>) : Decision
|
||||
|
||||
data object None : Decision
|
||||
|
||||
data object StopDueToNoInternet : Decision
|
||||
}
|
||||
}
|
||||
+89
-75
@@ -1,4 +1,4 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.autotunnel
|
||||
package com.zaneschepke.wireguardautotunnel.service.autotunnel
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.core.app.ServiceCompat
|
||||
@@ -7,16 +7,12 @@ import androidx.lifecycle.lifecycleScope
|
||||
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
|
||||
import com.zaneschepke.networkmonitor.StableNetworkEngine
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.AndroidNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.tile.AutoTunnelTileRefresher
|
||||
import com.zaneschepke.wireguardautotunnel.di.Dispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelActionSource
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.TunnelActionEvent
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.AutoTunnelSettings
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
||||
@@ -24,15 +20,22 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepos
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.AutoTunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.toDomain
|
||||
import com.zaneschepke.wireguardautotunnel.notification.AndroidNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelTileRefresher
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.to
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
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.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -63,8 +66,7 @@ class AutoTunnelService : LifecycleService() {
|
||||
private var autoTunnelJob: Job? = null
|
||||
private var permissionsJob: Job? = null
|
||||
private var overridesJob: Job? = null
|
||||
|
||||
@Volatile private var manualOverrideState = ManualOverrideState()
|
||||
private var noInternetStopJob: Job? = null
|
||||
|
||||
private data class PermissionWarningState(
|
||||
val detectionMethod: AndroidNetworkMonitor.WifiDetectionMethod,
|
||||
@@ -73,19 +75,19 @@ class AutoTunnelService : LifecycleService() {
|
||||
val ssidReadRequired: Boolean,
|
||||
)
|
||||
|
||||
private data class ManualOverrideState(
|
||||
val fingerprint: AutoTunnelState.NetworkFingerprint? = null,
|
||||
val stoppedTunnelIds: Set<Int> = emptySet(),
|
||||
val startedTunnelIds: Set<Int> = emptySet(),
|
||||
)
|
||||
@Volatile private var hasUserOverride = false
|
||||
private var lastNetworkFingerprint: AutoTunnelState.NetworkFingerprint? = null
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
private val autoTunnelStateFlow: Flow<AutoTunnelState> by lazy {
|
||||
val networkFlow = networkEngine.stableState.mapNotNull { it?.state?.toDomain() }
|
||||
|
||||
val settingsFlow = combineSettings()
|
||||
|
||||
val backendFlow =
|
||||
tunnelCoordinator.backendStatus.distinctUntilChangedBy { it.activeTunnels.keys.toSet() }
|
||||
tunnelCoordinator.backendStatus
|
||||
.distinctUntilChanged { old, new -> old.activeTunnels == new.activeTunnels }
|
||||
.debounce(300L.milliseconds)
|
||||
|
||||
combine(networkFlow, settingsFlow, backendFlow) { network, settings, backend ->
|
||||
AutoTunnelState(
|
||||
@@ -121,7 +123,7 @@ class AutoTunnelService : LifecycleService() {
|
||||
permissionsJob?.cancel()
|
||||
permissionsJob = startLocationPermissionsNotificationJob()
|
||||
overridesJob?.cancel()
|
||||
overridesJob = startOverridesJob()
|
||||
overridesJob = startUserOverrideJob()
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
@@ -130,48 +132,23 @@ class AutoTunnelService : LifecycleService() {
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
cancelNoInternetStopJob()
|
||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||
stateHolder.setActive(false)
|
||||
AutoTunnelTileRefresher.refresh(this)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun startOverridesJob(): Job =
|
||||
private fun startUserOverrideJob(): Job =
|
||||
lifecycleScope.launch(ioDispatcher) {
|
||||
tunnelCoordinator.actions.collect { action ->
|
||||
tunnelCoordinator.userOverrideFlow.collect {
|
||||
reconciliationMutex.withLock {
|
||||
manualOverrideState =
|
||||
when (action) {
|
||||
is TunnelActionEvent.Started -> {
|
||||
|
||||
if (action.source != TunnelActionSource.USER) {
|
||||
return@withLock
|
||||
}
|
||||
|
||||
manualOverrideState.copy(
|
||||
startedTunnelIds =
|
||||
manualOverrideState.startedTunnelIds + action.tunnelId,
|
||||
stoppedTunnelIds =
|
||||
manualOverrideState.stoppedTunnelIds - action.tunnelId,
|
||||
)
|
||||
}
|
||||
|
||||
is TunnelActionEvent.Stopped -> {
|
||||
|
||||
if (action.source != TunnelActionSource.USER) {
|
||||
return@withLock
|
||||
}
|
||||
|
||||
manualOverrideState.copy(
|
||||
stoppedTunnelIds =
|
||||
manualOverrideState.stoppedTunnelIds + action.tunnelId,
|
||||
startedTunnelIds =
|
||||
manualOverrideState.startedTunnelIds - action.tunnelId,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Timber.d("Updated manual overrides: $manualOverrideState")
|
||||
if (!hasUserOverride) {
|
||||
Timber.d(
|
||||
"User manually overrode Auto Tunnel on current network. Pausing auto decisions."
|
||||
)
|
||||
}
|
||||
hasUserOverride = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -202,50 +179,83 @@ class AutoTunnelService : LifecycleService() {
|
||||
)
|
||||
}
|
||||
|
||||
// Instead of stopping tunnel right away on no internet, we kick off this job to add short delay
|
||||
// and re-evaluation to prevent unwanted stops
|
||||
// on flaky networks and network transitions
|
||||
private fun scheduleNoInternetStop() {
|
||||
noInternetStopJob?.cancel()
|
||||
|
||||
noInternetStopJob =
|
||||
lifecycleScope.launch(ioDispatcher) {
|
||||
delay(NO_INTERNET_GRACE_PERIOD_MS.milliseconds)
|
||||
|
||||
reconciliationMutex.withLock {
|
||||
val currentNetworkState = networkEngine.stableState.value?.state?.toDomain()
|
||||
|
||||
val stillNoInternet = currentNetworkState?.hasInternet() == false
|
||||
val stopOnNoInternetEnabled =
|
||||
autoTunnelRepository.flow.firstOrNull()?.isStopOnNoInternetEnabled == true
|
||||
|
||||
if (stillNoInternet && stopOnNoInternetEnabled) {
|
||||
val currentActiveIds =
|
||||
tunnelCoordinator.backendStatus.value.activeTunnels.keys
|
||||
|
||||
if (currentActiveIds.isNotEmpty()) {
|
||||
Timber.w(
|
||||
"No internet grace period expired and still no internet. Stopping tunnels: $currentActiveIds"
|
||||
)
|
||||
currentActiveIds.forEach { tunnelId ->
|
||||
tunnelCoordinator.stopTunnel(
|
||||
tunnelId,
|
||||
TunnelActionSource.AUTO_TUNNEL,
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Timber.d(
|
||||
"No internet grace period expired, but internet is back or setting disabled. Doing nothing."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun cancelNoInternetStopJob() {
|
||||
noInternetStopJob?.cancel()
|
||||
noInternetStopJob = null
|
||||
}
|
||||
|
||||
private fun startAutoTunnelStateJob(): Job =
|
||||
lifecycleScope.launch(ioDispatcher) {
|
||||
autoTunnelStateFlow.collectLatest { state ->
|
||||
reconciliationMutex.withLock {
|
||||
updateFingerprintIfNeeded(state)
|
||||
|
||||
val rawEvent = engine.evaluate(state)
|
||||
|
||||
val event = applyOverrides(rawEvent)
|
||||
|
||||
Timber.d("AutoTunnel reconciliation event: $event")
|
||||
|
||||
handleAutoTunnelEvent(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateFingerprintIfNeeded(state: AutoTunnelState) {
|
||||
val fingerprint = state.networkFingerPrint
|
||||
val currentFingerprint = state.networkFingerPrint
|
||||
|
||||
if (manualOverrideState.fingerprint != fingerprint) {
|
||||
Timber.d("Network changed, clearing overrides")
|
||||
|
||||
manualOverrideState = ManualOverrideState(fingerprint = fingerprint)
|
||||
if (lastNetworkFingerprint != currentFingerprint) {
|
||||
if (hasUserOverride) {
|
||||
Timber.d("Network fingerprint changed, clearing user override")
|
||||
}
|
||||
hasUserOverride = false
|
||||
lastNetworkFingerprint = currentFingerprint
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyOverrides(event: AutoTunnelEvent): AutoTunnelEvent {
|
||||
|
||||
if (event !is AutoTunnelEvent.Sync) {
|
||||
return event
|
||||
return if (hasUserOverride) {
|
||||
AutoTunnelEvent.DoNothing
|
||||
} else {
|
||||
event
|
||||
}
|
||||
|
||||
val filteredStart =
|
||||
event.start.filterNot { it.id in manualOverrideState.stoppedTunnelIds }.toSet()
|
||||
|
||||
val filteredStop =
|
||||
event.stop.filterNot { it in manualOverrideState.startedTunnelIds }.toSet()
|
||||
|
||||
if (filteredStart.isEmpty() && filteredStop.isEmpty()) {
|
||||
return AutoTunnelEvent.DoNothing
|
||||
}
|
||||
|
||||
return event.copy(start = filteredStart, stop = filteredStop)
|
||||
}
|
||||
|
||||
private fun combineSettings():
|
||||
@@ -334,7 +344,7 @@ class AutoTunnelService : LifecycleService() {
|
||||
private suspend fun handleAutoTunnelEvent(event: AutoTunnelEvent) {
|
||||
when (event) {
|
||||
is AutoTunnelEvent.Sync -> {
|
||||
|
||||
cancelNoInternetStopJob()
|
||||
event.stop.forEach { tunnelId ->
|
||||
Timber.d("Stopping tunnel: $tunnelId")
|
||||
tunnelCoordinator.stopTunnel(tunnelId, TunnelActionSource.AUTO_TUNNEL)
|
||||
@@ -345,8 +355,12 @@ class AutoTunnelService : LifecycleService() {
|
||||
tunnelCoordinator.startTunnel(config, TunnelActionSource.AUTO_TUNNEL)
|
||||
}
|
||||
}
|
||||
|
||||
AutoTunnelEvent.StopAllDueToNoInternet -> scheduleNoInternetStop()
|
||||
AutoTunnelEvent.DoNothing -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val NO_INTERNET_GRACE_PERIOD_MS = 10_000L
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.autotunnel
|
||||
package com.zaneschepke.wireguardautotunnel.service.autotunnel
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.tile
|
||||
package com.zaneschepke.wireguardautotunnel.service.tile
|
||||
|
||||
import android.service.quicksettings.Tile
|
||||
import android.service.quicksettings.TileService
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.AutoTunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelStateHolder
|
||||
import com.zaneschepke.wireguardautotunnel.service.autotunnel.AutoTunnelStateHolder
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.tile
|
||||
package com.zaneschepke.wireguardautotunnel.service.tile
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.tile
|
||||
package com.zaneschepke.wireguardautotunnel.service.tile
|
||||
|
||||
import android.content.Context
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.tile
|
||||
package com.zaneschepke.wireguardautotunnel.service.tile
|
||||
|
||||
import android.os.Build
|
||||
import android.service.quicksettings.Tile
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.tile
|
||||
package com.zaneschepke.wireguardautotunnel.service.tile
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.common.dialog
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
|
||||
@Composable
|
||||
fun LocalNetworkPermissionDialog(onDismiss: () -> Unit, onContinue: () -> Unit) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(text = stringResource(R.string.local_network_permission_title)) },
|
||||
text = {
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(R.string.local_network_permission_intro),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.local_network_permission_issues_intro),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Medium,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.local_network_permission_feature_tunnels),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.local_network_permission_feature_autotunnel),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.local_network_permission_feature_proxy),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.local_network_permission_nearby_devices),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.local_network_permission_recommendation),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Medium,
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onContinue) { Text(text = stringResource(R.string._continue)) }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) { Text(text = stringResource(R.string.not_now)) }
|
||||
},
|
||||
)
|
||||
}
|
||||
-104
@@ -1,104 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.common.snackbar
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Favorite
|
||||
import androidx.compose.material.icons.rounded.Close
|
||||
import androidx.compose.material.icons.rounded.Info
|
||||
import androidx.compose.material.icons.rounded.Warning
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Snackbar
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
|
||||
|
||||
@Composable
|
||||
fun CustomSnackBar(
|
||||
message: AnnotatedString,
|
||||
onDismiss: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
type: SnackbarType = SnackbarType.INFO,
|
||||
containerColor: Color = MaterialTheme.colorScheme.surface,
|
||||
) {
|
||||
val isTv = LocalIsAndroidTV.current
|
||||
val icon =
|
||||
when (type) {
|
||||
SnackbarType.INFO -> Icons.Rounded.Info
|
||||
SnackbarType.WARNING -> Icons.Rounded.Warning
|
||||
SnackbarType.THANK_YOU -> Icons.Outlined.Favorite
|
||||
}
|
||||
val iconDescription =
|
||||
when (type) {
|
||||
SnackbarType.INFO -> stringResource(R.string.info)
|
||||
SnackbarType.WARNING -> stringResource(R.string.warning)
|
||||
SnackbarType.THANK_YOU -> stringResource(R.string.thank_you)
|
||||
}
|
||||
|
||||
Snackbar(
|
||||
containerColor = containerColor,
|
||||
modifier =
|
||||
modifier
|
||||
.wrapContentHeight(align = Alignment.Top)
|
||||
.padding(horizontal = if (isTv) 48.dp else 16.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
) {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.height(IntrinsicSize.Min)
|
||||
.width(IntrinsicSize.Min)
|
||||
.padding(vertical = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().weight(1f),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
) {
|
||||
Icon(
|
||||
icon,
|
||||
contentDescription = iconDescription,
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Text(
|
||||
text = message,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
maxLines = 8,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
|
||||
Row {
|
||||
IconButton(onClick = onDismiss, modifier = Modifier.size(24.dp)) {
|
||||
Icon(
|
||||
Icons.Rounded.Close,
|
||||
contentDescription = stringResource(R.string.stop),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-76
@@ -1,76 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.common.snackbar
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun rememberCustomSnackbarState(): CustomSnackbarState {
|
||||
return remember { CustomSnackbarState() }
|
||||
}
|
||||
|
||||
class CustomSnackbarState {
|
||||
private val _snackbars = Channel<SnackbarInfo>(Channel.BUFFERED)
|
||||
val snackbars: Channel<SnackbarInfo> = _snackbars
|
||||
|
||||
private var currentSnackbar by mutableStateOf<SnackbarInfo?>(null)
|
||||
private var isShowing by mutableStateOf(false)
|
||||
|
||||
fun showSnackbar(info: SnackbarInfo) {
|
||||
_snackbars.trySend(info)
|
||||
}
|
||||
|
||||
fun dismissCurrent() {
|
||||
currentSnackbar = null
|
||||
isShowing = false
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SnackbarHost(
|
||||
modifier: Modifier = Modifier,
|
||||
snackbar: @Composable (SnackbarInfo) -> Unit = { info ->
|
||||
CustomSnackBar(
|
||||
message = info.message,
|
||||
type = info.type,
|
||||
onDismiss = { dismissCurrent() },
|
||||
modifier = Modifier,
|
||||
containerColor = MaterialTheme.colorScheme.surface.copy(.1f),
|
||||
)
|
||||
},
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
LaunchedEffect(Unit) {
|
||||
for (info in snackbars) {
|
||||
currentSnackbar = info
|
||||
isShowing = true
|
||||
|
||||
scope.launch {
|
||||
delay(info.durationMs)
|
||||
if (currentSnackbar?.id == info.id) {
|
||||
dismissCurrent()
|
||||
}
|
||||
}
|
||||
|
||||
while (isShowing && currentSnackbar?.id == info.id) {
|
||||
delay(100)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
currentSnackbar?.let { info ->
|
||||
if (isShowing) {
|
||||
Box(modifier = modifier) { snackbar(info) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.common.snackbar
|
||||
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
|
||||
enum class SnackbarType {
|
||||
INFO,
|
||||
WARNING,
|
||||
THANK_YOU,
|
||||
}
|
||||
|
||||
data class SnackbarInfo(
|
||||
val message: AnnotatedString,
|
||||
val type: SnackbarType = SnackbarType.INFO,
|
||||
val durationMs: Long = 4000L,
|
||||
val id: String = System.currentTimeMillis().toString(),
|
||||
)
|
||||
+49
-33
@@ -1,13 +1,18 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.common.textbox
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
@@ -29,48 +34,59 @@ fun ConfigurationTextBox(
|
||||
leading: (@Composable () -> Unit)? = null,
|
||||
trailing: (@Composable (Modifier) -> Unit)? = null,
|
||||
supportingText: (@Composable () -> Unit)? = null,
|
||||
interactionSource: MutableInteractionSource = MutableInteractionSource(),
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||
enabled: Boolean = true,
|
||||
readOnly: Boolean = false,
|
||||
singleLine: Boolean = true,
|
||||
) {
|
||||
Box(modifier = modifier.padding(top = 6.dp)) {
|
||||
CustomTextField(
|
||||
isError = isError,
|
||||
textStyle =
|
||||
MaterialTheme.typography.bodySmall.copy(
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth().defaultMinSize(minHeight = 48.dp),
|
||||
value = value,
|
||||
visualTransformation = visualTransformation,
|
||||
singleLine = singleLine,
|
||||
interactionSource = interactionSource,
|
||||
onValueChange = onValueChange,
|
||||
label = null, // Disable built in label
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
placeholder = {
|
||||
Text(
|
||||
text = hint,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
},
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
trailing = trailing,
|
||||
supportingText = supportingText,
|
||||
leading = leading,
|
||||
readOnly = readOnly,
|
||||
enabled = enabled,
|
||||
)
|
||||
|
||||
CustomTextField(
|
||||
isError = isError,
|
||||
textStyle =
|
||||
MaterialTheme.typography.bodySmall.copy(color = MaterialTheme.colorScheme.onSurface),
|
||||
modifier = modifier.fillMaxWidth().defaultMinSize(minHeight = 48.dp),
|
||||
value = value,
|
||||
visualTransformation = visualTransformation,
|
||||
singleLine = singleLine,
|
||||
interactionSource = interactionSource,
|
||||
onValueChange = { onValueChange(it) },
|
||||
label = {
|
||||
// custom static label notch
|
||||
if (label.isNotEmpty()) {
|
||||
Text(
|
||||
label,
|
||||
text = label,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
modifier =
|
||||
Modifier.padding(start = 12.dp)
|
||||
.offset(y = (-8).dp)
|
||||
.background(MaterialTheme.colorScheme.surface)
|
||||
.padding(horizontal = 4.dp),
|
||||
)
|
||||
},
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
placeholder = {
|
||||
Text(
|
||||
hint,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
},
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
trailing = trailing,
|
||||
supportingText = supportingText,
|
||||
leading = leading,
|
||||
readOnly = readOnly,
|
||||
enabled = enabled,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+24
-15
@@ -22,7 +22,9 @@ import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@@ -33,7 +35,7 @@ fun CustomTextField(
|
||||
modifier: Modifier = Modifier,
|
||||
textStyle: TextStyle =
|
||||
MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.onSurface),
|
||||
label: @Composable () -> Unit,
|
||||
label: @Composable (() -> Unit)? = null,
|
||||
containerColor: Color,
|
||||
onValueChange: (value: String) -> Unit = {},
|
||||
singleLine: Boolean = true,
|
||||
@@ -47,10 +49,19 @@ fun CustomTextField(
|
||||
readOnly: Boolean = false,
|
||||
enabled: Boolean = true,
|
||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||
interactionSource: MutableInteractionSource = MutableInteractionSource(),
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
) {
|
||||
val space = " "
|
||||
var isFocused by remember { mutableStateOf(false) }
|
||||
var textFieldValueState by remember { mutableStateOf(TextFieldValue(text = value)) }
|
||||
val textFieldValue =
|
||||
remember(value, textFieldValueState) {
|
||||
if (textFieldValueState.text == value) {
|
||||
textFieldValueState
|
||||
} else {
|
||||
textFieldValueState.copy(text = value, selection = TextRange(value.length))
|
||||
}
|
||||
}
|
||||
|
||||
val cursorBrush =
|
||||
if (isFocused) SolidColor(MaterialTheme.colorScheme.primary)
|
||||
else SolidColor(Color.Transparent)
|
||||
@@ -67,9 +78,14 @@ fun CustomTextField(
|
||||
}
|
||||
|
||||
BasicTextField(
|
||||
value = value,
|
||||
value = textFieldValue,
|
||||
textStyle = effectiveTextStyle,
|
||||
onValueChange = { onValueChange(it) },
|
||||
onValueChange = { newTextFieldValue ->
|
||||
textFieldValueState = newTextFieldValue
|
||||
if (value != newTextFieldValue.text) {
|
||||
onValueChange(newTextFieldValue.text)
|
||||
}
|
||||
},
|
||||
keyboardActions = keyboardActions,
|
||||
keyboardOptions = keyboardOptions,
|
||||
readOnly = readOnly,
|
||||
@@ -90,15 +106,9 @@ fun CustomTextField(
|
||||
visualTransformation = visualTransformation,
|
||||
) {
|
||||
OutlinedTextFieldDefaults.DecorationBox(
|
||||
value = space + value,
|
||||
innerTextField = {
|
||||
if (value.isEmpty()) {
|
||||
if (placeholder != null) {
|
||||
placeholder()
|
||||
}
|
||||
}
|
||||
it.invoke()
|
||||
},
|
||||
value = value,
|
||||
innerTextField = it,
|
||||
placeholder = placeholder,
|
||||
contentPadding = OutlinedTextFieldDefaults.contentPadding(top = 14.dp, bottom = 14.dp),
|
||||
leadingIcon = leading,
|
||||
trailingIcon =
|
||||
@@ -141,7 +151,6 @@ fun CustomTextField(
|
||||
label = label,
|
||||
visualTransformation = visualTransformation,
|
||||
interactionSource = interactionSource,
|
||||
placeholder = placeholder,
|
||||
container = {
|
||||
OutlinedTextFieldDefaults.Container(
|
||||
enabled = enabled,
|
||||
|
||||
@@ -42,7 +42,12 @@ sealed class Route : NavKey {
|
||||
|
||||
@Keep @Serializable data object Display : Route()
|
||||
|
||||
@Keep @Serializable data object Tunnels : Route()
|
||||
@Keep
|
||||
@Serializable
|
||||
data object Tunnels : Route(), SecureRoute {
|
||||
override val requiresProtection: Boolean
|
||||
get() = true
|
||||
}
|
||||
|
||||
@Keep @Serializable data class TunnelSettings(val id: Int) : Route()
|
||||
|
||||
|
||||
+2
-2
@@ -250,7 +250,7 @@ fun currentRouteAsNavbarState(
|
||||
val global = route !is ConfigEdit
|
||||
val tunnelName =
|
||||
if (!global) globalState.tunnelNames[route.id]
|
||||
else context.getString(R.string.configuration_globals)
|
||||
else context.getString(R.string.tunnel_configuration)
|
||||
NavbarState(
|
||||
topLeading = { TvBackButton { navController.pop() } },
|
||||
showBottomItems = true,
|
||||
@@ -297,7 +297,7 @@ fun currentRouteAsNavbarState(
|
||||
is SplitTunnelGlobal -> {
|
||||
val tunnelName =
|
||||
if (route is SplitTunnel) globalState.tunnelNames[route.id]
|
||||
else context.getString(R.string.global_split_tunneling)
|
||||
else context.getString(R.string.splt_tunneling)
|
||||
NavbarState(
|
||||
topLeading = { TvBackButton { navController.pop() } },
|
||||
topTitle = tunnelName ?: "",
|
||||
|
||||
+5
-2
@@ -302,7 +302,9 @@ fun AutoTunnelScreen(
|
||||
SurfaceRow(
|
||||
leading = { Icon(Icons.Outlined.PublicOff, contentDescription = null) },
|
||||
title = stringResource(R.string.stop_on_no_internet),
|
||||
description = { DescriptionText(stringResource(R.string.stop_on_internet_loss)) },
|
||||
description = {
|
||||
DescriptionText(stringResource(R.string.stop_on_no_internet_desc))
|
||||
},
|
||||
trailing = {
|
||||
ThemedSwitch(
|
||||
checked = uiState.autoTunnelSettings.isStopOnNoInternetEnabled,
|
||||
@@ -318,7 +320,7 @@ fun AutoTunnelScreen(
|
||||
}
|
||||
Column {
|
||||
GroupLabel(
|
||||
stringResource(R.string.other),
|
||||
stringResource(R.string.automation),
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
SurfaceRow(
|
||||
@@ -330,6 +332,7 @@ fun AutoTunnelScreen(
|
||||
onClick = { viewModel.setStartAtBoot(it) },
|
||||
)
|
||||
},
|
||||
description = { DescriptionText(stringResource(R.string.start_on_boot_desc)) },
|
||||
onClick = { viewModel.setStartAtBoot(!uiState.autoTunnelSettings.startOnBoot) },
|
||||
)
|
||||
}
|
||||
|
||||
+9
-2
@@ -9,6 +9,7 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
|
||||
@@ -45,11 +46,17 @@ fun PinLockScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel())
|
||||
textColor = MaterialTheme.colorScheme.onSurface,
|
||||
onPinCorrect = { onPinCorrect() },
|
||||
onPinIncorrect = {
|
||||
sharedViewModel.showToast(StringValue.StringResource(R.string.incorrect_pin))
|
||||
sharedViewModel.showSnackMessage(
|
||||
StringValue.StringResource(R.string.incorrect_pin),
|
||||
ToastType.Warning,
|
||||
)
|
||||
},
|
||||
onPinCreated = {
|
||||
pinCreated = true
|
||||
sharedViewModel.showToast(StringValue.StringResource(R.string.pin_created))
|
||||
sharedViewModel.showSnackMessage(
|
||||
StringValue.StringResource(R.string.pin_created),
|
||||
ToastType.Success,
|
||||
)
|
||||
sharedViewModel.setPinLockEnabled(true)
|
||||
onPinCorrect()
|
||||
},
|
||||
|
||||
+51
-10
@@ -38,6 +38,7 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.wireguardautotunnel.MainActivity
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||
@@ -51,12 +52,12 @@ import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.text.DescriptionText
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.BackupBottomSheet
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.BackupEncryptionDialog
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.proxy.compoents.AppModeBottomSheet
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asString
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asTitleString
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.capitalize
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SettingsViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
@@ -88,6 +89,9 @@ fun SettingsScreen(
|
||||
}
|
||||
|
||||
var showBackupSheet by rememberSaveable { mutableStateOf(false) }
|
||||
var showEncryptionDialog by rememberSaveable { mutableStateOf(false) }
|
||||
var isRestoreAction by remember { mutableStateOf(false) }
|
||||
|
||||
var showAppModeSheet by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val appMode = uiState.settings.tunnelMode
|
||||
@@ -99,19 +103,53 @@ fun SettingsScreen(
|
||||
}
|
||||
|
||||
fun performBackupRestore(action: () -> Unit) {
|
||||
if (uiState.tunnelActive || globalUiState.isAutoTunnelActive)
|
||||
return context.showToast(R.string.all_services_disabled)
|
||||
showBackupSheet = false
|
||||
if (uiState.tunnelActive || globalUiState.isAutoTunnelActive) {
|
||||
sharedViewModel.showSnackMessage(
|
||||
StringValue.StringResource(R.string.all_services_disabled),
|
||||
ToastType.Warning,
|
||||
)
|
||||
return
|
||||
}
|
||||
action()
|
||||
}
|
||||
|
||||
if (showBackupSheet)
|
||||
if (showBackupSheet) {
|
||||
BackupBottomSheet(
|
||||
{ performBackupRestore { (context as? MainActivity)?.performBackup() } },
|
||||
{ performBackupRestore { (context as? MainActivity)?.performRestore() } },
|
||||
) {
|
||||
showBackupSheet = false
|
||||
}
|
||||
onBackup = {
|
||||
showBackupSheet = false
|
||||
isRestoreAction = false
|
||||
showEncryptionDialog = true
|
||||
},
|
||||
onRestore = {
|
||||
showBackupSheet = false
|
||||
isRestoreAction = true
|
||||
showEncryptionDialog = true
|
||||
},
|
||||
onDismiss = { showBackupSheet = false },
|
||||
)
|
||||
}
|
||||
|
||||
if (showEncryptionDialog) {
|
||||
BackupEncryptionDialog(
|
||||
isRestore = isRestoreAction,
|
||||
onConfirm = { encrypt, password ->
|
||||
showEncryptionDialog = false
|
||||
|
||||
if (isRestoreAction) {
|
||||
performBackupRestore {
|
||||
(context as? MainActivity)?.performRestore(encrypt, password)
|
||||
}
|
||||
} else {
|
||||
performBackupRestore {
|
||||
(context as? MainActivity)?.performBackup(encrypt, password)
|
||||
}
|
||||
}
|
||||
},
|
||||
onDismiss = { showEncryptionDialog = false },
|
||||
)
|
||||
}
|
||||
|
||||
if (showAppModeSheet)
|
||||
AppModeBottomSheet(sharedViewModel::setAppMode, uiState.settings.tunnelMode) {
|
||||
showAppModeSheet = false
|
||||
@@ -168,7 +206,8 @@ fun SettingsScreen(
|
||||
StringValue.StringResource(
|
||||
R.string.mode_disabled_template,
|
||||
appMode.asString(context),
|
||||
)
|
||||
),
|
||||
ToastType.Info,
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -176,6 +215,7 @@ fun SettingsScreen(
|
||||
leading = { Icon(Icons.Outlined.Public, contentDescription = null) },
|
||||
title = stringResource(R.string.tunnel_globals),
|
||||
onClick = { navController.push(Route.TunnelGlobals) },
|
||||
description = { DescriptionText(stringResource(R.string.tunnel_globals_desc)) },
|
||||
)
|
||||
SurfaceRow(
|
||||
leading = { Icon(Icons.Outlined.Terminal, contentDescription = null) },
|
||||
@@ -232,6 +272,7 @@ fun SettingsScreen(
|
||||
modifier = modifier,
|
||||
)
|
||||
},
|
||||
description = { DescriptionText(stringResource(R.string.local_logging_desc)) },
|
||||
onClick = { navController.push(Route.Logs) },
|
||||
)
|
||||
SurfaceRow(
|
||||
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Visibility
|
||||
import androidx.compose.material.icons.outlined.VisibilityOff
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.ThemedSwitch
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.textbox.CustomTextField
|
||||
|
||||
@Composable
|
||||
fun BackupEncryptionDialog(
|
||||
isRestore: Boolean,
|
||||
onConfirm: (encrypt: Boolean, password: String?) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
var encrypt by remember { mutableStateOf(false) }
|
||||
var password by remember { mutableStateOf("") }
|
||||
var confirmPassword by remember { mutableStateOf("") }
|
||||
var showPasswordError by remember { mutableStateOf(false) }
|
||||
var passwordVisible by remember { mutableStateOf(false) }
|
||||
var confirmPasswordVisible by remember { mutableStateOf(false) }
|
||||
|
||||
InfoDialog(
|
||||
title =
|
||||
if (isRestore) {
|
||||
stringResource(R.string.restore)
|
||||
} else {
|
||||
stringResource(R.string.backup)
|
||||
},
|
||||
confirmText =
|
||||
if (isRestore) {
|
||||
stringResource(R.string.restore)
|
||||
} else {
|
||||
stringResource(R.string.backup)
|
||||
},
|
||||
onAttest = {
|
||||
if (!isRestore && encrypt && password != confirmPassword) {
|
||||
showPasswordError = true
|
||||
return@InfoDialog
|
||||
}
|
||||
if (encrypt && password.isBlank()) {
|
||||
return@InfoDialog
|
||||
}
|
||||
|
||||
val finalPassword = if (encrypt) password else null
|
||||
onConfirm(encrypt, finalPassword)
|
||||
},
|
||||
onDismiss = onDismiss,
|
||||
body = {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(stringResource(R.string.encrypted))
|
||||
ThemedSwitch(checked = encrypt, onClick = { encrypt = it })
|
||||
}
|
||||
|
||||
if (encrypt) {
|
||||
CustomTextField(
|
||||
value = password,
|
||||
onValueChange = {
|
||||
password = it
|
||||
showPasswordError = false
|
||||
},
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
label = { Text(stringResource(R.string.password)) },
|
||||
visualTransformation =
|
||||
if (passwordVisible) VisualTransformation.None
|
||||
else PasswordVisualTransformation(),
|
||||
trailing = {
|
||||
IconButton(onClick = { passwordVisible = !passwordVisible }) {
|
||||
Icon(
|
||||
imageVector =
|
||||
if (passwordVisible) Icons.Outlined.VisibilityOff
|
||||
else Icons.Outlined.Visibility,
|
||||
contentDescription =
|
||||
if (passwordVisible) stringResource(R.string.hide_password)
|
||||
else stringResource(R.string.show_password),
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
|
||||
if (!isRestore) {
|
||||
CustomTextField(
|
||||
value = confirmPassword,
|
||||
onValueChange = {
|
||||
confirmPassword = it
|
||||
showPasswordError = false
|
||||
},
|
||||
label = { Text(stringResource(R.string.confirm_password)) },
|
||||
visualTransformation =
|
||||
if (confirmPasswordVisible) VisualTransformation.None
|
||||
else PasswordVisualTransformation(),
|
||||
trailing = {
|
||||
IconButton(
|
||||
onClick = { confirmPasswordVisible = !confirmPasswordVisible }
|
||||
) {
|
||||
Icon(
|
||||
imageVector =
|
||||
if (confirmPasswordVisible) Icons.Outlined.VisibilityOff
|
||||
else Icons.Outlined.Visibility,
|
||||
contentDescription =
|
||||
if (confirmPasswordVisible)
|
||||
stringResource(R.string.hide_password)
|
||||
else stringResource(R.string.show_password),
|
||||
)
|
||||
}
|
||||
},
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
isError = showPasswordError,
|
||||
)
|
||||
}
|
||||
|
||||
if (showPasswordError) {
|
||||
Text(
|
||||
text = stringResource(R.string.passwords_do_not_match),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
+2
-2
@@ -56,7 +56,7 @@ fun TunnelGlobalsScreen(
|
||||
)
|
||||
},
|
||||
enabled = sharedUiState.tunnelMode != TunnelMode.PROXY,
|
||||
title = stringResource(R.string.global_split_tunneling),
|
||||
title = stringResource(R.string.splt_tunneling),
|
||||
trailing = { modifier ->
|
||||
SwitchWithDivider(
|
||||
checked = uiState.settings.isGlobalSplitTunnelEnabled,
|
||||
@@ -82,7 +82,7 @@ fun TunnelGlobalsScreen(
|
||||
)
|
||||
SurfaceRow(
|
||||
leading = { Icon(Icons.Outlined.Description, contentDescription = null) },
|
||||
title = stringResource(R.string.configuration_globals),
|
||||
title = stringResource(R.string.tunnel_configuration),
|
||||
onClick = {
|
||||
uiState.globalTunnelConfig?.let {
|
||||
navController.push(Route.ConfigGlobal(id = it.id))
|
||||
|
||||
+6
-4
@@ -11,7 +11,7 @@ import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.Launch
|
||||
import androidx.compose.material.icons.filled.AppShortcut
|
||||
import androidx.compose.material.icons.filled.SmartToy
|
||||
import androidx.compose.material.icons.filled.SettingsRemote
|
||||
import androidx.compose.material.icons.outlined.AdminPanelSettings
|
||||
import androidx.compose.material.icons.outlined.ContentCopy
|
||||
import androidx.compose.material.icons.outlined.Key
|
||||
@@ -133,20 +133,22 @@ fun AndroidIntegrationsScreen(viewModel: SettingsViewModel = koinViewModel()) {
|
||||
onClick = { viewModel.setShortcutsEnabled(it) },
|
||||
)
|
||||
},
|
||||
title = stringResource(R.string.enabled_app_shortcuts),
|
||||
title = stringResource(R.string.app_shortcuts),
|
||||
description = { DescriptionText(stringResource(R.string.app_shortcuts_desc)) },
|
||||
onClick = {
|
||||
viewModel.setShortcutsEnabled(!settingsState.settings.isShortcutsEnabled)
|
||||
},
|
||||
)
|
||||
SurfaceRow(
|
||||
leading = { Icon(Icons.Filled.SmartToy, contentDescription = null) },
|
||||
leading = { Icon(Icons.Filled.SettingsRemote, contentDescription = null) },
|
||||
trailing = {
|
||||
ThemedSwitch(
|
||||
checked = settingsState.isRemoteEnabled,
|
||||
onClick = { viewModel.setRemoteEnabled(it) },
|
||||
)
|
||||
},
|
||||
title = stringResource(R.string.enable_remote_app_control),
|
||||
title = stringResource(R.string.remote_control),
|
||||
description = { DescriptionText(stringResource(R.string.remote_control_desc)) },
|
||||
onClick = { viewModel.setRemoteEnabled(!settingsState.isRemoteEnabled) },
|
||||
)
|
||||
AnimatedVisibility(settingsState.isRemoteEnabled) {
|
||||
|
||||
+5
-2
@@ -12,6 +12,7 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.logs.components.LogList
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.logs.components.LogsBottomSheet
|
||||
@@ -86,13 +87,15 @@ fun LogsScreen(
|
||||
},
|
||||
onCanceled = {
|
||||
sharedViewModel.showSnackMessage(
|
||||
StringValue.StringResource(R.string.export_canceled)
|
||||
StringValue.StringResource(R.string.export_canceled),
|
||||
ToastType.Warning,
|
||||
)
|
||||
showLogsSheet = false
|
||||
},
|
||||
onUnsupported = {
|
||||
sharedViewModel.showSnackMessage(
|
||||
StringValue.StringResource(R.string.export_unsupported)
|
||||
StringValue.StringResource(R.string.export_unsupported),
|
||||
ToastType.Warning,
|
||||
)
|
||||
showLogsSheet = false
|
||||
},
|
||||
|
||||
+9
-1
@@ -1,10 +1,12 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.logs.components
|
||||
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material3.scrollbar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -18,7 +20,13 @@ fun LogList(
|
||||
) {
|
||||
LazyColumn(
|
||||
state = lazyColumnListState,
|
||||
modifier = modifier.padding(horizontal = 12.dp),
|
||||
modifier =
|
||||
modifier
|
||||
.padding(horizontal = 12.dp)
|
||||
.scrollbar(
|
||||
state = lazyColumnListState.scrollIndicatorState,
|
||||
orientation = Orientation.Vertical,
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||
) {
|
||||
itemsIndexed(items = logs, key = { index, _ -> index }) { _, log -> LogItem(log = log) }
|
||||
|
||||
+8
@@ -2,6 +2,8 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings.proxy.compoents
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.sheet.CustomBottomSheet
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.sheet.SheetOption
|
||||
@@ -27,6 +29,12 @@ fun AppModeBottomSheet(
|
||||
onDismiss()
|
||||
onAppModeChange(it)
|
||||
},
|
||||
description =
|
||||
when (it) {
|
||||
TunnelMode.VPN -> stringResource(R.string.vpn_desc)
|
||||
TunnelMode.PROXY -> stringResource(R.string.local_proxy_desc)
|
||||
TunnelMode.LOCK_DOWN -> stringResource(R.string.lockdown_desc)
|
||||
},
|
||||
selected = tunnelMode == it,
|
||||
)
|
||||
}
|
||||
|
||||
+6
-1
@@ -19,6 +19,7 @@ import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.ThemedSwitch
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.text.DescriptionText
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
|
||||
import org.koin.compose.viewmodel.koinActivityViewModel
|
||||
@@ -45,13 +46,16 @@ fun SecurityScreen(viewModel: SharedAppViewModel = koinActivityViewModel()) {
|
||||
onClick = { viewModel.setScreenRecordingSecurity(it) },
|
||||
)
|
||||
},
|
||||
description = {
|
||||
DescriptionText(stringResource(R.string.screen_recording_protection_desc))
|
||||
},
|
||||
onClick = {
|
||||
viewModel.setScreenRecordingSecurity(!uiState.isScreenRecordingProtectionEnabled)
|
||||
},
|
||||
)
|
||||
SurfaceRow(
|
||||
leading = { Icon(Icons.Outlined.Pin, contentDescription = null) },
|
||||
title = stringResource(R.string.enable_app_lock),
|
||||
title = stringResource(R.string.app_lock),
|
||||
trailing = {
|
||||
ThemedSwitch(
|
||||
checked = uiState.pinLockEnabled,
|
||||
@@ -64,6 +68,7 @@ fun SecurityScreen(viewModel: SharedAppViewModel = koinActivityViewModel()) {
|
||||
},
|
||||
)
|
||||
},
|
||||
description = { DescriptionText(stringResource(R.string.app_lock_desc)) },
|
||||
onClick = {
|
||||
if (!uiState.pinLockEnabled) {
|
||||
navController.push(Route.Lock)
|
||||
|
||||
+67
-15
@@ -26,6 +26,7 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -37,8 +38,10 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
|
||||
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
|
||||
@@ -49,12 +52,13 @@ import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.components.PermissionDialog
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.components.UpdateDialog
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.launchPlayStoreListing
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.launchPlayStoreReview
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.launchSupportEmail
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SupportViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import org.orbitmvi.orbit.compose.collectAsState
|
||||
|
||||
@@ -63,11 +67,24 @@ fun SupportScreen(viewModel: SupportViewModel = koinViewModel()) {
|
||||
val context = LocalContext.current
|
||||
val navController = LocalNavController.current
|
||||
val isTv = LocalIsAndroidTV.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val supportState by viewModel.collectAsState()
|
||||
|
||||
val clipboardManager = rememberClipboardHelper()
|
||||
|
||||
val issuesUrl = stringResource(R.string.github_url)
|
||||
val izzyUrl = stringResource(R.string.fdroid_url)
|
||||
val telegramUrl = stringResource(R.string.telegram_url)
|
||||
val matrixUrl = stringResource(R.string.matrix_url)
|
||||
val docsUrl = stringResource(R.string.docs_url)
|
||||
val websiteUrl = stringResource(R.string.website_url)
|
||||
val translationUrl = stringResource(R.string.translation_url)
|
||||
val privacyPolicyUrl = stringResource(R.string.privacy_policy_url)
|
||||
val playStoreUrl = "https://play.google.com/store/apps/details?id=${context.packageName}"
|
||||
val playReviewsUrl =
|
||||
"https://play.google.com/store/apps/details?id=${context.packageName}&showAllReviews=true"
|
||||
|
||||
val version = remember {
|
||||
"v${BuildConfig.VERSION_NAME +
|
||||
if(BuildConfig.DEBUG) "-debug" else "" }"
|
||||
@@ -95,6 +112,19 @@ fun SupportScreen(viewModel: SupportViewModel = koinViewModel()) {
|
||||
PermissionDialog(context = context, onDismiss = { showPermissionDialog = false })
|
||||
}
|
||||
|
||||
fun openWebUrl(url: String) {
|
||||
context.openWebUrl(url).onFailure {
|
||||
scope.launch {
|
||||
viewModel.postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.no_browser_detected),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
@@ -115,19 +145,19 @@ fun SupportScreen(viewModel: SupportViewModel = koinViewModel()) {
|
||||
)
|
||||
SurfaceRow(
|
||||
stringResource(R.string.docs_description),
|
||||
onClick = { context.openWebUrl(context.getString(R.string.docs_url)) },
|
||||
onClick = { openWebUrl(docsUrl) },
|
||||
leading = { Icon(Icons.Outlined.Book, contentDescription = null) },
|
||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||
)
|
||||
SurfaceRow(
|
||||
stringResource(R.string.website),
|
||||
onClick = { context.openWebUrl(context.getString(R.string.website_url)) },
|
||||
onClick = { openWebUrl(websiteUrl) },
|
||||
leading = { Icon(Icons.Outlined.Web, contentDescription = null) },
|
||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||
)
|
||||
SurfaceRow(
|
||||
stringResource(R.string.translation),
|
||||
onClick = { context.openWebUrl(context.getString(R.string.translation_url)) },
|
||||
onClick = { openWebUrl(translationUrl) },
|
||||
description = { DescriptionText(stringResource(R.string.help_translate)) },
|
||||
leading = { Icon(Icons.Outlined.Translate, contentDescription = null) },
|
||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||
@@ -141,14 +171,16 @@ fun SupportScreen(viewModel: SupportViewModel = koinViewModel()) {
|
||||
leading = { Icon(Icons.Outlined.Policy, contentDescription = null) },
|
||||
title = stringResource(R.string.privacy_policy),
|
||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||
onClick = { context.openWebUrl(context.getString(R.string.privacy_policy_url)) },
|
||||
onClick = { openWebUrl(privacyPolicyUrl) },
|
||||
)
|
||||
if (BuildConfig.FLAVOR == Constants.GOOGLE_PLAY_FLAVOR) {
|
||||
SurfaceRow(
|
||||
leading = { Icon(Icons.Outlined.Reviews, contentDescription = null) },
|
||||
title = stringResource(R.string.review),
|
||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||
onClick = { context.launchPlayStoreReview() },
|
||||
onClick = {
|
||||
context.launchPlayStoreReview().onFailure { openWebUrl(playReviewsUrl) }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -167,7 +199,7 @@ fun SupportScreen(viewModel: SupportViewModel = koinViewModel()) {
|
||||
},
|
||||
title = stringResource(R.string.join_matrix),
|
||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||
onClick = { context.openWebUrl(context.getString(R.string.matrix_url)) },
|
||||
onClick = { openWebUrl(matrixUrl) },
|
||||
)
|
||||
SurfaceRow(
|
||||
leading = {
|
||||
@@ -179,7 +211,7 @@ fun SupportScreen(viewModel: SupportViewModel = koinViewModel()) {
|
||||
},
|
||||
title = stringResource(R.string.join_telegram),
|
||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||
onClick = { context.openWebUrl(context.getString(R.string.telegram_url)) },
|
||||
onClick = { openWebUrl(telegramUrl) },
|
||||
)
|
||||
SurfaceRow(
|
||||
leading = {
|
||||
@@ -191,13 +223,24 @@ fun SupportScreen(viewModel: SupportViewModel = koinViewModel()) {
|
||||
},
|
||||
title = stringResource(R.string.open_issue),
|
||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||
onClick = { context.openWebUrl(context.getString(R.string.github_url)) },
|
||||
onClick = { openWebUrl(issuesUrl) },
|
||||
)
|
||||
SurfaceRow(
|
||||
leading = { Icon(Icons.Outlined.Mail, contentDescription = null) },
|
||||
title = stringResource(R.string.email_description),
|
||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||
onClick = { context.launchSupportEmail() },
|
||||
onClick = {
|
||||
context.launchSupportEmail().onFailure {
|
||||
scope.launch {
|
||||
viewModel.postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.no_email_detected),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
Column {
|
||||
@@ -222,12 +265,21 @@ fun SupportScreen(viewModel: SupportViewModel = koinViewModel()) {
|
||||
leading = { Icon(Icons.Outlined.InstallMobile, contentDescription = null) },
|
||||
title = stringResource(R.string.check_for_update),
|
||||
onClick = {
|
||||
if (BuildConfig.DEBUG)
|
||||
return@SurfaceRow context.showToast(R.string.update_check_unsupported)
|
||||
if (BuildConfig.DEBUG) {
|
||||
scope.launch {
|
||||
viewModel.postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.update_check_unsupported),
|
||||
ToastType.Warning,
|
||||
)
|
||||
)
|
||||
}
|
||||
return@SurfaceRow
|
||||
}
|
||||
when (BuildConfig.FLAVOR) {
|
||||
Constants.GOOGLE_PLAY_FLAVOR -> context.launchPlayStoreListing()
|
||||
Constants.FDROID_FLAVOR ->
|
||||
context.openWebUrl(context.getString(R.string.fdroid_url))
|
||||
Constants.GOOGLE_PLAY_FLAVOR ->
|
||||
context.launchPlayStoreListing().onFailure { openWebUrl(playStoreUrl) }
|
||||
Constants.FDROID_FLAVOR -> openWebUrl(izzyUrl)
|
||||
else -> viewModel.checkForStandaloneUpdate()
|
||||
}
|
||||
},
|
||||
|
||||
+10
-1
@@ -1,10 +1,12 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.support.donate.crypto
|
||||
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.scrollbar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -13,10 +15,17 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.support.donate.crypto.comp
|
||||
|
||||
@Composable
|
||||
fun AddressesScreen() {
|
||||
val scrollState = rememberScrollState()
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()),
|
||||
modifier =
|
||||
Modifier.fillMaxSize()
|
||||
.verticalScroll(scrollState)
|
||||
.scrollbar(
|
||||
state = scrollState.scrollIndicatorState,
|
||||
orientation = Orientation.Vertical,
|
||||
),
|
||||
) {
|
||||
val clipboard = rememberClipboardHelper()
|
||||
Address.allAddresses.forEach { AddressItem(it) { address -> clipboard.copy(address) } }
|
||||
|
||||
+13
-1
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.support.license.component
|
||||
|
||||
import LicenseFileEntry
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -9,11 +10,13 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.Launch
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.scrollbar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -24,8 +27,17 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
||||
@Composable
|
||||
fun LicenseList(licenses: List<LicenseFileEntry>) {
|
||||
val context = LocalContext.current
|
||||
val lazyListState = rememberLazyListState()
|
||||
|
||||
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||
LazyColumn(
|
||||
modifier =
|
||||
Modifier.fillMaxSize()
|
||||
.scrollbar(
|
||||
state = lazyListState.scrollIndicatorState,
|
||||
orientation = Orientation.Vertical,
|
||||
),
|
||||
state = lazyListState,
|
||||
) {
|
||||
items(licenses) { entry ->
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
|
||||
+17
-7
@@ -13,6 +13,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
|
||||
@@ -50,11 +51,15 @@ fun TunnelsScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel())
|
||||
rememberFileExportLauncherForResult(
|
||||
onSuccess = { uri -> sharedViewModel.exportSelectedTunnels(uri) },
|
||||
onCanceled = {
|
||||
sharedViewModel.showToast(StringValue.StringResource(R.string.export_canceled))
|
||||
sharedViewModel.showSnackMessage(
|
||||
StringValue.StringResource(R.string.export_canceled),
|
||||
ToastType.Warning,
|
||||
)
|
||||
},
|
||||
onUnsupported = {
|
||||
sharedViewModel.showSnackMessage(
|
||||
StringValue.StringResource(R.string.export_unsupported)
|
||||
StringValue.StringResource(R.string.export_unsupported),
|
||||
ToastType.Warning,
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -73,7 +78,8 @@ fun TunnelsScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel())
|
||||
selectedTunnelsExportLauncher.launch(fileName)
|
||||
} else {
|
||||
sharedViewModel.showSnackMessage(
|
||||
StringValue.StringResource(R.string.error_no_file_explorer)
|
||||
StringValue.StringResource(R.string.error_no_file_explorer),
|
||||
ToastType.Error,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -87,7 +93,8 @@ fun TunnelsScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel())
|
||||
rememberFileImportLauncherForResult(
|
||||
onNoFileExplorer = {
|
||||
sharedViewModel.showSnackMessage(
|
||||
StringValue.StringResource(R.string.error_no_file_explorer)
|
||||
StringValue.StringResource(R.string.error_no_file_explorer),
|
||||
ToastType.Error,
|
||||
)
|
||||
},
|
||||
onData = { data -> sharedViewModel.importFromUri(data) },
|
||||
@@ -101,13 +108,15 @@ fun TunnelsScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel())
|
||||
}
|
||||
QRResult.QRMissingPermission -> {
|
||||
sharedViewModel.showSnackMessage(
|
||||
StringValue.StringResource(R.string.camera_permission_required)
|
||||
StringValue.StringResource(R.string.camera_permission_required),
|
||||
ToastType.Warning,
|
||||
)
|
||||
}
|
||||
is QRResult.QRSuccess -> {
|
||||
result.content.rawValue?.let { sharedViewModel.importFromQr(it) }
|
||||
?: sharedViewModel.showSnackMessage(
|
||||
StringValue.StringResource(R.string.config_error)
|
||||
StringValue.StringResource(R.string.config_error),
|
||||
ToastType.Error,
|
||||
)
|
||||
}
|
||||
QRResult.QRUserCanceled -> Unit
|
||||
@@ -119,7 +128,8 @@ fun TunnelsScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel())
|
||||
->
|
||||
if (!isGranted) {
|
||||
sharedViewModel.showSnackMessage(
|
||||
StringValue.StringResource(R.string.camera_permission_required)
|
||||
StringValue.StringResource(R.string.camera_permission_required),
|
||||
ToastType.Warning,
|
||||
)
|
||||
return@rememberLauncherForActivityResult
|
||||
}
|
||||
|
||||
+3
@@ -42,5 +42,8 @@ fun PeerStatisticsSection(peer: ActivePeer) {
|
||||
style = style,
|
||||
color = color,
|
||||
)
|
||||
peer.endpoint?.let {
|
||||
StatText(stringResource(R.string.endpoint_template, it), style = style, color = color)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -42,6 +42,6 @@ private fun TransferMetric(icon: ImageVector, text: String, style: TextStyle, co
|
||||
modifier = Modifier.size(12.dp),
|
||||
)
|
||||
|
||||
Text(text = text.lowercase(), style = style, color = color)
|
||||
Text(text = text, style = style, color = color)
|
||||
}
|
||||
}
|
||||
|
||||
+10
-4
@@ -1,6 +1,7 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.components
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.gestures.ScrollableDefaults
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.size
|
||||
@@ -12,8 +13,10 @@ import androidx.compose.foundation.rememberOverscrollEffect
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Circle
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.scrollbar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -66,7 +69,11 @@ fun TunnelList(
|
||||
viewModel.clearSelectedTunnels()
|
||||
}
|
||||
}
|
||||
.overscroll(rememberOverscrollEffect()),
|
||||
.overscroll(rememberOverscrollEffect())
|
||||
.scrollbar(
|
||||
state = lazyListState.scrollIndicatorState,
|
||||
orientation = Orientation.Vertical,
|
||||
),
|
||||
state = lazyListState,
|
||||
userScrollEnabled = true,
|
||||
reverseLayout = false,
|
||||
@@ -87,8 +94,7 @@ fun TunnelList(
|
||||
uiState.backendStatus.activeTunnels[tunnel.id] ?: ActiveTunnel()
|
||||
}
|
||||
|
||||
val displayState =
|
||||
uiState.displayStates[tunnel.id] ?: DisplayTunnelState.from(activeTunnel)
|
||||
val displayState = remember(activeTunnel) { DisplayTunnelState.from(activeTunnel) }
|
||||
|
||||
val isRunning = uiState.backendStatus.activeTunnels.containsKey(tunnel.id)
|
||||
|
||||
@@ -107,7 +113,7 @@ fun TunnelList(
|
||||
Icon(
|
||||
Icons.Rounded.Circle,
|
||||
contentDescription = stringResource(R.string.tunnel_monitoring),
|
||||
tint = displayState.asColor(),
|
||||
tint = remember(displayState) { displayState.asColor() },
|
||||
modifier = Modifier.size(14.dp),
|
||||
)
|
||||
},
|
||||
|
||||
+27
-29
@@ -141,6 +141,10 @@ fun TunnelSettingsScreen(
|
||||
onClick = { navController.push(Route.IPv6(tunnel.id)) },
|
||||
)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val meteredDisabled = sharedUiState.tunnelMode == TunnelMode.PROXY
|
||||
val meteredTunnelDesc =
|
||||
if (meteredDisabled) stringResource(R.string.unavailable_in_mode)
|
||||
else stringResource(R.string.metered_tunnel_desc)
|
||||
SurfaceRow(
|
||||
leading = {
|
||||
Icon(
|
||||
@@ -153,15 +157,9 @@ fun TunnelSettingsScreen(
|
||||
},
|
||||
title = stringResource(R.string.metered_tunnel),
|
||||
enabled = sharedUiState.tunnelMode != TunnelMode.PROXY,
|
||||
description =
|
||||
if (sharedUiState.tunnelMode == TunnelMode.PROXY) {
|
||||
{
|
||||
DescriptionText(
|
||||
stringResource(R.string.unavailable_in_mode),
|
||||
disabled = true,
|
||||
)
|
||||
}
|
||||
} else null,
|
||||
description = {
|
||||
DescriptionText(meteredTunnelDesc, disabled = meteredDisabled)
|
||||
},
|
||||
trailing = {
|
||||
ThemedSwitch(
|
||||
checked = tunnel.isMetered,
|
||||
@@ -172,26 +170,26 @@ fun TunnelSettingsScreen(
|
||||
onClick = { viewModel.onMetered(!tunnel.isMetered) },
|
||||
)
|
||||
}
|
||||
Column {
|
||||
GroupLabel(
|
||||
stringResource(R.string.automation),
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
SurfaceRow(
|
||||
leading = { Icon(Icons.Outlined.Dns, contentDescription = null) },
|
||||
title = stringResource(R.string.ddns_auto_update),
|
||||
description = {
|
||||
DescriptionText(stringResource(R.string.ddns_auto_update_description))
|
||||
},
|
||||
trailing = {
|
||||
ThemedSwitch(
|
||||
checked = tunnel.dynamicDnsEnabled,
|
||||
onClick = { viewModel.onDynamicDns(it) },
|
||||
)
|
||||
},
|
||||
onClick = { viewModel.onDynamicDns(!tunnel.dynamicDnsEnabled) },
|
||||
)
|
||||
}
|
||||
}
|
||||
Column {
|
||||
GroupLabel(
|
||||
stringResource(R.string.automation),
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
SurfaceRow(
|
||||
leading = { Icon(Icons.Outlined.Dns, contentDescription = null) },
|
||||
title = stringResource(R.string.ddns_auto_update),
|
||||
description = {
|
||||
DescriptionText(stringResource(R.string.ddns_auto_update_description))
|
||||
},
|
||||
trailing = {
|
||||
ThemedSwitch(
|
||||
checked = tunnel.dynamicDnsEnabled,
|
||||
onClick = { viewModel.onDynamicDns(it) },
|
||||
)
|
||||
},
|
||||
onClick = { viewModel.onDynamicDns(!tunnel.dynamicDnsEnabled) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+20
-3
@@ -1,5 +1,6 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.settings.config
|
||||
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
@@ -9,6 +10,7 @@ import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.scrollbar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -26,14 +28,16 @@ import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberClipboardHelper
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.settings.config.components.QrCodeDialog
|
||||
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.ConfigHeaderColor
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.ConfigKeyColor
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isTextTooLargeForQr
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelViewModel
|
||||
import org.koin.compose.viewmodel.koinActivityViewModel
|
||||
@@ -57,6 +61,8 @@ fun ConfigScreen(
|
||||
|
||||
var showQrModal by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
val rawConfig by
|
||||
remember(liveConfig, uiState.activeConfig, uiState.tunnel?.quickConfig) {
|
||||
derivedStateOf {
|
||||
@@ -72,7 +78,12 @@ fun ConfigScreen(
|
||||
when (sideEffect) {
|
||||
is LocalSideEffect.Modal.QR -> {
|
||||
if (tunnel.quickConfig.isTextTooLargeForQr()) {
|
||||
context.showToast(R.string.text_too_large_for_qr)
|
||||
sharedViewModel.postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.text_too_large_for_qr),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
} else {
|
||||
showQrModal = true
|
||||
}
|
||||
@@ -90,7 +101,13 @@ fun ConfigScreen(
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.Top),
|
||||
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()),
|
||||
modifier =
|
||||
Modifier.fillMaxSize()
|
||||
.verticalScroll(scrollState)
|
||||
.scrollbar(
|
||||
state = scrollState.scrollIndicatorState,
|
||||
orientation = Orientation.Vertical,
|
||||
),
|
||||
) {
|
||||
val displayText by
|
||||
remember(rawConfig, showKeys) { derivedStateOf { maskSensitive(rawConfig, showKeys) } }
|
||||
|
||||
+14
-4
@@ -1,5 +1,6 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.settings.config.edit
|
||||
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -12,6 +13,7 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.HdrAuto
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.scrollbar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -49,11 +51,12 @@ fun ConfigEditScreen(
|
||||
val uiState by viewModel.collectAsState()
|
||||
|
||||
if (uiState.isLoading) return
|
||||
|
||||
val locale = Locale.current.platformLocale
|
||||
|
||||
var showSelectionDialog by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
sharedViewModel.collectSideEffect { sideEffect ->
|
||||
when (sideEffect) {
|
||||
is LocalSideEffect.SaveChanges -> {
|
||||
@@ -104,7 +107,14 @@ fun ConfigEditScreen(
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Top),
|
||||
modifier = Modifier.fillMaxSize().imePadding().verticalScroll(rememberScrollState()),
|
||||
modifier =
|
||||
Modifier.fillMaxSize()
|
||||
.imePadding()
|
||||
.verticalScroll(scrollState)
|
||||
.scrollbar(
|
||||
state = scrollState.scrollIndicatorState,
|
||||
orientation = Orientation.Vertical,
|
||||
),
|
||||
) {
|
||||
if (uiState.isGlobalConfig) {
|
||||
Column {
|
||||
@@ -112,7 +122,7 @@ fun ConfigEditScreen(
|
||||
leading = {
|
||||
Icon(ImageVector.vectorResource(R.drawable.host), contentDescription = null)
|
||||
},
|
||||
title = stringResource(R.string.global_dns_servers),
|
||||
title = stringResource(R.string.dns_servers),
|
||||
trailing = { modifier ->
|
||||
ThemedSwitch(
|
||||
checked = uiState.globalSettings.dnsEnabled,
|
||||
@@ -126,7 +136,7 @@ fun ConfigEditScreen(
|
||||
)
|
||||
SurfaceRow(
|
||||
leading = { Icon(Icons.Outlined.HdrAuto, contentDescription = null) },
|
||||
title = stringResource(R.string.global_amnezia_configuration),
|
||||
title = stringResource(R.string.amnezia_configuration),
|
||||
trailing = { modifier ->
|
||||
ThemedSwitch(
|
||||
checked = uiState.globalSettings.amneziaEnabled,
|
||||
|
||||
+7
-1
@@ -1,5 +1,6 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.sort
|
||||
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.gestures.ScrollableDefaults
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -17,6 +18,7 @@ import androidx.compose.material.icons.filled.ArrowUpward
|
||||
import androidx.compose.material.icons.filled.DragHandle
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.scrollbar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -107,7 +109,11 @@ fun SortScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel()) {
|
||||
Modifier.pointerInput(Unit) {
|
||||
if (tunnelsUiState.tunnels.isEmpty()) return@pointerInput
|
||||
}
|
||||
.overscroll(rememberOverscrollEffect()),
|
||||
.overscroll(rememberOverscrollEffect())
|
||||
.scrollbar(
|
||||
state = lazyListState.scrollIndicatorState,
|
||||
orientation = Orientation.Vertical,
|
||||
),
|
||||
state = lazyListState,
|
||||
userScrollEnabled = true,
|
||||
reverseLayout = false,
|
||||
|
||||
+17
-50
@@ -26,38 +26,33 @@ sealed class DisplayTunnelState {
|
||||
|
||||
data object Connected : DisplayTunnelState()
|
||||
|
||||
data object Degraded : DisplayTunnelState()
|
||||
data object HandshakeFailure : DisplayTunnelState()
|
||||
|
||||
@StringRes
|
||||
fun labelRes(): Int {
|
||||
return when (this) {
|
||||
fun labelRes(): Int =
|
||||
when (this) {
|
||||
Disconnected -> R.string.tunnel_state_disconnected
|
||||
Connecting -> R.string.tunnel_state_starting
|
||||
ResolvingDns -> R.string.tunnel_state_resolving_dns
|
||||
Connecting,
|
||||
EstablishingConnection -> R.string.tunnel_state_establishing_connection
|
||||
ResolvingDns -> R.string.tunnel_state_resolving_dns
|
||||
Ready -> R.string.ready
|
||||
Connected -> R.string.tunnel_state_connected
|
||||
Degraded -> R.string.tunnel_state_handshake_failure
|
||||
HandshakeFailure -> R.string.tunnel_state_handshake_failure
|
||||
}
|
||||
}
|
||||
|
||||
fun asLocalizedString(context: Context): String {
|
||||
return context.getString(labelRes())
|
||||
}
|
||||
|
||||
fun asColor(): Color {
|
||||
return when (this) {
|
||||
fun asColor(): Color =
|
||||
when (this) {
|
||||
Disconnected -> CoolGray
|
||||
|
||||
Connecting,
|
||||
ResolvingDns,
|
||||
EstablishingConnection,
|
||||
Ready -> Straw
|
||||
|
||||
Connected -> SilverTree
|
||||
|
||||
Degraded -> AlertRed
|
||||
HandshakeFailure -> AlertRed
|
||||
}
|
||||
|
||||
fun asLocalizedString(context: Context): String {
|
||||
return context.getString(labelRes())
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -67,49 +62,21 @@ sealed class DisplayTunnelState {
|
||||
val mode = activeTunnel.mode
|
||||
val isVpnStyle = mode is BackendMode.Vpn || mode is BackendMode.Proxy.KillSwitchPrimary
|
||||
|
||||
// Static peers bootstrap never goes to complete, treat none the same
|
||||
val bootstrapPhaseDone =
|
||||
bootstrap is BootstrapState.Complete || bootstrap is BootstrapState.None
|
||||
|
||||
return when {
|
||||
transport is Tunnel.State.Down -> Disconnected
|
||||
bootstrap is BootstrapState.Failed -> HandshakeFailure
|
||||
|
||||
bootstrap is BootstrapState.Failed -> Degraded
|
||||
|
||||
// DNS resolution still in progress
|
||||
bootstrap is BootstrapState.ResolvingDns ||
|
||||
bootstrap is BootstrapState.UpdatingPeers -> ResolvingDns
|
||||
|
||||
transport is Tunnel.State.Up.Healthy -> Connected
|
||||
|
||||
transport is Tunnel.State.Up.HandshakeFailure -> {
|
||||
val age = System.currentTimeMillis() - activeTunnel.lastStateChangeMs
|
||||
transport is Tunnel.State.Up.HandshakeFailure -> HandshakeFailure
|
||||
|
||||
if (age > 15_000L && bootstrapPhaseDone) {
|
||||
Degraded
|
||||
} else if (isVpnStyle && bootstrapPhaseDone) {
|
||||
EstablishingConnection
|
||||
} else if (bootstrapPhaseDone) {
|
||||
// For regular proxy mode, we go to ready once past bootstrap phase
|
||||
Ready
|
||||
} else {
|
||||
Connecting
|
||||
}
|
||||
}
|
||||
transport is Tunnel.State.Starting ->
|
||||
if (isVpnStyle) EstablishingConnection else Ready
|
||||
|
||||
transport is Tunnel.State.Starting -> {
|
||||
when {
|
||||
bootstrapPhaseDone -> {
|
||||
if (isVpnStyle) EstablishingConnection else Ready
|
||||
}
|
||||
else -> Connecting
|
||||
}
|
||||
}
|
||||
|
||||
// Final fallback after bootstrap phase is done
|
||||
bootstrapPhaseDone -> if (isVpnStyle) EstablishingConnection else Ready
|
||||
|
||||
else -> Connecting
|
||||
else -> if (isVpnStyle) EstablishingConnection else Ready
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,5 +18,6 @@ data class GlobalAppUiState(
|
||||
val selectedTunnelCount: Int = 0,
|
||||
val alreadyDonated: Boolean = false,
|
||||
val isPinVerified: Boolean = false,
|
||||
val pendingWgImportUrl: String? = null,
|
||||
val isScreenRecordingProtectionEnabled: Boolean = false,
|
||||
)
|
||||
|
||||
@@ -16,14 +16,14 @@ val ElectricTeal = Color(0xFF4DD0E1)
|
||||
// Status colors
|
||||
val SilverTree = Color(0xFF6DB58B)
|
||||
val AlertRed = Color(0xFFCF6679)
|
||||
|
||||
val Straw = Color(0xFFD4C483)
|
||||
|
||||
val Disabled = CoolGray.copy(alpha = 0.4f)
|
||||
|
||||
// Config colors
|
||||
// Other colors
|
||||
val ConfigHeaderColor = Color(0xFFBB86FC)
|
||||
val ConfigKeyColor = Color(0xFF03DAC5)
|
||||
val Heart = Color(0xFFDB61A2)
|
||||
|
||||
sealed class ThemeColors(
|
||||
val background: Color,
|
||||
|
||||
+15
-30
@@ -14,13 +14,12 @@ import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import android.provider.Settings
|
||||
import android.service.quicksettings.TileService
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.net.toUri
|
||||
import com.zaneschepke.wireguardautotunnel.MainActivity
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.tile.AutoTunnelControlTile
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.tile.TunnelControlTile
|
||||
import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelControlTile
|
||||
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.splittunnel.state.TunnelApp
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||
@@ -30,17 +29,11 @@ import java.util.Locale
|
||||
import kotlin.system.exitProcess
|
||||
import timber.log.Timber
|
||||
|
||||
fun Context.openWebUrl(url: String): Result<Unit> {
|
||||
return kotlin
|
||||
.runCatching {
|
||||
val webpage: Uri = url.toUri()
|
||||
val intent =
|
||||
Intent(Intent.ACTION_VIEW, webpage).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
.onFailure { showToast(R.string.no_browser_detected) }
|
||||
fun Context.openWebUrl(url: String): Result<Unit> = runCatching {
|
||||
val webpage: Uri = url.toUri()
|
||||
val intent =
|
||||
Intent(Intent.ACTION_VIEW, webpage).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
fun Context.isBatteryOptimizationsDisabled(): Boolean {
|
||||
@@ -109,15 +102,7 @@ fun Context.launchShareFile(file: File) {
|
||||
this.startActivity(chooserIntent)
|
||||
}
|
||||
|
||||
fun Context.showToast(resId: Int) {
|
||||
Toast.makeText(this, this.getString(resId), Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
fun Context.showToast(message: String) {
|
||||
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
fun Context.launchSupportEmail() {
|
||||
fun Context.launchSupportEmail(): Result<Unit> = runCatching {
|
||||
val intent =
|
||||
Intent(Intent.ACTION_SENDTO).apply {
|
||||
data = "mailto:".toUri()
|
||||
@@ -132,7 +117,7 @@ fun Context.launchSupportEmail() {
|
||||
}
|
||||
)
|
||||
} else {
|
||||
showToast(R.string.no_email_detected)
|
||||
throw IllegalStateException("No email client found")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,9 +237,9 @@ fun Context.installApk(apkFile: File) {
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
fun Context.launchPlayStoreListing() {
|
||||
fun Context.launchPlayStoreListing(): Result<Unit> = runCatching {
|
||||
val intent =
|
||||
Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=$packageName")).apply {
|
||||
Intent(Intent.ACTION_VIEW, "market://details?id=$packageName".toUri()).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
setPackage("com.android.vending")
|
||||
}
|
||||
@@ -262,12 +247,12 @@ fun Context.launchPlayStoreListing() {
|
||||
if (intent.resolveActivity(packageManager) != null) {
|
||||
startActivity(intent)
|
||||
} else {
|
||||
openWebUrl("https://play.google.com/store/apps/details?id=$packageName")
|
||||
throw IllegalStateException("Play Store not found")
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.launchPlayStoreReview() {
|
||||
val uri = Uri.parse("market://details?id=$packageName&showAllReviews=true")
|
||||
fun Context.launchPlayStoreReview(): Result<Unit> = runCatching {
|
||||
val uri = "market://details?id=$packageName&showAllReviews=true".toUri()
|
||||
val intent =
|
||||
Intent(Intent.ACTION_VIEW, uri).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
@@ -276,7 +261,7 @@ fun Context.launchPlayStoreReview() {
|
||||
if (intent.resolveActivity(packageManager) != null) {
|
||||
startActivity(intent)
|
||||
} else {
|
||||
openWebUrl("https://play.google.com/store/apps/details?id=$packageName&showAllReviews=true")
|
||||
throw IllegalStateException("Play Store not found")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package com.zaneschepke.wireguardautotunnel.util.extensions
|
||||
|
||||
import io.ktor.client.statement.HttpResponse
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
|
||||
suspend fun HttpResponse.isHtmlResponse(): Boolean {
|
||||
val contentType = headers["Content-Type"] ?: ""
|
||||
if (contentType.contains("text/html", ignoreCase = true)) return true
|
||||
|
||||
val bodyStart = bodyAsText().trimStart()
|
||||
return bodyStart.startsWith("<!DOCTYPE", ignoreCase = true) ||
|
||||
bodyStart.startsWith("<html", ignoreCase = true)
|
||||
}
|
||||
+1
-1
@@ -61,7 +61,7 @@ fun TunnelMode.asTitleString(context: Context): String {
|
||||
fun TunnelMode.asString(context: Context): String {
|
||||
return when (this) {
|
||||
TunnelMode.VPN -> context.getString(R.string.vpn)
|
||||
TunnelMode.PROXY -> context.getString(R.string.proxy)
|
||||
TunnelMode.PROXY -> context.getString(R.string.local_proxy)
|
||||
TunnelMode.LOCK_DOWN -> context.getString(R.string.lockdown)
|
||||
}
|
||||
}
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package com.zaneschepke.wireguardautotunnel.util.permission
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import androidx.core.content.ContextCompat
|
||||
|
||||
object LocalNetworkPermissionHelper {
|
||||
|
||||
fun shouldRequestPermission(): Boolean {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.CINNAMON_BUN
|
||||
}
|
||||
|
||||
fun isPermissionGranted(context: Context): Boolean {
|
||||
return if (shouldRequestPermission()) {
|
||||
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_LOCAL_NETWORK) ==
|
||||
PackageManager.PERMISSION_GRANTED
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
-10
@@ -2,13 +2,12 @@ package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||
|
||||
import androidx.core.content.PermissionChecker.PERMISSION_GRANTED
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||
import com.zaneschepke.networkmonitor.StableNetworkEngine
|
||||
import com.zaneschepke.tunnel.backend.RootShell
|
||||
import com.zaneschepke.tunnel.util.RootShell
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.AutoTunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelStateHolder
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.WifiDetectionMethod
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
@@ -16,6 +15,8 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsR
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.autotunnel.AutoTunnelStateHolder
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.AutoTunnelUiState
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import kotlinx.coroutines.flow.combine
|
||||
@@ -99,7 +100,10 @@ class AutoTunnelViewModel(
|
||||
val trimmed = name.trim()
|
||||
if (state.autoTunnelSettings.trustedNetworkSSIDs.contains(name)) {
|
||||
return@intent postSideEffect(
|
||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.error_ssid_exists))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.error_ssid_exists),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
}
|
||||
setTrustedNetworkNames(
|
||||
@@ -153,11 +157,19 @@ class AutoTunnelViewModel(
|
||||
when (method) {
|
||||
WifiDetectionMethod.ROOT -> {
|
||||
val accepted = RootShell.requestRootPermission()
|
||||
val message =
|
||||
if (!accepted) StringValue.StringResource(R.string.error_root_denied)
|
||||
else StringValue.StringResource(R.string.root_accepted)
|
||||
postSideEffect(GlobalSideEffect.Snackbar(message))
|
||||
if (!accepted) return@intent
|
||||
if (!accepted)
|
||||
return@intent postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.error_root_denied),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.root_accepted),
|
||||
ToastType.Success,
|
||||
)
|
||||
)
|
||||
}
|
||||
WifiDetectionMethod.SHIZUKU -> {
|
||||
requestShizuku()
|
||||
@@ -188,7 +200,10 @@ class AutoTunnelViewModel(
|
||||
)
|
||||
} catch (_: Exception) {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.shizuku_not_detected))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.shizuku_not_detected),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+13
-6
@@ -1,6 +1,7 @@
|
||||
package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.MimicMode
|
||||
@@ -93,7 +94,6 @@ class ConfigEditViewModel(
|
||||
tunnel = tunnel,
|
||||
tunnels = tunnels,
|
||||
isRunning = isRunning,
|
||||
globalSettings = globalSettings,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -106,7 +106,10 @@ class ConfigEditViewModel(
|
||||
if (state.isTunnelNameTaken) {
|
||||
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.tunnel_name_taken))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.tunnel_name_taken),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
|
||||
return@intent
|
||||
@@ -114,7 +117,10 @@ class ConfigEditViewModel(
|
||||
|
||||
if (state.draft.tunnelName.isBlank()) {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.name_error_empty))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.name_error_empty),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
return@intent
|
||||
}
|
||||
@@ -148,8 +154,9 @@ class ConfigEditViewModel(
|
||||
}
|
||||
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Toast(
|
||||
StringValue.StringResource(R.string.config_changes_saved)
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.config_changes_saved),
|
||||
ToastType.Success,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -165,7 +172,7 @@ class ConfigEditViewModel(
|
||||
else -> StringValue.StringResource(R.string.unknown_error)
|
||||
}
|
||||
|
||||
postSideEffect(GlobalSideEffect.Snackbar(message))
|
||||
postSideEffect(GlobalSideEffect.Snackbar(message, ToastType.Error))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.DnsSettingsCoordinator
|
||||
@@ -9,7 +10,6 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.DnsSettingsReposito
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarType
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.DnsUiState
|
||||
import com.zaneschepke.wireguardautotunnel.util.DnsValidator
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
@@ -70,7 +70,7 @@ class DnsViewModel(
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(result.error.labelRes()),
|
||||
type = SnackbarType.WARNING,
|
||||
type = ToastType.Error,
|
||||
)
|
||||
)
|
||||
return@intent
|
||||
@@ -87,7 +87,10 @@ class DnsViewModel(
|
||||
|
||||
postSideEffect(GlobalSideEffect.PopBackStack)
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Toast(StringValue.StringResource(R.string.config_changes_saved))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.config_changes_saved),
|
||||
ToastType.Success,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+5
-1
@@ -1,6 +1,7 @@
|
||||
package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||
@@ -52,7 +53,10 @@ class LockdownViewModel(
|
||||
|
||||
postSideEffect(GlobalSideEffect.PopBackStack)
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Toast(StringValue.StringResource(R.string.config_changes_saved))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.config_changes_saved),
|
||||
ToastType.Success,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.logcatter.LogReader
|
||||
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
@@ -61,7 +62,10 @@ class LoggerViewModel(
|
||||
fun exportLogs(uri: Uri?) = intent {
|
||||
if (uri == null) {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Toast(StringValue.StringResource(R.string.export_unsupported))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.export_unsupported),
|
||||
ToastType.Warning,
|
||||
)
|
||||
)
|
||||
return@intent
|
||||
}
|
||||
@@ -76,11 +80,12 @@ class LoggerViewModel(
|
||||
Timber.e(action)
|
||||
intent {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Toast(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(
|
||||
R.string.export_failed,
|
||||
": ${action.localizedMessage}",
|
||||
)
|
||||
),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
+27
-17
@@ -1,6 +1,7 @@
|
||||
package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||
@@ -27,21 +28,26 @@ class ProxySettingsViewModel(
|
||||
combine(tunnelCoordinator.backendStatus, proxySettingsRepository.flow) {
|
||||
backendStatus,
|
||||
settings ->
|
||||
ProxySettingsUiState(
|
||||
proxySettings = settings,
|
||||
backendStatus = backendStatus,
|
||||
isLoading = false,
|
||||
socks5Enabled = settings.socks5ProxyEnabled,
|
||||
httpEnabled = settings.httpProxyEnabled,
|
||||
socksBindAddress = settings.socks5ProxyBindAddress ?: "",
|
||||
httpBindAddress = settings.httpProxyBindAddress ?: "",
|
||||
proxyUsername = settings.proxyUsername ?: "",
|
||||
proxyPassword = settings.proxyPassword ?: "",
|
||||
)
|
||||
if (state.isLoading) {
|
||||
ProxySettingsUiState(
|
||||
proxySettings = settings,
|
||||
backendStatus = backendStatus,
|
||||
isLoading = false,
|
||||
socks5Enabled = settings.socks5ProxyEnabled,
|
||||
httpEnabled = settings.httpProxyEnabled,
|
||||
socksBindAddress = settings.socks5ProxyBindAddress ?: "",
|
||||
httpBindAddress = settings.httpProxyBindAddress ?: "",
|
||||
proxyUsername = settings.proxyUsername ?: "",
|
||||
proxyPassword = settings.proxyPassword ?: "",
|
||||
)
|
||||
} else {
|
||||
state.copy(backendStatus = backendStatus)
|
||||
}
|
||||
}
|
||||
.collect { reduce { it } }
|
||||
}
|
||||
|
||||
// TODO add a dialog requesting restart if any tunnels active
|
||||
fun save() = intent {
|
||||
reduce { state.copy(showSaveModal = false) }
|
||||
|
||||
@@ -74,7 +80,8 @@ class ProxySettingsViewModel(
|
||||
if (socksPort == null || httpPort == null || socksPort == httpPort) {
|
||||
return@intent postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.ports_must_differ)
|
||||
StringValue.StringResource(R.string.ports_must_differ),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -91,18 +98,21 @@ class ProxySettingsViewModel(
|
||||
|
||||
if (updated.proxyPassword?.any { it.isWhitespace() } == true) {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.password_no_spaces))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.password_no_spaces),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
return@intent reduce { state.copy(isPasswordError = true) }
|
||||
}
|
||||
|
||||
proxySettingsRepository.upsert(updated)
|
||||
|
||||
tunnelCoordinator.toggleTunnels()
|
||||
tunnelCoordinator.toggleTunnels()
|
||||
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.config_changes_saved))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.config_changes_saved),
|
||||
ToastType.Success,
|
||||
)
|
||||
)
|
||||
postSideEffect(GlobalSideEffect.PopBackStack)
|
||||
}
|
||||
|
||||
+15
-6
@@ -1,7 +1,8 @@
|
||||
package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.zaneschepke.tunnel.backend.RootShell
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.tunnel.util.RootShell
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager
|
||||
@@ -96,11 +97,19 @@ class SettingsViewModel(
|
||||
fun setTunnelScriptedEnabled(to: Boolean) = intent {
|
||||
if (to) {
|
||||
val accepted = RootShell.requestRootPermission()
|
||||
val message =
|
||||
if (!accepted) StringValue.StringResource(R.string.error_root_denied)
|
||||
else StringValue.StringResource(R.string.root_accepted)
|
||||
postSideEffect(GlobalSideEffect.Snackbar(message))
|
||||
if (!accepted) return@intent
|
||||
if (!accepted)
|
||||
return@intent postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.error_root_denied),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.root_accepted),
|
||||
ToastType.Success,
|
||||
)
|
||||
)
|
||||
}
|
||||
settingsRepository.upsert(state.settings.copy(tunnelScriptingEnabled = to))
|
||||
}
|
||||
|
||||
+71
-32
@@ -3,11 +3,10 @@ package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelModeCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelStateHolder
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
|
||||
@@ -17,7 +16,10 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.SelectedTunnelsRepo
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.parser.ConfigParseException
|
||||
import com.zaneschepke.wireguardautotunnel.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.autotunnel.AutoTunnelStateHolder
|
||||
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.GlobalAppUiState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.TunnelsUiState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
|
||||
@@ -27,6 +29,7 @@ import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.QuickConfig
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelName
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asStringValue
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isHtmlResponse
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.saveTunnelsUniquely
|
||||
import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils
|
||||
import io.ktor.client.HttpClient
|
||||
@@ -71,10 +74,16 @@ class SharedAppViewModel(
|
||||
tunnelRepository.userTunnelsFlow,
|
||||
tunnelCoordinator.backendStatus,
|
||||
selectedTunnelsRepository.flow,
|
||||
tunnelCoordinator.tunnelDisplayStates,
|
||||
) { tunnels, backendStatus, selectedTuns, displayStates ->
|
||||
) { tunnels, backendStatus, selectedTuns ->
|
||||
val sortedTunnels = tunnels.sortedBy { it.position }
|
||||
|
||||
val displayStates =
|
||||
backendStatus.activeTunnels.mapValues { (_, activeTunnel) ->
|
||||
DisplayTunnelState.from(activeTunnel)
|
||||
}
|
||||
|
||||
TunnelsUiState(
|
||||
tunnels = tunnels,
|
||||
tunnels = sortedTunnels,
|
||||
backendStatus = backendStatus,
|
||||
displayStates = displayStates,
|
||||
selectedTunnels = selectedTuns,
|
||||
@@ -170,6 +179,10 @@ class SharedAppViewModel(
|
||||
appStateRepository.setShouldShowDonationSnackbar(to)
|
||||
}
|
||||
|
||||
fun showSnackMessage(message: StringValue, type: ToastType) = intent {
|
||||
postGlobalSideEffect(GlobalSideEffect.Snackbar(message, type))
|
||||
}
|
||||
|
||||
suspend fun postSideEffect(globalSideEffect: GlobalSideEffect) {
|
||||
globalEffectRepository.post(globalSideEffect)
|
||||
}
|
||||
@@ -180,12 +193,6 @@ class SharedAppViewModel(
|
||||
globalEffectRepository.post(sideEffect)
|
||||
}
|
||||
|
||||
fun showSnackMessage(message: StringValue) = intent {
|
||||
postGlobalSideEffect(GlobalSideEffect.Snackbar(message))
|
||||
}
|
||||
|
||||
fun showToast(message: StringValue) = intent { postSideEffect(GlobalSideEffect.Toast(message)) }
|
||||
|
||||
fun disableBatteryOptimizationsShown() = intent {
|
||||
appStateRepository.setBatteryOptimizationDisableShown(true)
|
||||
}
|
||||
@@ -193,14 +200,20 @@ class SharedAppViewModel(
|
||||
fun saveSortChanges(tunnels: List<TunnelConfig>) = intent {
|
||||
tunnelRepository.saveAll(tunnels.mapIndexed { index, conf -> conf.copy(position = index) })
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.config_changes_saved))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.config_changes_saved),
|
||||
ToastType.Success,
|
||||
)
|
||||
)
|
||||
postSideEffect(GlobalSideEffect.PopBackStack)
|
||||
}
|
||||
|
||||
fun sortByLatency(tunnels: List<TunnelConfig>) = intent {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.pinging_servers))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.pinging_servers),
|
||||
ToastType.Info,
|
||||
)
|
||||
)
|
||||
val sortedResult =
|
||||
withContext(Dispatchers.IO) {
|
||||
@@ -242,12 +255,17 @@ class SharedAppViewModel(
|
||||
TunnelConfig.tunnelConfFromQuick(config, name)
|
||||
}
|
||||
tunnelRepository.saveTunnelsUniquely(tunnelConfigs, state.tunnelNames.map { it.value })
|
||||
} catch (_: IOException) {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.read_failed))
|
||||
)
|
||||
} catch (e: ConfigParseException) {
|
||||
postSideEffect(GlobalSideEffect.Snackbar(e.asStringValue()))
|
||||
} catch (e: Exception) {
|
||||
if (e is ConfigParseException) {
|
||||
postSideEffect(GlobalSideEffect.Snackbar(e.asStringValue(), ToastType.Error))
|
||||
} else {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.config_error),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,22 +275,38 @@ class SharedAppViewModel(
|
||||
|
||||
fun importFromQr(conf: String) = intent { importFromClipboard(conf) }
|
||||
|
||||
fun promptWgImport(url: String) = intent { reduce { state.copy(pendingWgImportUrl = url) } }
|
||||
|
||||
fun dismissWgImport() = intent { reduce { state.copy(pendingWgImportUrl = null) } }
|
||||
|
||||
fun importFromUrl(url: String) = intent {
|
||||
reduce { state.copy(pendingWgImportUrl = null) }
|
||||
|
||||
try {
|
||||
httpClient.prepareGet(url).execute { response ->
|
||||
if (response.status.value in 200..299) {
|
||||
val body = response.bodyAsText()
|
||||
importFromClipboard(body)
|
||||
} else {
|
||||
throw IOException(
|
||||
"Failed to download file with error status: ${response.status.value}"
|
||||
)
|
||||
if (response.status.value !in 200..299) {
|
||||
throw IOException("Server returned error: ${response.status.value}")
|
||||
}
|
||||
|
||||
if (response.isHtmlResponse()) {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.error_invalid_config_url),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
return@execute
|
||||
}
|
||||
val body = response.bodyAsText()
|
||||
importFromClipboard(body)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Toast(StringValue.StringResource(R.string.error_download_failed))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.error_download_failed),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -287,7 +321,7 @@ class SharedAppViewModel(
|
||||
is IOException -> StringValue.StringResource(R.string.error_download_failed)
|
||||
else -> StringValue.StringResource(R.string.error_file_extension)
|
||||
}
|
||||
postSideEffect(GlobalSideEffect.Toast(message))
|
||||
postSideEffect(GlobalSideEffect.Snackbar(message, ToastType.Error))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,7 +353,8 @@ class SharedAppViewModel(
|
||||
if (selectedTuns.any { activeTunIds?.contains(it.id) == true })
|
||||
return@intent postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.delete_active_message)
|
||||
StringValue.StringResource(R.string.delete_active_message),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
tunnelRepository.delete(selectedTuns)
|
||||
@@ -342,11 +377,12 @@ class SharedAppViewModel(
|
||||
val onFailure = { action: Throwable ->
|
||||
intent {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Toast(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(
|
||||
R.string.export_failed,
|
||||
": ${action.localizedMessage}",
|
||||
)
|
||||
),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -363,7 +399,10 @@ class SharedAppViewModel(
|
||||
if (it.exists()) it.delete()
|
||||
}
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.export_success))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.export_success),
|
||||
ToastType.Success,
|
||||
)
|
||||
)
|
||||
clearSelectedTunnels()
|
||||
}
|
||||
|
||||
+8
-2
@@ -1,6 +1,7 @@
|
||||
package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.InstalledPackageRepository
|
||||
@@ -88,9 +89,14 @@ class SplitTunnelViewModel(
|
||||
editableInterface.copy(includedApplications = included, excludedApplications = excluded)
|
||||
val updatedProxyConfig = editableConfig.copy(`interface` = updatedInterface)
|
||||
val updatedConfig = updatedProxyConfig.buildConfig()
|
||||
tunnelRepository.save(tunnel.copy(quickConfig = updatedConfig.asQuickString()))
|
||||
tunnelRepository.save(
|
||||
tunnel.copy(quickConfig = updatedConfig.withName(tunnel.name).asQuickString())
|
||||
)
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.config_changes_saved))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.config_changes_saved),
|
||||
ToastType.Success,
|
||||
)
|
||||
)
|
||||
postSideEffect(GlobalSideEffect.PopBackStack)
|
||||
}
|
||||
|
||||
+15
-6
@@ -1,6 +1,7 @@
|
||||
package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.AppUpdate
|
||||
@@ -25,7 +26,10 @@ class SupportViewModel(
|
||||
|
||||
fun checkForStandaloneUpdate() = intent {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Toast(StringValue.StringResource(R.string.checking_for_update))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.checking_for_update),
|
||||
ToastType.Info,
|
||||
)
|
||||
)
|
||||
reduce { state.copy(isLoading = true) }
|
||||
updateRepository
|
||||
@@ -33,15 +37,19 @@ class SupportViewModel(
|
||||
.onSuccess { update ->
|
||||
if (update == null) {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Toast(
|
||||
StringValue.StringResource(R.string.latest_installed)
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.latest_installed),
|
||||
ToastType.Info,
|
||||
)
|
||||
)
|
||||
} else reduce { state.copy(appUpdate = update.sanitized()) }
|
||||
}
|
||||
.onFailure {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Toast(StringValue.StringResource(R.string.update_check_failed))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.update_check_failed),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
}
|
||||
reduce { state.copy(isLoading = false) }
|
||||
@@ -83,8 +91,9 @@ class SupportViewModel(
|
||||
.onSuccess { postSideEffect(GlobalSideEffect.InstallApk(it)) }
|
||||
.onFailure {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Toast(
|
||||
StringValue.StringResource(R.string.update_download_failed)
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.update_download_failed),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<group
|
||||
android:scaleX="1.18"
|
||||
android:scaleY="1.18"
|
||||
android:pivotX="512"
|
||||
android:pivotY="512"
|
||||
android:translateX="-45"
|
||||
android:translateY="-45">
|
||||
<path
|
||||
android:pathData="M779.7,207.8C782.5,207.8 785.4,207.8 788.2,207.8C851.8,207.6 851.8,207.6 871.1,225.5C882.4,237.3 887.6,250.9 887.5,267.1C886.9,284.9 879,300.9 872,317C871,319.2 870.1,321.4 869.1,323.7C867.7,327.1 866.2,330.6 864.7,334.1C861,342.7 857.3,351.4 853.7,360.1C847.1,375.7 840.5,391.3 833.8,406.9C831.1,413.1 828.5,419.2 825.8,425.4C822.9,432.3 819.9,439.2 817,446C804.9,474 804.9,474 793.3,502.2C791,507.8 788.5,513.3 785.9,518.7C782,527 778.5,535.5 775,544C772,551 769,557.9 766.1,564.9C765.6,566 765.1,567.2 764.6,568.4C763.5,570.7 762.5,573.1 761.5,575.5C760,579 758.5,582.6 757,586.1C751.8,598.2 746.6,610.4 741.2,622.4C737.3,631.3 733.4,640.3 729.5,649.2C725.8,657.9 722,666.6 718,675.2C715.7,680.4 713.5,685.6 711.4,690.8C708.5,697.8 705.4,704.7 702.1,711.6C700.8,714.5 699.4,717.4 698,720.4C685.7,746.7 672.9,772.5 643.9,783.1C639.6,784.4 635.5,784.7 631,785C630.2,785.1 629.5,785.1 628.7,785.2C610.7,785.8 596.7,779.5 583.5,767.8C569.4,754.5 562,735.7 554.3,718.3C552.1,713.5 549.9,708.7 547.5,704.1C543.9,696.9 540.8,689.6 537.6,682.2C534.6,675 531.4,667.8 528.3,660.6C522.3,646.9 516.3,633.3 510.4,619.6C508.5,615.1 506.5,610.6 504.6,606.1C494.1,582.1 494.1,582.1 489.6,571.2C488,567.4 486.4,563.6 484.6,559.8C481.8,553.6 479.2,547.4 476.6,541.1C472.4,531.2 472.4,531.2 468,521.5C461.3,507.3 455.5,492.6 449.5,478.1C444.9,466.9 440.2,455.8 435.3,444.7C431.5,436.1 427.7,427.4 424.1,418.8C423.8,418.1 423.5,417.4 423.2,416.6C420,409.1 416.8,401.6 413.7,394C413.4,393.3 413.2,392.7 412.9,392C411.6,389 410.4,386 409.2,383C406.8,377.1 404.3,371.4 401.6,365.7C397.9,357.9 394.6,349.9 391.3,341.9C390.4,339.6 389.5,337.4 388.6,335.2C388,333.7 387.3,332.2 386.7,330.7C384.7,325.7 382.6,320.7 380.6,315.8C380,314.5 379.5,313.2 378.9,311.9C377.9,309.5 376.9,307.1 375.9,304.7C368.4,286.7 361.9,264.8 369.7,245.7C373.8,237.4 377.9,230 385,224C385.6,223.5 386.2,223 386.8,222.4C397,213.9 412.5,209.7 425.8,210.1C442.3,212.1 455.2,220.4 466,233C471.9,241.6 476.5,250.7 480.9,260.1C481.5,261.3 482.2,262.6 482.8,263.9C487.3,273.5 491.7,283.1 496,292.8C497,294.9 497.9,297.1 498.9,299.2C503.6,309.6 508.1,320.1 512.5,330.6C515.1,336.6 517.7,342.5 520.5,348.4C524.1,356.1 527.4,363.9 530.6,371.8C531.7,374.4 532.8,377 533.9,379.7C534.3,380.6 534.3,380.6 534.7,381.6C536.3,385.3 537.8,388.9 539.5,392.5C541.5,396.7 543.3,401 545.1,405.3C545.4,406 545.6,406.6 545.9,407.3C547,409.9 548.1,412.6 549.2,415.2C552.5,423.2 555.9,431.1 559.6,438.9C561.7,443.4 563.7,447.9 565.5,452.5C567.6,457.7 569.8,462.9 572.3,467.9C575.9,475.7 579.2,483.5 582.5,491.4C585.8,499.2 589.1,507 592.5,514.7C617.2,571.5 617.2,571.5 625.1,591.7C625.8,593.9 625.8,593.9 627,595C627.3,594.3 627.6,593.5 627.9,592.7C632.8,580 637.7,567.3 643.4,554.9C646.9,547.2 650.2,539.5 653.4,531.8C653.7,531.1 654,530.4 654.3,529.7C657.2,522.9 660,516 662.9,509.1C663.3,508 663.8,506.9 664.2,505.8C665,503.9 665.9,501.9 666.7,499.9C668.9,494.5 671.3,489.1 673.8,483.7C675.7,479.4 677.6,475 679.5,470.7C679.9,469.7 680.3,468.7 680.8,467.7C682.2,464.5 683.6,461.2 685,458C686,455.6 687.1,453.3 688.1,450.9C689.7,447.1 691.3,443.4 693,439.6C695.8,433.2 698.5,426.8 701.3,420.3C714.6,389.7 714.6,389.7 727.2,358.9C729.7,352.7 732.4,346.8 735.3,340.8C736.9,337.4 738.2,333.8 739.6,330.3C740.1,329.1 740.5,327.9 741,326.7C741.3,325.8 741.6,324.9 742,324C741.2,324 741.2,324 740.3,324C726.5,324.1 712.7,324.1 698.9,324.1C692.2,324.1 685.5,324.1 678.9,324.2C672.4,324.2 665.9,324.2 659.5,324.2C657,324.2 654.6,324.2 652.1,324.2C640.5,324.3 629,324.2 617.4,323.1C616.6,323.1 615.8,323 614.9,322.9C597,321 581.4,314.3 569,301C566.5,297.8 564.7,294.6 563,291C562.5,290 562,289 561.5,288C556.5,276.1 555.8,261.8 559.6,249.4C562,243.5 565.3,238.2 569,233C569.7,232 569.7,232 570.4,231C581.2,217.3 599.1,212.3 615.8,210.2C637.1,208 659,208.7 680.4,208.5C683.5,208.4 686.5,208.4 689.5,208.4C719.6,208.1 749.6,207.9 779.7,207.8Z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
<path
|
||||
android:pathData="M263.3,223.5C276.3,234.5 282.6,248.9 289.6,264C290.9,266.8 292.2,269.5 293.5,272.2C303,291.5 311.4,311.3 320,331C320.3,331.7 320.6,332.5 321,333.2C323.4,338.9 325.9,344.5 328.3,350.2C332.2,359.1 336.1,368.1 340,377C346.2,391.1 352.3,405.2 358.4,419.3C360.8,424.9 363.3,430.5 365.7,436.1C366.5,438 367.3,439.9 368.1,441.7C370.6,447.4 373,453 375.5,458.7C382.5,474.8 389.4,491 396.4,507.2C398.1,511.3 399.9,515.5 401.7,519.6C408.7,536 415.8,552.5 422.7,568.9C424,572 425.3,575.2 426.7,578.3C433,593.2 439.2,608.1 445.5,623C447.5,627.7 449.4,632.4 451.4,637C453.8,642.9 456.3,648.7 458.7,654.6C459.9,657.5 461.2,660.4 462.4,663.3C467.1,674.6 471.8,685.9 476.4,697.3C477.1,698.8 477.1,698.8 477.7,700.4C484.3,716.8 488.7,735.5 482,752.6C479.3,758.3 475.9,763.2 472,768C471.6,768.6 471.1,769.2 470.7,769.8C464.5,777.8 455.6,782.4 446,785C445,785.3 444,785.7 443,786C431.6,787 420.6,786.5 410,782C408.7,781.5 408.7,781.5 407.4,781C380.7,769.4 369.2,736.1 358.2,711.6C356.7,708.3 355.1,705 353.6,701.7C340.8,674.4 329,646.7 317.5,618.9C316,615.2 314.3,611.5 312.7,607.9C309.7,601.4 306.9,594.9 304.1,588.4C303.9,587.8 303.6,587.2 303.3,586.5C299.7,578 296.1,569.4 292.6,560.8C291,556.9 289.2,553 287.4,549.1C284.7,543 282,536.9 279.3,530.8C278.7,529.3 278.1,527.9 277.4,526.5C276.2,523.6 274.9,520.8 273.7,517.9C272.3,514.6 270.8,511.3 269.4,508C264.2,496.1 259.1,484.2 254.1,472.3C250.4,463.6 246.7,455 242.9,446.3C242.5,445.4 242.2,444.5 241.8,443.6C240,439.6 238.3,435.6 236.5,431.6C234.9,427.8 233.2,424.1 231.6,420.4C231.4,419.7 231.1,419.1 230.8,418.5C226.1,407.6 221.5,396.7 217,385.8C214.5,379.6 211.9,373.5 209.2,367.5C207.4,363.7 205.8,359.9 204.1,356.1C203.8,355.2 203.4,354.4 203,353.5C199.1,344.2 195.2,334.9 191.3,325.6C191,324.9 190.7,324.1 190.3,323.3C187.1,315.5 183.8,307.6 180.7,299.8C180.2,298.6 179.7,297.4 179.2,296.2C177.6,291.9 176.2,287.5 175,283C174.8,282.4 174.7,281.8 174.5,281.2C171.2,266.5 173.7,250.9 181.4,238.2C191.3,223.5 203.1,215.7 220.6,212.3C236.3,210.9 250.7,213.5 263.3,223.5Z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
</group>
|
||||
</vector>
|
||||
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.5 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.4 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 5.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 7.8 KiB |
@@ -15,7 +15,6 @@
|
||||
<string name="delete_tunnel">Delete tunnel</string>
|
||||
<string name="tunnel_mobile_data">Tunnel on mobile data</string>
|
||||
<string name="logs">Logs</string>
|
||||
<string name="enable_app_lock">Enable app lock</string>
|
||||
<string name="config_changes_saved">Configuration changes saved.</string>
|
||||
<string name="join_telegram">Join Telegram community</string>
|
||||
<string name="pin_created">Pin successfully created</string>
|
||||
@@ -27,7 +26,6 @@
|
||||
<string name="remote_key">Remote key</string>
|
||||
<string name="mobile_data">Mobile data</string>
|
||||
<string name="use_shell_via_shizuku">Use shell via Shizuku to get Wi-Fi information, preventing the need for location permission on non-rooted devices</string>
|
||||
<string name="stop_on_internet_loss">Stop tunnel on internet loss</string>
|
||||
<string name="vpn">VPN</string>
|
||||
<string name="tunnel_boot_description">Start the default tunnel on boot</string>
|
||||
<string name="allow_lan_traffic">Allow LAN traffic</string>
|
||||
@@ -60,7 +58,7 @@
|
||||
<string name="wifi_settings">Wi-Fi settings</string>
|
||||
<string name="tunnel_on_wifi">Tunnel on Wi-Fi</string>
|
||||
<string name="add_peer">Add peer</string>
|
||||
<string name="backup_success">Backup success. %1$s</string>
|
||||
<string name="backup_success">Backup success</string>
|
||||
<string name="persistent_keepalive">Persistent keepalive</string>
|
||||
<string name="info">Info</string>
|
||||
<string name="exclude">Exclude</string>
|
||||
@@ -133,7 +131,6 @@
|
||||
<string name="kofi">Ko-fi</string>
|
||||
<string name="donation_signoff">Gratefully,</string>
|
||||
<string name="selected">Selected</string>
|
||||
<string name="global_split_tunneling">Global split tunneling</string>
|
||||
<string name="active_network">Active Network:</string>
|
||||
<string name="range_hint">(%1$d–%2$d)</string>
|
||||
<string name="delete_active_message">Cannot delete active tunnel.</string>
|
||||
@@ -156,7 +153,6 @@
|
||||
<string name="mtu">MTU</string>
|
||||
<string name="configuration">Configuration</string>
|
||||
<string name="drag_handle">Drag Handle</string>
|
||||
<string name="global_dns_servers">Global DNS servers</string>
|
||||
<string name="display_theme">Display theme</string>
|
||||
<string name="contact">Contact</string>
|
||||
<string name="ports_must_differ">Failed. Proxies must have different ports.</string>
|
||||
@@ -257,7 +253,6 @@
|
||||
<string name="unavailable_in_mode">Unavailable in current mode</string>
|
||||
<string name="server_port">Server:Port</string>
|
||||
<string name="camera_permission_required">Camera permission required</string>
|
||||
<string name="enabled_app_shortcuts">Enable app shortcuts</string>
|
||||
<string name="preferred_tunnel">Preferred tunnel</string>
|
||||
<string name="allow">Allow</string>
|
||||
<string name="underload_packet_magic_header">Underload packet magic header</string>
|
||||
@@ -272,7 +267,6 @@
|
||||
<string name="settings">Settings</string>
|
||||
<string name="incorrect_pin">Pin is incorrect</string>
|
||||
<string name="export_failed">Export failed</string>
|
||||
<string name="enable_remote_app_control">Enable remote app control</string>
|
||||
<string name="donation_closing">It\'s my dream to work for you on this project full-time.</string>
|
||||
<string name="update_download_failed">Update download failed.</string>
|
||||
<string name="network_name">Network:</string>
|
||||
@@ -357,4 +351,112 @@
|
||||
<string name="github_sponsors_url" translatable="false">https://github.com/sponsors/zaneschepke</string>
|
||||
<string name="transport_packet_junk_size">Transport packet junk size</string>
|
||||
<string name="cookie_packet_junk_size">Cookie packet junk size</string>
|
||||
<string name="fallback_to_ipv4">Fallback to IPv4</string>
|
||||
<string name="excluded_apps">%1$s apps excluded</string>
|
||||
<string name="kernel_wireguard_unsupported">Kernel does not include WireGuard support (module missing or not built-in into kernel)!</string>
|
||||
<string name="resolution_method">Resolution method</string>
|
||||
<string name="notification_ipv6_recovery_message">%1$s recovered IPv6 connectivity</string>
|
||||
<string name="tunnel_state_resolving_dns">Resolving DNS</string>
|
||||
<string name="handshake_template">handshake: %1$s</string>
|
||||
<string name="peer_template">peer: %1$s</string>
|
||||
<string name="errors">Errors</string>
|
||||
<string name="no_system_dns_information">No system DNS information</string>
|
||||
<string name="export_unsupported">Export is not supported on this device</string>
|
||||
<string name="tunnel_globals">Tunnel globals</string>
|
||||
<string name="sort_by_latency">Sort by latency</string>
|
||||
<string name="ready">Ready</string>
|
||||
<string name="no_system_dns_detected">No system DNS detected</string>
|
||||
<string name="name_error_empty">Tunnel name cannot be empty</string>
|
||||
<string name="notification_ipv4_fallback_message">%1$s switched to IPv4 connectivity</string>
|
||||
<string name="notification_tunnel_status_format">%1$s • %2$s</string>
|
||||
<string name="dns_error_invalid_ip_or_host">Invalid IP address or hostname</string>
|
||||
<string name="proxy_channel_description">Notifications for proxy tunnels.</string>
|
||||
<string name="system_dns_servers">Servers: %1$s</string>
|
||||
<string name="balanced">Balanced (3s)</string>
|
||||
<string name="import_url_description">The URL must be secure and serve a .conf file.</string>
|
||||
<string name="dns_error_invalid_port">Port must be between 1 and 65535</string>
|
||||
<string name="view_configuration">View configuration</string>
|
||||
<string name="dot">DNS over TLS (DoT)</string>
|
||||
<string name="ipv6_recovery">IPv6 recovery</string>
|
||||
<string name="screen_recording_protection">Screen recording protection</string>
|
||||
<string name="dns_error_invalid_host">Host cannot be empty</string>
|
||||
<string name="toggle_sensitive_data_visibility">Toggle sensitive data visibility</string>
|
||||
<string name="ipv4_fallback">IPv4 fallback</string>
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
<string name="app_selection">App selection</string>
|
||||
<string name="tunnel_state_handshake_failure">Handshake failure</string>
|
||||
<string name="notification_dynamic_dns_message">%1$s updated after Dynamic DNS change</string>
|
||||
<string name="tunnel_state_establishing_connection">Establishing connection</string>
|
||||
<string name="prefer_ipv6">Prefer IPv6</string>
|
||||
<string name="peer_endpoints">Peer endpoints</string>
|
||||
<string name="dns_endpoint_hint">IP, hostname, or DoH URL</string>
|
||||
<string name="restore_ipv6">Restore IPv6</string>
|
||||
<string name="network">Network</string>
|
||||
<string name="fallback_to_ipv4_desc">Switch to IPv4 if IPv6 fails, without restarting the tunnel.</string>
|
||||
<string name="tunnel_state_disconnected">Disconnected</string>
|
||||
<string name="live">Real-time (1s)</string>
|
||||
<string name="uptime_template">uptime: %1$s</string>
|
||||
<string name="events">Events</string>
|
||||
<string name="peer_resolution">Peer Resolution</string>
|
||||
<string name="restore_ipv6_desc">Switch back to IPv6 when an IPv6 network is detected.</string>
|
||||
<string name="errors_channel_description">A channel for application and tunnel errors</string>
|
||||
<string name="vpn_permission_required">VPN permission required</string>
|
||||
<string name="app_channel_description">A channel for general application notifications, like version updates</string>
|
||||
<string name="automation">Automation</string>
|
||||
<string name="balance_saver">Battery Saver (10s)</string>
|
||||
<string name="pinging_servers">Pinging servers…</string>
|
||||
<string name="dynamic_dns_update">Dynamic DNS update</string>
|
||||
<string name="statistics">Statistics</string>
|
||||
<string name="events_channel_description">A channel for app events, like automation event</string>
|
||||
<string name="private_dns_automatic">Private DNS: automatic</string>
|
||||
<string name="tunnel_statistics">Live tunnel statistics</string>
|
||||
<string name="ipv6_settings">IPv6 settings</string>
|
||||
<string name="error">Error</string>
|
||||
<string name="dns_error_invalid_url">Invalid URL format</string>
|
||||
<string name="view_live_tunnel">View live tunnel</string>
|
||||
<string name="mode">Mode</string>
|
||||
<string name="tunnel_state_starting">Starting</string>
|
||||
<string name="dns_endpoint_label">DNS server endpoint</string>
|
||||
<string name="plain_dns">Plain DNS (port 53)</string>
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
<string name="export_canceled">Export canceled</string>
|
||||
<string name="prefer_ipv6_desc">Use IPv6 endpoints when the network supports it.</string>
|
||||
<string name="stop_all">Stop all</string>
|
||||
<string name="copy_from">Copy from</string>
|
||||
<string name="special_junk_packet">Special junk packet</string>
|
||||
<string name="error_socks5_port_unavailable">SOCKS5 port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="tunnel_scripting">Pre/Post script support</string>
|
||||
<string name="initializing">Initializing…</string>
|
||||
<string name="errors_channel_id" translatable="false">Errors Channel</string>
|
||||
<string name="s2" translatable="false">S2</string>
|
||||
<string name="jc" translatable="false">Jc</string>
|
||||
<string name="i2" translatable="false">I2</string>
|
||||
<string name="events_channel_id" translatable="false">Events Channel</string>
|
||||
<string name="proxy_channel_id" translatable="false">Proxy Channel</string>
|
||||
<string name="jmax" translatable="false">Jmax</string>
|
||||
<string name="h2" translatable="false">H2</string>
|
||||
<string name="s3" translatable="false">S3</string>
|
||||
<string name="i1" translatable="false">I1</string>
|
||||
<string name="app_channel_id" translatable="false">App Channel</string>
|
||||
<string name="i3" translatable="false">I3</string>
|
||||
<string name="example_import_url" translatable="false">https://123.com/tun.conf</string>
|
||||
<string name="i5" translatable="false">I5</string>
|
||||
<string name="s4" translatable="false">S4</string>
|
||||
<string name="fdroid_url" translatable="false">https://apt.izzysoft.de/fdroid/index/apk/com.zaneschepke.wireguardautotunnel</string>
|
||||
<string name="h3" translatable="false">H3</string>
|
||||
<string name="h1" translatable="false">H1</string>
|
||||
<string name="jmin" translatable="false">Jmin</string>
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
</resources>
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
<string name="preshared_key">Předsdílený klíč</string>
|
||||
<string name="seconds">s</string>
|
||||
<string name="cancel">Zrušit</string>
|
||||
<string name="enabled_app_shortcuts">Zapnout zkratky aplikací</string>
|
||||
<string name="unknown_error">Došlo k neznámé chybě</string>
|
||||
<string name="tunnel_on_wifi">Tunelovat na Wi-Fi</string>
|
||||
<string name="email_subject">WG Tunnel podpora</string>
|
||||
@@ -65,7 +64,6 @@
|
||||
<string name="no_browser_detected">Žádný prohlížeč nebyl nalezen</string>
|
||||
<string name="pin_created">PIN úspěšně vytvořen</string>
|
||||
<string name="enter_pin">Zadejte PIN</string>
|
||||
<string name="enable_app_lock">Zapnout zámek aplikace</string>
|
||||
<string name="settings">Nastavení</string>
|
||||
<string name="support">Podpora</string>
|
||||
<string name="app_name">WG Tunnel</string>
|
||||
@@ -90,7 +88,6 @@
|
||||
<string name="primary_tunnel">Výchozí tunel</string>
|
||||
<string name="skip">Přeskočit</string>
|
||||
<string name="donate">Přispět na projekt</string>
|
||||
<string name="stop_on_internet_loss">Zastavit tunel při ztrátě internetu</string>
|
||||
<string name="stop_on_no_internet">Zastavit při ztrátě internetu</string>
|
||||
<string name="native_kill_switch">Nativní kill switch</string>
|
||||
<string name="vpn_channel_description">Kanál pro oznámení o stavu VPN</string>
|
||||
@@ -144,7 +141,6 @@
|
||||
<string name="vpn_denied_dialog_title">Oprávnění zamítnuto</string>
|
||||
<string name="app_permission_title">Ovládání tunelů a funkcí automatického tunelování</string>
|
||||
<string name="app_permission_description">https://hosted.weblate.org/translate/wg-tunnel/strings/en/?checksum=e52d7eb2e28a9a12 Ovládání funkcí tunelu a automatického tunelování.</string>
|
||||
<string name="enable_remote_app_control">Povolit vzdálené ovládání aplikace</string>
|
||||
<string name="export_success">Export byl úspěšně dokončen</string>
|
||||
<string name="download">Stáhnout</string>
|
||||
<string name="check_for_update">Zkontrolovat aktualizaci</string>
|
||||
@@ -249,7 +245,7 @@
|
||||
<string name="root_required_template">%1$s (vyžaduje root)</string>
|
||||
<string name="recommended_template">%1$s (doporučeno)</string>
|
||||
<string name="hint_template">(%1$s)</string>
|
||||
<string name="backup_success">Úspěšně zazálohováno. %1$s</string>
|
||||
<string name="backup_success">Úspěšně zazálohováno</string>
|
||||
<string name="config_error_template">Špatná konfigurace. %1$s v umístění: %2$s.</string>
|
||||
<string name="donation_dev_message">Jako jediný vývojář neúnavně pracuji na tom, aby se WG Tunnel stal nejlepším bezplatným a open-source WireGuard klientem pro Android, ale to je možné pouze s vaší podporou.</string>
|
||||
<string name="google_donation_message">Bohužel, kvůli pravidlům společnosti Google nejsou odkazy na darování povoleny ve verzi této aplikace z Obchodu Play. Projděte si prosím webové stránky projektu, abyste zjistili, kde můžete přispět.</string>
|
||||
@@ -276,7 +272,6 @@
|
||||
<string name="back">Zpět</string>
|
||||
<string name="already_donated">Již darováno</string>
|
||||
<string name="selected">Vybrané</string>
|
||||
<string name="global_split_tunneling">Globální dělené tunelování</string>
|
||||
<string name="active_network">Aktivní síť:</string>
|
||||
<string name="delete_active_message">Aktivní tunel nelze odstranit.</string>
|
||||
<string name="help_translate">Pomozte s překladem aplikace</string>
|
||||
@@ -284,7 +279,6 @@
|
||||
<string name="other">Ostatní</string>
|
||||
<string name="kill_switch">kill switch</string>
|
||||
<string name="configuration">Konfigurace</string>
|
||||
<string name="global_dns_servers">Globální DNS servery</string>
|
||||
<string name="contact">Kontakt</string>
|
||||
<string name="backup_and_restore">Zálohování a obnovení</string>
|
||||
<string name="about">O aplikaci</string>
|
||||
@@ -357,4 +351,112 @@
|
||||
<string name="github_sponsors_url" translatable="false">https://github.com/sponsors/zaneschepke</string>
|
||||
<string name="transport_packet_junk_size">Transport packet junk velikost</string>
|
||||
<string name="cookie_packet_junk_size">Cookie packet junk velikost</string>
|
||||
<string name="fallback_to_ipv4">Fallback to IPv4</string>
|
||||
<string name="excluded_apps">%1$s apps excluded</string>
|
||||
<string name="kernel_wireguard_unsupported">Kernel does not include WireGuard support (module missing or not built-in into kernel)!</string>
|
||||
<string name="resolution_method">Resolution method</string>
|
||||
<string name="notification_ipv6_recovery_message">%1$s recovered IPv6 connectivity</string>
|
||||
<string name="tunnel_state_resolving_dns">Resolving DNS</string>
|
||||
<string name="handshake_template">handshake: %1$s</string>
|
||||
<string name="peer_template">peer: %1$s</string>
|
||||
<string name="errors">Errors</string>
|
||||
<string name="no_system_dns_information">No system DNS information</string>
|
||||
<string name="export_unsupported">Export is not supported on this device</string>
|
||||
<string name="tunnel_globals">Tunnel globals</string>
|
||||
<string name="sort_by_latency">Sort by latency</string>
|
||||
<string name="ready">Ready</string>
|
||||
<string name="no_system_dns_detected">No system DNS detected</string>
|
||||
<string name="name_error_empty">Tunnel name cannot be empty</string>
|
||||
<string name="notification_ipv4_fallback_message">%1$s switched to IPv4 connectivity</string>
|
||||
<string name="notification_tunnel_status_format">%1$s • %2$s</string>
|
||||
<string name="dns_error_invalid_ip_or_host">Invalid IP address or hostname</string>
|
||||
<string name="proxy_channel_description">Notifications for proxy tunnels.</string>
|
||||
<string name="system_dns_servers">Servers: %1$s</string>
|
||||
<string name="balanced">Balanced (3s)</string>
|
||||
<string name="import_url_description">The URL must be secure and serve a .conf file.</string>
|
||||
<string name="dns_error_invalid_port">Port must be between 1 and 65535</string>
|
||||
<string name="view_configuration">View configuration</string>
|
||||
<string name="dot">DNS over TLS (DoT)</string>
|
||||
<string name="ipv6_recovery">IPv6 recovery</string>
|
||||
<string name="screen_recording_protection">Screen recording protection</string>
|
||||
<string name="dns_error_invalid_host">Host cannot be empty</string>
|
||||
<string name="toggle_sensitive_data_visibility">Toggle sensitive data visibility</string>
|
||||
<string name="ipv4_fallback">IPv4 fallback</string>
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
<string name="app_selection">App selection</string>
|
||||
<string name="tunnel_state_handshake_failure">Handshake failure</string>
|
||||
<string name="notification_dynamic_dns_message">%1$s updated after Dynamic DNS change</string>
|
||||
<string name="tunnel_state_establishing_connection">Establishing connection</string>
|
||||
<string name="prefer_ipv6">Prefer IPv6</string>
|
||||
<string name="peer_endpoints">Peer endpoints</string>
|
||||
<string name="dns_endpoint_hint">IP, hostname, or DoH URL</string>
|
||||
<string name="restore_ipv6">Restore IPv6</string>
|
||||
<string name="network">Network</string>
|
||||
<string name="fallback_to_ipv4_desc">Switch to IPv4 if IPv6 fails, without restarting the tunnel.</string>
|
||||
<string name="tunnel_state_disconnected">Disconnected</string>
|
||||
<string name="live">Real-time (1s)</string>
|
||||
<string name="uptime_template">uptime: %1$s</string>
|
||||
<string name="events">Events</string>
|
||||
<string name="peer_resolution">Peer Resolution</string>
|
||||
<string name="restore_ipv6_desc">Switch back to IPv6 when an IPv6 network is detected.</string>
|
||||
<string name="errors_channel_description">A channel for application and tunnel errors</string>
|
||||
<string name="vpn_permission_required">VPN permission required</string>
|
||||
<string name="app_channel_description">A channel for general application notifications, like version updates</string>
|
||||
<string name="automation">Automation</string>
|
||||
<string name="balance_saver">Battery Saver (10s)</string>
|
||||
<string name="pinging_servers">Pinging servers…</string>
|
||||
<string name="dynamic_dns_update">Dynamic DNS update</string>
|
||||
<string name="statistics">Statistics</string>
|
||||
<string name="events_channel_description">A channel for app events, like automation event</string>
|
||||
<string name="private_dns_automatic">Private DNS: automatic</string>
|
||||
<string name="tunnel_statistics">Live tunnel statistics</string>
|
||||
<string name="ipv6_settings">IPv6 settings</string>
|
||||
<string name="error">Error</string>
|
||||
<string name="dns_error_invalid_url">Invalid URL format</string>
|
||||
<string name="view_live_tunnel">View live tunnel</string>
|
||||
<string name="mode">Mode</string>
|
||||
<string name="tunnel_state_starting">Starting</string>
|
||||
<string name="dns_endpoint_label">DNS server endpoint</string>
|
||||
<string name="plain_dns">Plain DNS (port 53)</string>
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
<string name="export_canceled">Export canceled</string>
|
||||
<string name="prefer_ipv6_desc">Use IPv6 endpoints when the network supports it.</string>
|
||||
<string name="stop_all">Stop all</string>
|
||||
<string name="copy_from">Copy from</string>
|
||||
<string name="special_junk_packet">Special junk packet</string>
|
||||
<string name="error_socks5_port_unavailable">SOCKS5 port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="tunnel_scripting">Pre/Post script support</string>
|
||||
<string name="initializing">Initializing…</string>
|
||||
<string name="errors_channel_id" translatable="false">Errors Channel</string>
|
||||
<string name="s2" translatable="false">S2</string>
|
||||
<string name="jc" translatable="false">Jc</string>
|
||||
<string name="i2" translatable="false">I2</string>
|
||||
<string name="events_channel_id" translatable="false">Events Channel</string>
|
||||
<string name="proxy_channel_id" translatable="false">Proxy Channel</string>
|
||||
<string name="jmax" translatable="false">Jmax</string>
|
||||
<string name="h2" translatable="false">H2</string>
|
||||
<string name="s3" translatable="false">S3</string>
|
||||
<string name="i1" translatable="false">I1</string>
|
||||
<string name="app_channel_id" translatable="false">App Channel</string>
|
||||
<string name="i3" translatable="false">I3</string>
|
||||
<string name="example_import_url" translatable="false">https://123.com/tun.conf</string>
|
||||
<string name="i5" translatable="false">I5</string>
|
||||
<string name="s4" translatable="false">S4</string>
|
||||
<string name="fdroid_url" translatable="false">https://apt.izzysoft.de/fdroid/index/apk/com.zaneschepke.wireguardautotunnel</string>
|
||||
<string name="h3" translatable="false">H3</string>
|
||||
<string name="h1" translatable="false">H1</string>
|
||||
<string name="jmin" translatable="false">Jmin</string>
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
</resources>
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
<string name="delete_tunnel">Delete tunnel</string>
|
||||
<string name="tunnel_mobile_data">Tunnel on mobile data</string>
|
||||
<string name="logs">Logs</string>
|
||||
<string name="enable_app_lock">Enable app lock</string>
|
||||
<string name="config_changes_saved">Configuration changes saved.</string>
|
||||
<string name="join_telegram">Join Telegram community</string>
|
||||
<string name="pin_created">Pin successfully created</string>
|
||||
@@ -29,7 +28,6 @@
|
||||
<string name="remote_key">Remote key</string>
|
||||
<string name="mobile_data">Mobile data</string>
|
||||
<string name="use_shell_via_shizuku">Use shell via Shizuku to get Wi-Fi information, preventing the need for location permission on non-rooted devices</string>
|
||||
<string name="stop_on_internet_loss">Stop tunnel on internet loss</string>
|
||||
<string name="vpn">VPN</string>
|
||||
<string name="tunnel_boot_description">Start the default tunnel on boot</string>
|
||||
<string name="allow_lan_traffic">Allow LAN traffic</string>
|
||||
@@ -62,7 +60,7 @@
|
||||
<string name="wifi_settings">Wi-Fi settings</string>
|
||||
<string name="tunnel_on_wifi">Tunnel on Wi-Fi</string>
|
||||
<string name="add_peer">Add peer</string>
|
||||
<string name="backup_success">Backup success. %1$s</string>
|
||||
<string name="backup_success">Backup success</string>
|
||||
<string name="persistent_keepalive">Persistent keepalive</string>
|
||||
<string name="info">Info</string>
|
||||
<string name="exclude">Exclude</string>
|
||||
@@ -135,7 +133,6 @@
|
||||
<string name="kofi">Ko-fi</string>
|
||||
<string name="donation_signoff">Gratefully,</string>
|
||||
<string name="selected">Selected</string>
|
||||
<string name="global_split_tunneling">Global split tunneling</string>
|
||||
<string name="active_network">Active Network:</string>
|
||||
<string name="range_hint">(%1$d–%2$d)</string>
|
||||
<string name="delete_active_message">Cannot delete active tunnel.</string>
|
||||
@@ -158,7 +155,6 @@
|
||||
<string name="mtu">MTU</string>
|
||||
<string name="configuration">Configuration</string>
|
||||
<string name="drag_handle">Drag Handle</string>
|
||||
<string name="global_dns_servers">Global DNS servers</string>
|
||||
<string name="display_theme">Display theme</string>
|
||||
<string name="contact">Contact</string>
|
||||
<string name="ports_must_differ">Failed. Proxies must have different ports.</string>
|
||||
@@ -258,7 +254,6 @@
|
||||
<string name="unavailable_in_mode">Unavailable in current mode</string>
|
||||
<string name="server_port">Server:Port</string>
|
||||
<string name="camera_permission_required">Camera permission required</string>
|
||||
<string name="enabled_app_shortcuts">Enable app shortcuts</string>
|
||||
<string name="preferred_tunnel">Preferred tunnel</string>
|
||||
<string name="allow">Allow</string>
|
||||
<string name="underload_packet_magic_header">Underload packet magic header</string>
|
||||
@@ -273,7 +268,6 @@
|
||||
<string name="settings">Settings</string>
|
||||
<string name="incorrect_pin">Pin is incorrect</string>
|
||||
<string name="export_failed">Export failed</string>
|
||||
<string name="enable_remote_app_control">Enable remote app control</string>
|
||||
<string name="donation_closing">It\'s my dream to work for you on this project full-time.</string>
|
||||
<string name="update_download_failed">Update download failed.</string>
|
||||
<string name="network_name">Network:</string>
|
||||
@@ -357,4 +351,112 @@
|
||||
<string name="github_sponsors_url" translatable="false">https://github.com/sponsors/zaneschepke</string>
|
||||
<string name="transport_packet_junk_size">Transport packet junk size</string>
|
||||
<string name="cookie_packet_junk_size">Cookie packet junk size</string>
|
||||
<string name="fallback_to_ipv4">Fallback to IPv4</string>
|
||||
<string name="excluded_apps">%1$s apps excluded</string>
|
||||
<string name="kernel_wireguard_unsupported">Kernel does not include WireGuard support (module missing or not built-in into kernel)!</string>
|
||||
<string name="resolution_method">Resolution method</string>
|
||||
<string name="notification_ipv6_recovery_message">%1$s recovered IPv6 connectivity</string>
|
||||
<string name="tunnel_state_resolving_dns">Resolving DNS</string>
|
||||
<string name="handshake_template">handshake: %1$s</string>
|
||||
<string name="peer_template">peer: %1$s</string>
|
||||
<string name="errors">Errors</string>
|
||||
<string name="no_system_dns_information">No system DNS information</string>
|
||||
<string name="export_unsupported">Export is not supported on this device</string>
|
||||
<string name="tunnel_globals">Tunnel globals</string>
|
||||
<string name="sort_by_latency">Sort by latency</string>
|
||||
<string name="ready">Ready</string>
|
||||
<string name="no_system_dns_detected">No system DNS detected</string>
|
||||
<string name="name_error_empty">Tunnel name cannot be empty</string>
|
||||
<string name="notification_ipv4_fallback_message">%1$s switched to IPv4 connectivity</string>
|
||||
<string name="notification_tunnel_status_format">%1$s • %2$s</string>
|
||||
<string name="dns_error_invalid_ip_or_host">Invalid IP address or hostname</string>
|
||||
<string name="proxy_channel_description">Notifications for proxy tunnels.</string>
|
||||
<string name="system_dns_servers">Servers: %1$s</string>
|
||||
<string name="balanced">Balanced (3s)</string>
|
||||
<string name="import_url_description">The URL must be secure and serve a .conf file.</string>
|
||||
<string name="dns_error_invalid_port">Port must be between 1 and 65535</string>
|
||||
<string name="view_configuration">View configuration</string>
|
||||
<string name="dot">DNS over TLS (DoT)</string>
|
||||
<string name="ipv6_recovery">IPv6 recovery</string>
|
||||
<string name="screen_recording_protection">Screen recording protection</string>
|
||||
<string name="dns_error_invalid_host">Host cannot be empty</string>
|
||||
<string name="toggle_sensitive_data_visibility">Toggle sensitive data visibility</string>
|
||||
<string name="ipv4_fallback">IPv4 fallback</string>
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
<string name="app_selection">App selection</string>
|
||||
<string name="tunnel_state_handshake_failure">Handshake failure</string>
|
||||
<string name="notification_dynamic_dns_message">%1$s updated after Dynamic DNS change</string>
|
||||
<string name="tunnel_state_establishing_connection">Establishing connection</string>
|
||||
<string name="prefer_ipv6">Prefer IPv6</string>
|
||||
<string name="peer_endpoints">Peer endpoints</string>
|
||||
<string name="dns_endpoint_hint">IP, hostname, or DoH URL</string>
|
||||
<string name="restore_ipv6">Restore IPv6</string>
|
||||
<string name="network">Network</string>
|
||||
<string name="fallback_to_ipv4_desc">Switch to IPv4 if IPv6 fails, without restarting the tunnel.</string>
|
||||
<string name="tunnel_state_disconnected">Disconnected</string>
|
||||
<string name="live">Real-time (1s)</string>
|
||||
<string name="uptime_template">uptime: %1$s</string>
|
||||
<string name="events">Events</string>
|
||||
<string name="peer_resolution">Peer Resolution</string>
|
||||
<string name="restore_ipv6_desc">Switch back to IPv6 when an IPv6 network is detected.</string>
|
||||
<string name="errors_channel_description">A channel for application and tunnel errors</string>
|
||||
<string name="vpn_permission_required">VPN permission required</string>
|
||||
<string name="app_channel_description">A channel for general application notifications, like version updates</string>
|
||||
<string name="automation">Automation</string>
|
||||
<string name="balance_saver">Battery Saver (10s)</string>
|
||||
<string name="pinging_servers">Pinging servers…</string>
|
||||
<string name="dynamic_dns_update">Dynamic DNS update</string>
|
||||
<string name="statistics">Statistics</string>
|
||||
<string name="events_channel_description">A channel for app events, like automation event</string>
|
||||
<string name="private_dns_automatic">Private DNS: automatic</string>
|
||||
<string name="tunnel_statistics">Live tunnel statistics</string>
|
||||
<string name="ipv6_settings">IPv6 settings</string>
|
||||
<string name="error">Error</string>
|
||||
<string name="dns_error_invalid_url">Invalid URL format</string>
|
||||
<string name="view_live_tunnel">View live tunnel</string>
|
||||
<string name="mode">Mode</string>
|
||||
<string name="tunnel_state_starting">Starting</string>
|
||||
<string name="dns_endpoint_label">DNS server endpoint</string>
|
||||
<string name="plain_dns">Plain DNS (port 53)</string>
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
<string name="export_canceled">Export canceled</string>
|
||||
<string name="prefer_ipv6_desc">Use IPv6 endpoints when the network supports it.</string>
|
||||
<string name="stop_all">Stop all</string>
|
||||
<string name="copy_from">Copy from</string>
|
||||
<string name="special_junk_packet">Special junk packet</string>
|
||||
<string name="error_socks5_port_unavailable">SOCKS5 port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="tunnel_scripting">Pre/Post script support</string>
|
||||
<string name="initializing">Initializing…</string>
|
||||
<string name="errors_channel_id" translatable="false">Errors Channel</string>
|
||||
<string name="s2" translatable="false">S2</string>
|
||||
<string name="jc" translatable="false">Jc</string>
|
||||
<string name="i2" translatable="false">I2</string>
|
||||
<string name="events_channel_id" translatable="false">Events Channel</string>
|
||||
<string name="proxy_channel_id" translatable="false">Proxy Channel</string>
|
||||
<string name="jmax" translatable="false">Jmax</string>
|
||||
<string name="h2" translatable="false">H2</string>
|
||||
<string name="s3" translatable="false">S3</string>
|
||||
<string name="i1" translatable="false">I1</string>
|
||||
<string name="app_channel_id" translatable="false">App Channel</string>
|
||||
<string name="i3" translatable="false">I3</string>
|
||||
<string name="example_import_url" translatable="false">https://123.com/tun.conf</string>
|
||||
<string name="i5" translatable="false">I5</string>
|
||||
<string name="s4" translatable="false">S4</string>
|
||||
<string name="fdroid_url" translatable="false">https://apt.izzysoft.de/fdroid/index/apk/com.zaneschepke.wireguardautotunnel</string>
|
||||
<string name="h3" translatable="false">H3</string>
|
||||
<string name="h1" translatable="false">H1</string>
|
||||
<string name="jmin" translatable="false">Jmin</string>
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
</resources>
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
<string name="base64_key">base64-Schlüssel</string>
|
||||
<string name="delete_tunnel">Tunnel löschen</string>
|
||||
<string name="persistent_keepalive">Dauerhaftes Keepalive</string>
|
||||
<string name="enable_app_lock">App-Sperre aktivieren</string>
|
||||
<string name="interface_">Schnittstelle</string>
|
||||
<string name="listen_port">Eingehender Port</string>
|
||||
<string name="random">(zufällig)</string>
|
||||
@@ -45,7 +44,6 @@
|
||||
<string name="seconds">Sekunden</string>
|
||||
<string name="cancel">Abbrechen</string>
|
||||
<string name="preshared_key">Geteilter Schlüssel</string>
|
||||
<string name="enabled_app_shortcuts">App-Verknüpfungen aktivieren</string>
|
||||
<string name="tunnel_on_wifi">Tunnel bei WLAN</string>
|
||||
<string name="email_subject">WG Tunnel Unterstützung</string>
|
||||
<string name="docs_description">Dokumentation lesen</string>
|
||||
@@ -125,7 +123,6 @@
|
||||
<string name="pre_down">Vor Deaktivierung</string>
|
||||
<string name="post_down">Nach Deaktivierung</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="hide_scripts">Skripte verbergen</string>
|
||||
<string name="enable_amnezia_compatibility">Amnezia Kompatibilität aktivieren</string>
|
||||
@@ -141,6 +138,7 @@
|
||||
<string name="config_error">Ungültige Konfiguration</string>
|
||||
<string name="join_matrix">Matrix-Community beitreten</string>
|
||||
<string name="error_download_failed">Download der Konfiguration fehlgeschlagen</string>
|
||||
<string name="wg_url_confirm_message">Möchtest du wirklich Tunnel von %1$s hinzufügen? Verbinde dich niemals mit einem nicht vertrauenswürdigen VPN!</string>
|
||||
<string name="add_from_url">Von URL hinzufügen</string>
|
||||
<string name="export_logs">Gespeicherte Logs exportieren</string>
|
||||
<string name="app_permission_title">Steuere Tunnel und Auto-Tunnel Funktionen.</string>
|
||||
@@ -153,7 +151,6 @@
|
||||
<string name="copy">Kopieren</string>
|
||||
<string name="service_running_error">Dienst läuft nicht</string>
|
||||
<string name="auth_error">Nicht autorisiert</string>
|
||||
<string name="enable_remote_app_control">App-Fernsteuerung aktivieren</string>
|
||||
<string name="export_failed">Export fehlgeschlagen</string>
|
||||
<string name="app_permission_description">https://hosted.weblate.org/translate/wg-tunnel/strings/en/?checksum=e52d7eb2e28a9a12Steuere Tunnel und Auto-Tunnel Funktionen.</string>
|
||||
<string name="dns_resolve_error">DNS-Auflösung fehlgeschlagen</string>
|
||||
@@ -206,7 +203,7 @@
|
||||
<string name="auto_tunnel_running">Auto-Tunnel läuft</string>
|
||||
<string name="auto_tunnel_not_running">Auto-Tunnel läuft nicht</string>
|
||||
<string name="tunnel_monitoring">Tunnelüberwachung</string>
|
||||
<string name="backup_success">Backuperfolg. %1$s</string>
|
||||
<string name="backup_success">Backuperfolg</string>
|
||||
<string name="restore_success">Wiederherstellerfolg. %1$s</string>
|
||||
<string name="restarting_app">Starte App neu, um Änderungen anzuwenden …</string>
|
||||
<string name="restore_failed">Wiederherstellung aus Backup fehlgeschlagen.</string>
|
||||
@@ -281,14 +278,12 @@
|
||||
<string name="resources">Resourcen</string>
|
||||
<string name="back">Zurück</string>
|
||||
<string name="already_donated">Bereits gespendet</string>
|
||||
<string name="global_split_tunneling">Globales Split-Tunneling</string>
|
||||
<string name="active_network">Aktives Netzwerk:</string>
|
||||
<string name="help_translate">Hilf mit, die App zu übersetzen</string>
|
||||
<string name="ethernet">Ethernet</string>
|
||||
<string name="other">Sonstiges</string>
|
||||
<string name="kill_switch">Notschalter</string>
|
||||
<string name="configuration">Konfiguration</string>
|
||||
<string name="global_dns_servers">Globale DNS Server</string>
|
||||
<string name="contact">Kontakt</string>
|
||||
<string name="backup_and_restore">Sichern und Wiederherstellen</string>
|
||||
<string name="about">Über</string>
|
||||
@@ -357,4 +352,112 @@
|
||||
<string name="github_sponsors_url" translatable="false">https://github.com/sponsors/zaneschepke</string>
|
||||
<string name="transport_packet_junk_size">Transport-Paket Junk Größe</string>
|
||||
<string name="cookie_packet_junk_size">Cookie-Paket Junk Größe</string>
|
||||
<string name="fallback_to_ipv4">Fallback to IPv4</string>
|
||||
<string name="excluded_apps">%1$s apps excluded</string>
|
||||
<string name="kernel_wireguard_unsupported">Kernel does not include WireGuard support (module missing or not built-in into kernel)!</string>
|
||||
<string name="resolution_method">Resolution method</string>
|
||||
<string name="notification_ipv6_recovery_message">%1$s recovered IPv6 connectivity</string>
|
||||
<string name="tunnel_state_resolving_dns">Resolving DNS</string>
|
||||
<string name="handshake_template">handshake: %1$s</string>
|
||||
<string name="peer_template">peer: %1$s</string>
|
||||
<string name="errors">Errors</string>
|
||||
<string name="no_system_dns_information">No system DNS information</string>
|
||||
<string name="export_unsupported">Export is not supported on this device</string>
|
||||
<string name="tunnel_globals">Tunnel globals</string>
|
||||
<string name="sort_by_latency">Sort by latency</string>
|
||||
<string name="ready">Ready</string>
|
||||
<string name="no_system_dns_detected">No system DNS detected</string>
|
||||
<string name="name_error_empty">Tunnel name cannot be empty</string>
|
||||
<string name="notification_ipv4_fallback_message">%1$s switched to IPv4 connectivity</string>
|
||||
<string name="notification_tunnel_status_format">%1$s • %2$s</string>
|
||||
<string name="dns_error_invalid_ip_or_host">Invalid IP address or hostname</string>
|
||||
<string name="proxy_channel_description">Notifications for proxy tunnels.</string>
|
||||
<string name="system_dns_servers">Servers: %1$s</string>
|
||||
<string name="balanced">Balanced (3s)</string>
|
||||
<string name="import_url_description">The URL must be secure and serve a .conf file.</string>
|
||||
<string name="dns_error_invalid_port">Port must be between 1 and 65535</string>
|
||||
<string name="view_configuration">View configuration</string>
|
||||
<string name="dot">DNS over TLS (DoT)</string>
|
||||
<string name="ipv6_recovery">IPv6 recovery</string>
|
||||
<string name="screen_recording_protection">Screen recording protection</string>
|
||||
<string name="dns_error_invalid_host">Host cannot be empty</string>
|
||||
<string name="toggle_sensitive_data_visibility">Toggle sensitive data visibility</string>
|
||||
<string name="ipv4_fallback">IPv4 fallback</string>
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
<string name="app_selection">App selection</string>
|
||||
<string name="tunnel_state_handshake_failure">Handshake failure</string>
|
||||
<string name="notification_dynamic_dns_message">%1$s updated after Dynamic DNS change</string>
|
||||
<string name="tunnel_state_establishing_connection">Establishing connection</string>
|
||||
<string name="prefer_ipv6">Prefer IPv6</string>
|
||||
<string name="peer_endpoints">Peer endpoints</string>
|
||||
<string name="dns_endpoint_hint">IP, hostname, or DoH URL</string>
|
||||
<string name="restore_ipv6">Restore IPv6</string>
|
||||
<string name="network">Network</string>
|
||||
<string name="fallback_to_ipv4_desc">Switch to IPv4 if IPv6 fails, without restarting the tunnel.</string>
|
||||
<string name="tunnel_state_disconnected">Disconnected</string>
|
||||
<string name="live">Real-time (1s)</string>
|
||||
<string name="uptime_template">uptime: %1$s</string>
|
||||
<string name="events">Events</string>
|
||||
<string name="peer_resolution">Peer Resolution</string>
|
||||
<string name="restore_ipv6_desc">Switch back to IPv6 when an IPv6 network is detected.</string>
|
||||
<string name="errors_channel_description">A channel for application and tunnel errors</string>
|
||||
<string name="vpn_permission_required">VPN permission required</string>
|
||||
<string name="app_channel_description">A channel for general application notifications, like version updates</string>
|
||||
<string name="automation">Automation</string>
|
||||
<string name="balance_saver">Battery Saver (10s)</string>
|
||||
<string name="pinging_servers">Pinging servers…</string>
|
||||
<string name="dynamic_dns_update">Dynamic DNS update</string>
|
||||
<string name="statistics">Statistics</string>
|
||||
<string name="events_channel_description">A channel for app events, like automation event</string>
|
||||
<string name="private_dns_automatic">Private DNS: automatic</string>
|
||||
<string name="tunnel_statistics">Live tunnel statistics</string>
|
||||
<string name="ipv6_settings">IPv6 settings</string>
|
||||
<string name="error">Error</string>
|
||||
<string name="dns_error_invalid_url">Invalid URL format</string>
|
||||
<string name="view_live_tunnel">View live tunnel</string>
|
||||
<string name="mode">Mode</string>
|
||||
<string name="tunnel_state_starting">Starting</string>
|
||||
<string name="dns_endpoint_label">DNS server endpoint</string>
|
||||
<string name="plain_dns">Plain DNS (port 53)</string>
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
<string name="export_canceled">Export canceled</string>
|
||||
<string name="prefer_ipv6_desc">Use IPv6 endpoints when the network supports it.</string>
|
||||
<string name="stop_all">Stop all</string>
|
||||
<string name="copy_from">Copy from</string>
|
||||
<string name="special_junk_packet">Special junk packet</string>
|
||||
<string name="error_socks5_port_unavailable">SOCKS5 port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="tunnel_scripting">Pre/Post script support</string>
|
||||
<string name="initializing">Initializing…</string>
|
||||
<string name="errors_channel_id" translatable="false">Errors Channel</string>
|
||||
<string name="s2" translatable="false">S2</string>
|
||||
<string name="jc" translatable="false">Jc</string>
|
||||
<string name="i2" translatable="false">I2</string>
|
||||
<string name="events_channel_id" translatable="false">Events Channel</string>
|
||||
<string name="proxy_channel_id" translatable="false">Proxy Channel</string>
|
||||
<string name="jmax" translatable="false">Jmax</string>
|
||||
<string name="h2" translatable="false">H2</string>
|
||||
<string name="s3" translatable="false">S3</string>
|
||||
<string name="i1" translatable="false">I1</string>
|
||||
<string name="app_channel_id" translatable="false">App Channel</string>
|
||||
<string name="i3" translatable="false">I3</string>
|
||||
<string name="example_import_url" translatable="false">https://123.com/tun.conf</string>
|
||||
<string name="i5" translatable="false">I5</string>
|
||||
<string name="s4" translatable="false">S4</string>
|
||||
<string name="fdroid_url" translatable="false">https://apt.izzysoft.de/fdroid/index/apk/com.zaneschepke.wireguardautotunnel</string>
|
||||
<string name="h3" translatable="false">H3</string>
|
||||
<string name="h1" translatable="false">H1</string>
|
||||
<string name="jmin" translatable="false">Jmin</string>
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
</resources>
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
<string name="optional">(opcional)</string>
|
||||
<string name="seconds">Segundos</string>
|
||||
<string name="cancel">Cancelar</string>
|
||||
<string name="enabled_app_shortcuts">Habilitar acesos directos de app</string>
|
||||
<string name="unknown_error">Error desconocido</string>
|
||||
<string name="email_subject">Ayuda de WG Tunnel</string>
|
||||
<string name="interface_">Interfaz</string>
|
||||
@@ -65,7 +64,6 @@
|
||||
<string name="pin_created">Pin creado con éxito</string>
|
||||
<string name="enter_pin">Introduce tu pin</string>
|
||||
<string name="create_pin">Crear pin</string>
|
||||
<string name="enable_app_lock">Activar el bloqueo de aplicaciones</string>
|
||||
<string name="set_primary_tunnel">Establecer como túnel Principal</string>
|
||||
<string name="edit_tunnel">Editar túnel</string>
|
||||
<string name="settings">Ajustes</string>
|
||||
@@ -121,7 +119,6 @@
|
||||
<string name="learn_more">Saber 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="native_kill_switch">Interruptor de apagado nativo</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>
|
||||
@@ -166,7 +163,6 @@
|
||||
<string name="check_for_update">Comprobar actualización</string>
|
||||
<string name="update_check_failed">Fallo al comprobar la actualización.</string>
|
||||
<string name="flavor_template">Variante: %1$s</string>
|
||||
<string name="enable_remote_app_control">Activar control remoto de la app</string>
|
||||
<string name="export_success">Exportación completada</string>
|
||||
<string name="download">Descargar</string>
|
||||
<string name="latest_installed">Ya se está ejecutando la última versión.</string>
|
||||
@@ -206,7 +202,7 @@
|
||||
<string name="auto_tunnel_running">El túnel automático está activo</string>
|
||||
<string name="auto_tunnel_not_running">El túnel automático no está activo</string>
|
||||
<string name="tunnel_monitoring">Monitoreo del túnel</string>
|
||||
<string name="backup_success">Copia de seguridad realizada con éxito. %1$s</string>
|
||||
<string name="backup_success">Copia de seguridad realizada con éxito</string>
|
||||
<string name="restore_success">Restauración completada. %1$s</string>
|
||||
<string name="restarting_app">Reiniciando la app para aplicar los cambios…</string>
|
||||
<string name="restore_failed">No se pudo restaurar la copia de seguridad.</string>
|
||||
@@ -280,14 +276,12 @@
|
||||
<string name="resources">Recursos</string>
|
||||
<string name="back">Atrás</string>
|
||||
<string name="already_donated">Ya he donado</string>
|
||||
<string name="global_split_tunneling">Túnel dividido global</string>
|
||||
<string name="active_network">Red activa:</string>
|
||||
<string name="help_translate">Ayuda a traducir la app</string>
|
||||
<string name="ethernet">Ethernet</string>
|
||||
<string name="other">Otros</string>
|
||||
<string name="kill_switch">kill switch</string>
|
||||
<string name="configuration">Configuración</string>
|
||||
<string name="global_dns_servers">Servidores DNS globales</string>
|
||||
<string name="contact">Contacto</string>
|
||||
<string name="backup_and_restore">Copia de seguridad/Restaurar</string>
|
||||
<string name="about">Acerca de</string>
|
||||
@@ -357,4 +351,112 @@
|
||||
<string name="github_sponsors_url" translatable="false">https://github.com/sponsors/zaneschepke</string>
|
||||
<string name="transport_packet_junk_size">Tamaño de los datos «ruido» del paquete de transporte</string>
|
||||
<string name="cookie_packet_junk_size">Tamaño de los datos «ruido» del paquete de cookies</string>
|
||||
<string name="fallback_to_ipv4">Fallback to IPv4</string>
|
||||
<string name="excluded_apps">%1$s apps excluded</string>
|
||||
<string name="kernel_wireguard_unsupported">Kernel does not include WireGuard support (module missing or not built-in into kernel)!</string>
|
||||
<string name="resolution_method">Resolution method</string>
|
||||
<string name="notification_ipv6_recovery_message">%1$s recovered IPv6 connectivity</string>
|
||||
<string name="tunnel_state_resolving_dns">Resolving DNS</string>
|
||||
<string name="handshake_template">handshake: %1$s</string>
|
||||
<string name="peer_template">peer: %1$s</string>
|
||||
<string name="errors">Errors</string>
|
||||
<string name="no_system_dns_information">No system DNS information</string>
|
||||
<string name="export_unsupported">Export is not supported on this device</string>
|
||||
<string name="tunnel_globals">Tunnel globals</string>
|
||||
<string name="sort_by_latency">Sort by latency</string>
|
||||
<string name="ready">Ready</string>
|
||||
<string name="no_system_dns_detected">No system DNS detected</string>
|
||||
<string name="name_error_empty">Tunnel name cannot be empty</string>
|
||||
<string name="notification_ipv4_fallback_message">%1$s switched to IPv4 connectivity</string>
|
||||
<string name="notification_tunnel_status_format">%1$s • %2$s</string>
|
||||
<string name="dns_error_invalid_ip_or_host">Invalid IP address or hostname</string>
|
||||
<string name="proxy_channel_description">Notifications for proxy tunnels.</string>
|
||||
<string name="system_dns_servers">Servers: %1$s</string>
|
||||
<string name="balanced">Balanced (3s)</string>
|
||||
<string name="import_url_description">The URL must be secure and serve a .conf file.</string>
|
||||
<string name="dns_error_invalid_port">Port must be between 1 and 65535</string>
|
||||
<string name="view_configuration">View configuration</string>
|
||||
<string name="dot">DNS over TLS (DoT)</string>
|
||||
<string name="ipv6_recovery">IPv6 recovery</string>
|
||||
<string name="screen_recording_protection">Screen recording protection</string>
|
||||
<string name="dns_error_invalid_host">Host cannot be empty</string>
|
||||
<string name="toggle_sensitive_data_visibility">Toggle sensitive data visibility</string>
|
||||
<string name="ipv4_fallback">IPv4 fallback</string>
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
<string name="app_selection">App selection</string>
|
||||
<string name="tunnel_state_handshake_failure">Handshake failure</string>
|
||||
<string name="notification_dynamic_dns_message">%1$s updated after Dynamic DNS change</string>
|
||||
<string name="tunnel_state_establishing_connection">Establishing connection</string>
|
||||
<string name="prefer_ipv6">Prefer IPv6</string>
|
||||
<string name="peer_endpoints">Peer endpoints</string>
|
||||
<string name="dns_endpoint_hint">IP, hostname, or DoH URL</string>
|
||||
<string name="restore_ipv6">Restore IPv6</string>
|
||||
<string name="network">Network</string>
|
||||
<string name="fallback_to_ipv4_desc">Switch to IPv4 if IPv6 fails, without restarting the tunnel.</string>
|
||||
<string name="tunnel_state_disconnected">Disconnected</string>
|
||||
<string name="live">Real-time (1s)</string>
|
||||
<string name="uptime_template">uptime: %1$s</string>
|
||||
<string name="events">Events</string>
|
||||
<string name="peer_resolution">Peer Resolution</string>
|
||||
<string name="restore_ipv6_desc">Switch back to IPv6 when an IPv6 network is detected.</string>
|
||||
<string name="errors_channel_description">A channel for application and tunnel errors</string>
|
||||
<string name="vpn_permission_required">VPN permission required</string>
|
||||
<string name="app_channel_description">A channel for general application notifications, like version updates</string>
|
||||
<string name="automation">Automation</string>
|
||||
<string name="balance_saver">Battery Saver (10s)</string>
|
||||
<string name="pinging_servers">Pinging servers…</string>
|
||||
<string name="dynamic_dns_update">Dynamic DNS update</string>
|
||||
<string name="statistics">Statistics</string>
|
||||
<string name="events_channel_description">A channel for app events, like automation event</string>
|
||||
<string name="private_dns_automatic">Private DNS: automatic</string>
|
||||
<string name="tunnel_statistics">Live tunnel statistics</string>
|
||||
<string name="ipv6_settings">IPv6 settings</string>
|
||||
<string name="error">Error</string>
|
||||
<string name="dns_error_invalid_url">Invalid URL format</string>
|
||||
<string name="view_live_tunnel">View live tunnel</string>
|
||||
<string name="mode">Mode</string>
|
||||
<string name="tunnel_state_starting">Starting</string>
|
||||
<string name="dns_endpoint_label">DNS server endpoint</string>
|
||||
<string name="plain_dns">Plain DNS (port 53)</string>
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
<string name="export_canceled">Export canceled</string>
|
||||
<string name="prefer_ipv6_desc">Use IPv6 endpoints when the network supports it.</string>
|
||||
<string name="stop_all">Stop all</string>
|
||||
<string name="copy_from">Copy from</string>
|
||||
<string name="special_junk_packet">Special junk packet</string>
|
||||
<string name="error_socks5_port_unavailable">SOCKS5 port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="tunnel_scripting">Pre/Post script support</string>
|
||||
<string name="initializing">Initializing…</string>
|
||||
<string name="errors_channel_id" translatable="false">Errors Channel</string>
|
||||
<string name="s2" translatable="false">S2</string>
|
||||
<string name="jc" translatable="false">Jc</string>
|
||||
<string name="i2" translatable="false">I2</string>
|
||||
<string name="events_channel_id" translatable="false">Events Channel</string>
|
||||
<string name="proxy_channel_id" translatable="false">Proxy Channel</string>
|
||||
<string name="jmax" translatable="false">Jmax</string>
|
||||
<string name="h2" translatable="false">H2</string>
|
||||
<string name="s3" translatable="false">S3</string>
|
||||
<string name="i1" translatable="false">I1</string>
|
||||
<string name="app_channel_id" translatable="false">App Channel</string>
|
||||
<string name="i3" translatable="false">I3</string>
|
||||
<string name="example_import_url" translatable="false">https://123.com/tun.conf</string>
|
||||
<string name="i5" translatable="false">I5</string>
|
||||
<string name="s4" translatable="false">S4</string>
|
||||
<string name="fdroid_url" translatable="false">https://apt.izzysoft.de/fdroid/index/apk/com.zaneschepke.wireguardautotunnel</string>
|
||||
<string name="h3" translatable="false">H3</string>
|
||||
<string name="h1" translatable="false">H1</string>
|
||||
<string name="jmin" translatable="false">Jmin</string>
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
</resources>
|
||||
|
||||
@@ -47,7 +47,6 @@
|
||||
<string name="allow">Luba</string>
|
||||
<string name="select_all">Vali kõik</string>
|
||||
<string name="export_success">Eksportimine õnnestus</string>
|
||||
<string name="enable_remote_app_control">Luba rakenduse kaugjuhtimine</string>
|
||||
<string name="add_from_url">Lisa võrguaadressilt</string>
|
||||
<string name="enter_config_url">Sisesta seadistuse võrguaadress</string>
|
||||
<string name="error_download_failed">Seadistuse allalaadimine ei õnnestunud</string>
|
||||
@@ -151,8 +150,6 @@
|
||||
<string name="splt_tunneling">Jagatud tunneldus</string>
|
||||
<string name="stop">Peata</string>
|
||||
<string name="stop_on_no_internet">Peata internetiühenduse puudumisel</string>
|
||||
<string name="stop_on_internet_loss">Peata tunnel internetiühenduse kadumisel</string>
|
||||
<string name="enable_app_lock">Luba rakenduse lukustamine</string>
|
||||
<string name="launch_app_settings">Käivita rakenduse seadistused</string>
|
||||
<string name="use_wildcards">Kasuta nimedes metamärke</string>
|
||||
<string name="wildcards_active">Metamärgid on kasutusel</string>
|
||||
@@ -181,7 +178,6 @@
|
||||
<string name="_default">Vaikimisi meetod</string>
|
||||
<string name="wifi_detection_method">WiFi tuvastamise meetod</string>
|
||||
<string name="current_template">Praegune: %1$s</string>
|
||||
<string name="enabled_app_shortcuts">Luba rakenduse otseteed</string>
|
||||
<string name="auto_tunnel_title">Tunneli automaatne loomine</string>
|
||||
<string name="auto_tunnel_not_running">Tunneli automaatne käivitamine pole kasutusel</string>
|
||||
<string name="auto_tunnel_running">Tunneli automaatne käivitamine on kasutusel</string>
|
||||
@@ -198,7 +194,7 @@
|
||||
<string name="recommended_template">%1$s (soovitatav)</string>
|
||||
<string name="hint_template">(%1$s)</string>
|
||||
<string name="tunnel_monitoring">Tunnelo monitooring</string>
|
||||
<string name="backup_success">Varundus õnnestus. %1$s</string>
|
||||
<string name="backup_success">Varundus õnnestus</string>
|
||||
<string name="restore_success">Taastamine õnnestus. %1$s</string>
|
||||
<string name="restarting_app">Muudatuste jõustamiseks taaskäivitan rakenduse…</string>
|
||||
<string name="restore_failed">Varukoopiast taastamine ei õnnestunud.</string>
|
||||
@@ -280,14 +276,12 @@
|
||||
<string name="resources">Ressursid</string>
|
||||
<string name="back">Tagasi</string>
|
||||
<string name="already_donated">Juba annetasin</string>
|
||||
<string name="global_split_tunneling">Üldine jagatud tunneldus</string>
|
||||
<string name="active_network">Aktiivne võrk:</string>
|
||||
<string name="help_translate">Aita seda rakendust tõlkida</string>
|
||||
<string name="ethernet">Ethernet</string>
|
||||
<string name="other">Muu</string>
|
||||
<string name="kill_switch">kiirpeatamine</string>
|
||||
<string name="configuration">Seadistused</string>
|
||||
<string name="global_dns_servers">Üldised nimeserverid</string>
|
||||
<string name="contact">Kontakt</string>
|
||||
<string name="backup_and_restore">Varundus ja taastamine</string>
|
||||
<string name="about">Rakenduse teave</string>
|
||||
@@ -357,4 +351,112 @@
|
||||
<string name="github_sponsors_url" translatable="false">https://github.com/sponsors/zaneschepke</string>
|
||||
<string name="transport_packet_junk_size">Transpordipaketi rämpsuosa suurus</string>
|
||||
<string name="cookie_packet_junk_size">Küpsisepaketi rämpsuosa suurus</string>
|
||||
<string name="fallback_to_ipv4">Fallback to IPv4</string>
|
||||
<string name="excluded_apps">%1$s apps excluded</string>
|
||||
<string name="kernel_wireguard_unsupported">Kernel does not include WireGuard support (module missing or not built-in into kernel)!</string>
|
||||
<string name="resolution_method">Resolution method</string>
|
||||
<string name="notification_ipv6_recovery_message">%1$s recovered IPv6 connectivity</string>
|
||||
<string name="tunnel_state_resolving_dns">Resolving DNS</string>
|
||||
<string name="handshake_template">handshake: %1$s</string>
|
||||
<string name="peer_template">peer: %1$s</string>
|
||||
<string name="errors">Errors</string>
|
||||
<string name="no_system_dns_information">No system DNS information</string>
|
||||
<string name="export_unsupported">Export is not supported on this device</string>
|
||||
<string name="tunnel_globals">Tunnel globals</string>
|
||||
<string name="sort_by_latency">Sort by latency</string>
|
||||
<string name="ready">Ready</string>
|
||||
<string name="no_system_dns_detected">No system DNS detected</string>
|
||||
<string name="name_error_empty">Tunnel name cannot be empty</string>
|
||||
<string name="notification_ipv4_fallback_message">%1$s switched to IPv4 connectivity</string>
|
||||
<string name="notification_tunnel_status_format">%1$s • %2$s</string>
|
||||
<string name="dns_error_invalid_ip_or_host">Invalid IP address or hostname</string>
|
||||
<string name="proxy_channel_description">Notifications for proxy tunnels.</string>
|
||||
<string name="system_dns_servers">Servers: %1$s</string>
|
||||
<string name="balanced">Balanced (3s)</string>
|
||||
<string name="import_url_description">The URL must be secure and serve a .conf file.</string>
|
||||
<string name="dns_error_invalid_port">Port must be between 1 and 65535</string>
|
||||
<string name="view_configuration">View configuration</string>
|
||||
<string name="dot">DNS over TLS (DoT)</string>
|
||||
<string name="ipv6_recovery">IPv6 recovery</string>
|
||||
<string name="screen_recording_protection">Screen recording protection</string>
|
||||
<string name="dns_error_invalid_host">Host cannot be empty</string>
|
||||
<string name="toggle_sensitive_data_visibility">Toggle sensitive data visibility</string>
|
||||
<string name="ipv4_fallback">IPv4 fallback</string>
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
<string name="app_selection">App selection</string>
|
||||
<string name="tunnel_state_handshake_failure">Handshake failure</string>
|
||||
<string name="notification_dynamic_dns_message">%1$s updated after Dynamic DNS change</string>
|
||||
<string name="tunnel_state_establishing_connection">Establishing connection</string>
|
||||
<string name="prefer_ipv6">Prefer IPv6</string>
|
||||
<string name="peer_endpoints">Peer endpoints</string>
|
||||
<string name="dns_endpoint_hint">IP, hostname, or DoH URL</string>
|
||||
<string name="restore_ipv6">Restore IPv6</string>
|
||||
<string name="network">Network</string>
|
||||
<string name="fallback_to_ipv4_desc">Switch to IPv4 if IPv6 fails, without restarting the tunnel.</string>
|
||||
<string name="tunnel_state_disconnected">Disconnected</string>
|
||||
<string name="live">Real-time (1s)</string>
|
||||
<string name="uptime_template">uptime: %1$s</string>
|
||||
<string name="events">Events</string>
|
||||
<string name="peer_resolution">Peer Resolution</string>
|
||||
<string name="restore_ipv6_desc">Switch back to IPv6 when an IPv6 network is detected.</string>
|
||||
<string name="errors_channel_description">A channel for application and tunnel errors</string>
|
||||
<string name="vpn_permission_required">VPN permission required</string>
|
||||
<string name="app_channel_description">A channel for general application notifications, like version updates</string>
|
||||
<string name="automation">Automation</string>
|
||||
<string name="balance_saver">Battery Saver (10s)</string>
|
||||
<string name="pinging_servers">Pinging servers…</string>
|
||||
<string name="dynamic_dns_update">Dynamic DNS update</string>
|
||||
<string name="statistics">Statistics</string>
|
||||
<string name="events_channel_description">A channel for app events, like automation event</string>
|
||||
<string name="private_dns_automatic">Private DNS: automatic</string>
|
||||
<string name="tunnel_statistics">Live tunnel statistics</string>
|
||||
<string name="ipv6_settings">IPv6 settings</string>
|
||||
<string name="error">Error</string>
|
||||
<string name="dns_error_invalid_url">Invalid URL format</string>
|
||||
<string name="view_live_tunnel">View live tunnel</string>
|
||||
<string name="mode">Mode</string>
|
||||
<string name="tunnel_state_starting">Starting</string>
|
||||
<string name="dns_endpoint_label">DNS server endpoint</string>
|
||||
<string name="plain_dns">Plain DNS (port 53)</string>
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
<string name="export_canceled">Export canceled</string>
|
||||
<string name="prefer_ipv6_desc">Use IPv6 endpoints when the network supports it.</string>
|
||||
<string name="stop_all">Stop all</string>
|
||||
<string name="copy_from">Copy from</string>
|
||||
<string name="special_junk_packet">Special junk packet</string>
|
||||
<string name="error_socks5_port_unavailable">SOCKS5 port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="tunnel_scripting">Pre/Post script support</string>
|
||||
<string name="initializing">Initializing…</string>
|
||||
<string name="errors_channel_id" translatable="false">Errors Channel</string>
|
||||
<string name="s2" translatable="false">S2</string>
|
||||
<string name="jc" translatable="false">Jc</string>
|
||||
<string name="i2" translatable="false">I2</string>
|
||||
<string name="events_channel_id" translatable="false">Events Channel</string>
|
||||
<string name="proxy_channel_id" translatable="false">Proxy Channel</string>
|
||||
<string name="jmax" translatable="false">Jmax</string>
|
||||
<string name="h2" translatable="false">H2</string>
|
||||
<string name="s3" translatable="false">S3</string>
|
||||
<string name="i1" translatable="false">I1</string>
|
||||
<string name="app_channel_id" translatable="false">App Channel</string>
|
||||
<string name="i3" translatable="false">I3</string>
|
||||
<string name="example_import_url" translatable="false">https://123.com/tun.conf</string>
|
||||
<string name="i5" translatable="false">I5</string>
|
||||
<string name="s4" translatable="false">S4</string>
|
||||
<string name="fdroid_url" translatable="false">https://apt.izzysoft.de/fdroid/index/apk/com.zaneschepke.wireguardautotunnel</string>
|
||||
<string name="h3" translatable="false">H3</string>
|
||||
<string name="h1" translatable="false">H1</string>
|
||||
<string name="jmin" translatable="false">Jmin</string>
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
</resources>
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
<string name="save">Save</string>
|
||||
<string name="delete_tunnel">Delete tunnel</string>
|
||||
<string name="logs">Logs</string>
|
||||
<string name="enable_app_lock">Enable app lock</string>
|
||||
<string name="config_changes_saved">Configuration changes saved.</string>
|
||||
<string name="join_telegram">Join Telegram community</string>
|
||||
<string name="pin_created">Pin successfully created</string>
|
||||
@@ -39,7 +38,6 @@
|
||||
<string name="remote_key">Remote key</string>
|
||||
<string name="mobile_data">Mobile data</string>
|
||||
<string name="use_shell_via_shizuku">Use shell via Shizuku to get Wi-Fi information, preventing the need for location permission on non-rooted devices</string>
|
||||
<string name="stop_on_internet_loss">Stop tunnel on internet loss</string>
|
||||
<string name="vpn">VPN</string>
|
||||
<string name="tunnel_boot_description">Start the default tunnel on boot</string>
|
||||
<string name="allow_lan_traffic">Allow LAN traffic</string>
|
||||
@@ -72,7 +70,7 @@
|
||||
<string name="wifi_settings">Wi-Fi settings</string>
|
||||
<string name="tunnel_on_wifi">Tunnel on Wi-Fi</string>
|
||||
<string name="add_peer">Add peer</string>
|
||||
<string name="backup_success">Backup success. %1$s</string>
|
||||
<string name="backup_success">Backup success</string>
|
||||
<string name="persistent_keepalive">Persistent keepalive</string>
|
||||
<string name="info">Info</string>
|
||||
<string name="exclude">Exclude</string>
|
||||
@@ -143,7 +141,6 @@
|
||||
<string name="kofi">Ko-fi</string>
|
||||
<string name="donation_signoff">Gratefully,</string>
|
||||
<string name="selected">Selected</string>
|
||||
<string name="global_split_tunneling">Global split tunneling</string>
|
||||
<string name="active_network">Active Network:</string>
|
||||
<string name="range_hint">(%1$d–%2$d)</string>
|
||||
<string name="delete_active_message">Cannot delete active tunnel.</string>
|
||||
@@ -165,7 +162,6 @@
|
||||
<string name="mtu">MTU</string>
|
||||
<string name="configuration">Configuration</string>
|
||||
<string name="drag_handle">Drag Handle</string>
|
||||
<string name="global_dns_servers">Global DNS servers</string>
|
||||
<string name="display_theme">Display theme</string>
|
||||
<string name="contact">Contact</string>
|
||||
<string name="ports_must_differ">Failed. Proxies must have different ports.</string>
|
||||
@@ -259,7 +255,6 @@
|
||||
<string name="unavailable_in_mode">Unavailable in current mode</string>
|
||||
<string name="server_port">Server:Port</string>
|
||||
<string name="camera_permission_required">Camera permission required</string>
|
||||
<string name="enabled_app_shortcuts">Enable app shortcuts</string>
|
||||
<string name="preferred_tunnel">Preferred tunnel</string>
|
||||
<string name="allow">Allow</string>
|
||||
<string name="underload_packet_magic_header">Underload packet magic header</string>
|
||||
@@ -274,7 +269,6 @@
|
||||
<string name="settings">Settings</string>
|
||||
<string name="incorrect_pin">Pin is incorrect</string>
|
||||
<string name="export_failed">Export failed</string>
|
||||
<string name="enable_remote_app_control">Enable remote app control</string>
|
||||
<string name="donation_closing">It\'s my dream to work for you on this project full-time.</string>
|
||||
<string name="update_download_failed">Update download failed.</string>
|
||||
<string name="network_name">Network:</string>
|
||||
@@ -357,4 +351,112 @@
|
||||
<string name="github_sponsors_url" translatable="false">https://github.com/sponsors/zaneschepke</string>
|
||||
<string name="transport_packet_junk_size">Transport packet junk size</string>
|
||||
<string name="cookie_packet_junk_size">Cookie packet junk size</string>
|
||||
<string name="fallback_to_ipv4">Fallback to IPv4</string>
|
||||
<string name="excluded_apps">%1$s apps excluded</string>
|
||||
<string name="kernel_wireguard_unsupported">Kernel does not include WireGuard support (module missing or not built-in into kernel)!</string>
|
||||
<string name="resolution_method">Resolution method</string>
|
||||
<string name="notification_ipv6_recovery_message">%1$s recovered IPv6 connectivity</string>
|
||||
<string name="tunnel_state_resolving_dns">Resolving DNS</string>
|
||||
<string name="handshake_template">handshake: %1$s</string>
|
||||
<string name="peer_template">peer: %1$s</string>
|
||||
<string name="errors">Errors</string>
|
||||
<string name="no_system_dns_information">No system DNS information</string>
|
||||
<string name="export_unsupported">Export is not supported on this device</string>
|
||||
<string name="tunnel_globals">Tunnel globals</string>
|
||||
<string name="sort_by_latency">Sort by latency</string>
|
||||
<string name="ready">Ready</string>
|
||||
<string name="no_system_dns_detected">No system DNS detected</string>
|
||||
<string name="name_error_empty">Tunnel name cannot be empty</string>
|
||||
<string name="notification_ipv4_fallback_message">%1$s switched to IPv4 connectivity</string>
|
||||
<string name="notification_tunnel_status_format">%1$s • %2$s</string>
|
||||
<string name="dns_error_invalid_ip_or_host">Invalid IP address or hostname</string>
|
||||
<string name="proxy_channel_description">Notifications for proxy tunnels.</string>
|
||||
<string name="system_dns_servers">Servers: %1$s</string>
|
||||
<string name="balanced">Balanced (3s)</string>
|
||||
<string name="import_url_description">The URL must be secure and serve a .conf file.</string>
|
||||
<string name="dns_error_invalid_port">Port must be between 1 and 65535</string>
|
||||
<string name="view_configuration">View configuration</string>
|
||||
<string name="dot">DNS over TLS (DoT)</string>
|
||||
<string name="ipv6_recovery">IPv6 recovery</string>
|
||||
<string name="screen_recording_protection">Screen recording protection</string>
|
||||
<string name="dns_error_invalid_host">Host cannot be empty</string>
|
||||
<string name="toggle_sensitive_data_visibility">Toggle sensitive data visibility</string>
|
||||
<string name="ipv4_fallback">IPv4 fallback</string>
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
<string name="app_selection">App selection</string>
|
||||
<string name="tunnel_state_handshake_failure">Handshake failure</string>
|
||||
<string name="notification_dynamic_dns_message">%1$s updated after Dynamic DNS change</string>
|
||||
<string name="tunnel_state_establishing_connection">Establishing connection</string>
|
||||
<string name="prefer_ipv6">Prefer IPv6</string>
|
||||
<string name="peer_endpoints">Peer endpoints</string>
|
||||
<string name="dns_endpoint_hint">IP, hostname, or DoH URL</string>
|
||||
<string name="restore_ipv6">Restore IPv6</string>
|
||||
<string name="network">Network</string>
|
||||
<string name="fallback_to_ipv4_desc">Switch to IPv4 if IPv6 fails, without restarting the tunnel.</string>
|
||||
<string name="tunnel_state_disconnected">Disconnected</string>
|
||||
<string name="live">Real-time (1s)</string>
|
||||
<string name="uptime_template">uptime: %1$s</string>
|
||||
<string name="events">Events</string>
|
||||
<string name="peer_resolution">Peer Resolution</string>
|
||||
<string name="restore_ipv6_desc">Switch back to IPv6 when an IPv6 network is detected.</string>
|
||||
<string name="errors_channel_description">A channel for application and tunnel errors</string>
|
||||
<string name="vpn_permission_required">VPN permission required</string>
|
||||
<string name="app_channel_description">A channel for general application notifications, like version updates</string>
|
||||
<string name="automation">Automation</string>
|
||||
<string name="balance_saver">Battery Saver (10s)</string>
|
||||
<string name="pinging_servers">Pinging servers…</string>
|
||||
<string name="dynamic_dns_update">Dynamic DNS update</string>
|
||||
<string name="statistics">Statistics</string>
|
||||
<string name="events_channel_description">A channel for app events, like automation event</string>
|
||||
<string name="private_dns_automatic">Private DNS: automatic</string>
|
||||
<string name="tunnel_statistics">Live tunnel statistics</string>
|
||||
<string name="ipv6_settings">IPv6 settings</string>
|
||||
<string name="error">Error</string>
|
||||
<string name="dns_error_invalid_url">Invalid URL format</string>
|
||||
<string name="view_live_tunnel">View live tunnel</string>
|
||||
<string name="mode">Mode</string>
|
||||
<string name="tunnel_state_starting">Starting</string>
|
||||
<string name="dns_endpoint_label">DNS server endpoint</string>
|
||||
<string name="plain_dns">Plain DNS (port 53)</string>
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
<string name="export_canceled">Export canceled</string>
|
||||
<string name="prefer_ipv6_desc">Use IPv6 endpoints when the network supports it.</string>
|
||||
<string name="stop_all">Stop all</string>
|
||||
<string name="copy_from">Copy from</string>
|
||||
<string name="special_junk_packet">Special junk packet</string>
|
||||
<string name="error_socks5_port_unavailable">SOCKS5 port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="tunnel_scripting">Pre/Post script support</string>
|
||||
<string name="initializing">Initializing…</string>
|
||||
<string name="errors_channel_id" translatable="false">Errors Channel</string>
|
||||
<string name="s2" translatable="false">S2</string>
|
||||
<string name="jc" translatable="false">Jc</string>
|
||||
<string name="i2" translatable="false">I2</string>
|
||||
<string name="events_channel_id" translatable="false">Events Channel</string>
|
||||
<string name="proxy_channel_id" translatable="false">Proxy Channel</string>
|
||||
<string name="jmax" translatable="false">Jmax</string>
|
||||
<string name="h2" translatable="false">H2</string>
|
||||
<string name="s3" translatable="false">S3</string>
|
||||
<string name="i1" translatable="false">I1</string>
|
||||
<string name="app_channel_id" translatable="false">App Channel</string>
|
||||
<string name="i3" translatable="false">I3</string>
|
||||
<string name="example_import_url" translatable="false">https://123.com/tun.conf</string>
|
||||
<string name="i5" translatable="false">I5</string>
|
||||
<string name="s4" translatable="false">S4</string>
|
||||
<string name="fdroid_url" translatable="false">https://apt.izzysoft.de/fdroid/index/apk/com.zaneschepke.wireguardautotunnel</string>
|
||||
<string name="h3" translatable="false">H3</string>
|
||||
<string name="h1" translatable="false">H1</string>
|
||||
<string name="jmin" translatable="false">Jmin</string>
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
</resources>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user