Compare commits

..

14 Commits

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

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

Closes #68

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

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

Improved/refactored error handling.
2024-05-11 20:42:24 -04:00
Zane Schepke cb1b8ee7d6 add version metadata 2024-05-11 00:18:58 -04:00
Zane Schepke 4153351fc4 Merge branch 'main' of https://github.com/zaneschepke/wgtunnel 2024-05-10 23:52:43 -04:00
dependabot[bot] 48e6f341cb build(deps): bump actions/upload-artifact from 4.3.1 to 4.3.3 (#179)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-10 23:52:03 -04:00
Zane Schepke d531adede5 remove comments 2024-05-10 23:49:38 -04:00
Zane Schepke 2df1bb07ab change auto tunnel to not watch vpn state 2024-05-10 23:46:50 -04:00
Zane Schepke a5e60c3fbe add amnezia import/export 2024-05-10 21:42:59 -04:00
Weblate (bot) e4af481402 Translations update from Hosted Weblate (#206)
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
2024-05-10 12:34:10 -04:00
Weblate (bot) 77b3fc8360 Translations update from Hosted Weblate (#204)
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
2024-05-09 15:26:47 -04:00
Zane Schepke 4fd908f271 fix: notification workflow links 2024-05-08 17:30:00 -04:00
maskedeken 632da245ab fix kernel mode switch not work (#196) 2024-05-08 17:25:28 -04:00
Weblate (bot) 04f22cb92d Translations update from Hosted Weblate (#200)
Co-authored-by: Gabriel Franz <gabrielfranz31@gmail.com>
2024-05-08 13:13:58 -04:00
Weblate (bot) 31194d8b88 Translations update from Hosted Weblate (#194)
Co-authored-by: Mat1RX <ladved@gmail.com>
2024-05-05 21:39:09 -04:00
Zane Schepke 421bf418d1 fix: fastlane metadata 2024-05-05 21:36:44 -04:00
62 changed files with 992 additions and 664 deletions
+1 -1
View File
@@ -15,6 +15,6 @@ jobs:
run: |
msg_text='${{ github.actor }} updated an issue:
status: ${{ github.event.issue.state }} - #${{ github.event.issue.number }} ${{ github.event.issue.title }}
${{ github.event.issue.url }}'
https://github.com/zaneschepke/wgtunnel/issues/${{ github.event.issue.number }}'
curl -s -X POST 'https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN }}/sendMessage' \
-d "chat_id=${{ secrets.TELEGRAM_TO }}&text=${msg_text}&message_thread_id=${{ secrets.TELEGRAM_TOPIC }}"
+1 -1
View File
@@ -67,7 +67,7 @@ jobs:
echo "VERSION_CODE=$version_code" >> $GITHUB_ENV
# Save the APK after the Build job is complete to publish it as a Github release in the next job
- name: Upload APK
uses: actions/upload-artifact@v4.3.1
uses: actions/upload-artifact@v4.3.3
with:
name: wgtunnel
path: ${{ steps.apk-path.outputs.path }}
+1 -1
View File
@@ -16,6 +16,6 @@ jobs:
msg_text='${{ github.actor }} published a new release:
Release: ${{ github.event.release.tag_name }}
${{ github.event.release.body }}
${{ github.event.release.url }}'
https://github.com/zaneschepke/wgtunnel/releases/tag/${{ github.event.release.tag_name }}'
curl -s -X POST 'https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN }}/sendMessage' \
-d "chat_id=${{ secrets.TELEGRAM_TO }}&text=${msg_text}&message_thread_id=${{ secrets.TELEGRAM_TOPIC }}"
+1 -1
View File
@@ -69,7 +69,7 @@ jobs:
echo "VERSION_CODE=$version_code" >> $GITHUB_ENV
# Save the APK after the Build job is complete to publish it as a Github release in the next job
- name: Upload APK
uses: actions/upload-artifact@v4.3.1
uses: actions/upload-artifact@v4.3.3
with:
name: wgtunnel
path: ${{ steps.apk-path.outputs.path }}
+3 -2
View File
@@ -22,7 +22,7 @@ WG Tunnel
<div align="left">
This is an alternative Android Application for [WireGuard](https://www.wireguard.com/) with added
This is an alternative Android Application for [WireGuard](https://www.wireguard.com/) and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) with added
features. Built using the [wireguard-android](https://github.com/WireGuard/wireguard-android)
library and [Jetpack Compose](https://developer.android.com/jetpack/compose), this application was
inspired by the official [WireGuard Android](https://github.com/WireGuard/wireguard-android) app.
@@ -53,8 +53,9 @@ and on while on different networks. This app was created to offer a free solutio
* Auto connect to tunnels 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 tunnels to zip
* Export Amnezia and WireGuard tunnels to zip
* Quick tile support for tunnel toggling, auto-tunneling
* Static shortcuts support for tunnel toggling, auto-tunneling
* Intent automation support for all tunnels
+3
View File
@@ -139,6 +139,7 @@ dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
// helpers for implementing LifecycleOwner in a Service
implementation(libs.androidx.lifecycle.service)
implementation(libs.androidx.activity.compose)
@@ -173,6 +174,8 @@ dependencies {
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.hilt.navigation.compose)
implementation(libs.zaneschepke.multifab)
// hilt
implementation(libs.hilt.android)
ksp(libs.hilt.android.compiler)
@@ -2,7 +2,6 @@ package com.zaneschepke.wireguardautotunnel
import android.app.Application
import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager
import android.service.quicksettings.TileService
import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelControlTile
@@ -40,16 +39,16 @@ class WireGuardAutoTunnel : Application() {
return instance.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
}
fun requestTunnelTileServiceStateUpdate(context: Context) {
fun requestTunnelTileServiceStateUpdate() {
TileService.requestListeningState(
context,
instance,
ComponentName(instance, TunnelControlTile::class.java),
)
}
fun requestAutoTunnelTileServiceUpdate(context: Context) {
fun requestAutoTunnelTileServiceUpdate() {
TileService.requestListeningState(
context,
instance,
ComponentName(instance, AutoTunnelControlTile::class.java),
)
}
@@ -20,6 +20,9 @@ interface TunnelConfigDao {
@Query("SELECT * FROM TunnelConfig WHERE id=:id")
suspend fun getById(id: Long): TunnelConfig?
@Query("SELECT * FROM TunnelConfig WHERE name=:name")
suspend fun getByName(name: String) : TunnelConfig?
@Query("SELECT * FROM TunnelConfig")
suspend fun getAll(): TunnelConfigs
@@ -31,7 +31,7 @@ data class TunnelConfig(
name = "am_quick",
defaultValue = "",
)
val amQuick: String = "",
val amQuick: String = AM_QUICK_DEFAULT,
) {
companion object {
fun configFromWgQuick(wgQuick: String): Config {
@@ -46,5 +46,6 @@ data class TunnelConfig(
org.amnezia.awg.config.Config.parse(it)
}
}
const val AM_QUICK_DEFAULT = ""
}
}
@@ -54,6 +54,10 @@ class RoomTunnelConfigRepository(private val tunnelConfigDao: TunnelConfigDao) :
return tunnelConfigDao.count().toInt()
}
override suspend fun findByTunnelName(name: String): TunnelConfig? {
return tunnelConfigDao.getByName(name)
}
override suspend fun findByTunnelNetworksName(name: String): TunnelConfigs {
return tunnelConfigDao.findByTunnelNetworkName(name)
}
@@ -22,6 +22,8 @@ interface TunnelConfigRepository {
suspend fun count(): Int
suspend fun findByTunnelName(name : String) : TunnelConfig?
suspend fun findByTunnelNetworksName(name: String): TunnelConfigs
suspend fun findByMobileDataTunnel(): TunnelConfigs
@@ -1,84 +1,63 @@
package com.zaneschepke.wireguardautotunnel.service.foreground
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
data class WatcherState(
val isWifiConnected: Boolean = false,
val config: TunnelConfig? = null,
val vpnStatus: TunnelState = TunnelState.DOWN,
val isEthernetConnected: Boolean = false,
val isMobileDataConnected: Boolean = false,
val currentNetworkSSID: String = "",
val settings: Settings = Settings()
) {
private fun isVpnConnected() = vpnStatus == TunnelState.UP
fun isEthernetConditionMet(): Boolean {
return (isEthernetConnected &&
settings.isTunnelOnEthernetEnabled &&
!isVpnConnected())
settings.isTunnelOnEthernetEnabled)
}
fun isMobileDataConditionMet(): Boolean {
return (!isEthernetConnected &&
settings.isTunnelOnMobileDataEnabled &&
!isWifiConnected &&
isMobileDataConnected &&
!isVpnConnected())
isMobileDataConnected)
}
fun isTunnelNotMobileDataPreferredConditionMet(): Boolean {
fun isTunnelOnMobileDataPreferredConditionMet(): Boolean {
return (!isEthernetConnected &&
settings.isTunnelOnMobileDataEnabled &&
!isWifiConnected &&
isMobileDataConnected &&
config?.isMobileDataTunnel == false && isVpnConnected())
isMobileDataConnected)
}
fun isTunnelOffOnMobileDataConditionMet(): Boolean {
return (!isEthernetConnected &&
!settings.isTunnelOnMobileDataEnabled &&
isMobileDataConnected &&
!isWifiConnected &&
isVpnConnected())
!isWifiConnected)
}
fun isUntrustedWifiConditionMet(): Boolean {
return (!isEthernetConnected &&
isWifiConnected &&
!settings.trustedNetworkSSIDs.contains(currentNetworkSSID) &&
settings.isTunnelOnWifiEnabled
&& !isVpnConnected())
}
fun isTunnelNotWifiNamePreferredMet(ssid: String): Boolean {
return (!isEthernetConnected &&
isWifiConnected &&
!settings.trustedNetworkSSIDs.contains(currentNetworkSSID) &&
settings.isTunnelOnWifiEnabled && config?.tunnelNetworks?.contains(ssid) == false && isVpnConnected())
settings.isTunnelOnWifiEnabled)
}
fun isTrustedWifiConditionMet(): Boolean {
return (!isEthernetConnected &&
(isWifiConnected &&
settings.trustedNetworkSSIDs.contains(currentNetworkSSID)) &&
(isVpnConnected()))
settings.trustedNetworkSSIDs.contains(currentNetworkSSID)))
}
fun isTunnelOffOnWifiConditionMet(): Boolean {
return (!isEthernetConnected &&
(isWifiConnected &&
!settings.isTunnelOnWifiEnabled &&
(isVpnConnected())))
!settings.isTunnelOnWifiEnabled))
}
fun isTunnelOffOnNoConnectivityMet(): Boolean {
return (!isEthernetConnected &&
!isWifiConnected &&
!isMobileDataConnected &&
(isVpnConnected()))
!isMobileDataConnected)
}
}
@@ -24,6 +24,7 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
import java.net.InetAddress
@@ -162,10 +163,6 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
watchForEthernetConnectivityChanges()
}
}
launch {
Timber.i("Starting vpn state watcher")
watchForVpnConnectivityChanges()
}
launch {
Timber.i("Starting settings watcher")
watchForSettingsChanges()
@@ -185,29 +182,32 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
}
private suspend fun watchForMobileDataConnectivityChanges() {
mobileDataService.networkStatus.collect {
when (it) {
mobileDataService.networkStatus.collect { status ->
when (status) {
is NetworkStatus.Available -> {
Timber.i("Gained Mobile data connection")
networkEventsFlow.value =
networkEventsFlow.value.copy(
networkEventsFlow.update {
it.copy(
isMobileDataConnected = true,
)
}
}
is NetworkStatus.CapabilitiesChanged -> {
networkEventsFlow.value =
networkEventsFlow.value.copy(
networkEventsFlow.update {
it.copy(
isMobileDataConnected = true,
)
}
Timber.i("Mobile data capabilities changed")
}
is NetworkStatus.Unavailable -> {
networkEventsFlow.value =
networkEventsFlow.value.copy(
networkEventsFlow.update {
it.copy(
isMobileDataConnected = false,
)
}
Timber.i("Lost mobile data connection")
}
}
@@ -225,7 +225,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
val host = if (peer.endpoint.isPresent &&
peer.endpoint.get().resolved.isPresent)
peer.endpoint.get().resolved.get().host
else Constants.BACKUP_PING_HOST
else Constants.DEFAULT_PING_IP
Timber.i("Checking reachability of: $host")
val reachable = InetAddress.getByName(host)
.isReachable(Constants.PING_TIMEOUT.toInt())
@@ -236,7 +236,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
Timber.i("Restarting VPN for ping failure")
serviceManager.stopVpnServiceForeground(this)
delay(Constants.VPN_RESTART_DELAY)
serviceManager.startVpnServiceForeground(this)
serviceManager.startVpnServiceForeground(this, it.id)
delay(Constants.PING_COOLDOWN)
}
}
@@ -249,54 +249,48 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
}
private suspend fun watchForSettingsChanges() {
appDataRepository.settings.getSettingsFlow().collect {
if (networkEventsFlow.value.settings.isAutoTunnelPaused != it.isAutoTunnelPaused) {
when (it.isAutoTunnelPaused) {
appDataRepository.settings.getSettingsFlow().collect { settings ->
if (networkEventsFlow.value.settings.isAutoTunnelPaused != settings.isAutoTunnelPaused) {
when (settings.isAutoTunnelPaused) {
true -> launchWatcherPausedNotification()
false -> launchWatcherNotification()
}
}
networkEventsFlow.value =
networkEventsFlow.value.copy(
settings = it,
)
}
}
private suspend fun watchForVpnConnectivityChanges() {
vpnService.vpnState.collect {
networkEventsFlow.value =
networkEventsFlow.value.copy(
vpnStatus = it.status,
config = it.tunnelConfig,
networkEventsFlow.update {
it.copy(
settings = settings,
)
}
}
}
private suspend fun watchForEthernetConnectivityChanges() {
ethernetService.networkStatus.collect {
when (it) {
ethernetService.networkStatus.collect { status ->
when (status) {
is NetworkStatus.Available -> {
Timber.i("Gained Ethernet connection")
networkEventsFlow.value =
networkEventsFlow.value.copy(
networkEventsFlow.update {
it.copy(
isEthernetConnected = true,
)
}
}
is NetworkStatus.CapabilitiesChanged -> {
Timber.i("Ethernet capabilities changed")
networkEventsFlow.value =
networkEventsFlow.value.copy(
networkEventsFlow.update {
it.copy(
isEthernetConnected = true,
)
}
}
is NetworkStatus.Unavailable -> {
networkEventsFlow.value =
networkEventsFlow.value.copy(
networkEventsFlow.update {
it.copy(
isEthernetConnected = false,
)
}
Timber.i("Lost Ethernet connection")
}
}
@@ -304,40 +298,43 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
}
private suspend fun watchForWifiConnectivityChanges() {
wifiService.networkStatus.collect {
when (it) {
wifiService.networkStatus.collect { status ->
when (status) {
is NetworkStatus.Available -> {
Timber.i("Gained Wi-Fi connection")
networkEventsFlow.value =
networkEventsFlow.value.copy(
networkEventsFlow.update {
it.copy(
isWifiConnected = true,
)
}
}
is NetworkStatus.CapabilitiesChanged -> {
Timber.i("Wifi capabilities changed")
networkEventsFlow.value =
networkEventsFlow.value.copy(
networkEventsFlow.update {
it.copy(
isWifiConnected = true,
)
val ssid = wifiService.getNetworkName(it.networkCapabilities)
}
val ssid = wifiService.getNetworkName(status.networkCapabilities)
ssid?.let { name ->
if(name.contains(Constants.UNREADABLE_SSID)) {
Timber.w("SSID unreadable: missing permissions")
} else Timber.i("Detected valid SSID")
appDataRepository.appState.setCurrentSsid(name)
networkEventsFlow.value =
networkEventsFlow.value.copy(
networkEventsFlow.update {
it.copy(
currentNetworkSSID = name,
)
}
} ?: Timber.w("Failed to read ssid")
}
is NetworkStatus.Unavailable -> {
networkEventsFlow.value =
networkEventsFlow.value.copy(
networkEventsFlow.update {
it.copy(
isWifiConnected = false,
)
}
Timber.i("Lost Wi-Fi connection")
}
}
@@ -352,72 +349,74 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
return appDataRepository.tunnels.findByTunnelNetworksName(ssid).firstOrNull()
}
private fun isTunnelDown() : Boolean {
return vpnService.vpnState.value.status == TunnelState.DOWN
}
private suspend fun manageVpn() {
networkEventsFlow.collectLatest { watcherState ->
val autoTunnel = "Auto-tunnel watcher"
if (!watcherState.settings.isAutoTunnelPaused) {
//delay for rapid network state changes and then collect latest
delay(Constants.WATCHER_COLLECTION_DELAY)
val tunnelConfig = vpnService.vpnState.value.tunnelConfig
when {
watcherState.isEthernetConditionMet() -> {
Timber.i("$autoTunnel - tunnel on on ethernet condition met")
serviceManager.startVpnServiceForeground(this)
if(isTunnelDown()) serviceManager.startVpnServiceForeground(this)
}
watcherState.isMobileDataConditionMet() -> {
Timber.i("$autoTunnel - tunnel on on mobile data condition met")
serviceManager.startVpnServiceForeground(this, getMobileDataTunnel()?.id)
if(isTunnelDown()) serviceManager.startVpnServiceForeground(this, getMobileDataTunnel()?.id)
}
watcherState.isTunnelNotMobileDataPreferredConditionMet() -> {
getMobileDataTunnel()?.let {
Timber.i("$autoTunnel - tunnel connected on mobile data is not preferred condition met, switching to preferred")
serviceManager.startVpnServiceForeground(
this,
getMobileDataTunnel()?.id,
)
watcherState.isTunnelOnMobileDataPreferredConditionMet() -> {
if(tunnelConfig?.isMobileDataTunnel == false) {
getMobileDataTunnel()?.let {
Timber.i("$autoTunnel - tunnel connected on mobile data is not preferred condition met, switching to preferred")
if(isTunnelDown()) serviceManager.startVpnServiceForeground(
this,
getMobileDataTunnel()?.id,
)
}
}
}
watcherState.isTunnelOffOnMobileDataConditionMet() -> {
Timber.i("$autoTunnel - tunnel off on mobile data met, turning vpn off")
serviceManager.stopVpnServiceForeground(this)
}
watcherState.isTunnelNotWifiNamePreferredMet(watcherState.currentNetworkSSID) -> {
Timber.i("$autoTunnel - tunnel on ssid not associated with current tunnel condition met")
getSsidTunnel(watcherState.currentNetworkSSID)?.let {
Timber.i("Found tunnel associated with this SSID, bringing tunnel up")
serviceManager.startVpnServiceForeground(this, it.id)
} ?: suspend {
Timber.i("No tunnel associated with this SSID, using defaults")
if (appDataRepository.getPrimaryOrFirstTunnel()?.name != vpnService.name) {
serviceManager.startVpnServiceForeground(this)
}
}.invoke()
if(!isTunnelDown()) serviceManager.stopVpnServiceForeground(this)
}
watcherState.isUntrustedWifiConditionMet() -> {
Timber.i("$autoTunnel - tunnel on untrusted wifi condition met")
serviceManager.startVpnServiceForeground(
this,
getSsidTunnel(watcherState.currentNetworkSSID)?.id,
)
if(tunnelConfig?.tunnelNetworks?.contains(watcherState.currentNetworkSSID) == false ||
tunnelConfig == null) {
Timber.i("$autoTunnel - tunnel on ssid not associated with current tunnel condition met")
getSsidTunnel(watcherState.currentNetworkSSID)?.let {
Timber.i("Found tunnel associated with this SSID, bringing tunnel up")
if(isTunnelDown()) serviceManager.startVpnServiceForeground(this, it.id)
} ?: suspend {
Timber.i("No tunnel associated with this SSID, using defaults")
if (appDataRepository.getPrimaryOrFirstTunnel()?.name != vpnService.name) {
if(isTunnelDown()) serviceManager.startVpnServiceForeground(this)
}
}.invoke()
}
}
watcherState.isTrustedWifiConditionMet() -> {
Timber.i("$autoTunnel - tunnel off on trusted wifi condition met, turning vpn off")
serviceManager.stopVpnServiceForeground(this)
if(!isTunnelDown()) serviceManager.stopVpnServiceForeground(this)
}
watcherState.isTunnelOffOnWifiConditionMet() -> {
Timber.i("$autoTunnel - tunnel off on wifi condition met, turning vpn off")
serviceManager.stopVpnServiceForeground(this)
if(!isTunnelDown()) serviceManager.stopVpnServiceForeground(this)
}
watcherState.isTunnelOffOnNoConnectivityMet() -> {
Timber.i("$autoTunnel - tunnel off on no connectivity met, turning vpn off")
serviceManager.stopVpnServiceForeground(this)
if(!isTunnelDown()) serviceManager.stopVpnServiceForeground(this)
}
else -> {
@@ -5,14 +5,12 @@ import android.content.Intent
import android.os.Bundle
import androidx.core.app.ServiceCompat
import androidx.lifecycle.lifecycleScope
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.handshakeStatus
@@ -52,7 +52,6 @@ class TunnelControlTile : TileService() {
setTileDescription(it.name)
} ?: setUnavailable()
}
else -> setInactive()
}
}
@@ -22,7 +22,6 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import org.amnezia.awg.backend.Tunnel
import org.amnezia.awg.config.Config
import timber.log.Timber
import javax.inject.Inject
@@ -130,10 +129,15 @@ constructor(
)
}
private fun resetVpnState() {
_vpnState.tryEmit(VpnState())
}
override suspend fun stopTunnel() {
try {
if (getState() == TunnelState.UP) {
val state = setState(null, TunnelState.DOWN)
resetVpnState()
emitTunnelState(state)
}
} catch (e: BackendException) {
@@ -160,7 +164,7 @@ constructor(
private fun handleStateChange(state: TunnelState) {
val tunnel = this
emitTunnelState(state)
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(WireGuardAutoTunnel.instance)
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
if (state == TunnelState.UP) {
statsJob =
scope.launch {
@@ -1,6 +1,5 @@
package com.zaneschepke.wireguardautotunnel.ui
import android.app.Application
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
@@ -9,7 +8,6 @@ import android.widget.Toast
import androidx.compose.runtime.mutableStateListOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.journeyapps.barcodescanner.BarcodeEncoder
import com.wireguard.android.backend.GoBackend
import com.zaneschepke.logcatter.Logcatter
import com.zaneschepke.logcatter.model.LogMessage
@@ -32,10 +32,12 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
@@ -47,6 +49,7 @@ import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar
import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.options.OptionsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.pinlock.PinLockScreen
@@ -84,7 +87,7 @@ class MainActivity : AppCompatActivity() {
// load preferences into memory and init data
lifecycleScope.launch {
dataStoreManager.init()
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(this@MainActivity)
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
val settings = settingsRepository.getSettings()
if (settings.isAutoTunnelEnabled) {
serviceManager.startWatcherService(application.applicationContext)
@@ -236,14 +239,28 @@ class MainActivity : AppCompatActivity() {
composable(Screen.Support.Logs.route) {
LogsScreen(appViewModel)
}
composable("${Screen.Config.route}/{id}") {
//TODO fix navigation for amnezia
composable("${Screen.Config.route}/{id}?configType={configType}", arguments =
listOf(
navArgument("id") {
type = NavType.StringType
defaultValue = "0"
},
navArgument("configType") {
type = NavType.StringType
defaultValue = ConfigType.WIREGUARD.name
}
)
) {
val id = it.arguments?.getString("id")
val configType = ConfigType.valueOf( it.arguments?.getString("configType") ?: ConfigType.WIREGUARD.name)
if (!id.isNullOrBlank()) {
ConfigScreen(
navController = navController,
tunnelId = id,
appViewModel = appViewModel,
focusRequester = focusRequester,
configType = configType
)
}
}
@@ -4,11 +4,9 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Home
import androidx.compose.material.icons.rounded.QuestionMark
import androidx.compose.material.icons.rounded.Settings
import androidx.compose.ui.res.stringResource
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
import com.zaneschepke.wireguardautotunnel.util.StringValue
sealed class Screen(val route: String) {
data object Main : Screen("main") {
@@ -79,9 +79,9 @@ import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.Event
import com.zaneschepke.wireguardautotunnel.util.Result
import com.zaneschepke.wireguardautotunnel.util.getMessage
import kotlinx.coroutines.delay
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@@ -94,7 +94,8 @@ fun ConfigScreen(
focusRequester: FocusRequester,
navController: NavController,
appViewModel: AppViewModel,
tunnelId: String
tunnelId: String,
configType: ConfigType
) {
val context = LocalContext.current
val clipboardManager: ClipboardManager = LocalClipboardManager.current
@@ -148,11 +149,11 @@ fun ConfigScreen(
},
onError = {
showAuthPrompt = false
appViewModel.showSnackbarMessage(Event.Error.AuthenticationFailed.message)
appViewModel.showSnackbarMessage(context.getString(R.string.error_authentication_failed))
},
onFailure = {
showAuthPrompt = false
appViewModel.showSnackbarMessage(Event.Error.AuthorizationFailed.message)
appViewModel.showSnackbarMessage(context.getString(R.string.error_authorization_failed))
},
)
}
@@ -319,15 +320,11 @@ fun ConfigScreen(
}
},
onClick = {
viewModel.onSaveAllChanges().let {
when (it) {
is Result.Success -> {
appViewModel.showSnackbarMessage(it.data.message)
navController.navigate(Screen.Main.route)
}
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
}
viewModel.onSaveAllChanges(configType).onSuccess {
appViewModel.showSnackbarMessage(context.getString(R.string.config_changes_saved))
navController.navigate(Screen.Main.route)
}.onFailure {
appViewModel.showSnackbarMessage(it.getMessage(context))
}
},
containerColor = fobColor,
@@ -486,7 +483,7 @@ fun ConfigScreen(
modifier = Modifier.width(IntrinsicSize.Min),
)
}
if(uiState.isAmneziaEnabled) {
if(configType == ConfigType.AMNEZIA) {
ConfigurationTextBox(
value = uiState.interfaceProxy.junkPacketCount,
onValueChange = { value -> viewModel.onJunkPacketCountChanged(value) },
@@ -557,16 +554,6 @@ fun ConfigScreen(
.fillMaxWidth()
.focusRequester(focusRequester),
)
ConfigurationTextBox(
value = uiState.interfaceProxy.transportPacketMagicHeader,
onValueChange = { value -> viewModel.onTransportPacketMagicHeader(value) },
keyboardActions = keyboardActions,
label = stringResource(R.string.transport_packet_magic_header),
hint = stringResource(R.string.transport_packet_magic_header).lowercase(),
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
)
ConfigurationTextBox(
value = uiState.interfaceProxy.underloadPacketMagicHeader,
onValueChange = { value -> viewModel.onUnderloadPacketMagicHeader(value) },
@@ -577,6 +564,16 @@ fun ConfigScreen(
.fillMaxWidth()
.focusRequester(focusRequester),
)
ConfigurationTextBox(
value = uiState.interfaceProxy.transportPacketMagicHeader,
onValueChange = { value -> viewModel.onTransportPacketMagicHeader(value) },
keyboardActions = keyboardActions,
label = stringResource(R.string.transport_packet_magic_header),
hint = stringResource(R.string.transport_packet_magic_header).lowercase(),
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
)
}
Row(
verticalAlignment = Alignment.CenterVertically,
@@ -1,7 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.config
import android.Manifest
import android.app.Application
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.os.Build
@@ -12,15 +11,17 @@ import com.wireguard.config.Interface
import com.wireguard.config.Peer
import com.wireguard.crypto.Key
import com.wireguard.crypto.KeyPair
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.ui.screens.config.model.PeerProxy
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.Event
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
import com.zaneschepke.wireguardautotunnel.util.Result
import com.zaneschepke.wireguardautotunnel.util.StringValue
import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions
import com.zaneschepke.wireguardautotunnel.util.removeAt
import com.zaneschepke.wireguardautotunnel.util.update
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -36,12 +37,11 @@ import javax.inject.Inject
class ConfigViewModel
@Inject
constructor(
private val application: Application,
private val settingsRepository: SettingsRepository,
private val appDataRepository: AppDataRepository
) : ViewModel() {
private val packageManager = application.packageManager
private val packageManager = WireGuardAutoTunnel.instance.packageManager
private val _uiState = MutableStateFlow(ConfigUiState())
val uiState = _uiState.asStateFlow()
@@ -108,7 +108,7 @@ constructor(
}
fun getPackageLabel(packageInfo: PackageInfo): String {
return packageInfo.applicationInfo.loadLabel(application.packageManager).toString()
return packageInfo.applicationInfo.loadLabel(packageManager).toString()
}
private fun getAllInternetCapablePackages(): List<PackageInfo> {
@@ -137,7 +137,7 @@ constructor(
viewModelScope.launch {
if (tunnelConfig != null) {
saveConfig(tunnelConfig).join()
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(application)
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
}
}
@@ -248,27 +248,33 @@ constructor(
return org.amnezia.awg.config.Config.Builder().addPeers(peerList).setInterface(amInterface).build()
}
fun onSaveAllChanges(): Result<Event> {
fun onSaveAllChanges(configType: ConfigType): Result<Unit> {
return try {
val config = buildConfig()
val wgQuick = buildConfig().toWgQuickString()
val amQuick = if(configType == ConfigType.AMNEZIA) {
buildAmConfig().toAwgQuickString()
} else TunnelConfig.AM_QUICK_DEFAULT
val tunnelConfig = when (uiState.value.tunnel) {
null -> TunnelConfig(
name = _uiState.value.tunnelName,
wgQuick = config.toWgQuickString(),
wgQuick = wgQuick,
amQuick = amQuick
)
else -> uiState.value.tunnel!!.copy(
name = _uiState.value.tunnelName,
wgQuick = config.toWgQuickString(),
amQuick = if(uiState.value.isAmneziaEnabled) buildAmConfig().toAwgQuickString()
else _uiState.value.tunnel?.amQuick ?: ""
wgQuick = wgQuick,
amQuick = amQuick
)
}
updateTunnelConfig(tunnelConfig)
Result.Success(Event.Message.ConfigSaved)
Result.success(Unit)
} catch (e: Exception) {
Timber.e(e)
val message = e.message?.substringAfter(":", missingDelimiterValue = "")
Result.Error(Event.Error.ConfigParseError(message ?: ""))
val stringValue = message?.let {
StringValue.DynamicString(message)
} ?: StringValue.StringResource(R.string.unknown_error)
Result.failure(WgTunnelExceptions.ConfigParseError(stringValue))
}
}
@@ -55,8 +55,8 @@ data class InterfaceProxy(
responsePacketJunkSize = if(i.responsePacketJunkSize.isPresent) i.responsePacketJunkSize.get().toString() else "",
initPacketMagicHeader = if(i.initPacketMagicHeader.isPresent) i.initPacketMagicHeader.get().toString() else "",
responsePacketMagicHeader = if(i.responsePacketMagicHeader.isPresent) i.responsePacketMagicHeader.get().toString() else "",
underloadPacketMagicHeader = if(i.underloadPacketMagicHeader.isPresent) i.underloadPacketMagicHeader.get().toString() else "",
transportPacketMagicHeader = if(i.transportPacketMagicHeader.isPresent) i.transportPacketMagicHeader.get().toString() else "",
underloadPacketMagicHeader = if(i.underloadPacketMagicHeader.isPresent) i.underloadPacketMagicHeader.get().toString() else "",
)
}
}
@@ -0,0 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main
enum class ConfigType {
AMNEZIA,
WIREGUARD
}
@@ -34,7 +34,6 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Create
import androidx.compose.material.icons.filled.FileOpen
import androidx.compose.material.icons.filled.QrCode
import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.Bolt
import androidx.compose.material.icons.rounded.Circle
import androidx.compose.material.icons.rounded.CopyAll
@@ -46,7 +45,6 @@ import androidx.compose.material.icons.rounded.Star
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FabPosition
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@@ -91,6 +89,10 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import com.iamageo.multifablibrary.FabIcon
import com.iamageo.multifablibrary.FabOption
import com.iamageo.multifablibrary.MultiFabItem
import com.iamageo.multifablibrary.MultiFloatingActionButton
import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanOptions
import com.zaneschepke.wireguardautotunnel.R
@@ -106,16 +108,13 @@ import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
import com.zaneschepke.wireguardautotunnel.ui.theme.corn
import com.zaneschepke.wireguardautotunnel.ui.theme.mint
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.Event
import com.zaneschepke.wireguardautotunnel.util.Result
import com.zaneschepke.wireguardautotunnel.util.getMessage
import com.zaneschepke.wireguardautotunnel.util.handshakeStatus
import com.zaneschepke.wireguardautotunnel.util.mapPeerStats
import com.zaneschepke.wireguardautotunnel.util.truncateWithEllipsis
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.Timer
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@@ -133,6 +132,7 @@ fun MainScreen(
val sheetState = rememberModalBottomSheetState()
var showBottomSheet by remember { mutableStateOf(false) }
var configType by remember { mutableStateOf(ConfigType.WIREGUARD) }
// Nested scroll for control FAB
val nestedScrollConnection = remember {
@@ -193,7 +193,7 @@ fun MainScreen(
name.startsWith(Constants.ANDROID_TV_EXPLORER_STUB)
}
) {
appViewModel.showSnackbarMessage(Event.Error.FileExplorerRequired.message)
appViewModel.showSnackbarMessage(context.getString(R.string.error_no_file_explorer))
}
return intent
}
@@ -201,11 +201,8 @@ fun MainScreen(
) { data ->
if (data == null) return@rememberLauncherForActivityResult
scope.launch {
viewModel.onTunnelFileSelected(data).let {
when (it) {
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
is Result.Success -> {}
}
viewModel.onTunnelFileSelected(data, configType, context).onFailure {
appViewModel.showSnackbarMessage(it.getMessage(context))
}
}
}
@@ -215,11 +212,8 @@ fun MainScreen(
onResult = {
if (it.contents != null) {
scope.launch {
viewModel.onTunnelQrResult(it.contents).let { result ->
when (result) {
is Result.Success -> {}
is Result.Error -> appViewModel.showSnackbarMessage(result.error.message)
}
viewModel.onTunnelQrResult(it.contents, configType).onFailure {
appViewModel.showSnackbarMessage(it.getMessage(context))
}
}
}
@@ -232,7 +226,7 @@ fun MainScreen(
confirmButton = {
TextButton(
onClick = {
selectedTunnel?.let { viewModel.onDelete(it) }
selectedTunnel?.let { viewModel.onDelete(it, context) }
showDeleteTunnelAlertDialog = false
selectedTunnel = null
},
@@ -252,7 +246,7 @@ fun MainScreen(
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) {
if (appViewModel.isRequiredPermissionGranted()) {
if (checked) viewModel.onTunnelStart(tunnel) else viewModel.onTunnelStop()
if (checked) viewModel.onTunnelStart(tunnel, context) else viewModel.onTunnelStop(context)
}
}
@@ -260,6 +254,19 @@ fun MainScreen(
return LoadingScreen()
}
fun launchQrScanner() {
val scanOptions = ScanOptions()
scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE)
scanOptions.setOrientationLocked(true)
scanOptions.setPrompt(
context.getString(R.string.scanning_qr),
)
scanOptions.setBeepEnabled(false)
scanOptions.captureActivity =
CaptureActivityPortrait::class.java
scanLauncher.launch(scanOptions)
}
Scaffold(
modifier =
Modifier.pointerInput(Unit) {
@@ -282,7 +289,7 @@ fun MainScreen(
val secondaryColor = MaterialTheme.colorScheme.secondary
val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
var fobColor by remember { mutableStateOf(secondaryColor) }
FloatingActionButton(
MultiFloatingActionButton(
modifier =
(if (
WireGuardAutoTunnel.isRunningOnAndroidTv() &&
@@ -295,16 +302,42 @@ fun MainScreen(
fobColor = if (it.isFocused) hoverColor else secondaryColor
}
},
onClick = { showBottomSheet = true },
containerColor = fobColor,
fabIcon = FabIcon(
iconRes = R.drawable.add,
iconResAfterRotate = R.drawable.close,
iconRotate = 180f
),
fabOption = FabOption(
iconTint = MaterialTheme.colorScheme.background,
backgroundTint = MaterialTheme.colorScheme.primary,
),
itemsMultiFab = listOf(
MultiFabItem(
label = {
Text(
stringResource(id = R.string.amnezia),
color = Color.White,
textAlign = TextAlign.Center,
modifier = Modifier.padding(end = 10.dp)
)
},
icon = R.drawable.add,
value = ConfigType.AMNEZIA.name,
),
MultiFabItem(
label = {
Text(stringResource(id = R.string.wireguard), color = Color.White, textAlign = TextAlign.Center, modifier = Modifier.padding(end = 10.dp))
},
icon = R.drawable.add,
value = ConfigType.WIREGUARD.name
),
),
onFabItemClicked = {
showBottomSheet = true
configType = ConfigType.valueOf(it.value)
},
shape = RoundedCornerShape(16.dp),
) {
Icon(
imageVector = Icons.Rounded.Add,
contentDescription = stringResource(id = R.string.add_tunnel),
tint = Color.DarkGray,
)
}
)
}
},
) {
@@ -343,16 +376,7 @@ fun MainScreen(
.clickable {
scope.launch {
showBottomSheet = false
val scanOptions = ScanOptions()
scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE)
scanOptions.setOrientationLocked(true)
scanOptions.setPrompt(
context.getString(R.string.scanning_qr),
)
scanOptions.setBeepEnabled(false)
scanOptions.captureActivity =
CaptureActivityPortrait::class.java
scanLauncher.launch(scanOptions)
launchQrScanner()
}
}
.padding(10.dp),
@@ -376,7 +400,7 @@ fun MainScreen(
.clickable {
showBottomSheet = false
navController.navigate(
"${Screen.Config.route}/${Constants.MANUAL_TUNNEL_CONFIG_ID}",
"${Screen.Config.route}/${Constants.MANUAL_TUNNEL_CONFIG_ID}?configType=${configType}",
)
}
.padding(10.dp),
@@ -544,7 +568,7 @@ fun MainScreen(
(uiState.vpnState.status == TunnelState.UP) &&
(tunnel.name == uiState.vpnState.tunnelConfig?.name)
) {
appViewModel.showSnackbarMessage(Event.Message.TunnelOffAction.message)
appViewModel.showSnackbarMessage(context.getString(R.string.turn_off_tunnel))
return@RowListItem
}
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
@@ -578,7 +602,7 @@ fun MainScreen(
!uiState.settings.isAutoTunnelPaused
) {
appViewModel.showSnackbarMessage(
Event.Message.AutoTunnelOffAction.message,
context.getString(R.string.turn_off_tunnel),
)
} else {
navController.navigate(
@@ -633,7 +657,7 @@ fun MainScreen(
onClick = {
if (uiState.settings.isAutoTunnelEnabled) {
appViewModel.showSnackbarMessage(
Event.Message.AutoTunnelOffAction.message,
context.getString(R.string.turn_off_auto),
)
} else {
selectedTunnel = tunnel
@@ -659,7 +683,7 @@ fun MainScreen(
expanded.value = !expanded.value
} else {
appViewModel.showSnackbarMessage(
Event.Message.TunnelOnAction.message,
context.getString(R.string.turn_on_tunnel),
)
}
},
@@ -680,7 +704,7 @@ fun MainScreen(
tunnel.name == uiState.vpnState.tunnelConfig?.name
) {
appViewModel.showSnackbarMessage(
Event.Message.TunnelOffAction.message,
context.getString(R.string.turn_off_tunnel),
)
} else {
selectedTunnel = tunnel
@@ -1,6 +1,5 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main
import android.app.Application
import android.content.Context
import android.database.Cursor
import android.net.Uri
@@ -15,9 +14,9 @@ import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.Event
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
import com.zaneschepke.wireguardautotunnel.util.Result
import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions
import com.zaneschepke.wireguardautotunnel.util.toWgQuickString
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
@@ -34,7 +33,6 @@ import javax.inject.Inject
class MainViewModel
@Inject
constructor(
private val application: Application,
private val appDataRepository: AppDataRepository,
private val serviceManager: ServiceManager,
val vpnService: VpnService
@@ -54,21 +52,21 @@ constructor(
MainUiState(),
)
private fun stopWatcherService() =
private fun stopWatcherService(context: Context) =
viewModelScope.launch(Dispatchers.IO) {
serviceManager.stopWatcherService(application.applicationContext)
serviceManager.stopWatcherService(context)
}
fun onDelete(tunnel: TunnelConfig) {
fun onDelete(tunnel: TunnelConfig, context: Context) {
viewModelScope.launch(Dispatchers.IO) {
val settings = appDataRepository.settings.getSettings()
val isPrimary = tunnel.isPrimaryTunnel
if (appDataRepository.tunnels.count() == 1 || isPrimary) {
stopWatcherService()
stopWatcherService(context)
resetTunnelSetting(settings)
}
appDataRepository.tunnels.delete(tunnel)
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(application)
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
}
}
@@ -81,80 +79,133 @@ constructor(
)
}
fun onTunnelStart(tunnelConfig: TunnelConfig) =
fun onTunnelStart(tunnelConfig: TunnelConfig, context: Context) =
viewModelScope.launch(Dispatchers.IO) {
Timber.d("On start called!")
serviceManager.startVpnService(
application.applicationContext,
context,
tunnelConfig.id,
isManualStart = true,
)
}
fun onTunnelStop() =
fun onTunnelStop(context: Context) =
viewModelScope.launch(Dispatchers.IO) {
Timber.i("Stopping active tunnel")
serviceManager.stopVpnService(application.applicationContext, isManualStop = true)
serviceManager.stopVpnService(context, isManualStop = true)
}
private fun validateConfigString(config: String) {
TunnelConfig.configFromWgQuick(config)
private fun validateConfigString(config: String, configType: ConfigType) {
when(configType) {
ConfigType.AMNEZIA -> TunnelConfig.configFromAmQuick(config)
ConfigType.WIREGUARD -> TunnelConfig.configFromWgQuick(config)
}
}
suspend fun onTunnelQrResult(result: String): Result<Unit> {
private fun generateQrCodeDefaultName(config : String, configType: ConfigType) : String {
return try {
validateConfigString(result)
val tunnelConfig =
TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = result)
when(configType) {
ConfigType.AMNEZIA -> {
TunnelConfig.configFromAmQuick(config).peers[0].endpoint.get().host
}
ConfigType.WIREGUARD -> {
TunnelConfig.configFromWgQuick(config).peers[0].endpoint.get().host
}
}
} catch (e : Exception) {
Timber.e(e)
NumberUtils.generateRandomTunnelName()
}
}
private fun generateQrCodeTunnelName(config : String, configType: ConfigType) : String {
var defaultName = generateQrCodeDefaultName(config, configType)
val lines = config.lines().toMutableList()
val linesIterator = lines.iterator()
while(linesIterator.hasNext()) {
val next = linesIterator.next()
if(next.contains(Constants.QR_CODE_NAME_PROPERTY)) {
defaultName = next.substringAfter(Constants.QR_CODE_NAME_PROPERTY).trim()
break
}
}
return defaultName
}
suspend fun onTunnelQrResult(result: String, configType: ConfigType): Result<Unit> {
return try {
validateConfigString(result, configType)
val tunnelName = makeTunnelNameUnique(generateQrCodeTunnelName(result, configType))
val tunnelConfig = when(configType) {
ConfigType.AMNEZIA ->{
TunnelConfig(name = tunnelName, amQuick = result,
wgQuick = TunnelConfig.configFromAmQuick(result).toWgQuickString())
}
ConfigType.WIREGUARD -> TunnelConfig(name = tunnelName, wgQuick = result)
}
addTunnel(tunnelConfig)
Result.Success(Unit)
Result.success(Unit)
} catch (e: Exception) {
Timber.e(e)
Result.Error(Event.Error.InvalidQrCode)
Result.failure(WgTunnelExceptions.InvalidQrCode())
}
}
private suspend fun saveTunnelConfigFromStream(stream: InputStream, fileName: String) {
val bufferReader = stream.bufferedReader(charset = Charsets.UTF_8)
val config = Config.parse(bufferReader)
val tunnelName = getNameFromFileName(fileName)
addTunnel(TunnelConfig(name = tunnelName, wgQuick = config.toWgQuickString()))
withContext(Dispatchers.IO) { stream.close() }
private suspend fun makeTunnelNameUnique(name : String) : String {
val tunnels = appDataRepository.tunnels.getAll()
var tunnelName = name
var num = 1
while (tunnels.any { it.name == tunnelName }) {
tunnelName = name + "(${num})"
num++
}
return tunnelName
}
private fun getInputStreamFromUri(uri: Uri): InputStream? {
return application.applicationContext.contentResolver.openInputStream(uri)
}
suspend fun onTunnelFileSelected(uri: Uri): Result<Unit> {
try {
if (isValidUriContentScheme(uri)) {
val fileName = getFileName(application.applicationContext, uri)
when (getFileExtensionFromFileName(fileName)) {
Constants.CONF_FILE_EXTENSION ->
saveTunnelFromConfUri(fileName, uri).let {
when (it) {
is Result.Error -> return Result.Error(Event.Error.FileReadFailed)
is Result.Success -> return it
}
}
Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri)
else -> return Result.Error(Event.Error.InvalidFileExtension)
private suspend fun saveTunnelConfigFromStream(stream: InputStream, fileName: String, type: ConfigType) {
var amQuick : String? = null
val wgQuick = stream.use {
when(type) {
ConfigType.AMNEZIA -> {
val config = org.amnezia.awg.config.Config.parse(it)
amQuick = config.toAwgQuickString()
config.toWgQuickString()
}
ConfigType.WIREGUARD -> {
Config.parse(it).toWgQuickString()
}
}
}
val tunnelName = makeTunnelNameUnique(getNameFromFileName(fileName))
addTunnel(TunnelConfig(name = tunnelName, wgQuick = wgQuick, amQuick = amQuick ?: TunnelConfig.AM_QUICK_DEFAULT))
}
private fun getInputStreamFromUri(uri: Uri, context: Context): InputStream? {
return context.applicationContext.contentResolver.openInputStream(uri)
}
suspend fun onTunnelFileSelected(uri: Uri, configType: ConfigType, context: Context): Result<Unit> {
return try {
if (isValidUriContentScheme(uri)) {
val fileName = getFileName(context, uri)
return when (getFileExtensionFromFileName(fileName)) {
Constants.CONF_FILE_EXTENSION ->
saveTunnelFromConfUri(fileName, uri, configType, context)
Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri, configType, context)
else -> Result.failure(WgTunnelExceptions.InvalidFileExtension())
}
return Result.Success(Unit)
} else {
return Result.Error(Event.Error.InvalidFileExtension)
Result.failure(WgTunnelExceptions.InvalidFileExtension())
}
} catch (e: Exception) {
Timber.e(e)
return Result.Error(Event.Error.FileReadFailed)
Result.failure(WgTunnelExceptions.FileReadFailed())
}
}
private suspend fun saveTunnelsFromZipUri(uri: Uri) {
ZipInputStream(getInputStreamFromUri(uri)).use { zip ->
private suspend fun saveTunnelsFromZipUri(uri: Uri, configType: ConfigType, context: Context) : Result<Unit> {
return ZipInputStream(getInputStreamFromUri(uri, context)).use { zip ->
generateSequence { zip.nextEntry }
.filterNot {
it.isDirectory ||
@@ -162,40 +213,57 @@ constructor(
}
.forEach {
val name = getNameFromFileName(it.name)
val config = Config.parse(zip)
viewModelScope.launch(Dispatchers.IO) {
addTunnel(TunnelConfig(name = name, wgQuick = config.toWgQuickString()))
withContext(viewModelScope.coroutineContext + Dispatchers.IO) {
try {
var amQuick : String? = null
val wgQuick =
when(configType) {
ConfigType.AMNEZIA -> {
val config = org.amnezia.awg.config.Config.parse(zip)
amQuick = config.toAwgQuickString()
config.toWgQuickString()
}
ConfigType.WIREGUARD -> {
Config.parse(zip).toWgQuickString()
}
}
addTunnel(TunnelConfig(name = makeTunnelNameUnique(name), wgQuick = wgQuick, amQuick = amQuick ?: TunnelConfig.AM_QUICK_DEFAULT))
Result.success(Unit)
} catch (e : Exception) {
Result.failure(WgTunnelExceptions.FileReadFailed())
}
}
}
Result.success(Unit)
}
}
private suspend fun saveTunnelFromConfUri(name: String, uri: Uri): Result<Unit> {
val stream = getInputStreamFromUri(uri)
private suspend fun saveTunnelFromConfUri(name: String, uri: Uri, configType: ConfigType, context: Context): Result<Unit> {
val stream = getInputStreamFromUri(uri, context)
return if (stream != null) {
saveTunnelConfigFromStream(stream, name)
Result.Success(Unit)
saveTunnelConfigFromStream(stream, name, configType)
Result.success(Unit)
} else {
Result.Error(Event.Error.FileReadFailed)
Result.failure(WgTunnelExceptions.FileReadFailed())
}
}
private suspend fun addTunnel(tunnelConfig: TunnelConfig) {
val firstTunnel = appDataRepository.tunnels.count() == 0
saveTunnel(tunnelConfig)
if (firstTunnel) WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(application)
if (firstTunnel) WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
}
fun pauseAutoTunneling() =
viewModelScope.launch {
appDataRepository.settings.save(uiState.value.settings.copy(isAutoTunnelPaused = true))
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate(application)
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate()
}
fun resumeAutoTunneling() =
viewModelScope.launch {
appDataRepository.settings.save(uiState.value.settings.copy(isAutoTunnelPaused = false))
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate(application)
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate()
}
private suspend fun saveTunnel(tunnelConfig: TunnelConfig) {
@@ -239,12 +307,12 @@ constructor(
return fileName.substring(0, fileName.lastIndexOf('.'))
}
private fun getFileExtensionFromFileName(fileName: String): String {
private fun getFileExtensionFromFileName(fileName: String): String? {
return try {
fileName.substring(fileName.lastIndexOf('.'))
} catch (e: Exception) {
Timber.e(e)
""
null
}
}
@@ -1,5 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.options
import android.annotation.SuppressLint
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
@@ -7,7 +8,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
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.height
@@ -24,9 +24,10 @@ 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.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -38,16 +39,23 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import com.iamageo.multifablibrary.FabIcon
import com.iamageo.multifablibrary.FabOption
import com.iamageo.multifablibrary.MultiFabItem
import com.iamageo.multifablibrary.MultiFloatingActionButton
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
@@ -55,11 +63,13 @@ import com.zaneschepke.wireguardautotunnel.ui.Screen
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.Result
import com.zaneschepke.wireguardautotunnel.util.getMessage
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun OptionsScreen(
@@ -71,6 +81,8 @@ fun OptionsScreen(
) {
val scrollState = rememberScrollState()
val uiState by optionsViewModel.uiState.collectAsStateWithLifecycle()
val context = LocalContext.current
val interactionSource = remember { MutableInteractionSource() }
val scope = rememberCoroutineScope()
val focusManager = LocalFocusManager.current
@@ -90,196 +102,236 @@ fun OptionsScreen(
fun saveTrustedSSID() {
if (currentText.isNotEmpty()) {
scope.launch {
optionsViewModel.onSaveRunSSID(currentText).let {
when (it) {
is Result.Success -> currentText = ""
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
}
optionsViewModel.onSaveRunSSID(currentText).onSuccess {
currentText = ""
}.onFailure {
appViewModel.showSnackbarMessage(it.getMessage(context))
}
}
}
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top,
modifier =
Modifier
.fillMaxSize()
.verticalScroll(scrollState)
.clickable(
indication = null,
interactionSource = interactionSource,
) {
focusManager.clearFocus()
},
) {
Surface(
tonalElevation = 2.dp,
shadowElevation = 2.dp,
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.surface,
modifier =
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
Modifier
.height(IntrinsicSize.Min)
.fillMaxWidth(fillMaxWidth)
.padding(top = 10.dp)
} else {
Modifier
.fillMaxWidth(fillMaxWidth)
.padding(top = 20.dp)
})
.padding(bottom = 10.dp),
) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top,
modifier = Modifier.padding(15.dp),
) {
SectionTitle(
title = stringResource(id = R.string.general),
padding = screenPadding,
Scaffold(
floatingActionButton = {
val secondaryColor = MaterialTheme.colorScheme.secondary
val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
var fobColor by remember { mutableStateOf(secondaryColor) }
MultiFloatingActionButton(
modifier =
(if (
WireGuardAutoTunnel.isRunningOnAndroidTv()
)
ConfigurationToggle(
stringResource(R.string.set_primary_tunnel),
enabled = true,
checked = uiState.isDefaultTunnel,
modifier = Modifier
.focusRequester(focusRequester),
padding = screenPadding,
onCheckChanged = { optionsViewModel.onTogglePrimaryTunnel() },
)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxSize()
.padding(top = 5.dp),
horizontalArrangement = Arrangement.Center,
) {
TextButton(
onClick = {
navController.navigate(
"${Screen.Config.route}/${tunnelId}",
Modifier.focusRequester(focusRequester)
else Modifier)
.onFocusChanged {
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
fobColor = if (it.isFocused) hoverColor else secondaryColor
}
},
fabIcon = FabIcon(
iconRes = R.drawable.edit,
iconResAfterRotate = R.drawable.close,
iconRotate = 180f
),
fabOption = FabOption(
iconTint = MaterialTheme.colorScheme.background,
backgroundTint = MaterialTheme.colorScheme.primary,
),
itemsMultiFab = listOf(
MultiFabItem(
label = {
Text(
stringResource(id = R.string.amnezia),
color = Color.White,
textAlign = TextAlign.Center,
modifier = Modifier.padding(end = 10.dp)
)
},
) {
Text(stringResource(R.string.edit_tunnel))
}
icon = R.drawable.edit,
value = ConfigType.AMNEZIA.name,
),
MultiFabItem(
label = {
Text(stringResource(id = R.string.wireguard), color = Color.White, textAlign = TextAlign.Center, modifier = Modifier.padding(end = 10.dp))
},
icon = R.drawable.edit,
value = ConfigType.WIREGUARD.name
),
),
onFabItemClicked = {
val configType = ConfigType.valueOf(it.value)
navController.navigate(
"${Screen.Config.route}/${tunnelId}?configType=${configType.name}",
)
},
shape = RoundedCornerShape(16.dp),
)
}
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top,
modifier =
Modifier
.fillMaxSize()
.verticalScroll(scrollState)
.clickable(
indication = null,
interactionSource = interactionSource,
) {
focusManager.clearFocus()
},
) {
Surface(
tonalElevation = 2.dp,
shadowElevation = 2.dp,
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.surface,
modifier =
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
Modifier
.height(IntrinsicSize.Min)
.fillMaxWidth(fillMaxWidth)
.padding(top = 10.dp)
} else {
Modifier
.fillMaxWidth(fillMaxWidth)
.padding(top = 20.dp)
})
.padding(bottom = 10.dp),
) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top,
modifier = Modifier.padding(15.dp),
) {
SectionTitle(
title = stringResource(id = R.string.general),
padding = screenPadding,
)
ConfigurationToggle(
stringResource(R.string.set_primary_tunnel),
enabled = true,
checked = uiState.isDefaultTunnel,
modifier = Modifier
.focusRequester(focusRequester),
padding = screenPadding,
onCheckChanged = { optionsViewModel.onTogglePrimaryTunnel() },
)
}
}
}
Surface(
tonalElevation = 2.dp,
shadowElevation = 2.dp,
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.surface,
modifier =
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
Modifier
.height(IntrinsicSize.Min)
.fillMaxWidth(fillMaxWidth)
.padding(top = 10.dp)
} else {
Modifier
.fillMaxWidth(fillMaxWidth)
.padding(top = 20.dp)
})
.padding(bottom = 10.dp),
) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top,
modifier = Modifier.padding(15.dp),
Surface(
tonalElevation = 2.dp,
shadowElevation = 2.dp,
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.surface,
modifier =
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
Modifier
.height(IntrinsicSize.Min)
.fillMaxWidth(fillMaxWidth)
.padding(top = 10.dp)
} else {
Modifier
.fillMaxWidth(fillMaxWidth)
.padding(top = 20.dp)
})
.padding(bottom = 10.dp),
) {
SectionTitle(
title = stringResource(id = R.string.auto_tunneling),
padding = screenPadding,
)
ConfigurationToggle(
stringResource(R.string.mobile_data_tunnel),
enabled = true,
checked = uiState.tunnel?.isMobileDataTunnel == true,
padding = screenPadding,
onCheckChanged = { optionsViewModel.onToggleIsMobileDataTunnel() },
)
Column {
FlowRow(
modifier = Modifier
.padding(screenPadding)
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(5.dp),
) {
uiState.tunnel?.tunnelNetworks?.forEach { ssid ->
ClickableIconButton(
onClick = {
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
focusRequester.requestFocus()
optionsViewModel.onDeleteRunSSID(ssid)
}
},
onIconClick = {
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) focusRequester.requestFocus()
optionsViewModel.onDeleteRunSSID(ssid)
},
text = ssid,
icon = Icons.Filled.Close,
enabled = true,
)
}
if (uiState.tunnel == null || uiState.tunnel?.tunnelNetworks?.isEmpty() == true) {
Text(
stringResource(R.string.no_wifi_names_configured),
fontStyle = FontStyle.Italic,
color = Color.Gray,
)
}
}
OutlinedTextField(
enabled = true,
value = currentText,
onValueChange = { currentText = it },
label = { Text(stringResource(id = R.string.use_tunnel_on_wifi_name)) },
modifier =
Modifier
.padding(
start = screenPadding,
top = 5.dp,
bottom = 10.dp,
),
maxLines = 1,
keyboardOptions =
KeyboardOptions(
capitalization = KeyboardCapitalization.None,
imeAction = ImeAction.Done,
),
keyboardActions = KeyboardActions(onDone = { saveTrustedSSID() }),
trailingIcon = {
if (currentText != "") {
IconButton(onClick = { saveTrustedSSID() }) {
Icon(
imageVector = Icons.Outlined.Add,
contentDescription =
if (currentText == "") {
stringResource(
id =
R.string
.trusted_ssid_empty_description,
)
} else {
stringResource(
id =
R.string
.trusted_ssid_value_description,
)
},
tint = MaterialTheme.colorScheme.primary,
)
}
}
},
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top,
modifier = Modifier.padding(15.dp),
) {
SectionTitle(
title = stringResource(id = R.string.auto_tunneling),
padding = screenPadding,
)
ConfigurationToggle(
stringResource(R.string.mobile_data_tunnel),
enabled = true,
checked = uiState.tunnel?.isMobileDataTunnel == true,
padding = screenPadding,
onCheckChanged = { optionsViewModel.onToggleIsMobileDataTunnel() },
)
Column {
FlowRow(
modifier = Modifier
.padding(screenPadding)
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(5.dp),
) {
uiState.tunnel?.tunnelNetworks?.forEach { ssid ->
ClickableIconButton(
onClick = {
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
focusRequester.requestFocus()
optionsViewModel.onDeleteRunSSID(ssid)
}
},
onIconClick = {
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) focusRequester.requestFocus()
optionsViewModel.onDeleteRunSSID(ssid)
},
text = ssid,
icon = Icons.Filled.Close,
enabled = true,
)
}
if (uiState.tunnel == null || uiState.tunnel?.tunnelNetworks?.isEmpty() == true) {
Text(
stringResource(R.string.no_wifi_names_configured),
fontStyle = FontStyle.Italic,
color = Color.Gray,
)
}
}
OutlinedTextField(
enabled = true,
value = currentText,
onValueChange = { currentText = it },
label = { Text(stringResource(id = R.string.use_tunnel_on_wifi_name)) },
modifier =
Modifier
.padding(
start = screenPadding,
top = 5.dp,
bottom = 10.dp,
),
maxLines = 1,
keyboardOptions =
KeyboardOptions(
capitalization = KeyboardCapitalization.None,
imeAction = ImeAction.Done,
),
keyboardActions = KeyboardActions(onDone = { saveTrustedSSID() }),
trailingIcon = {
if (currentText != "") {
IconButton(onClick = { saveTrustedSSID() }) {
Icon(
imageVector = Icons.Outlined.Add,
contentDescription =
if (currentText == "") {
stringResource(
id =
R.string
.trusted_ssid_empty_description,
)
} else {
stringResource(
id =
R.string
.trusted_ssid_value_description,
)
},
tint = MaterialTheme.colorScheme.primary,
)
}
}
},
)
}
}
}
}
@@ -7,8 +7,7 @@ import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.Event
import com.zaneschepke.wireguardautotunnel.util.Result
import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
@@ -76,9 +75,9 @@ constructor(
tunnelsWithName.isEmpty()) {
uiState.value.tunnel?.tunnelNetworks?.add(trimmed)
saveTunnel(uiState.value.tunnel)
Result.Success(Unit)
Result.success(Unit)
} else {
Result.Error(Event.Error.SsidConflict)
Result.failure(WgTunnelExceptions.SsidConflict())
}
}
@@ -98,7 +97,7 @@ constructor(
false -> uiState.value.tunnel
},
)
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(WireGuardAutoTunnel.instance)
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
}
}
}
@@ -70,10 +70,10 @@ import androidx.navigation.NavController
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import com.wireguard.android.backend.Tunnel
import com.wireguard.android.backend.WgQuickBackend
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.Screen
@@ -81,9 +81,8 @@ import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
import com.zaneschepke.wireguardautotunnel.util.Event
import com.zaneschepke.wireguardautotunnel.util.FileUtils
import com.zaneschepke.wireguardautotunnel.util.Result
import com.zaneschepke.wireguardautotunnel.util.getMessage
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import timber.log.Timber
@@ -131,16 +130,28 @@ fun SettingsScreen(
fun exportAllConfigs() {
try {
val files = uiState.tunnels.map { File(context.cacheDir, "${it.name}.conf") }
files.forEachIndexed { index, file ->
file.outputStream().use { it.write(uiState.tunnels[index].wgQuick.toByteArray()) }
val wgFiles = uiState.tunnels.map { config ->
val file = File(context.cacheDir, "${config.name}-wg.conf")
file.outputStream().use {
it.write(config.wgQuick.toByteArray())
}
file
}
val amFiles = uiState.tunnels.mapNotNull { config -> if(config.amQuick != TunnelConfig.AM_QUICK_DEFAULT) {
val file = File(context.cacheDir, "${config.name}-am.conf")
file.outputStream().use {
it.write(config.amQuick.toByteArray())
}
file
} else null }
FileUtils.saveFilesToZip(context, wgFiles + amFiles).onFailure {
appViewModel.showSnackbarMessage(it.getMessage(context))
}.onSuccess {
didExportFiles = true
appViewModel.showSnackbarMessage(context.getString(R.string.exported_configs_message))
}
FileUtils.saveFilesToZip(context, files)
didExportFiles = true
appViewModel.showSnackbarMessage(Event.Message.ConfigsExported.message)
} catch (e: Exception) {
Timber.e(e)
appViewModel.showSnackbarMessage(Event.Error.Exception(e).message)
}
}
@@ -161,7 +172,7 @@ fun SettingsScreen(
fun handleAutoTunnelToggle() {
if (uiState.isBatteryOptimizeDisableShown || isBatteryOptimizationsDisabled()) {
if (appViewModel.isRequiredPermissionGranted()) {
viewModel.onToggleAutoTunnel()
viewModel.onToggleAutoTunnel(context)
}
} else {
requestBatteryOptimizationsDisabled()
@@ -170,11 +181,10 @@ fun SettingsScreen(
fun saveTrustedSSID() {
if (currentText.isNotEmpty()) {
viewModel.onSaveTrustedSSID(currentText).let {
when (it) {
is Result.Success -> currentText = ""
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
}
viewModel.onSaveTrustedSSID(currentText).onSuccess {
currentText = ""
}.onFailure {
appViewModel.showSnackbarMessage(it.getMessage(context))
}
}
}
@@ -308,11 +318,11 @@ fun SettingsScreen(
},
onError = { _ ->
showAuthPrompt = false
appViewModel.showSnackbarMessage(Event.Error.AuthenticationFailed.message)
appViewModel.showSnackbarMessage(context.getString(R.string.error_authentication_failed))
},
onFailure = {
showAuthPrompt = false
appViewModel.showSnackbarMessage(Event.Error.AuthorizationFailed.message)
appViewModel.showSnackbarMessage(context.getString(R.string.error_authorization_failed))
},
)
}
@@ -520,12 +530,12 @@ fun SettingsScreen(
when (false) {
isBackgroundLocationGranted ->
appViewModel.showSnackbarMessage(
Event.Error.BackgroundLocationRequired.message,
context.getString(R.string.background_location_required),
)
fineLocationState.status.isGranted ->
appViewModel.showSnackbarMessage(
Event.Error.PreciseLocationRequired.message,
context.getString(R.string.precise_location_required),
)
viewModel.isLocationEnabled(context) ->
@@ -591,11 +601,8 @@ fun SettingsScreen(
checked = uiState.settings.isKernelEnabled,
padding = screenPadding,
onCheckChanged = {
viewModel.onToggleKernelMode().let {
when (it) {
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
is Result.Success -> {}
}
viewModel.onToggleKernelMode().onFailure {
appViewModel.showSnackbarMessage(it.getMessage(context))
}
},
)
@@ -1,6 +1,5 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings
import android.app.Application
import android.content.Context
import android.location.LocationManager
import androidx.core.location.LocationManagerCompat
@@ -13,8 +12,7 @@ import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.Event
import com.zaneschepke.wireguardautotunnel.util.Result
import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
@@ -27,7 +25,6 @@ import javax.inject.Inject
class SettingsViewModel
@Inject
constructor(
private val application: Application,
private val appDataRepository: AppDataRepository,
private val serviceManager: ServiceManager,
private val rootShell: RootShell,
@@ -60,9 +57,9 @@ constructor(
return if (!uiState.value.settings.trustedNetworkSSIDs.contains(trimmed)) {
uiState.value.settings.trustedNetworkSSIDs.add(trimmed)
saveSettings(uiState.value.settings)
Result.Success(Unit)
Result.success(Unit)
} else {
Result.Error(Event.Error.SsidConflict)
Result.failure(WgTunnelExceptions.SsidConflict())
}
}
@@ -93,15 +90,15 @@ constructor(
)
}
fun onToggleAutoTunnel() =
fun onToggleAutoTunnel(context: Context) =
viewModelScope.launch {
val isAutoTunnelEnabled = uiState.value.settings.isAutoTunnelEnabled
var isAutoTunnelPaused = uiState.value.settings.isAutoTunnelPaused
if (isAutoTunnelEnabled) {
serviceManager.stopWatcherService(application)
serviceManager.stopWatcherService(context)
} else {
serviceManager.startWatcherService(application)
serviceManager.startWatcherService(context)
isAutoTunnelPaused = false
}
saveSettings(
@@ -110,7 +107,7 @@ constructor(
isAutoTunnelPaused = isAutoTunnelPaused,
),
)
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate(application)
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate()
}
fun onToggleAlwaysOnVPN() =
@@ -182,18 +179,22 @@ constructor(
try {
rootShell.start()
Timber.i("Root shell accepted!")
saveKernelMode(on = true)
saveAmneziaMode(false)
saveSettings(
uiState.value.settings.copy(
isKernelEnabled = true,
isAmneziaEnabled = false,
),
)
} catch (e: RootShell.RootShellException) {
Timber.e(e)
saveKernelMode(on = false)
return Result.Error(Event.Error.RootDenied)
return Result.failure(WgTunnelExceptions.RootDenied())
}
} else {
saveKernelMode(on = false)
}
return Result.Success(Unit)
return Result.success(Unit)
}
fun onToggleRestartOnPing() = viewModelScope.launch {
@@ -25,7 +25,7 @@ object Constants {
const val SUBSCRIPTION_TIMEOUT = 5_000L
const val FOCUS_REQUEST_DELAY = 500L
const val BACKUP_PING_HOST = "1.1.1.1"
const val DEFAULT_PING_IP = "1.1.1.1"
const val PING_TIMEOUT = 5_000L
const val VPN_RESTART_DELAY = 1_000L
const val PING_INTERVAL = 60_000L
@@ -37,4 +37,7 @@ object Constants {
const val UNREADABLE_SSID = "<unknown ssid>"
val amneziaProperties = listOf("Jc", "Jmin", "Jmax", "S1", "S2", "H1", "H2", "H3", "H4")
const val QR_CODE_NAME_PROPERTY = "# Name ="
}
@@ -1,117 +0,0 @@
package com.zaneschepke.wireguardautotunnel.util
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
sealed class Event {
abstract val message: String
sealed class Error : Event() {
data object None : Error() {
override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.error_none)
}
data object SsidConflict : Error() {
override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.error_ssid_exists)
}
data class ConfigParseError(val appendedMessage: String) : Error() {
override val message: String =
WireGuardAutoTunnel.instance.getString(R.string.config_parse_error) + (
if (appendedMessage != "") ": ${appendedMessage.trim()}" else "")
}
data object RootDenied : Error() {
override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.error_root_denied)
}
data class General(val customMessage: String) : Error() {
override val message: String
get() = customMessage
}
data class Exception(val exception: kotlin.Exception) : Error() {
override val message: String
get() =
exception.message
?: WireGuardAutoTunnel.instance.getString(R.string.unknown_error)
}
data object InvalidQrCode : Error() {
override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.error_invalid_code)
}
data object InvalidFileExtension : Error() {
override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.error_file_extension)
}
data object FileReadFailed : Error() {
override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.error_file_extension)
}
data object AuthenticationFailed : Error() {
override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.error_authentication_failed)
}
data object AuthorizationFailed : Error() {
override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.error_authorization_failed)
}
data object BackgroundLocationRequired : Error() {
override val message: String
get() =
WireGuardAutoTunnel.instance.getString(R.string.background_location_required)
}
data object LocationServicesRequired : Error() {
override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.location_services_required)
}
data object PreciseLocationRequired : Error() {
override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.precise_location_required)
}
data object FileExplorerRequired : Error() {
override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.error_no_file_explorer)
}
}
sealed class Message : Event() {
data object ConfigSaved : Message() {
override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.config_changes_saved)
}
data object ConfigsExported : Message() {
override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.exported_configs_message)
}
data object TunnelOffAction : Message() {
override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.turn_off_tunnel)
}
data object TunnelOnAction : Message() {
override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.turn_on_tunnel)
}
data object AutoTunnelOffAction : Message() {
override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.turn_off_auto)
}
}
}
@@ -1,7 +1,9 @@
package com.zaneschepke.wireguardautotunnel.util
import android.content.BroadcastReceiver
import android.content.Context
import android.content.pm.PackageInfo
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
@@ -9,6 +11,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.amnezia.awg.config.Config
import java.math.BigDecimal
import java.text.DecimalFormat
import kotlin.coroutines.CoroutineContext
@@ -69,3 +72,25 @@ fun TunnelStatistics.PeerStats.handshakeStatus(): HandshakeStatus {
}
}
}
fun Config.toWgQuickString() : String {
val amQuick = toAwgQuickString()
val lines = amQuick.lines().toMutableList()
val linesIterator = lines.iterator()
while(linesIterator.hasNext()) {
val next = linesIterator.next()
Constants.amneziaProperties.forEach {
if(next.startsWith(it, ignoreCase = true)) {
linesIterator.remove()
}
}
}
return lines.joinToString(System.lineSeparator())
}
fun Throwable.getMessage(context: Context) : String {
return when(this) {
is WgTunnelExceptions -> this.getMessage(context)
else -> this.message ?: StringValue.StringResource(R.string.unknown_error).asString(context)
}
}
@@ -6,6 +6,7 @@ import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import android.provider.MediaStore.MediaColumns
import timber.log.Timber
import java.io.File
import java.io.FileOutputStream
import java.io.OutputStream
@@ -70,21 +71,27 @@ object FileUtils {
}
}
fun saveFilesToZip(context: Context, files: List<File>) {
val zipOutputStream =
createDownloadsFileOutputStream(
context,
"wg-export_${Instant.now().epochSecond}.zip",
ZIP_FILE_MIME_TYPE,
)
ZipOutputStream(zipOutputStream).use { zos ->
files.forEach { file ->
val entry = ZipEntry(file.name)
zos.putNextEntry(entry)
if (file.isFile) {
file.inputStream().use { fis -> fis.copyTo(zos) }
fun saveFilesToZip(context: Context, files: List<File>) : Result<Unit> {
return try {
val zipOutputStream =
createDownloadsFileOutputStream(
context,
"wg-export_${Instant.now().epochSecond}.zip",
ZIP_FILE_MIME_TYPE,
)
ZipOutputStream(zipOutputStream).use { zos ->
files.forEach { file ->
val entry = ZipEntry(file.name)
zos.putNextEntry(entry)
if (file.isFile) {
file.inputStream().use { fis -> fis.copyTo(zos) }
}
}
return Result.success(Unit)
}
} catch (e : Exception) {
Timber.e(e)
Result.failure(WgTunnelExceptions.ConfigExportFailed())
}
}
}
@@ -1,16 +0,0 @@
package com.zaneschepke.wireguardautotunnel.util
import timber.log.Timber
sealed class Result<T> {
class Success<T>(val data: T) : Result<T>()
class Error<T>(val error: Event.Error) : Result<T>() {
init {
when (this.error) {
is Event.Error.Exception -> Timber.e(this.error.exception)
else -> Timber.e(this.error.message)
}
}
}
}
@@ -0,0 +1,124 @@
package com.zaneschepke.wireguardautotunnel.util
import android.content.Context
import com.zaneschepke.wireguardautotunnel.R
sealed class WgTunnelExceptions : Exception() {
abstract fun getMessage(context: Context) : String
data class General(private val userMessage : StringValue) : WgTunnelExceptions() {
override fun getMessage(context: Context) : String {
return userMessage.asString(context)
}
}
data class SsidConflict(private val userMessage : StringValue = StringValue.StringResource(R.string.error_ssid_exists)) : WgTunnelExceptions() {
override fun getMessage(context: Context) : String {
return userMessage.asString(context)
}
}
data class ConfigExportFailed(private val userMessage : StringValue = StringValue.StringResource(R.string.export_configs_failed)) : WgTunnelExceptions() {
override fun getMessage(context: Context) : String {
return userMessage.asString(context)
}
}
data class ConfigParseError(private val appendMessage : StringValue = StringValue.Empty) : WgTunnelExceptions() {
override fun getMessage(context: Context) : String {
return StringValue.StringResource(R.string.config_parse_error).asString(context) + (
if (appendMessage != StringValue.Empty) ": ${appendMessage.asString(context)}" else "")
}
}
data class RootDenied(private val userMessage : StringValue = StringValue.StringResource(R.string.error_root_denied)) : WgTunnelExceptions() {
override fun getMessage(context: Context) : String {
return userMessage.asString(context)
}
}
data class InvalidQrCode(private val userMessage : StringValue = StringValue.StringResource(R.string.error_invalid_code)) : WgTunnelExceptions() {
override fun getMessage(context: Context) : String {
return userMessage.asString(context)
}
}
data class InvalidFileExtension(private val userMessage : StringValue = StringValue.StringResource(R.string.error_file_extension)) : WgTunnelExceptions() {
override fun getMessage(context: Context) : String {
return userMessage.asString(context)
}
}
data class FileReadFailed(private val userMessage : StringValue = StringValue.StringResource(R.string.error_file_format)) : WgTunnelExceptions() {
override fun getMessage(context: Context) : String {
return userMessage.asString(context)
}
}
data class AuthenticationFailed(private val userMessage : StringValue = StringValue.StringResource(R.string.error_authentication_failed)) : WgTunnelExceptions() {
override fun getMessage(context: Context) : String {
return userMessage.asString(context)
}
}
data class AuthorizationFailed(private val userMessage : StringValue = StringValue.StringResource(R.string.error_authorization_failed)) : WgTunnelExceptions() {
override fun getMessage(context: Context) : String {
return userMessage.asString(context)
}
}
data class BackgroundLocationRequired(private val userMessage : StringValue = StringValue.StringResource(R.string.background_location_required)) : WgTunnelExceptions() {
override fun getMessage(context: Context) : String {
return userMessage.asString(context)
}
}
data class LocationServicesRequired(private val userMessage : StringValue = StringValue.StringResource(R.string.location_services_required)) : WgTunnelExceptions() {
override fun getMessage(context: Context) : String {
return userMessage.asString(context)
}
}
data class PreciseLocationRequired(private val userMessage : StringValue = StringValue.StringResource(R.string.precise_location_required)) : WgTunnelExceptions() {
override fun getMessage(context: Context) : String {
return userMessage.asString(context)
}
}
data class FileExplorerRequired (private val userMessage : StringValue = StringValue.StringResource(R.string.error_no_file_explorer)) : WgTunnelExceptions() {
override fun getMessage(context: Context) : String {
return userMessage.asString(context)
}
}
// sealed class Message : Event() {
// data object ConfigSaved : Message() {
// override val message: String
// get() = WireGuardAutoTunnel.instance.getString(R.string.config_changes_saved)
// }
//
// data object ConfigsExported : Message() {
// override val message: String
// get() = WireGuardAutoTunnel.instance.getString(R.string.exported_configs_message)
// }
//
// data object TunnelOffAction : Message() {
// override val message: String
// get() = WireGuardAutoTunnel.instance.getString(R.string.turn_off_tunnel)
// }
//
// data object TunnelOnAction : Message() {
// override val message: String
// get() = WireGuardAutoTunnel.instance.getString(R.string.turn_on_tunnel)
// }
//
// data object AutoTunnelOffAction : Message() {
// override val message: String
// get() = WireGuardAutoTunnel.instance.getString(R.string.turn_off_auto)
// }
// }
}
+9
View File
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M440,520L200,520v-80h240v-240h80v240h240v80L520,520v240h-80v-240Z"
android:fillColor="#e8eaed"/>
</vector>
+9
View File
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="m256,760 l-56,-56 224,-224 -224,-224 56,-56 224,224 224,-224 56,56 -224,224 224,224 -56,56 -224,-224 -224,224Z"
android:fillColor="#e8eaed"/>
</vector>
+9
View File
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M200,760h57l391,-391 -57,-57 -391,391v57ZM120,840v-170l528,-527q12,-11 26.5,-17t30.5,-6q16,0 31,6t26,18l55,56q12,11 17.5,26t5.5,30q0,16 -5.5,30.5T817,313L290,840L120,840ZM760,256 L704,200 760,256ZM619,341 L591,312 648,369 619,341Z"
android:fillColor="#e8eaed"/>
</vector>
+7
View File
@@ -154,4 +154,11 @@
<string name="kernel">Kernel</string>
<string name="use_kernel">Verwende das Kernel Modul</string>
<string name="error_ssid_exists">SSID existiert bereits</string>
<string name="use_amnezia">"Benutze Amnezia Benuzterumgebung "</string>
<string name="junk_packet_count">Müll Packet Anzahk</string>
<string name="junk_packet_maximum_size">Müll Packet maximale Grösse</string>
<string name="init_packet_junk_size">Erstes Packet Müllgrösse</string>
<string name="backend">Backend</string>
<string name="junk_packet_minimum_size">Müll Packet minimale Grösse</string>
<string name="response_packet_junk_size">Antwortpaket Müllgrösse</string>
</resources>
+37 -1
View File
@@ -1,3 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>
<string name="turn_off_tunnel">Действие требует отключения туннеля</string>
<string name="add_tunnel">Добавить туннель</string>
<string name="mtu">MTU</string>
<string name="tunnel_name">Имя туннеля</string>
<string name="public_key">Публичный ключ</string>
<string name="name">Имя</string>
<string name="peer">Пир</string>
<string name="privacy_policy">Посмотреть политику конфиденциальности</string>
<string name="icon">Иконка</string>
<string name="turn_on">Включить</string>
<string name="add_from_qr">Добавить из QR</string>
<string name="qr_scan">Сканер QR</string>
<string name="auto_tunneling">Авто-туннелирование</string>
<string name="no_tunnels">Туннели еще не добавлены!</string>
<string name="open_file">Открыть файл</string>
<string name="exclude">Исключить</string>
<string name="include">Включить</string>
<string name="tunnel_all">Туннель для всех приложений</string>
<string name="config_changes_saved">Изменения конфигурации сохранены.</string>
<string name="save_changes">Сохранить</string>
<string name="no_thanks">Нет, спасибо</string>
<string name="map">Карта</string>
<string name="addresses">Адреса</string>
<string name="dns_servers">DNS сервера</string>
<string name="allowed_ips">Разрешенные IP</string>
<string name="endpoint">Конечная точка</string>
<string name="restart">Перезагрузить туннель</string>
<string name="vpn_connection_failed">Ошибка соединения</string>
<string name="always_on_vpn_support">Разрешить постоянный VPN</string>
<string name="hint_search_packages">Поиск приложений</string>
<string name="other">Другое</string>
<string name="vpn_on">VPN вкл.</string>
<string name="vpn_off">VPN откл.</string>
<string name="interface_">Интерфейс</string>
<string name="optional">(необязательно)</string>
<string name="optional_no_recommend">(необязательно, не рекомендуется)</string>
</resources>
+5 -1
View File
@@ -94,6 +94,7 @@
<string name="error_authorization_failed">Failed to authorize</string>
<string name="enabled_app_shortcuts">Enable app shortcuts</string>
<string name="export_configs">Export configs</string>
<string name="export_configs_failed">Failed to export configs</string>
<string name="location_services_required">Location services required</string>
<string name="background_location_required">Background location required</string>
<string name="precise_location_required">Precise location required</string>
@@ -173,5 +174,8 @@
<string name="unsure_how">if you are unsure how to proceed</string>
<string name="see_the">See the</string>
<string name="getting_started_url" translatable="false">https://zaneschepke.com/wgtunnel-docs/getting-started.html</string>
<string name="getting_started_guide">Getting started guide</string>
<string name="getting_started_guide">getting started guide</string>
<string name="amnezia" translatable="false">Amnezia</string>
<string name="wireguard" translatable="false">WireGuard</string>
<string name="error_file_format">Invalid tunnel config format</string>
</resources>
+2 -2
View File
@@ -1,7 +1,7 @@
object Constants {
const val VERSION_NAME = "3.4.3-beta"
const val VERSION_NAME = "3.4.4"
const val JVM_TARGET = "17"
const val VERSION_CODE = 34202
const val VERSION_CODE = 34400
const val TARGET_SDK = 34
const val MIN_SDK = 26
const val APP_ID = "com.zaneschepke.wireguardautotunnel"
@@ -0,0 +1,3 @@
Was ist neu:
- Amnezia seite-zu-seite mit WireGuard hinzugefügt
- App Shortcut Bug behoben
@@ -0,0 +1,6 @@
What's new:
- Official support for AmneziaWG
- Import/export for Amnezia configs
- Auto-tunnel to only toggle once per network change
- Additional languages support
- Other bug fixes and improvements
@@ -0,0 +1,5 @@
What's new:
- Improve tunnel import naming
- Fix auto tunneling init state bug
- Improved error handling
- Fix Amnezia zip import bug
@@ -4,8 +4,9 @@ Features
- 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 tunnels to zip
- 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
@@ -0,0 +1,7 @@
Mejoras:
- Gestión del estado de refactorización.
- Mejorar la navegación de AndroidTV
- Mejorar la eficiencia del túnel automático
- Mejorar la navegación
- Función de pausa de túnel automático
- Muchas correcciones de errores
@@ -0,0 +1,7 @@
Mejoras:
- Gestión del estado de refactorización.
- Mejorar la navegación de AndroidTV
- Mejorar la eficiencia del túnel automático
- Mejorar la navegación
- Función de pausa de túnel automático
- Muchas correcciones de errores
@@ -0,0 +1,8 @@
Mejoras:
- Gestión del estado de refactorización.
- Mejorar la navegación de AndroidTV
- Mejorar la eficiencia del túnel automático
- Mejorar la navegación
- Función de pausa de túnel automático
- Se corrigió el inicio del túnel automático en primer plano.
- Muchas correcciones de errores
@@ -0,0 +1,7 @@
Mejoras:
- Se agregó confirmación al eliminar el túnel.
- Se agregó permiso en segundo plano para el modo de ahorro de energía.
Corrección de errores:
- La aplicación se congela cuando el túnel está desactivado.
- Error en el campo del destinatario del correo electrónico
- Edición de configuración con campo DNS vacío
@@ -0,0 +1,3 @@
Mejoras:
- La configuración creada no se guardó.
- Versión elevada
@@ -0,0 +1,2 @@
Qué hay de nuevo:
- Esta es una versión de prueba de CI
@@ -0,0 +1,5 @@
Qué hay de nuevo:
- Inicio automático al reiniciar para el modo kernel de VPN siempre activo
- Soporte para íconos de temas adaptables
- Arreglar íconos de notificación, ícono de mosaico
- Reparar iconos de AndroidTV
@@ -0,0 +1,5 @@
¿Qué hay de nuevo?:
- Mejorar el flujo del primer lanzamiento.
- Cambiar a la bifurcación wireguard lib
- Solicite permiso de VPN en el primer inicio de VPN
- Versiones de golpe
@@ -0,0 +1,2 @@
Novedades:
- Corrección de errores en la interfaz de usuario de Tunnel
+5 -3
View File
@@ -10,18 +10,19 @@ coreKtx = "1.13.1"
datastorePreferences = "1.1.1"
desugar_jdk_libs = "2.0.4"
espressoCore = "3.5.1"
hiltAndroid = "2.51"
hiltAndroid = "2.51.1"
hiltNavigationCompose = "1.2.0"
junit = "4.13.2"
kotlinx-serialization-json = "1.6.3"
lifecycle-runtime-compose = "2.7.0"
material3 = "1.2.1"
multifabVersion = "1.0.9"
navigationCompose = "2.7.7"
pinLockCompose = "1.0.3"
roomVersion = "2.6.1"
timber = "5.0.1"
tunnel = "1.0.20230706"
androidGradlePlugin = "8.4.0-rc02"
androidGradlePlugin = "8.4.0"
kotlin = "1.9.23"
ksp = "1.9.23-1.0.19"
composeBom = "2024.05.00"
@@ -31,7 +32,7 @@ zxingCore = "3.5.3"
#plugins
gradlePlugins-kotlinxSerialization = "1.9.23"
material = "1.11.0"
material = "1.12.0"
[libraries]
@@ -87,6 +88,7 @@ pin-lock-compose = { module = "com.zaneschepke:pin_lock_compose", version.ref =
timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
tunnel = { module = "com.wireguard.android:tunnel", version.ref = "tunnel" }
zaneschepke-multifab = { module = "com.zaneschepke:multifab", version.ref = "multifabVersion" }
zxing-core = { module = "com.google.zxing:core", version.ref = "zxingCore" }
zxing-android-embedded = { module = "com.journeyapps:zxing-android-embedded", version.ref = "zxingAndroidEmbedded" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }