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.
This commit is contained in:
Zane Schepke 2024-05-11 20:42:24 -04:00
parent cb1b8ee7d6
commit d44baa84a8
28 changed files with 421 additions and 395 deletions

View File

@ -2,7 +2,6 @@ package com.zaneschepke.wireguardautotunnel
import android.app.Application import android.app.Application
import android.content.ComponentName import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.service.quicksettings.TileService import android.service.quicksettings.TileService
import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelControlTile import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelControlTile
@ -40,16 +39,16 @@ class WireGuardAutoTunnel : Application() {
return instance.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK) return instance.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
} }
fun requestTunnelTileServiceStateUpdate(context: Context) { fun requestTunnelTileServiceStateUpdate() {
TileService.requestListeningState( TileService.requestListeningState(
context, instance,
ComponentName(instance, TunnelControlTile::class.java), ComponentName(instance, TunnelControlTile::class.java),
) )
} }
fun requestAutoTunnelTileServiceUpdate(context: Context) { fun requestAutoTunnelTileServiceUpdate() {
TileService.requestListeningState( TileService.requestListeningState(
context, instance,
ComponentName(instance, AutoTunnelControlTile::class.java), ComponentName(instance, AutoTunnelControlTile::class.java),
) )
} }

View File

@ -20,6 +20,9 @@ interface TunnelConfigDao {
@Query("SELECT * FROM TunnelConfig WHERE id=:id") @Query("SELECT * FROM TunnelConfig WHERE id=:id")
suspend fun getById(id: Long): TunnelConfig? suspend fun getById(id: Long): TunnelConfig?
@Query("SELECT * FROM TunnelConfig WHERE name=:name")
suspend fun getByName(name: String) : TunnelConfig?
@Query("SELECT * FROM TunnelConfig") @Query("SELECT * FROM TunnelConfig")
suspend fun getAll(): TunnelConfigs suspend fun getAll(): TunnelConfigs

View File

@ -54,6 +54,10 @@ class RoomTunnelConfigRepository(private val tunnelConfigDao: TunnelConfigDao) :
return tunnelConfigDao.count().toInt() return tunnelConfigDao.count().toInt()
} }
override suspend fun findByTunnelName(name: String): TunnelConfig? {
return tunnelConfigDao.getByName(name)
}
override suspend fun findByTunnelNetworksName(name: String): TunnelConfigs { override suspend fun findByTunnelNetworksName(name: String): TunnelConfigs {
return tunnelConfigDao.findByTunnelNetworkName(name) return tunnelConfigDao.findByTunnelNetworkName(name)
} }

View File

@ -22,6 +22,8 @@ interface TunnelConfigRepository {
suspend fun count(): Int suspend fun count(): Int
suspend fun findByTunnelName(name : String) : TunnelConfig?
suspend fun findByTunnelNetworksName(name: String): TunnelConfigs suspend fun findByTunnelNetworksName(name: String): TunnelConfigs
suspend fun findByMobileDataTunnel(): TunnelConfigs suspend fun findByMobileDataTunnel(): TunnelConfigs

View File

@ -1,11 +1,9 @@
package com.zaneschepke.wireguardautotunnel.service.foreground package com.zaneschepke.wireguardautotunnel.service.foreground
import com.zaneschepke.wireguardautotunnel.data.domain.Settings import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
data class WatcherState( data class WatcherState(
val isWifiConnected: Boolean = false, val isWifiConnected: Boolean = false,
val config: TunnelConfig? = null,
val isEthernetConnected: Boolean = false, val isEthernetConnected: Boolean = false,
val isMobileDataConnected: Boolean = false, val isMobileDataConnected: Boolean = false,
val currentNetworkSSID: String = "", val currentNetworkSSID: String = "",
@ -27,8 +25,7 @@ data class WatcherState(
return (!isEthernetConnected && return (!isEthernetConnected &&
settings.isTunnelOnMobileDataEnabled && settings.isTunnelOnMobileDataEnabled &&
!isWifiConnected && !isWifiConnected &&
isMobileDataConnected && isMobileDataConnected)
config?.isMobileDataTunnel == false)
} }
fun isTunnelOffOnMobileDataConditionMet(): Boolean { fun isTunnelOffOnMobileDataConditionMet(): Boolean {
@ -45,13 +42,6 @@ data class WatcherState(
settings.isTunnelOnWifiEnabled) settings.isTunnelOnWifiEnabled)
} }
fun isTunnelNotWifiNamePreferredMet(ssid: String): Boolean {
return (!isEthernetConnected &&
isWifiConnected &&
!settings.trustedNetworkSSIDs.contains(currentNetworkSSID) &&
settings.isTunnelOnWifiEnabled && config?.tunnelNetworks?.contains(ssid) == false)
}
fun isTrustedWifiConditionMet(): Boolean { fun isTrustedWifiConditionMet(): Boolean {
return (!isEthernetConnected && return (!isEthernetConnected &&
(isWifiConnected && (isWifiConnected &&

View File

@ -24,6 +24,7 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import java.net.InetAddress import java.net.InetAddress
@ -162,10 +163,6 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
watchForEthernetConnectivityChanges() watchForEthernetConnectivityChanges()
} }
} }
launch {
Timber.i("Starting vpn state watcher")
watchForVpnConnectivityChanges()
}
launch { launch {
Timber.i("Starting settings watcher") Timber.i("Starting settings watcher")
watchForSettingsChanges() watchForSettingsChanges()
@ -185,29 +182,32 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
} }
private suspend fun watchForMobileDataConnectivityChanges() { private suspend fun watchForMobileDataConnectivityChanges() {
mobileDataService.networkStatus.collect { mobileDataService.networkStatus.collect { status ->
when (it) { when (status) {
is NetworkStatus.Available -> { is NetworkStatus.Available -> {
Timber.i("Gained Mobile data connection") Timber.i("Gained Mobile data connection")
networkEventsFlow.value = networkEventsFlow.update {
networkEventsFlow.value.copy( it.copy(
isMobileDataConnected = true, isMobileDataConnected = true,
) )
} }
}
is NetworkStatus.CapabilitiesChanged -> { is NetworkStatus.CapabilitiesChanged -> {
networkEventsFlow.value = networkEventsFlow.update {
networkEventsFlow.value.copy( it.copy(
isMobileDataConnected = true, isMobileDataConnected = true,
) )
}
Timber.i("Mobile data capabilities changed") Timber.i("Mobile data capabilities changed")
} }
is NetworkStatus.Unavailable -> { is NetworkStatus.Unavailable -> {
networkEventsFlow.value = networkEventsFlow.update {
networkEventsFlow.value.copy( it.copy(
isMobileDataConnected = false, isMobileDataConnected = false,
) )
}
Timber.i("Lost mobile data connection") Timber.i("Lost mobile data connection")
} }
} }
@ -249,53 +249,48 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
} }
private suspend fun watchForSettingsChanges() { private suspend fun watchForSettingsChanges() {
appDataRepository.settings.getSettingsFlow().collect { appDataRepository.settings.getSettingsFlow().collect { settings ->
if (networkEventsFlow.value.settings.isAutoTunnelPaused != it.isAutoTunnelPaused) { if (networkEventsFlow.value.settings.isAutoTunnelPaused != settings.isAutoTunnelPaused) {
when (it.isAutoTunnelPaused) { when (settings.isAutoTunnelPaused) {
true -> launchWatcherPausedNotification() true -> launchWatcherPausedNotification()
false -> launchWatcherNotification() false -> launchWatcherNotification()
} }
} }
networkEventsFlow.value = networkEventsFlow.update {
networkEventsFlow.value.copy( it.copy(
settings = it, settings = settings,
) )
} }
} }
private suspend fun watchForVpnConnectivityChanges() {
vpnService.vpnState.collect {
networkEventsFlow.value =
networkEventsFlow.value.copy(
config = it.tunnelConfig,
)
}
} }
private suspend fun watchForEthernetConnectivityChanges() { private suspend fun watchForEthernetConnectivityChanges() {
ethernetService.networkStatus.collect { ethernetService.networkStatus.collect { status ->
when (it) { when (status) {
is NetworkStatus.Available -> { is NetworkStatus.Available -> {
Timber.i("Gained Ethernet connection") Timber.i("Gained Ethernet connection")
networkEventsFlow.value = networkEventsFlow.update {
networkEventsFlow.value.copy( it.copy(
isEthernetConnected = true, isEthernetConnected = true,
) )
} }
}
is NetworkStatus.CapabilitiesChanged -> { is NetworkStatus.CapabilitiesChanged -> {
Timber.i("Ethernet capabilities changed") Timber.i("Ethernet capabilities changed")
networkEventsFlow.value = networkEventsFlow.update {
networkEventsFlow.value.copy( it.copy(
isEthernetConnected = true, isEthernetConnected = true,
) )
} }
}
is NetworkStatus.Unavailable -> { is NetworkStatus.Unavailable -> {
networkEventsFlow.value = networkEventsFlow.update {
networkEventsFlow.value.copy( it.copy(
isEthernetConnected = false, isEthernetConnected = false,
) )
}
Timber.i("Lost Ethernet connection") Timber.i("Lost Ethernet connection")
} }
} }
@ -303,40 +298,43 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
} }
private suspend fun watchForWifiConnectivityChanges() { private suspend fun watchForWifiConnectivityChanges() {
wifiService.networkStatus.collect { wifiService.networkStatus.collect { status ->
when (it) { when (status) {
is NetworkStatus.Available -> { is NetworkStatus.Available -> {
Timber.i("Gained Wi-Fi connection") Timber.i("Gained Wi-Fi connection")
networkEventsFlow.value = networkEventsFlow.update {
networkEventsFlow.value.copy( it.copy(
isWifiConnected = true, isWifiConnected = true,
) )
} }
}
is NetworkStatus.CapabilitiesChanged -> { is NetworkStatus.CapabilitiesChanged -> {
Timber.i("Wifi capabilities changed") Timber.i("Wifi capabilities changed")
networkEventsFlow.value = networkEventsFlow.update {
networkEventsFlow.value.copy( it.copy(
isWifiConnected = true, isWifiConnected = true,
) )
val ssid = wifiService.getNetworkName(it.networkCapabilities) }
val ssid = wifiService.getNetworkName(status.networkCapabilities)
ssid?.let { name -> ssid?.let { name ->
if(name.contains(Constants.UNREADABLE_SSID)) { if(name.contains(Constants.UNREADABLE_SSID)) {
Timber.w("SSID unreadable: missing permissions") Timber.w("SSID unreadable: missing permissions")
} else Timber.i("Detected valid SSID") } else Timber.i("Detected valid SSID")
appDataRepository.appState.setCurrentSsid(name) appDataRepository.appState.setCurrentSsid(name)
networkEventsFlow.value = networkEventsFlow.update {
networkEventsFlow.value.copy( it.copy(
currentNetworkSSID = name, currentNetworkSSID = name,
) )
}
} ?: Timber.w("Failed to read ssid") } ?: Timber.w("Failed to read ssid")
} }
is NetworkStatus.Unavailable -> { is NetworkStatus.Unavailable -> {
networkEventsFlow.value = networkEventsFlow.update {
networkEventsFlow.value.copy( it.copy(
isWifiConnected = false, isWifiConnected = false,
) )
}
Timber.i("Lost Wi-Fi connection") Timber.i("Lost Wi-Fi connection")
} }
} }
@ -361,6 +359,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
if (!watcherState.settings.isAutoTunnelPaused) { if (!watcherState.settings.isAutoTunnelPaused) {
//delay for rapid network state changes and then collect latest //delay for rapid network state changes and then collect latest
delay(Constants.WATCHER_COLLECTION_DELAY) delay(Constants.WATCHER_COLLECTION_DELAY)
val tunnelConfig = vpnService.vpnState.value.tunnelConfig
when { when {
watcherState.isEthernetConditionMet() -> { watcherState.isEthernetConditionMet() -> {
Timber.i("$autoTunnel - tunnel on on ethernet condition met") Timber.i("$autoTunnel - tunnel on on ethernet condition met")
@ -373,6 +372,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
} }
watcherState.isTunnelOnMobileDataPreferredConditionMet() -> { watcherState.isTunnelOnMobileDataPreferredConditionMet() -> {
if(tunnelConfig?.isMobileDataTunnel == false) {
getMobileDataTunnel()?.let { getMobileDataTunnel()?.let {
Timber.i("$autoTunnel - tunnel connected on mobile data is not preferred condition met, switching to preferred") Timber.i("$autoTunnel - tunnel connected on mobile data is not preferred condition met, switching to preferred")
if(isTunnelDown()) serviceManager.startVpnServiceForeground( if(isTunnelDown()) serviceManager.startVpnServiceForeground(
@ -381,13 +381,16 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
) )
} }
} }
}
watcherState.isTunnelOffOnMobileDataConditionMet() -> { watcherState.isTunnelOffOnMobileDataConditionMet() -> {
Timber.i("$autoTunnel - tunnel off on mobile data met, turning vpn off") Timber.i("$autoTunnel - tunnel off on mobile data met, turning vpn off")
if(!isTunnelDown()) serviceManager.stopVpnServiceForeground(this) if(!isTunnelDown()) serviceManager.stopVpnServiceForeground(this)
} }
watcherState.isTunnelNotWifiNamePreferredMet(watcherState.currentNetworkSSID) -> { watcherState.isUntrustedWifiConditionMet() -> {
if(tunnelConfig?.tunnelNetworks?.contains(watcherState.currentNetworkSSID) == false ||
tunnelConfig == null) {
Timber.i("$autoTunnel - tunnel on ssid not associated with current tunnel condition met") Timber.i("$autoTunnel - tunnel on ssid not associated with current tunnel condition met")
getSsidTunnel(watcherState.currentNetworkSSID)?.let { getSsidTunnel(watcherState.currentNetworkSSID)?.let {
Timber.i("Found tunnel associated with this SSID, bringing tunnel up") Timber.i("Found tunnel associated with this SSID, bringing tunnel up")
@ -399,13 +402,6 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
} }
}.invoke() }.invoke()
} }
watcherState.isUntrustedWifiConditionMet() -> {
Timber.i("$autoTunnel - tunnel on untrusted wifi condition met")
if(isTunnelDown()) serviceManager.startVpnServiceForeground(
this,
getSsidTunnel(watcherState.currentNetworkSSID)?.id,
)
} }
watcherState.isTrustedWifiConditionMet() -> { watcherState.isTrustedWifiConditionMet() -> {

View File

@ -5,14 +5,12 @@ import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.core.app.ServiceCompat import androidx.core.app.ServiceCompat
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.handshakeStatus import com.zaneschepke.wireguardautotunnel.util.handshakeStatus

View File

@ -22,7 +22,6 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.amnezia.awg.backend.Tunnel import org.amnezia.awg.backend.Tunnel
import org.amnezia.awg.config.Config
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -130,10 +129,15 @@ constructor(
) )
} }
private fun resetVpnState() {
_vpnState.tryEmit(VpnState())
}
override suspend fun stopTunnel() { override suspend fun stopTunnel() {
try { try {
if (getState() == TunnelState.UP) { if (getState() == TunnelState.UP) {
val state = setState(null, TunnelState.DOWN) val state = setState(null, TunnelState.DOWN)
resetVpnState()
emitTunnelState(state) emitTunnelState(state)
} }
} catch (e: BackendException) { } catch (e: BackendException) {
@ -160,7 +164,7 @@ constructor(
private fun handleStateChange(state: TunnelState) { private fun handleStateChange(state: TunnelState) {
val tunnel = this val tunnel = this
emitTunnelState(state) emitTunnelState(state)
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(WireGuardAutoTunnel.instance) WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
if (state == TunnelState.UP) { if (state == TunnelState.UP) {
statsJob = statsJob =
scope.launch { scope.launch {

View File

@ -1,6 +1,5 @@
package com.zaneschepke.wireguardautotunnel.ui package com.zaneschepke.wireguardautotunnel.ui
import android.app.Application
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@ -9,7 +8,6 @@ import android.widget.Toast
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.journeyapps.barcodescanner.BarcodeEncoder
import com.wireguard.android.backend.GoBackend import com.wireguard.android.backend.GoBackend
import com.zaneschepke.logcatter.Logcatter import com.zaneschepke.logcatter.Logcatter
import com.zaneschepke.logcatter.model.LogMessage import com.zaneschepke.logcatter.model.LogMessage

View File

@ -87,7 +87,7 @@ class MainActivity : AppCompatActivity() {
// load preferences into memory and init data // load preferences into memory and init data
lifecycleScope.launch { lifecycleScope.launch {
dataStoreManager.init() dataStoreManager.init()
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(this@MainActivity) WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
val settings = settingsRepository.getSettings() val settings = settingsRepository.getSettings()
if (settings.isAutoTunnelEnabled) { if (settings.isAutoTunnelEnabled) {
serviceManager.startWatcherService(application.applicationContext) serviceManager.startWatcherService(application.applicationContext)

View File

@ -4,11 +4,9 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Home import androidx.compose.material.icons.rounded.Home
import androidx.compose.material.icons.rounded.QuestionMark import androidx.compose.material.icons.rounded.QuestionMark
import androidx.compose.material.icons.rounded.Settings import androidx.compose.material.icons.rounded.Settings
import androidx.compose.ui.res.stringResource
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
import com.zaneschepke.wireguardautotunnel.util.StringValue
sealed class Screen(val route: String) { sealed class Screen(val route: String) {
data object Main : Screen("main") { data object Main : Screen("main") {

View File

@ -81,8 +81,7 @@ import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.Event import com.zaneschepke.wireguardautotunnel.util.getMessage
import com.zaneschepke.wireguardautotunnel.util.Result
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@ -150,11 +149,11 @@ fun ConfigScreen(
}, },
onError = { onError = {
showAuthPrompt = false showAuthPrompt = false
appViewModel.showSnackbarMessage(Event.Error.AuthenticationFailed.message) appViewModel.showSnackbarMessage(context.getString(R.string.error_authentication_failed))
}, },
onFailure = { onFailure = {
showAuthPrompt = false showAuthPrompt = false
appViewModel.showSnackbarMessage(Event.Error.AuthorizationFailed.message) appViewModel.showSnackbarMessage(context.getString(R.string.error_authorization_failed))
}, },
) )
} }
@ -321,15 +320,11 @@ fun ConfigScreen(
} }
}, },
onClick = { onClick = {
viewModel.onSaveAllChanges(configType).let { viewModel.onSaveAllChanges(configType).onSuccess {
when (it) { appViewModel.showSnackbarMessage(context.getString(R.string.config_changes_saved))
is Result.Success -> {
appViewModel.showSnackbarMessage(it.data.message)
navController.navigate(Screen.Main.route) navController.navigate(Screen.Main.route)
} }.onFailure {
appViewModel.showSnackbarMessage(it.getMessage(context))
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
}
} }
}, },
containerColor = fobColor, containerColor = fobColor,

View File

@ -1,7 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.config package com.zaneschepke.wireguardautotunnel.ui.screens.config
import android.Manifest import android.Manifest
import android.app.Application
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build import android.os.Build
@ -12,6 +11,7 @@ import com.wireguard.config.Interface
import com.wireguard.config.Peer import com.wireguard.config.Peer
import com.wireguard.crypto.Key import com.wireguard.crypto.Key
import com.wireguard.crypto.KeyPair import com.wireguard.crypto.KeyPair
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
@ -19,9 +19,9 @@ import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.ui.screens.config.model.PeerProxy import com.zaneschepke.wireguardautotunnel.ui.screens.config.model.PeerProxy
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.Event
import com.zaneschepke.wireguardautotunnel.util.NumberUtils import com.zaneschepke.wireguardautotunnel.util.NumberUtils
import com.zaneschepke.wireguardautotunnel.util.Result import com.zaneschepke.wireguardautotunnel.util.StringValue
import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions
import com.zaneschepke.wireguardautotunnel.util.removeAt import com.zaneschepke.wireguardautotunnel.util.removeAt
import com.zaneschepke.wireguardautotunnel.util.update import com.zaneschepke.wireguardautotunnel.util.update
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
@ -37,12 +37,11 @@ import javax.inject.Inject
class ConfigViewModel class ConfigViewModel
@Inject @Inject
constructor( constructor(
private val application: Application,
private val settingsRepository: SettingsRepository, private val settingsRepository: SettingsRepository,
private val appDataRepository: AppDataRepository private val appDataRepository: AppDataRepository
) : ViewModel() { ) : ViewModel() {
private val packageManager = application.packageManager private val packageManager = WireGuardAutoTunnel.instance.packageManager
private val _uiState = MutableStateFlow(ConfigUiState()) private val _uiState = MutableStateFlow(ConfigUiState())
val uiState = _uiState.asStateFlow() val uiState = _uiState.asStateFlow()
@ -109,7 +108,7 @@ constructor(
} }
fun getPackageLabel(packageInfo: PackageInfo): String { fun getPackageLabel(packageInfo: PackageInfo): String {
return packageInfo.applicationInfo.loadLabel(application.packageManager).toString() return packageInfo.applicationInfo.loadLabel(packageManager).toString()
} }
private fun getAllInternetCapablePackages(): List<PackageInfo> { private fun getAllInternetCapablePackages(): List<PackageInfo> {
@ -138,7 +137,7 @@ constructor(
viewModelScope.launch { viewModelScope.launch {
if (tunnelConfig != null) { if (tunnelConfig != null) {
saveConfig(tunnelConfig).join() saveConfig(tunnelConfig).join()
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(application) WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
} }
} }
@ -249,7 +248,7 @@ constructor(
return org.amnezia.awg.config.Config.Builder().addPeers(peerList).setInterface(amInterface).build() return org.amnezia.awg.config.Config.Builder().addPeers(peerList).setInterface(amInterface).build()
} }
fun onSaveAllChanges(configType: ConfigType): Result<Event> { fun onSaveAllChanges(configType: ConfigType): Result<Unit> {
return try { return try {
val wgQuick = buildConfig().toWgQuickString() val wgQuick = buildConfig().toWgQuickString()
val amQuick = if(configType == ConfigType.AMNEZIA) { val amQuick = if(configType == ConfigType.AMNEZIA) {
@ -268,11 +267,14 @@ constructor(
) )
} }
updateTunnelConfig(tunnelConfig) updateTunnelConfig(tunnelConfig)
Result.Success(Event.Message.ConfigSaved) Result.success(Unit)
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e) Timber.e(e)
val message = e.message?.substringAfter(":", missingDelimiterValue = "") val message = e.message?.substringAfter(":", missingDelimiterValue = "")
Result.Error(Event.Error.ConfigParseError(message ?: "")) val stringValue = message?.let {
StringValue.DynamicString(message)
} ?: StringValue.StringResource(R.string.unknown_error)
Result.failure(WgTunnelExceptions.ConfigParseError(stringValue))
} }
} }

View File

@ -108,8 +108,7 @@ import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
import com.zaneschepke.wireguardautotunnel.ui.theme.corn import com.zaneschepke.wireguardautotunnel.ui.theme.corn
import com.zaneschepke.wireguardautotunnel.ui.theme.mint import com.zaneschepke.wireguardautotunnel.ui.theme.mint
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.Event import com.zaneschepke.wireguardautotunnel.util.getMessage
import com.zaneschepke.wireguardautotunnel.util.Result
import com.zaneschepke.wireguardautotunnel.util.handshakeStatus import com.zaneschepke.wireguardautotunnel.util.handshakeStatus
import com.zaneschepke.wireguardautotunnel.util.mapPeerStats import com.zaneschepke.wireguardautotunnel.util.mapPeerStats
import com.zaneschepke.wireguardautotunnel.util.truncateWithEllipsis import com.zaneschepke.wireguardautotunnel.util.truncateWithEllipsis
@ -194,7 +193,7 @@ fun MainScreen(
name.startsWith(Constants.ANDROID_TV_EXPLORER_STUB) name.startsWith(Constants.ANDROID_TV_EXPLORER_STUB)
} }
) { ) {
appViewModel.showSnackbarMessage(Event.Error.FileExplorerRequired.message) appViewModel.showSnackbarMessage(context.getString(R.string.error_no_file_explorer))
} }
return intent return intent
} }
@ -202,11 +201,8 @@ fun MainScreen(
) { data -> ) { data ->
if (data == null) return@rememberLauncherForActivityResult if (data == null) return@rememberLauncherForActivityResult
scope.launch { scope.launch {
viewModel.onTunnelFileSelected(data, configType).let { viewModel.onTunnelFileSelected(data, configType, context).onFailure {
when (it) { appViewModel.showSnackbarMessage(it.getMessage(context))
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
is Result.Success -> {}
}
} }
} }
} }
@ -216,11 +212,8 @@ fun MainScreen(
onResult = { onResult = {
if (it.contents != null) { if (it.contents != null) {
scope.launch { scope.launch {
viewModel.onTunnelQrResult(it.contents, configType).let { result -> viewModel.onTunnelQrResult(it.contents, configType).onFailure {
when (result) { appViewModel.showSnackbarMessage(it.getMessage(context))
is Result.Success -> {}
is Result.Error -> appViewModel.showSnackbarMessage(result.error.message)
}
} }
} }
} }
@ -233,7 +226,7 @@ fun MainScreen(
confirmButton = { confirmButton = {
TextButton( TextButton(
onClick = { onClick = {
selectedTunnel?.let { viewModel.onDelete(it) } selectedTunnel?.let { viewModel.onDelete(it, context) }
showDeleteTunnelAlertDialog = false showDeleteTunnelAlertDialog = false
selectedTunnel = null selectedTunnel = null
}, },
@ -253,7 +246,7 @@ fun MainScreen(
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) { fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) {
if (appViewModel.isRequiredPermissionGranted()) { if (appViewModel.isRequiredPermissionGranted()) {
if (checked) viewModel.onTunnelStart(tunnel) else viewModel.onTunnelStop() if (checked) viewModel.onTunnelStart(tunnel, context) else viewModel.onTunnelStop(context)
} }
} }
@ -575,7 +568,7 @@ fun MainScreen(
(uiState.vpnState.status == TunnelState.UP) && (uiState.vpnState.status == TunnelState.UP) &&
(tunnel.name == uiState.vpnState.tunnelConfig?.name) (tunnel.name == uiState.vpnState.tunnelConfig?.name)
) { ) {
appViewModel.showSnackbarMessage(Event.Message.TunnelOffAction.message) appViewModel.showSnackbarMessage(context.getString(R.string.turn_off_tunnel))
return@RowListItem return@RowListItem
} }
haptic.performHapticFeedback(HapticFeedbackType.LongPress) haptic.performHapticFeedback(HapticFeedbackType.LongPress)
@ -609,7 +602,7 @@ fun MainScreen(
!uiState.settings.isAutoTunnelPaused !uiState.settings.isAutoTunnelPaused
) { ) {
appViewModel.showSnackbarMessage( appViewModel.showSnackbarMessage(
Event.Message.AutoTunnelOffAction.message, context.getString(R.string.turn_off_tunnel),
) )
} else { } else {
navController.navigate( navController.navigate(
@ -664,7 +657,7 @@ fun MainScreen(
onClick = { onClick = {
if (uiState.settings.isAutoTunnelEnabled) { if (uiState.settings.isAutoTunnelEnabled) {
appViewModel.showSnackbarMessage( appViewModel.showSnackbarMessage(
Event.Message.AutoTunnelOffAction.message, context.getString(R.string.turn_off_auto),
) )
} else { } else {
selectedTunnel = tunnel selectedTunnel = tunnel
@ -690,7 +683,7 @@ fun MainScreen(
expanded.value = !expanded.value expanded.value = !expanded.value
} else { } else {
appViewModel.showSnackbarMessage( appViewModel.showSnackbarMessage(
Event.Message.TunnelOnAction.message, context.getString(R.string.turn_on_tunnel),
) )
} }
}, },
@ -711,7 +704,7 @@ fun MainScreen(
tunnel.name == uiState.vpnState.tunnelConfig?.name tunnel.name == uiState.vpnState.tunnelConfig?.name
) { ) {
appViewModel.showSnackbarMessage( appViewModel.showSnackbarMessage(
Event.Message.TunnelOffAction.message, context.getString(R.string.turn_off_tunnel),
) )
} else { } else {
selectedTunnel = tunnel selectedTunnel = tunnel

View File

@ -1,6 +1,5 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main package com.zaneschepke.wireguardautotunnel.ui.screens.main
import android.app.Application
import android.content.Context import android.content.Context
import android.database.Cursor import android.database.Cursor
import android.net.Uri import android.net.Uri
@ -15,9 +14,8 @@ import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.Event
import com.zaneschepke.wireguardautotunnel.util.NumberUtils import com.zaneschepke.wireguardautotunnel.util.NumberUtils
import com.zaneschepke.wireguardautotunnel.util.Result import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions
import com.zaneschepke.wireguardautotunnel.util.toWgQuickString import com.zaneschepke.wireguardautotunnel.util.toWgQuickString
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -25,6 +23,7 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
import java.io.InputStream import java.io.InputStream
import java.util.zip.ZipInputStream import java.util.zip.ZipInputStream
@ -34,7 +33,6 @@ import javax.inject.Inject
class MainViewModel class MainViewModel
@Inject @Inject
constructor( constructor(
private val application: Application,
private val appDataRepository: AppDataRepository, private val appDataRepository: AppDataRepository,
private val serviceManager: ServiceManager, private val serviceManager: ServiceManager,
val vpnService: VpnService val vpnService: VpnService
@ -54,21 +52,21 @@ constructor(
MainUiState(), MainUiState(),
) )
private fun stopWatcherService() = private fun stopWatcherService(context: Context) =
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
serviceManager.stopWatcherService(application.applicationContext) serviceManager.stopWatcherService(context)
} }
fun onDelete(tunnel: TunnelConfig) { fun onDelete(tunnel: TunnelConfig, context: Context) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val settings = appDataRepository.settings.getSettings() val settings = appDataRepository.settings.getSettings()
val isPrimary = tunnel.isPrimaryTunnel val isPrimary = tunnel.isPrimaryTunnel
if (appDataRepository.tunnels.count() == 1 || isPrimary) { if (appDataRepository.tunnels.count() == 1 || isPrimary) {
stopWatcherService() stopWatcherService(context)
resetTunnelSetting(settings) resetTunnelSetting(settings)
} }
appDataRepository.tunnels.delete(tunnel) appDataRepository.tunnels.delete(tunnel)
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(application) WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
} }
} }
@ -81,21 +79,21 @@ constructor(
) )
} }
fun onTunnelStart(tunnelConfig: TunnelConfig) = fun onTunnelStart(tunnelConfig: TunnelConfig, context: Context) =
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
Timber.d("On start called!") Timber.d("On start called!")
serviceManager.startVpnService( serviceManager.startVpnService(
application.applicationContext, context,
tunnelConfig.id, tunnelConfig.id,
isManualStart = true, isManualStart = true,
) )
} }
fun onTunnelStop() = fun onTunnelStop(context: Context) =
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
Timber.i("Stopping active tunnel") Timber.i("Stopping active tunnel")
serviceManager.stopVpnService(application.applicationContext, isManualStop = true) serviceManager.stopVpnService(context, isManualStop = true)
} }
private fun validateConfigString(config: String, configType: ConfigType) { private fun validateConfigString(config: String, configType: ConfigType) {
@ -105,24 +103,66 @@ constructor(
} }
} }
private fun generateQrCodeDefaultName(config : String, configType: ConfigType) : String {
return try {
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> { suspend fun onTunnelQrResult(result: String, configType: ConfigType): Result<Unit> {
return try { return try {
validateConfigString(result, configType) validateConfigString(result, configType)
val tunnelName = makeTunnelNameUnique(generateQrCodeTunnelName(result, configType))
val tunnelConfig = when(configType) { val tunnelConfig = when(configType) {
ConfigType.AMNEZIA ->{ ConfigType.AMNEZIA ->{
TunnelConfig(name = NumberUtils.generateRandomTunnelName(), amQuick = result, TunnelConfig(name = tunnelName, amQuick = result,
wgQuick = TunnelConfig.configFromAmQuick(result).toWgQuickString()) wgQuick = TunnelConfig.configFromAmQuick(result).toWgQuickString())
} }
ConfigType.WIREGUARD -> TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = result) ConfigType.WIREGUARD -> TunnelConfig(name = tunnelName, wgQuick = result)
} }
addTunnel(tunnelConfig) addTunnel(tunnelConfig)
Result.Success(Unit) Result.success(Unit)
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e) Timber.e(e)
Result.Error(Event.Error.InvalidQrCode) Result.failure(WgTunnelExceptions.InvalidQrCode())
} }
} }
private suspend fun 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 suspend fun saveTunnelConfigFromStream(stream: InputStream, fileName: String, type: ConfigType) { private suspend fun saveTunnelConfigFromStream(stream: InputStream, fileName: String, type: ConfigType) {
var amQuick : String? = null var amQuick : String? = null
val wgQuick = stream.use { val wgQuick = stream.use {
@ -137,42 +177,35 @@ constructor(
} }
} }
} }
val tunnelName = getNameFromFileName(fileName) val tunnelName = makeTunnelNameUnique(getNameFromFileName(fileName))
addTunnel(TunnelConfig(name = tunnelName, wgQuick = wgQuick, amQuick = amQuick ?: TunnelConfig.AM_QUICK_DEFAULT)) addTunnel(TunnelConfig(name = tunnelName, wgQuick = wgQuick, amQuick = amQuick ?: TunnelConfig.AM_QUICK_DEFAULT))
} }
private fun getInputStreamFromUri(uri: Uri): InputStream? { private fun getInputStreamFromUri(uri: Uri, context: Context): InputStream? {
return application.applicationContext.contentResolver.openInputStream(uri) return context.applicationContext.contentResolver.openInputStream(uri)
} }
suspend fun onTunnelFileSelected(uri: Uri, configType: ConfigType): Result<Unit> { suspend fun onTunnelFileSelected(uri: Uri, configType: ConfigType, context: Context): Result<Unit> {
try { return try {
if (isValidUriContentScheme(uri)) { if (isValidUriContentScheme(uri)) {
val fileName = getFileName(application.applicationContext, uri) val fileName = getFileName(context, uri)
when (getFileExtensionFromFileName(fileName)) { return when (getFileExtensionFromFileName(fileName)) {
Constants.CONF_FILE_EXTENSION -> Constants.CONF_FILE_EXTENSION ->
saveTunnelFromConfUri(fileName, uri, configType).let { saveTunnelFromConfUri(fileName, uri, configType, context)
when (it) { Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri, configType, context)
is Result.Error -> return Result.Error(Event.Error.FileReadFailed) else -> Result.failure(WgTunnelExceptions.InvalidFileExtension())
is Result.Success -> return it
} }
}
Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri, configType)
else -> return Result.Error(Event.Error.InvalidFileExtension)
}
return Result.Success(Unit)
} else { } else {
return Result.Error(Event.Error.InvalidFileExtension) Result.failure(WgTunnelExceptions.InvalidFileExtension())
} }
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e) Timber.e(e)
return Result.Error(Event.Error.FileReadFailed) Result.failure(WgTunnelExceptions.FileReadFailed())
} }
} }
private suspend fun saveTunnelsFromZipUri(uri: Uri, configType: ConfigType) { private suspend fun saveTunnelsFromZipUri(uri: Uri, configType: ConfigType, context: Context) : Result<Unit> {
ZipInputStream(getInputStreamFromUri(uri)).use { zip -> return ZipInputStream(getInputStreamFromUri(uri, context)).use { zip ->
generateSequence { zip.nextEntry } generateSequence { zip.nextEntry }
.filterNot { .filterNot {
it.isDirectory || it.isDirectory ||
@ -180,7 +213,8 @@ constructor(
} }
.forEach { .forEach {
val name = getNameFromFileName(it.name) val name = getNameFromFileName(it.name)
viewModelScope.launch(Dispatchers.IO) { withContext(viewModelScope.coroutineContext + Dispatchers.IO) {
try {
var amQuick : String? = null var amQuick : String? = null
val wgQuick = val wgQuick =
when(configType) { when(configType) {
@ -193,38 +227,43 @@ constructor(
Config.parse(zip).toWgQuickString() Config.parse(zip).toWgQuickString()
} }
} }
addTunnel(TunnelConfig(name = name, wgQuick = wgQuick, amQuick = amQuick ?: TunnelConfig.AM_QUICK_DEFAULT)) 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, configType: ConfigType): Result<Unit> { private suspend fun saveTunnelFromConfUri(name: String, uri: Uri, configType: ConfigType, context: Context): Result<Unit> {
val stream = getInputStreamFromUri(uri) val stream = getInputStreamFromUri(uri, context)
return if (stream != null) { return if (stream != null) {
saveTunnelConfigFromStream(stream, name, configType) saveTunnelConfigFromStream(stream, name, configType)
Result.Success(Unit) Result.success(Unit)
} else { } else {
Result.Error(Event.Error.FileReadFailed) Result.failure(WgTunnelExceptions.FileReadFailed())
} }
} }
private suspend fun addTunnel(tunnelConfig: TunnelConfig) { private suspend fun addTunnel(tunnelConfig: TunnelConfig) {
val firstTunnel = appDataRepository.tunnels.count() == 0 val firstTunnel = appDataRepository.tunnels.count() == 0
saveTunnel(tunnelConfig) saveTunnel(tunnelConfig)
if (firstTunnel) WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(application) if (firstTunnel) WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
} }
fun pauseAutoTunneling() = fun pauseAutoTunneling() =
viewModelScope.launch { viewModelScope.launch {
appDataRepository.settings.save(uiState.value.settings.copy(isAutoTunnelPaused = true)) appDataRepository.settings.save(uiState.value.settings.copy(isAutoTunnelPaused = true))
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate(application) WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate()
} }
fun resumeAutoTunneling() = fun resumeAutoTunneling() =
viewModelScope.launch { viewModelScope.launch {
appDataRepository.settings.save(uiState.value.settings.copy(isAutoTunnelPaused = false)) appDataRepository.settings.save(uiState.value.settings.copy(isAutoTunnelPaused = false))
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate(application) WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate()
} }
private suspend fun saveTunnel(tunnelConfig: TunnelConfig) { private suspend fun saveTunnel(tunnelConfig: TunnelConfig) {
@ -268,12 +307,12 @@ constructor(
return fileName.substring(0, fileName.lastIndexOf('.')) return fileName.substring(0, fileName.lastIndexOf('.'))
} }
private fun getFileExtensionFromFileName(fileName: String): String { private fun getFileExtensionFromFileName(fileName: String): String? {
return try { return try {
fileName.substring(fileName.lastIndexOf('.')) fileName.substring(fileName.lastIndexOf('.'))
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e) Timber.e(e)
"" null
} }
} }

View File

@ -27,7 +27,6 @@ import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@ -42,6 +41,7 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
@ -65,7 +65,7 @@ import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.Result import com.zaneschepke.wireguardautotunnel.util.getMessage
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -81,6 +81,8 @@ fun OptionsScreen(
) { ) {
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
val uiState by optionsViewModel.uiState.collectAsStateWithLifecycle() val uiState by optionsViewModel.uiState.collectAsStateWithLifecycle()
val context = LocalContext.current
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
@ -100,11 +102,10 @@ fun OptionsScreen(
fun saveTrustedSSID() { fun saveTrustedSSID() {
if (currentText.isNotEmpty()) { if (currentText.isNotEmpty()) {
scope.launch { scope.launch {
optionsViewModel.onSaveRunSSID(currentText).let { optionsViewModel.onSaveRunSSID(currentText).onSuccess {
when (it) { currentText = ""
is Result.Success -> currentText = "" }.onFailure {
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message) appViewModel.showSnackbarMessage(it.getMessage(context))
}
} }
} }
} }

View File

@ -7,8 +7,7 @@ import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.Event import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions
import com.zaneschepke.wireguardautotunnel.util.Result
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -76,9 +75,9 @@ constructor(
tunnelsWithName.isEmpty()) { tunnelsWithName.isEmpty()) {
uiState.value.tunnel?.tunnelNetworks?.add(trimmed) uiState.value.tunnel?.tunnelNetworks?.add(trimmed)
saveTunnel(uiState.value.tunnel) saveTunnel(uiState.value.tunnel)
Result.Success(Unit) Result.success(Unit)
} else { } else {
Result.Error(Event.Error.SsidConflict) Result.failure(WgTunnelExceptions.SsidConflict())
} }
} }
@ -98,7 +97,7 @@ constructor(
false -> uiState.value.tunnel false -> uiState.value.tunnel
}, },
) )
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(WireGuardAutoTunnel.instance) WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
} }
} }
} }

View File

@ -70,7 +70,6 @@ import androidx.navigation.NavController
import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState import com.google.accompanist.permissions.rememberPermissionState
import com.wireguard.android.backend.Tunnel
import com.wireguard.android.backend.WgQuickBackend import com.wireguard.android.backend.WgQuickBackend
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
@ -82,9 +81,8 @@ import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
import com.zaneschepke.wireguardautotunnel.util.Event
import com.zaneschepke.wireguardautotunnel.util.FileUtils import com.zaneschepke.wireguardautotunnel.util.FileUtils
import com.zaneschepke.wireguardautotunnel.util.Result import com.zaneschepke.wireguardautotunnel.util.getMessage
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
@ -146,12 +144,14 @@ fun SettingsScreen(
} }
file file
} else null } } else null }
FileUtils.saveFilesToZip(context, wgFiles + amFiles) FileUtils.saveFilesToZip(context, wgFiles + amFiles).onFailure {
appViewModel.showSnackbarMessage(it.getMessage(context))
}.onSuccess {
didExportFiles = true didExportFiles = true
appViewModel.showSnackbarMessage(Event.Message.ConfigsExported.message) appViewModel.showSnackbarMessage(context.getString(R.string.exported_configs_message))
}
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e) Timber.e(e)
appViewModel.showSnackbarMessage(Event.Error.Exception(e).message)
} }
} }
@ -172,7 +172,7 @@ fun SettingsScreen(
fun handleAutoTunnelToggle() { fun handleAutoTunnelToggle() {
if (uiState.isBatteryOptimizeDisableShown || isBatteryOptimizationsDisabled()) { if (uiState.isBatteryOptimizeDisableShown || isBatteryOptimizationsDisabled()) {
if (appViewModel.isRequiredPermissionGranted()) { if (appViewModel.isRequiredPermissionGranted()) {
viewModel.onToggleAutoTunnel() viewModel.onToggleAutoTunnel(context)
} }
} else { } else {
requestBatteryOptimizationsDisabled() requestBatteryOptimizationsDisabled()
@ -181,11 +181,10 @@ fun SettingsScreen(
fun saveTrustedSSID() { fun saveTrustedSSID() {
if (currentText.isNotEmpty()) { if (currentText.isNotEmpty()) {
viewModel.onSaveTrustedSSID(currentText).let { viewModel.onSaveTrustedSSID(currentText).onSuccess {
when (it) { currentText = ""
is Result.Success -> currentText = "" }.onFailure {
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message) appViewModel.showSnackbarMessage(it.getMessage(context))
}
} }
} }
} }
@ -319,11 +318,11 @@ fun SettingsScreen(
}, },
onError = { _ -> onError = { _ ->
showAuthPrompt = false showAuthPrompt = false
appViewModel.showSnackbarMessage(Event.Error.AuthenticationFailed.message) appViewModel.showSnackbarMessage(context.getString(R.string.error_authentication_failed))
}, },
onFailure = { onFailure = {
showAuthPrompt = false showAuthPrompt = false
appViewModel.showSnackbarMessage(Event.Error.AuthorizationFailed.message) appViewModel.showSnackbarMessage(context.getString(R.string.error_authorization_failed))
}, },
) )
} }
@ -531,12 +530,12 @@ fun SettingsScreen(
when (false) { when (false) {
isBackgroundLocationGranted -> isBackgroundLocationGranted ->
appViewModel.showSnackbarMessage( appViewModel.showSnackbarMessage(
Event.Error.BackgroundLocationRequired.message, context.getString(R.string.background_location_required),
) )
fineLocationState.status.isGranted -> fineLocationState.status.isGranted ->
appViewModel.showSnackbarMessage( appViewModel.showSnackbarMessage(
Event.Error.PreciseLocationRequired.message, context.getString(R.string.precise_location_required),
) )
viewModel.isLocationEnabled(context) -> viewModel.isLocationEnabled(context) ->
@ -602,11 +601,8 @@ fun SettingsScreen(
checked = uiState.settings.isKernelEnabled, checked = uiState.settings.isKernelEnabled,
padding = screenPadding, padding = screenPadding,
onCheckChanged = { onCheckChanged = {
viewModel.onToggleKernelMode().let { viewModel.onToggleKernelMode().onFailure {
when (it) { appViewModel.showSnackbarMessage(it.getMessage(context))
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
is Result.Success -> {}
}
} }
}, },
) )

View File

@ -1,6 +1,5 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings package com.zaneschepke.wireguardautotunnel.ui.screens.settings
import android.app.Application
import android.content.Context import android.content.Context
import android.location.LocationManager import android.location.LocationManager
import androidx.core.location.LocationManagerCompat import androidx.core.location.LocationManagerCompat
@ -13,8 +12,7 @@ import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.Event import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions
import com.zaneschepke.wireguardautotunnel.util.Result
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
@ -27,7 +25,6 @@ import javax.inject.Inject
class SettingsViewModel class SettingsViewModel
@Inject @Inject
constructor( constructor(
private val application: Application,
private val appDataRepository: AppDataRepository, private val appDataRepository: AppDataRepository,
private val serviceManager: ServiceManager, private val serviceManager: ServiceManager,
private val rootShell: RootShell, private val rootShell: RootShell,
@ -60,9 +57,9 @@ constructor(
return if (!uiState.value.settings.trustedNetworkSSIDs.contains(trimmed)) { return if (!uiState.value.settings.trustedNetworkSSIDs.contains(trimmed)) {
uiState.value.settings.trustedNetworkSSIDs.add(trimmed) uiState.value.settings.trustedNetworkSSIDs.add(trimmed)
saveSettings(uiState.value.settings) saveSettings(uiState.value.settings)
Result.Success(Unit) Result.success(Unit)
} else { } else {
Result.Error(Event.Error.SsidConflict) Result.failure(WgTunnelExceptions.SsidConflict())
} }
} }
@ -93,15 +90,15 @@ constructor(
) )
} }
fun onToggleAutoTunnel() = fun onToggleAutoTunnel(context: Context) =
viewModelScope.launch { viewModelScope.launch {
val isAutoTunnelEnabled = uiState.value.settings.isAutoTunnelEnabled val isAutoTunnelEnabled = uiState.value.settings.isAutoTunnelEnabled
var isAutoTunnelPaused = uiState.value.settings.isAutoTunnelPaused var isAutoTunnelPaused = uiState.value.settings.isAutoTunnelPaused
if (isAutoTunnelEnabled) { if (isAutoTunnelEnabled) {
serviceManager.stopWatcherService(application) serviceManager.stopWatcherService(context)
} else { } else {
serviceManager.startWatcherService(application) serviceManager.startWatcherService(context)
isAutoTunnelPaused = false isAutoTunnelPaused = false
} }
saveSettings( saveSettings(
@ -110,7 +107,7 @@ constructor(
isAutoTunnelPaused = isAutoTunnelPaused, isAutoTunnelPaused = isAutoTunnelPaused,
), ),
) )
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate(application) WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate()
} }
fun onToggleAlwaysOnVPN() = fun onToggleAlwaysOnVPN() =
@ -192,12 +189,12 @@ constructor(
} catch (e: RootShell.RootShellException) { } catch (e: RootShell.RootShellException) {
Timber.e(e) Timber.e(e)
saveKernelMode(on = false) saveKernelMode(on = false)
return Result.Error(Event.Error.RootDenied) return Result.failure(WgTunnelExceptions.RootDenied())
} }
} else { } else {
saveKernelMode(on = false) saveKernelMode(on = false)
} }
return Result.Success(Unit) return Result.success(Unit)
} }
fun onToggleRestartOnPing() = viewModelScope.launch { fun onToggleRestartOnPing() = viewModelScope.launch {

View File

@ -38,5 +38,6 @@ object Constants {
const val UNREADABLE_SSID = "<unknown ssid>" const val UNREADABLE_SSID = "<unknown ssid>"
val amneziaProperties = listOf("Jc", "Jmin", "Jmax", "S1", "S2", "H1", "H2", "H3", "H4") val amneziaProperties = listOf("Jc", "Jmin", "Jmax", "S1", "S2", "H1", "H2", "H3", "H4")
const val QR_CODE_NAME_PROPERTY = "# Name ="
} }

View File

@ -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_format)
}
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)
}
}
}

View File

@ -1,8 +1,9 @@
package com.zaneschepke.wireguardautotunnel.util package com.zaneschepke.wireguardautotunnel.util
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import com.wireguard.config.Peer import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
@ -11,7 +12,6 @@ import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.amnezia.awg.config.Config import org.amnezia.awg.config.Config
import timber.log.Timber
import java.math.BigDecimal import java.math.BigDecimal
import java.text.DecimalFormat import java.text.DecimalFormat
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
@ -87,3 +87,10 @@ fun Config.toWgQuickString() : String {
} }
return lines.joinToString(System.lineSeparator()) 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)
}
}

View File

@ -6,6 +6,7 @@ import android.os.Build
import android.os.Environment import android.os.Environment
import android.provider.MediaStore import android.provider.MediaStore
import android.provider.MediaStore.MediaColumns import android.provider.MediaStore.MediaColumns
import timber.log.Timber
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.OutputStream import java.io.OutputStream
@ -70,7 +71,8 @@ object FileUtils {
} }
} }
fun saveFilesToZip(context: Context, files: List<File>) { fun saveFilesToZip(context: Context, files: List<File>) : Result<Unit> {
return try {
val zipOutputStream = val zipOutputStream =
createDownloadsFileOutputStream( createDownloadsFileOutputStream(
context, context,
@ -85,6 +87,11 @@ object FileUtils {
file.inputStream().use { fis -> fis.copyTo(zos) } file.inputStream().use { fis -> fis.copyTo(zos) }
} }
} }
return Result.success(Unit)
}
} catch (e : Exception) {
Timber.e(e)
Result.failure(WgTunnelExceptions.ConfigExportFailed())
} }
} }
} }

