mirror of
https://github.com/wgtunnel/android.git
synced 2026-07-03 14:07:49 +02:00
Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e990b9644d | |||
| 5f791ffda1 | |||
| ec244eeda3 | |||
| ff2a2cc082 | |||
| a873546e9e | |||
| 757669ddbe | |||
| c71c4e5b29 | |||
| 7f0fea3766 | |||
| 53c19762ef | |||
| c98fa04f73 | |||
| aba0f7d4d3 | |||
| fa517b2124 | |||
| d7e2648393 | |||
| 53ff3bb1e5 | |||
| 97ede3d5b4 | |||
| dcd15f7bd8 | |||
| 6031d85edd | |||
| a71f8f86b1 | |||
| 007c9f4c5d | |||
| e32a99db77 | |||
| 6670a62e2f | |||
| 34b20bd7f7 | |||
| 01e15099ca | |||
| 8f2fd93e77 | |||
| 94197c9943 | |||
| 39fc9014d8 | |||
| 8860b45230 | |||
| e220b26d88 | |||
| 2302b473b4 | |||
| b01473073f | |||
| 3ae9a24ca4 | |||
| dc3f7fa736 | |||
| 93d6f8aa45 | |||
| 3ea4aea5cf | |||
| 68b41c8925 | |||
| 06de1f24c2 | |||
| a39feeeea6 | |||
| c1619ff012 | |||
| 2534b86005 | |||
| 15c550737c | |||
| e1e7e27bb5 | |||
| 8021c133a5 | |||
| 6009445a15 | |||
| f80af9dd5e | |||
| 3f912ed532 |
@@ -4,7 +4,7 @@ WG Tunnel
|
|||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
An alternative Android client app for [WireGuard®](https://www.wireguard.com/)
|
An alternative Android client app for [WireGuard](https://www.wireguard.com/)
|
||||||
and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/)
|
and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/)
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
@@ -23,14 +23,14 @@ and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/)
|
|||||||
[](https://play.google.com/store/apps/details?id=com.zaneschepke.wireguardautotunnel)
|
[](https://play.google.com/store/apps/details?id=com.zaneschepke.wireguardautotunnel)
|
||||||
[](https://f-droid.org/packages/com.zaneschepke.wireguardautotunnel/)
|
[](https://f-droid.org/packages/com.zaneschepke.wireguardautotunnel/)
|
||||||
[](https://github.com/zaneschepke/fdroid)
|
[](https://github.com/zaneschepke/fdroid)
|
||||||
|
[](https://apps.obtainium.imranr.dev/redirect?r=obtainium://app/%7B%22id%22%3A%22com.zaneschepke.wireguardautotunnel%22%2C%22url%22%3A%22https%3A%2F%2Fgithub.com%2Fzaneschepke%2Fwgtunnel%22%2C%22author%22%3A%22zaneschepke%22%2C%22name%22%3A%22WG%20Tunnel%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Atrue%2C%5C%22sortMethodChoice%5C%22%3A%5C%22date%5C%22%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Atrue%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22WG%20Tunnel%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22Zane%20Schepke%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%7D%22%2C%22overrideSource%22%3Anull%7D)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
[](https://discord.gg/rbRRNh6H7V)
|
[<img src="https://img.shields.io/badge/Telegram-26A5E4.svg?style=for-the-badge&logo=Telegram&logoColor=white">](https://t.me/wgtunnel)
|
||||||
[](https://t.me/wgtunnel)
|
[<img src="https://img.shields.io/badge/Matrix-000000.svg?style=for-the-badge&logo=Matrix&logoColor=white">](https://matrix.to/#/#wg-tunnel-space:matrix.org)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<details open="open">
|
<details open="open">
|
||||||
@@ -49,7 +49,7 @@ and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/)
|
|||||||
<div style="text-align: left;">
|
<div style="text-align: left;">
|
||||||
|
|
||||||
## About
|
## About
|
||||||
Inspired by the official [wireguard-android](https://github.com/WireGuard/wireguard-android) app, WG Tunnel was created to address features and support missing from the official app. This app combines support for both [WireGuard®](https://www.wireguard.com/)
|
Inspired by the official [wireguard-android](https://github.com/WireGuard/wireguard-android) app, WG Tunnel was created to address features and support missing from the official app. This app combines support for both [WireGuard](https://www.wireguard.com/)
|
||||||
and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/), with its primary feature of auto-tunneling (on-demand tunneling).
|
and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/), with its primary feature of auto-tunneling (on-demand tunneling).
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -61,14 +61,14 @@ and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/), with its pr
|
|||||||
Thank you to the following:
|
Thank you to the following:
|
||||||
|
|
||||||
- All of the users that have helped contribute to the project with ideas, translations, feedback, bug reports, testing, and donations.
|
- All of the users that have helped contribute to the project with ideas, translations, feedback, bug reports, testing, and donations.
|
||||||
- [WireGuard®](https://www.wireguard.com/) - © Jason A. Donenfeld (https://github.com/WireGuard/wireguard-android)
|
- [WireGuard](https://www.wireguard.com/) - Jason A. Donenfeld (https://github.com/WireGuard/wireguard-android)
|
||||||
|
|
||||||
- [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) - Amnezia Team (https://github.com/amnezia-vpn/amneziawg-android)
|
- [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) - Amnezia Team (https://github.com/amnezia-vpn/amneziawg-android)
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; flex-wrap: wrap; justify-content: center; gap: 10px;">
|
<div style="display: flex; flex-wrap: wrap; justify-content: left; gap: 10px;">
|
||||||
<img label="Main" src="fastlane/metadata/android/en-US/images/phoneScreenshots/main_screen.png" width="200" />
|
<img label="Main" src="fastlane/metadata/android/en-US/images/phoneScreenshots/main_screen.png" width="200" />
|
||||||
<img label="Settings" src="fastlane/metadata/android/en-US/images/phoneScreenshots/settings_screen.png" width="200" />
|
<img label="Settings" src="fastlane/metadata/android/en-US/images/phoneScreenshots/settings_screen.png" width="200" />
|
||||||
<img label="Auto" src="fastlane/metadata/android/en-US/images/phoneScreenshots/auto_screen.png" width="200" />
|
<img label="Auto" src="fastlane/metadata/android/en-US/images/phoneScreenshots/auto_screen.png" width="200" />
|
||||||
|
|||||||
@@ -144,8 +144,8 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
implementation(project(":logcatter"))
|
implementation(project(":logcatter"))
|
||||||
|
implementation(project(":networkmonitor"))
|
||||||
|
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
|
|||||||
@@ -3,3 +3,40 @@
|
|||||||
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
|
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
|
||||||
<fields>;
|
<fields>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Keep all classes in the org.xbill.DNS package and subpackages
|
||||||
|
-keep class org.xbill.DNS.** { *; }
|
||||||
|
-dontwarn org.xbill.DNS.**
|
||||||
|
|
||||||
|
# Preserve JNA classes if used (e.g., for IPHlpAPI on Windows)
|
||||||
|
-keep class com.sun.jna.** { *; }
|
||||||
|
-dontwarn com.sun.jna.**
|
||||||
|
|
||||||
|
# Keep DNS resolver configuration classes that might be loaded dynamically
|
||||||
|
-keep class org.xbill.DNS.config.** { *; }
|
||||||
|
-dontwarn org.xbill.DNS.config.**
|
||||||
|
|
||||||
|
-keep class org.xbill.DNS.** { *; }
|
||||||
|
|
||||||
|
# Prevent optimization issues with native or reflection-based calls
|
||||||
|
-dontoptimize
|
||||||
|
-dontshrink
|
||||||
|
# Uncomment the above if errors persist, but use sparingly as they’re broad
|
||||||
|
|
||||||
|
# Suppress warnings about missing classes if not all features are used
|
||||||
|
-dontwarn java.lang.management.**
|
||||||
|
-dontwarn sun.nio.ch.**
|
||||||
|
|
||||||
|
-dontwarn com.google.api.client.http.GenericUrl
|
||||||
|
-dontwarn com.google.api.client.http.HttpHeaders
|
||||||
|
-dontwarn com.google.api.client.http.HttpRequest
|
||||||
|
-dontwarn com.google.api.client.http.HttpRequestFactory
|
||||||
|
-dontwarn com.google.api.client.http.HttpResponse
|
||||||
|
-dontwarn com.google.api.client.http.HttpTransport
|
||||||
|
-dontwarn com.google.api.client.http.javanet.NetHttpTransport$Builder
|
||||||
|
-dontwarn com.google.api.client.http.javanet.NetHttpTransport
|
||||||
|
-dontwarn javax.lang.model.element.Modifier
|
||||||
|
-dontwarn org.joda.time.Instant
|
||||||
|
-dontwarn org.slf4j.impl.StaticLoggerBinder
|
||||||
|
-dontwarn org.slf4j.impl.StaticMDCBinder
|
||||||
|
-dontwarn org.slf4j.impl.StaticMarkerBinder
|
||||||
Vendored
+37
@@ -22,3 +22,40 @@
|
|||||||
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
|
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
|
||||||
<fields>;
|
<fields>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Keep all classes in the org.xbill.DNS package and subpackages
|
||||||
|
-keep class org.xbill.DNS.** { *; }
|
||||||
|
-dontwarn org.xbill.DNS.**
|
||||||
|
|
||||||
|
# Preserve JNA classes if used (e.g., for IPHlpAPI on Windows)
|
||||||
|
-keep class com.sun.jna.** { *; }
|
||||||
|
-dontwarn com.sun.jna.**
|
||||||
|
|
||||||
|
# Keep DNS resolver configuration classes that might be loaded dynamically
|
||||||
|
-keep class org.xbill.DNS.config.** { *; }
|
||||||
|
-dontwarn org.xbill.DNS.config.**
|
||||||
|
|
||||||
|
-keep class org.xbill.DNS.** { *; }
|
||||||
|
|
||||||
|
# Prevent optimization issues with native or reflection-based calls
|
||||||
|
-dontoptimize
|
||||||
|
-dontshrink
|
||||||
|
# Uncomment the above if errors persist, but use sparingly as they’re broad
|
||||||
|
|
||||||
|
# Suppress warnings about missing classes if not all features are used
|
||||||
|
-dontwarn java.lang.management.**
|
||||||
|
-dontwarn sun.nio.ch.**
|
||||||
|
|
||||||
|
-dontwarn com.google.api.client.http.GenericUrl
|
||||||
|
-dontwarn com.google.api.client.http.HttpHeaders
|
||||||
|
-dontwarn com.google.api.client.http.HttpRequest
|
||||||
|
-dontwarn com.google.api.client.http.HttpRequestFactory
|
||||||
|
-dontwarn com.google.api.client.http.HttpResponse
|
||||||
|
-dontwarn com.google.api.client.http.HttpTransport
|
||||||
|
-dontwarn com.google.api.client.http.javanet.NetHttpTransport$Builder
|
||||||
|
-dontwarn com.google.api.client.http.javanet.NetHttpTransport
|
||||||
|
-dontwarn javax.lang.model.element.Modifier
|
||||||
|
-dontwarn org.joda.time.Instant
|
||||||
|
-dontwarn org.slf4j.impl.StaticLoggerBinder
|
||||||
|
-dontwarn org.slf4j.impl.StaticMDCBinder
|
||||||
|
-dontwarn org.slf4j.impl.StaticMarkerBinder
|
||||||
@@ -3,13 +3,8 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
|
||||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
|
||||||
<!--foreground service exempt android 14-->
|
<!--foreground service exempt android 14-->
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
|
||||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"
|
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"
|
||||||
@@ -168,25 +163,17 @@
|
|||||||
</service>
|
</service>
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".core.broadcast.BootReceiver"
|
android:name=".core.broadcast.RestartReceiver"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
<action android:name="android.intent.action.ACTION_BOOT_COMPLETED" />
|
|
||||||
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
|
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
|
||||||
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
|
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
|
||||||
|
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
<receiver
|
|
||||||
android:name=".core.broadcast.AppUpdateReceiver"
|
|
||||||
android:exported="false">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".core.broadcast.KernelReceiver"
|
android:name=".core.broadcast.KernelReceiver"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel
|
package com.zaneschepke.wireguardautotunnel
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@@ -32,15 +34,16 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import androidx.navigation.toRoute
|
import androidx.navigation.toRoute
|
||||||
|
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||||
import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager
|
import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.worker.ServiceWorker
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.Route
|
import com.zaneschepke.wireguardautotunnel.ui.Route
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
|
||||||
@@ -48,12 +51,12 @@ import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
|
|||||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.CustomSnackBar
|
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.CustomSnackBar
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarControllerProvider
|
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarControllerProvider
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.ConfigScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.OptionsScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.OptionsScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.PinLockScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.PinLockScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ScannerScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ScannerScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.SplitTunnelScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.splittunnel.SplitTunnelScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.TunnelAutoTunnelScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.TunnelAutoTunnelScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.AdvancedScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.AdvancedScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.AppearanceScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.AppearanceScreen
|
||||||
@@ -67,9 +70,9 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.support.LogsScreen
|
|||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
|
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
|
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
@@ -85,6 +88,11 @@ class MainActivity : AppCompatActivity() {
|
|||||||
@Inject
|
@Inject
|
||||||
lateinit var shortcutManager: ShortcutManager
|
lateinit var shortcutManager: ShortcutManager
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var networkMonitor: NetworkMonitor
|
||||||
|
|
||||||
|
private var lastLocationPermissionState: Boolean? = null
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
enableEdgeToEdge(
|
enableEdgeToEdge(
|
||||||
statusBarStyle = SystemBarStyle.Companion.auto(Color.TRANSPARENT, Color.TRANSPARENT),
|
statusBarStyle = SystemBarStyle.Companion.auto(Color.TRANSPARENT, Color.TRANSPARENT),
|
||||||
@@ -117,26 +125,13 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
viewModel.getEmitSplitTunnelApps(this@MainActivity)
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(appUiState.autoTunnelActive) {
|
|
||||||
requestAutoTunnelTileServiceUpdate()
|
|
||||||
}
|
|
||||||
|
|
||||||
with(appUiState.appSettings) {
|
with(appUiState.appSettings) {
|
||||||
LaunchedEffect(isAutoTunnelEnabled) {
|
|
||||||
this@MainActivity.requestAutoTunnelTileServiceUpdate()
|
|
||||||
}
|
|
||||||
LaunchedEffect(isShortcutsEnabled) {
|
LaunchedEffect(isShortcutsEnabled) {
|
||||||
if (!isShortcutsEnabled) return@LaunchedEffect shortcutManager.removeShortcuts()
|
if (!isShortcutsEnabled) return@LaunchedEffect shortcutManager.removeShortcuts()
|
||||||
shortcutManager.addShortcuts()
|
shortcutManager.addShortcuts()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ServiceWorker.start(this)
|
|
||||||
|
|
||||||
CompositionLocalProvider(LocalNavController provides navController) {
|
CompositionLocalProvider(LocalNavController provides navController) {
|
||||||
SnackbarControllerProvider { host ->
|
SnackbarControllerProvider { host ->
|
||||||
WireguardAutoTunnelTheme(theme = appUiState.generalState.theme) {
|
WireguardAutoTunnelTheme(theme = appUiState.generalState.theme) {
|
||||||
@@ -221,16 +216,17 @@ class MainActivity : AppCompatActivity() {
|
|||||||
composable<Route.Logs> {
|
composable<Route.Logs> {
|
||||||
LogsScreen()
|
LogsScreen()
|
||||||
}
|
}
|
||||||
composable<Route.Config> {
|
composable<Route.Config> { backStack ->
|
||||||
val args = it.toRoute<Route.Config>()
|
val args = backStack.toRoute<Route.Config>()
|
||||||
val config =
|
val config =
|
||||||
appUiState.tunnels.firstOrNull { it.id == args.id }
|
appUiState.tunnels.firstOrNull { it.id == args.id }
|
||||||
ConfigScreen(config, viewModel)
|
ConfigScreen(config)
|
||||||
}
|
}
|
||||||
composable<Route.TunnelOptions> {
|
composable<Route.TunnelOptions> { backStack ->
|
||||||
val args = it.toRoute<Route.TunnelOptions>()
|
val args = backStack.toRoute<Route.TunnelOptions>()
|
||||||
val config = appUiState.tunnels.first { it.id == args.id }
|
appUiState.tunnels.firstOrNull { it.id == args.id }?.let { config ->
|
||||||
OptionsScreen(config)
|
OptionsScreen(config, appUiState)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
composable<Route.Lock> {
|
composable<Route.Lock> {
|
||||||
PinLockScreen(viewModel)
|
PinLockScreen(viewModel)
|
||||||
@@ -242,14 +238,13 @@ class MainActivity : AppCompatActivity() {
|
|||||||
KillSwitchScreen(appUiState, viewModel)
|
KillSwitchScreen(appUiState, viewModel)
|
||||||
}
|
}
|
||||||
composable<Route.SplitTunnel> {
|
composable<Route.SplitTunnel> {
|
||||||
val args = it.toRoute<Route.SplitTunnel>()
|
SplitTunnelScreen()
|
||||||
val config = appUiState.tunnels.first { it.id == args.id }
|
|
||||||
SplitTunnelScreen(config, viewModel)
|
|
||||||
}
|
}
|
||||||
composable<Route.TunnelAutoTunnel> {
|
composable<Route.TunnelAutoTunnel> { backStack ->
|
||||||
val args = it.toRoute<Route.TunnelOptions>()
|
val args = backStack.toRoute<Route.TunnelOptions>()
|
||||||
val config = appUiState.tunnels.first { it.id == args.id }
|
appUiState.tunnels.firstOrNull { it.id == args.id }?.let {
|
||||||
TunnelAutoTunnelScreen(config, appUiState.appSettings)
|
TunnelAutoTunnelScreen(it, appUiState.appSettings)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BackHandler {
|
BackHandler {
|
||||||
@@ -264,4 +259,22 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
checkPermissionAndNotify()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkPermissionAndNotify() {
|
||||||
|
val hasLocation = ContextCompat.checkSelfPermission(
|
||||||
|
this,
|
||||||
|
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||||
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
|
if (lastLocationPermissionState != hasLocation) {
|
||||||
|
Timber.d("Location permission changed to: $hasLocation")
|
||||||
|
if (hasLocation) {
|
||||||
|
networkMonitor.sendLocationPermissionsGrantedBroadcast()
|
||||||
|
}
|
||||||
|
lastLocationPermissionState = hasLocation
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import androidx.work.Configuration
|
|||||||
import com.wireguard.android.backend.GoBackend
|
import com.wireguard.android.backend.GoBackend
|
||||||
import com.zaneschepke.logcatter.LogReader
|
import com.zaneschepke.logcatter.LogReader
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.worker.ServiceWorker
|
||||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||||
import com.zaneschepke.wireguardautotunnel.di.MainDispatcher
|
import com.zaneschepke.wireguardautotunnel.di.MainDispatcher
|
||||||
@@ -91,9 +92,11 @@ class WireGuardAutoTunnel : Application(), Configuration.Provider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ServiceWorker.start(this)
|
||||||
|
|
||||||
applicationScope.launch {
|
applicationScope.launch {
|
||||||
withContext(mainDispatcher) {
|
withContext(mainDispatcher) {
|
||||||
if (appDataRepository.appState.isLocalLogsEnabled() && !isRunningOnTv()) logReader.initialize()
|
if (appDataRepository.appState.isLocalLogsEnabled() && !isRunningOnTv()) logReader.start()
|
||||||
}
|
}
|
||||||
if (!appDataRepository.settings.get().isKernelEnabled) {
|
if (!appDataRepository.settings.get().isKernelEnabled) {
|
||||||
tunnelManager.setBackendState(BackendState.SERVICE_ACTIVE, emptyList())
|
tunnelManager.setBackendState(BackendState.SERVICE_ACTIVE, emptyList())
|
||||||
|
|||||||
-45
@@ -1,45 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.broadcast
|
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class AppUpdateReceiver : BroadcastReceiver() {
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
@ApplicationScope
|
|
||||||
lateinit var applicationScope: CoroutineScope
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var appDataRepository: AppDataRepository
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var serviceManager: ServiceManager
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var tunnelManager: TunnelManager
|
|
||||||
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
if (intent.action != Intent.ACTION_MY_PACKAGE_REPLACED) return
|
|
||||||
serviceManager.updateTunnelTile()
|
|
||||||
serviceManager.updateAutoTunnelTile()
|
|
||||||
applicationScope.launch {
|
|
||||||
with(appDataRepository.settings.get()) {
|
|
||||||
if (isRestoreOnBootEnabled) {
|
|
||||||
// If auto tunnel is enabled, just start it and let auto tunnel start appropriate tun
|
|
||||||
if (isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) return@launch serviceManager.startAutoTunnel(true)
|
|
||||||
tunnelManager.restorePreviousState()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.broadcast
|
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class BootReceiver : BroadcastReceiver() {
|
|
||||||
@Inject
|
|
||||||
lateinit var appDataRepository: AppDataRepository
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
@ApplicationScope
|
|
||||||
lateinit var applicationScope: CoroutineScope
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var serviceManager: ServiceManager
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var tunnelManager: TunnelManager
|
|
||||||
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
if (Intent.ACTION_BOOT_COMPLETED != intent.action) return
|
|
||||||
serviceManager.updateTunnelTile()
|
|
||||||
serviceManager.updateAutoTunnelTile()
|
|
||||||
applicationScope.launch {
|
|
||||||
with(appDataRepository.settings.get()) {
|
|
||||||
if (isRestoreOnBootEnabled) {
|
|
||||||
// If auto tunnel is enabled, just start it and let auto tunnel start appropriate tun
|
|
||||||
if (isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) return@launch serviceManager.startAutoTunnel(true)
|
|
||||||
tunnelManager.restorePreviousState()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+2
-2
@@ -32,8 +32,8 @@ class KernelReceiver : BroadcastReceiver() {
|
|||||||
val action = intent.action ?: return
|
val action = intent.action ?: return
|
||||||
applicationScope.launch {
|
applicationScope.launch {
|
||||||
if (action == REFRESH_TUNNELS_ACTION) {
|
if (action == REFRESH_TUNNELS_ACTION) {
|
||||||
tunnelManager.runningTunnelNames().forEach {
|
tunnelManager.runningTunnelNames().forEach { name ->
|
||||||
val tunnel = tunnelRepository.findByTunnelName(it)
|
val tunnel = tunnelRepository.findByTunnelName(name)
|
||||||
tunnel?.let {
|
tunnel?.let {
|
||||||
tunnelRepository.save(it.copy(isActive = true))
|
tunnelRepository.save(it.copy(isActive = true))
|
||||||
}
|
}
|
||||||
|
|||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.core.broadcast
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class RestartReceiver : BroadcastReceiver() {
|
||||||
|
@Inject
|
||||||
|
lateinit var appDataRepository: AppDataRepository
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@ApplicationScope
|
||||||
|
lateinit var applicationScope: CoroutineScope
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var serviceManager: ServiceManager
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var tunnelManager: TunnelManager
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@IoDispatcher
|
||||||
|
lateinit var ioDispatcher: CoroutineDispatcher
|
||||||
|
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
val action = intent.action ?: return
|
||||||
|
if (action != Intent.ACTION_BOOT_COMPLETED &&
|
||||||
|
action != Intent.ACTION_MY_PACKAGE_REPLACED &&
|
||||||
|
action != "com.htc.intent.action.QUICKBOOT_POWERON"
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Timber.d("RestartReceiver triggered with action: ${intent.action}")
|
||||||
|
applicationScope.launch(ioDispatcher) {
|
||||||
|
serviceManager.updateTunnelTile()
|
||||||
|
serviceManager.updateAutoTunnelTile()
|
||||||
|
val settings = appDataRepository.settings.get()
|
||||||
|
if (settings.isRestoreOnBootEnabled) {
|
||||||
|
if (settings.isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) {
|
||||||
|
Timber.d("Starting auto-tunnel on boot/update")
|
||||||
|
serviceManager.startAutoTunnel(true)
|
||||||
|
} else {
|
||||||
|
Timber.d("Restoring previous tunnel state")
|
||||||
|
tunnelManager.restorePreviousState()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Timber.d("Restore on boot disabled, skipping")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
-161
@@ -1,161 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.network
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.ConnectivityManager
|
|
||||||
import android.net.Network
|
|
||||||
import android.net.NetworkCapabilities
|
|
||||||
import android.net.NetworkRequest
|
|
||||||
import android.net.wifi.SupplicantState
|
|
||||||
import android.net.wifi.WifiManager
|
|
||||||
import android.os.Build
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.ConnectivityState
|
|
||||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.channels.awaitClose
|
|
||||||
import kotlinx.coroutines.flow.callbackFlow
|
|
||||||
import kotlinx.coroutines.flow.flowOn
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
class InternetConnectivityMonitor
|
|
||||||
@Inject
|
|
||||||
constructor(
|
|
||||||
@ApplicationContext private val context: Context,
|
|
||||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
|
||||||
) : NetworkMonitor {
|
|
||||||
|
|
||||||
private val connectivityManager =
|
|
||||||
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
|
||||||
|
|
||||||
@get:Synchronized @set:Synchronized
|
|
||||||
private var wifiCapabilities: NetworkCapabilities? = null
|
|
||||||
|
|
||||||
@get:Synchronized @set:Synchronized
|
|
||||||
private var wifiNetworkChanged: Boolean = false
|
|
||||||
|
|
||||||
override val didWifiChangeSinceLastCapabilitiesQuery: Boolean
|
|
||||||
get() = wifiNetworkChanged
|
|
||||||
|
|
||||||
override val status = callbackFlow {
|
|
||||||
|
|
||||||
var wifiState: Boolean = false
|
|
||||||
var ethernetState: Boolean = false
|
|
||||||
var cellularState: Boolean = false
|
|
||||||
|
|
||||||
fun emitState() {
|
|
||||||
trySend(ConnectivityState(wifiState, ethernetState, cellularState))
|
|
||||||
}
|
|
||||||
|
|
||||||
val currentNetwork = connectivityManager.activeNetwork
|
|
||||||
if (currentNetwork == null) {
|
|
||||||
emitState()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateCapabilityState(up: Boolean, network: Network) {
|
|
||||||
with(connectivityManager.getNetworkCapabilities(network)) {
|
|
||||||
when {
|
|
||||||
this == null -> return
|
|
||||||
hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> wifiState = up
|
|
||||||
hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ->
|
|
||||||
cellularState = up
|
|
||||||
|
|
||||||
hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) ->
|
|
||||||
ethernetState = up
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onWifiChange(network: Network, callback: () -> Unit) {
|
|
||||||
if (connectivityManager.getNetworkCapabilities(network)?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true) {
|
|
||||||
callback()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onAvailable(network: Network) {
|
|
||||||
onWifiChange(network) {
|
|
||||||
wifiNetworkChanged = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
|
|
||||||
onWifiChange(network) {
|
|
||||||
wifiCapabilities = networkCapabilities
|
|
||||||
}
|
|
||||||
updateCapabilityState(true, network)
|
|
||||||
emitState()
|
|
||||||
}
|
|
||||||
|
|
||||||
val networkStatusCallback =
|
|
||||||
when (Build.VERSION.SDK_INT) {
|
|
||||||
in Build.VERSION_CODES.S..Int.MAX_VALUE -> {
|
|
||||||
object :
|
|
||||||
ConnectivityManager.NetworkCallback(
|
|
||||||
FLAG_INCLUDE_LOCATION_INFO,
|
|
||||||
) {
|
|
||||||
override fun onAvailable(network: Network) {
|
|
||||||
onAvailable(network)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLost(network: Network) {
|
|
||||||
updateCapabilityState(false, network)
|
|
||||||
emitState()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
|
|
||||||
onCapabilitiesChanged(network, networkCapabilities)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
object : ConnectivityManager.NetworkCallback() {
|
|
||||||
override fun onAvailable(network: Network) {
|
|
||||||
onAvailable(network)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLost(network: Network) {
|
|
||||||
updateCapabilityState(false, network)
|
|
||||||
emitState()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
|
|
||||||
onCapabilitiesChanged(network, networkCapabilities)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val request =
|
|
||||||
NetworkRequest.Builder()
|
|
||||||
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
|
|
||||||
.addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
|
|
||||||
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
|
|
||||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
|
||||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
|
||||||
.build()
|
|
||||||
connectivityManager.registerNetworkCallback(request, networkStatusCallback)
|
|
||||||
|
|
||||||
awaitClose { connectivityManager.unregisterNetworkCallback(networkStatusCallback) }
|
|
||||||
}.flowOn(ioDispatcher)
|
|
||||||
|
|
||||||
override fun getWifiCapabilities(): NetworkCapabilities? {
|
|
||||||
wifiNetworkChanged = false
|
|
||||||
return wifiCapabilities
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun getNetworkName(networkCapabilities: NetworkCapabilities, context: Context): String? {
|
|
||||||
var ssid = networkCapabilities.getWifiName()
|
|
||||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S) {
|
|
||||||
val wifiManager =
|
|
||||||
context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
val info = wifiManager.connectionInfo
|
|
||||||
if (info.supplicantState === SupplicantState.COMPLETED) {
|
|
||||||
ssid = info.ssid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ssid?.trim('"')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-16
@@ -1,16 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.network
|
|
||||||
|
|
||||||
import android.net.NetworkCapabilities
|
|
||||||
import android.net.wifi.WifiInfo
|
|
||||||
import android.os.Build
|
|
||||||
|
|
||||||
fun NetworkCapabilities.getWifiName(): String? {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
||||||
val info: WifiInfo
|
|
||||||
if (transportInfo is WifiInfo) {
|
|
||||||
info = transportInfo as WifiInfo
|
|
||||||
return info.ssid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.network
|
|
||||||
|
|
||||||
import android.net.NetworkCapabilities
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.ConnectivityState
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
|
|
||||||
interface NetworkMonitor {
|
|
||||||
val status: Flow<ConnectivityState>
|
|
||||||
|
|
||||||
// util to help limit location queries
|
|
||||||
val didWifiChangeSinceLastCapabilitiesQuery: Boolean
|
|
||||||
fun getWifiCapabilities(): NetworkCapabilities?
|
|
||||||
}
|
|
||||||
+78
-55
@@ -3,35 +3,36 @@ package com.zaneschepke.wireguardautotunnel.core.service
|
|||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelService
|
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelService
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.tile.AutoTunnelControlTile
|
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.tile.TunnelControlTile
|
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
|
import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
|
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
|
||||||
import jakarta.inject.Inject
|
import jakarta.inject.Inject
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
class ServiceManager @Inject constructor(
|
||||||
class ServiceManager
|
private val context: Context,
|
||||||
@Inject constructor(private val context: Context, private val ioDispatcher: CoroutineDispatcher, private val appDataRepository: AppDataRepository) {
|
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||||
|
@ApplicationScope private val applicationScope: CoroutineScope,
|
||||||
|
private val appDataRepository: AppDataRepository,
|
||||||
|
) {
|
||||||
|
|
||||||
private val _autoTunnelActive = MutableStateFlow(false)
|
private val _autoTunnelActive = MutableStateFlow(false)
|
||||||
|
|
||||||
val autoTunnelActive = _autoTunnelActive.asStateFlow()
|
val autoTunnelActive = _autoTunnelActive.asStateFlow()
|
||||||
|
|
||||||
var autoTunnelService = CompletableDeferred<AutoTunnelService>()
|
var autoTunnelService = CompletableDeferred<AutoTunnelService>()
|
||||||
var backgroundService = CompletableDeferred<TunnelForegroundService>()
|
var backgroundService = CompletableDeferred<TunnelForegroundService>()
|
||||||
var autoTunnelTile = CompletableDeferred<AutoTunnelControlTile>()
|
|
||||||
var tunnelControlTile = CompletableDeferred<TunnelControlTile>()
|
|
||||||
|
|
||||||
private fun <T : Service> startService(cls: Class<T>, background: Boolean) {
|
private fun <T : Service> startService(cls: Class<T>, background: Boolean) {
|
||||||
runCatching {
|
runCatching {
|
||||||
@@ -44,76 +45,98 @@ class ServiceManager
|
|||||||
}.onFailure { Timber.e(it) }
|
}.onFailure { Timber.e(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun startAutoTunnel(background: Boolean) {
|
fun startAutoTunnel(background: Boolean) {
|
||||||
val settings = appDataRepository.settings.get()
|
applicationScope.launch(ioDispatcher) {
|
||||||
appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = true))
|
val settings = appDataRepository.settings.get()
|
||||||
if (autoTunnelService.isCompleted) return _autoTunnelActive.update { true }
|
appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = true))
|
||||||
runCatching {
|
if (autoTunnelService.isCompleted) {
|
||||||
startService(AutoTunnelService::class.java, background)
|
_autoTunnelActive.update { true }
|
||||||
autoTunnelService.await()
|
return@launch
|
||||||
autoTunnelService.getCompleted().start()
|
}
|
||||||
_autoTunnelActive.update { true }
|
runCatching {
|
||||||
updateAutoTunnelTile()
|
autoTunnelService = CompletableDeferred()
|
||||||
}.onFailure {
|
startService(AutoTunnelService::class.java, background)
|
||||||
Timber.e(it)
|
_autoTunnelActive.update { true }
|
||||||
|
}.onFailure {
|
||||||
|
Timber.e(it)
|
||||||
|
_autoTunnelActive.update { false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateAutoTunnelTile()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startTunnelForegroundService(tunnelConf: TunnelConf) {
|
||||||
|
applicationScope.launch(ioDispatcher) {
|
||||||
|
if (backgroundService.isCompleted) return@launch
|
||||||
|
runCatching {
|
||||||
|
backgroundService = CompletableDeferred()
|
||||||
|
startService(TunnelForegroundService::class.java, true)
|
||||||
|
val service = withTimeoutOrNull(SERVICE_START_TIMEOUT) { backgroundService.await() }
|
||||||
|
?: throw IllegalStateException("Background service start timed out")
|
||||||
|
service.start(tunnelConf)
|
||||||
|
}.onFailure {
|
||||||
|
Timber.e(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun startBackgroundService(tunnelConf: TunnelConf) {
|
fun updateTunnelForegroundServiceNotification(tunnelConf: TunnelConf) {
|
||||||
if (backgroundService.isCompleted) return
|
applicationScope.launch(ioDispatcher) {
|
||||||
runCatching {
|
if (!backgroundService.isCompleted) return@launch
|
||||||
startService(TunnelForegroundService::class.java, true)
|
runCatching {
|
||||||
backgroundService.await()
|
val service = backgroundService.await()
|
||||||
backgroundService.getCompleted().start(tunnelConf)
|
service.start(tunnelConf)
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
Timber.e(it)
|
Timber.e(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopBackgroundService() {
|
fun stopTunnelForegroundService() {
|
||||||
if (!backgroundService.isCompleted) return
|
applicationScope.launch(ioDispatcher) {
|
||||||
runCatching {
|
if (!backgroundService.isCompleted) return@launch
|
||||||
backgroundService.getCompleted().stop()
|
runCatching {
|
||||||
}.onFailure {
|
val service = backgroundService.await()
|
||||||
Timber.e(it)
|
service.stop()
|
||||||
|
backgroundService = CompletableDeferred()
|
||||||
|
}.onFailure {
|
||||||
|
Timber.e(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun toggleAutoTunnel(background: Boolean) {
|
fun toggleAutoTunnel(background: Boolean) {
|
||||||
withContext(ioDispatcher) {
|
applicationScope.launch(ioDispatcher) {
|
||||||
if (_autoTunnelActive.value) return@withContext stopAutoTunnel()
|
if (_autoTunnelActive.value) stopAutoTunnel() else startAutoTunnel(background)
|
||||||
startAutoTunnel(background)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateAutoTunnelTile() {
|
fun updateAutoTunnelTile() {
|
||||||
if (autoTunnelTile.isCompleted) {
|
context.requestAutoTunnelTileServiceUpdate()
|
||||||
autoTunnelTile.getCompleted().updateTileState()
|
|
||||||
} else {
|
|
||||||
context.requestAutoTunnelTileServiceUpdate()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateTunnelTile() {
|
fun updateTunnelTile() {
|
||||||
if (tunnelControlTile.isCompleted) {
|
context.requestTunnelTileServiceStateUpdate()
|
||||||
tunnelControlTile.getCompleted().updateTileState()
|
|
||||||
} else {
|
|
||||||
context.requestTunnelTileServiceStateUpdate()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun stopAutoTunnel() {
|
fun stopAutoTunnel() {
|
||||||
withContext(ioDispatcher) {
|
applicationScope.launch(ioDispatcher) {
|
||||||
val settings = appDataRepository.settings.get()
|
val settings = appDataRepository.settings.get()
|
||||||
appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = false))
|
appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = false))
|
||||||
if (!autoTunnelService.isCompleted) return@withContext
|
if (!autoTunnelService.isCompleted) return@launch
|
||||||
runCatching {
|
runCatching {
|
||||||
autoTunnelService.getCompleted().stop()
|
val service = autoTunnelService.await()
|
||||||
|
service.stop()
|
||||||
_autoTunnelActive.update { false }
|
_autoTunnelActive.update { false }
|
||||||
|
autoTunnelService = CompletableDeferred()
|
||||||
updateAutoTunnelTile()
|
updateAutoTunnelTile()
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
Timber.e(it)
|
Timber.e(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val SERVICE_START_TIMEOUT = 5_000L
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+108
-2
@@ -5,14 +5,30 @@ import android.content.Intent
|
|||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import androidx.core.app.ServiceCompat
|
import androidx.core.app.ServiceCompat
|
||||||
import androidx.lifecycle.LifecycleService
|
import androidx.lifecycle.LifecycleService
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||||
|
import com.zaneschepke.networkmonitor.NetworkStatus
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
|
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
|
||||||
|
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
@@ -24,6 +40,18 @@ class TunnelForegroundService : LifecycleService() {
|
|||||||
@Inject
|
@Inject
|
||||||
lateinit var serviceManager: ServiceManager
|
lateinit var serviceManager: ServiceManager
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var networkMonitor: NetworkMonitor
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@IoDispatcher
|
||||||
|
lateinit var ioDispatcher: CoroutineDispatcher
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var tunnelRepo: TunnelRepository
|
||||||
|
|
||||||
|
private val isNetworkConnected = MutableStateFlow(true)
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
serviceManager.backgroundService.complete(this)
|
serviceManager.backgroundService.complete(this)
|
||||||
@@ -40,21 +68,94 @@ class TunnelForegroundService : LifecycleService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun start(tunnelConf: TunnelConf) {
|
fun start(tunnelConf: TunnelConf) {
|
||||||
|
Timber.d("Service starting with TunnelConf instance: ${tunnelConf.hashCode()}")
|
||||||
ServiceCompat.startForeground(
|
ServiceCompat.startForeground(
|
||||||
this@TunnelForegroundService,
|
this@TunnelForegroundService,
|
||||||
NotificationManager.KERNEL_SERVICE_NOTIFICATION_ID,
|
NotificationManager.KERNEL_SERVICE_NOTIFICATION_ID,
|
||||||
createNotification(tunnelConf),
|
createNotification(tunnelConf),
|
||||||
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
|
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
|
||||||
)
|
)
|
||||||
|
// monitor if we have internet connectivity
|
||||||
|
startNetworkMonitorJob()
|
||||||
|
|
||||||
|
// job to trigger stats emit on interval
|
||||||
|
startTunnelStatsJob(tunnelConf)
|
||||||
|
|
||||||
|
// monitor changes to the tunnel config
|
||||||
|
startTunnelConfChangesJob(tunnelConf)
|
||||||
|
|
||||||
|
startPingJob(tunnelConf)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startTunnelConfChangesJob(tunnelConf: TunnelConf) = lifecycleScope.launch(ioDispatcher) {
|
||||||
|
tunnelRepo.flow
|
||||||
|
.flowOn(ioDispatcher)
|
||||||
|
.map { storedTunnels ->
|
||||||
|
storedTunnels.firstOrNull { it.id == tunnelConf.id }
|
||||||
|
}
|
||||||
|
.filterNotNull()
|
||||||
|
// only emit when one of these 3 values change
|
||||||
|
.distinctUntilChanged { old, new ->
|
||||||
|
old.tunName == new.tunName && old.wgQuick == new.wgQuick && old.amQuick == new.amQuick
|
||||||
|
}
|
||||||
|
.collect { storedTunnel ->
|
||||||
|
if (tunnelConf.isTunnelConfigChanged(storedTunnel)) {
|
||||||
|
Timber.d("Config changed for ${storedTunnel.tunName}, bouncing")
|
||||||
|
tunnelConf.bounceTunnel(storedTunnel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startNetworkMonitorJob() = lifecycleScope.launch(ioDispatcher) {
|
||||||
|
networkMonitor.networkStatusFlow
|
||||||
|
.flowOn(ioDispatcher)
|
||||||
|
.collectLatest { status ->
|
||||||
|
val isAvailable = status !is NetworkStatus.Disconnected
|
||||||
|
isNetworkConnected.value = isAvailable
|
||||||
|
Timber.d("Network available: $status")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startTunnelStatsJob(tunnel: TunnelConf) = lifecycleScope.launch(ioDispatcher) {
|
||||||
|
while (isActive) {
|
||||||
|
tunnel.onUpdateStatistics()
|
||||||
|
delay(STATS_DELAY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startPingJob(tunnel: TunnelConf) = lifecycleScope.launch(ioDispatcher) {
|
||||||
|
delay(PING_START_DELAY)
|
||||||
|
while (isActive) {
|
||||||
|
val shouldBounce = shouldBounceTunnel(tunnel)
|
||||||
|
val delayMs = if (shouldBounce) {
|
||||||
|
tunnel.bounceTunnel(tunnel)
|
||||||
|
tunnel.pingCooldown ?: Constants.PING_COOLDOWN
|
||||||
|
} else {
|
||||||
|
tunnel.pingInterval ?: Constants.PING_INTERVAL
|
||||||
|
}
|
||||||
|
delay(delayMs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun shouldBounceTunnel(tunnel: TunnelConf): Boolean {
|
||||||
|
if (!isNetworkConnected.value) {
|
||||||
|
Timber.d("Network disconnected, skipping ping for ${tunnel.tunName}")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return runCatching {
|
||||||
|
!tunnel.isTunnelPingable(ioDispatcher)
|
||||||
|
}.onFailure { e ->
|
||||||
|
Timber.e(e, "Ping check failed for ${tunnel.tunName}")
|
||||||
|
}.getOrDefault(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stop() {
|
fun stop() {
|
||||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
|
||||||
stopSelf()
|
stopSelf()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
serviceManager.backgroundService = CompletableDeferred()
|
serviceManager.backgroundService = CompletableDeferred()
|
||||||
|
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,4 +168,9 @@ class TunnelForegroundService : LifecycleService() {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val STATS_DELAY = 1_000L
|
||||||
|
const val PING_START_DELAY = 30_000L
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+45
-44
@@ -1,44 +1,42 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.service.autotunnel
|
package com.zaneschepke.wireguardautotunnel.core.service.autotunnel
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.NetworkCapabilities
|
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import androidx.core.app.ServiceCompat
|
import androidx.core.app.ServiceCompat
|
||||||
import androidx.lifecycle.LifecycleService
|
import androidx.lifecycle.LifecycleService
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.wireguard.android.util.RootShell
|
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||||
|
import com.zaneschepke.networkmonitor.NetworkStatus
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.di.AppShell
|
|
||||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
|
||||||
import com.zaneschepke.wireguardautotunnel.di.MainImmediateDispatcher
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.AutoTunnelState
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.NetworkState
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.network.InternetConnectivityMonitor
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.network.NetworkMonitor
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.ConnectivityState
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
|
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||||
|
import com.zaneschepke.wireguardautotunnel.di.MainImmediateDispatcher
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
|
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
|
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.events.KillSwitchEvent
|
import com.zaneschepke.wireguardautotunnel.domain.events.KillSwitchEvent
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.state.AutoTunnelState
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.state.NetworkState
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.Tunnels
|
import com.zaneschepke.wireguardautotunnel.util.extensions.Tunnels
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.getCurrentWifiName
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.debounce
|
import kotlinx.coroutines.flow.debounce
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -49,10 +47,6 @@ import javax.inject.Provider
|
|||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class AutoTunnelService : LifecycleService() {
|
class AutoTunnelService : LifecycleService() {
|
||||||
|
|
||||||
@Inject
|
|
||||||
@AppShell
|
|
||||||
lateinit var rootShell: Provider<RootShell>
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var networkMonitor: NetworkMonitor
|
lateinit var networkMonitor: NetworkMonitor
|
||||||
|
|
||||||
@@ -100,9 +94,11 @@ class AutoTunnelService : LifecycleService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
super.onStartCommand(intent, flags, startId)
|
||||||
Timber.d("onStartCommand executed with startId: $startId")
|
Timber.d("onStartCommand executed with startId: $startId")
|
||||||
serviceManager.autoTunnelService.complete(this)
|
serviceManager.autoTunnelService.complete(this)
|
||||||
return super.onStartCommand(intent, flags, startId)
|
start()
|
||||||
|
return START_STICKY
|
||||||
}
|
}
|
||||||
|
|
||||||
fun start() {
|
fun start() {
|
||||||
@@ -161,49 +157,54 @@ class AutoTunnelService : LifecycleService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun buildNetworkState(connectivityState: ConnectivityState): NetworkState {
|
private fun buildNetworkState(networkStatus: NetworkStatus): NetworkState {
|
||||||
return with(autoTunnelStateFlow.value.networkState) {
|
return with(autoTunnelStateFlow.value.networkState) {
|
||||||
val wifiName = when {
|
val wifiName = when (networkStatus) {
|
||||||
connectivityState.wifiAvailable &&
|
is NetworkStatus.Connected -> {
|
||||||
(wifiName == null || wifiName == Constants.UNREADABLE_SSID || networkMonitor.didWifiChangeSinceLastCapabilitiesQuery) -> {
|
networkStatus.wifiSsid
|
||||||
networkMonitor.getWifiCapabilities()?.let { getWifiName(it) } ?: wifiName
|
|
||||||
}
|
}
|
||||||
!connectivityState.wifiAvailable -> null
|
else -> null
|
||||||
else -> wifiName
|
|
||||||
}
|
}
|
||||||
copy(
|
copy(
|
||||||
isWifiConnected = connectivityState.wifiAvailable,
|
isWifiConnected = networkStatus.wifiConnected,
|
||||||
isMobileDataConnected = connectivityState.cellularAvailable,
|
isMobileDataConnected = networkStatus.cellularConnected,
|
||||||
isEthernetConnected = isEthernetConnected,
|
isEthernetConnected = networkStatus.ethernetConnected,
|
||||||
wifiName = wifiName,
|
wifiName = wifiName,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
private fun startAutoTunnelStateJob() = lifecycleScope.launch(ioDispatcher) {
|
private fun startAutoTunnelStateJob() = lifecycleScope.launch(ioDispatcher) {
|
||||||
combine(
|
combine(
|
||||||
combineSettings(),
|
combineSettings(),
|
||||||
networkMonitor.status.map {
|
appDataRepository.get().settings.flow
|
||||||
buildNetworkState(it)
|
.distinctUntilChanged { old, new -> old.isKernelEnabled == new.isKernelEnabled } // Only emit when isKernelEnabled changes
|
||||||
}.distinctUntilChanged(),
|
.flatMapLatest {
|
||||||
|
networkMonitor.networkStatusFlow
|
||||||
|
.flowOn(ioDispatcher)
|
||||||
|
.map { buildNetworkState(it) }
|
||||||
|
}
|
||||||
|
.distinctUntilChanged(),
|
||||||
) { double, networkState ->
|
) { double, networkState ->
|
||||||
AutoTunnelState(tunnelManager.activeTunnels().value, networkState, double.first, double.second)
|
AutoTunnelState(
|
||||||
|
tunnelManager.activeTunnels.value,
|
||||||
|
networkState,
|
||||||
|
double.first,
|
||||||
|
double.second,
|
||||||
|
)
|
||||||
}.collect { state ->
|
}.collect { state ->
|
||||||
autoTunnelStateFlow.update {
|
autoTunnelStateFlow.update {
|
||||||
it.copy(activeTunnels = state.activeTunnels, networkState = state.networkState, settings = state.settings, tunnels = state.tunnels)
|
it.copy(
|
||||||
|
activeTunnels = state.activeTunnels,
|
||||||
|
networkState = state.networkState,
|
||||||
|
settings = state.settings,
|
||||||
|
tunnels = state.tunnels,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getWifiName(wifiCapabilities: NetworkCapabilities): String? {
|
|
||||||
val setting = appDataRepository.get().settings.get()
|
|
||||||
return if (setting.isWifiNameByShellEnabled) {
|
|
||||||
rootShell.get().getCurrentWifiName()
|
|
||||||
} else {
|
|
||||||
InternetConnectivityMonitor.getNetworkName(wifiCapabilities, this@AutoTunnelService)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun combineSettings(): Flow<Pair<AppSettings, Tunnels>> {
|
private fun combineSettings(): Flow<Pair<AppSettings, Tunnels>> {
|
||||||
return combine(
|
return combine(
|
||||||
appDataRepository.get().settings.flow,
|
appDataRepository.get().settings.flow,
|
||||||
@@ -240,7 +241,7 @@ class AutoTunnelService : LifecycleService() {
|
|||||||
Timber.d("Starting with debounce delay of: ${settings.debounceDelaySeconds} seconds")
|
Timber.d("Starting with debounce delay of: ${settings.debounceDelaySeconds} seconds")
|
||||||
autoTunnelStateFlow.debounce(settings.debounceDelayMillis()).collect { watcherState ->
|
autoTunnelStateFlow.debounce(settings.debounceDelayMillis()).collect { watcherState ->
|
||||||
if (watcherState == defaultState) return@collect
|
if (watcherState == defaultState) return@collect
|
||||||
Timber.d("New auto tunnel state emitted")
|
Timber.d("New auto tunnel state emitted ${watcherState.networkState}")
|
||||||
when (val event = watcherState.asAutoTunnelEvent()) {
|
when (val event = watcherState.asAutoTunnelEvent()) {
|
||||||
is AutoTunnelEvent.Start -> (event.tunnelConf ?: appDataRepository.get().getPrimaryOrFirstTunnel())?.let {
|
is AutoTunnelEvent.Start -> (event.tunnelConf ?: appDataRepository.get().getPrimaryOrFirstTunnel())?.let {
|
||||||
tunnelManager.startTunnel(it)
|
tunnelManager.startTunnel(it)
|
||||||
|
|||||||
+26
-19
@@ -4,57 +4,61 @@ import android.content.Intent
|
|||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.service.quicksettings.Tile
|
import android.service.quicksettings.Tile
|
||||||
import android.service.quicksettings.TileService
|
import android.service.quicksettings.TileService
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.LifecycleRegistry
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class AutoTunnelControlTile : TileService() {
|
class AutoTunnelControlTile : TileService(), LifecycleOwner {
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var appDataRepository: AppDataRepository
|
lateinit var appDataRepository: AppDataRepository
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var serviceManager: ServiceManager
|
lateinit var serviceManager: ServiceManager
|
||||||
|
|
||||||
@Inject
|
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
|
||||||
@ApplicationScope
|
|
||||||
lateinit var applicationScope: CoroutineScope
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
serviceManager.autoTunnelTile.complete(this)
|
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
serviceManager.autoTunnelTile = CompletableDeferred()
|
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartListening() {
|
override fun onStartListening() {
|
||||||
super.onStartListening()
|
super.onStartListening()
|
||||||
serviceManager.autoTunnelTile.complete(this)
|
Timber.d("Start listening called for auto tunnel tile")
|
||||||
applicationScope.launch {
|
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
|
||||||
if (appDataRepository.tunnels.getAll().isEmpty()) return@launch setUnavailable()
|
lifecycleScope.launch {
|
||||||
updateTileState()
|
serviceManager.autoTunnelActive.collect {
|
||||||
|
if (it) setActive() else setInactive()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
lifecycleScope.launch {
|
||||||
|
appDataRepository.tunnels.flow.collect {
|
||||||
fun updateTileState() {
|
if (it.isEmpty()) {
|
||||||
serviceManager.autoTunnelActive.value.let {
|
setUnavailable()
|
||||||
if (it) setActive() else setInactive()
|
} else {
|
||||||
|
if (qsTile.state == Tile.STATE_ACTIVE) setInactive()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClick() {
|
override fun onClick() {
|
||||||
super.onClick()
|
super.onClick()
|
||||||
unlockAndRun {
|
unlockAndRun {
|
||||||
applicationScope.launch {
|
lifecycleScope.launch {
|
||||||
if (serviceManager.autoTunnelActive.value) {
|
if (serviceManager.autoTunnelActive.value) {
|
||||||
serviceManager.stopAutoTunnel()
|
serviceManager.stopAutoTunnel()
|
||||||
setInactive()
|
setInactive()
|
||||||
@@ -97,4 +101,7 @@ class AutoTunnelControlTile : TileService() {
|
|||||||
qsTile.updateTile()
|
qsTile.updateTile()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override val lifecycle: Lifecycle
|
||||||
|
get() = lifecycleRegistry
|
||||||
}
|
}
|
||||||
|
|||||||
+32
-20
@@ -5,56 +5,66 @@ import android.os.Build
|
|||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.service.quicksettings.Tile
|
import android.service.quicksettings.Tile
|
||||||
import android.service.quicksettings.TileService
|
import android.service.quicksettings.TileService
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.LifecycleRegistry
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class TunnelControlTile : TileService() {
|
class TunnelControlTile : TileService(), LifecycleOwner {
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var appDataRepository: AppDataRepository
|
lateinit var appDataRepository: AppDataRepository
|
||||||
|
|
||||||
@Inject
|
|
||||||
@ApplicationScope
|
|
||||||
lateinit var applicationScope: CoroutineScope
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var serviceManager: ServiceManager
|
lateinit var serviceManager: ServiceManager
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var tunnelManager: TunnelManager
|
lateinit var tunnelManager: TunnelManager
|
||||||
|
|
||||||
|
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
|
||||||
|
|
||||||
|
private var isCollecting = false
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
serviceManager.tunnelControlTile.complete(this)
|
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
serviceManager.tunnelControlTile = CompletableDeferred()
|
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartListening() {
|
override fun onStartListening() {
|
||||||
super.onStartListening()
|
super.onStartListening()
|
||||||
Timber.d("Start listening called")
|
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
|
||||||
serviceManager.tunnelControlTile.complete(this)
|
Timber.d("Start listening called for tunnel tile")
|
||||||
applicationScope.launch {
|
if (isCollecting) return
|
||||||
updateTileState()
|
isCollecting = true
|
||||||
|
lifecycleScope.launch {
|
||||||
|
tunnelManager.activeTunnels.collect {
|
||||||
|
updateTileState()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateTileState() = applicationScope.launch {
|
private fun updateTileState() = lifecycleScope.launch {
|
||||||
if (appDataRepository.tunnels.getAll().isEmpty()) return@launch setUnavailable()
|
val tunnels = appDataRepository.tunnels.getAll()
|
||||||
with(tunnelManager.activeTunnels().value) {
|
if (tunnels.isEmpty()) return@launch setUnavailable()
|
||||||
if (isNotEmpty()) return@launch updateTile(if (size == 1) first().tunName else getString(R.string.multiple), true)
|
with(tunnelManager.activeTunnels.value) {
|
||||||
|
if (isNotEmpty()) if (size == 1) {
|
||||||
|
tunnels.firstOrNull { it.id == keys.first().id }?.let { return@launch updateTile(it.tunName, true) }
|
||||||
|
} else {
|
||||||
|
return@launch updateTile(getString(R.string.multiple), true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
appDataRepository.getStartTunnelConfig()?.let {
|
appDataRepository.getStartTunnelConfig()?.let {
|
||||||
updateTile(it.tunName, false)
|
updateTile(it.tunName, false)
|
||||||
@@ -64,8 +74,8 @@ class TunnelControlTile : TileService() {
|
|||||||
override fun onClick() {
|
override fun onClick() {
|
||||||
super.onClick()
|
super.onClick()
|
||||||
unlockAndRun {
|
unlockAndRun {
|
||||||
applicationScope.launch {
|
lifecycleScope.launch {
|
||||||
if (tunnelManager.activeTunnels().value.isNotEmpty()) return@launch tunnelManager.stopTunnel()
|
if (tunnelManager.activeTunnels.value.isNotEmpty()) return@launch tunnelManager.stopTunnel()
|
||||||
appDataRepository.getStartTunnelConfig()?.let {
|
appDataRepository.getStartTunnelConfig()?.let {
|
||||||
tunnelManager.startTunnel(it)
|
tunnelManager.startTunnel(it)
|
||||||
}
|
}
|
||||||
@@ -127,4 +137,6 @@ class TunnelControlTile : TileService() {
|
|||||||
Timber.e(it)
|
Timber.e(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
override val lifecycle: Lifecycle
|
||||||
|
get() = lifecycleRegistry
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-7
@@ -6,6 +6,7 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
|||||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelService
|
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelService
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -21,6 +22,9 @@ class ShortcutsActivity : ComponentActivity() {
|
|||||||
@Inject
|
@Inject
|
||||||
lateinit var serviceManager: ServiceManager
|
lateinit var serviceManager: ServiceManager
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var tunnelManager: TunnelManager
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
@ApplicationScope
|
@ApplicationScope
|
||||||
lateinit var applicationScope: CoroutineScope
|
lateinit var applicationScope: CoroutineScope
|
||||||
@@ -39,13 +43,13 @@ class ShortcutsActivity : ComponentActivity() {
|
|||||||
.firstOrNull { it.tunName == tunnelName }
|
.firstOrNull { it.tunName == tunnelName }
|
||||||
} ?: appDataRepository.getStartTunnelConfig()
|
} ?: appDataRepository.getStartTunnelConfig()
|
||||||
Timber.d("Shortcut action on name: ${tunnelConfig?.tunName}")
|
Timber.d("Shortcut action on name: ${tunnelConfig?.tunName}")
|
||||||
// tunnelConfig?.let {
|
tunnelConfig?.let {
|
||||||
// when (intent.action) {
|
when (intent.action) {
|
||||||
// Action.START.name -> tunnelService.get().startTunnel(it)
|
Action.START.name -> tunnelManager.startTunnel(it)
|
||||||
// Action.STOP.name -> tunnelService.get().stopTunnel()
|
Action.STOP.name -> tunnelManager.stopTunnel()
|
||||||
// else -> Unit
|
else -> Unit
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
}
|
}
|
||||||
AutoTunnelService::class.java.simpleName, LEGACY_AUTO_TUNNEL_SERVICE_NAME -> {
|
AutoTunnelService::class.java.simpleName, LEGACY_AUTO_TUNNEL_SERVICE_NAME -> {
|
||||||
when (intent.action) {
|
when (intent.action) {
|
||||||
|
|||||||
@@ -1,187 +1,70 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||||
|
|
||||||
|
import com.wireguard.android.backend.BackendException
|
||||||
|
import com.wireguard.android.backend.Tunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.core.network.NetworkMonitor
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
|
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider.Companion.CHECK_INTERVAL
|
|
||||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendError
|
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendError
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
|
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.cancelWithMessage
|
import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendError
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.coroutineScope
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.isActive
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
open class BaseTunnel(
|
abstract class BaseTunnel(
|
||||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||||
@ApplicationScope private val applicationScope: CoroutineScope,
|
@ApplicationScope private val applicationScope: CoroutineScope,
|
||||||
private val networkMonitor: NetworkMonitor,
|
|
||||||
private val appDataRepository: AppDataRepository,
|
private val appDataRepository: AppDataRepository,
|
||||||
private val serviceManager: ServiceManager,
|
private val serviceManager: ServiceManager,
|
||||||
private val notificationManager: NotificationManager,
|
private val notificationManager: NotificationManager,
|
||||||
) : TunnelProvider {
|
) : TunnelProvider {
|
||||||
|
|
||||||
internal val tunnels = MutableStateFlow<List<TunnelConf>>(emptyList())
|
private val activeTuns = MutableStateFlow<Map<TunnelConf, TunnelState>>(emptyMap())
|
||||||
|
override val activeTunnels = activeTuns.asStateFlow()
|
||||||
|
|
||||||
private val tunnelJobs = mutableMapOf<TunnelConf, Job>()
|
private val isBounce = AtomicBoolean(false)
|
||||||
|
|
||||||
private val isNetworkAvailable = AtomicBoolean(false)
|
private val mutex = Mutex()
|
||||||
|
|
||||||
init {
|
abstract suspend fun startBackend(tunnel: TunnelConf)
|
||||||
applicationScope.launch(ioDispatcher) {
|
|
||||||
launch {
|
|
||||||
startNetworkJob()
|
|
||||||
}
|
|
||||||
tunnels.collect { tuns ->
|
|
||||||
val previousTuns = tunnelJobs.keys.toSet()
|
|
||||||
val newTuns = tuns - previousTuns
|
|
||||||
val removedItems = previousTuns - tuns.toSet()
|
|
||||||
|
|
||||||
newTuns.forEach { tun ->
|
abstract fun stopBackend(tunnel: TunnelConf)
|
||||||
Timber.d("Starting tunnel jobs for tun ${tun.name}")
|
|
||||||
tunnelJobs[tun] = startTunnelJobs(tun)
|
|
||||||
}
|
|
||||||
|
|
||||||
removedItems.forEach { tun ->
|
private fun findActiveTunnel(id: Int): TunnelConf? = activeTuns.value.keys.find { it.id == id }
|
||||||
tunnelJobs[tun]?.cancelWithMessage("Canceling tunnel jobs for tunnel: ${tun.name}")
|
|
||||||
tunnelJobs.remove(tun)
|
private fun isTunnelActive(id: Int) = (activeTuns.value.any { it.key.id == id })
|
||||||
}
|
|
||||||
serviceManager.updateTunnelTile()
|
private fun handleBackendThrowable(throwable: Throwable) {
|
||||||
}
|
val backendError = when (throwable) {
|
||||||
|
is BackendException -> throwable.toBackendError()
|
||||||
|
is org.amnezia.awg.backend.BackendException -> throwable.toBackendError()
|
||||||
|
else -> BackendError.Unknown
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun startTunnelJobs(tunnel: TunnelConf) = applicationScope.launch(ioDispatcher) {
|
|
||||||
launch {
|
|
||||||
startTunnelStatisticsJob(tunnel)
|
|
||||||
}
|
|
||||||
launch {
|
|
||||||
startPingJob(tunnel)
|
|
||||||
}
|
|
||||||
launch {
|
|
||||||
startTunnelConfigChangeJob(tunnel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun bounceTunnel(tunnelConf: TunnelConf) {
|
|
||||||
if (tunnels.value.any { it.id == tunnelConf.id }) {
|
|
||||||
toggleTunnel(tunnelConf, TunnelStatus.DOWN)
|
|
||||||
toggleTunnel(tunnelConf, TunnelStatus.UP)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun runningTunnelNames(): Set<String> {
|
|
||||||
return emptySet()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun activeTunnels(): StateFlow<List<TunnelConf>> {
|
|
||||||
return tunnels.asStateFlow()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun startTunnel(tunnelConf: TunnelConf) {
|
|
||||||
if (tunnels.value.any { it.id == tunnelConf.id }) return Timber.w("Tunnel already running")
|
|
||||||
serviceManager.startBackgroundService(tunnelConf)
|
|
||||||
appDataRepository.tunnels.save(tunnelConf.copy(isActive = true))
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun stopTunnel(tunnelConf: TunnelConf?) {
|
|
||||||
}
|
|
||||||
|
|
||||||
open suspend fun toggleTunnel(tunnelConf: TunnelConf, state: TunnelStatus) {
|
|
||||||
}
|
|
||||||
|
|
||||||
open suspend fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics {
|
|
||||||
throw NotImplementedError("Get statistics not implemented in base class")
|
|
||||||
}
|
|
||||||
|
|
||||||
internal suspend fun onTunnelStop(tunnelConf: TunnelConf) {
|
|
||||||
appDataRepository.tunnels.save(tunnelConf.copy(isActive = false))
|
|
||||||
removeFromActiveTunnels(tunnelConf)
|
|
||||||
if (tunnels.value.isEmpty()) serviceManager.stopBackgroundService()
|
|
||||||
}
|
|
||||||
|
|
||||||
internal suspend fun stopAllTunnels() {
|
|
||||||
tunnels.value.forEach {
|
|
||||||
stopTunnel(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun addToActiveTunnels(conf: TunnelConf) {
|
|
||||||
tunnels.update {
|
|
||||||
it.toMutableList().apply {
|
|
||||||
add(conf)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun removeFromActiveTunnels(conf: TunnelConf) {
|
|
||||||
tunnels.update {
|
|
||||||
it.toMutableList().apply {
|
|
||||||
remove(conf)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun startNetworkJob() = coroutineScope {
|
|
||||||
networkMonitor.status.distinctUntilChanged().collect {
|
|
||||||
isNetworkAvailable.set(!it.allOffline)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun startPingJob(tunnel: TunnelConf) = coroutineScope {
|
|
||||||
while (isActive) {
|
|
||||||
if (isNetworkAvailable.get() && tunnel.isActive) {
|
|
||||||
val pingResult = tunnel.pingTunnel(ioDispatcher)
|
|
||||||
handlePingResult(tunnel, pingResult)
|
|
||||||
}
|
|
||||||
delay(CHECK_INTERVAL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun handlePingResult(tunnel: TunnelConf, pingResult: List<Boolean>) {
|
|
||||||
if (pingResult.contains(false)) {
|
|
||||||
if (isNetworkAvailable.get()) {
|
|
||||||
Timber.i("Ping result: target was not reachable, bouncing the tunnel")
|
|
||||||
bounceTunnel(tunnel)
|
|
||||||
delay(tunnel.pingCooldown ?: Constants.PING_COOLDOWN)
|
|
||||||
} else {
|
|
||||||
Timber.i("Ping result: target was not reachable, but no network available")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Timber.i("Ping result: all ping targets were reached successfully")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun handleBackendThrowable(backendError: BackendError) {
|
|
||||||
val message = when (backendError) {
|
val message = when (backendError) {
|
||||||
BackendError.Config -> StringValue.StringResource(R.string.start_failed_config)
|
BackendError.Config -> StringValue.StringResource(R.string.start_failed_config)
|
||||||
BackendError.DNS -> StringValue.StringResource(R.string.dns_error)
|
BackendError.DNS -> StringValue.StringResource(R.string.dns_error)
|
||||||
BackendError.Unauthorized -> StringValue.StringResource(R.string.unauthorized)
|
BackendError.Unauthorized -> StringValue.StringResource(R.string.unauthorized)
|
||||||
|
BackendError.Unknown -> StringValue.StringResource(R.string.unknown_error)
|
||||||
}
|
}
|
||||||
if (WireGuardAutoTunnel.isForeground()) {
|
if (WireGuardAutoTunnel.isForeground()) {
|
||||||
SnackbarController.showMessage(message)
|
SnackbarController.showMessage(message)
|
||||||
@@ -197,23 +80,134 @@ open class BaseTunnel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun startTunnelConfigChangeJob(tunnel: TunnelConf) = coroutineScope {
|
private fun updateTunnelState(tunnelConf: TunnelConf, state: TunnelStatus? = null, stats: TunnelStatistics? = null) {
|
||||||
appDataRepository.tunnels.flow.collect { storageTuns ->
|
applicationScope.launch(ioDispatcher) {
|
||||||
storageTuns.firstOrNull { it.id == tunnel.id }?.let { storageTun ->
|
mutex.withLock {
|
||||||
if (tunnel.isQuickConfigChanged(storageTun) || tunnel.isPingConfigMatching(storageTun)) {
|
activeTuns.update { current ->
|
||||||
bounceTunnel(tunnel)
|
val originalConf = current.getKeyById(tunnelConf.id) ?: tunnelConf
|
||||||
|
val existingState = current.getValueById(tunnelConf.id) ?: TunnelState()
|
||||||
|
val newState = state ?: existingState.state
|
||||||
|
if (newState == TunnelStatus.DOWN) {
|
||||||
|
Timber.d("Removing tunnel ${tunnelConf.id} from activeTunnels as state is DOWN")
|
||||||
|
current - originalConf
|
||||||
|
} else if (existingState.state == newState && stats == null) {
|
||||||
|
Timber.d("Skipping redundant state update for ${tunnelConf.id}: $newState")
|
||||||
|
current
|
||||||
|
} else {
|
||||||
|
val updated = existingState.copy(
|
||||||
|
state = newState,
|
||||||
|
statistics = stats ?: existingState.statistics,
|
||||||
|
)
|
||||||
|
current + (originalConf to updated)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun startTunnelStatisticsJob(tunnel: TunnelConf) = coroutineScope {
|
protected suspend fun stopActiveTunnels() {
|
||||||
while (isActive) {
|
activeTunnels.value.forEach { (config, state) ->
|
||||||
val stats = getStatistics(tunnel)
|
if (state.state.isUp()) {
|
||||||
tunnel.state.update {
|
stopTunnel(config)
|
||||||
it.copy(statistics = stats)
|
delay(300)
|
||||||
}
|
}
|
||||||
delay(CHECK_INTERVAL)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun configureTunnelCallbacks(tunnelConf: TunnelConf) {
|
||||||
|
Timber.d("Configuring TunnelConf instance: ${tunnelConf.hashCode()}")
|
||||||
|
|
||||||
|
tunnelConf.setStateChangeCallback { state ->
|
||||||
|
Timber.d("State change callback triggered for tunnel ${tunnelConf.id}: ${tunnelConf.tunName} with state $state at ${System.currentTimeMillis()}")
|
||||||
|
when (state) {
|
||||||
|
is Tunnel.State -> updateTunnelState(tunnelConf, state.asTunnelState())
|
||||||
|
is org.amnezia.awg.backend.Tunnel.State -> updateTunnelState(tunnelConf, state.asTunnelState())
|
||||||
|
}
|
||||||
|
serviceManager.updateTunnelTile()
|
||||||
|
}
|
||||||
|
tunnelConf.setTunnelStatsCallback {
|
||||||
|
val stats = getStatistics(tunnelConf)
|
||||||
|
updateTunnelState(tunnelConf, null, stats)
|
||||||
|
}
|
||||||
|
tunnelConf.setBounceTunnelCallback(::bounceTunnel)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun startTunnel(tunnelConf: TunnelConf) {
|
||||||
|
applicationScope.launch(ioDispatcher) {
|
||||||
|
runCatching {
|
||||||
|
if (isTunnelActive(tunnelConf.id)) return@launch
|
||||||
|
startTunnelInner(tunnelConf)
|
||||||
|
}.onFailure { exception ->
|
||||||
|
Timber.e(exception, "Failed to start tunnel ${tunnelConf.id} userspace")
|
||||||
|
stopTunnel(tunnelConf)
|
||||||
|
handleBackendThrowable(exception)
|
||||||
|
}.onSuccess {
|
||||||
|
Timber.i("Tunnel ${tunnelConf.id} started successfully")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun startTunnelInner(tunnelConf: TunnelConf) {
|
||||||
|
mutex.withLock {
|
||||||
|
configureTunnelCallbacks(tunnelConf)
|
||||||
|
startBackend(tunnelConf)
|
||||||
|
saveTunnelActiveState(tunnelConf, true)
|
||||||
|
if (!isBounce.get()) serviceManager.startTunnelForegroundService(tunnelConf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun saveTunnelActiveState(tunnelConf: TunnelConf, active: Boolean) {
|
||||||
|
val tunnelCopy = tunnelConf.copyWithCallback(isActive = active)
|
||||||
|
appDataRepository.tunnels.save(tunnelCopy)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stopTunnel(tunnelConf: TunnelConf?) {
|
||||||
|
applicationScope.launch(ioDispatcher) {
|
||||||
|
runCatching {
|
||||||
|
if (tunnelConf == null) return@launch stopActiveTunnels()
|
||||||
|
stopTunnelInner(tunnelConf)
|
||||||
|
}.onFailure { e ->
|
||||||
|
Timber.e(e, "Failed to stop tunnel ${tunnelConf?.id}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun stopTunnelInner(tunnelConf: TunnelConf) {
|
||||||
|
mutex.withLock {
|
||||||
|
val tunnel = findActiveTunnel(tunnelConf.id) ?: return
|
||||||
|
stopBackend(tunnel)
|
||||||
|
removeActiveTunnel(tunnel)
|
||||||
|
// use latest tunnel
|
||||||
|
saveTunnelActiveState(tunnelConf, false)
|
||||||
|
handleServiceChangesOnStop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleServiceChangesOnStop() {
|
||||||
|
if (activeTuns.value.isEmpty() && !isBounce.get()) return serviceManager.stopTunnelForegroundService()
|
||||||
|
val nextActive = activeTuns.value.keys.firstOrNull()
|
||||||
|
if (nextActive != null) {
|
||||||
|
Timber.d("Next active tunnel: ${nextActive.id}")
|
||||||
|
serviceManager.updateTunnelForegroundServiceNotification(nextActive)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeActiveTunnel(tunnelConf: TunnelConf) {
|
||||||
|
activeTuns.update { current ->
|
||||||
|
current.toMutableMap().apply { remove(tunnelConf) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun bounceTunnel(tunnelConf: TunnelConf) {
|
||||||
|
applicationScope.launch(ioDispatcher) {
|
||||||
|
Timber.i("Bounce tunnel ${tunnelConf.name}")
|
||||||
|
isBounce.set(true)
|
||||||
|
stopTunnel(tunnelConf)
|
||||||
|
delay(300)
|
||||||
|
startTunnel(tunnelConf)
|
||||||
|
isBounce.set(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun runningTunnelNames(): Set<String> = activeTuns.value.keys.map { it.tunName }.toSet()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||||
|
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||||
|
|
||||||
|
fun Map<TunnelConf, TunnelState>.allDown(): Boolean {
|
||||||
|
return this.all { it.value.state.isDown() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Map<TunnelConf, TunnelState>.hasActive(): Boolean {
|
||||||
|
return this.any { it.value.state.isUp() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Map<TunnelConf, TunnelState>.getValueById(id: Int): TunnelState? {
|
||||||
|
val key = this.keys.find { it.id == id }
|
||||||
|
return key?.let { this@getValueById[it] }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Map<TunnelConf, TunnelState>.getKeyById(id: Int): TunnelConf? {
|
||||||
|
return this.keys.find { it.id == id }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Map<TunnelConf, TunnelState>.isUp(tunnelConf: TunnelConf): Boolean {
|
||||||
|
return this.getValueById(tunnelConf.id)?.state?.isUp() ?: false
|
||||||
|
}
|
||||||
@@ -1,23 +1,18 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||||
|
|
||||||
import com.wireguard.android.backend.Backend
|
import com.wireguard.android.backend.Backend
|
||||||
import com.wireguard.android.backend.BackendException
|
|
||||||
import com.wireguard.android.backend.Tunnel
|
import com.wireguard.android.backend.Tunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.core.network.NetworkMonitor
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.WireGuardStatistics
|
import com.zaneschepke.wireguardautotunnel.domain.state.WireGuardStatistics
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendError
|
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -28,49 +23,24 @@ class KernelTunnel @Inject constructor(
|
|||||||
appDataRepository: AppDataRepository,
|
appDataRepository: AppDataRepository,
|
||||||
notificationManager: NotificationManager,
|
notificationManager: NotificationManager,
|
||||||
private val backend: Backend,
|
private val backend: Backend,
|
||||||
networkMonitor: NetworkMonitor,
|
) : BaseTunnel(ioDispatcher, applicationScope, appDataRepository, serviceManager, notificationManager) {
|
||||||
) : BaseTunnel(ioDispatcher, applicationScope, networkMonitor, appDataRepository, serviceManager, notificationManager) {
|
|
||||||
|
|
||||||
override suspend fun startTunnel(tunnelConf: TunnelConf) {
|
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics? {
|
||||||
withContext(ioDispatcher) {
|
return try {
|
||||||
super.startTunnel(tunnelConf)
|
WireGuardStatistics(backend.getStatistics(tunnelConf))
|
||||||
runCatching {
|
} catch (e: Exception) {
|
||||||
backend.setState(tunnelConf, Tunnel.State.UP, tunnelConf.toWgConfig())
|
Timber.e(e)
|
||||||
addToActiveTunnels(tunnelConf)
|
null
|
||||||
}.onFailure {
|
|
||||||
onTunnelStop(tunnelConf)
|
|
||||||
if (it is BackendException) {
|
|
||||||
handleBackendThrowable(it.toBackendError())
|
|
||||||
} else {
|
|
||||||
Timber.e(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics {
|
override suspend fun startBackend(tunnel: TunnelConf) {
|
||||||
return WireGuardStatistics(backend.getStatistics(tunnelConf))
|
backend.setState(tunnel, Tunnel.State.UP, tunnel.toWgConfig())
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun stopTunnel(tunnelConf: TunnelConf?) {
|
override fun stopBackend(tunnel: TunnelConf) {
|
||||||
withContext(ioDispatcher) {
|
Timber.i("Stopping tunnel ${tunnel.id} kernel")
|
||||||
val tunnel = tunnels.value.firstOrNull { it.id == tunnelConf?.id }
|
backend.setState(tunnel, Tunnel.State.DOWN, tunnel.toWgConfig())
|
||||||
runCatching {
|
|
||||||
tunnel?.let {
|
|
||||||
backend.setState(it, Tunnel.State.DOWN, it.toWgConfig())
|
|
||||||
onTunnelStop(it)
|
|
||||||
} ?: stopAllTunnels()
|
|
||||||
}.onFailure {
|
|
||||||
Timber.e(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun toggleTunnel(tunnelConf: TunnelConf, status: TunnelStatus) {
|
|
||||||
when (status) {
|
|
||||||
TunnelStatus.UP -> backend.setState(tunnelConf, Tunnel.State.UP, tunnelConf.toWgConfig())
|
|
||||||
TunnelStatus.DOWN -> backend.setState(tunnelConf, Tunnel.State.DOWN, tunnelConf.toWgConfig())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
|
override suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
|
||||||
|
|||||||
+52
-54
@@ -4,20 +4,20 @@ import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
|||||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||||
import com.zaneschepke.wireguardautotunnel.di.Kernel
|
import com.zaneschepke.wireguardautotunnel.di.Kernel
|
||||||
import com.zaneschepke.wireguardautotunnel.di.Userspace
|
import com.zaneschepke.wireguardautotunnel.di.Userspace
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.withData
|
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.plus
|
import kotlinx.coroutines.plus
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class TunnelManager @Inject constructor(
|
class TunnelManager @Inject constructor(
|
||||||
@@ -28,71 +28,69 @@ class TunnelManager @Inject constructor(
|
|||||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||||
) : TunnelProvider {
|
) : TunnelProvider {
|
||||||
|
|
||||||
val appSettings: StateFlow<AppSettings?> = appDataRepository.settings.flow.stateIn(
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
scope = applicationScope.plus(ioDispatcher),
|
private val tunnelProviderFlow = appDataRepository.settings.flow
|
||||||
started = SharingStarted.Eagerly,
|
.filterNotNull()
|
||||||
initialValue = null,
|
.flatMapLatest { settings ->
|
||||||
)
|
MutableStateFlow(if (settings.isKernelEnabled) kernelTunnel else userspaceTunnel)
|
||||||
|
}
|
||||||
|
.stateIn(
|
||||||
|
scope = applicationScope.plus(ioDispatcher),
|
||||||
|
started = SharingStarted.Eagerly,
|
||||||
|
initialValue = userspaceTunnel,
|
||||||
|
)
|
||||||
|
|
||||||
override suspend fun activeTunnels(): StateFlow<List<TunnelConf>> {
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
return withContext(ioDispatcher) {
|
override val activeTunnels = appDataRepository.settings.flow
|
||||||
appSettings.filterNotNull().first().let {
|
.filterNotNull()
|
||||||
if (it.isKernelEnabled) return@withContext kernelTunnel.activeTunnels()
|
.flatMapLatest { settings ->
|
||||||
userspaceTunnel.activeTunnels()
|
if (settings.isKernelEnabled) {
|
||||||
|
kernelTunnel.activeTunnels
|
||||||
|
} else {
|
||||||
|
userspaceTunnel.activeTunnels
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.stateIn(
|
||||||
|
scope = applicationScope.plus(ioDispatcher),
|
||||||
|
started = SharingStarted.Eagerly,
|
||||||
|
initialValue = emptyMap(),
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun startTunnel(tunnelConf: TunnelConf) {
|
||||||
|
tunnelProviderFlow.value.startTunnel(tunnelConf)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun startTunnel(tunnelConf: TunnelConf) {
|
override fun stopTunnel(tunnelConf: TunnelConf?) {
|
||||||
appSettings.withData {
|
tunnelProviderFlow.value.stopTunnel(tunnelConf)
|
||||||
if (it.isKernelEnabled) return@withData kernelTunnel.startTunnel(tunnelConf)
|
|
||||||
userspaceTunnel.startTunnel(tunnelConf)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun stopTunnel(tunnelConf: TunnelConf?) {
|
override fun bounceTunnel(tunnelConf: TunnelConf) {
|
||||||
appSettings.withData {
|
tunnelProviderFlow.value.bounceTunnel(tunnelConf)
|
||||||
if (it.isKernelEnabled) return@withData kernelTunnel.stopTunnel(tunnelConf)
|
|
||||||
userspaceTunnel.stopTunnel(tunnelConf)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun bounceTunnel(tunnelConf: TunnelConf) {
|
|
||||||
appSettings.withData {
|
|
||||||
if (it.isKernelEnabled) return@withData kernelTunnel.stopTunnel(tunnelConf)
|
|
||||||
userspaceTunnel.stopTunnel(tunnelConf)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
|
override suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
|
||||||
appSettings.withData {
|
tunnelProviderFlow.value.setBackendState(backendState, allowedIps)
|
||||||
if (it.isKernelEnabled) return@withData kernelTunnel.setBackendState(backendState, allowedIps)
|
|
||||||
userspaceTunnel.setBackendState(backendState, allowedIps)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun runningTunnelNames(): Set<String> {
|
override suspend fun runningTunnelNames(): Set<String> {
|
||||||
appSettings.filterNotNull().first().let {
|
return tunnelProviderFlow.value.runningTunnelNames()
|
||||||
if (it.isKernelEnabled) return kernelTunnel.runningTunnelNames()
|
|
||||||
return userspaceTunnel.runningTunnelNames()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun restorePreviousState() {
|
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics? {
|
||||||
withContext(ioDispatcher) {
|
return tunnelProviderFlow.value.getStatistics(tunnelConf)
|
||||||
with(appDataRepository.settings.get()) {
|
}
|
||||||
if (isRestoreOnBootEnabled) {
|
|
||||||
val previouslyActiveTuns = appDataRepository.tunnels.getActive()
|
fun restorePreviousState() = applicationScope.launch(ioDispatcher) {
|
||||||
// handle kernel mode
|
val settings = appDataRepository.settings.get()
|
||||||
val tunsToStart = previouslyActiveTuns.filterNot { tun -> activeTunnels().value.any { tun.id == it.id } }
|
if (settings.isRestoreOnBootEnabled) {
|
||||||
if (isKernelEnabled) {
|
val previouslyActiveTuns = appDataRepository.tunnels.getActive()
|
||||||
return@withContext tunsToStart.forEach {
|
val tunsToStart = previouslyActiveTuns.filterNot { tun -> activeTunnels.value.any { tun.id == it.key.id } }
|
||||||
startTunnel(it)
|
if (settings.isKernelEnabled) {
|
||||||
}
|
return@launch tunsToStart.forEach {
|
||||||
}
|
startTunnel(it)
|
||||||
// handle userspace
|
|
||||||
if (activeTunnels().value.isEmpty()) tunsToStart.firstOrNull()?.let { startTunnel(it) }
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
tunsToStart.firstOrNull()?.let { startTunnel(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-14
@@ -2,23 +2,16 @@ package com.zaneschepke.wireguardautotunnel.core.tunnel
|
|||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
interface TunnelProvider {
|
interface TunnelProvider {
|
||||||
|
fun startTunnel(tunnelConf: TunnelConf)
|
||||||
suspend fun activeTunnels(): StateFlow<List<TunnelConf>>
|
fun stopTunnel(tunnelConf: TunnelConf? = null)
|
||||||
|
fun bounceTunnel(tunnelConf: TunnelConf)
|
||||||
suspend fun startTunnel(tunnelConf: TunnelConf)
|
|
||||||
|
|
||||||
suspend fun stopTunnel(tunnelConf: TunnelConf? = null)
|
|
||||||
|
|
||||||
suspend fun bounceTunnel(tunnelConf: TunnelConf)
|
|
||||||
|
|
||||||
suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>)
|
suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>)
|
||||||
|
|
||||||
suspend fun runningTunnelNames(): Set<String>
|
suspend fun runningTunnelNames(): Set<String>
|
||||||
|
fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics?
|
||||||
companion object {
|
val activeTunnels: StateFlow<Map<TunnelConf, TunnelState>>
|
||||||
const val CHECK_INTERVAL = 1_000L
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-44
@@ -1,22 +1,17 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||||
|
|
||||||
import com.wireguard.android.backend.BackendException
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.network.NetworkMonitor
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.AmneziaStatistics
|
import com.zaneschepke.wireguardautotunnel.domain.state.AmneziaStatistics
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asAmBackendState
|
import com.zaneschepke.wireguardautotunnel.util.extensions.asAmBackendState
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendError
|
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.amnezia.awg.backend.Backend
|
import org.amnezia.awg.backend.Backend
|
||||||
import org.amnezia.awg.backend.Tunnel
|
import org.amnezia.awg.backend.Tunnel
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
@@ -29,55 +24,33 @@ class UserspaceTunnel @Inject constructor(
|
|||||||
appDataRepository: AppDataRepository,
|
appDataRepository: AppDataRepository,
|
||||||
notificationManager: NotificationManager,
|
notificationManager: NotificationManager,
|
||||||
private val backend: Backend,
|
private val backend: Backend,
|
||||||
networkMonitor: NetworkMonitor,
|
) : BaseTunnel(ioDispatcher, applicationScope, appDataRepository, serviceManager, notificationManager) {
|
||||||
) : TunnelProvider, BaseTunnel(ioDispatcher, applicationScope, networkMonitor, appDataRepository, serviceManager, notificationManager) {
|
|
||||||
|
|
||||||
override suspend fun startTunnel(tunnelConf: TunnelConf) {
|
override suspend fun startBackend(tunnel: TunnelConf) {
|
||||||
withContext(ioDispatcher) {
|
stopActiveTunnels()
|
||||||
super.startTunnel(tunnelConf)
|
backend.setState(tunnel, Tunnel.State.UP, tunnel.toAmConfig())
|
||||||
runCatching {
|
|
||||||
backend.setState(tunnelConf, Tunnel.State.UP, tunnelConf.toAmConfig())
|
|
||||||
addToActiveTunnels(tunnelConf)
|
|
||||||
}.onFailure {
|
|
||||||
onTunnelStop(tunnelConf)
|
|
||||||
if (it is BackendException) {
|
|
||||||
handleBackendThrowable(it.toBackendError())
|
|
||||||
} else {
|
|
||||||
Timber.e(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics {
|
override fun stopBackend(tunnel: TunnelConf) {
|
||||||
return AmneziaStatistics(backend.getStatistics(tunnelConf))
|
Timber.i("Stopping tunnel ${tunnel.id} userspace")
|
||||||
}
|
backend.setState(tunnel, Tunnel.State.DOWN, tunnel.toAmConfig())
|
||||||
|
|
||||||
override suspend fun toggleTunnel(tunnelConf: TunnelConf, status: TunnelStatus) {
|
|
||||||
when (status) {
|
|
||||||
TunnelStatus.UP -> backend.setState(tunnelConf, Tunnel.State.UP, tunnelConf.toAmConfig())
|
|
||||||
TunnelStatus.DOWN -> backend.setState(tunnelConf, Tunnel.State.DOWN, tunnelConf.toAmConfig())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun stopTunnel(tunnelConf: TunnelConf?) {
|
|
||||||
withContext(ioDispatcher) {
|
|
||||||
runCatching {
|
|
||||||
tunnels.value.firstOrNull { it.id == tunnelConf?.id }?.let {
|
|
||||||
backend.setState(it, Tunnel.State.DOWN, it.toAmConfig())
|
|
||||||
onTunnelStop(it)
|
|
||||||
} ?: stopAllTunnels()
|
|
||||||
}.onFailure {
|
|
||||||
Timber.e(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
|
override suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
|
||||||
|
Timber.d("Setting backend state: $backendState with allowedIps: $allowedIps")
|
||||||
backend.setBackendState(backendState.asAmBackendState(), allowedIps)
|
backend.setBackendState(backendState.asAmBackendState(), allowedIps)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun runningTunnelNames(): Set<String> {
|
override suspend fun runningTunnelNames(): Set<String> {
|
||||||
return backend.runningTunnelNames
|
return backend.runningTunnelNames
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics? {
|
||||||
|
return try {
|
||||||
|
AmneziaStatistics(backend.getStatistics(tunnelConf))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "Failed to get stats for ${tunnelConf.tunName}")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ class ServiceWorker @AssistedInject constructor(
|
|||||||
Timber.i("Service worker started")
|
Timber.i("Service worker started")
|
||||||
with(appDataRepository.settings.get()) {
|
with(appDataRepository.settings.get()) {
|
||||||
if (isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) return@with serviceManager.startAutoTunnel(true)
|
if (isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) return@with serviceManager.startAutoTunnel(true)
|
||||||
if (tunnelManager.activeTunnels().value.isEmpty()) tunnelManager.restorePreviousState()
|
if (tunnelManager.activeTunnels.value.isEmpty()) tunnelManager.restorePreviousState()
|
||||||
}
|
}
|
||||||
Result.success()
|
Result.success()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.di
|
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.network.InternetConnectivityMonitor
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.network.NetworkMonitor
|
|
||||||
import dagger.Binds
|
|
||||||
import dagger.Module
|
|
||||||
import dagger.hilt.InstallIn
|
|
||||||
import dagger.hilt.components.SingletonComponent
|
|
||||||
|
|
||||||
@Module
|
|
||||||
@InstallIn(SingletonComponent::class)
|
|
||||||
abstract class ServiceModule {
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
abstract fun provideInternetConnectivityService(wifiService: InternetConnectivityMonitor): NetworkMonitor
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,8 @@ import android.content.Context
|
|||||||
import com.wireguard.android.backend.WgQuickBackend
|
import com.wireguard.android.backend.WgQuickBackend
|
||||||
import com.wireguard.android.util.RootShell
|
import com.wireguard.android.util.RootShell
|
||||||
import com.wireguard.android.util.ToolsInstaller
|
import com.wireguard.android.util.ToolsInstaller
|
||||||
import com.zaneschepke.wireguardautotunnel.core.network.NetworkMonitor
|
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
|
||||||
|
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.KernelTunnel
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.KernelTunnel
|
||||||
@@ -12,6 +13,7 @@ import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
|||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.UserspaceTunnel
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.UserspaceTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppSettingRepository
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
@@ -19,6 +21,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
|||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.amnezia.awg.backend.Backend
|
import org.amnezia.awg.backend.Backend
|
||||||
import org.amnezia.awg.backend.GoBackend
|
import org.amnezia.awg.backend.GoBackend
|
||||||
import org.amnezia.awg.backend.RootTunnelActionHandler
|
import org.amnezia.awg.backend.RootTunnelActionHandler
|
||||||
@@ -64,11 +67,10 @@ class TunnelModule {
|
|||||||
@ApplicationScope applicationScope: CoroutineScope,
|
@ApplicationScope applicationScope: CoroutineScope,
|
||||||
serviceManager: ServiceManager,
|
serviceManager: ServiceManager,
|
||||||
appDataRepository: AppDataRepository,
|
appDataRepository: AppDataRepository,
|
||||||
networkMonitor: NetworkMonitor,
|
|
||||||
notificationManager: NotificationManager,
|
notificationManager: NotificationManager,
|
||||||
backend: com.wireguard.android.backend.Backend,
|
backend: com.wireguard.android.backend.Backend,
|
||||||
): TunnelProvider {
|
): TunnelProvider {
|
||||||
return KernelTunnel(ioDispatcher, applicationScope, serviceManager, appDataRepository, notificationManager, backend, networkMonitor)
|
return KernelTunnel(ioDispatcher, applicationScope, serviceManager, appDataRepository, notificationManager, backend)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@@ -80,10 +82,9 @@ class TunnelModule {
|
|||||||
serviceManager: ServiceManager,
|
serviceManager: ServiceManager,
|
||||||
appDataRepository: AppDataRepository,
|
appDataRepository: AppDataRepository,
|
||||||
notificationManager: NotificationManager,
|
notificationManager: NotificationManager,
|
||||||
networkMonitor: NetworkMonitor,
|
|
||||||
backend: Backend,
|
backend: Backend,
|
||||||
): TunnelProvider {
|
): TunnelProvider {
|
||||||
return UserspaceTunnel(ioDispatcher, applicationScope, serviceManager, appDataRepository, notificationManager, backend, networkMonitor)
|
return UserspaceTunnel(ioDispatcher, applicationScope, serviceManager, appDataRepository, notificationManager, backend)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@@ -98,13 +99,20 @@ class TunnelModule {
|
|||||||
return TunnelManager(kernelTunnel, userspaceTunnel, appDataRepository, applicationScope, ioDispatcher)
|
return TunnelManager(kernelTunnel, userspaceTunnel, appDataRepository, applicationScope, ioDispatcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideNetworkMonitor(@ApplicationContext context: Context, settingsRepository: AppSettingRepository): NetworkMonitor {
|
||||||
|
return AndroidNetworkMonitor(context) { runBlocking { settingsRepository.get().isWifiNameByShellEnabled } }
|
||||||
|
}
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@Provides
|
@Provides
|
||||||
fun provideServiceManager(
|
fun provideServiceManager(
|
||||||
@ApplicationContext context: Context,
|
@ApplicationContext context: Context,
|
||||||
@IoDispatcher ioDispatcher: CoroutineDispatcher,
|
@IoDispatcher ioDispatcher: CoroutineDispatcher,
|
||||||
|
@ApplicationScope applicationScope: CoroutineScope,
|
||||||
appDataRepository: AppDataRepository,
|
appDataRepository: AppDataRepository,
|
||||||
): ServiceManager {
|
): ServiceManager {
|
||||||
return ServiceManager(context, ioDispatcher, appDataRepository)
|
return ServiceManager(context, ioDispatcher, applicationScope, appDataRepository)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,15 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.domain.entity
|
package com.zaneschepke.wireguardautotunnel.domain.entity
|
||||||
|
|
||||||
|
import com.wireguard.android.backend.Tunnel
|
||||||
import com.wireguard.config.Config
|
import com.wireguard.config.Config
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isReachable
|
import com.zaneschepke.wireguardautotunnel.util.extensions.isReachable
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.toWgQuickString
|
import com.zaneschepke.wireguardautotunnel.util.extensions.toWgQuickString
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.amnezia.awg.backend.Tunnel
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
data class TunnelConf(
|
data class TunnelConf(
|
||||||
@@ -29,10 +26,61 @@ data class TunnelConf(
|
|||||||
val pingCooldown: Long? = null,
|
val pingCooldown: Long? = null,
|
||||||
val pingIp: String? = null,
|
val pingIp: String? = null,
|
||||||
val isEthernetTunnel: Boolean = false,
|
val isEthernetTunnel: Boolean = false,
|
||||||
val isIpv4Preferred: Boolean = false,
|
val isIpv4Preferred: Boolean = true,
|
||||||
) : Tunnel, com.wireguard.android.backend.Tunnel {
|
@Transient
|
||||||
|
private var stateChangeCallback: ((Any) -> Unit)? = null,
|
||||||
|
@Transient
|
||||||
|
private var tunnelStatsCallback: (() -> Unit)? = null,
|
||||||
|
@Transient
|
||||||
|
private var bounceTunnelCallback: ((TunnelConf) -> Unit)? = null,
|
||||||
|
) : Tunnel, org.amnezia.awg.backend.Tunnel {
|
||||||
|
|
||||||
val state = MutableStateFlow(TunnelState())
|
fun setStateChangeCallback(callback: (Any) -> Unit) {
|
||||||
|
stateChangeCallback = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setTunnelStatsCallback(callback: (() -> Unit)) {
|
||||||
|
tunnelStatsCallback = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setBounceTunnelCallback(callback: (TunnelConf) -> Unit) {
|
||||||
|
bounceTunnelCallback = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
fun copyWithCallback(
|
||||||
|
id: Int = this.id,
|
||||||
|
tunName: String = this.tunName,
|
||||||
|
wgQuick: String = this.wgQuick,
|
||||||
|
tunnelNetworks: List<String> = this.tunnelNetworks,
|
||||||
|
isMobileDataTunnel: Boolean = this.isMobileDataTunnel,
|
||||||
|
isPrimaryTunnel: Boolean = this.isPrimaryTunnel,
|
||||||
|
amQuick: String = this.amQuick,
|
||||||
|
isActive: Boolean = this.isActive,
|
||||||
|
isPingEnabled: Boolean = this.isPingEnabled,
|
||||||
|
pingInterval: Long? = this.pingInterval,
|
||||||
|
pingCooldown: Long? = this.pingCooldown,
|
||||||
|
pingIp: String? = this.pingIp,
|
||||||
|
isEthernetTunnel: Boolean = this.isEthernetTunnel,
|
||||||
|
isIpv4Preferred: Boolean = this.isIpv4Preferred,
|
||||||
|
): TunnelConf {
|
||||||
|
return TunnelConf(
|
||||||
|
id, tunName, wgQuick, tunnelNetworks, isMobileDataTunnel, isPrimaryTunnel,
|
||||||
|
amQuick, isActive, isPingEnabled, pingInterval, pingCooldown, pingIp,
|
||||||
|
isEthernetTunnel, isIpv4Preferred,
|
||||||
|
).apply {
|
||||||
|
stateChangeCallback = this@TunnelConf.stateChangeCallback
|
||||||
|
tunnelStatsCallback = this@TunnelConf.tunnelStatsCallback
|
||||||
|
bounceTunnelCallback = this@TunnelConf.bounceTunnelCallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onUpdateStatistics() {
|
||||||
|
tunnelStatsCallback?.invoke()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bounceTunnel(tunnelConf: TunnelConf) {
|
||||||
|
bounceTunnelCallback?.invoke(tunnelConf)
|
||||||
|
}
|
||||||
|
|
||||||
fun toAmConfig(): org.amnezia.awg.config.Config {
|
fun toAmConfig(): org.amnezia.awg.config.Config {
|
||||||
return configFromAmQuick(amQuick.ifBlank { wgQuick })
|
return configFromAmQuick(amQuick.ifBlank { wgQuick })
|
||||||
@@ -42,49 +90,35 @@ data class TunnelConf(
|
|||||||
return configFromWgQuick(wgQuick)
|
return configFromWgQuick(wgQuick)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getName(): String {
|
override fun getName(): String = tunName
|
||||||
return tunName
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isIpv4ResolutionPreferred(): Boolean {
|
override fun isIpv4ResolutionPreferred(): Boolean = isIpv4Preferred
|
||||||
return isIpv4Preferred
|
|
||||||
|
override fun onStateChange(newState: org.amnezia.awg.backend.Tunnel.State) {
|
||||||
|
stateChangeCallback?.invoke(newState)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStateChange(newState: Tunnel.State) {
|
override fun onStateChange(newState: Tunnel.State) {
|
||||||
state.update {
|
stateChangeCallback?.invoke(newState)
|
||||||
it.copy(state = newState.asTunnelState())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStateChange(newState: com.wireguard.android.backend.Tunnel.State) {
|
fun isTunnelConfigChanged(updatedConf: TunnelConf): Boolean {
|
||||||
state.update {
|
return updatedConf.wgQuick != wgQuick || updatedConf.amQuick != amQuick || updatedConf.name != name
|
||||||
it.copy(state = newState.asTunnelState())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isQuickConfigChanged(updatedConf: TunnelConf): Boolean {
|
suspend fun isTunnelPingable(context: CoroutineContext): Boolean {
|
||||||
return updatedConf.wgQuick != wgQuick ||
|
|
||||||
updatedConf.amQuick != amQuick
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isPingConfigMatching(updatedConf: TunnelConf): Boolean {
|
|
||||||
return updatedConf.isPingEnabled == isPingEnabled &&
|
|
||||||
pingIp == updatedConf.pingIp &&
|
|
||||||
updatedConf.pingCooldown == pingCooldown &&
|
|
||||||
updatedConf.pingInterval == pingInterval
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun pingTunnel(context: CoroutineContext): List<Boolean> {
|
|
||||||
return withContext(context) {
|
return withContext(context) {
|
||||||
val config = toWgConfig()
|
val config = toWgConfig()
|
||||||
if (pingIp != null) {
|
if (pingIp != null) {
|
||||||
Timber.i("Pinging custom ip")
|
return@withContext InetAddress.getByName(pingIp)
|
||||||
listOf(InetAddress.getByName(pingIp).isReachable(Constants.PING_TIMEOUT.toInt()))
|
.isReachable(Constants.PING_TIMEOUT.toInt()).also {
|
||||||
} else {
|
Timber.i("Ping reachable $pingIp: $it")
|
||||||
Timber.i("Pinging all peers")
|
}
|
||||||
config.peers.map { peer ->
|
}
|
||||||
peer.isReachable(isIpv4Preferred)
|
config.peers.map { peer ->
|
||||||
}
|
peer.isReachable()
|
||||||
|
}.all { true }.also {
|
||||||
|
Timber.i("Ping of all peers reachable: $it")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,14 +126,14 @@ data class TunnelConf(
|
|||||||
companion object {
|
companion object {
|
||||||
fun configFromWgQuick(wgQuick: String): Config {
|
fun configFromWgQuick(wgQuick: String): Config {
|
||||||
val inputStream: InputStream = wgQuick.byteInputStream()
|
val inputStream: InputStream = wgQuick.byteInputStream()
|
||||||
return inputStream.bufferedReader(Charsets.UTF_8).use {
|
return inputStream.bufferedReader(StandardCharsets.UTF_8).use {
|
||||||
Config.parse(it)
|
Config.parse(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun configFromAmQuick(amQuick: String): org.amnezia.awg.config.Config {
|
fun configFromAmQuick(amQuick: String): org.amnezia.awg.config.Config {
|
||||||
val inputStream: InputStream = amQuick.byteInputStream()
|
val inputStream: InputStream = amQuick.byteInputStream()
|
||||||
return inputStream.bufferedReader(Charsets.UTF_8).use {
|
return inputStream.bufferedReader(StandardCharsets.UTF_8).use {
|
||||||
org.amnezia.awg.config.Config.parse(it)
|
org.amnezia.awg.config.Config.parse(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,4 +4,5 @@ sealed class BackendError() {
|
|||||||
data object DNS : BackendError()
|
data object DNS : BackendError()
|
||||||
data object Unauthorized : BackendError()
|
data object Unauthorized : BackendError()
|
||||||
data object Config : BackendError()
|
data object Config : BackendError()
|
||||||
|
data object Unknown : BackendError()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package com.zaneschepke.wireguardautotunnel.domain.enums
|
|||||||
enum class TunnelStatus {
|
enum class TunnelStatus {
|
||||||
UP,
|
UP,
|
||||||
DOWN,
|
DOWN,
|
||||||
|
STARTING,
|
||||||
|
STOPPING,
|
||||||
;
|
;
|
||||||
|
|
||||||
fun isDown(): Boolean {
|
fun isDown(): Boolean {
|
||||||
|
|||||||
+16
-13
@@ -1,5 +1,8 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.domain.state
|
package com.zaneschepke.wireguardautotunnel.domain.state
|
||||||
|
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.allDown
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.hasActive
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.isUp
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.events.KillSwitchEvent
|
import com.zaneschepke.wireguardautotunnel.domain.events.KillSwitchEvent
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
|
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||||
@@ -7,7 +10,7 @@ import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
|
|||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isMatchingToWildcardList
|
import com.zaneschepke.wireguardautotunnel.util.extensions.isMatchingToWildcardList
|
||||||
|
|
||||||
data class AutoTunnelState(
|
data class AutoTunnelState(
|
||||||
val activeTunnels: List<TunnelConf> = emptyList(),
|
val activeTunnels: Map<TunnelConf, TunnelState> = emptyMap(),
|
||||||
val networkState: NetworkState = NetworkState(),
|
val networkState: NetworkState = NetworkState(),
|
||||||
val settings: AppSettings = AppSettings(),
|
val settings: AppSettings = AppSettings(),
|
||||||
val tunnels: List<TunnelConf> = emptyList(),
|
val tunnels: List<TunnelConf> = emptyList(),
|
||||||
@@ -20,12 +23,12 @@ data class AutoTunnelState(
|
|||||||
private fun isMobileTunnelDataChangeNeeded(): Boolean {
|
private fun isMobileTunnelDataChangeNeeded(): Boolean {
|
||||||
val preferredTunnel = preferredMobileDataTunnel()
|
val preferredTunnel = preferredMobileDataTunnel()
|
||||||
return preferredTunnel != null &&
|
return preferredTunnel != null &&
|
||||||
activeTunnels.isNotEmpty() && !activeTunnels.any { it.id == preferredTunnel.id }
|
activeTunnels.isNotEmpty() && !activeTunnels.isUp(preferredTunnel)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isEthernetTunnelChangeNeeded(): Boolean {
|
private fun isEthernetTunnelChangeNeeded(): Boolean {
|
||||||
val preferredTunnel = preferredEthernetTunnel()
|
val preferredTunnel = preferredEthernetTunnel()
|
||||||
return preferredTunnel != null && activeTunnels.isNotEmpty() && !activeTunnels.any { it.id == preferredTunnel.id }
|
return preferredTunnel != null && activeTunnels.isNotEmpty() && !activeTunnels.isUp(preferredTunnel)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun preferredMobileDataTunnel(): TunnelConf? {
|
private fun preferredMobileDataTunnel(): TunnelConf? {
|
||||||
@@ -45,11 +48,11 @@ data class AutoTunnelState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun startOnEthernet(): Boolean {
|
private fun startOnEthernet(): Boolean {
|
||||||
return networkState.isEthernetConnected && settings.isTunnelOnEthernetEnabled && activeTunnels.isEmpty()
|
return networkState.isEthernetConnected && settings.isTunnelOnEthernetEnabled && activeTunnels.allDown()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun stopOnEthernet(): Boolean {
|
private fun stopOnEthernet(): Boolean {
|
||||||
return networkState.isEthernetConnected && !settings.isTunnelOnEthernetEnabled && activeTunnels.isNotEmpty()
|
return networkState.isEthernetConnected && !settings.isTunnelOnEthernetEnabled && activeTunnels.hasActive()
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO test removed kill switch state check
|
// TODO test removed kill switch state check
|
||||||
@@ -62,16 +65,16 @@ data class AutoTunnelState(
|
|||||||
return settings.isVpnKillSwitchEnabled && (!settings.isDisableKillSwitchOnTrustedEnabled || !isCurrentSSIDTrusted())
|
return settings.isVpnKillSwitchEnabled && (!settings.isDisableKillSwitchOnTrustedEnabled || !isCurrentSSIDTrusted())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isNoConnectivity(): Boolean {
|
private fun isNoConnectivity(): Boolean {
|
||||||
return !networkState.isEthernetConnected && !networkState.isWifiConnected && !networkState.isMobileDataConnected
|
return !networkState.isEthernetConnected && !networkState.isWifiConnected && !networkState.isMobileDataConnected
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun stopOnMobileData(): Boolean {
|
private fun stopOnMobileData(): Boolean {
|
||||||
return isMobileDataActive() && !settings.isTunnelOnMobileDataEnabled && activeTunnels.isNotEmpty()
|
return isMobileDataActive() && !settings.isTunnelOnMobileDataEnabled && activeTunnels.hasActive()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startOnMobileData(): Boolean {
|
private fun startOnMobileData(): Boolean {
|
||||||
return isMobileDataActive() && settings.isTunnelOnMobileDataEnabled && activeTunnels.isEmpty()
|
return isMobileDataActive() && settings.isTunnelOnMobileDataEnabled && activeTunnels.allDown()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun changeOnMobileData(): Boolean {
|
private fun changeOnMobileData(): Boolean {
|
||||||
@@ -83,24 +86,24 @@ data class AutoTunnelState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun stopOnWifi(): Boolean {
|
private fun stopOnWifi(): Boolean {
|
||||||
return isWifiActive() && !settings.isTunnelOnWifiEnabled && activeTunnels.isNotEmpty()
|
return isWifiActive() && !settings.isTunnelOnWifiEnabled && activeTunnels.hasActive()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun stopOnTrustedWifi(): Boolean {
|
private fun stopOnTrustedWifi(): Boolean {
|
||||||
return isWifiActive() && settings.isTunnelOnWifiEnabled && activeTunnels.isNotEmpty() && isCurrentSSIDTrusted()
|
return isWifiActive() && settings.isTunnelOnWifiEnabled && activeTunnels.hasActive() && isCurrentSSIDTrusted()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startOnUntrustedWifi(): Boolean {
|
private fun startOnUntrustedWifi(): Boolean {
|
||||||
return isWifiActive() && settings.isTunnelOnWifiEnabled && activeTunnels.isEmpty() && !isCurrentSSIDTrusted()
|
return isWifiActive() && settings.isTunnelOnWifiEnabled && activeTunnels.allDown() && !isCurrentSSIDTrusted()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun changeOnUntrustedWifi(): Boolean {
|
private fun changeOnUntrustedWifi(): Boolean {
|
||||||
return isWifiActive() && settings.isTunnelOnWifiEnabled && activeTunnels.isNotEmpty() && !isCurrentSSIDTrusted() && !isWifiTunnelPreferred()
|
return isWifiActive() && settings.isTunnelOnWifiEnabled && activeTunnels.hasActive() && !isCurrentSSIDTrusted() && !isWifiTunnelPreferred()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isWifiTunnelPreferred(): Boolean {
|
private fun isWifiTunnelPreferred(): Boolean {
|
||||||
val preferred = preferredWifiTunnel()
|
val preferred = preferredWifiTunnel()
|
||||||
return activeTunnels.any { it.id == preferred?.id }
|
return preferred?.let { activeTunnels.isUp(it) } ?: true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun asAutoTunnelEvent(): AutoTunnelEvent {
|
fun asAutoTunnelEvent(): AutoTunnelEvent {
|
||||||
|
|||||||
@@ -52,7 +52,11 @@ sealed class Route {
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class SplitTunnel(
|
data class SplitTunnel(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
) : Route()
|
) : Route() {
|
||||||
|
companion object {
|
||||||
|
const val KEY_ID = "id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class TunnelAutoTunnel(
|
data class TunnelAutoTunnel(
|
||||||
|
|||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.common.animation
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.LinearEasing
|
||||||
|
import androidx.compose.animation.core.animateFloat
|
||||||
|
import androidx.compose.animation.core.infiniteRepeatable
|
||||||
|
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ShimmerEffect(modifier: Modifier = Modifier): Brush {
|
||||||
|
val shimmerColors = listOf(
|
||||||
|
Color.LightGray.copy(alpha = 0.9f),
|
||||||
|
Color.LightGray.copy(alpha = 0.3f),
|
||||||
|
Color.LightGray.copy(alpha = 0.9f),
|
||||||
|
)
|
||||||
|
|
||||||
|
val transition = rememberInfiniteTransition()
|
||||||
|
val translateAnim by transition.animateFloat(
|
||||||
|
initialValue = 0f,
|
||||||
|
targetValue = 1000f,
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = tween(durationMillis = 1200, easing = LinearEasing),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return Brush.linearGradient(
|
||||||
|
colors = shimmerColors,
|
||||||
|
start = Offset(0f, 0f),
|
||||||
|
end = Offset(translateAnim, translateAnim),
|
||||||
|
)
|
||||||
|
}
|
||||||
-808
@@ -1,808 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.main
|
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.focusGroup
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.IntrinsicSize
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.imePadding
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.outlined.Save
|
|
||||||
import androidx.compose.material.icons.rounded.ContentCopy
|
|
||||||
import androidx.compose.material.icons.rounded.Delete
|
|
||||||
import androidx.compose.material.icons.rounded.MoreVert
|
|
||||||
import androidx.compose.material.icons.rounded.Refresh
|
|
||||||
import androidx.compose.material3.DropdownMenu
|
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.OutlinedTextField
|
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.Surface
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.runtime.toMutableStateList
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.shadow
|
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
|
||||||
import androidx.compose.ui.focus.focusRequester
|
|
||||||
import androidx.compose.ui.platform.ClipboardManager
|
|
||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
|
||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
|
||||||
import androidx.compose.ui.text.input.VisualTransformation
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.enums.InterfaceActions
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.enums.PeerActions
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.state.InterfaceProxy
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.state.PeerProxy
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
|
|
||||||
import org.amnezia.awg.crypto.KeyPair
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ConfigScreen(tunnelConf: TunnelConf?, appViewModel: AppViewModel) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
val snackbar = SnackbarController.current
|
|
||||||
val clipboardManager: ClipboardManager = LocalClipboardManager.current
|
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
|
||||||
val navController = LocalNavController.current
|
|
||||||
|
|
||||||
var isInterfaceDropDownExpanded by remember {
|
|
||||||
mutableStateOf(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
val popBackStack by appViewModel.popBackStack.collectAsStateWithLifecycle(false)
|
|
||||||
|
|
||||||
val configPair = Pair(tunnelConf?.tunName ?: "", tunnelConf?.toAmConfig())
|
|
||||||
|
|
||||||
var tunnelName by remember {
|
|
||||||
mutableStateOf(configPair.first)
|
|
||||||
}
|
|
||||||
|
|
||||||
var interfaceState by remember {
|
|
||||||
mutableStateOf(configPair.second?.let { InterfaceProxy.from(it.`interface`) } ?: InterfaceProxy())
|
|
||||||
}
|
|
||||||
|
|
||||||
var showAmneziaValues by remember {
|
|
||||||
mutableStateOf(configPair.second?.`interface`?.junkPacketCount?.isPresent == true)
|
|
||||||
}
|
|
||||||
|
|
||||||
var showScripts by remember {
|
|
||||||
mutableStateOf(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
val peersState = remember {
|
|
||||||
(configPair.second?.peers?.map { PeerProxy.from(it) } ?: listOf(PeerProxy())).toMutableStateList()
|
|
||||||
}
|
|
||||||
|
|
||||||
var showAuthPrompt by remember { mutableStateOf(false) }
|
|
||||||
var isAuthenticated by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
val keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() })
|
|
||||||
val keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done)
|
|
||||||
|
|
||||||
if (showAuthPrompt) {
|
|
||||||
AuthorizationPrompt(
|
|
||||||
onSuccess = {
|
|
||||||
showAuthPrompt = false
|
|
||||||
isAuthenticated = true
|
|
||||||
},
|
|
||||||
onError = {
|
|
||||||
showAuthPrompt = false
|
|
||||||
snackbar.showMessage(
|
|
||||||
context.getString(R.string.error_authentication_failed),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onFailure = {
|
|
||||||
showAuthPrompt = false
|
|
||||||
snackbar.showMessage(
|
|
||||||
context.getString(R.string.error_authorization_failed),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(popBackStack) {
|
|
||||||
if (popBackStack) navController.popBackStack()
|
|
||||||
}
|
|
||||||
|
|
||||||
Scaffold(
|
|
||||||
topBar = {
|
|
||||||
TopNavBar(stringResource(R.string.edit_tunnel), trailing = {
|
|
||||||
IconButton(onClick = {
|
|
||||||
tunnelConf?.let {
|
|
||||||
appViewModel.updateExistingTunnelConfig(
|
|
||||||
it,
|
|
||||||
tunnelName,
|
|
||||||
peersState,
|
|
||||||
interfaceState,
|
|
||||||
)
|
|
||||||
} ?: appViewModel.saveNewTunnel(tunnelName, peersState, interfaceState)
|
|
||||||
}) {
|
|
||||||
val icon = Icons.Outlined.Save
|
|
||||||
Icon(
|
|
||||||
imageVector = icon,
|
|
||||||
contentDescription = icon.name,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
) { padding ->
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.Start,
|
|
||||||
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top),
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(padding)
|
|
||||||
.imePadding()
|
|
||||||
.verticalScroll(rememberScrollState())
|
|
||||||
.padding(top = 24.dp.scaledHeight())
|
|
||||||
.padding(horizontal = 24.dp.scaledWidth()),
|
|
||||||
) {
|
|
||||||
Surface(
|
|
||||||
shape = RoundedCornerShape(12.dp),
|
|
||||||
color = MaterialTheme.colorScheme.surface,
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.Start,
|
|
||||||
verticalArrangement = Arrangement.spacedBy(5.dp, Alignment.Top),
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(16.dp.scaledWidth())
|
|
||||||
.focusGroup(),
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier =
|
|
||||||
Modifier.fillMaxWidth(),
|
|
||||||
) {
|
|
||||||
GroupLabel(
|
|
||||||
stringResource(R.string.interface_),
|
|
||||||
)
|
|
||||||
Column {
|
|
||||||
IconButton(
|
|
||||||
modifier = Modifier.size(iconSize),
|
|
||||||
onClick = {
|
|
||||||
isInterfaceDropDownExpanded = true
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
val icon = Icons.Rounded.MoreVert
|
|
||||||
Icon(icon, icon.name)
|
|
||||||
}
|
|
||||||
DropdownMenu(
|
|
||||||
containerColor = MaterialTheme.colorScheme.surface,
|
|
||||||
expanded = isInterfaceDropDownExpanded,
|
|
||||||
modifier = Modifier.shadow(12.dp).background(MaterialTheme.colorScheme.surface),
|
|
||||||
onDismissRequest = {
|
|
||||||
isInterfaceDropDownExpanded = false
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
val isAmneziaCompatibilitySet = interfaceState.isAmneziaCompatibilityModeSet()
|
|
||||||
InterfaceActions.entries.forEach { action ->
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = {
|
|
||||||
Text(
|
|
||||||
text = when (action) {
|
|
||||||
InterfaceActions.TOGGLE_SHOW_SCRIPTS -> if (showScripts) {
|
|
||||||
stringResource(R.string.hide_scripts)
|
|
||||||
} else {
|
|
||||||
stringResource(R.string.show_scripts)
|
|
||||||
}
|
|
||||||
InterfaceActions.TOGGLE_AMNEZIA_VALUES -> if (showAmneziaValues) {
|
|
||||||
stringResource(R.string.hide_amnezia_properties)
|
|
||||||
} else {
|
|
||||||
stringResource(R.string.show_amnezia_properties)
|
|
||||||
}
|
|
||||||
|
|
||||||
InterfaceActions.SET_AMNEZIA_COMPATIBILITY -> if (isAmneziaCompatibilitySet) {
|
|
||||||
stringResource(R.string.remove_amnezia_compatibility)
|
|
||||||
} else {
|
|
||||||
stringResource(R.string.enable_amnezia_compatibility)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
isInterfaceDropDownExpanded = false
|
|
||||||
when (action) {
|
|
||||||
InterfaceActions.TOGGLE_AMNEZIA_VALUES -> showAmneziaValues = !showAmneziaValues
|
|
||||||
InterfaceActions.TOGGLE_SHOW_SCRIPTS -> showScripts = !showScripts
|
|
||||||
InterfaceActions.SET_AMNEZIA_COMPATIBILITY -> if (isAmneziaCompatibilitySet) {
|
|
||||||
showAmneziaValues = false
|
|
||||||
interfaceState = interfaceState.resetAmneziaProperties()
|
|
||||||
} else {
|
|
||||||
showAmneziaValues = true
|
|
||||||
interfaceState = interfaceState.toAmneziaCompatibilityConfig()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ConfigurationTextBox(
|
|
||||||
value = tunnelName,
|
|
||||||
onValueChange = { tunnelName = it },
|
|
||||||
keyboardActions = keyboardActions,
|
|
||||||
label = stringResource(R.string.name),
|
|
||||||
hint = stringResource(R.string.tunnel_name).lowercase(),
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth(),
|
|
||||||
)
|
|
||||||
val privateKeyEnabled = (tunnelConf == null) || isAuthenticated
|
|
||||||
OutlinedTextField(
|
|
||||||
textStyle = MaterialTheme.typography.labelLarge,
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable { showAuthPrompt = true },
|
|
||||||
value = interfaceState.privateKey,
|
|
||||||
visualTransformation =
|
|
||||||
if (privateKeyEnabled) {
|
|
||||||
VisualTransformation.None
|
|
||||||
} else {
|
|
||||||
PasswordVisualTransformation()
|
|
||||||
},
|
|
||||||
enabled = privateKeyEnabled,
|
|
||||||
onValueChange = { interfaceState = interfaceState.copy(privateKey = it) },
|
|
||||||
trailingIcon = {
|
|
||||||
IconButton(
|
|
||||||
enabled = privateKeyEnabled,
|
|
||||||
modifier = Modifier.focusRequester(FocusRequester.Default).size(iconSize),
|
|
||||||
onClick = {
|
|
||||||
val keypair = KeyPair()
|
|
||||||
interfaceState = interfaceState.copy(
|
|
||||||
privateKey = keypair.privateKey.toBase64(),
|
|
||||||
publicKey = keypair.publicKey.toBase64(),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Icons.Rounded.Refresh,
|
|
||||||
stringResource(R.string.rotate_keys),
|
|
||||||
tint = if (privateKeyEnabled) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.outline,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
label = {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.private_key),
|
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
|
||||||
style = MaterialTheme.typography.labelMedium,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
singleLine = true,
|
|
||||||
placeholder = {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.base64_key),
|
|
||||||
style = MaterialTheme.typography.labelLarge,
|
|
||||||
color = MaterialTheme.colorScheme.outline,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
keyboardOptions = keyboardOptions,
|
|
||||||
keyboardActions = keyboardActions,
|
|
||||||
)
|
|
||||||
OutlinedTextField(
|
|
||||||
textStyle = MaterialTheme.typography.labelLarge,
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.focusRequester(FocusRequester.Default),
|
|
||||||
value = interfaceState.publicKey,
|
|
||||||
enabled = false,
|
|
||||||
onValueChange = {
|
|
||||||
interfaceState = interfaceState.copy(publicKey = it)
|
|
||||||
},
|
|
||||||
trailingIcon = {
|
|
||||||
IconButton(
|
|
||||||
modifier = Modifier.focusRequester(FocusRequester.Default).size(iconSize),
|
|
||||||
onClick = {
|
|
||||||
clipboardManager.setText(
|
|
||||||
AnnotatedString(interfaceState.publicKey),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Icons.Rounded.ContentCopy,
|
|
||||||
stringResource(R.string.copy_public_key),
|
|
||||||
tint = MaterialTheme.colorScheme.onSurface,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
label = {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.public_key),
|
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
|
||||||
style = MaterialTheme.typography.labelMedium,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
singleLine = true,
|
|
||||||
placeholder = {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.base64_key),
|
|
||||||
style = MaterialTheme.typography.labelLarge,
|
|
||||||
color = MaterialTheme.colorScheme.outline,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
keyboardOptions = keyboardOptions,
|
|
||||||
keyboardActions = keyboardActions,
|
|
||||||
)
|
|
||||||
ConfigurationTextBox(
|
|
||||||
value = interfaceState.addresses,
|
|
||||||
onValueChange = {
|
|
||||||
interfaceState = interfaceState.copy(addresses = it)
|
|
||||||
},
|
|
||||||
keyboardActions = keyboardActions,
|
|
||||||
label = stringResource(R.string.addresses),
|
|
||||||
hint = stringResource(R.string.comma_separated_list),
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(end = 5.dp),
|
|
||||||
)
|
|
||||||
ConfigurationTextBox(
|
|
||||||
value = interfaceState.listenPort,
|
|
||||||
onValueChange = {
|
|
||||||
interfaceState = interfaceState.copy(listenPort = it)
|
|
||||||
},
|
|
||||||
keyboardActions = keyboardActions,
|
|
||||||
label = stringResource(R.string.listen_port),
|
|
||||||
hint = stringResource(R.string.random),
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
)
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(5.dp),
|
|
||||||
) {
|
|
||||||
ConfigurationTextBox(
|
|
||||||
value = interfaceState.dnsServers,
|
|
||||||
onValueChange = {
|
|
||||||
interfaceState = interfaceState.copy(dnsServers = it)
|
|
||||||
},
|
|
||||||
keyboardActions = keyboardActions,
|
|
||||||
label = stringResource(R.string.dns_servers),
|
|
||||||
hint = stringResource(R.string.comma_separated_list),
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth(3 / 5f)
|
|
||||||
.padding(end = 5.dp),
|
|
||||||
)
|
|
||||||
ConfigurationTextBox(
|
|
||||||
value = interfaceState.mtu,
|
|
||||||
onValueChange = {
|
|
||||||
interfaceState = interfaceState.copy(mtu = it)
|
|
||||||
},
|
|
||||||
keyboardActions = keyboardActions,
|
|
||||||
label = stringResource(R.string.mtu),
|
|
||||||
hint = stringResource(R.string.auto),
|
|
||||||
modifier = Modifier.width(IntrinsicSize.Min),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (showScripts) {
|
|
||||||
ConfigurationTextBox(
|
|
||||||
value = interfaceState.preUp,
|
|
||||||
onValueChange = {
|
|
||||||
interfaceState = interfaceState.copy(preUp = it)
|
|
||||||
},
|
|
||||||
keyboardActions = keyboardActions,
|
|
||||||
label = stringResource(R.string.pre_up),
|
|
||||||
hint = stringResource(R.string.comma_separated_list).lowercase(),
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
)
|
|
||||||
ConfigurationTextBox(
|
|
||||||
value = interfaceState.postUp,
|
|
||||||
onValueChange = {
|
|
||||||
interfaceState = interfaceState.copy(postUp = it)
|
|
||||||
},
|
|
||||||
keyboardActions = keyboardActions,
|
|
||||||
label = stringResource(R.string.post_up),
|
|
||||||
hint = stringResource(R.string.comma_separated_list).lowercase(),
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
)
|
|
||||||
ConfigurationTextBox(
|
|
||||||
value = interfaceState.preDown,
|
|
||||||
onValueChange = {
|
|
||||||
interfaceState = interfaceState.copy(preDown = it)
|
|
||||||
},
|
|
||||||
keyboardActions = keyboardActions,
|
|
||||||
label = stringResource(R.string.pre_down),
|
|
||||||
hint = stringResource(R.string.comma_separated_list).lowercase(),
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
)
|
|
||||||
ConfigurationTextBox(
|
|
||||||
value = interfaceState.postDown,
|
|
||||||
onValueChange = {
|
|
||||||
interfaceState = interfaceState.copy(postDown = it)
|
|
||||||
},
|
|
||||||
keyboardActions = keyboardActions,
|
|
||||||
label = stringResource(R.string.post_down),
|
|
||||||
hint = stringResource(R.string.comma_separated_list).lowercase(),
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (showAmneziaValues) {
|
|
||||||
ConfigurationTextBox(
|
|
||||||
value = interfaceState.junkPacketCount,
|
|
||||||
onValueChange = {
|
|
||||||
interfaceState = interfaceState.copy(junkPacketCount = it)
|
|
||||||
},
|
|
||||||
keyboardActions = keyboardActions,
|
|
||||||
label = stringResource(R.string.junk_packet_count),
|
|
||||||
hint = stringResource(R.string.junk_packet_count).lowercase(),
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth(),
|
|
||||||
)
|
|
||||||
ConfigurationTextBox(
|
|
||||||
value = interfaceState.junkPacketMinSize,
|
|
||||||
onValueChange = {
|
|
||||||
interfaceState = interfaceState.copy(junkPacketMinSize = it)
|
|
||||||
},
|
|
||||||
keyboardActions = keyboardActions,
|
|
||||||
label = stringResource(R.string.junk_packet_minimum_size),
|
|
||||||
hint =
|
|
||||||
stringResource(
|
|
||||||
R.string.junk_packet_minimum_size,
|
|
||||||
).lowercase(),
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth(),
|
|
||||||
)
|
|
||||||
ConfigurationTextBox(
|
|
||||||
value = interfaceState.junkPacketMaxSize,
|
|
||||||
onValueChange = {
|
|
||||||
interfaceState = interfaceState.copy(junkPacketMaxSize = it)
|
|
||||||
},
|
|
||||||
keyboardActions = keyboardActions,
|
|
||||||
label = stringResource(R.string.junk_packet_maximum_size),
|
|
||||||
hint =
|
|
||||||
stringResource(
|
|
||||||
R.string.junk_packet_maximum_size,
|
|
||||||
).lowercase(),
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth(),
|
|
||||||
)
|
|
||||||
ConfigurationTextBox(
|
|
||||||
value = interfaceState.initPacketJunkSize,
|
|
||||||
onValueChange = {
|
|
||||||
interfaceState = interfaceState.copy(initPacketJunkSize = it)
|
|
||||||
},
|
|
||||||
keyboardActions = keyboardActions,
|
|
||||||
label = stringResource(R.string.init_packet_junk_size),
|
|
||||||
hint = stringResource(R.string.init_packet_junk_size).lowercase(),
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth(),
|
|
||||||
)
|
|
||||||
ConfigurationTextBox(
|
|
||||||
value = interfaceState.responsePacketJunkSize,
|
|
||||||
onValueChange = {
|
|
||||||
interfaceState = interfaceState.copy(responsePacketJunkSize = it)
|
|
||||||
},
|
|
||||||
keyboardActions = keyboardActions,
|
|
||||||
label = stringResource(R.string.response_packet_junk_size),
|
|
||||||
hint =
|
|
||||||
stringResource(
|
|
||||||
R.string.response_packet_junk_size,
|
|
||||||
).lowercase(),
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth(),
|
|
||||||
)
|
|
||||||
ConfigurationTextBox(
|
|
||||||
value = interfaceState.initPacketMagicHeader,
|
|
||||||
onValueChange = {
|
|
||||||
interfaceState = interfaceState.copy(initPacketMagicHeader = it)
|
|
||||||
},
|
|
||||||
keyboardActions = keyboardActions,
|
|
||||||
label = stringResource(R.string.init_packet_magic_header),
|
|
||||||
hint =
|
|
||||||
stringResource(
|
|
||||||
R.string.init_packet_magic_header,
|
|
||||||
).lowercase(),
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth(),
|
|
||||||
)
|
|
||||||
ConfigurationTextBox(
|
|
||||||
value = interfaceState.responsePacketMagicHeader,
|
|
||||||
onValueChange = {
|
|
||||||
interfaceState = interfaceState.copy(responsePacketMagicHeader = it)
|
|
||||||
},
|
|
||||||
keyboardActions = keyboardActions,
|
|
||||||
label = stringResource(R.string.response_packet_magic_header),
|
|
||||||
hint =
|
|
||||||
stringResource(
|
|
||||||
R.string.response_packet_magic_header,
|
|
||||||
).lowercase(),
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth(),
|
|
||||||
)
|
|
||||||
ConfigurationTextBox(
|
|
||||||
value = interfaceState.underloadPacketMagicHeader,
|
|
||||||
onValueChange = {
|
|
||||||
interfaceState = interfaceState.copy(underloadPacketMagicHeader = it)
|
|
||||||
},
|
|
||||||
keyboardActions = keyboardActions,
|
|
||||||
label = stringResource(R.string.underload_packet_magic_header),
|
|
||||||
hint =
|
|
||||||
stringResource(
|
|
||||||
R.string.underload_packet_magic_header,
|
|
||||||
).lowercase(),
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth(),
|
|
||||||
)
|
|
||||||
ConfigurationTextBox(
|
|
||||||
value = interfaceState.transportPacketMagicHeader,
|
|
||||||
onValueChange = {
|
|
||||||
interfaceState = interfaceState.copy(transportPacketMagicHeader = it)
|
|
||||||
},
|
|
||||||
keyboardActions = keyboardActions,
|
|
||||||
label = stringResource(R.string.transport_packet_magic_header),
|
|
||||||
hint =
|
|
||||||
stringResource(
|
|
||||||
R.string.transport_packet_magic_header,
|
|
||||||
).lowercase(),
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
peersState.forEachIndexed { index, peer ->
|
|
||||||
var isPeerDropDownExpanded by remember {
|
|
||||||
mutableStateOf(false)
|
|
||||||
}
|
|
||||||
val isLanExcluded = peer.isLanExcluded()
|
|
||||||
Surface(
|
|
||||||
shape = RoundedCornerShape(12.dp),
|
|
||||||
color = MaterialTheme.colorScheme.surface,
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.Start,
|
|
||||||
verticalArrangement = Arrangement.spacedBy(10.dp, Alignment.Top),
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(16.dp.scaledWidth())
|
|
||||||
.focusGroup(),
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier =
|
|
||||||
Modifier.fillMaxWidth(),
|
|
||||||
) {
|
|
||||||
GroupLabel(
|
|
||||||
stringResource(R.string.peer),
|
|
||||||
)
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.End),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
IconButton(
|
|
||||||
modifier = Modifier.size(iconSize),
|
|
||||||
onClick = {
|
|
||||||
// TODO make a dialog to confirm this
|
|
||||||
peersState.removeAt(index)
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
val icon = Icons.Rounded.Delete
|
|
||||||
Icon(icon, icon.name)
|
|
||||||
}
|
|
||||||
Column {
|
|
||||||
IconButton(
|
|
||||||
modifier = Modifier.size(iconSize),
|
|
||||||
onClick = {
|
|
||||||
isPeerDropDownExpanded = true
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
val icon = Icons.Rounded.MoreVert
|
|
||||||
Icon(icon, icon.name)
|
|
||||||
}
|
|
||||||
DropdownMenu(
|
|
||||||
containerColor = MaterialTheme.colorScheme.surface,
|
|
||||||
expanded = isPeerDropDownExpanded,
|
|
||||||
modifier = Modifier.shadow(12.dp).background(MaterialTheme.colorScheme.surface),
|
|
||||||
onDismissRequest = {
|
|
||||||
isPeerDropDownExpanded = false
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
PeerActions.entries.forEach { action ->
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = {
|
|
||||||
Text(
|
|
||||||
text = when (action) {
|
|
||||||
PeerActions.EXCLUDE_LAN -> if (isLanExcluded) {
|
|
||||||
stringResource(R.string.include_lan)
|
|
||||||
} else {
|
|
||||||
stringResource(R.string.exclude_lan)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
isPeerDropDownExpanded = false
|
|
||||||
when (action) {
|
|
||||||
PeerActions.EXCLUDE_LAN -> if (isLanExcluded) {
|
|
||||||
peersState[index] = peer.includeLan()
|
|
||||||
} else {
|
|
||||||
peersState[index] = peer.excludeLan()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ConfigurationTextBox(
|
|
||||||
value = peer.publicKey,
|
|
||||||
onValueChange = { value ->
|
|
||||||
peersState[index] = peersState[index].copy(publicKey = value)
|
|
||||||
},
|
|
||||||
keyboardActions = keyboardActions,
|
|
||||||
label = stringResource(R.string.public_key),
|
|
||||||
hint = stringResource(R.string.base64_key),
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
)
|
|
||||||
val presharedKeyEnabled = (tunnelConf == null) || isAuthenticated ||
|
|
||||||
with(configPair.second?.peers[index]?.preSharedKey) { this?.isEmpty == true || this?.isPresent == false }
|
|
||||||
OutlinedTextField(
|
|
||||||
textStyle = MaterialTheme.typography.labelLarge,
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable { showAuthPrompt = true },
|
|
||||||
value = peer.preSharedKey,
|
|
||||||
visualTransformation =
|
|
||||||
if (presharedKeyEnabled) {
|
|
||||||
VisualTransformation.None
|
|
||||||
} else {
|
|
||||||
PasswordVisualTransformation()
|
|
||||||
},
|
|
||||||
enabled = presharedKeyEnabled,
|
|
||||||
onValueChange = { value -> peersState[index] = peersState[index].copy(preSharedKey = value) },
|
|
||||||
label = {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.preshared_key),
|
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
|
||||||
style = MaterialTheme.typography.labelMedium,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
singleLine = true,
|
|
||||||
placeholder = {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.optional),
|
|
||||||
style = MaterialTheme.typography.labelLarge,
|
|
||||||
color = MaterialTheme.colorScheme.outline,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
keyboardOptions = keyboardOptions,
|
|
||||||
keyboardActions = keyboardActions,
|
|
||||||
)
|
|
||||||
OutlinedTextField(
|
|
||||||
textStyle = MaterialTheme.typography.labelLarge,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
value = peer.persistentKeepalive,
|
|
||||||
enabled = true,
|
|
||||||
onValueChange = { value ->
|
|
||||||
peersState[index] = peersState[index].copy(persistentKeepalive = value)
|
|
||||||
},
|
|
||||||
trailingIcon = {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.seconds),
|
|
||||||
modifier = Modifier.padding(end = 10.dp),
|
|
||||||
style = MaterialTheme.typography.labelMedium,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
label = {
|
|
||||||
Text(stringResource(R.string.persistent_keepalive), color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.labelMedium)
|
|
||||||
},
|
|
||||||
singleLine = true,
|
|
||||||
placeholder = {
|
|
||||||
Text(stringResource(R.string.optional_no_recommend), style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.outline)
|
|
||||||
},
|
|
||||||
keyboardOptions = keyboardOptions,
|
|
||||||
keyboardActions = keyboardActions,
|
|
||||||
)
|
|
||||||
ConfigurationTextBox(
|
|
||||||
value = peer.endpoint,
|
|
||||||
onValueChange = { value ->
|
|
||||||
peersState[index] = peersState[index].copy(endpoint = value)
|
|
||||||
},
|
|
||||||
keyboardActions = keyboardActions,
|
|
||||||
label = stringResource(R.string.endpoint),
|
|
||||||
hint = stringResource(R.string.endpoint).lowercase(),
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
)
|
|
||||||
OutlinedTextField(
|
|
||||||
textStyle = MaterialTheme.typography.labelLarge,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
value = peer.allowedIps,
|
|
||||||
enabled = true,
|
|
||||||
onValueChange = { value ->
|
|
||||||
peersState[index] = peersState[index].copy(allowedIps = value)
|
|
||||||
},
|
|
||||||
label = {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.allowed_ips),
|
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
|
||||||
style = MaterialTheme.typography.labelMedium,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
singleLine = true,
|
|
||||||
placeholder = {
|
|
||||||
Text(stringResource(R.string.comma_separated_list), style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.outline)
|
|
||||||
},
|
|
||||||
keyboardOptions = keyboardOptions,
|
|
||||||
keyboardActions = keyboardActions,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(bottom = 140.dp),
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.Center,
|
|
||||||
) {
|
|
||||||
TextButton(onClick = {
|
|
||||||
peersState.add(PeerProxy())
|
|
||||||
}) {
|
|
||||||
Text(stringResource(R.string.add_peer))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+22
-13
@@ -38,6 +38,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.getValueById
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
|
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
|
||||||
@@ -55,6 +56,7 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.GettingSta
|
|||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.ScrollDismissFab
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.ScrollDismissFab
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.TunnelImportSheet
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.TunnelImportSheet
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.TunnelRowItem
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.TunnelRowItem
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.UrlImportDialog
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
||||||
@@ -75,9 +77,10 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
|
|||||||
var isFabVisible by rememberSaveable { mutableStateOf(true) }
|
var isFabVisible by rememberSaveable { mutableStateOf(true) }
|
||||||
var showDeleteTunnelAlertDialog by remember { mutableStateOf(false) }
|
var showDeleteTunnelAlertDialog by remember { mutableStateOf(false) }
|
||||||
var selectedTunnel by remember { mutableStateOf<TunnelConf?>(null) }
|
var selectedTunnel by remember { mutableStateOf<TunnelConf?>(null) }
|
||||||
|
var showUrlImportDialog by remember { mutableStateOf(false) }
|
||||||
val isRunningOnTv = remember { context.isRunningOnTv() }
|
val isRunningOnTv = remember { context.isRunningOnTv() }
|
||||||
|
|
||||||
val activeTunnels by viewModel.activeTunnels.collectAsStateWithLifecycle(emptyList())
|
val activeTunnels by viewModel.tunnelManager.activeTunnels.collectAsStateWithLifecycle(emptyMap())
|
||||||
|
|
||||||
val collator = Collator.getInstance(Locale.getDefault())
|
val collator = Collator.getInstance(Locale.getDefault())
|
||||||
|
|
||||||
@@ -89,6 +92,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
|
|||||||
val startTunnel = withVpnPermission<TunnelConf> {
|
val startTunnel = withVpnPermission<TunnelConf> {
|
||||||
viewModel.onTunnelStart(it)
|
viewModel.onTunnelStart(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
val autoTunnelToggleBattery = withIgnoreBatteryOpt(uiState.generalState.isBatteryOptimizationDisableShown) {
|
val autoTunnelToggleBattery = withIgnoreBatteryOpt(uiState.generalState.isBatteryOptimizationDisableShown) {
|
||||||
if (!uiState.generalState.isBatteryOptimizationDisableShown) viewModel.setBatteryOptimizeDisableShown()
|
if (!uiState.generalState.isBatteryOptimizationDisableShown) viewModel.setBatteryOptimizeDisableShown()
|
||||||
if (uiState.appSettings.isKernelEnabled) {
|
if (uiState.appSettings.isKernelEnabled) {
|
||||||
@@ -132,15 +136,8 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConf) {
|
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConf) {
|
||||||
if (!checked) {
|
if (!checked) return viewModel.onTunnelStop(tunnel).let { }
|
||||||
viewModel.onTunnelStop(tunnel)
|
if (uiState.appSettings.isKernelEnabled) viewModel.onTunnelStart(tunnel) else startTunnel(tunnel)
|
||||||
return
|
|
||||||
}
|
|
||||||
if (uiState.appSettings.isKernelEnabled) {
|
|
||||||
viewModel.onTunnelStart(tunnel)
|
|
||||||
} else {
|
|
||||||
startTunnel.invoke(tunnel)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
@@ -203,7 +200,19 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
|
|||||||
Route.Config(Constants.MANUAL_TUNNEL_CONFIG_ID),
|
Route.Config(Constants.MANUAL_TUNNEL_CONFIG_ID),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
onUrlClick = { showUrlImportDialog = true }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (showUrlImportDialog) {
|
||||||
|
UrlImportDialog(
|
||||||
|
onDismiss = { showUrlImportDialog = false },
|
||||||
|
onConfirm = { url ->
|
||||||
|
viewModel.onUrlImport(url)
|
||||||
|
showUrlImportDialog = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
verticalArrangement = Arrangement.spacedBy(5.dp, Alignment.Top),
|
verticalArrangement = Arrangement.spacedBy(5.dp, Alignment.Top),
|
||||||
@@ -233,13 +242,13 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
|
|||||||
key = { tunnel -> tunnel.id },
|
key = { tunnel -> tunnel.id },
|
||||||
) { tunnel ->
|
) { tunnel ->
|
||||||
val expanded = uiState.generalState.isTunnelStatsExpanded
|
val expanded = uiState.generalState.isTunnelStatsExpanded
|
||||||
val tunnelState = activeTunnels.firstOrNull { it.id == tunnel.id }?.state?.collectAsStateWithLifecycle()
|
val tunnelState = activeTunnels.getValueById(tunnel.id) ?: TunnelState()
|
||||||
TunnelRowItem(
|
TunnelRowItem(
|
||||||
tunnel.isActive,
|
tunnelState.state.isUp(),
|
||||||
expanded,
|
expanded,
|
||||||
selectedTunnel?.id == tunnel.id,
|
selectedTunnel?.id == tunnel.id,
|
||||||
tunnel,
|
tunnel,
|
||||||
tunnelState = tunnelState?.value ?: TunnelState(),
|
tunnelState = tunnelState,
|
||||||
{ selectedTunnel = tunnel },
|
{ selectedTunnel = tunnel },
|
||||||
{ viewModel.onExpandedChanged(!expanded) },
|
{ viewModel.onExpandedChanged(!expanded) },
|
||||||
onDelete = { showDeleteTunnelAlertDialog = true },
|
onDelete = { showDeleteTunnelAlertDialog = true },
|
||||||
|
|||||||
-269
@@ -1,269 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.main
|
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.outlined.Check
|
|
||||||
import androidx.compose.material.icons.outlined.Save
|
|
||||||
import androidx.compose.material.icons.outlined.Search
|
|
||||||
import androidx.compose.material3.Checkbox
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.MultiChoiceSegmentedButtonRow
|
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.SegmentedButton
|
|
||||||
import androidx.compose.material3.SegmentedButtonDefaults
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.runtime.toMutableStateList
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
|
||||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
||||||
import com.google.accompanist.drawablepainter.rememberDrawablePainter
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.SelectionItemButton
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.textbox.CustomTextField
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.enums.SplitOptions
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.state.InterfaceProxy
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
|
|
||||||
import java.text.Collator
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SplitTunnelScreen(tunnelConf: TunnelConf, viewModel: AppViewModel) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
val navController = LocalNavController.current
|
|
||||||
|
|
||||||
val inputHeight = 45.dp
|
|
||||||
|
|
||||||
val collator = Collator.getInstance(Locale.getDefault())
|
|
||||||
|
|
||||||
val popBackStack by viewModel.popBackStack.collectAsStateWithLifecycle(false)
|
|
||||||
|
|
||||||
LaunchedEffect(popBackStack) {
|
|
||||||
if (popBackStack) navController.popBackStack()
|
|
||||||
}
|
|
||||||
|
|
||||||
val splitTunnelApps by viewModel.splitTunnelApps.collectAsStateWithLifecycle()
|
|
||||||
|
|
||||||
var proxyInterface by remember { mutableStateOf(InterfaceProxy()) }
|
|
||||||
|
|
||||||
var selectedSplitOption by remember { mutableStateOf(SplitOptions.ALL) }
|
|
||||||
|
|
||||||
val selectedPackages = remember { mutableStateListOf<String>() }
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
proxyInterface = InterfaceProxy.from(tunnelConf.toAmConfig().`interface`)
|
|
||||||
val pair = when {
|
|
||||||
proxyInterface.excludedApplications.isNotEmpty() -> Pair(SplitOptions.EXCLUDE, proxyInterface.excludedApplications)
|
|
||||||
proxyInterface.includedApplications.isNotEmpty() -> Pair(SplitOptions.INCLUDE, proxyInterface.includedApplications)
|
|
||||||
else -> Pair(SplitOptions.ALL, mutableSetOf())
|
|
||||||
}
|
|
||||||
selectedSplitOption = pair.first
|
|
||||||
selectedPackages.addAll(pair.second)
|
|
||||||
}
|
|
||||||
|
|
||||||
var query: String by remember { mutableStateOf("") }
|
|
||||||
|
|
||||||
val sortedPackages by remember {
|
|
||||||
derivedStateOf {
|
|
||||||
splitTunnelApps.sortedWith(compareBy(collator) { it.name }).filter { it.name.lowercase().contains(query.lowercase()) }.toMutableStateList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
// clean up any split tunnel packages for apps that were uninstalled
|
|
||||||
viewModel.cleanUpUninstalledApps(tunnelConf, splitTunnelApps.map { it.`package` })
|
|
||||||
}
|
|
||||||
|
|
||||||
Scaffold(
|
|
||||||
topBar = {
|
|
||||||
TopNavBar(stringResource(R.string.tunneling_apps), trailing = {
|
|
||||||
IconButton(onClick = {
|
|
||||||
proxyInterface.apply {
|
|
||||||
includedApplications.clear()
|
|
||||||
excludedApplications.clear()
|
|
||||||
}
|
|
||||||
when (selectedSplitOption) {
|
|
||||||
SplitOptions.INCLUDE -> proxyInterface.includedApplications.apply {
|
|
||||||
addAll(selectedPackages)
|
|
||||||
}
|
|
||||||
SplitOptions.EXCLUDE -> proxyInterface.excludedApplications.apply {
|
|
||||||
addAll(selectedPackages)
|
|
||||||
}
|
|
||||||
SplitOptions.ALL -> Unit
|
|
||||||
}
|
|
||||||
viewModel.updateExistingTunnelConfig(tunnelConf, `interface` = proxyInterface)
|
|
||||||
}) {
|
|
||||||
val icon = Icons.Outlined.Save
|
|
||||||
Icon(
|
|
||||||
imageVector = icon,
|
|
||||||
contentDescription = icon.name,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
) { padding ->
|
|
||||||
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.CenterVertically),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(padding)
|
|
||||||
.padding(top = 24.dp.scaledHeight()),
|
|
||||||
) {
|
|
||||||
MultiChoiceSegmentedButtonRow(
|
|
||||||
modifier = Modifier.background(color = MaterialTheme.colorScheme.background).fillMaxWidth()
|
|
||||||
.padding(horizontal = 24.dp.scaledWidth()).height(inputHeight),
|
|
||||||
) {
|
|
||||||
SplitOptions.entries.forEachIndexed { index, entry ->
|
|
||||||
val active = selectedSplitOption == entry
|
|
||||||
SegmentedButton(
|
|
||||||
shape = SegmentedButtonDefaults.itemShape(index = index, count = SplitOptions.entries.size, baseShape = RoundedCornerShape(8.dp)),
|
|
||||||
icon = {
|
|
||||||
SegmentedButtonDefaults.Icon(active = active, activeContent = {
|
|
||||||
val icon = Icons.Outlined.Check
|
|
||||||
Icon(imageVector = icon, icon.name, tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(SegmentedButtonDefaults.IconSize))
|
|
||||||
}) {
|
|
||||||
Icon(
|
|
||||||
imageVector = entry.icon(),
|
|
||||||
contentDescription = entry.icon().name,
|
|
||||||
modifier = Modifier.size(SegmentedButtonDefaults.IconSize),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
colors = SegmentedButtonDefaults.colors().copy(
|
|
||||||
activeContainerColor = MaterialTheme.colorScheme.surface,
|
|
||||||
inactiveContainerColor = MaterialTheme.colorScheme.background,
|
|
||||||
),
|
|
||||||
onCheckedChange = {
|
|
||||||
selectedSplitOption = entry
|
|
||||||
},
|
|
||||||
checked = active,
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
entry.text().asString(context)
|
|
||||||
.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() },
|
|
||||||
color = MaterialTheme.colorScheme.onBackground,
|
|
||||||
style = MaterialTheme.typography.labelMedium,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (selectedSplitOption != SplitOptions.ALL) {
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.Center,
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth(),
|
|
||||||
) {
|
|
||||||
CustomTextField(
|
|
||||||
textStyle = MaterialTheme.typography.labelMedium.copy(
|
|
||||||
color = MaterialTheme.colorScheme.onBackground,
|
|
||||||
),
|
|
||||||
value = query,
|
|
||||||
onValueChange = { input ->
|
|
||||||
query = input
|
|
||||||
},
|
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
|
||||||
label = {},
|
|
||||||
leading = {
|
|
||||||
val icon = Icons.Outlined.Search
|
|
||||||
Icon(icon, icon.name)
|
|
||||||
},
|
|
||||||
containerColor = MaterialTheme.colorScheme.background,
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth().height(inputHeight).padding(horizontal = 24.dp.scaledWidth()),
|
|
||||||
singleLine = true,
|
|
||||||
keyboardOptions =
|
|
||||||
KeyboardOptions(
|
|
||||||
capitalization = KeyboardCapitalization.None,
|
|
||||||
imeAction = ImeAction.Done,
|
|
||||||
),
|
|
||||||
keyboardActions = KeyboardActions(),
|
|
||||||
)
|
|
||||||
LazyColumn(
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
verticalArrangement = Arrangement.Top,
|
|
||||||
contentPadding = PaddingValues(top = 10.dp),
|
|
||||||
) {
|
|
||||||
items(sortedPackages, key = { it.`package` }) { app ->
|
|
||||||
val checked = selectedPackages.contains(app.`package`)
|
|
||||||
val onClick = {
|
|
||||||
if (checked) selectedPackages.remove(app.`package`) else selectedPackages.add(app.`package`)
|
|
||||||
}
|
|
||||||
SelectionItemButton(
|
|
||||||
{
|
|
||||||
Image(
|
|
||||||
rememberDrawablePainter(app.icon),
|
|
||||||
app.name,
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.padding(horizontal = 24.dp.scaledWidth())
|
|
||||||
.size(
|
|
||||||
iconSize,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
buttonText = app.name,
|
|
||||||
onClick = {
|
|
||||||
onClick()
|
|
||||||
},
|
|
||||||
trailing = {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.End,
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
Checkbox(
|
|
||||||
checked = checked,
|
|
||||||
onCheckedChange = {
|
|
||||||
onClick()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+4
-1
@@ -32,6 +32,7 @@ import androidx.compose.ui.text.input.KeyboardType
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.isUp
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.Route
|
import com.zaneschepke.wireguardautotunnel.ui.Route
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
|
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
|
||||||
@@ -41,6 +42,7 @@ import com.zaneschepke.wireguardautotunnel.ui.common.config.SubmitConfigurationT
|
|||||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.ForwardButton
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.ForwardButton
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isValidIpv4orIpv6Address
|
import com.zaneschepke.wireguardautotunnel.util.extensions.isValidIpv4orIpv6Address
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
|
||||||
@@ -51,7 +53,7 @@ import kotlin.text.isNullOrBlank
|
|||||||
import kotlin.text.toLong
|
import kotlin.text.toLong
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun OptionsScreen(tunnelConf: TunnelConf, viewModel: TunnelOptionsViewModel = hiltViewModel()) {
|
fun OptionsScreen(tunnelConf: TunnelConf, appUiState: AppUiState, viewModel: TunnelOptionsViewModel = hiltViewModel()) {
|
||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
|
|
||||||
var currentText by remember { mutableStateOf("") }
|
var currentText by remember { mutableStateOf("") }
|
||||||
@@ -194,6 +196,7 @@ fun OptionsScreen(tunnelConf: TunnelConf, viewModel: TunnelOptionsViewModel = hi
|
|||||||
trailing = {
|
trailing = {
|
||||||
ScaledSwitch(
|
ScaledSwitch(
|
||||||
checked = tunnelConf.isPingEnabled,
|
checked = tunnelConf.isPingEnabled,
|
||||||
|
enabled = !appUiState.activeTunnels.isUp(tunnelConf),
|
||||||
onClick = { onPingToggle() },
|
onClick = { onPingToggle() },
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
+23
@@ -9,6 +9,7 @@ import androidx.compose.material.icons.filled.ContentPasteGo
|
|||||||
import androidx.compose.material.icons.filled.Create
|
import androidx.compose.material.icons.filled.Create
|
||||||
import androidx.compose.material.icons.filled.FileOpen
|
import androidx.compose.material.icons.filled.FileOpen
|
||||||
import androidx.compose.material.icons.filled.QrCode
|
import androidx.compose.material.icons.filled.QrCode
|
||||||
|
import androidx.compose.material.icons.filled.Link
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
@@ -33,6 +34,7 @@ fun TunnelImportSheet(
|
|||||||
onQrClick: () -> Unit,
|
onQrClick: () -> Unit,
|
||||||
onManualImportClick: () -> Unit,
|
onManualImportClick: () -> Unit,
|
||||||
onClipboardClick: () -> Unit,
|
onClipboardClick: () -> Unit,
|
||||||
|
onUrlClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val sheetState = rememberModalBottomSheetState()
|
val sheetState = rememberModalBottomSheetState()
|
||||||
|
|
||||||
@@ -110,6 +112,27 @@ fun TunnelImportSheet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
|
Row(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable {
|
||||||
|
onDismiss()
|
||||||
|
onUrlClick()
|
||||||
|
}
|
||||||
|
.padding(10.dp),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Filled.Link,
|
||||||
|
contentDescription = stringResource(id = R.string.add_from_url),
|
||||||
|
modifier = Modifier.padding(10.dp),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
stringResource(id = R.string.add_from_url),
|
||||||
|
modifier = Modifier.padding(10.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
HorizontalDivider()
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
|
|||||||
+58
@@ -0,0 +1,58 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.main.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun UrlImportDialog(
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onConfirm: (String) -> Unit,
|
||||||
|
) {
|
||||||
|
var url by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
title = { Text(stringResource(R.string.add_from_url)) },
|
||||||
|
text = {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 8.dp)
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = url,
|
||||||
|
onValueChange = { url = it },
|
||||||
|
label = { Text(stringResource(R.string.enter_config_url)) },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = { onConfirm(url) },
|
||||||
|
enabled = url.isNotBlank()
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.okay))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = onDismiss) {
|
||||||
|
Text(stringResource(R.string.cancel))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
+108
@@ -0,0 +1,108 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.main.config
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.imePadding
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Save
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.components.AddPeerButton
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.components.InterfaceSection
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.components.PeersSection
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ConfigScreen(tunnelConf: TunnelConf?, viewModel: ConfigViewModel = hiltViewModel()) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val snackbar = SnackbarController.current
|
||||||
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
|
||||||
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
LaunchedEffect(tunnelConf) {
|
||||||
|
viewModel.initFromTunnel(tunnelConf)
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(uiState.message) {
|
||||||
|
uiState.message?.let { message ->
|
||||||
|
snackbar.showMessage(message.asString(context))
|
||||||
|
viewModel.setMessage(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uiState.showAuthPrompt) {
|
||||||
|
AuthorizationPrompt(
|
||||||
|
onSuccess = {
|
||||||
|
viewModel.toggleShowAuthPrompt()
|
||||||
|
viewModel.onAuthenticated()
|
||||||
|
},
|
||||||
|
onError = {
|
||||||
|
viewModel.toggleShowAuthPrompt()
|
||||||
|
snackbar.showMessage(
|
||||||
|
context.getString(R.string.error_authentication_failed),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onFailure = {
|
||||||
|
viewModel.toggleShowAuthPrompt()
|
||||||
|
snackbar.showMessage(
|
||||||
|
context.getString(R.string.error_authorization_failed),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopNavBar(
|
||||||
|
title = stringResource(R.string.edit_tunnel),
|
||||||
|
trailing = {
|
||||||
|
IconButton(onClick = {
|
||||||
|
keyboardController?.hide()
|
||||||
|
viewModel.save(tunnelConf)
|
||||||
|
}) {
|
||||||
|
Icon(Icons.Outlined.Save, contentDescription = stringResource(R.string.save))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) { padding ->
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(padding)
|
||||||
|
.imePadding()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(top = 24.dp.scaledHeight())
|
||||||
|
.padding(horizontal = 24.dp.scaledWidth()),
|
||||||
|
) {
|
||||||
|
InterfaceSection(uiState, viewModel)
|
||||||
|
PeersSection(uiState, viewModel)
|
||||||
|
AddPeerButton(viewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+172
@@ -0,0 +1,172 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.main.config
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.state.ConfigUiState
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.state.ConfigProxy
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.state.InterfaceProxy
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.state.PeerProxy
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class ConfigViewModel @Inject constructor(
|
||||||
|
private val tunnelRepository: TunnelRepository,
|
||||||
|
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val _uiState = MutableStateFlow(ConfigUiState())
|
||||||
|
val uiState: StateFlow<ConfigUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
fun initFromTunnel(tunnelConf: TunnelConf?) {
|
||||||
|
if (tunnelConf == null) return
|
||||||
|
_uiState.update {
|
||||||
|
val proxy = ConfigProxy.from(tunnelConf.toAmConfig())
|
||||||
|
it.copy(
|
||||||
|
tunnelName = tunnelConf.name,
|
||||||
|
configProxy = proxy,
|
||||||
|
showScripts = proxy.hasScripts(),
|
||||||
|
showAmneziaValues = proxy.`interface`.junkPacketCount.isNotBlank(),
|
||||||
|
isAuthenticated = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateTunnelName(name: String) {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(tunnelName = name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateInterface(newInterface: InterfaceProxy) {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
configProxy = it.configProxy.copy(
|
||||||
|
`interface` = newInterface,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleAmneziaValues() {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(showAmneziaValues = !it.showAmneziaValues)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleScripts() {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(showScripts = !it.showScripts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleAmneziaCompatibility() {
|
||||||
|
val (show, `interface`) = with(_uiState.value.configProxy) {
|
||||||
|
if (`interface`.isAmneziaCompatibilityModeSet()) {
|
||||||
|
Pair(false, `interface`.resetAmneziaProperties())
|
||||||
|
} else {
|
||||||
|
Pair(true, `interface`.toAmneziaCompatibilityConfig())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
showScripts = show,
|
||||||
|
configProxy = it.configProxy.copy(
|
||||||
|
`interface` = `interface`,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addPeer() {
|
||||||
|
_uiState.update { currentState ->
|
||||||
|
currentState.copy(
|
||||||
|
configProxy = currentState.configProxy.copy(
|
||||||
|
peers = currentState.configProxy.peers + PeerProxy(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removePeer(index: Int) {
|
||||||
|
_uiState.update { currentState ->
|
||||||
|
currentState.copy(
|
||||||
|
configProxy = currentState.configProxy.copy(
|
||||||
|
peers = currentState.configProxy.peers.toMutableList().apply { removeAt(index) },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updatePeer(index: Int, peer: PeerProxy) {
|
||||||
|
_uiState.update { currentState ->
|
||||||
|
currentState.copy(
|
||||||
|
configProxy = currentState.configProxy.copy(
|
||||||
|
peers = currentState.configProxy.peers.toMutableList().apply { set(index, peer) },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleLanExclusion(index: Int) {
|
||||||
|
val peer = _uiState.value.configProxy.peers[index]
|
||||||
|
val updated = if (peer.isLanExcluded()) peer.includeLan() else peer.excludeLan()
|
||||||
|
updatePeer(index, updated)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setMessage(message: StringValue?) {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(message = message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO improve error messaging
|
||||||
|
fun save(tunnelConf: TunnelConf?) = viewModelScope.launch(ioDispatcher) {
|
||||||
|
val message = try {
|
||||||
|
val saveConfig = buildTunnelConfFromState(tunnelConf)
|
||||||
|
tunnelRepository.save(saveConfig)
|
||||||
|
StringValue.StringResource(R.string.config_changes_saved)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.message?.let { message ->
|
||||||
|
(StringValue.DynamicString(message))
|
||||||
|
} ?: StringValue.StringResource(R.string.unknown_error)
|
||||||
|
}
|
||||||
|
setMessage(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildTunnelConfFromState(tunnelConf: TunnelConf?): TunnelConf {
|
||||||
|
val (wg, am) = _uiState.value.configProxy.buildConfigs()
|
||||||
|
val name = _uiState.value.tunnelName
|
||||||
|
return tunnelConf?.copyWithCallback(
|
||||||
|
tunName = name,
|
||||||
|
amQuick = am.toAwgQuickString(true),
|
||||||
|
wgQuick = wg.toWgQuickString(true),
|
||||||
|
) ?: TunnelConf(
|
||||||
|
tunName = name, amQuick = am.toAwgQuickString(true),
|
||||||
|
wgQuick = wg.toWgQuickString(true),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onAuthenticated() {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(isAuthenticated = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleShowAuthPrompt() {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(showAuthPrompt = !it.showAuthPrompt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.main.config.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.ConfigViewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AddPeerButton(viewModel: ConfigViewModel) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(bottom = 140.dp),
|
||||||
|
) {
|
||||||
|
TextButton(onClick = { viewModel.addPeer() }) {
|
||||||
|
Text(stringResource(R.string.add_peer))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+72
@@ -0,0 +1,72 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.main.config.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.rounded.MoreVert
|
||||||
|
import androidx.compose.material3.DropdownMenu
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun InterfaceDropdown(
|
||||||
|
expanded: Boolean,
|
||||||
|
onExpandedChange: (Boolean) -> Unit,
|
||||||
|
showScripts: Boolean,
|
||||||
|
showAmneziaValues: Boolean,
|
||||||
|
isAmneziaCompatibilitySet: Boolean,
|
||||||
|
onToggleScripts: () -> Unit,
|
||||||
|
onToggleAmneziaValues: () -> Unit,
|
||||||
|
onToggleAmneziaCompatibility: () -> Unit,
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
IconButton(
|
||||||
|
modifier = Modifier.size(iconSize),
|
||||||
|
onClick = { onExpandedChange(true) },
|
||||||
|
) {
|
||||||
|
Icon(Icons.Rounded.MoreVert, contentDescription = "More")
|
||||||
|
}
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = expanded,
|
||||||
|
onDismissRequest = { onExpandedChange(false) },
|
||||||
|
modifier = Modifier.shadow(12.dp).background(MaterialTheme.colorScheme.surface),
|
||||||
|
) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(if (showScripts) stringResource(R.string.hide_scripts) else stringResource(R.string.show_scripts)) },
|
||||||
|
onClick = {
|
||||||
|
onToggleScripts()
|
||||||
|
onExpandedChange(false)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(if (showAmneziaValues) stringResource(R.string.hide_amnezia_properties) else stringResource(R.string.show_amnezia_properties)) },
|
||||||
|
onClick = {
|
||||||
|
onToggleAmneziaValues()
|
||||||
|
onExpandedChange(false)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = {
|
||||||
|
Text(
|
||||||
|
if (isAmneziaCompatibilitySet) stringResource(R.string.remove_amnezia_compatibility) else stringResource(R.string.enable_amnezia_compatibility),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
onToggleAmneziaCompatibility()
|
||||||
|
onExpandedChange(false)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+220
@@ -0,0 +1,220 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.main.config.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.rounded.ContentCopy
|
||||||
|
import androidx.compose.material.icons.rounded.Refresh
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.state.InterfaceProxy
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun InterfaceFields(
|
||||||
|
interfaceState: InterfaceProxy,
|
||||||
|
showAuthPrompt: () -> Unit,
|
||||||
|
isAuthenticated: Boolean,
|
||||||
|
showScripts: Boolean,
|
||||||
|
showAmneziaValues: Boolean,
|
||||||
|
onInterfaceChange: (InterfaceProxy) -> Unit,
|
||||||
|
) {
|
||||||
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
val clipboardManager = LocalClipboardManager.current
|
||||||
|
val keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() })
|
||||||
|
val keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done)
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = interfaceState.privateKey,
|
||||||
|
onValueChange = { onInterfaceChange(interfaceState.copy(privateKey = it)) },
|
||||||
|
label = { Text(stringResource(R.string.private_key)) },
|
||||||
|
modifier = Modifier.fillMaxWidth().clickable {
|
||||||
|
if (!isAuthenticated) showAuthPrompt()
|
||||||
|
},
|
||||||
|
visualTransformation = if (isAuthenticated) VisualTransformation.None else PasswordVisualTransformation(),
|
||||||
|
trailingIcon = {
|
||||||
|
IconButton(enabled = isAuthenticated, onClick = {
|
||||||
|
val keypair = com.wireguard.crypto.KeyPair()
|
||||||
|
onInterfaceChange(
|
||||||
|
interfaceState.copy(
|
||||||
|
privateKey = keypair.privateKey.toBase64(),
|
||||||
|
publicKey = keypair.publicKey.toBase64(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
Icons.Rounded.Refresh,
|
||||||
|
stringResource(R.string.rotate_keys),
|
||||||
|
tint = if (isAuthenticated) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.outline,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled = isAuthenticated,
|
||||||
|
singleLine = true,
|
||||||
|
keyboardOptions = keyboardOptions,
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = interfaceState.publicKey,
|
||||||
|
onValueChange = { onInterfaceChange(interfaceState.copy(publicKey = it)) },
|
||||||
|
label = { Text(stringResource(R.string.public_key)) },
|
||||||
|
enabled = false,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
singleLine = true,
|
||||||
|
trailingIcon = {
|
||||||
|
IconButton(onClick = {
|
||||||
|
clipboardManager.setText(AnnotatedString(interfaceState.publicKey))
|
||||||
|
}) {
|
||||||
|
Icon(Icons.Rounded.ContentCopy, stringResource(R.string.copy_public_key))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
keyboardOptions = keyboardOptions,
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
)
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = interfaceState.addresses,
|
||||||
|
onValueChange = { onInterfaceChange(interfaceState.copy(addresses = it)) },
|
||||||
|
label = stringResource(R.string.addresses),
|
||||||
|
hint = stringResource(R.string.comma_separated_list),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = interfaceState.listenPort,
|
||||||
|
onValueChange = { onInterfaceChange(interfaceState.copy(listenPort = it)) },
|
||||||
|
label = stringResource(R.string.listen_port),
|
||||||
|
hint = stringResource(R.string.random),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(5.dp),
|
||||||
|
) {
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = interfaceState.dnsServers,
|
||||||
|
onValueChange = { onInterfaceChange(interfaceState.copy(dnsServers = it)) },
|
||||||
|
label = stringResource(R.string.dns_servers),
|
||||||
|
hint = stringResource(R.string.comma_separated_list),
|
||||||
|
modifier = Modifier.weight(3f),
|
||||||
|
)
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = interfaceState.mtu,
|
||||||
|
onValueChange = { onInterfaceChange(interfaceState.copy(mtu = it)) },
|
||||||
|
label = stringResource(R.string.mtu),
|
||||||
|
hint = stringResource(R.string.auto),
|
||||||
|
modifier = Modifier.weight(2f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (showScripts) {
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = interfaceState.preUp,
|
||||||
|
onValueChange = { onInterfaceChange(interfaceState.copy(preUp = it)) },
|
||||||
|
label = stringResource(R.string.pre_up),
|
||||||
|
hint = stringResource(R.string.comma_separated_list).lowercase(),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = interfaceState.postUp,
|
||||||
|
onValueChange = { onInterfaceChange(interfaceState.copy(postUp = it)) },
|
||||||
|
label = stringResource(R.string.post_up),
|
||||||
|
hint = stringResource(R.string.comma_separated_list).lowercase(),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = interfaceState.preDown,
|
||||||
|
onValueChange = { onInterfaceChange(interfaceState.copy(preDown = it)) },
|
||||||
|
label = stringResource(R.string.pre_down),
|
||||||
|
hint = stringResource(R.string.comma_separated_list).lowercase(),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = interfaceState.postDown,
|
||||||
|
onValueChange = { onInterfaceChange(interfaceState.copy(postDown = it)) },
|
||||||
|
label = stringResource(R.string.post_down),
|
||||||
|
hint = stringResource(R.string.comma_separated_list).lowercase(),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (showAmneziaValues) {
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = interfaceState.junkPacketCount,
|
||||||
|
onValueChange = { onInterfaceChange(interfaceState.copy(junkPacketCount = it)) },
|
||||||
|
label = stringResource(R.string.junk_packet_count),
|
||||||
|
hint = stringResource(R.string.junk_packet_count).lowercase(),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = interfaceState.junkPacketMinSize,
|
||||||
|
onValueChange = { onInterfaceChange(interfaceState.copy(junkPacketMinSize = it)) },
|
||||||
|
label = stringResource(R.string.junk_packet_minimum_size),
|
||||||
|
hint = stringResource(R.string.junk_packet_minimum_size).lowercase(),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = interfaceState.junkPacketMaxSize,
|
||||||
|
onValueChange = { onInterfaceChange(interfaceState.copy(junkPacketMaxSize = it)) },
|
||||||
|
label = stringResource(R.string.junk_packet_maximum_size),
|
||||||
|
hint = stringResource(R.string.junk_packet_maximum_size).lowercase(),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = interfaceState.initPacketJunkSize,
|
||||||
|
onValueChange = { onInterfaceChange(interfaceState.copy(initPacketJunkSize = it)) },
|
||||||
|
label = stringResource(R.string.init_packet_junk_size),
|
||||||
|
hint = stringResource(R.string.init_packet_junk_size).lowercase(),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = interfaceState.responsePacketJunkSize,
|
||||||
|
onValueChange = { onInterfaceChange(interfaceState.copy(responsePacketJunkSize = it)) },
|
||||||
|
label = stringResource(R.string.response_packet_junk_size),
|
||||||
|
hint = stringResource(R.string.response_packet_junk_size).lowercase(),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = interfaceState.initPacketMagicHeader,
|
||||||
|
onValueChange = { onInterfaceChange(interfaceState.copy(initPacketMagicHeader = it)) },
|
||||||
|
label = stringResource(R.string.init_packet_magic_header),
|
||||||
|
hint = stringResource(R.string.init_packet_magic_header).lowercase(),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = interfaceState.responsePacketMagicHeader,
|
||||||
|
onValueChange = { onInterfaceChange(interfaceState.copy(responsePacketMagicHeader = it)) },
|
||||||
|
label = stringResource(R.string.response_packet_magic_header),
|
||||||
|
hint = stringResource(R.string.response_packet_magic_header).lowercase(),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = interfaceState.underloadPacketMagicHeader,
|
||||||
|
onValueChange = { onInterfaceChange(interfaceState.copy(underloadPacketMagicHeader = it)) },
|
||||||
|
label = stringResource(R.string.underload_packet_magic_header),
|
||||||
|
hint = stringResource(R.string.underload_packet_magic_header).lowercase(),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = interfaceState.transportPacketMagicHeader,
|
||||||
|
onValueChange = { onInterfaceChange(interfaceState.copy(transportPacketMagicHeader = it)) },
|
||||||
|
label = stringResource(R.string.transport_packet_magic_header),
|
||||||
|
hint = stringResource(R.string.transport_packet_magic_header).lowercase(),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+74
@@ -0,0 +1,74 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.main.config.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.focusGroup
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.ConfigViewModel
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.state.ConfigUiState
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun InterfaceSection(uiState: ConfigUiState, viewModel: ConfigViewModel) {
|
||||||
|
var isDropDownExpanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(5.dp),
|
||||||
|
modifier = Modifier.padding(16.dp.scaledWidth()).focusGroup(),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
GroupLabel(stringResource(R.string.interface_))
|
||||||
|
InterfaceDropdown(
|
||||||
|
expanded = isDropDownExpanded,
|
||||||
|
onExpandedChange = { isDropDownExpanded = it },
|
||||||
|
showScripts = uiState.showScripts,
|
||||||
|
showAmneziaValues = uiState.showAmneziaValues,
|
||||||
|
isAmneziaCompatibilitySet = uiState.configProxy.`interface`.isAmneziaCompatibilityModeSet(),
|
||||||
|
onToggleScripts = viewModel::toggleScripts,
|
||||||
|
onToggleAmneziaValues = viewModel::toggleAmneziaValues,
|
||||||
|
onToggleAmneziaCompatibility = viewModel::toggleAmneziaCompatibility,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = uiState.tunnelName,
|
||||||
|
onValueChange = viewModel::updateTunnelName,
|
||||||
|
label = stringResource(R.string.name),
|
||||||
|
hint = stringResource(R.string.tunnel_name).lowercase(),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
InterfaceFields(
|
||||||
|
interfaceState = uiState.configProxy.`interface`,
|
||||||
|
showAuthPrompt = { viewModel.toggleShowAuthPrompt() },
|
||||||
|
showScripts = uiState.showScripts,
|
||||||
|
showAmneziaValues = uiState.showAmneziaValues,
|
||||||
|
onInterfaceChange = viewModel::updateInterface,
|
||||||
|
isAuthenticated = uiState.isAuthenticated,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+73
@@ -0,0 +1,73 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.main.config.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.state.PeerProxy
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PeerFields(peer: PeerProxy, onPeerChange: (PeerProxy) -> Unit, showAuthPrompt: () -> Unit, isAuthenticated: Boolean) {
|
||||||
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
val keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() })
|
||||||
|
val keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done)
|
||||||
|
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = peer.publicKey,
|
||||||
|
onValueChange = { onPeerChange(peer.copy(publicKey = it)) },
|
||||||
|
label = stringResource(R.string.public_key),
|
||||||
|
hint = stringResource(R.string.base64_key),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
visualTransformation = if (isAuthenticated) VisualTransformation.None else PasswordVisualTransformation(),
|
||||||
|
value = peer.preSharedKey,
|
||||||
|
enabled = isAuthenticated,
|
||||||
|
onValueChange = { onPeerChange(peer.copy(preSharedKey = it)) },
|
||||||
|
label = { Text(stringResource(R.string.preshared_key)) },
|
||||||
|
placeholder = { Text(stringResource(R.string.optional)) },
|
||||||
|
modifier = Modifier.fillMaxWidth().clickable { if (!isAuthenticated) showAuthPrompt() },
|
||||||
|
keyboardOptions = keyboardOptions,
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
singleLine = true,
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = peer.persistentKeepalive,
|
||||||
|
onValueChange = { onPeerChange(peer.copy(persistentKeepalive = it)) },
|
||||||
|
label = { Text(stringResource(R.string.persistent_keepalive)) },
|
||||||
|
placeholder = { Text(stringResource(R.string.optional_no_recommend)) },
|
||||||
|
trailingIcon = { Text(stringResource(R.string.seconds), modifier = Modifier.padding(end = 10.dp)) },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
keyboardOptions = keyboardOptions,
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
)
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = peer.endpoint,
|
||||||
|
onValueChange = { onPeerChange(peer.copy(endpoint = it)) },
|
||||||
|
label = stringResource(R.string.endpoint),
|
||||||
|
hint = stringResource(R.string.endpoint).lowercase(),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = peer.allowedIps,
|
||||||
|
onValueChange = { onPeerChange(peer.copy(allowedIps = it)) },
|
||||||
|
label = { Text(stringResource(R.string.allowed_ips)) },
|
||||||
|
placeholder = { Text(stringResource(R.string.comma_separated_list)) },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
keyboardOptions = keyboardOptions,
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
)
|
||||||
|
}
|
||||||
+102
@@ -0,0 +1,102 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.main.config.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.focusGroup
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.rounded.Delete
|
||||||
|
import androidx.compose.material.icons.rounded.MoreVert
|
||||||
|
import androidx.compose.material3.DropdownMenu
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.ConfigViewModel
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.state.ConfigUiState
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PeersSection(uiState: ConfigUiState, viewModel: ConfigViewModel) {
|
||||||
|
uiState.configProxy.peers.forEachIndexed { index, peer ->
|
||||||
|
var isDropDownExpanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
|
modifier = Modifier.padding(16.dp.scaledWidth()).focusGroup(),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
GroupLabel(stringResource(R.string.peer))
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
IconButton(
|
||||||
|
modifier = Modifier.size(iconSize),
|
||||||
|
onClick = { viewModel.removePeer(index) },
|
||||||
|
) {
|
||||||
|
Icon(Icons.Rounded.Delete, contentDescription = "Delete")
|
||||||
|
}
|
||||||
|
Column {
|
||||||
|
IconButton(
|
||||||
|
modifier = Modifier.size(iconSize),
|
||||||
|
onClick = { isDropDownExpanded = true },
|
||||||
|
) {
|
||||||
|
Icon(Icons.Rounded.MoreVert, contentDescription = "More")
|
||||||
|
}
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = isDropDownExpanded,
|
||||||
|
onDismissRequest = { isDropDownExpanded = false },
|
||||||
|
modifier = Modifier.shadow(12.dp).background(MaterialTheme.colorScheme.surface),
|
||||||
|
) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = {
|
||||||
|
Text(if (peer.isLanExcluded()) stringResource(R.string.include_lan) else stringResource(R.string.exclude_lan))
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
viewModel.toggleLanExclusion(index)
|
||||||
|
isDropDownExpanded = false
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PeerFields(
|
||||||
|
peer = peer,
|
||||||
|
onPeerChange = { viewModel.updatePeer(index, it) },
|
||||||
|
showAuthPrompt = { viewModel.toggleShowAuthPrompt() },
|
||||||
|
isAuthenticated = uiState.isAuthenticated,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.main.config.state
|
||||||
|
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.state.ConfigProxy
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.state.InterfaceProxy
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.state.PeerProxy
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||||
|
|
||||||
|
data class ConfigUiState(
|
||||||
|
val tunnelName: String = "",
|
||||||
|
val configProxy: ConfigProxy = ConfigProxy(`interface` = InterfaceProxy(), peers = listOf(PeerProxy())),
|
||||||
|
val showAmneziaValues: Boolean = false,
|
||||||
|
val showScripts: Boolean = false,
|
||||||
|
val isAuthenticated: Boolean = true,
|
||||||
|
val showAuthPrompt: Boolean = false,
|
||||||
|
val message: StringValue? = null,
|
||||||
|
)
|
||||||
+60
@@ -0,0 +1,60 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.main.splittunnel
|
||||||
|
|
||||||
|
import androidx.compose.animation.Crossfade
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Save
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.splittunnel.components.SplitTunnelContent
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SplitTunnelScreen(viewModel: SplitTunnelViewModel = hiltViewModel()) {
|
||||||
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopNavBar(
|
||||||
|
title = stringResource(R.string.tunneling_apps),
|
||||||
|
trailing = {
|
||||||
|
IconButton(onClick = { viewModel.saveChanges() }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Save,
|
||||||
|
contentDescription = stringResource(R.string.save),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) { padding ->
|
||||||
|
Crossfade(
|
||||||
|
targetState = uiState.loading,
|
||||||
|
animationSpec = tween(200),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(padding),
|
||||||
|
) { isLoading ->
|
||||||
|
if (isLoading) {
|
||||||
|
SplitTunnelSkeleton()
|
||||||
|
} else {
|
||||||
|
SplitTunnelContent(
|
||||||
|
uiState = uiState,
|
||||||
|
onSplitOptionChange = viewModel::updateSplitOption,
|
||||||
|
onAppSelectionToggle = viewModel::toggleAppSelection,
|
||||||
|
onQueryChange = viewModel::onSearchQuery,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+111
@@ -0,0 +1,111 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.main.splittunnel
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
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.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.animation.ShimmerEffect
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SplitTunnelSkeleton() {
|
||||||
|
val shimmerBrush = ShimmerEffect()
|
||||||
|
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.CenterVertically),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 24.dp.scaledHeight()),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 24.dp.scaledWidth())
|
||||||
|
.height(45.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
repeat(3) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(45.dp)
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.background(shimmerBrush),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 24.dp.scaledWidth())
|
||||||
|
.height(45.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(45.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.background(shimmerBrush),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Top,
|
||||||
|
contentPadding = PaddingValues(top = 10.dp),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
items(20) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 24.dp.scaledWidth(), vertical = 8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(iconSize)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(shimmerBrush),
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(20.dp)
|
||||||
|
.weight(1f)
|
||||||
|
.clip(RoundedCornerShape(4.dp))
|
||||||
|
.background(shimmerBrush),
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(shimmerBrush),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+164
@@ -0,0 +1,164 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.main.splittunnel
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.Route
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.splittunnel.state.SplitOption
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.splittunnel.state.SplitTunnelUiState
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.splittunnel.state.TunnelApp
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.state.ConfigProxy
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.state.InterfaceProxy
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.getAllInternetCapablePackages
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.text.Collator
|
||||||
|
import java.util.*
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class SplitTunnelViewModel @Inject constructor(
|
||||||
|
@ApplicationContext private val context: Context,
|
||||||
|
private val tunnelRepository: TunnelRepository,
|
||||||
|
savedStateHandle: SavedStateHandle,
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val _uiState = MutableStateFlow(SplitTunnelUiState())
|
||||||
|
val uiState: StateFlow<SplitTunnelUiState> = _uiState
|
||||||
|
.stateIn(
|
||||||
|
scope = viewModelScope,
|
||||||
|
started = SharingStarted.WhileSubscribed(5000L),
|
||||||
|
initialValue = SplitTunnelUiState(),
|
||||||
|
)
|
||||||
|
|
||||||
|
private val tunnelId: Int? = savedStateHandle.get<Int>(Route.SplitTunnel.KEY_ID)
|
||||||
|
private var allTunneledApps: List<Pair<TunnelApp, Boolean>> = emptyList()
|
||||||
|
|
||||||
|
init {
|
||||||
|
tunnelId?.let { loadInitialState(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadInitialState(tunnelId: Int) = viewModelScope.launch {
|
||||||
|
val tunnel = tunnelRepository.getById(tunnelId) ?: return@launch
|
||||||
|
val proxyInterface = InterfaceProxy.from(tunnel.toAmConfig().`interface`)
|
||||||
|
val splitOption = when {
|
||||||
|
proxyInterface.excludedApplications.isNotEmpty() -> SplitOption.EXCLUDE
|
||||||
|
proxyInterface.includedApplications.isNotEmpty() -> SplitOption.INCLUDE
|
||||||
|
else -> SplitOption.ALL
|
||||||
|
}
|
||||||
|
|
||||||
|
val packages = context.getAllInternetCapablePackages()
|
||||||
|
|
||||||
|
val installedPackages = packages
|
||||||
|
.map { it.packageName }
|
||||||
|
.toSet()
|
||||||
|
|
||||||
|
// remove uninstalled apps
|
||||||
|
proxyInterface.includedApplications.retainAll { it in installedPackages }
|
||||||
|
proxyInterface.excludedApplications.retainAll { it in installedPackages }
|
||||||
|
|
||||||
|
var configProxy = ConfigProxy.from(tunnel.toAmConfig())
|
||||||
|
configProxy = configProxy.copy(`interface` = proxyInterface)
|
||||||
|
saveProxyConfig(configProxy, tunnel)
|
||||||
|
|
||||||
|
val collator = Collator.getInstance(Locale.getDefault())
|
||||||
|
val tunneledApps = packages
|
||||||
|
.filter { it.applicationInfo != null }
|
||||||
|
.map { pack ->
|
||||||
|
val selected = when (splitOption) {
|
||||||
|
SplitOption.INCLUDE -> proxyInterface.includedApplications.contains(pack.packageName)
|
||||||
|
SplitOption.ALL -> false
|
||||||
|
SplitOption.EXCLUDE -> proxyInterface.excludedApplications.contains(pack.packageName)
|
||||||
|
}
|
||||||
|
Pair(
|
||||||
|
TunnelApp(
|
||||||
|
name = context.packageManager.getApplicationLabel(pack.applicationInfo!!).toString(),
|
||||||
|
`package` = pack.packageName,
|
||||||
|
),
|
||||||
|
selected,
|
||||||
|
)
|
||||||
|
}.sortedWith(compareBy(collator) { it.first.name })
|
||||||
|
|
||||||
|
allTunneledApps = tunneledApps
|
||||||
|
|
||||||
|
delay(500)
|
||||||
|
|
||||||
|
_uiState.update {
|
||||||
|
SplitTunnelUiState(
|
||||||
|
loading = false,
|
||||||
|
tunnelConf = tunnel,
|
||||||
|
tunneledApps = tunneledApps,
|
||||||
|
splitOption = splitOption,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onSearchQuery(query: String) {
|
||||||
|
val filteredApps = if (query.isBlank()) {
|
||||||
|
allTunneledApps
|
||||||
|
} else {
|
||||||
|
allTunneledApps.filter {
|
||||||
|
it.first.name.contains(query, ignoreCase = true) ||
|
||||||
|
it.first.`package`.contains(query, ignoreCase = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
searchQuery = query,
|
||||||
|
tunneledApps = filteredApps,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateSplitOption(newOption: SplitOption) {
|
||||||
|
_uiState.value = _uiState.value.copy(splitOption = newOption)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleAppSelection(packageName: String) {
|
||||||
|
val currentState = _uiState.value
|
||||||
|
val updatedApps = currentState.tunneledApps.map { (app, selected) ->
|
||||||
|
if (app.`package` == packageName) Pair(app, !selected) else Pair(app, selected)
|
||||||
|
}
|
||||||
|
_uiState.value = currentState.copy(tunneledApps = updatedApps)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveChanges() = viewModelScope.launch {
|
||||||
|
val state = _uiState.value
|
||||||
|
val tunnel = state.tunnelConf ?: return@launch
|
||||||
|
val configProxy = ConfigProxy.from(tunnel.toAmConfig())
|
||||||
|
val updatedApps = state.tunneledApps
|
||||||
|
with(configProxy.`interface`) {
|
||||||
|
includedApplications.clear()
|
||||||
|
excludedApplications.clear()
|
||||||
|
when (state.splitOption) {
|
||||||
|
SplitOption.INCLUDE -> {
|
||||||
|
includedApplications.addAll(updatedApps.filter { it.second }.map { it.first.`package` })
|
||||||
|
}
|
||||||
|
SplitOption.EXCLUDE -> {
|
||||||
|
excludedApplications.addAll(updatedApps.filter { it.second }.map { it.first.`package` })
|
||||||
|
}
|
||||||
|
SplitOption.ALL -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
saveProxyConfig(configProxy, tunnel)
|
||||||
|
SnackbarController.showMessage(StringValue.StringResource(R.string.config_changes_saved))
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun saveProxyConfig(proxy: ConfigProxy, tunnel: TunnelConf) {
|
||||||
|
val (wg, am) = proxy.buildConfigs()
|
||||||
|
tunnelRepository.save(tunnel.copyWithCallback(amQuick = am.toAwgQuickString(true), wgQuick = wg.toWgQuickString(true)))
|
||||||
|
}
|
||||||
|
}
|
||||||
+59
@@ -0,0 +1,59 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.main.splittunnel.components
|
||||||
|
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.google.accompanist.drawablepainter.rememberDrawablePainter
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.button.SelectionItemButton
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.splittunnel.state.TunnelApp
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AppListItem(appInfo: TunnelApp, isSelected: Boolean, onToggle: () -> Unit) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val icon = remember(appInfo.`package`) {
|
||||||
|
try {
|
||||||
|
context.packageManager.getApplicationIcon(appInfo.`package`)
|
||||||
|
} catch (e: PackageManager.NameNotFoundException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SelectionItemButton(
|
||||||
|
leading = {
|
||||||
|
Image(
|
||||||
|
painter = rememberDrawablePainter(icon),
|
||||||
|
contentDescription = appInfo.name,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 24.dp.scaledWidth())
|
||||||
|
.size(iconSize),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
buttonText = appInfo.name,
|
||||||
|
onClick = onToggle,
|
||||||
|
trailing = {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.End,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Checkbox(
|
||||||
|
checked = isSelected,
|
||||||
|
onCheckedChange = { onToggle() },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
+77
@@ -0,0 +1,77 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.main.splittunnel.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Search
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.textbox.CustomTextField
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.splittunnel.state.TunnelApp
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AppListSection(apps: List<Pair<TunnelApp, Boolean>>, onAppSelectionToggle: (String) -> Unit, onQueryChange: (String) -> Unit, query: String) {
|
||||||
|
val inputHeight = 45.dp
|
||||||
|
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
CustomTextField(
|
||||||
|
textStyle = MaterialTheme.typography.labelMedium.copy(
|
||||||
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
|
),
|
||||||
|
value = query,
|
||||||
|
onValueChange = onQueryChange,
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
label = {},
|
||||||
|
leading = {
|
||||||
|
Icon(Icons.Outlined.Search, stringResource(R.string.search))
|
||||||
|
},
|
||||||
|
containerColor = MaterialTheme.colorScheme.background,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(inputHeight)
|
||||||
|
.padding(horizontal = 24.dp.scaledWidth()),
|
||||||
|
singleLine = true,
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
capitalization = KeyboardCapitalization.None,
|
||||||
|
imeAction = ImeAction.Done,
|
||||||
|
),
|
||||||
|
keyboardActions = KeyboardActions(),
|
||||||
|
)
|
||||||
|
LazyColumn(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Top,
|
||||||
|
contentPadding = PaddingValues(top = 10.dp),
|
||||||
|
) {
|
||||||
|
items(apps, key = { it.first.`package` }) { app ->
|
||||||
|
AppListItem(
|
||||||
|
appInfo = app.first,
|
||||||
|
isSelected = app.second,
|
||||||
|
onToggle = { onAppSelectionToggle(app.first.`package`) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+79
@@ -0,0 +1,79 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.main.splittunnel.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
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.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Check
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.MultiChoiceSegmentedButtonRow
|
||||||
|
import androidx.compose.material3.SegmentedButton
|
||||||
|
import androidx.compose.material3.SegmentedButtonDefaults
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.splittunnel.state.SplitOption
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SplitOptionSelector(selectedOption: SplitOption, onOptionChange: (SplitOption) -> Unit) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val inputHeight = 45.dp
|
||||||
|
|
||||||
|
MultiChoiceSegmentedButtonRow(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(color = MaterialTheme.colorScheme.background)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 24.dp.scaledWidth())
|
||||||
|
.height(inputHeight),
|
||||||
|
) {
|
||||||
|
SplitOption.entries.forEachIndexed { index, entry ->
|
||||||
|
val active = selectedOption == entry
|
||||||
|
SegmentedButton(
|
||||||
|
shape = SegmentedButtonDefaults.itemShape(
|
||||||
|
index = index,
|
||||||
|
count = SplitOption.entries.size,
|
||||||
|
baseShape = RoundedCornerShape(8.dp),
|
||||||
|
),
|
||||||
|
icon = {
|
||||||
|
SegmentedButtonDefaults.Icon(active = active, activeContent = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Check,
|
||||||
|
contentDescription = stringResource(R.string.select),
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.size(SegmentedButtonDefaults.IconSize),
|
||||||
|
)
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
imageVector = entry.icon(),
|
||||||
|
contentDescription = entry.icon().name,
|
||||||
|
modifier = Modifier.size(SegmentedButtonDefaults.IconSize),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = SegmentedButtonDefaults.colors().copy(
|
||||||
|
activeContainerColor = MaterialTheme.colorScheme.surface,
|
||||||
|
inactiveContainerColor = MaterialTheme.colorScheme.background,
|
||||||
|
),
|
||||||
|
onCheckedChange = { onOptionChange(entry) },
|
||||||
|
checked = active,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
entry.text().asString(context)
|
||||||
|
.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() },
|
||||||
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.main.splittunnel.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.splittunnel.state.SplitOption
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.splittunnel.state.SplitTunnelUiState
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SplitTunnelContent(
|
||||||
|
uiState: SplitTunnelUiState,
|
||||||
|
onSplitOptionChange: (SplitOption) -> Unit,
|
||||||
|
onAppSelectionToggle: (String) -> Unit,
|
||||||
|
onQueryChange: (String) -> Unit,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.CenterVertically),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 24.dp.scaledHeight()),
|
||||||
|
) {
|
||||||
|
SplitOptionSelector(
|
||||||
|
selectedOption = uiState.splitOption,
|
||||||
|
onOptionChange = onSplitOptionChange,
|
||||||
|
)
|
||||||
|
if (uiState.splitOption != SplitOption.ALL) {
|
||||||
|
AppListSection(
|
||||||
|
apps = uiState.tunneledApps,
|
||||||
|
onAppSelectionToggle = onAppSelectionToggle,
|
||||||
|
onQueryChange = onQueryChange,
|
||||||
|
uiState.searchQuery,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.enums
|
package com.zaneschepke.wireguardautotunnel.ui.screens.main.splittunnel.state
|
||||||
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
@@ -8,7 +8,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
|||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||||
|
|
||||||
enum class SplitOptions {
|
enum class SplitOption {
|
||||||
INCLUDE,
|
INCLUDE,
|
||||||
ALL,
|
ALL,
|
||||||
EXCLUDE,
|
EXCLUDE,
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.main.splittunnel.state
|
||||||
|
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||||
|
|
||||||
|
data class SplitTunnelUiState(
|
||||||
|
val loading: Boolean = true,
|
||||||
|
val tunnelConf: TunnelConf? = null,
|
||||||
|
val tunneledApps: SplitTunnelApps = emptyList(),
|
||||||
|
val splitOption: SplitOption = SplitOption.ALL,
|
||||||
|
val searchQuery: String = "",
|
||||||
|
)
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.main.splittunnel.state
|
||||||
|
|
||||||
|
data class TunnelApp(
|
||||||
|
val name: String,
|
||||||
|
val `package`: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
typealias SplitTunnelApps = List<Pair<TunnelApp, Boolean>>
|
||||||
+5
-5
@@ -352,11 +352,11 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
|
|||||||
ScaledSwitch(
|
ScaledSwitch(
|
||||||
uiState.appSettings.isKernelEnabled,
|
uiState.appSettings.isKernelEnabled,
|
||||||
onClick = { appViewModel.onToggleKernelMode() },
|
onClick = { appViewModel.onToggleKernelMode() },
|
||||||
// enabled = !(
|
enabled = !(
|
||||||
// uiState.settings.isAutoTunnelEnabled ||
|
uiState.appSettings.isAutoTunnelEnabled ||
|
||||||
// uiState.settings.isAlwaysOnVpnEnabled ||
|
uiState.appSettings.isAlwaysOnVpnEnabled ||
|
||||||
// (uiState.vpnState.status == TunnelState.UP)
|
uiState.activeTunnels.isNotEmpty()
|
||||||
// ),
|
),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
|
|||||||
+2
-19
@@ -18,8 +18,6 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -36,7 +34,6 @@ import com.zaneschepke.wireguardautotunnel.ui.Route
|
|||||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
|
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
|
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
|
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
|
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.label.VersionLabel
|
import com.zaneschepke.wireguardautotunnel.ui.common.label.VersionLabel
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
|
||||||
@@ -51,20 +48,6 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
|
|||||||
fun SupportScreen(appUiState: AppUiState, appViewModel: AppViewModel) {
|
fun SupportScreen(appUiState: AppUiState, appViewModel: AppViewModel) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
|
|
||||||
var showDialog by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
if (showDialog) {
|
|
||||||
InfoDialog(onAttest = {
|
|
||||||
showDialog = false
|
|
||||||
appViewModel.onToggleLocalLogging()
|
|
||||||
}, onDismiss = {
|
|
||||||
showDialog = false
|
|
||||||
}, title = {
|
|
||||||
Text(stringResource(R.string.configuration_change))
|
|
||||||
}, body = { Text(stringResource(R.string.requires_app_relaunch)) }, confirmText = { Text(stringResource(R.string.yes)) })
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top),
|
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top),
|
||||||
@@ -115,12 +98,12 @@ fun SupportScreen(appUiState: AppUiState, appViewModel: AppViewModel) {
|
|||||||
ScaledSwitch(
|
ScaledSwitch(
|
||||||
appUiState.generalState.isLocalLogsEnabled,
|
appUiState.generalState.isLocalLogsEnabled,
|
||||||
onClick = {
|
onClick = {
|
||||||
showDialog = true
|
appViewModel.onToggleLocalLogging()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
showDialog = true
|
appViewModel.onToggleLocalLogging()
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ package com.zaneschepke.wireguardautotunnel.ui.state
|
|||||||
import com.zaneschepke.wireguardautotunnel.data.model.GeneralState
|
import com.zaneschepke.wireguardautotunnel.data.model.GeneralState
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
|
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||||
|
|
||||||
data class AppUiState(
|
data class AppUiState(
|
||||||
val appSettings: AppSettings = AppSettings(),
|
val appSettings: AppSettings = AppSettings(),
|
||||||
val tunnels: List<TunnelConf> = emptyList(),
|
val tunnels: List<TunnelConf> = emptyList(),
|
||||||
|
val activeTunnels: Map<TunnelConf, TunnelState> = emptyMap(),
|
||||||
val generalState: GeneralState = GeneralState(),
|
val generalState: GeneralState = GeneralState(),
|
||||||
val autoTunnelActive: Boolean = false,
|
val autoTunnelActive: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.state
|
||||||
|
|
||||||
|
import org.amnezia.awg.config.Config
|
||||||
|
|
||||||
|
data class ConfigProxy(
|
||||||
|
val peers: List<PeerProxy>,
|
||||||
|
val `interface`: InterfaceProxy,
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun hasScripts(): Boolean {
|
||||||
|
return `interface`.preUp.isNotBlank() || `interface`.preDown.isNotBlank() ||
|
||||||
|
`interface`.postUp.isNotBlank() || `interface`.postDown.isNotBlank()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildConfigs(): Pair<com.wireguard.config.Config, Config> {
|
||||||
|
return Pair(
|
||||||
|
com.wireguard.config.Config.Builder().apply {
|
||||||
|
addPeers(peers.map { it.toWgPeer() })
|
||||||
|
setInterface(`interface`.toWgInterface())
|
||||||
|
}.build(),
|
||||||
|
Config.Builder().apply {
|
||||||
|
addPeers(peers.map { it.toAmPeer() })
|
||||||
|
setInterface(`interface`.toAmInterface())
|
||||||
|
}.build(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun from(amConfig: Config): ConfigProxy {
|
||||||
|
return ConfigProxy(
|
||||||
|
`interface` = InterfaceProxy.from(amConfig.`interface`),
|
||||||
|
peers = amConfig.peers.map { PeerProxy.from(it) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.state
|
|
||||||
|
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
|
|
||||||
data class SplitTunnelApp(
|
|
||||||
val icon: Drawable,
|
|
||||||
val name: String,
|
|
||||||
val `package`: String,
|
|
||||||
)
|
|
||||||
+6
-16
@@ -2,7 +2,6 @@ package com.zaneschepke.wireguardautotunnel.util.extensions
|
|||||||
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import com.wireguard.android.backend.BackendException
|
import com.wireguard.android.backend.BackendException
|
||||||
import com.wireguard.android.util.RootShell
|
|
||||||
import com.wireguard.config.Peer
|
import com.wireguard.config.Peer
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendError
|
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendError
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
||||||
@@ -43,15 +42,12 @@ fun TunnelStatistics.PeerStats.handshakeStatus(): HandshakeStatus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Peer.isReachable(preferIpv4: Boolean): Boolean {
|
fun Peer.isReachable(): Boolean {
|
||||||
val host =
|
val host = if (this.endpoint.isPresent) {
|
||||||
if (this.endpoint.isPresent &&
|
this.endpoint.get().host
|
||||||
this.endpoint.get().getResolved(preferIpv4).isPresent
|
} else {
|
||||||
) {
|
Constants.DEFAULT_PING_IP
|
||||||
this.endpoint.get().getResolved(preferIpv4).get().host
|
}
|
||||||
} else {
|
|
||||||
Constants.DEFAULT_PING_IP
|
|
||||||
}
|
|
||||||
Timber.d("Checking reachability of peer: $host")
|
Timber.d("Checking reachability of peer: $host")
|
||||||
val reachable =
|
val reachable =
|
||||||
InetAddress.getByName(host)
|
InetAddress.getByName(host)
|
||||||
@@ -87,12 +83,6 @@ fun Config.toWgQuickString(): String {
|
|||||||
return lines.joinToString(System.lineSeparator())
|
return lines.joinToString(System.lineSeparator())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun RootShell.getCurrentWifiName(): String? {
|
|
||||||
val response = mutableListOf<String>()
|
|
||||||
this.run(response, "dumpsys wifi | grep 'Supplicant state: COMPLETED' | grep -o 'SSID: [^,]*' | cut -d ' ' -f2- | tr -d '\"'")
|
|
||||||
return response.firstOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Backend.BackendState.asBackendState(): BackendState {
|
fun Backend.BackendState.asBackendState(): BackendState {
|
||||||
return BackendState.valueOf(this.name)
|
return BackendState.valueOf(this.name)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,12 +8,6 @@ import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
|||||||
import com.zaneschepke.wireguardautotunnel.ui.Route
|
import com.zaneschepke.wireguardautotunnel.ui.Route
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.isCurrentRoute
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.isCurrentRoute
|
||||||
|
|
||||||
fun NavController.navigateAndForget(route: Route) {
|
|
||||||
navigate(route) {
|
|
||||||
popUpTo(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun NavController.goFromRoot(route: Route) {
|
fun NavController.goFromRoot(route: Route) {
|
||||||
if (currentBackStackEntry?.isCurrentRoute(route::class) == true) return
|
if (currentBackStackEntry?.isCurrentRoute(route::class) == true) return
|
||||||
this.navigate(route) {
|
this.navigate(route) {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.viewmodel
|
package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.wireguard.android.backend.WgQuickBackend
|
import com.wireguard.android.backend.WgQuickBackend
|
||||||
import com.wireguard.android.util.RootShell
|
import com.wireguard.android.util.RootShell
|
||||||
@@ -16,31 +15,22 @@ import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
|||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
|
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
|
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.state.InterfaceProxy
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.state.PeerProxy
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.state.SplitTunnelApp
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
|
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
|
||||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.getAllInternetCapablePackages
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.withData
|
import com.zaneschepke.wireguardautotunnel.util.extensions.withData
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.onCompletion
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.takeWhile
|
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.plus
|
import kotlinx.coroutines.plus
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.amnezia.awg.config.Config
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -58,28 +48,24 @@ constructor(
|
|||||||
private val logReader: LogReader,
|
private val logReader: LogReader,
|
||||||
) : BaseViewModel(appDataRepository) {
|
) : BaseViewModel(appDataRepository) {
|
||||||
|
|
||||||
private val _popBackStack = MutableSharedFlow<Boolean>()
|
|
||||||
val popBackStack = _popBackStack.asSharedFlow()
|
|
||||||
|
|
||||||
private val _isAppReady = MutableStateFlow(false)
|
private val _isAppReady = MutableStateFlow(false)
|
||||||
val isAppReady = _isAppReady.asStateFlow()
|
val isAppReady = _isAppReady.asStateFlow()
|
||||||
|
|
||||||
private val _configurationChange = MutableStateFlow(false)
|
private val _configurationChange = MutableStateFlow(false)
|
||||||
val configurationChange = _configurationChange.asStateFlow()
|
val configurationChange = _configurationChange.asStateFlow()
|
||||||
|
|
||||||
private val _splitTunnelApps = MutableStateFlow<List<SplitTunnelApp>>(emptyList())
|
|
||||||
val splitTunnelApps = _splitTunnelApps.asStateFlow()
|
|
||||||
|
|
||||||
val uiState =
|
val uiState =
|
||||||
combine(
|
combine(
|
||||||
appDataRepository.settings.flow,
|
appDataRepository.settings.flow,
|
||||||
appDataRepository.tunnels.flow,
|
appDataRepository.tunnels.flow,
|
||||||
appDataRepository.appState.flow,
|
appDataRepository.appState.flow,
|
||||||
|
tunnelManager.activeTunnels,
|
||||||
serviceManager.autoTunnelActive,
|
serviceManager.autoTunnelActive,
|
||||||
) { settings, tunnels, generalState, autoTunnel ->
|
) { settings, tunnels, generalState, activeTunnels, autoTunnel ->
|
||||||
AppUiState(
|
AppUiState(
|
||||||
settings,
|
settings,
|
||||||
tunnels,
|
tunnels,
|
||||||
|
activeTunnels,
|
||||||
generalState,
|
generalState,
|
||||||
autoTunnel,
|
autoTunnel,
|
||||||
)
|
)
|
||||||
@@ -103,14 +89,13 @@ constructor(
|
|||||||
|
|
||||||
private suspend fun appReadyCheck() {
|
private suspend fun appReadyCheck() {
|
||||||
val tunnelCount = appDataRepository.tunnels.count()
|
val tunnelCount = appDataRepository.tunnels.count()
|
||||||
uiState.takeWhile { it.tunnels.size != tunnelCount }.onCompletion {
|
uiState.first { it.tunnels.count() == tunnelCount }
|
||||||
_isAppReady.emit(true)
|
_isAppReady.emit(true)
|
||||||
}.collect()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun initTunnels() {
|
private suspend fun initTunnels() {
|
||||||
tunnels.withData {
|
tunnels.withData { tunnels ->
|
||||||
it.filter { it.isActive }.forEach {
|
tunnels.filter { it.isActive }.forEach {
|
||||||
tunnelManager.startTunnel(it)
|
tunnelManager.startTunnel(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -146,17 +131,14 @@ constructor(
|
|||||||
with(uiState.value.generalState) {
|
with(uiState.value.generalState) {
|
||||||
val toggledOn = !isLocalLogsEnabled
|
val toggledOn = !isLocalLogsEnabled
|
||||||
appDataRepository.appState.setLocalLogsEnabled(toggledOn)
|
appDataRepository.appState.setLocalLogsEnabled(toggledOn)
|
||||||
if (!toggledOn) onLoggerStop()
|
if (!toggledOn) {
|
||||||
_configurationChange.update {
|
logReader.stop()
|
||||||
true
|
} else {
|
||||||
|
logReader.start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun onLoggerStop() {
|
|
||||||
logReader.deleteAndClearLogs()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onToggleAlwaysOnVPN() = viewModelScope.launch {
|
fun onToggleAlwaysOnVPN() = viewModelScope.launch {
|
||||||
with(uiState.value.appSettings) {
|
with(uiState.value.appSettings) {
|
||||||
appDataRepository.settings.save(
|
appDataRepository.settings.save(
|
||||||
@@ -242,7 +224,7 @@ constructor(
|
|||||||
if (!isKernelEnabled) {
|
if (!isKernelEnabled) {
|
||||||
requestRoot().onSuccess {
|
requestRoot().onSuccess {
|
||||||
if (!isKernelSupported()) {
|
if (!isKernelSupported()) {
|
||||||
return@onSuccess SnackbarController.Companion.showMessage(
|
return@onSuccess SnackbarController.showMessage(
|
||||||
StringValue.StringResource(R.string.kernel_not_supported),
|
StringValue.StringResource(R.string.kernel_not_supported),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -266,140 +248,14 @@ constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getEmitSplitTunnelApps(context: Context) {
|
private suspend fun requestRoot(): Result<Unit> {
|
||||||
withContext(ioDispatcher) {
|
|
||||||
val apps = context.getAllInternetCapablePackages().filter { it.applicationInfo != null }
|
|
||||||
.map { pack ->
|
|
||||||
SplitTunnelApp(
|
|
||||||
context.packageManager.getApplicationIcon(pack.applicationInfo!!),
|
|
||||||
context.packageManager.getApplicationLabel(pack.applicationInfo!!)
|
|
||||||
.toString(),
|
|
||||||
pack.packageName,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
_splitTunnelApps.emit(apps)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun requestRoot(): Result<Unit> {
|
|
||||||
return withContext(ioDispatcher) {
|
return withContext(ioDispatcher) {
|
||||||
runCatching {
|
runCatching {
|
||||||
rootShell.get().start()
|
rootShell.get().start()
|
||||||
SnackbarController.Companion.showMessage(StringValue.StringResource(R.string.root_accepted))
|
SnackbarController.showMessage(StringValue.StringResource(R.string.root_accepted))
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
SnackbarController.Companion.showMessage(StringValue.StringResource(R.string.error_root_denied))
|
SnackbarController.showMessage(StringValue.StringResource(R.string.error_root_denied))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateExistingTunnelConfig(
|
|
||||||
tunnelConfig: TunnelConf,
|
|
||||||
tunnelName: String? = null,
|
|
||||||
peers: List<PeerProxy>? = null,
|
|
||||||
`interface`: InterfaceProxy? = null,
|
|
||||||
) = viewModelScope.launch {
|
|
||||||
runCatching {
|
|
||||||
val amConfig = tunnelConfig.toAmConfig()
|
|
||||||
val wgConfig = tunnelConfig.toWgConfig()
|
|
||||||
updateTunnelConfig(tunnelConfig, tunnelName, amConfig, wgConfig, peers, `interface`)
|
|
||||||
_popBackStack.emit(true)
|
|
||||||
SnackbarController.Companion.showMessage(StringValue.StringResource(R.string.config_changes_saved))
|
|
||||||
}.onFailure {
|
|
||||||
onConfigSaveError(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun saveNewTunnel(tunnelName: String, peers: List<PeerProxy>, `interface`: InterfaceProxy) = viewModelScope.launch {
|
|
||||||
runCatching {
|
|
||||||
val config = buildConfigs(peers, `interface`)
|
|
||||||
appDataRepository.tunnels.save(
|
|
||||||
TunnelConf(
|
|
||||||
tunName = tunnelName,
|
|
||||||
wgQuick = config.first.toWgQuickString(true),
|
|
||||||
amQuick = config.second.toAwgQuickString(true),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
_popBackStack.emit(true)
|
|
||||||
SnackbarController.Companion.showMessage(StringValue.StringResource(R.string.config_changes_saved))
|
|
||||||
}.onFailure {
|
|
||||||
onConfigSaveError(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onConfigSaveError(throwable: Throwable) {
|
|
||||||
Timber.Forest.e(throwable)
|
|
||||||
SnackbarController.Companion.showMessage(
|
|
||||||
throwable.message?.let { message ->
|
|
||||||
(StringValue.DynamicString(message))
|
|
||||||
} ?: StringValue.StringResource(R.string.unknown_error),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun updateTunnelConfig(
|
|
||||||
tunnelConf: TunnelConf,
|
|
||||||
tunnelName: String? = null,
|
|
||||||
amConfig: Config,
|
|
||||||
wgConfig: com.wireguard.config.Config,
|
|
||||||
peers: List<PeerProxy>? = null,
|
|
||||||
`interface`: InterfaceProxy? = null,
|
|
||||||
) {
|
|
||||||
val configs = rebuildConfigs(amConfig, wgConfig, peers, `interface`)
|
|
||||||
appDataRepository.tunnels.save(
|
|
||||||
tunnelConf.copy(
|
|
||||||
tunName = tunnelName ?: tunnelConf.tunName,
|
|
||||||
amQuick = configs.second.toAwgQuickString(true),
|
|
||||||
wgQuick = configs.first.toWgQuickString(true),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun cleanUpUninstalledApps(tunnelConfig: TunnelConf, packages: List<String>) = viewModelScope.launch(ioDispatcher) {
|
|
||||||
runCatching {
|
|
||||||
val amConfig = tunnelConfig.toAmConfig()
|
|
||||||
val wgConfig = tunnelConfig.toWgConfig()
|
|
||||||
val proxy = InterfaceProxy.Companion.from(amConfig.`interface`)
|
|
||||||
if (proxy.includedApplications.isEmpty() && proxy.excludedApplications.isEmpty()) return@launch
|
|
||||||
if (proxy.includedApplications.retainAll(packages.toSet()) || proxy.excludedApplications.retainAll(packages.toSet())) {
|
|
||||||
updateTunnelConfig(tunnelConfig, amConfig = amConfig, wgConfig = wgConfig, `interface` = proxy)
|
|
||||||
Timber.Forest.i("Removed split tunnel package for app that no longer exists on the device")
|
|
||||||
}
|
|
||||||
}.onFailure {
|
|
||||||
Timber.Forest.e(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun rebuildConfigs(
|
|
||||||
amConfig: Config,
|
|
||||||
wgConfig: com.wireguard.config.Config,
|
|
||||||
peers: List<PeerProxy>? = null,
|
|
||||||
`interface`: InterfaceProxy? = null,
|
|
||||||
): Pair<com.wireguard.config.Config, Config> {
|
|
||||||
return withContext(ioDispatcher) {
|
|
||||||
Pair(
|
|
||||||
com.wireguard.config.Config.Builder().apply {
|
|
||||||
addPeers(peers?.map { it.toWgPeer() } ?: wgConfig.peers)
|
|
||||||
setInterface(`interface`?.toWgInterface() ?: wgConfig.`interface`)
|
|
||||||
}.build(),
|
|
||||||
Config.Builder().apply {
|
|
||||||
addPeers(peers?.map { it.toAmPeer() } ?: amConfig.peers)
|
|
||||||
setInterface(`interface`?.toAmInterface() ?: amConfig.`interface`)
|
|
||||||
}.build(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun buildConfigs(peers: List<PeerProxy>, `interface`: InterfaceProxy): Pair<com.wireguard.config.Config, Config> {
|
|
||||||
return withContext(ioDispatcher) {
|
|
||||||
Pair(
|
|
||||||
com.wireguard.config.Config.Builder().apply {
|
|
||||||
addPeers(peers.map { it.toWgPeer() })
|
|
||||||
setInterface(`interface`.toWgInterface())
|
|
||||||
}.build(),
|
|
||||||
Config.Builder().apply {
|
|
||||||
addPeers(peers.map { it.toAmPeer() })
|
|
||||||
setInterface(`interface`.toAmInterface())
|
|
||||||
}.build(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,13 +24,12 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.toWgQuickString
|
|||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.withData
|
import com.zaneschepke.wireguardautotunnel.util.extensions.withData
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.amnezia.awg.config.Config
|
import org.amnezia.awg.config.Config
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
import java.net.URL
|
||||||
import java.util.zip.ZipInputStream
|
import java.util.zip.ZipInputStream
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -44,15 +43,6 @@ constructor(
|
|||||||
appDataRepository: AppDataRepository,
|
appDataRepository: AppDataRepository,
|
||||||
) : BaseViewModel(appDataRepository) {
|
) : BaseViewModel(appDataRepository) {
|
||||||
|
|
||||||
private val _activeTunnels = MutableStateFlow<List<TunnelConf>>(emptyList())
|
|
||||||
val activeTunnels = _activeTunnels.asStateFlow()
|
|
||||||
|
|
||||||
init {
|
|
||||||
viewModelScope.launch {
|
|
||||||
tunnelManager.activeTunnels().collect(_activeTunnels::emit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onDelete(tunnel: TunnelConf) = viewModelScope.launch {
|
fun onDelete(tunnel: TunnelConf) = viewModelScope.launch {
|
||||||
appSettings.withData { settings ->
|
appSettings.withData { settings ->
|
||||||
tunnels.withData {
|
tunnels.withData {
|
||||||
@@ -252,4 +242,28 @@ constructor(
|
|||||||
SnackbarController.Companion.showMessage(StringValue.StringResource(R.string.error_file_format))
|
SnackbarController.Companion.showMessage(StringValue.StringResource(R.string.error_file_format))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onUrlImport(urlString: String) = viewModelScope.launch(ioDispatcher) {
|
||||||
|
runCatching {
|
||||||
|
val url = URL(urlString)
|
||||||
|
val fileName = urlString.substringAfterLast("/")
|
||||||
|
if (!fileName.endsWith(Constants.CONF_FILE_EXTENSION)) {
|
||||||
|
throw InvalidFileExtensionException
|
||||||
|
}
|
||||||
|
|
||||||
|
url.openStream().use { stream ->
|
||||||
|
saveTunnelConfigFromStream(stream, fileName)
|
||||||
|
}
|
||||||
|
}.onFailure {
|
||||||
|
Timber.Forest.e(it)
|
||||||
|
when (it) {
|
||||||
|
is InvalidFileExtensionException -> {
|
||||||
|
SnackbarController.Companion.showMessage(StringValue.StringResource(R.string.error_file_extension))
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
SnackbarController.Companion.showMessage(StringValue.StringResource(R.string.error_download_failed))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -162,7 +162,7 @@
|
|||||||
<string name="monitoring_state_changes">Überwache Statusänderungen</string>
|
<string name="monitoring_state_changes">Überwache Statusänderungen</string>
|
||||||
<string name="stop_on_no_internet">Stoppen wenn keine Internetverbindung besteht</string>
|
<string name="stop_on_no_internet">Stoppen wenn keine Internetverbindung besteht</string>
|
||||||
<string name="ethernet_tunnel">Ethernet Tunnel</string>
|
<string name="ethernet_tunnel">Ethernet Tunnel</string>
|
||||||
<string name="set_ethernet_tunnel">Als Ethernet Tunnel setzten</string>
|
<string name="set_ethernet_tunnel">Als Ethernet Tunnel setzen</string>
|
||||||
<string name="native_kill_switch">Nativer Notschalter</string>
|
<string name="native_kill_switch">Nativer Notschalter</string>
|
||||||
<string name="vpn_kill_switch">VPN Notschalter</string>
|
<string name="vpn_kill_switch">VPN Notschalter</string>
|
||||||
<string name="kill_switch_options">Notschalteroptionen</string>
|
<string name="kill_switch_options">Notschalteroptionen</string>
|
||||||
@@ -192,4 +192,17 @@
|
|||||||
<string name="hide_amnezia_properties">Amnezia Eigenschaften verbergen</string>
|
<string name="hide_amnezia_properties">Amnezia Eigenschaften verbergen</string>
|
||||||
<string name="include_lan">LAN einschließen</string>
|
<string name="include_lan">LAN einschließen</string>
|
||||||
<string name="exclude_lan">LAN ausschließen</string>
|
<string name="exclude_lan">LAN ausschließen</string>
|
||||||
|
<string name="tunnel_control">Tunnelsteuerung</string>
|
||||||
|
<string name="kill_switch_off">Notschalter stoppen bei vertrauenswürdigen</string>
|
||||||
|
<string name="error_tunnel_start">Start des Tunnels fehlgeschlagen</string>
|
||||||
|
<string name="auto_tunnel">Auto-Tunnel</string>
|
||||||
|
<string name="export_amnezia">Als Amnezia exportieren</string>
|
||||||
|
<string name="export_wireguard">Als WireGuard exportieren</string>
|
||||||
|
<string name="server_ipv4">IPv4 Hostnamensauflösung</string>
|
||||||
|
<string name="prefer_ipv4">IPv4 Verbindung bevorzugen</string>
|
||||||
|
<string name="dns_error">Fehler bei Endpunkt DNS Auflösung.</string>
|
||||||
|
<string name="start_failed_config">Starten des Tunnels wegen Konfigfehler fehlgeschlagen.</string>
|
||||||
|
<string name="unauthorized">Starten des Tunnels fehlgeschlagen, unberechtigt.</string>
|
||||||
|
<string name="tunne_start_failed_title">Tunnelfehler</string>
|
||||||
|
<string name="multiple">Mehrere</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -192,4 +192,17 @@
|
|||||||
<string name="wg_compat_mode">Modo compatibilidad de WG</string>
|
<string name="wg_compat_mode">Modo compatibilidad de WG</string>
|
||||||
<string name="quick_actions">Acciones rápidas</string>
|
<string name="quick_actions">Acciones rápidas</string>
|
||||||
<string name="remove_amnezia_compatibility">Eliminar compatibilidad con Amnezia</string>
|
<string name="remove_amnezia_compatibility">Eliminar compatibilidad con Amnezia</string>
|
||||||
|
<string name="error_tunnel_start">Fallo al iniciar el túnel</string>
|
||||||
|
<string name="tunnel_control">Control del túnel</string>
|
||||||
|
<string name="auto_tunnel">Túnel automático</string>
|
||||||
|
<string name="kill_switch_off">Detener interruptor de apagado en confianza</string>
|
||||||
|
<string name="server_ipv4">Resolución de host IPv4</string>
|
||||||
|
<string name="prefer_ipv4">Preferir conexión IPv4</string>
|
||||||
|
<string name="dns_error">No se ha podido resolver el DNS del punto final.</string>
|
||||||
|
<string name="start_failed_config">Fallo al iniciar túnel con error de configuración.</string>
|
||||||
|
<string name="unauthorized">Fallo al iniciar túnel, no autorizado.</string>
|
||||||
|
<string name="tunne_start_failed_title">Fallo del túnel</string>
|
||||||
|
<string name="multiple">Múltiple</string>
|
||||||
|
<string name="export_amnezia">Exportar cómo Amnezia</string>
|
||||||
|
<string name="export_wireguard">Exportar cómo WireGuard</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -13,8 +13,8 @@
|
|||||||
<string name="error_no_file_explorer">Nessun esploratore di file installato</string>
|
<string name="error_no_file_explorer">Nessun esploratore di file installato</string>
|
||||||
<string name="error_invalid_code">QR code non valido</string>
|
<string name="error_invalid_code">QR code non valido</string>
|
||||||
<string name="app_name">Tunnel WG</string>
|
<string name="app_name">Tunnel WG</string>
|
||||||
<string name="vpn_channel_name">Canale di notifica VPN</string>
|
<string name="vpn_channel_name">Canale di notifica VPN</string>
|
||||||
<string name="turn_off_tunnel">L\'operaz. richiede la disatt. del tunnel</string>
|
<string name="turn_off_tunnel">L\'operaz. richiede la disatt. del tunnel</string>
|
||||||
<string name="public_key">Chiave pubblica</string>
|
<string name="public_key">Chiave pubblica</string>
|
||||||
<string name="addresses">Indirizzi</string>
|
<string name="addresses">Indirizzi</string>
|
||||||
<string name="dns_servers">Server DNS</string>
|
<string name="dns_servers">Server DNS</string>
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
<string name="always_on_vpn_support">Permetti VPN sempre attiva</string>
|
<string name="always_on_vpn_support">Permetti VPN sempre attiva</string>
|
||||||
<string name="location_services_not_detected">Servizi di localizzazione non rilevati</string>
|
<string name="location_services_not_detected">Servizi di localizzazione non rilevati</string>
|
||||||
<string name="hint_search_packages">Cerca pacchetti</string>
|
<string name="hint_search_packages">Cerca pacchetti</string>
|
||||||
<string name="auto_tunneling">Tunnel automatico</string>
|
<string name="auto_tunneling">Tunnel automatico</string>
|
||||||
<string name="vpn_on">VPN on</string>
|
<string name="vpn_on">VPN on</string>
|
||||||
<string name="vpn_off">VPN off</string>
|
<string name="vpn_off">VPN off</string>
|
||||||
<string name="create_import">Crea da zero</string>
|
<string name="create_import">Crea da zero</string>
|
||||||
@@ -56,4 +56,73 @@
|
|||||||
<string name="use_kernel">Usa modulo kernel</string>
|
<string name="use_kernel">Usa modulo kernel</string>
|
||||||
<string name="error_ssid_exists">L\'SSID esiste già</string>
|
<string name="error_ssid_exists">L\'SSID esiste già</string>
|
||||||
<string name="error_root_denied">Shell di root negata</string>
|
<string name="error_root_denied">Shell di root negata</string>
|
||||||
|
<string name="prominent_background_location_title">Permesso localizzazione in background</string>
|
||||||
|
<string name="location_services_missing_message">Questa app non rileva nessun servizio di localizzazione attiva sul tuo dispositivo. Dipendentemente dal dispositivo, questo potrebbe causare il fallimento a leggere il nome wifi da parte della funzione di wifi non fidate. Vuoi continuare comunque?</string>
|
||||||
|
<string name="read_logs">Leggi i log</string>
|
||||||
|
<string name="auto">(auto)</string>
|
||||||
|
<string name="underload_packet_magic_header">Magic header pacchetto sottocarico</string>
|
||||||
|
<string name="unsure_how">se non sei sicuro di come procedere</string>
|
||||||
|
<string name="vpn_settings">Impostazioni sistema VPN</string>
|
||||||
|
<string name="always_on_message">Permessi connessione VPN negati. Verifica la</string>
|
||||||
|
<string name="chat_description">Unisciti alla community</string>
|
||||||
|
<string name="junk_packet_maximum_size">Dimensione massima pacchetti indesiderati</string>
|
||||||
|
<string name="delete_tunnel">Cancella tunnel</string>
|
||||||
|
<string name="init_packet_junk_size">Inizializza la dimensione dei pacchetti indesiderati</string>
|
||||||
|
<string name="always_on_message2">per essere sicuro che la VPN Always-on sia spenta per tutte le altre app e riprova</string>
|
||||||
|
<string name="background_location_message">Permetti i permessi per la localizzazione durante tutto il tempo e/o la localizzazione precisa è richiesta per questa funzione. Vedi la</string>
|
||||||
|
<string name="config_changes_saved">Salvate modifiche configurazione.</string>
|
||||||
|
<string name="delete_tunnel_message">Sei certo di voler cancellare questo tunnel?</string>
|
||||||
|
<string name="thank_you">Grazie per usare WG Tunnel!</string>
|
||||||
|
<string name="trusted_ssid_value_description">Invia SSID</string>
|
||||||
|
<string name="exclude">Escludi</string>
|
||||||
|
<string name="include">Includi</string>
|
||||||
|
<string name="yes">Si</string>
|
||||||
|
<string name="all">tutte</string>
|
||||||
|
<string name="no_email_detected">Nessuna app email rilevata</string>
|
||||||
|
<string name="no_browser_detected">Nessun browser rilevato</string>
|
||||||
|
<string name="incorrect_pin">Il PIN non è corretto</string>
|
||||||
|
<string name="pin_created">PIN correttamente creato</string>
|
||||||
|
<string name="enable_app_lock">Abilita blocco app</string>
|
||||||
|
<string name="restart_on_ping">Riavvia su fallimento ping (beta)</string>
|
||||||
|
<string name="mobile_data_tunnel">Imposta come tunnel dati mobili</string>
|
||||||
|
<string name="set_primary_tunnel">Imposta come tunnel principale</string>
|
||||||
|
<string name="use_tunnel_on_wifi_name">Usa tunnel su nome wifi</string>
|
||||||
|
<string name="edit_tunnel">Modifica tunnel</string>
|
||||||
|
<string name="version">Versione</string>
|
||||||
|
<string name="settings">Impostazioni</string>
|
||||||
|
<string name="support">Supporto</string>
|
||||||
|
<string name="kernel">Kernel</string>
|
||||||
|
<string name="junk_packet_count">Numero pacchetti indesiderati</string>
|
||||||
|
<string name="junk_packet_minimum_size">Dimensione minima pacchetti indesiderati</string>
|
||||||
|
<string name="response_packet_junk_size">Dimensione pacchetto indesiderato risposta</string>
|
||||||
|
<string name="init_packet_magic_header">Magic header pacchetto inizializzazione</string>
|
||||||
|
<string name="response_packet_magic_header">Magic header pacchetto risposta</string>
|
||||||
|
<string name="getting_started_guide">guida di avvio rapido</string>
|
||||||
|
<string name="see_the">Vedi la</string>
|
||||||
|
<string name="error_file_format">Formato configurazione tunnel non valido</string>
|
||||||
|
<string name="restart_at_boot">Riavvia al boot</string>
|
||||||
|
<string name="vpn_denied_dialog_title">Permesso Negato</string>
|
||||||
|
<string name="tunnel_required">Questa funzione richiede almeno un tunnel</string>
|
||||||
|
<string name="app_settings">impostazioni app</string>
|
||||||
|
<string name="background_location_message2">per assicurarti che questi permessi siano abilitati</string>
|
||||||
|
<string name="root_accepted">Accesso alla shell root accettata</string>
|
||||||
|
<string name="set_custom_ping_ip">Imposta ip ping personalizzato</string>
|
||||||
|
<string name="default_ping_ip">(opzionale, default ai peers)</string>
|
||||||
|
<string name="set_custom_ping_internal">Intervallo ping (sec)</string>
|
||||||
|
<string name="optional_default">"opzionale, default: "</string>
|
||||||
|
<string name="set_custom_ping_cooldown">Tempo attesa prima della ripartenza ping (sec)</string>
|
||||||
|
<string name="show_amnezia_properties">Mostra proprietà Amnezia</string>
|
||||||
|
<string name="add_tunnels_text">Aggiungi da file o zip</string>
|
||||||
|
<string name="open_file">Apri file</string>
|
||||||
|
<string name="add_from_qr">Aggiungi da codice QR</string>
|
||||||
|
<string name="qr_scan">Scansione QR</string>
|
||||||
|
<string name="tunnel_name">Nome Tunnel</string>
|
||||||
|
<string name="tunneling_apps">App nel tunnel</string>
|
||||||
|
<string name="open_issue">Apri un problema</string>
|
||||||
|
<string name="enter_pin">Inserisci il tuo PIN</string>
|
||||||
|
<string name="create_pin">Crea PIN</string>
|
||||||
|
<string name="transport_packet_magic_header">Magic header pacchetto trasporto</string>
|
||||||
|
<string name="kill_switch_off">Ferma interruttore di spegnimento su fidate</string>
|
||||||
|
<string name="prominent_background_location_message">Questa caratteristica richiede il permesso di localizzazione in background per abilitare il monitoraggio dell\'SSID Wi-fi anche quando l\'applicazione è chiusa. Per più dettagli, verifica la Privacy Policy linkata nella schermata di supporto.</string>
|
||||||
|
<string name="auto_tunnel_title">Servizio auto-tunnel</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<string name="tunnels">Tunele</string>
|
<string name="tunnels">Tunele</string>
|
||||||
<string name="app_name">WG Tunnel</string>
|
<string name="app_name">WG Tunnel</string>
|
||||||
<string name="unsure_how">jeśli nie masz pewności, jak postępować</string>
|
<string name="unsure_how">jeśli nie masz pewności, jak postępować</string>
|
||||||
<string name="getting_started_guide">przewodnik wprowadzający</string>
|
<string name="getting_started_guide">przewodnik wprowadzający,</string>
|
||||||
<string name="peer">Peer</string>
|
<string name="peer">Peer</string>
|
||||||
<string name="background_location_message2">w celu upewnienia się, że uprawnienia te są włączone</string>
|
<string name="background_location_message2">w celu upewnienia się, że uprawnienia te są włączone</string>
|
||||||
<string name="rotate_keys">Rotuj klucze</string>
|
<string name="rotate_keys">Rotuj klucze</string>
|
||||||
@@ -196,4 +196,14 @@
|
|||||||
<string name="error_tunnel_start">Nie udało się uruchomić tunelu</string>
|
<string name="error_tunnel_start">Nie udało się uruchomić tunelu</string>
|
||||||
<string name="tunnel_control">Kontrola tunelu</string>
|
<string name="tunnel_control">Kontrola tunelu</string>
|
||||||
<string name="auto_tunnel">Autotunel</string>
|
<string name="auto_tunnel">Autotunel</string>
|
||||||
|
<string name="kill_switch_off">Zatrzymaj wyłącznik awaryjny w zaufanej</string>
|
||||||
|
<string name="server_ipv4">Rozpoznawanie nazw hostów IPv6</string>
|
||||||
|
<string name="prefer_ipv4">Preferuj połączenie IPv4</string>
|
||||||
|
<string name="unauthorized">Nie udało się uruchomić tunelu, brak autoryzacji.</string>
|
||||||
|
<string name="multiple">Wiele</string>
|
||||||
|
<string name="start_failed_config">Nie udało się uruchomić tunelu z powodu błędu konfiguracji.</string>
|
||||||
|
<string name="tunne_start_failed_title">Awaria tunelu</string>
|
||||||
|
<string name="dns_error">Nie udało się rozpoznać punktu końcowego DNS.</string>
|
||||||
|
<string name="export_amnezia">Eksportuj jako Amnezia</string>
|
||||||
|
<string name="export_wireguard">Eksportuj jako WireGuard</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,2 +1,177 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources></resources>
|
<resources>
|
||||||
|
<string name="tunnel_on_ethernet">Túnel na ethernet</string>
|
||||||
|
<string name="public_key">Chave pública</string>
|
||||||
|
<string name="addresses">Endereços</string>
|
||||||
|
<string name="dns_servers">Servidores DNS</string>
|
||||||
|
<string name="endpoint">Endpoint</string>
|
||||||
|
<string name="name">Nome</string>
|
||||||
|
<string name="create_import">Criar do zero</string>
|
||||||
|
<string name="rotate_keys">Revezar chaves</string>
|
||||||
|
<string name="private_key">Chave privada</string>
|
||||||
|
<string name="base64_key">Chave base64</string>
|
||||||
|
<string name="optional_no_recommend">(opcional, não recomendado)</string>
|
||||||
|
<string name="preshared_key">Chave pré-partilhada</string>
|
||||||
|
<string name="seconds">segundos</string>
|
||||||
|
<string name="export_configs">Exportar configurações</string>
|
||||||
|
<string name="error_no_file_explorer">Nenhum explorador de ficheiros instalado</string>
|
||||||
|
<string name="error_invalid_code">Código QR inválido</string>
|
||||||
|
<string name="auto_tunnel_title">Serviço de Auto-túnel</string>
|
||||||
|
<string name="all">todos</string>
|
||||||
|
<string name="enter_pin">Digite o seu pin</string>
|
||||||
|
<string name="use_tunnel_on_wifi_name">Usar túnel em wifi com nome</string>
|
||||||
|
<string name="version">Versão</string>
|
||||||
|
<string name="junk_packet_count">Quantidade de pacotes-lixo</string>
|
||||||
|
<string name="junk_packet_minimum_size">Tamanho mínimo de pacote-lixo</string>
|
||||||
|
<string name="junk_packet_maximum_size">Tamanho máximo de pacote-lixo</string>
|
||||||
|
<string name="init_packet_junk_size">Tamanho de pacote-lixo inicial</string>
|
||||||
|
<string name="response_packet_junk_size">Tamanho de resposta de pacote-lixo</string>
|
||||||
|
<string name="app_name">WG Tunnel</string>
|
||||||
|
<string name="no_tunnels">Nenhum túnel foi adicionado!</string>
|
||||||
|
<string name="error_file_extension">O ficheiro não é .conf ou .zip</string>
|
||||||
|
<string name="prominent_background_location_message">Este recurso precisa de permissões de localização em segundo plano para ativar o monitoramento do SSID da rede Wi-Fi mesmo quando a aplicação está fechado. Para mais pormenores, por favor veja a Política de Privacidade no ecrã de Suporte.</string>
|
||||||
|
<string name="turn_off_tunnel">Esta ação só é possível com o túnel inativo</string>
|
||||||
|
<string name="enabled_app_shortcuts">Ativar atalhos de aplicações</string>
|
||||||
|
<string name="tunnels">Túneis</string>
|
||||||
|
<string name="privacy_policy">Ver a Política de Privacidade</string>
|
||||||
|
<string name="okay">OK</string>
|
||||||
|
<string name="tunnel_mobile_data">Túnel em dados móveis</string>
|
||||||
|
<string name="prominent_background_location_title">Revelar a localização em segundo plano</string>
|
||||||
|
<string name="thank_you">Obrigado por usar o WG Tunnel!</string>
|
||||||
|
<string name="trusted_ssid_value_description">Envie o SSID</string>
|
||||||
|
<string name="open_file">Abrir Ficheiro</string>
|
||||||
|
<string name="add_from_qr">Adicionar a partir de código QR</string>
|
||||||
|
<string name="add_tunnels_text">Adicionar a partir de ficheiro ou zip</string>
|
||||||
|
<string name="qr_scan">Escanear o código QR</string>
|
||||||
|
<string name="tunnel_name">Nome do Túnel</string>
|
||||||
|
<string name="config_changes_saved">Mudanças nas configurações gravadas.</string>
|
||||||
|
<string name="exclude">Excluir</string>
|
||||||
|
<string name="include">Incluir</string>
|
||||||
|
<string name="mtu">MTU</string>
|
||||||
|
<string name="always_on_vpn_support">Permitir VPN sempre ligada</string>
|
||||||
|
<string name="allowed_ips">IPs Permitidos</string>
|
||||||
|
<string name="peer">Par</string>
|
||||||
|
<string name="location_services_not_detected">Serviço de localização não foi detetado</string>
|
||||||
|
<string name="hint_search_packages">Procurar pacotes</string>
|
||||||
|
<string name="auto_tunneling">Auto-túnel</string>
|
||||||
|
<string name="vpn_on">VPN ligada</string>
|
||||||
|
<string name="vpn_off">VPN desligada</string>
|
||||||
|
<string name="listen_port">Porta de escuta</string>
|
||||||
|
<string name="turn_on_tunnel">Esta ação precisa um túnel ativo</string>
|
||||||
|
<string name="add_peer">Adicionar par</string>
|
||||||
|
<string name="interface_">Interface</string>
|
||||||
|
<string name="copy_public_key">Copiar chave pública</string>
|
||||||
|
<string name="comma_separated_list">Lista separada por vírgulas</string>
|
||||||
|
<string name="optional">(opcional)</string>
|
||||||
|
<string name="random">(aleatório)</string>
|
||||||
|
<string name="persistent_keepalive">Manter a conexão persistente (keepalive)</string>
|
||||||
|
<string name="cancel">Cancelar</string>
|
||||||
|
<string name="error_authentication_failed">Autenticação falhou</string>
|
||||||
|
<string name="error_authorization_failed">Autorização falhou</string>
|
||||||
|
<string name="restart_on_ping">Reiniciar em falha de ping (beta)</string>
|
||||||
|
<string name="email_description">Me envie um email</string>
|
||||||
|
<string name="error_ssid_exists">SSID já existe</string>
|
||||||
|
<string name="delete_tunnel_message">Tem certeza que quer apagar este túnel?</string>
|
||||||
|
<string name="yes">Sim</string>
|
||||||
|
<string name="unknown_error">Ocorreu um erro desconhecido</string>
|
||||||
|
<string name="email_subject">Apoio para o WG Tunnel</string>
|
||||||
|
<string name="tunnel_on_wifi">Túnel em Wi-Fi não confiável</string>
|
||||||
|
<string name="delete_tunnel">Apagar túnel</string>
|
||||||
|
<string name="email_chooser">Enviar um email…</string>
|
||||||
|
<string name="use_kernel">Usar o módulo do kernel</string>
|
||||||
|
<string name="docs_description">Ler a documentação</string>
|
||||||
|
<string name="error_root_denied">Shell Root negado</string>
|
||||||
|
<string name="location_services_missing_message">A aplicação não detetou o serviço de localização ativado no seu dispositivo. Dependendo do dispositivo, isto pode causar que a função de Wi-Fi não confiável falhe em ler o nome do Wi-Fi. Quer continuar mesmo assim?</string>
|
||||||
|
<string name="open_issue">Abrir um problema</string>
|
||||||
|
<string name="tunneling_apps">Aplicações com túnel</string>
|
||||||
|
<string name="no_email_detected">Nenhuma aplicação de email detetado</string>
|
||||||
|
<string name="no_browser_detected">Nenhum navegador detetado</string>
|
||||||
|
<string name="incorrect_pin">O Pin está errado</string>
|
||||||
|
<string name="auto">(automático)</string>
|
||||||
|
<string name="read_logs">Ler os registos</string>
|
||||||
|
<string name="pin_created">Pin criado com sucesso</string>
|
||||||
|
<string name="create_pin">Criar um pin</string>
|
||||||
|
<string name="enable_app_lock">Ligar bloqueio de aplicação</string>
|
||||||
|
<string name="edit_tunnel">Editar túnel</string>
|
||||||
|
<string name="mobile_data_tunnel">Selecionar como túnel em dados móveis</string>
|
||||||
|
<string name="set_primary_tunnel">Selecionar como túnel principal</string>
|
||||||
|
<string name="support">Suporte</string>
|
||||||
|
<string name="kernel">Kernel</string>
|
||||||
|
<string name="settings">Configurações</string>
|
||||||
|
<string name="unsure_how">se não tiver certeza em como continuar</string>
|
||||||
|
<string name="see_the">Veja o</string>
|
||||||
|
<string name="getting_started_guide">guia de início rápido</string>
|
||||||
|
<string name="error_file_format">Formato de configuração inválido</string>
|
||||||
|
<string name="vpn_channel_name">Canal de notificações VPN</string>
|
||||||
|
<string name="set_custom_ping_ip">Definir ip ping personalizado</string>
|
||||||
|
<string name="vpn_denied_dialog_title">Permissão negada</string>
|
||||||
|
<string name="vpn_settings">Configurações do sistema VPN</string>
|
||||||
|
<string name="always_on_message">A permissão de conexão VPN foi negada. Por favor, verifique</string>
|
||||||
|
<string name="chat_description">Junte-se à comunidade</string>
|
||||||
|
<string name="tunnel_required">Característica requer pelo menos um túnel</string>
|
||||||
|
<string name="app_settings">configurações da app</string>
|
||||||
|
<string name="background_location_message2">para garantir que essas permissões estejam ativadas.</string>
|
||||||
|
<string name="root_accepted">Shell root aceito</string>
|
||||||
|
<string name="optional_default">"opcional, padrão: "</string>
|
||||||
|
<string name="show_amnezia_properties">Mostrar propriedades de Amnezia</string>
|
||||||
|
<string name="never">nunca</string>
|
||||||
|
<string name="default_ping_ip">(opcional, padrão para pares)</string>
|
||||||
|
<string name="set_custom_ping_internal">Intervalo de Ping (seg)</string>
|
||||||
|
<string name="handshake">handshake</string>
|
||||||
|
<string name="background_location_message">Permitir que toda a permissão de localização do tempo e/ou localização precisa é necessária para este recurso. Por favor, veja</string>
|
||||||
|
<string name="sec">seg</string>
|
||||||
|
<string name="notifications">Notificações</string>
|
||||||
|
<string name="exclude_lan">Excluir LAN</string>
|
||||||
|
<string name="hide_scripts">Ocultar scripts</string>
|
||||||
|
<string name="trusted_wifi_names">Nomes de Wi-Fi confiáveis</string>
|
||||||
|
<string name="hide_amnezia_properties">Ocultar propriedades Amnezia</string>
|
||||||
|
<string name="remove_amnezia_compatibility">Remover compatibilidade Amnezia</string>
|
||||||
|
<string name="include_lan">Incluir LAN</string>
|
||||||
|
<string name="language">Idioma</string>
|
||||||
|
<string name="add_wifi_name">Adicionar nome Wi-Fi</string>
|
||||||
|
<string name="display_theme">Tema</string>
|
||||||
|
<string name="on_demand_rules">Regras de tunelamento sob demanda</string>
|
||||||
|
<string name="dark">Escuro</string>
|
||||||
|
<string name="dynamic">Dinâmico</string>
|
||||||
|
<string name="skip">Pular</string>
|
||||||
|
<string name="mobile_tunnel">Túnel com dados móveis</string>
|
||||||
|
<string name="requires_app_relaunch">Para aplicar as mudanças é necessário reiniciar o aplicativo. Deseja prosseguir ?</string>
|
||||||
|
<string name="add_from_clipboard">Adicionar da área de transferência</string>
|
||||||
|
<string name="restart_at_boot">Ativar na inicialização</string>
|
||||||
|
<string name="appearance">Aparência</string>
|
||||||
|
<string name="automatic">Automático</string>
|
||||||
|
<string name="light">Claro</string>
|
||||||
|
<string name="wildcards_active">Wildcards ativos</string>
|
||||||
|
<string name="learn_more">Saber mais</string>
|
||||||
|
<string name="use_wildcards">Usar nomes coringas</string>
|
||||||
|
<string name="wifi_name_via_shell">Nome do Wi-Fi por shell</string>
|
||||||
|
<string name="use_root_shell_for_wifi">Obter o nome do Wi-Fi através do shell root</string>
|
||||||
|
<string name="start_auto">Iniciar túnel automático</string>
|
||||||
|
<string name="stop_auto">Pausar túnel automático</string>
|
||||||
|
<string name="monitoring_state_changes">Monitorar status de alterações</string>
|
||||||
|
<string name="tunnel_running">Túnel em execução</string>
|
||||||
|
<string name="donate">Contribua com esse projeto</string>
|
||||||
|
<string name="local_logging">Registro local</string>
|
||||||
|
<string name="enable_local_logging">Ativar registro local</string>
|
||||||
|
<string name="configuration_change">Configuração alterada</string>
|
||||||
|
<string name="stop_on_no_internet">Interromper quando não há internet</string>
|
||||||
|
<string name="stop_on_internet_loss">Interrompa o túnel quando a internet não estiver disponível</string>
|
||||||
|
<string name="ethernet_tunnel">Túnel ethernet</string>
|
||||||
|
<string name="set_ethernet_tunnel">Definir como túnel ethernet</string>
|
||||||
|
<string name="native_kill_switch">Interruptor de desligamento padrão</string>
|
||||||
|
<string name="vpn_kill_switch">Interruptor de desligamento VPN</string>
|
||||||
|
<string name="kill_switch_options">Opções do interruptor de desligamento</string>
|
||||||
|
<string name="allow_lan_traffic">Permitir tráfego LAN</string>
|
||||||
|
<string name="bypass_lan_for_kill_switch">Ignorar LAN no interruptor de desligamento</string>
|
||||||
|
<string name="splt_tunneling">Tunelamento dividido</string>
|
||||||
|
<string name="stop">pausar</string>
|
||||||
|
<string name="tunnel_specific_settings">Configurações específicas no túnel</string>
|
||||||
|
<string name="show_scripts">Mostrar scripts</string>
|
||||||
|
<string name="amnezia_kernel_message">Amnezia indisponível no kernel</string>
|
||||||
|
<string name="enable_amnezia">Ativar Amnezia</string>
|
||||||
|
<string name="wg_compat_mode">Modo de compatibilidade WG</string>
|
||||||
|
<string name="quick_actions">Ações rápidas</string>
|
||||||
|
<string name="kernel_not_supported">Kernel não suportado</string>
|
||||||
|
<string name="advanced_settings">Configurações avançadas</string>
|
||||||
|
<string name="enable_amnezia_compatibility">Ativar compatibilidade Amnezia</string>
|
||||||
|
</resources>
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string name="tunnel_on_ethernet">Túnel na ethernet</string>
|
|
||||||
<string name="public_key">Chave pública</string>
|
|
||||||
<string name="addresses">Endereços</string>
|
|
||||||
<string name="dns_servers">Servidores DNS</string>
|
|
||||||
<string name="endpoint">Endpoint</string>
|
|
||||||
<string name="name">Nome</string>
|
|
||||||
<string name="create_import">Criar do zero</string>
|
|
||||||
<string name="rotate_keys">Revezar chaves</string>
|
|
||||||
<string name="private_key">Chave privada</string>
|
|
||||||
<string name="base64_key">Chave base64</string>
|
|
||||||
<string name="optional_no_recommend">(opcional, não recomendado)</string>
|
|
||||||
<string name="preshared_key">Chave pré-partilhada</string>
|
|
||||||
<string name="seconds">segundos</string>
|
|
||||||
<string name="export_configs">Exportar configurações</string>
|
|
||||||
<string name="error_no_file_explorer">Nenhum explorador de ficheiros instalado</string>
|
|
||||||
<string name="error_invalid_code">Código QR inválido</string>
|
|
||||||
<string name="auto_tunnel_title">Serviço de Auto-túnel</string>
|
|
||||||
<string name="all">todos</string>
|
|
||||||
<string name="enter_pin">Digite o seu pin</string>
|
|
||||||
<string name="use_tunnel_on_wifi_name">Usar túnel em wifi com nome</string>
|
|
||||||
<string name="version">Versão</string>
|
|
||||||
<string name="junk_packet_count">Quantidade de pacotes-lixo</string>
|
|
||||||
<string name="junk_packet_minimum_size">Tamanho mínimo de pacote-lixo</string>
|
|
||||||
<string name="junk_packet_maximum_size">Tamanho máximo de pacote-lixo</string>
|
|
||||||
<string name="init_packet_junk_size">Tamanho de pacote-lixo inicial</string>
|
|
||||||
<string name="response_packet_junk_size">Tamanho de resposta de pacote-lixo</string>
|
|
||||||
<string name="app_name">WG Tunnel</string>
|
|
||||||
<string name="no_tunnels">Nenhum túnel foi adicionado!</string>
|
|
||||||
<string name="error_file_extension">O ficheiro não é .conf ou .zip</string>
|
|
||||||
<string name="prominent_background_location_message">Este recurso precisa de permissões de localização em segundo plano para ativar o monitoramento do SSID da rede Wi-Fi mesmo quando a aplicação está fechado. Para mais pormenores, por favor veja a Política de Privacidade no ecrã de Suporte.</string>
|
|
||||||
<string name="turn_off_tunnel">Esta ação só é possível com o túnel inativo</string>
|
|
||||||
<string name="enabled_app_shortcuts">Ativar atalhos de aplicações</string>
|
|
||||||
<string name="tunnels">Túneis</string>
|
|
||||||
<string name="privacy_policy">Ver a Política de Privacidade</string>
|
|
||||||
<string name="okay">OK</string>
|
|
||||||
<string name="tunnel_mobile_data">Túnel em dados móveis</string>
|
|
||||||
<string name="prominent_background_location_title">Revelar a localização em segundo plano</string>
|
|
||||||
<string name="thank_you">Obrigado por usar o WG Tunnel!</string>
|
|
||||||
<string name="trusted_ssid_value_description">Envie o SSID</string>
|
|
||||||
<string name="open_file">Abrir Ficheiro</string>
|
|
||||||
<string name="add_from_qr">Adicionar a partir de código QR</string>
|
|
||||||
<string name="add_tunnels_text">Adicionar a partir de ficheiro ou zip</string>
|
|
||||||
<string name="qr_scan">Escanear o código QR</string>
|
|
||||||
<string name="tunnel_name">Nome do Túnel</string>
|
|
||||||
<string name="config_changes_saved">Mudanças nas configurações gravadas.</string>
|
|
||||||
<string name="exclude">Excluir</string>
|
|
||||||
<string name="include">Incluir</string>
|
|
||||||
<string name="mtu">MTU</string>
|
|
||||||
<string name="always_on_vpn_support">Permitir VPN sempre ligada</string>
|
|
||||||
<string name="allowed_ips">IPs Permitidos</string>
|
|
||||||
<string name="peer">Par</string>
|
|
||||||
<string name="location_services_not_detected">Serviço de localização não foi detetado</string>
|
|
||||||
<string name="hint_search_packages">Procurar pacotes</string>
|
|
||||||
<string name="auto_tunneling">Auto-túnel</string>
|
|
||||||
<string name="vpn_on">VPN ligada</string>
|
|
||||||
<string name="vpn_off">VPN desligada</string>
|
|
||||||
<string name="listen_port">Porta de escuta</string>
|
|
||||||
<string name="turn_on_tunnel">Esta ação precisa um túnel ativo</string>
|
|
||||||
<string name="add_peer">Adicionar par</string>
|
|
||||||
<string name="interface_">Interface</string>
|
|
||||||
<string name="copy_public_key">Copiar chave pública</string>
|
|
||||||
<string name="comma_separated_list">Lista separada por vírgulas</string>
|
|
||||||
<string name="optional">(opcional)</string>
|
|
||||||
<string name="random">(aleatório)</string>
|
|
||||||
<string name="persistent_keepalive">Manter a conexão persistente (keepalive)</string>
|
|
||||||
<string name="cancel">Cancelar</string>
|
|
||||||
<string name="error_authentication_failed">Autenticação falhou</string>
|
|
||||||
<string name="error_authorization_failed">Autorização falhou</string>
|
|
||||||
<string name="restart_on_ping">Reiniciar em falha de ping (beta)</string>
|
|
||||||
<string name="email_description">Me envie um email</string>
|
|
||||||
<string name="error_ssid_exists">SSID já existe</string>
|
|
||||||
<string name="delete_tunnel_message">Tem certeza que quer apagar este túnel?</string>
|
|
||||||
<string name="yes">Sim</string>
|
|
||||||
<string name="unknown_error">Ocorreu um erro desconhecido</string>
|
|
||||||
<string name="email_subject">Apoio para o WG Tunnel</string>
|
|
||||||
<string name="tunnel_on_wifi">Túnel em Wi-Fi não confiável</string>
|
|
||||||
<string name="delete_tunnel">Apagar túnel</string>
|
|
||||||
<string name="email_chooser">Enviar um email…</string>
|
|
||||||
<string name="use_kernel">Usar o módulo do kernel</string>
|
|
||||||
<string name="docs_description">Ler a documentação</string>
|
|
||||||
<string name="error_root_denied">Shell Root negado</string>
|
|
||||||
<string name="location_services_missing_message">A aplicação não detetou o serviço de localização ativado no seu dispositivo. Dependendo do dispositivo, isto pode causar que a função de Wi-Fi não confiável falhe em ler o nome do Wi-Fi. Quer continuar mesmo assim?</string>
|
|
||||||
<string name="open_issue">Abrir um problema</string>
|
|
||||||
<string name="tunneling_apps">Aplicações com túnel</string>
|
|
||||||
<string name="no_email_detected">Nenhuma aplicação de email detetado</string>
|
|
||||||
<string name="no_browser_detected">Nenhum navegador detetado</string>
|
|
||||||
<string name="incorrect_pin">O Pin está errado</string>
|
|
||||||
<string name="auto">(automático)</string>
|
|
||||||
<string name="read_logs">Ler os registos</string>
|
|
||||||
<string name="pin_created">Pin criado com sucesso</string>
|
|
||||||
<string name="create_pin">Criar um pin</string>
|
|
||||||
<string name="enable_app_lock">Ligar bloqueio de aplicação</string>
|
|
||||||
<string name="edit_tunnel">Editar túnel</string>
|
|
||||||
<string name="mobile_data_tunnel">Selecionar como túnel em dados móveis</string>
|
|
||||||
<string name="set_primary_tunnel">Selecionar como túnel principal</string>
|
|
||||||
<string name="support">Suporte</string>
|
|
||||||
<string name="kernel">Kernel</string>
|
|
||||||
<string name="settings">Configurações</string>
|
|
||||||
<string name="unsure_how">se não tiver certeza em como continuar</string>
|
|
||||||
<string name="see_the">Veja o</string>
|
|
||||||
<string name="getting_started_guide">guia de início rápido</string>
|
|
||||||
<string name="error_file_format">Formato de configuração inválido</string>
|
|
||||||
<string name="vpn_channel_name">Canal de notificações VPN</string>
|
|
||||||
<string name="set_custom_ping_ip">Definir ip ping personalizado</string>
|
|
||||||
<string name="vpn_denied_dialog_title">Permissão negada</string>
|
|
||||||
<string name="vpn_settings">Configurações do sistema VPN</string>
|
|
||||||
<string name="always_on_message">A permissão de conexão VPN foi negada. Por favor, verifique</string>
|
|
||||||
<string name="chat_description">Junte-se à comunidade</string>
|
|
||||||
<string name="tunnel_required">Característica requer pelo menos um túnel</string>
|
|
||||||
<string name="app_settings">configurações da app</string>
|
|
||||||
<string name="background_location_message2">para garantir que essas permissões estejam ativadas.</string>
|
|
||||||
<string name="root_accepted">Shell root aceito</string>
|
|
||||||
<string name="optional_default">"opcional, padrão: "</string>
|
|
||||||
<string name="show_amnezia_properties">Mostrar propriedades de Amnezia</string>
|
|
||||||
<string name="never">nunca</string>
|
|
||||||
<string name="default_ping_ip">(opcional, padrão para pares)</string>
|
|
||||||
<string name="set_custom_ping_internal">Intervalo de Ping (seg)</string>
|
|
||||||
<string name="handshake">handshake</string>
|
|
||||||
<string name="background_location_message">Permitir que toda a permissão de localização do tempo e/ou localização precisa é necessária para este recurso. Por favor, veja</string>
|
|
||||||
<string name="sec">seg</string>
|
|
||||||
<string name="notifications">Notificações</string>
|
|
||||||
<string name="exclude_lan">Excluir LAN</string>
|
|
||||||
<string name="hide_scripts">Ocultar scripts</string>
|
|
||||||
<string name="trusted_wifi_names">Nomes de Wi-Fi confiáveis</string>
|
|
||||||
<string name="hide_amnezia_properties">Ocultar propriedades Amnezia</string>
|
|
||||||
<string name="remove_amnezia_compatibility">Remover compatibilidade Amnezia</string>
|
|
||||||
<string name="include_lan">Incluir LAN</string>
|
|
||||||
<string name="language">Idioma</string>
|
|
||||||
<string name="add_wifi_name">Adicionar nome Wi-Fi</string>
|
|
||||||
<string name="display_theme">Tema</string>
|
|
||||||
<string name="on_demand_rules">Regras de tunelamento sob demanda</string>
|
|
||||||
<string name="dark">Escuro</string>
|
|
||||||
<string name="dynamic">Dinâmico</string>
|
|
||||||
<string name="skip">Pular</string>
|
|
||||||
<string name="mobile_tunnel">Túnel com dados móveis</string>
|
|
||||||
<string name="requires_app_relaunch">Para aplicar as mudanças é necessário reiniciar o aplicativo. Deseja prosseguir ?</string>
|
|
||||||
<string name="add_from_clipboard">Adicionar da área de transferência</string>
|
|
||||||
<string name="restart_at_boot">Ativar na inicialização</string>
|
|
||||||
<string name="appearance">Aparência</string>
|
|
||||||
<string name="automatic">Automático</string>
|
|
||||||
<string name="light">Claro</string>
|
|
||||||
<string name="wildcards_active">Wildcards ativos</string>
|
|
||||||
<string name="learn_more">Saber mais</string>
|
|
||||||
<string name="use_wildcards">Usar nomes coringas</string>
|
|
||||||
<string name="wifi_name_via_shell">Nome do Wi-Fi por shell</string>
|
|
||||||
<string name="use_root_shell_for_wifi">Obter o nome do Wi-Fi através do shell root</string>
|
|
||||||
<string name="start_auto">Iniciar túnel automático</string>
|
|
||||||
<string name="stop_auto">Pausar túnel automático</string>
|
|
||||||
<string name="monitoring_state_changes">Monitorar status de alterações</string>
|
|
||||||
<string name="tunnel_running">Túnel em execução</string>
|
|
||||||
<string name="donate">Contribua com esse projeto</string>
|
|
||||||
<string name="local_logging">Registro local</string>
|
|
||||||
<string name="enable_local_logging">Ativar registro local</string>
|
|
||||||
<string name="configuration_change">Configuração alterada</string>
|
|
||||||
<string name="stop_on_no_internet">Interromper quando não há internet</string>
|
|
||||||
<string name="stop_on_internet_loss">Interrompa o túnel quando a internet não estiver disponível</string>
|
|
||||||
<string name="ethernet_tunnel">Túnel ethernet</string>
|
|
||||||
<string name="set_ethernet_tunnel">Definir como túnel ethernet</string>
|
|
||||||
<string name="native_kill_switch">Interruptor de desligamento padrão</string>
|
|
||||||
<string name="vpn_kill_switch">Interruptor de desligamento VPN</string>
|
|
||||||
<string name="kill_switch_options">Opções do interruptor de desligamento</string>
|
|
||||||
<string name="allow_lan_traffic">Permitir tráfego LAN</string>
|
|
||||||
<string name="bypass_lan_for_kill_switch">Ignorar LAN no interruptor de desligamento</string>
|
|
||||||
<string name="splt_tunneling">Tunelamento dividido</string>
|
|
||||||
<string name="stop">pausar</string>
|
|
||||||
<string name="tunnel_specific_settings">Configurações específicas no túnel</string>
|
|
||||||
<string name="show_scripts">Mostrar scripts</string>
|
|
||||||
<string name="amnezia_kernel_message">Amnezia indisponível no kernel</string>
|
|
||||||
<string name="enable_amnezia">Ativar Amnezia</string>
|
|
||||||
<string name="wg_compat_mode">Modo de compatibilidade WG</string>
|
|
||||||
<string name="quick_actions">Ações rápidas</string>
|
|
||||||
<string name="kernel_not_supported">Kernel não suportado</string>
|
|
||||||
<string name="advanced_settings">Configurações avançadas</string>
|
|
||||||
<string name="enable_amnezia_compatibility">Ativar compatibilidade Amnezia</string>
|
|
||||||
</resources>
|
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<string name="turn_off_tunnel">Действие требует отключения туннеля</string>
|
<string name="turn_off_tunnel">Действие требует отключения туннеля</string>
|
||||||
<string name="mtu">MTU</string>
|
<string name="mtu">MTU</string>
|
||||||
<string name="tunnel_name">Имя туннеля</string>
|
<string name="tunnel_name">Имя туннеля</string>
|
||||||
<string name="public_key">Публичный ключ</string>
|
<string name="public_key">Открытый ключ</string>
|
||||||
<string name="name">Имя</string>
|
<string name="name">Имя</string>
|
||||||
<string name="peer">Пир</string>
|
<string name="peer">Пир</string>
|
||||||
<string name="privacy_policy">Посмотреть политику конфиденциальности</string>
|
<string name="privacy_policy">Посмотреть политику конфиденциальности</string>
|
||||||
@@ -129,7 +129,7 @@
|
|||||||
<string name="handshake">рукопожатие</string>
|
<string name="handshake">рукопожатие</string>
|
||||||
<string name="logs">Журналы</string>
|
<string name="logs">Журналы</string>
|
||||||
<string name="light">Светлая</string>
|
<string name="light">Светлая</string>
|
||||||
<string name="automatic">Автоматически</string>
|
<string name="automatic">Автоматическая</string>
|
||||||
<string name="dynamic">Динамическая</string>
|
<string name="dynamic">Динамическая</string>
|
||||||
<string name="language">Язык</string>
|
<string name="language">Язык</string>
|
||||||
<string name="trusted_wifi_names">Доверенные сети Wi-Fi</string>
|
<string name="trusted_wifi_names">Доверенные сети Wi-Fi</string>
|
||||||
@@ -192,4 +192,17 @@
|
|||||||
<string name="advanced_settings">Дополнительные настройки</string>
|
<string name="advanced_settings">Дополнительные настройки</string>
|
||||||
<string name="enable_amnezia_compatibility">Включить совместимость с Amnezia</string>
|
<string name="enable_amnezia_compatibility">Включить совместимость с Amnezia</string>
|
||||||
<string name="include_lan">Включить LAN</string>
|
<string name="include_lan">Включить LAN</string>
|
||||||
|
<string name="auto_tunnel">Автотуннель</string>
|
||||||
|
<string name="tunnel_control">Управление туннелем</string>
|
||||||
|
<string name="error_tunnel_start">Невозможно запустить туннель</string>
|
||||||
|
<string name="kill_switch_off">Без экстренного отключения в доверенных</string>
|
||||||
|
<string name="prefer_ipv4">Предпочитать соединение IPv4</string>
|
||||||
|
<string name="dns_error">Не получить конечную точку DNS.</string>
|
||||||
|
<string name="start_failed_config">Невозможно запустить туннель с ошибкой конфигурации.</string>
|
||||||
|
<string name="unauthorized">Невозможно запустить туннель без авторизации.</string>
|
||||||
|
<string name="tunne_start_failed_title">Ошибка туннеля</string>
|
||||||
|
<string name="server_ipv4">Получение имени узла IPv4</string>
|
||||||
|
<string name="multiple">Несколько</string>
|
||||||
|
<string name="export_amnezia">Экспортировать как Amnezia</string>
|
||||||
|
<string name="export_wireguard">Экспортировать как WireGuard</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,43 +1,50 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">WG Tunnel</string>
|
<string name="app_name">WG Tunnel</string>
|
||||||
<string name="vpn_channel_name">VPN Bildirim Kanalı</string>
|
<string name="vpn_channel_id" translatable="false">VPN Channel</string>
|
||||||
<string name="error_file_extension">Dosya .conf veya .zip değil</string>
|
<string name="vpn_channel_name">VPN Bildirim Kanalı</string>
|
||||||
<string name="turn_off_tunnel">İşlem için tünelin kapalı olması gerekiyor</string>
|
<string name="github_url" translatable="false">https://github.com/zaneschepke/wgtunnel/issues</string>
|
||||||
|
<string name="docs_url" translatable="false">https://zaneschepke.com/wgtunnel-docs/overview.html</string>
|
||||||
|
<string name="privacy_policy_url" translatable="false">https://zaneschepke.com/wgtunnel-docs/privacypolicy.html</string>
|
||||||
|
<string name="docs_wildcards" translatable="false">https://zaneschepke.com/wgtunnel-docs/features.html#wildcard-wi-fi-name-support</string>
|
||||||
|
<string name="donate_url" translatable="false">https://zaneschepke.com/donate/</string>
|
||||||
|
<string name="error_file_extension">Dosya .conf veya .zip değil</string>
|
||||||
|
<string name="turn_off_tunnel">Bu işlem tünelin kapalı olmasını gerektirir</string>
|
||||||
<string name="no_tunnels">Henüz tünel eklenmedi!</string>
|
<string name="no_tunnels">Henüz tünel eklenmedi!</string>
|
||||||
<string name="tunnels">Tüneller</string>
|
<string name="tunnels">Tüneller</string>
|
||||||
<string name="tunnel_mobile_data">Mobil veride tünel</string>
|
<string name="tunnel_mobile_data">Mobil veride tünel</string>
|
||||||
<string name="privacy_policy">Gizlilik Politikasını Görüntüle</string>
|
<string name="privacy_policy">Gizlilik politikasını görüntüle</string>
|
||||||
<string name="okay">Tamam</string>
|
<string name="okay">Tamam</string>
|
||||||
<string name="tunnel_on_ethernet">Ethernet\'te tünel</string>
|
<string name="tunnel_on_ethernet">Ethernet üzerinde tünel</string>
|
||||||
<string name="prominent_background_location_message">Bu özellik, uygulama kapalıyken bile Wi-Fi SSID izlemesini etkinleştirmek için arka plan konum iznine ihtiyaç duyar. Daha fazla ayrıntı için lütfen Destek ekranında bağlantısı verilen Gizlilik Politikasına bakın.</string>
|
<string name="prominent_background_location_message">Bu özellik, uygulamanın kapalı olduğu durumlarda bile Wi-Fi SSID izlemesini etkinleştirmek için arka planda konum izni gerektirir. Daha fazla ayrıntı için lütfen Destek ekranında bağlantısı verilen Gizlilik Politikasına bakın.</string>
|
||||||
<string name="prominent_background_location_title">Arka Plan Konum Açıklaması</string>
|
<string name="prominent_background_location_title">Arka Plan Konum Açıklaması</string>
|
||||||
<string name="thank_you">WG Tunnel\'ı kullandığınız için teşekkürler!</string>
|
<string name="thank_you">WG Tunnel’i kullandığınız için teşekkürler!</string>
|
||||||
<string name="trusted_ssid_value_description">SSID\'yi gönder</string>
|
<string name="trusted_ssid_value_description">SSID Gönder</string>
|
||||||
<string name="add_tunnels_text">Dosyadan veya zip\'ten ekle</string>
|
<string name="add_tunnels_text">Dosyadan veya zip’ten ekle</string>
|
||||||
<string name="open_file">Dosya Aç</string>
|
<string name="open_file">Dosya Aç</string>
|
||||||
<string name="add_from_qr">QR kodundan ekle</string>
|
<string name="add_from_qr">QR kodundan ekle</string>
|
||||||
<string name="qr_scan">QR Tarama</string>
|
<string name="qr_scan">QR Tara</string>
|
||||||
<string name="tunnel_name">Tünel Adı</string>
|
<string name="tunnel_name">Tünel Adı</string>
|
||||||
<string name="exclude">Hariç tut</string>
|
<string name="exclude">Hariç Tut</string>
|
||||||
<string name="include">Dahil et</string>
|
<string name="include">Dahil Et</string>
|
||||||
<string name="config_changes_saved">Yapılandırma değişiklikleri kaydedildi.</string>
|
<string name="config_changes_saved">Yapılandırma değişiklikleri kaydedildi.</string>
|
||||||
<string name="public_key">Genel anahtar</string>
|
<string name="public_key">Genel anahtar</string>
|
||||||
<string name="addresses">Adresler</string>
|
<string name="addresses">Adresler</string>
|
||||||
<string name="dns_servers">DNS sunucuları</string>
|
<string name="dns_servers">DNS sunucuları</string>
|
||||||
<string name="mtu">MTU</string>
|
<string name="mtu">MTU</string>
|
||||||
<string name="peer">Eş (peer)</string>
|
<string name="peer">Eş</string>
|
||||||
<string name="allowed_ips">İzin verilen IP\'ler</string>
|
<string name="allowed_ips">İzin verilen IP’ler</string>
|
||||||
<string name="endpoint">Uç nokta (endpoint)</string>
|
<string name="endpoint">Uç Nokta</string>
|
||||||
<string name="name">Ad</string>
|
<string name="name">Ad</string>
|
||||||
<string name="always_on_vpn_support">Her Zaman Açık VPN\'e İzin Ver</string>
|
<string name="always_on_vpn_support">Her Zaman Açık VPN’e İzin Ver</string>
|
||||||
<string name="location_services_not_detected">Konum Hizmetleri Algılanmadı</string>
|
<string name="location_services_not_detected">Konum Servisleri Algılanmadı</string>
|
||||||
<string name="hint_search_packages">Paketlerde ara</string>
|
<string name="hint_search_packages">Paketleri ara</string>
|
||||||
|
<string name="db_name" translatable="false">wg-tunnel-db</string>
|
||||||
<string name="auto_tunneling">Otomatik tünelleme</string>
|
<string name="auto_tunneling">Otomatik tünelleme</string>
|
||||||
<string name="vpn_on">VPN açık</string>
|
<string name="vpn_on">VPN açık</string>
|
||||||
<string name="vpn_off">VPN kapalı</string>
|
<string name="vpn_off">VPN kapalı</string>
|
||||||
<string name="create_import">Sıfırdan oluştur</string>
|
<string name="create_import">Sıfırdan oluştur</string>
|
||||||
<string name="turn_on_tunnel">İşlem için aktif tünel gerekiyor</string>
|
<string name="turn_on_tunnel">Bu işlem aktif bir tünel gerektirir</string>
|
||||||
<string name="add_peer">Eş ekle</string>
|
<string name="add_peer">Eş ekle</string>
|
||||||
<string name="interface_">Arayüz</string>
|
<string name="interface_">Arayüz</string>
|
||||||
<string name="rotate_keys">Anahtarları döndür</string>
|
<string name="rotate_keys">Anahtarları döndür</string>
|
||||||
@@ -49,41 +56,42 @@
|
|||||||
<string name="random">(rastgele)</string>
|
<string name="random">(rastgele)</string>
|
||||||
<string name="optional">(isteğe bağlı)</string>
|
<string name="optional">(isteğe bağlı)</string>
|
||||||
<string name="optional_no_recommend">(isteğe bağlı, önerilmez)</string>
|
<string name="optional_no_recommend">(isteğe bağlı, önerilmez)</string>
|
||||||
<string name="preshared_key">Önceden paylaşılmış anahtar</string>
|
<string name="preshared_key">Ön paylaşımlı anahtar</string>
|
||||||
<string name="seconds">saniye</string>
|
<string name="seconds">saniye</string>
|
||||||
<string name="persistent_keepalive">Kalıcı canlı tutma</string>
|
<string name="persistent_keepalive">Kalıcı canlı tutma</string>
|
||||||
<string name="cancel">İptal</string>
|
<string name="cancel">İptal</string>
|
||||||
<string name="error_authentication_failed">Kimlik doğrulama başarısız oldu</string>
|
<string name="error_authentication_failed">Kimlik doğrulama başarısız</string>
|
||||||
<string name="error_authorization_failed">Yetkilendirme başarısız oldu</string>
|
<string name="error_authorization_failed">Yetkilendirme başarısız</string>
|
||||||
<string name="enabled_app_shortcuts">Uygulama kısayollarını etkinleştir</string>
|
<string name="enabled_app_shortcuts">Uygulama kısayollarını etkinleştir</string>
|
||||||
<string name="export_configs">Yapılandırmaları dışa aktar</string>
|
<string name="export_configs">Yapılandırmaları dışa aktar</string>
|
||||||
<string name="unknown_error">Bilinmeyen bir hata oluştu</string>
|
<string name="unknown_error">Bilinmeyen bir hata oluştu</string>
|
||||||
<string name="tunnel_on_wifi">Güvenilmeyen wifi\'da tünel</string>
|
<string name="tunnel_on_wifi">Güvenilmeyen wifi’da tünel</string>
|
||||||
<string name="email_subject">WG Tunnel Desteği</string>
|
<string name="my_email" translatable="false">support@zaneschepke.com</string>
|
||||||
|
<string name="email_subject">WG Tunnel Desteği</string>
|
||||||
<string name="email_chooser">E-posta gönder…</string>
|
<string name="email_chooser">E-posta gönder…</string>
|
||||||
<string name="docs_description">Belgeleri oku</string>
|
<string name="docs_description">Belgeleri oku</string>
|
||||||
<string name="email_description">Bana e-posta gönder</string>
|
<string name="email_description">Bana e-posta gönder</string>
|
||||||
<string name="use_kernel">Kernel modülünü kullan</string>
|
<string name="use_kernel">Çekirdek modülünü kullan</string>
|
||||||
<string name="error_ssid_exists">SSID zaten mevcut</string>
|
<string name="error_ssid_exists">SSID zaten mevcut</string>
|
||||||
<string name="error_root_denied">Root kabuğu reddedildi</string>
|
<string name="error_root_denied">Root kabuğu reddedildi</string>
|
||||||
<string name="error_no_file_explorer">Dosya gezgini yüklü değil</string>
|
<string name="error_no_file_explorer">Dosya gezgini yüklü değil</string>
|
||||||
<string name="error_invalid_code">Geçersiz QR kodu</string>
|
<string name="error_invalid_code">Geçersiz QR kodu</string>
|
||||||
<string name="location_services_missing_message">Uygulama, cihazınızda etkinleştirilmiş herhangi bir konum hizmeti algılamıyor. Cihaza bağlı olarak, bu durum güvenilmeyen wifi özelliğinin wifi adını okumasını engelleyebilir. Yine de devam etmek istiyor musunuz?</string>
|
<string name="location_services_missing_message">Uygulama, cihazınızda etkinleştirilmiş herhangi bir konum servisi algılamıyor. Cihaza bağlı olarak, bu durum güvenilmeyen wifi özelliğinin wifi adını okuyamamasını sağlayabilir. Yine de devam etmek ister misiniz?</string>
|
||||||
<string name="auto_tunnel_title">Otomatik Tünel Hizmeti</string>
|
<string name="auto_tunnel_title">Otomatik tünel servisi</string>
|
||||||
<string name="delete_tunnel">Tüneli sil</string>
|
<string name="delete_tunnel">Tüneli sil</string>
|
||||||
<string name="delete_tunnel_message">Bu tüneli silmek istediğinizden emin misiniz?</string>
|
<string name="delete_tunnel_message">Bu tüneli silmek istediğinizden emin misiniz?</string>
|
||||||
<string name="yes">Evet</string>
|
<string name="yes">Evet</string>
|
||||||
<string name="tunneling_apps">Tünellenen uygulamalar</string>
|
<string name="tunneling_apps">Tünelleme uygulamaları</string>
|
||||||
<string name="all">tümü</string>
|
<string name="all">tümü</string>
|
||||||
<string name="no_email_detected">E-posta uygulaması algılanmadı</string>
|
<string name="no_email_detected">E-posta uygulaması algılanmadı</string>
|
||||||
<string name="no_browser_detected">Tarayıcı algılanmadı</string>
|
<string name="no_browser_detected">Tarayıcı algılanmadı</string>
|
||||||
<string name="open_issue">Sorun bildir</string>
|
<string name="open_issue">Bir sorun aç</string>
|
||||||
<string name="read_logs">Günlükleri oku</string>
|
<string name="read_logs">Günlükleri oku</string>
|
||||||
<string name="auto">(otomatik)</string>
|
<string name="auto">(otomatik)</string>
|
||||||
<string name="incorrect_pin">PIN yanlış</string>
|
<string name="incorrect_pin">Pin yanlış</string>
|
||||||
<string name="pin_created">PIN başarıyla oluşturuldu</string>
|
<string name="pin_created">Pin başarıyla oluşturuldu</string>
|
||||||
<string name="enter_pin">PIN\'inizi girin</string>
|
<string name="enter_pin">Pin’inizi girin</string>
|
||||||
<string name="create_pin">PIN oluştur</string>
|
<string name="create_pin">Pin oluştur</string>
|
||||||
<string name="enable_app_lock">Uygulama kilidini etkinleştir</string>
|
<string name="enable_app_lock">Uygulama kilidini etkinleştir</string>
|
||||||
<string name="restart_on_ping">Ping başarısız olduğunda yeniden başlat (beta)</string>
|
<string name="restart_on_ping">Ping başarısız olduğunda yeniden başlat (beta)</string>
|
||||||
<string name="mobile_data_tunnel">Mobil veri tüneli olarak ayarla</string>
|
<string name="mobile_data_tunnel">Mobil veri tüneli olarak ayarla</string>
|
||||||
@@ -94,18 +102,118 @@
|
|||||||
<string name="settings">Ayarlar</string>
|
<string name="settings">Ayarlar</string>
|
||||||
<string name="support">Destek</string>
|
<string name="support">Destek</string>
|
||||||
<string name="kernel">Çekirdek</string>
|
<string name="kernel">Çekirdek</string>
|
||||||
<string name="junk_packet_count">Gereksiz paket sayısı</string>
|
<string name="junk_packet_count">Çöp paket sayısı</string>
|
||||||
<string name="junk_packet_minimum_size">Gereksiz paket minimum boyutu</string>
|
<string name="junk_packet_minimum_size">Çöp paket minimum boyutu</string>
|
||||||
<string name="junk_packet_maximum_size">Gereksiz paket maksimum boyutu</string>
|
<string name="junk_packet_maximum_size">Çöp paket maksimum boyutu</string>
|
||||||
<string name="init_packet_junk_size">Başlatma paketi gereksiz boyutu</string>
|
<string name="init_packet_junk_size">Başlangıç paketi çöp boyutu</string>
|
||||||
<string name="response_packet_junk_size">Yanıt paketi gereksiz boyutu</string>
|
<string name="response_packet_junk_size">Yanıt paketi çöp boyutu</string>
|
||||||
<string name="init_packet_magic_header">Başlatma paketi sihirli başlığı</string>
|
<string name="init_packet_magic_header">Başlangıç paketi sihirli başlığı</string>
|
||||||
<string name="response_packet_magic_header">Yanıt paketi sihirli başlığı</string>
|
<string name="response_packet_magic_header">Yanıt paketi sihirli başlığı</string>
|
||||||
<string name="transport_packet_magic_header">Taşıma paketi sihirli başlığı</string>
|
<string name="transport_packet_magic_header">Taşıma paketi sihirli başlığı</string>
|
||||||
<string name="underload_packet_magic_header">Düşük yük paketi sihirli başlığı</string>
|
<string name="underload_packet_magic_header">Düşük yük paketi sihirli başlığı</string>
|
||||||
<string name="unsure_how">nasıl devam edeceğinizden emin değilseniz</string>
|
<string name="telegram_url" translatable="false">https://t.me/wgtunnel</string>
|
||||||
<string name="see_the">Bakın:</string>
|
<string name="unsure_how">nasıl devam edeceğinizden emin değilseniz</string>
|
||||||
<string name="getting_started_guide">başlangıç kılavuzu</string>
|
<string name="see_the">Bakınız</string>
|
||||||
|
<string name="getting_started_url" translatable="false">https://zaneschepke.com/wgtunnel-docs/getting-started.html</string>
|
||||||
|
<string name="getting_started_guide">başlangıç kılavuzu</string>
|
||||||
<string name="error_file_format">Geçersiz tünel yapılandırma formatı</string>
|
<string name="error_file_format">Geçersiz tünel yapılandırma formatı</string>
|
||||||
<string name="restart_at_boot">Önyüklemede yeniden başlat</string>
|
<string name="restart_at_boot">Başlangıçta yeniden başlat</string>
|
||||||
|
<string name="vpn_denied_dialog_title">İzin Reddedildi</string>
|
||||||
|
<string name="vpn_settings">VPN sistem ayarları</string>
|
||||||
|
<string name="always_on_message">VPN bağlantı izni reddedildi. Lütfen</string>
|
||||||
|
<string name="always_on_message2">diğer tüm uygulamalar için Her Zaman Açık VPN’in kapalı olduğundan emin olun ve tekrar deneyin</string>
|
||||||
|
<string name="chat_description">Topluluğa katıl</string>
|
||||||
|
<string name="tunnel_required">Bu özellik en az bir tünel gerektirir</string>
|
||||||
|
<string name="background_location_message">Bu özellik için her zaman konum izni ve/veya hassas konum gereklidir. Lütfen</string>
|
||||||
|
<string name="app_settings">uygulama ayarları</string>
|
||||||
|
<string name="background_location_message2">bu izinlerin etkin olduğundan emin olun</string>
|
||||||
|
<string name="root_accepted">Root kabuğu kabul edildi</string>
|
||||||
|
<string name="set_custom_ping_ip">Özel ping IP’si ayarla</string>
|
||||||
|
<string name="default_ping_ip">(isteğe bağlı, varsayılan eşler)</string>
|
||||||
|
<string name="set_custom_ping_internal">Ping aralığı (saniye)</string>
|
||||||
|
<string name="optional_default">"isteğe bağlı, varsayılan: "</string>
|
||||||
|
<string name="set_custom_ping_cooldown">Ping yeniden başlatma bekleme süresi (saniye)</string>
|
||||||
|
<string name="show_amnezia_properties">Amnezia özelliklerini göster</string>
|
||||||
|
<string name="never">asla</string>
|
||||||
|
<string name="sec">sn</string>
|
||||||
|
<string name="handshake">el sıkışma</string>
|
||||||
|
<string name="logs">Günlükler</string>
|
||||||
|
<string name="kill_switch">Kill Switch</string>
|
||||||
|
<string name="appearance">Görünüm</string>
|
||||||
|
<string name="notifications">Bildirimler</string>
|
||||||
|
<string name="automatic">Otomatik</string>
|
||||||
|
<string name="light">Açık</string>
|
||||||
|
<string name="dark">Koyu</string>
|
||||||
|
<string name="dynamic">Dinamik</string>
|
||||||
|
<string name="language">Dil</string>
|
||||||
|
<string name="display_theme">Ekran teması</string>
|
||||||
|
<string name="trusted_wifi_names">Güvenilir wifi adları</string>
|
||||||
|
<string name="add_wifi_name">Wifi adı ekle</string>
|
||||||
|
<string name="on_demand_rules">İsteğe bağlı tünel kuralları</string>
|
||||||
|
<string name="primary_tunnel">Birincil tünel</string>
|
||||||
|
<string name="mobile_tunnel">Mobil veri tüneli</string>
|
||||||
|
<string name="skip">Atla</string>
|
||||||
|
<string name="launch_app_settings">Uygulama ayarlarını başlat</string>
|
||||||
|
<string name="use_wildcards">İsim jokerlerini kullan</string>
|
||||||
|
<string name="learn_more">Daha fazla bilgi</string>
|
||||||
|
<string name="wildcards_active">Jokerler etkin</string>
|
||||||
|
<string name="wifi_name_via_shell">Kabuk üzerinden wifi adı</string>
|
||||||
|
<string name="use_root_shell_for_wifi">Wifi adını almak için root kabuğunu kullan</string>
|
||||||
|
<string name="kernel_not_supported">Çekirdek desteklenmiyor</string>
|
||||||
|
<string name="start_auto">Otomatik tüneli başlat</string>
|
||||||
|
<string name="stop_auto">Otomatik tüneli durdur</string>
|
||||||
|
<string name="tunnel_running">Tünel çalışıyor</string>
|
||||||
|
<string name="monitoring_state_changes">Durum değişikliklerini izleme</string>
|
||||||
|
<string name="donate">Projeye bağış yap</string>
|
||||||
|
<string name="local_logging">Yerel günlüğe kaydetme</string>
|
||||||
|
<string name="enable_local_logging">Yerel günlüğe kaydetmeyi etkinleştir</string>
|
||||||
|
<string name="configuration_change">Yapılandırma değişikliği</string>
|
||||||
|
<string name="requires_app_relaunch">Bu değişiklik uygulamanın yeniden başlatılmasını gerektirir. Devam etmek ister misiniz?</string>
|
||||||
|
<string name="add_from_clipboard">Panodan ekle</string>
|
||||||
|
<string name="stop_on_no_internet">İnternet olmadığında durdur</string>
|
||||||
|
<string name="stop_on_internet_loss">İnternet kaybında tüneli durdur</string>
|
||||||
|
<string name="ethernet_tunnel">Ethernet tüneli</string>
|
||||||
|
<string name="set_ethernet_tunnel">Ethernet tüneli olarak ayarla</string>
|
||||||
|
<string name="native_kill_switch">Yerel kill switch</string>
|
||||||
|
<string name="vpn_kill_switch">VPN kill switch</string>
|
||||||
|
<string name="kill_switch_options">Kill switch seçenekleri</string>
|
||||||
|
<string name="allow_lan_traffic">LAN trafiğine izin ver</string>
|
||||||
|
<string name="bypass_lan_for_kill_switch">Kill switch için LAN’ı atla</string>
|
||||||
|
<string name="vpn_channel_description">VPN durum bildirimleri için bir kanal</string>
|
||||||
|
<string name="auto_tunnel_channel_id" translatable="false">Auto-tunnel Channel</string>
|
||||||
|
<string name="auto_tunnel_channel_name">Otomatik Tünel Bildirim Kanalı</string>
|
||||||
|
<string name="auto_tunnel_channel_description">Otomatik tünel durum bildirimleri için bir kanal</string>
|
||||||
|
<string name="stop">durdur</string>
|
||||||
|
<string name="splt_tunneling">Bölünmüş tünelleme</string>
|
||||||
|
<string name="tunnel_specific_settings">Tünele özgü ayarlar</string>
|
||||||
|
<string name="show_scripts">Komut dosyalarını göster</string>
|
||||||
|
<string name="pre_up">Ön çalıştırma</string>
|
||||||
|
<string name="post_up">Sonra çalıştırma</string>
|
||||||
|
<string name="pre_down">Ön kapatma</string>
|
||||||
|
<string name="post_down">Sonra kapatma</string>
|
||||||
|
<string name="amnezia_kernel_message">Amnezia çekirdek modunda kullanılamaz</string>
|
||||||
|
<string name="enable_amnezia">Amnezia’yı etkinleştir</string>
|
||||||
|
<string name="wg_compat_mode">WG uyumluluk modu</string>
|
||||||
|
<string name="quick_actions">Hızlı eylemler</string>
|
||||||
|
<string name="advanced_settings">Gelişmiş ayarlar</string>
|
||||||
|
<string name="debounce_delay">Gecikme süresi</string>
|
||||||
|
<string name="hide_amnezia_properties">Amnezia özelliklerini gizle</string>
|
||||||
|
<string name="hide_scripts">Komut dosyalarını gizle</string>
|
||||||
|
<string name="enable_amnezia_compatibility">Amnezia uyumluluğunu etkinleştir</string>
|
||||||
|
<string name="remove_amnezia_compatibility">Amnezia uyumluluğunu kaldır</string>
|
||||||
|
<string name="exclude_lan">LAN’ı hariç tut</string>
|
||||||
|
<string name="include_lan">LAN’ı dahil et</string>
|
||||||
|
<string name="error_tunnel_start">Tünel başlatma başarısız</string>
|
||||||
|
<string name="tunnel_control">Tünel kontrolü</string>
|
||||||
|
<string name="auto_tunnel">Otomatik tünel</string>
|
||||||
|
<string name="kill_switch_off">Güvenilirde kill switch’i durdur</string>
|
||||||
|
<string name="server_ipv4">IPv4 ana makine çözünürlüğü</string>
|
||||||
|
<string name="prefer_ipv4">IPv4 bağlantısını tercih et</string>
|
||||||
|
<string name="dns_error">Uç nokta DNS’si çözülemedi.</string>
|
||||||
|
<string name="start_failed_config">Yapılandırma hatası nedeniyle tünel başlatılamadı.</string>
|
||||||
|
<string name="unauthorized">Yetkisiz, tünel başlatılamadı.</string>
|
||||||
|
<string name="tunne_start_failed_title">Tünel hatası</string>
|
||||||
|
<string name="multiple">Çoklu</string>
|
||||||
|
<string name="export_amnezia">Amnezia olarak dışa aktar</string>
|
||||||
|
<string name="export_wireguard">WireGuard olarak dışa aktar</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
<string name="getting_started_guide">інструкція щодо початку роботи</string>
|
<string name="getting_started_guide">інструкція щодо початку роботи</string>
|
||||||
<string name="error_file_format">некоректний формат конфігурації тунелю</string>
|
<string name="error_file_format">некоректний формат конфігурації тунелю</string>
|
||||||
<string name="vpn_channel_name">Канал сповіщення VPN</string>
|
<string name="vpn_channel_name">Канал сповіщення VPN</string>
|
||||||
<string name="error_file_extension">Файл не є .conf або .zip файлом</string>
|
<string name="error_file_extension">Файл не є .conf або .zip файлом</string>
|
||||||
<string name="turn_off_tunnel">Дія потребує вимкнення тунелю</string>
|
<string name="turn_off_tunnel">Дія потребує вимкнення тунелю</string>
|
||||||
<string name="tunnel_mobile_data">Тунелювати мобільні дані</string>
|
<string name="tunnel_mobile_data">Тунелювати мобільні дані</string>
|
||||||
<string name="privacy_policy">Переглянути політику конфіденційності</string>
|
<string name="privacy_policy">Переглянути політику конфіденційності</string>
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
<string name="name">Ім\'я</string>
|
<string name="name">Ім\'я</string>
|
||||||
<string name="always_on_vpn_support">Дозволили Always-ON VPN</string>
|
<string name="always_on_vpn_support">Дозволили Always-ON VPN</string>
|
||||||
<string name="location_services_not_detected">Сервіси місце знаходження не знайдено</string>
|
<string name="location_services_not_detected">Сервіси місце знаходження не знайдено</string>
|
||||||
<string name="auto_tunneling">Авто-тунелювання</string>
|
<string name="auto_tunneling">Авто-тунелювання</string>
|
||||||
<string name="vpn_on">VPN увімк.</string>
|
<string name="vpn_on">VPN увімк.</string>
|
||||||
<string name="vpn_off">VPN вимк.</string>
|
<string name="vpn_off">VPN вимк.</string>
|
||||||
<string name="create_import">Створити з нуля</string>
|
<string name="create_import">Створити з нуля</string>
|
||||||
@@ -59,9 +59,9 @@
|
|||||||
<string name="interface_">Інтерфейс</string>
|
<string name="interface_">Інтерфейс</string>
|
||||||
<string name="copy_public_key">Копіювати відкритий ключ</string>
|
<string name="copy_public_key">Копіювати відкритий ключ</string>
|
||||||
<string name="listen_port">Слухати порт</string>
|
<string name="listen_port">Слухати порт</string>
|
||||||
<string name="preshared_key">Pre-shared key</string>
|
<string name="preshared_key">Загальний ключ</string>
|
||||||
<string name="seconds">секунд</string>
|
<string name="seconds">секунд</string>
|
||||||
<string name="persistent_keepalive">Persistent keepalive</string>
|
<string name="persistent_keepalive">Підтримка роботи тунелю (keepalive)</string>
|
||||||
<string name="error_authorization_failed">Не вдалося авторизуватися</string>
|
<string name="error_authorization_failed">Не вдалося авторизуватися</string>
|
||||||
<string name="enabled_app_shortcuts">Дозволити ярлики</string>
|
<string name="enabled_app_shortcuts">Дозволити ярлики</string>
|
||||||
<string name="error_authentication_failed">Помилка автентифікації</string>
|
<string name="error_authentication_failed">Помилка автентифікації</string>
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
<string name="use_kernel">Використовувати модуль режиму ядра</string>
|
<string name="use_kernel">Використовувати модуль режиму ядра</string>
|
||||||
<string name="error_ssid_exists">SSID вже існує</string>
|
<string name="error_ssid_exists">SSID вже існує</string>
|
||||||
<string name="error_no_file_explorer">Не знайдено файловий менеджер</string>
|
<string name="error_no_file_explorer">Не знайдено файловий менеджер</string>
|
||||||
<string name="auto_tunnel_title">Сервіс авто-тунелювання</string>
|
<string name="auto_tunnel_title">Сервіс автотунелювання</string>
|
||||||
<string name="delete_tunnel">Видалити тунель</string>
|
<string name="delete_tunnel">Видалити тунель</string>
|
||||||
<string name="location_services_missing_message">Додаток не знайшов служб місце знаходження на вашому пристрої. На деяких пристроях це може привести до неможливості визначення назви мережі Wi-Fi і помилок функції недовірених Wi-Fi мереж. Все рівно хочете продовжити?</string>
|
<string name="location_services_missing_message">Додаток не знайшов служб місце знаходження на вашому пристрої. На деяких пристроях це може привести до неможливості визначення назви мережі Wi-Fi і помилок функції недовірених Wi-Fi мереж. Все рівно хочете продовжити?</string>
|
||||||
<string name="delete_tunnel_message">Ви дійсно хочете видалити цей тунель?</string>
|
<string name="delete_tunnel_message">Ви дійсно хочете видалити цей тунель?</string>
|
||||||
@@ -107,4 +107,102 @@
|
|||||||
<string name="response_packet_magic_header">Заголовок пакету відповіді</string>
|
<string name="response_packet_magic_header">Заголовок пакету відповіді</string>
|
||||||
<string name="unsure_how">, якщо не впевнені що робити далі</string>
|
<string name="unsure_how">, якщо не впевнені що робити далі</string>
|
||||||
<string name="see_the">Дивіться</string>
|
<string name="see_the">Дивіться</string>
|
||||||
|
<string name="skip">Пропустити</string>
|
||||||
|
<string name="trusted_wifi_names">Довірені мережі Wi-Fi</string>
|
||||||
|
<string name="vpn_denied_dialog_title">Немає дозволу</string>
|
||||||
|
<string name="app_settings">налаштування програми</string>
|
||||||
|
<string name="background_location_message2">, щоб переконатися, що ці дозволи надано</string>
|
||||||
|
<string name="default_ping_ip">(необов\'язково, за замовчуванням для пірів)</string>
|
||||||
|
<string name="set_custom_ping_internal">Інтервал пінгу (сек.)</string>
|
||||||
|
<string name="set_custom_ping_cooldown">Час очікування перезапуску пінгу (сек.)</string>
|
||||||
|
<string name="show_amnezia_properties">Показати налаштування Amnezia</string>
|
||||||
|
<string name="sec">сек.</string>
|
||||||
|
<string name="handshake">рукостискання</string>
|
||||||
|
<string name="notifications">Сповіщення</string>
|
||||||
|
<string name="light">Світла</string>
|
||||||
|
<string name="dark">Темна</string>
|
||||||
|
<string name="language">Мова</string>
|
||||||
|
<string name="add_wifi_name">Додати мережу Wi-Fi</string>
|
||||||
|
<string name="on_demand_rules">Правила тунелю на запит</string>
|
||||||
|
<string name="primary_tunnel">Основний тунель</string>
|
||||||
|
<string name="launch_app_settings">Налаштування запуску програми</string>
|
||||||
|
<string name="learn_more">Дізнатись більше</string>
|
||||||
|
<string name="wildcards_active">Підстановочні знаки використовуються</string>
|
||||||
|
<string name="wifi_name_via_shell">Ім\'я Wi-Fi через root</string>
|
||||||
|
<string name="kill_switch">Екстрене відключення</string>
|
||||||
|
<string name="restart_at_boot">Перезапуск під час завантаження</string>
|
||||||
|
<string name="vpn_settings">Системні налаштування VPN</string>
|
||||||
|
<string name="always_on_message">Дозвіл на VPN-з\'єднання було відхилено, перевірте</string>
|
||||||
|
<string name="root_accepted">Root-доступ дозволено</string>
|
||||||
|
<string name="set_custom_ping_ip">Призначити свій IP для пінгу</string>
|
||||||
|
<string name="logs">Журнали</string>
|
||||||
|
<string name="mobile_tunnel">Тунель для мобільних даних</string>
|
||||||
|
<string name="display_theme">Тема</string>
|
||||||
|
<string name="chat_description">Приєднатися до спільноти</string>
|
||||||
|
<string name="automatic">Автоматично</string>
|
||||||
|
<string name="never">ніколи</string>
|
||||||
|
<string name="appearance">Зовнішній вигляд</string>
|
||||||
|
<string name="dynamic">Динамічна</string>
|
||||||
|
<string name="use_root_shell_for_wifi">Використовувати root-доступ для отримання імені мережі Wi-Fi</string>
|
||||||
|
<string name="use_wildcards">Використовувати підстановочні знаки в імені</string>
|
||||||
|
<string name="optional_default">"необов\'язково, за замовчуванням: "</string>
|
||||||
|
<string name="always_on_message2">, щоб переконатися, що функція «Постійний VPN» вимкнена для всіх інших програм, і спробуйте ще раз</string>
|
||||||
|
<string name="tunnel_required">Для цієї функції необхідний хоча б один тунель</string>
|
||||||
|
<string name="background_location_message">Дозволяти весь час, доки для роботи цієї функції потрібен доступ на місцезнаходження та/або точне місцезнаходження. Дивіться</string>
|
||||||
|
<string name="advanced_settings">Додаткові налаштування</string>
|
||||||
|
<string name="quick_actions">Швидкі дії</string>
|
||||||
|
<string name="tunnel_running">Тунель працює</string>
|
||||||
|
<string name="monitoring_state_changes">Відстеження змін стану</string>
|
||||||
|
<string name="donate">Пожертвувати на проект</string>
|
||||||
|
<string name="requires_app_relaunch">Дана зміна потребує перезапуску програми. Продовжити?</string>
|
||||||
|
<string name="add_from_clipboard">Додати з буфера обміну</string>
|
||||||
|
<string name="stop_on_no_internet">Зупинити без інтернету</string>
|
||||||
|
<string name="native_kill_switch">Штатне екстрене відключення</string>
|
||||||
|
<string name="vpn_kill_switch">Екстрене відключення VPN</string>
|
||||||
|
<string name="kill_switch_options">Налаштування екстреного вимкнення</string>
|
||||||
|
<string name="allow_lan_traffic">Обхід LAN</string>
|
||||||
|
<string name="bypass_lan_for_kill_switch">Дозволяти трафік LAN при екстреному вимкненні</string>
|
||||||
|
<string name="auto_tunnel_channel_name">Канал сповіщень автотунелю</string>
|
||||||
|
<string name="auto_tunnel_channel_description">Канал сповіщень про стан автотунелю</string>
|
||||||
|
<string name="stop">стоп</string>
|
||||||
|
<string name="splt_tunneling">Роздільне тунелювання</string>
|
||||||
|
<string name="show_scripts">Показати сценарії</string>
|
||||||
|
<string name="pre_up">До активації</string>
|
||||||
|
<string name="post_up">Після активації</string>
|
||||||
|
<string name="pre_down">До деактивації</string>
|
||||||
|
<string name="post_down">Після деактивації</string>
|
||||||
|
<string name="amnezia_kernel_message">Amnezia недоступна у режимі ядра</string>
|
||||||
|
<string name="enable_amnezia">Використовувати Amnezia</string>
|
||||||
|
<string name="wg_compat_mode">Режим сумісності WG</string>
|
||||||
|
<string name="debounce_delay">Затримка відбою</string>
|
||||||
|
<string name="hide_amnezia_properties">Приховати налаштування Amnezia</string>
|
||||||
|
<string name="hide_scripts">Приховати сценарії</string>
|
||||||
|
<string name="remove_amnezia_compatibility">Вимкнути сумісність з Amnezia</string>
|
||||||
|
<string name="exclude_lan">Виключити LAN</string>
|
||||||
|
<string name="include_lan">Увімкнути LAN</string>
|
||||||
|
<string name="error_tunnel_start">Неможливо запустити тунель</string>
|
||||||
|
<string name="tunnel_control">Управління тунелем</string>
|
||||||
|
<string name="auto_tunnel">Автотунель</string>
|
||||||
|
<string name="local_logging">Локальне ведення журналу</string>
|
||||||
|
<string name="kernel_not_supported">Ядро не підтримується</string>
|
||||||
|
<string name="start_auto">Запустити автотунель</string>
|
||||||
|
<string name="stop_auto">Зупинити автотунель</string>
|
||||||
|
<string name="enable_local_logging">Увімкнути ведення журналу</string>
|
||||||
|
<string name="configuration_change">Зміна конфігурації</string>
|
||||||
|
<string name="stop_on_internet_loss">Зупинити тунель під час втрати інтернету</string>
|
||||||
|
<string name="ethernet_tunnel">Тунель для Ethernet</string>
|
||||||
|
<string name="set_ethernet_tunnel">Призначити як тунель для Ethernet</string>
|
||||||
|
<string name="tunnel_specific_settings">Спеціальні налаштування тунелю</string>
|
||||||
|
<string name="vpn_channel_description">Канал сповіщень про стан VPN</string>
|
||||||
|
<string name="enable_amnezia_compatibility">Включити сумісність із Amnezia</string>
|
||||||
|
<string name="kill_switch_off">Без екстреного вимкнення у довірених</string>
|
||||||
|
<string name="server_ipv4">Отримання імені вузла IPv4</string>
|
||||||
|
<string name="prefer_ipv4">Віддавати перевагу з\'єднанню IPv4</string>
|
||||||
|
<string name="dns_error">Не отримати кінцеву точку DNS.</string>
|
||||||
|
<string name="start_failed_config">Неможливо запустити тунель із помилкою конфігурації.</string>
|
||||||
|
<string name="unauthorized">Неможливо запустити тунель без авторизації.</string>
|
||||||
|
<string name="tunne_start_failed_title">Помилка тунелю</string>
|
||||||
|
<string name="multiple">Декілька</string>
|
||||||
|
<string name="export_amnezia">Експортувати як Amnezia</string>
|
||||||
|
<string name="export_wireguard">Експортувати як WireGuard</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -65,7 +65,7 @@
|
|||||||
<string name="config_changes_saved">设置已保存。</string>
|
<string name="config_changes_saved">设置已保存。</string>
|
||||||
<string name="interface_">接口</string>
|
<string name="interface_">接口</string>
|
||||||
<string name="email_subject">WG Tunnel 支持</string>
|
<string name="email_subject">WG Tunnel 支持</string>
|
||||||
<string name="auto_tunnel_title">自动连接服务</string>
|
<string name="auto_tunnel_title">自动隧道服务</string>
|
||||||
<string name="delete_tunnel">删除隧道</string>
|
<string name="delete_tunnel">删除隧道</string>
|
||||||
<string name="delete_tunnel_message">确定删除这个隧道吗?</string>
|
<string name="delete_tunnel_message">确定删除这个隧道吗?</string>
|
||||||
<string name="location_services_missing_message">此应用不会在您的设备上检测任何已开启的定位服务。根据不同的设备,可能会导致无法获得不可信 WiFi 的名称。您想继续吗?</string>
|
<string name="location_services_missing_message">此应用不会在您的设备上检测任何已开启的定位服务。根据不同的设备,可能会导致无法获得不可信 WiFi 的名称。您想继续吗?</string>
|
||||||
@@ -108,7 +108,7 @@
|
|||||||
<string name="vpn_denied_dialog_title">拒绝访问</string>
|
<string name="vpn_denied_dialog_title">拒绝访问</string>
|
||||||
<string name="tunnel_required">此功能需要至少一个隧道</string>
|
<string name="tunnel_required">此功能需要至少一个隧道</string>
|
||||||
<string name="app_settings">应用设置</string>
|
<string name="app_settings">应用设置</string>
|
||||||
<string name="background_location_message2">请确保这些权限已开启。</string>
|
<string name="background_location_message2">请确保这些权限已开启</string>
|
||||||
<string name="logs">日志</string>
|
<string name="logs">日志</string>
|
||||||
<string name="restart_on_ping">Ping 失败之后自动重启隧道(beta)</string>
|
<string name="restart_on_ping">Ping 失败之后自动重启隧道(beta)</string>
|
||||||
<string name="edit_tunnel">编辑隧道</string>
|
<string name="edit_tunnel">编辑隧道</string>
|
||||||
@@ -160,4 +160,49 @@
|
|||||||
<string name="enable_local_logging">开启本地日志</string>
|
<string name="enable_local_logging">开启本地日志</string>
|
||||||
<string name="configuration_change">配置更改</string>
|
<string name="configuration_change">配置更改</string>
|
||||||
<string name="requires_app_relaunch">此更改需要重新启动应用程序。您是否要继续?</string>
|
<string name="requires_app_relaunch">此更改需要重新启动应用程序。您是否要继续?</string>
|
||||||
|
<string name="stop_on_no_internet">无网络时停用</string>
|
||||||
|
<string name="stop_on_internet_loss">网络丢失时停止隧道</string>
|
||||||
|
<string name="bypass_lan_for_kill_switch">绕过局域网流量</string>
|
||||||
|
<string name="auto_tunnel_channel_name">自动隧道通知频道</string>
|
||||||
|
<string name="auto_tunnel_channel_description">自动隧道状态通知频道</string>
|
||||||
|
<string name="stop">停止</string>
|
||||||
|
<string name="tunnel_specific_settings">隧道特殊设置</string>
|
||||||
|
<string name="splt_tunneling">隧道分流</string>
|
||||||
|
<string name="show_scripts">显示脚本</string>
|
||||||
|
<string name="pre_up">启动前</string>
|
||||||
|
<string name="post_up">启动后</string>
|
||||||
|
<string name="pre_down">关闭前</string>
|
||||||
|
<string name="post_down">关闭后</string>
|
||||||
|
<string name="amnezia_kernel_message">Amnezia 在内核模式中不可用</string>
|
||||||
|
<string name="enable_amnezia">开启 Amnezia</string>
|
||||||
|
<string name="wg_compat_mode">WG 兼容性模式</string>
|
||||||
|
<string name="quick_actions">快捷操作</string>
|
||||||
|
<string name="advanced_settings">高级设置</string>
|
||||||
|
<string name="debounce_delay">防抖延迟</string>
|
||||||
|
<string name="hide_amnezia_properties">隐藏 Amnezia 属性</string>
|
||||||
|
<string name="hide_scripts">隐藏脚本</string>
|
||||||
|
<string name="enable_amnezia_compatibility">开启 Amnezia 兼容性</string>
|
||||||
|
<string name="remove_amnezia_compatibility">移除 Amnezia 兼容性</string>
|
||||||
|
<string name="exclude_lan">排除局域网</string>
|
||||||
|
<string name="include_lan">包含局域网</string>
|
||||||
|
<string name="error_tunnel_start">开启隧道失败</string>
|
||||||
|
<string name="tunnel_control">隧道控制</string>
|
||||||
|
<string name="auto_tunnel">自动隧道</string>
|
||||||
|
<string name="ethernet_tunnel">以太网隧道</string>
|
||||||
|
<string name="set_ethernet_tunnel">设置为以太网隧道</string>
|
||||||
|
<string name="native_kill_switch">系统 VPN 开关</string>
|
||||||
|
<string name="vpn_kill_switch">VPN 开关</string>
|
||||||
|
<string name="kill_switch_options">开关选项</string>
|
||||||
|
<string name="allow_lan_traffic">允许局域网流量</string>
|
||||||
|
<string name="vpn_channel_description">VPN 状态通知频道</string>
|
||||||
|
<string name="kill_switch_off">在受信任网络上停止 Kill Switch</string>
|
||||||
|
<string name="prefer_ipv4">首选 IPv4 连接</string>
|
||||||
|
<string name="dns_error">解析端点 DNS 失败。</string>
|
||||||
|
<string name="unauthorized">身份验证未通过,启动流量隧道失败。</string>
|
||||||
|
<string name="tunne_start_failed_title">隧道失败</string>
|
||||||
|
<string name="multiple">多个</string>
|
||||||
|
<string name="server_ipv4">IPv4 主机名解析</string>
|
||||||
|
<string name="start_failed_config">配置文件错误,启动流量隧道失败。</string>
|
||||||
|
<string name="export_amnezia">导出为 Amnezia</string>
|
||||||
|
<string name="export_wireguard">导出为 WireGuard</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,2 +1,101 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources></resources>
|
<resources>
|
||||||
|
<string name="sec">秒</string>
|
||||||
|
<string name="error_file_extension">檔案類型不是 .conf 或 .zip</string>
|
||||||
|
<string name="language">語言</string>
|
||||||
|
<string name="interface_">界面</string>
|
||||||
|
<string name="launch_app_settings">打開應用程式設定</string>
|
||||||
|
<string name="use_kernel">使用核心模組</string>
|
||||||
|
<string name="version">版本</string>
|
||||||
|
<string name="app_name">WG Tunnel</string>
|
||||||
|
<string name="name">名稱</string>
|
||||||
|
<string name="public_key">公鑰</string>
|
||||||
|
<string name="privacy_policy">查看隱私政策</string>
|
||||||
|
<string name="tunnels">隧道列表</string>
|
||||||
|
<string name="thank_you">感謝您使用 WG Tunnel!</string>
|
||||||
|
<string name="open_file">開啟檔案</string>
|
||||||
|
<string name="mtu">MTU</string>
|
||||||
|
<string name="okay">好</string>
|
||||||
|
<string name="qr_scan">掃描 QR code</string>
|
||||||
|
<string name="dns_servers">DNS 伺服器</string>
|
||||||
|
<string name="tunnel_name">隧道名稱</string>
|
||||||
|
<string name="config_changes_saved">組態變更已儲存。</string>
|
||||||
|
<string name="exclude">排除</string>
|
||||||
|
<string name="include">包含</string>
|
||||||
|
<string name="addresses">地址</string>
|
||||||
|
<string name="add_tunnels_text">從檔案或 zip 壓縮檔新增</string>
|
||||||
|
<string name="add_from_qr">從 QR code 新增</string>
|
||||||
|
<string name="copy_public_key">複製公鑰</string>
|
||||||
|
<string name="optional_no_recommend">(可選, 不建議)</string>
|
||||||
|
<string name="optional">(可選)</string>
|
||||||
|
<string name="random">(隨機)</string>
|
||||||
|
<string name="cancel">取消</string>
|
||||||
|
<string name="private_key">私鑰</string>
|
||||||
|
<string name="listen_port">監聽連接埠</string>
|
||||||
|
<string name="export_configs">匯出組態</string>
|
||||||
|
<string name="create_import">手動建立</string>
|
||||||
|
<string name="seconds">秒</string>
|
||||||
|
<string name="docs_description">閱讀文件</string>
|
||||||
|
<string name="error_ssid_exists">SSID 已經存在</string>
|
||||||
|
<string name="delete_tunnel">刪除隧道</string>
|
||||||
|
<string name="delete_tunnel_message">您確定要刪除此隧道?</string>
|
||||||
|
<string name="yes">是</string>
|
||||||
|
<string name="error_invalid_code">無效的 QR code</string>
|
||||||
|
<string name="no_browser_detected">未安裝瀏覽器</string>
|
||||||
|
<string name="no_email_detected">未安裝電子郵件應用程式</string>
|
||||||
|
<string name="open_issue">建立新的問題</string>
|
||||||
|
<string name="auto">(自動)</string>
|
||||||
|
<string name="create_pin">建立 PIN</string>
|
||||||
|
<string name="enter_pin">輸入 PIN</string>
|
||||||
|
<string name="pin_created">成功建立 PIN</string>
|
||||||
|
<string name="incorrect_pin">PIN 不正確</string>
|
||||||
|
<string name="enable_app_lock">啟用應用程式鎖定</string>
|
||||||
|
<string name="set_primary_tunnel">設為主要隧道</string>
|
||||||
|
<string name="edit_tunnel">編輯隧道</string>
|
||||||
|
<string name="kernel">核心</string>
|
||||||
|
<string name="vpn_settings">系統 VPN 設定</string>
|
||||||
|
<string name="support">支持</string>
|
||||||
|
<string name="getting_started_guide">取得入門指南</string>
|
||||||
|
<string name="settings">設定</string>
|
||||||
|
<string name="restart_at_boot">開機時重新啟動</string>
|
||||||
|
<string name="chat_description">加入社區</string>
|
||||||
|
<string name="junk_packet_count">無效封包數</string>
|
||||||
|
<string name="set_custom_ping_internal">Ping 間隔 (秒)</string>
|
||||||
|
<string name="app_settings">應用程式設定</string>
|
||||||
|
<string name="logs">日誌</string>
|
||||||
|
<string name="dark">暗色</string>
|
||||||
|
<string name="light">亮色</string>
|
||||||
|
<string name="donate">捐贈給專案</string>
|
||||||
|
<string name="appearance">外觀</string>
|
||||||
|
<string name="display_theme">主題</string>
|
||||||
|
<string name="primary_tunnel">主要隧道</string>
|
||||||
|
<string name="learn_more">了解更多</string>
|
||||||
|
<string name="kernel_not_supported">核心不支持</string>
|
||||||
|
<string name="notifications">通知</string>
|
||||||
|
<string name="dynamic">動態</string>
|
||||||
|
<string name="never">從不</string>
|
||||||
|
<string name="automatic">自動</string>
|
||||||
|
<string name="add_wifi_name">新增WiFi SSID</string>
|
||||||
|
<string name="allow_lan_traffic">允許 LAN 流量</string>
|
||||||
|
<string name="configuration_change">組態變更</string>
|
||||||
|
<string name="stop_on_no_internet">沒有連上網路時停用</string>
|
||||||
|
<string name="stop_on_internet_loss">網路連線斷開時停止隧道</string>
|
||||||
|
<string name="add_from_clipboard">從剪貼簿新增</string>
|
||||||
|
<string name="stop">停止</string>
|
||||||
|
<string name="amnezia_kernel_message">Amnezia 無法在核心模式使用</string>
|
||||||
|
<string name="enable_amnezia">啟用 Amnezia</string>
|
||||||
|
<string name="wg_compat_mode">WG 相容性模式</string>
|
||||||
|
<string name="advanced_settings">進階設定</string>
|
||||||
|
<string name="exclude_lan">排除 LAN</string>
|
||||||
|
<string name="include_lan">包含 LAN</string>
|
||||||
|
<string name="error_tunnel_start">啟用隧道失敗</string>
|
||||||
|
<string name="unknown_error">發生未知錯誤</string>
|
||||||
|
<string name="error_file_format">無效的隧道組態檔案格式</string>
|
||||||
|
<string name="endpoint">端點</string>
|
||||||
|
<string name="location_services_not_detected">未啟用定位服務</string>
|
||||||
|
<string name="junk_packet_maximum_size">最大無效封包</string>
|
||||||
|
<string name="no_tunnels">還沒有新增任何隧道!</string>
|
||||||
|
<string name="allowed_ips">允許的 IP</string>
|
||||||
|
<string name="junk_packet_minimum_size">最小無效封包</string>
|
||||||
|
<string name="error_no_file_explorer">未安裝任何檔案管理器</string>
|
||||||
|
</resources>
|
||||||
|
|||||||
@@ -215,4 +215,11 @@
|
|||||||
<string name="multiple">Multiple</string>
|
<string name="multiple">Multiple</string>
|
||||||
<string name="export_amnezia">Export as Amnezia</string>
|
<string name="export_amnezia">Export as Amnezia</string>
|
||||||
<string name="export_wireguard">Export as WireGuard</string>
|
<string name="export_wireguard">Export as WireGuard</string>
|
||||||
|
<string name="add_from_url">Add from URL</string>
|
||||||
|
<string name="enter_config_url">Enter config URL</string>
|
||||||
|
<string name="error_download_failed">Failed to download config</string>
|
||||||
|
<string name="error_invalid_url">Invalid URL</string>
|
||||||
|
<string name="save">Save</string>
|
||||||
|
<string name="search">Search</string>
|
||||||
|
<string name="select">Select</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
object Constants {
|
object Constants {
|
||||||
const val VERSION_NAME = "3.6.6"
|
const val VERSION_NAME = "3.7.2"
|
||||||
const val JVM_TARGET = "17"
|
const val JVM_TARGET = "17"
|
||||||
const val VERSION_CODE = 36600
|
const val VERSION_CODE = 37200
|
||||||
const val TARGET_SDK = 35
|
const val TARGET_SDK = 35
|
||||||
const val MIN_SDK = 26
|
const val MIN_SDK = 26
|
||||||
const val APP_ID = "com.zaneschepke.wireguardautotunnel"
|
const val APP_ID = "com.zaneschepke.wireguardautotunnel"
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
Was ist neu?
|
||||||
|
- Die Ping-Funktion funktioniert jetzt unabhängig vom automatischen Tunnel
|
||||||
|
- Komfortaktion für Amnezia-Kompatibilität hinzugefügt
|
||||||
|
- Komfortaktion zum Ausschließen von LAN aus dem Tunnel hinzugefügt
|
||||||
|
- Option zur Einstellung der Entprellungszeit für Autotunnel hinzugefügt
|
||||||
|
- Viele Fehlerkorrekturen und Verbesserungen
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
What's new:
|
||||||
|
- Multiple tunnel support for kernel mode
|
||||||
|
- Override for WG default DNS Ipv4 preference
|
||||||
|
- Stop kill switch on trusted support
|
||||||
|
- Limit location querying by auto tunnel
|
||||||
|
- Various bug fixes and improvements
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
What's new:
|
||||||
|
- Static IPv6 peer endpoint bug fix
|
||||||
|
- Dynamic shortcuts bug fix
|
||||||
|
- Localizations
|
||||||
|
- Add peer bug fix
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
What's new:
|
||||||
|
- Auto tunnel regression fix
|
||||||
|
- Tile sync improvements
|
||||||
|
- Optimize wifi name querying
|
||||||
|
- Improve network monitoring permission checks
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
Features
|
||||||
|
|
||||||
|
- Add tunnels via .conf file, zip, manual entry, or QR code
|
||||||
|
- Auto connect to VPN based on Wi-Fi SSID, ethernet, or mobile data
|
||||||
|
- Split tunneling by application with search
|
||||||
|
- WireGuard support for kernel and userspace modes
|
||||||
|
- Amnezia support for userspace mode for DPI/censorship protection
|
||||||
|
- Always-On VPN support
|
||||||
|
- Export Amnezia and WireGuard tunnels to zip
|
||||||
|
- Quick tile support for VPN toggling
|
||||||
|
- Static shortcuts support for primary tunnel for automation integration
|
||||||
|
- Intent automation support for all tunnels
|
||||||
|
- Automatic service restart after reboot
|
||||||
|
- Battery preservation measures
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
Novità:
|
||||||
|
- aggiornamenti UI
|
||||||
|
- Migliorie navigazione AndroidTV
|
||||||
|
- Bug consumo batteria risolto
|
||||||
|
- Correzione impostazioni opzionali caratteri jolly
|
||||||
|
- Altre migliorie
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
Novità:
|
||||||
|
- Aggiungi tunnel dagli appunti
|
||||||
|
- Aggiunte localizzazioni
|
||||||
|
- Corretto bug consumo batteria
|
||||||
|
- Corretto bug cancellazione
|
||||||
|
- Migliorata sincronizzazione tile tunnel
|
||||||
|
- Altre correzioni e migliorie
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
Novità:
|
||||||
|
- Aggiunto interruttore spegnimento VPN con esclusione LAN
|
||||||
|
- Migliorata velocità e stabilità auto tunnel
|
||||||
|
- Migliorata sincronizzazione tile
|
||||||
|
- Vari bug risolti e migliorie
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
Novità:
|
||||||
|
- Corretto bug commutatore modalità kernel
|
||||||
|
- Corretto bug crash notifica
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
Novità:
|
||||||
|
- Migliorata stabilità auto tunnel
|
||||||
|
- Corretto salvataggio split tunnel e migliore prestazioni
|
||||||
|
- Corretto bug modalità kernel
|
||||||
|
- Corretto bug tile
|
||||||
|
- Vare altre correzioni e migliorie
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
Novità:
|
||||||
|
- Il ping ora funziona indipendentemente dall'auto tunnel
|
||||||
|
- Aggiunte azioni per compatibilità Amnezia
|
||||||
|
- Aggiunte azioni esclusione LAN dal tunnel
|
||||||
|
- Aggiunta opzione regolazione tempo di rimbalzo per auto tunnel
|
||||||
|
- Molte correzioni a bug e migliore
|
||||||
@@ -1 +0,0 @@
|
|||||||
Uma alternativa de cliente WireGuard VPN com recursos adicionais
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
Поліпшення:
|
||||||
|
- Виправлена помилка з дозволами на Android < 9
|
||||||
|
- Інші оптимізації
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
Поліпшення:
|
||||||
|
- Додано статистику тунелю на головний екран
|
||||||
|
- Поліпшено навігацію в екрані налаштувань на Android TV
|
||||||
|
- Видалено вібрацію при оповіщенні
|
||||||
|
- Інші виправлення
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
Поліпшення:
|
||||||
|
- Додана підтримка авто-тунелювання лише через мобільну мережу
|
||||||
|
- Покращено інтерфейс екрану підтримки
|
||||||
|
- Оновлено посилання на ресурси
|
||||||
|
- Інші виправлення помилок
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
Поліпшення:
|
||||||
|
- Базова підтримка модуля WireGuard режиму ядра
|
||||||
|
- Покращено процес розкриття інформації про місцезнаходження
|
||||||
|
- Виправлено помилку з дозволами авто-тунелю
|
||||||
|
- Інші виправлення інтерфейсу
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Виправлення:
|
||||||
|
- Дозвіл переднього плану в Android 14
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
Поліпшення:
|
||||||
|
- Рефакторинг зберігання даних додатком
|
||||||
|
- Поліпшено навігацію в інтерфейсі для Android TV
|
||||||
|
- Збільшено ефективність авто-тунелювання
|
||||||
|
- Поліпшено навігацію
|
||||||
|
- Можливість призупинити роботу авто-тунелю
|
||||||
|
- Безліч виправлень помилок
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user