diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt index 1b1fe5a6..7a674133 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt @@ -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), ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/TunnelConfigDao.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/TunnelConfigDao.kt index 7eb9000d..d33e61e7 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/TunnelConfigDao.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/TunnelConfigDao.kt @@ -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 diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/RoomTunnelConfigRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/RoomTunnelConfigRepository.kt index f2f79e43..ac36a044 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/RoomTunnelConfigRepository.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/RoomTunnelConfigRepository.kt @@ -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) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/TunnelConfigRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/TunnelConfigRepository.kt index 999464bd..6e4503d9 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/TunnelConfigRepository.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/TunnelConfigRepository.kt @@ -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 diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WatcherState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WatcherState.kt index 5e6c02de..5ac2f72a 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WatcherState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WatcherState.kt @@ -1,11 +1,9 @@ package com.zaneschepke.wireguardautotunnel.service.foreground import com.zaneschepke.wireguardautotunnel.data.domain.Settings -import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig data class WatcherState( val isWifiConnected: Boolean = false, - val config: TunnelConfig? = null, val isEthernetConnected: Boolean = false, val isMobileDataConnected: Boolean = false, val currentNetworkSSID: String = "", @@ -27,8 +25,7 @@ data class WatcherState( return (!isEthernetConnected && settings.isTunnelOnMobileDataEnabled && !isWifiConnected && - isMobileDataConnected && - config?.isMobileDataTunnel == false) + isMobileDataConnected) } fun isTunnelOffOnMobileDataConditionMet(): Boolean { @@ -45,13 +42,6 @@ data class WatcherState( settings.isTunnelOnWifiEnabled) } - fun isTunnelNotWifiNamePreferredMet(ssid: String): Boolean { - return (!isEthernetConnected && - isWifiConnected && - !settings.trustedNetworkSSIDs.contains(currentNetworkSSID) && - settings.isTunnelOnWifiEnabled && config?.tunnelNetworks?.contains(ssid) == false) - } - fun isTrustedWifiConditionMet(): Boolean { return (!isEthernetConnected && (isWifiConnected && diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardConnectivityWatcherService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardConnectivityWatcherService.kt index bbf1d37f..051174f6 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardConnectivityWatcherService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardConnectivityWatcherService.kt @@ -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") } } @@ -249,53 +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( - 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") } } @@ -303,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") } } @@ -361,6 +359,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() { 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") @@ -373,12 +372,14 @@ class WireGuardConnectivityWatcherService : ForegroundService() { } watcherState.isTunnelOnMobileDataPreferredConditionMet() -> { - 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, - ) + 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, + ) + } } } @@ -387,25 +388,20 @@ class WireGuardConnectivityWatcherService : ForegroundService() { if(!isTunnelDown()) 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") - 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.isUntrustedWifiConditionMet() -> { - Timber.i("$autoTunnel - tunnel on untrusted wifi condition met") - if(isTunnelDown()) 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() -> { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardTunnelService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardTunnelService.kt index 61fede62..6ad524e6 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardTunnelService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardTunnelService.kt @@ -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 diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt index 665aea64..63795e8f 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt @@ -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 { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppViewModel.kt index d525571f..b6ff4e64 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppViewModel.kt @@ -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 diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt index dba17e8b..d7798c61 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt @@ -87,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) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Screen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Screen.kt index d1ea9a7c..554a7594 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Screen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Screen.kt @@ -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") { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt index 074c4699..ff9b0567 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt @@ -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.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") @@ -150,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)) }, ) } @@ -321,15 +320,11 @@ fun ConfigScreen( } }, onClick = { - viewModel.onSaveAllChanges(configType).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, diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt index 27f6521d..0bafded9 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt @@ -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,6 +11,7 @@ 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 @@ -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.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 @@ -37,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() @@ -109,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 { @@ -138,7 +137,7 @@ constructor( viewModelScope.launch { if (tunnelConfig != null) { 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() } - fun onSaveAllChanges(configType: ConfigType): Result { + fun onSaveAllChanges(configType: ConfigType): Result { return try { val wgQuick = buildConfig().toWgQuickString() val amQuick = if(configType == ConfigType.AMNEZIA) { @@ -268,11 +267,14 @@ constructor( ) } 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)) } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt index 6b117a8d..e851c241 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt @@ -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.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 @@ -194,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 } @@ -202,11 +201,8 @@ fun MainScreen( ) { data -> if (data == null) return@rememberLauncherForActivityResult scope.launch { - viewModel.onTunnelFileSelected(data, configType).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)) } } } @@ -216,11 +212,8 @@ fun MainScreen( onResult = { if (it.contents != null) { scope.launch { - viewModel.onTunnelQrResult(it.contents, configType).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)) } } } @@ -233,7 +226,7 @@ fun MainScreen( confirmButton = { TextButton( onClick = { - selectedTunnel?.let { viewModel.onDelete(it) } + selectedTunnel?.let { viewModel.onDelete(it, context) } showDeleteTunnelAlertDialog = false selectedTunnel = null }, @@ -253,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) } } @@ -575,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) @@ -609,7 +602,7 @@ fun MainScreen( !uiState.settings.isAutoTunnelPaused ) { appViewModel.showSnackbarMessage( - Event.Message.AutoTunnelOffAction.message, + context.getString(R.string.turn_off_tunnel), ) } else { navController.navigate( @@ -664,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 @@ -690,7 +683,7 @@ fun MainScreen( expanded.value = !expanded.value } else { 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 ) { appViewModel.showSnackbarMessage( - Event.Message.TunnelOffAction.message, + context.getString(R.string.turn_off_tunnel), ) } else { selectedTunnel = tunnel diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt index a541d4b9..d4d45ccd 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt @@ -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,8 @@ 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 @@ -25,6 +23,7 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import timber.log.Timber import java.io.InputStream import java.util.zip.ZipInputStream @@ -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,21 +79,21 @@ 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, configType: ConfigType) { @@ -105,22 +103,64 @@ 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 { return try { validateConfigString(result, configType) + val tunnelName = makeTunnelNameUnique(generateQrCodeTunnelName(result, configType)) val tunnelConfig = when(configType) { ConfigType.AMNEZIA ->{ - TunnelConfig(name = NumberUtils.generateRandomTunnelName(), amQuick = result, + TunnelConfig(name = tunnelName, amQuick = result, wgQuick = TunnelConfig.configFromAmQuick(result).toWgQuickString()) } - ConfigType.WIREGUARD -> TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = result) + 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 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) { @@ -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)) } - private fun getInputStreamFromUri(uri: Uri): InputStream? { - return application.applicationContext.contentResolver.openInputStream(uri) + private fun getInputStreamFromUri(uri: Uri, context: Context): InputStream? { + return context.applicationContext.contentResolver.openInputStream(uri) } - suspend fun onTunnelFileSelected(uri: Uri, configType: ConfigType): Result { - try { + suspend fun onTunnelFileSelected(uri: Uri, configType: ConfigType, context: Context): Result { + return try { if (isValidUriContentScheme(uri)) { - val fileName = getFileName(application.applicationContext, uri) - when (getFileExtensionFromFileName(fileName)) { + val fileName = getFileName(context, uri) + return when (getFileExtensionFromFileName(fileName)) { Constants.CONF_FILE_EXTENSION -> - saveTunnelFromConfUri(fileName, uri, configType).let { - when (it) { - is Result.Error -> return Result.Error(Event.Error.FileReadFailed) - is Result.Success -> return it - } - } - - Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri, configType) - else -> return Result.Error(Event.Error.InvalidFileExtension) + 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, configType: ConfigType) { - ZipInputStream(getInputStreamFromUri(uri)).use { zip -> + private suspend fun saveTunnelsFromZipUri(uri: Uri, configType: ConfigType, context: Context) : Result { + return ZipInputStream(getInputStreamFromUri(uri, context)).use { zip -> generateSequence { zip.nextEntry } .filterNot { it.isDirectory || @@ -180,51 +213,57 @@ constructor( } .forEach { val name = getNameFromFileName(it.name) - viewModelScope.launch(Dispatchers.IO) { - 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() + 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 = 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 { - val stream = getInputStreamFromUri(uri) + private suspend fun saveTunnelFromConfUri(name: String, uri: Uri, configType: ConfigType, context: Context): Result { + val stream = getInputStreamFromUri(uri, context) return if (stream != null) { saveTunnelConfigFromStream(stream, name, configType) - Result.Success(Unit) + 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) { @@ -268,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 } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsScreen.kt index 7e1893a5..c2b59d42 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsScreen.kt @@ -27,7 +27,6 @@ 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 @@ -42,6 +41,7 @@ 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 @@ -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.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 @@ -81,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 @@ -100,11 +102,10 @@ 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)) } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsViewModel.kt index b189b8d2..d8fa8518 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsViewModel.kt @@ -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() } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt index 5147ef93..65812d1e 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt @@ -70,7 +70,6 @@ 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 @@ -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.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 @@ -146,12 +144,14 @@ fun SettingsScreen( } file } else null } - FileUtils.saveFilesToZip(context, wgFiles + amFiles) - didExportFiles = true - appViewModel.showSnackbarMessage(Event.Message.ConfigsExported.message) + FileUtils.saveFilesToZip(context, wgFiles + amFiles).onFailure { + appViewModel.showSnackbarMessage(it.getMessage(context)) + }.onSuccess { + didExportFiles = true + appViewModel.showSnackbarMessage(context.getString(R.string.exported_configs_message)) + } } catch (e: Exception) { Timber.e(e) - appViewModel.showSnackbarMessage(Event.Error.Exception(e).message) } } @@ -172,7 +172,7 @@ fun SettingsScreen( fun handleAutoTunnelToggle() { if (uiState.isBatteryOptimizeDisableShown || isBatteryOptimizationsDisabled()) { if (appViewModel.isRequiredPermissionGranted()) { - viewModel.onToggleAutoTunnel() + viewModel.onToggleAutoTunnel(context) } } else { requestBatteryOptimizationsDisabled() @@ -181,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)) } } } @@ -319,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)) }, ) } @@ -531,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) -> @@ -602,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)) } }, ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt index 2d6051f4..5c6e89e3 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt @@ -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() = @@ -192,12 +189,12 @@ constructor( } 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 { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Constants.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Constants.kt index 0cb1bd5c..42f162a7 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Constants.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Constants.kt @@ -38,5 +38,6 @@ object Constants { const val UNREADABLE_SSID = "" val amneziaProperties = listOf("Jc", "Jmin", "Jmax", "S1", "S2", "H1", "H2", "H3", "H4") + const val QR_CODE_NAME_PROPERTY = "# Name =" } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Event.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Event.kt deleted file mode 100644 index 22639ead..00000000 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Event.kt +++ /dev/null @@ -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) - } - } -} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Extensions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Extensions.kt index 6d9b59af..3fc3b0e9 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Extensions.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Extensions.kt @@ -1,8 +1,9 @@ package com.zaneschepke.wireguardautotunnel.util import android.content.BroadcastReceiver +import android.content.Context 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.service.tunnel.HandshakeStatus import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics @@ -11,7 +12,6 @@ import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import org.amnezia.awg.config.Config -import timber.log.Timber import java.math.BigDecimal import java.text.DecimalFormat import kotlin.coroutines.CoroutineContext @@ -87,3 +87,10 @@ fun Config.toWgQuickString() : String { } 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) + } +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/FileUtils.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/FileUtils.kt index 8afd01a1..b4e69051 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/FileUtils.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/FileUtils.kt @@ -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) { - 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) : Result { + 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()) } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Result.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Result.kt deleted file mode 100644 index b0515cb1..00000000 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Result.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.zaneschepke.wireguardautotunnel.util - -import timber.log.Timber - -sealed class Result { - class Success(val data: T) : Result() - - class Error(val error: Event.Error) : Result() { - init { - when (this.error) { - is Event.Error.Exception -> Timber.e(this.error.exception) - else -> Timber.e(this.error.message) - } - } - } -} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/WgTunnelExceptions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/WgTunnelExceptions.kt new file mode 100644 index 00000000..9a9f662b --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/WgTunnelExceptions.kt @@ -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) +// } +// } + } + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index eab41c12..ded5d919 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -94,6 +94,7 @@ Failed to authorize Enable app shortcuts Export configs + Failed to export configs Location services required Background location required Precise location required diff --git a/buildSrc/src/main/kotlin/Constants.kt b/buildSrc/src/main/kotlin/Constants.kt index 0d85144b..81454bdf 100644 --- a/buildSrc/src/main/kotlin/Constants.kt +++ b/buildSrc/src/main/kotlin/Constants.kt @@ -1,7 +1,7 @@ object Constants { - const val VERSION_NAME = "3.4.3" + const val VERSION_NAME = "3.4.4" const val JVM_TARGET = "17" - const val VERSION_CODE = 34300 + const val VERSION_CODE = 34400 const val TARGET_SDK = 34 const val MIN_SDK = 26 const val APP_ID = "com.zaneschepke.wireguardautotunnel" diff --git a/fastlane/metadata/android/en-US/changelogs/34400.txt b/fastlane/metadata/android/en-US/changelogs/34400.txt new file mode 100644 index 00000000..f796bf84 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/34400.txt @@ -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 \ No newline at end of file