View File

@ -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)
}
}
}
}

View File

@ -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)
// }
// }
}

View File

@ -94,6 +94,7 @@
<string name="error_authorization_failed">Failed to authorize</string> <string name="error_authorization_failed">Failed to authorize</string>
<string name="enabled_app_shortcuts">Enable app shortcuts</string> <string name="enabled_app_shortcuts">Enable app shortcuts</string>
<string name="export_configs">Export configs</string> <string name="export_configs">Export configs</string>
<string name="export_configs_failed">Failed to export configs</string>
<string name="location_services_required">Location services required</string> <string name="location_services_required">Location services required</string>
<string name="background_location_required">Background location required</string> <string name="background_location_required">Background location required</string>
<string name="precise_location_required">Precise location required</string> <string name="precise_location_required">Precise location required</string>

View File

@ -1,7 +1,7 @@
object Constants { object Constants {
const val VERSION_NAME = "3.4.3" const val VERSION_NAME = "3.4.4"
const val JVM_TARGET = "17" const val JVM_TARGET = "17"
const val VERSION_CODE = 34300 const val VERSION_CODE = 34400
const val TARGET_SDK = 34 const val TARGET_SDK = 34
const val MIN_SDK = 26 const val MIN_SDK = 26
const val APP_ID = "com.zaneschepke.wireguardautotunnel" const val APP_ID = "com.zaneschepke.wireguardautotunnel"

View File

@ -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