From 12a9e849a61a9dfde34590cdde59f0e6709c3b2f Mon Sep 17 00:00:00 2001 From: Zane Schepke Date: Wed, 27 Dec 2023 19:39:46 -0500 Subject: [PATCH] refactor state, errors, nav --- .../wireguardautotunnel/Constants.kt | 5 +- .../wireguardautotunnel/Extensions.kt | 36 ++ .../data/repository/SettingsRepository.kt | 2 +- .../data/repository/SettingsRepositoryImpl.kt | 2 +- .../data/repository/TunnelConfigRepository.kt | 9 + .../repository/TunnelConfigRepositoryImpl.kt | 21 + .../foreground/WireGuardTunnelService.kt | 73 ++-- .../service/tile/TunnelControlTile.kt | 22 +- .../service/tunnel/HandshakeStatus.kt | 3 +- .../service/tunnel/VpnService.kt | 7 +- .../service/tunnel/VpnState.kt | 10 + .../service/tunnel/WireGuardTunnel.kt | 125 ++---- .../wireguardautotunnel/ui/MainActivity.kt | 102 ++--- .../ui/screens/config/ConfigScreen.kt | 119 +++--- .../ui/screens/config/ConfigUiState.kt | 20 + .../ui/screens/config/ConfigViewModel.kt | 384 +++++++----------- .../screens/config/ConfigViewModelFactory.kt | 8 + .../ui/screens/main/MainScreen.kt | 136 +++---- .../ui/screens/main/MainUiState.kt | 14 + .../ui/screens/main/MainViewModel.kt | 91 ++--- .../ui/screens/settings/SettingsScreen.kt | 253 +++++++----- .../ui/screens/settings/SettingsUiState.kt | 7 +- .../ui/screens/settings/SettingsViewModel.kt | 41 +- .../wireguardautotunnel/util/Error.kt | 23 ++ app/src/main/res/values/strings.xml | 12 +- buildSrc/src/main/kotlin/Constants.kt | 4 +- 26 files changed, 749 insertions(+), 780 deletions(-) create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/VpnState.kt create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigUiState.kt create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModelFactory.kt create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainUiState.kt create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Error.kt diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/Constants.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/Constants.kt index 8f55662..4f54a99 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/Constants.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/Constants.kt @@ -6,9 +6,6 @@ object Constants { const val VPN_CONNECTIVITY_CHECK_INTERVAL = 3000L const val VPN_STATISTIC_CHECK_INTERVAL = 1000L const val TOGGLE_TUNNEL_DELAY = 500L - const val FADE_IN_ANIMATION_DURATION = 1000 - const val SLIDE_IN_ANIMATION_DURATION = 500 - const val SLIDE_IN_TRANSITION_OFFSET = 1000 const val CONF_FILE_EXTENSION = ".conf" const val ZIP_FILE_EXTENSION = ".zip" const val URI_CONTENT_SCHEME = "content" @@ -18,4 +15,6 @@ object Constants { const val ANDROID_TV_EXPLORER_STUB = "com.android.tv.frameworkpackagestubs" const val EMAIL_MIME_TYPE = "message/rfc822" const val SYSTEM_EXEMPT_SERVICE_TYPE_ID = 1024 + + const val SUBSCRIPTION_TIMEOUT = 5_000L } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/Extensions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/Extensions.kt index 540c2a7..58e595f 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/Extensions.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/Extensions.kt @@ -1,6 +1,13 @@ package com.zaneschepke.wireguardautotunnel import android.content.BroadcastReceiver +import android.content.pm.PackageInfo +import com.wireguard.android.backend.Statistics +import com.wireguard.android.backend.Statistics.PeerStats +import com.wireguard.crypto.Key +import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig +import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus +import com.zaneschepke.wireguardautotunnel.util.NumberUtils import java.math.BigDecimal import java.text.DecimalFormat import kotlin.coroutines.CoroutineContext @@ -30,3 +37,32 @@ fun BigDecimal.toThreeDecimalPlaceString(): String { return df.format(this) } +fun List.update(index: Int, item: T): List = toMutableList().apply { this[index] = item } +fun List.removeAt(index: Int): List = toMutableList().apply { this.removeAt(index) } + +typealias TunnelConfigs = List +typealias Packages = List + +fun Statistics.mapPeerStats(): Map { + return this.peers().associateWith { key -> + (this.peer(key)) + } +} + +fun PeerStats.latestHandshakeSeconds() : Long? { + return NumberUtils.getSecondsBetweenTimestampAndNow(this.latestHandshakeEpochMillis) +} + +fun PeerStats.handshakeStatus() : HandshakeStatus { + return this.latestHandshakeSeconds().let { + when { + it == null -> HandshakeStatus.NOT_STARTED + it <= HandshakeStatus.STALE_TIME_LIMIT_SEC -> HandshakeStatus.HEALTHY + it > HandshakeStatus.STALE_TIME_LIMIT_SEC -> HandshakeStatus.STALE + else -> { + HandshakeStatus.UNKNOWN + } + } + } +} + diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/SettingsRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/SettingsRepository.kt index 1619b39..53cd48f 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/SettingsRepository.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/SettingsRepository.kt @@ -5,7 +5,7 @@ import kotlinx.coroutines.flow.Flow interface SettingsRepository { suspend fun save(settings : Settings) - fun getSettings() : Flow + fun getSettingsFlow() : Flow suspend fun getAll() : List } \ No newline at end of file diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/SettingsRepositoryImpl.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/SettingsRepositoryImpl.kt index c9b5057..587cb4f 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/SettingsRepositoryImpl.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/SettingsRepositoryImpl.kt @@ -10,7 +10,7 @@ class SettingsRepositoryImpl(private val settingsDoa: SettingsDao) : SettingsRep settingsDoa.save(settings) } - override fun getSettings(): Flow { + override fun getSettingsFlow(): Flow { return settingsDoa.getSettingsFlow() } 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 427753f..d96e8dc 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 @@ -1,5 +1,14 @@ package com.zaneschepke.wireguardautotunnel.data.repository +import com.zaneschepke.wireguardautotunnel.TunnelConfigs +import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig +import kotlinx.coroutines.flow.Flow + interface TunnelConfigRepository { + fun getTunnelConfigsFlow() : Flow + suspend fun getAll() : TunnelConfigs + suspend fun save(tunnelConfig: TunnelConfig) + suspend fun delete(tunnelConfig: TunnelConfig) + suspend fun count() : Int } \ No newline at end of file diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/TunnelConfigRepositoryImpl.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/TunnelConfigRepositoryImpl.kt index 10db72a..5997965 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/TunnelConfigRepositoryImpl.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/TunnelConfigRepositoryImpl.kt @@ -1,7 +1,28 @@ package com.zaneschepke.wireguardautotunnel.data.repository +import com.zaneschepke.wireguardautotunnel.TunnelConfigs import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao +import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig +import kotlinx.coroutines.flow.Flow class TunnelConfigRepositoryImpl(private val tunnelConfigDao: TunnelConfigDao) : TunnelConfigRepository { + override fun getTunnelConfigsFlow(): Flow { + return tunnelConfigDao.getAllFlow() + } + override suspend fun getAll(): TunnelConfigs { + return tunnelConfigDao.getAll() + } + + override suspend fun save(tunnelConfig: TunnelConfig) { + tunnelConfigDao.save(tunnelConfig) + } + + override suspend fun delete(tunnelConfig: TunnelConfig) { + tunnelConfigDao.delete(tunnelConfig) + } + + override suspend fun count(): Int { + return tunnelConfigDao.count().toInt() + } } \ No newline at end of file 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 0d5f611..0078ca3 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 @@ -75,42 +75,43 @@ class WireGuardTunnelService : ForegroundService() { } } } - launch { - var didShowConnected = false - var didShowFailedHandshakeNotification = false - vpnService.handshakeStatus.collect { - when (it) { - HandshakeStatus.NOT_STARTED -> { - } - HandshakeStatus.NEVER_CONNECTED -> { - if (!didShowFailedHandshakeNotification) { - launchVpnConnectionFailedNotification( - getString(R.string.initial_connection_failure_message) - ) - didShowFailedHandshakeNotification = true - didShowConnected = false - } - } - - HandshakeStatus.HEALTHY -> { - if (!didShowConnected) { - launchVpnConnectedNotification() - didShowConnected = true - } - } - HandshakeStatus.STALE -> {} - HandshakeStatus.UNHEALTHY -> { - if (!didShowFailedHandshakeNotification) { - launchVpnConnectionFailedNotification( - getString(R.string.lost_connection_failure_message) - ) - didShowFailedHandshakeNotification = true - didShowConnected = false - } - } - } - } - } + //TODO fix connected notification +// launch { +// var didShowConnected = false +// var didShowFailedHandshakeNotification = false +// vpnService.handshakeStatus.collect { +// when (it) { +// HandshakeStatus.NOT_STARTED -> { +// } +// HandshakeStatus.NEVER_CONNECTED -> { +// if (!didShowFailedHandshakeNotification) { +// launchVpnConnectionFailedNotification( +// getString(R.string.initial_connection_failure_message) +// ) +// didShowFailedHandshakeNotification = true +// didShowConnected = false +// } +// } +// +// HandshakeStatus.HEALTHY -> { +// if (!didShowConnected) { +// launchVpnConnectedNotification() +// didShowConnected = true +// } +// } +// HandshakeStatus.STALE -> {} +// HandshakeStatus.UNHEALTHY -> { +// if (!didShowFailedHandshakeNotification) { +// launchVpnConnectionFailedNotification( +// getString(R.string.lost_connection_failure_message) +// ) +// didShowFailedHandshakeNotification = true +// didShowConnected = false +// } +// } +// } +// } +// } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/TunnelControlTile.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/TunnelControlTile.kt index d98069b..3b295bf 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/TunnelControlTile.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/TunnelControlTile.kt @@ -116,27 +116,19 @@ class TunnelControlTile : TileService() { } private suspend fun updateTileState() { - vpnService.state.collect { + vpnService.vpnState.collect { + when(it.status) { + Tunnel.State.UP -> qsTile.state = Tile.STATE_ACTIVE + Tunnel.State.DOWN -> qsTile.state = Tile.STATE_INACTIVE + else -> qsTile.state = Tile.STATE_UNAVAILABLE + } try { - when (it) { - Tunnel.State.UP -> { - qsTile.state = Tile.STATE_ACTIVE - } - - Tunnel.State.DOWN -> { - qsTile.state = Tile.STATE_INACTIVE - } - - else -> { - qsTile.state = Tile.STATE_UNAVAILABLE - } - } val config = determineTileTunnel() setTileDescription( config?.name ?: this.resources.getString(R.string.no_tunnel_available) ) qsTile.updateTile() - } catch (e: Exception) { + } catch (e : Exception) { Timber.e("Unable to update tile state") } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/HandshakeStatus.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/HandshakeStatus.kt index a1b59c4..87621cc 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/HandshakeStatus.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/HandshakeStatus.kt @@ -3,8 +3,7 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel enum class HandshakeStatus { HEALTHY, STALE, - UNHEALTHY, - NEVER_CONNECTED, + UNKNOWN, NOT_STARTED ; diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/VpnService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/VpnService.kt index 0f6aaa7..80144cc 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/VpnService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/VpnService.kt @@ -5,17 +5,14 @@ import com.wireguard.android.backend.Tunnel import com.wireguard.crypto.Key import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow interface VpnService : Tunnel { suspend fun startTunnel(tunnelConfig: TunnelConfig): Tunnel.State suspend fun stopTunnel() - val state: SharedFlow - val tunnelName: SharedFlow - val statistics: SharedFlow - val lastHandshake: SharedFlow> - val handshakeStatus: SharedFlow + val vpnState: StateFlow fun getState(): Tunnel.State } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/VpnState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/VpnState.kt new file mode 100644 index 0000000..077f7ad --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/VpnState.kt @@ -0,0 +1,10 @@ +package com.zaneschepke.wireguardautotunnel.service.tunnel + +import com.wireguard.android.backend.Statistics +import com.wireguard.android.backend.Tunnel + +data class VpnState( + val status : Tunnel.State = Tunnel.State.DOWN, + val name : String = "", + val statistics : Statistics? = null +) 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 1c6e045..fdb3e10 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 @@ -3,28 +3,23 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel import com.wireguard.android.backend.Backend import com.wireguard.android.backend.BackendException import com.wireguard.android.backend.Statistics -import com.wireguard.android.backend.Tunnel +import com.wireguard.android.backend.Tunnel.State import com.wireguard.config.Config -import com.wireguard.crypto.Key import com.zaneschepke.wireguardautotunnel.Constants -import com.zaneschepke.wireguardautotunnel.module.Kernel -import com.zaneschepke.wireguardautotunnel.module.Userspace import com.zaneschepke.wireguardautotunnel.data.SettingsDao import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig -import com.zaneschepke.wireguardautotunnel.util.NumberUtils -import javax.inject.Inject +import com.zaneschepke.wireguardautotunnel.module.Kernel +import com.zaneschepke.wireguardautotunnel.module.Userspace import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import timber.log.Timber +import javax.inject.Inject class WireGuardTunnel @Inject @@ -33,30 +28,8 @@ constructor( @Kernel private val kernelBackend: Backend, private val settingsRepo: SettingsDao ) : VpnService { - private val _tunnelName = MutableStateFlow("") - override val tunnelName get() = _tunnelName.asStateFlow() - - private val _state = - MutableSharedFlow( - onBufferOverflow = BufferOverflow.DROP_OLDEST, - replay = 1 - ) - - private val _handshakeStatus = - MutableSharedFlow( - replay = 1, - onBufferOverflow = BufferOverflow.DROP_OLDEST - ) - override val state get() = _state.asSharedFlow() - - private val _statistics = MutableSharedFlow(replay = 1) - override val statistics get() = _statistics.asSharedFlow() - - private val _lastHandshake = MutableSharedFlow>(replay = 1) - override val lastHandshake get() = _lastHandshake.asSharedFlow() - - override val handshakeStatus: SharedFlow - get() = _handshakeStatus.asSharedFlow() + private val _vpnState = MutableStateFlow(VpnState()) + override val vpnState: StateFlow = _vpnState.asStateFlow() private val scope = CoroutineScope(Dispatchers.IO) @@ -85,7 +58,7 @@ constructor( } } - override suspend fun startTunnel(tunnelConfig: TunnelConfig): Tunnel.State { + override suspend fun startTunnel(tunnelConfig: TunnelConfig): State { return try { stopTunnelOnConfigChange(tunnelConfig) emitTunnelName(tunnelConfig.name) @@ -93,95 +66,83 @@ constructor( val state = backend.setState( this, - Tunnel.State.UP, + State.UP, config ) - _state.emit(state) + emitTunnelState(state) state } catch (e: Exception) { Timber.e("Failed to start tunnel with error: ${e.message}") - Tunnel.State.DOWN + State.DOWN } } + private fun emitTunnelState(state: State) { + _vpnState.tryEmit( + _vpnState.value.copy( + status = state + ) + ) + } + + private fun emitBackendStatistics(statistics: Statistics) { + _vpnState.tryEmit( + _vpnState.value.copy( + statistics = statistics + ) + ) + } + private suspend fun emitTunnelName(name: String) { - _tunnelName.emit(name) + _vpnState.emit( + _vpnState.value.copy( + name = name + ) + ) } private suspend fun stopTunnelOnConfigChange(tunnelConfig: TunnelConfig) { - if (getState() == Tunnel.State.UP && _tunnelName.value != tunnelConfig.name) { + if (getState() == State.UP && _vpnState.value.name != tunnelConfig.name) { stopTunnel() } } override fun getName(): String { - return _tunnelName.value + return _vpnState.value.name } override suspend fun stopTunnel() { try { - if (getState() == Tunnel.State.UP) { - val state = backend.setState(this, Tunnel.State.DOWN, null) - _state.emit(state) + if (getState() == State.UP) { + val state = backend.setState(this, State.DOWN, null) + emitTunnelState(state) } } catch (e: BackendException) { Timber.e("Failed to stop tunnel with error: ${e.message}") } } - override fun getState(): Tunnel.State { + override fun getState(): State { return backend.getState(this) } - override fun onStateChange(state: Tunnel.State) { + override fun onStateChange(state: State) { val tunnel = this - _state.tryEmit(state) - if (state == Tunnel.State.UP) { + emitTunnelState(state) + if (state == State.UP) { statsJob = scope.launch { - val handshakeMap = HashMap() - var neverHadHandshakeCounter = 0 while (true) { val statistics = backend.getStatistics(tunnel) - _statistics.emit(statistics) - statistics.peers().forEach { key -> - val handshakeEpoch = - statistics.peer(key)?.latestHandshakeEpochMillis ?: 0L - handshakeMap[key] = handshakeEpoch - if (handshakeEpoch == 0L) { - if (neverHadHandshakeCounter >= HandshakeStatus.NEVER_CONNECTED_TO_UNHEALTHY_TIME_LIMIT_SEC) { - _handshakeStatus.emit(HandshakeStatus.NEVER_CONNECTED) - } else { - _handshakeStatus.emit(HandshakeStatus.NOT_STARTED) - } - if (neverHadHandshakeCounter <= HandshakeStatus.NEVER_CONNECTED_TO_UNHEALTHY_TIME_LIMIT_SEC) { - neverHadHandshakeCounter += (1 * Constants.VPN_STATISTIC_CHECK_INTERVAL / 1000).toInt() - } - return@forEach - } - // TODO one day make each peer have their own dedicated status - val lastHandshake = NumberUtils.getSecondsBetweenTimestampAndNow( - handshakeEpoch - ) - if (lastHandshake != null) { - if (lastHandshake >= HandshakeStatus.STALE_TIME_LIMIT_SEC) { - _handshakeStatus.emit(HandshakeStatus.STALE) - } else { - _handshakeStatus.emit(HandshakeStatus.HEALTHY) - } - } - } - _lastHandshake.emit(handshakeMap) + emitBackendStatistics(statistics) delay(Constants.VPN_STATISTIC_CHECK_INTERVAL) } } } - if (state == Tunnel.State.DOWN) { + if (state == State.DOWN) { if (this::statsJob.isInitialized) { statsJob.cancel() } - _handshakeStatus.tryEmit(HandshakeStatus.NOT_STARTED) - _lastHandshake.tryEmit(emptyMap()) } } } 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 507b558..7a4a274 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt @@ -10,11 +10,8 @@ import android.view.KeyEvent import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.setContent import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity -import androidx.compose.animation.ExitTransition -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.slideInHorizontally import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarData @@ -32,7 +29,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.lifecycleScope import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable @@ -47,12 +43,15 @@ import com.zaneschepke.wireguardautotunnel.ui.common.PermissionRequestFailedScre import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen +import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigViewModel +import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigViewModelFactory import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen import com.zaneschepke.wireguardautotunnel.ui.theme.TransparentSystemBars import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme import dagger.hilt.android.AndroidEntryPoint +import dagger.hilt.android.lifecycle.withCreationCallback import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import timber.log.Timber @@ -66,6 +65,7 @@ class MainActivity : AppCompatActivity() { super.onCreate(savedInstanceState) setContent { // val activityViewModel = hiltViewModel() + val navController = rememberNavController() val focusRequester = remember { FocusRequester() } @@ -102,20 +102,16 @@ class MainActivity : AppCompatActivity() { } } - fun showSnackBarMessage(message: String) { + fun showSnackBarMessage(message: Int) { lifecycleScope.launch(Dispatchers.Main) { val result = snackbarHostState.showSnackbar( - message = message, + message = getString(message), actionLabel = applicationContext.getString(R.string.okay), duration = SnackbarDuration.Short ) when (result) { - SnackbarResult.ActionPerformed -> { - snackbarHostState.currentSnackbarData?.dismiss() - } - - SnackbarResult.Dismissed -> { + SnackbarResult.ActionPerformed, SnackbarResult.Dismissed -> { snackbarHostState.currentSnackbarData?.dismiss() } } @@ -192,86 +188,38 @@ class MainActivity : AppCompatActivity() { ) return@Scaffold } - NavHost(navController, startDestination = Routes.Main.name) { composable( Routes.Main.name, - enterTransition = { - when (initialState.destination.route) { - Routes.Settings.name, Routes.Support.name -> - slideInHorizontally( - initialOffsetX = { - -Constants.SLIDE_IN_TRANSITION_OFFSET - }, - animationSpec = tween( - Constants.SLIDE_IN_ANIMATION_DURATION - ) - ) - - else -> { - fadeIn( - animationSpec = tween( - Constants.FADE_IN_ANIMATION_DURATION - ) - ) - } - } - }, - exitTransition = { - ExitTransition.None - } ) { MainScreen(padding = padding, showSnackbarMessage = { message -> showSnackBarMessage(message) }, navController = navController) } - composable(Routes.Settings.name, enterTransition = { - when (initialState.destination.route) { - Routes.Main.name -> - slideInHorizontally( - initialOffsetX = { Constants.SLIDE_IN_TRANSITION_OFFSET }, - animationSpec = tween(Constants.SLIDE_IN_ANIMATION_DURATION) - ) - - Routes.Support.name -> { - slideInHorizontally( - initialOffsetX = { -Constants.SLIDE_IN_TRANSITION_OFFSET }, - animationSpec = tween(Constants.SLIDE_IN_ANIMATION_DURATION) - ) - } - - else -> { - fadeIn( - animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION) - ) - } - } - }) { + composable(Routes.Settings.name, + ) { SettingsScreen(padding = padding, showSnackbarMessage = { message -> showSnackBarMessage(message) }, focusRequester = focusRequester) } - composable(Routes.Support.name, enterTransition = { - when (initialState.destination.route) { - Routes.Settings.name, Routes.Main.name -> - slideInHorizontally( - initialOffsetX = { Constants.SLIDE_IN_ANIMATION_DURATION }, - animationSpec = tween(Constants.SLIDE_IN_ANIMATION_DURATION) - ) - - else -> { - fadeIn( - animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION) - ) - } - } - }) { SupportScreen(padding = padding, focusRequester = focusRequester) } - composable("${Routes.Config.name}/{id}", enterTransition = { - fadeIn(animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION)) - }) { + composable(Routes.Support.name, + ) { + SupportScreen(padding = padding, focusRequester = focusRequester) + } + composable("${Routes.Config.name}/{id}") { val id = it.arguments?.getString("id") if (!id.isNullOrBlank()) { + //https://dagger.dev/hilt/view-model#assisted-injection + val configViewModel by viewModels( + extrasProducer = { + defaultViewModelCreationExtras.withCreationCallback< + ConfigViewModelFactory> { factory -> + factory.create(id) + } + } + ) ConfigScreen( + viewModel = configViewModel, navController = navController, id = id, showSnackbarMessage = { message -> 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 43ccfa6..2c23b25 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 @@ -93,25 +93,16 @@ import timber.log.Timber ) @Composable fun ConfigScreen( - viewModel: ConfigViewModel = hiltViewModel(), + viewModel: ConfigViewModel, focusRequester: FocusRequester, navController: NavController, - showSnackbarMessage: (String) -> Unit, + showSnackbarMessage: (Int) -> Unit, id: String ) { val context = LocalContext.current val scope = rememberCoroutineScope() val clipboardManager: ClipboardManager = LocalClipboardManager.current val keyboardController = LocalSoftwareKeyboardController.current - - val tunnel by viewModel.tunnel.collectAsStateWithLifecycle(null) - val tunnelName = viewModel.tunnelName.collectAsStateWithLifecycle() - val packages by viewModel.packages.collectAsStateWithLifecycle() - val checkedPackages by viewModel.checkedPackages.collectAsStateWithLifecycle() - val include by viewModel.include.collectAsStateWithLifecycle() - val isAllApplicationsEnabled by viewModel.isAllApplicationsEnabled.collectAsStateWithLifecycle() - val proxyPeers by viewModel.proxyPeers.collectAsStateWithLifecycle() - val proxyInterface by viewModel.interfaceProxy.collectAsStateWithLifecycle() var showApplicationsDialog by remember { mutableStateOf(false) } var showAuthPrompt by remember { mutableStateOf(false) } var isAuthenticated by remember { mutableStateOf(false) } @@ -122,6 +113,8 @@ fun ConfigScreen( } } + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val keyboardActions = KeyboardActions( onDone = { @@ -139,23 +132,12 @@ fun ConfigScreen( val fillMaxWidth = .85f val screenPadding = 5.dp - LaunchedEffect(Unit) { - scope.launch(Dispatchers.IO) { - try { - viewModel.onScreenLoad(id) - } catch (e: Exception) { - showSnackbarMessage(e.message!!) - navController.navigate(Routes.Main.name) - } - } - } - val applicationButtonText = { "Tunneling apps: " + - if (isAllApplicationsEnabled) { + if (uiState.isAllApplicationsEnabled) { "all" } else { - "${checkedPackages.size} " + (if (include) "included" else "excluded") + "${uiState.checkedPackageNames.size} " + (if (uiState.include) "included" else "excluded") } } @@ -166,20 +148,20 @@ fun ConfigScreen( isAuthenticated = true }, onError = { error -> - showSnackbarMessage(error) showAuthPrompt = false + showSnackbarMessage(R.string.error_authentication_failed) }, onFailure = { showAuthPrompt = false - showSnackbarMessage(context.getString(R.string.authentication_failed)) + showSnackbarMessage(R.string.error_authentication_failed) } ) } if (showApplicationsDialog) { val sortedPackages = - remember(packages) { - packages.sortedBy { viewModel.getPackageLabel(it) } + remember(uiState.packages) { + uiState.packages.sortedBy { viewModel.getPackageLabel(it) } } AlertDialog(onDismissRequest = { showApplicationsDialog = false @@ -192,7 +174,7 @@ fun ConfigScreen( modifier = Modifier .fillMaxWidth() - .fillMaxHeight(if (isAllApplicationsEnabled) 1 / 5f else 4 / 5f) + .fillMaxHeight(if (uiState.isAllApplicationsEnabled) 1 / 5f else 4 / 5f) ) { Column( modifier = Modifier.fillMaxWidth() @@ -207,13 +189,13 @@ fun ConfigScreen( ) { Text(stringResource(id = R.string.tunnel_all)) Switch( - checked = isAllApplicationsEnabled, + checked = uiState.isAllApplicationsEnabled, onCheckedChange = { viewModel.onAllApplicationsChange(it) } ) } - if (!isAllApplicationsEnabled) { + if (!uiState.isAllApplicationsEnabled) { Row( modifier = Modifier @@ -231,9 +213,9 @@ fun ConfigScreen( ) { Text(stringResource(id = R.string.include)) Checkbox( - checked = include, + checked = uiState.include, onCheckedChange = { - viewModel.onIncludeChange(!include) + viewModel.onIncludeChange(!uiState.include) } ) } @@ -243,9 +225,9 @@ fun ConfigScreen( ) { Text(stringResource(id = R.string.exclude)) Checkbox( - checked = !include, + checked = !uiState.include, onCheckedChange = { - viewModel.onIncludeChange(!include) + viewModel.onIncludeChange(!uiState.include) } ) } @@ -324,7 +306,7 @@ fun ConfigScreen( } Checkbox( modifier = Modifier.fillMaxSize(), - checked = (checkedPackages.contains(pack.packageName)), + checked = (uiState.checkedPackageNames.contains(pack.packageName)), onCheckedChange = { if (it) { viewModel.onAddCheckedPackage( @@ -362,7 +344,7 @@ fun ConfigScreen( } } - if (tunnel != null) { + if (uiState.tunnel != null) { Scaffold( floatingActionButtonPosition = FabPosition.End, floatingActionButton = { @@ -371,22 +353,25 @@ fun ConfigScreen( var fobColor by remember { mutableStateOf(secondaryColor) } FloatingActionButton( modifier = - Modifier.padding(bottom = 90.dp).onFocusChanged { - if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) { - fobColor = if (it.isFocused) hoverColor else secondaryColor - } - }, + Modifier + .padding(bottom = 90.dp) + .onFocusChanged { + if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) { + fobColor = if (it.isFocused) hoverColor else secondaryColor + } + }, onClick = { scope.launch { try { viewModel.onSaveAllChanges() navController.navigate(Routes.Main.name) showSnackbarMessage( - context.resources.getString(R.string.config_changes_saved) + R.string.config_changes_saved ) } catch (e: Exception) { Timber.e(e.message) - showSnackbarMessage(e.message!!) + //TODO fix error handling + //showSnackbarMessage(e.message!!) } } }, @@ -433,30 +418,36 @@ fun ConfigScreen( Column( horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.Top, - modifier = Modifier.padding(15.dp).focusGroup() + modifier = Modifier + .padding(15.dp) + .focusGroup() ) { SectionTitle( stringResource(R.string.interface_), padding = screenPadding ) ConfigurationTextBox( - value = tunnelName.value, + value = uiState.tunnelName, onValueChange = { value -> viewModel.onTunnelNameChange(value) }, keyboardActions = keyboardActions, label = stringResource(R.string.name), hint = stringResource(R.string.tunnel_name).lowercase(), - modifier = baseTextBoxModifier.fillMaxWidth().focusRequester( - focusRequester - ) + modifier = baseTextBoxModifier + .fillMaxWidth() + .focusRequester( + focusRequester + ) ) OutlinedTextField( modifier = - baseTextBoxModifier.fillMaxWidth().clickable { - showAuthPrompt = true - }, - value = proxyInterface.privateKey, + baseTextBoxModifier + .fillMaxWidth() + .clickable { + showAuthPrompt = true + }, + value = uiState.interfaceProxy.privateKey, visualTransformation = if ((id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated) VisualTransformation.None else PasswordVisualTransformation(), enabled = (id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated, onValueChange = { value -> @@ -483,10 +474,12 @@ fun ConfigScreen( keyboardActions = keyboardActions ) OutlinedTextField( - modifier = baseTextBoxModifier.fillMaxWidth().focusRequester( - FocusRequester.Default - ), - value = proxyInterface.publicKey, + modifier = baseTextBoxModifier + .fillMaxWidth() + .focusRequester( + FocusRequester.Default + ), + value = uiState.interfaceProxy.publicKey, enabled = false, onValueChange = {}, trailingIcon = { @@ -494,7 +487,7 @@ fun ConfigScreen( modifier = Modifier.focusRequester(FocusRequester.Default), onClick = { clipboardManager.setText( - AnnotatedString(proxyInterface.publicKey) + AnnotatedString(uiState.interfaceProxy.publicKey) ) } ) { @@ -513,7 +506,7 @@ fun ConfigScreen( ) Row(modifier = Modifier.fillMaxWidth()) { ConfigurationTextBox( - value = proxyInterface.addresses, + value = uiState.interfaceProxy.addresses, onValueChange = { value -> viewModel.onAddressesChanged(value) }, @@ -526,7 +519,7 @@ fun ConfigScreen( .padding(end = 5.dp) ) ConfigurationTextBox( - value = proxyInterface.listenPort, + value = uiState.interfaceProxy.listenPort, onValueChange = { value -> viewModel.onListenPortChanged(value) }, keyboardActions = keyboardActions, label = stringResource(R.string.listen_port), @@ -536,7 +529,7 @@ fun ConfigScreen( } Row(modifier = Modifier.fillMaxWidth()) { ConfigurationTextBox( - value = proxyInterface.dnsServers, + value = uiState.interfaceProxy.dnsServers, onValueChange = { value -> viewModel.onDnsServersChanged(value) }, keyboardActions = keyboardActions, label = stringResource(R.string.dns_servers), @@ -547,7 +540,7 @@ fun ConfigScreen( .padding(end = 5.dp) ) ConfigurationTextBox( - value = proxyInterface.mtu, + value = uiState.interfaceProxy.mtu, onValueChange = { value -> viewModel.onMtuChanged(value) }, keyboardActions = keyboardActions, label = stringResource(R.string.mtu), @@ -573,7 +566,7 @@ fun ConfigScreen( } } } - proxyPeers.forEachIndexed { index, peer -> + uiState.proxyPeers.forEachIndexed { index, peer -> Surface( tonalElevation = 2.dp, shadowElevation = 2.dp, diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigUiState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigUiState.kt new file mode 100644 index 0000000..436ad51 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigUiState.kt @@ -0,0 +1,20 @@ +package com.zaneschepke.wireguardautotunnel.ui.screens.config + +import com.zaneschepke.wireguardautotunnel.Packages +import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig +import com.zaneschepke.wireguardautotunnel.ui.models.InterfaceProxy +import com.zaneschepke.wireguardautotunnel.ui.models.PeerProxy +import com.zaneschepke.wireguardautotunnel.util.Error + +data class ConfigUiState( + val proxyPeers: List = arrayListOf(PeerProxy()), + val interfaceProxy: InterfaceProxy = InterfaceProxy(), + val packages: Packages = emptyList(), + val checkedPackageNames: List = emptyList(), + val include: Boolean = true, + val isAllApplicationsEnabled : Boolean = false, + val isLoading: Boolean = true, + val tunnel: TunnelConfig? = null, + val tunnelName: String = "", + val errorEvent: Error = Error.NONE +) \ No newline at end of file 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 7f2e103..4116f2c 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 @@ -5,8 +5,6 @@ import android.app.Application import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.os.Build -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.toMutableStateList import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wireguard.config.Config @@ -14,210 +12,100 @@ import com.wireguard.config.Interface import com.wireguard.config.Peer import com.wireguard.crypto.Key import com.wireguard.crypto.KeyPair -import com.zaneschepke.wireguardautotunnel.Constants -import com.zaneschepke.wireguardautotunnel.data.SettingsDao -import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig +import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository +import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository +import com.zaneschepke.wireguardautotunnel.removeAt import com.zaneschepke.wireguardautotunnel.ui.models.InterfaceProxy import com.zaneschepke.wireguardautotunnel.ui.models.PeerProxy +import com.zaneschepke.wireguardautotunnel.update +import com.zaneschepke.wireguardautotunnel.util.Error import com.zaneschepke.wireguardautotunnel.util.NumberUtils import com.zaneschepke.wireguardautotunnel.util.WgTunnelException +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch -@HiltViewModel +@HiltViewModel(assistedFactory = ConfigViewModelFactory::class) class ConfigViewModel -@Inject +@AssistedInject constructor( private val application: Application, - private val tunnelRepo: TunnelConfigDao, - private val settingsRepo: SettingsDao + private val tunnelConfigRepository: TunnelConfigRepository, + private val settingsRepository: SettingsRepository, + @Assisted val tunnelId : String ) : ViewModel() { - private val _tunnel = MutableStateFlow(null) - private val _tunnelName = MutableStateFlow("") - val tunnelName get() = _tunnelName.asStateFlow() - val tunnel get() = _tunnel.asStateFlow() - private var _proxyPeers = MutableStateFlow(mutableStateListOf()) - val proxyPeers get() = _proxyPeers.asStateFlow() - - private var _interface = MutableStateFlow(InterfaceProxy()) - val interfaceProxy = _interface.asStateFlow() - - private val _packages = MutableStateFlow(emptyList()) - val packages get() = _packages.asStateFlow() private val packageManager = application.packageManager - private val _checkedPackages = MutableStateFlow(mutableStateListOf()) - val checkedPackages get() = _checkedPackages.asStateFlow() - private val _include = MutableStateFlow(true) - val include get() = _include.asStateFlow() + private val _uiState = MutableStateFlow(ConfigUiState()) + val uiState = _uiState.asStateFlow() - private val _isAllApplicationsEnabled = MutableStateFlow(false) - val isAllApplicationsEnabled get() = _isAllApplicationsEnabled.asStateFlow() - private val _isDefaultTunnel = MutableStateFlow(false) - - private lateinit var tunnelConfig: TunnelConfig - - suspend fun onScreenLoad(id: String) { - if (id != Constants.MANUAL_TUNNEL_CONFIG_ID) { - tunnelConfig = getTunnelConfigById(id) ?: throw WgTunnelException( - "Config not found" - ) - emitScreenData() - } else { - emitEmptyScreenData() - } - } - - private fun emitEmptyScreenData() { - tunnelConfig = TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = "") + init { viewModelScope.launch { - emitTunnelConfig() - emitPeerProxy(PeerProxy()) - emitInterfaceProxy(InterfaceProxy()) - emitTunnelConfigName() - emitDefaultTunnelStatus() - emitQueriedPackages("") - emitTunnelAllApplicationsEnabled() + val packages = getQueriedPackages("") + val tunnelConfig = tunnelConfigRepository.getAll().firstOrNull{ it.id.toString() == tunnelId } + val state = if(tunnelConfig != null) { + val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick) + val proxyPeers = config.peers.map { PeerProxy.from(it) } + val proxyInterface = InterfaceProxy.from(config.`interface`) + var include = true + var isAllApplicationsEnabled = false + val checkedPackages = if(config.`interface`.includedApplications.isNotEmpty()) { + config.`interface`.includedApplications + } else if(config.`interface`.excludedApplications.isNotEmpty()) { + include = false + config.`interface`.excludedApplications + } else { + isAllApplicationsEnabled = true + emptySet() + } + ConfigUiState(proxyPeers,proxyInterface, packages,checkedPackages.toList(), + include, isAllApplicationsEnabled, false, tunnelConfig, tunnelConfig.name, Error.NONE) + } else { + ConfigUiState(isLoading = false, packages = packages) + } + _uiState.value = state } } - private suspend fun emitScreenData() { - emitTunnelConfig() - emitPeersFromConfig() - emitInterfaceFromConfig() - emitTunnelConfigName() - emitDefaultTunnelStatus() - emitQueriedPackages("") - emitCurrentPackageConfigurations() - } - - private suspend fun emitDefaultTunnelStatus() { - val settings = settingsRepo.getAll() - if (settings.isNotEmpty()) { - _isDefaultTunnel.value = settings.first().isTunnelConfigDefault(tunnelConfig) - } - } - - private fun emitInterfaceFromConfig() { - val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick) - _interface.value = InterfaceProxy.from(config.`interface`) - } - - private fun emitPeersFromConfig() { - val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick) - config.peers.forEach { - _proxyPeers.value.add(PeerProxy.from(it)) - } - } - - private fun emitPeerProxy(peerProxy: PeerProxy) { - _proxyPeers.value.add(peerProxy) - } - - private fun emitInterfaceProxy(interfaceProxy: InterfaceProxy) { - _interface.value = interfaceProxy - } - - private suspend fun getTunnelConfigById(id: String): TunnelConfig? { - return try { - tunnelRepo.getById(id.toLong()) - } catch (_: Exception) { - null - } - } - - private suspend fun emitTunnelConfig() { - _tunnel.emit(tunnelConfig) - } - - private suspend fun emitTunnelConfigName() { - _tunnelName.emit(tunnelConfig.name) - } - fun onTunnelNameChange(name: String) { - _tunnelName.value = name + _uiState.value = _uiState.value.copy( + tunnelName = name + ) } fun onIncludeChange(include: Boolean) { - _include.value = include + _uiState.value = _uiState.value.copy( + include = include + ) } fun onAddCheckedPackage(packageName: String) { - _checkedPackages.value.add(packageName) + _uiState.value = _uiState.value.copy( + checkedPackageNames = _uiState.value.checkedPackageNames + packageName + ) } fun onAllApplicationsChange(isAllApplicationsEnabled: Boolean) { - _isAllApplicationsEnabled.value = isAllApplicationsEnabled + _uiState.value = _uiState.value.copy( + isAllApplicationsEnabled = isAllApplicationsEnabled + ) } fun onRemoveCheckedPackage(packageName: String) { - _checkedPackages.value.remove(packageName) + _uiState.value = _uiState.value.copy( + checkedPackageNames = _uiState.value.checkedPackageNames - packageName + ) } - private suspend fun emitSplitTunnelConfiguration(config: Config) { - val excludedApps = config.`interface`.excludedApplications - val includedApps = config.`interface`.includedApplications - if (excludedApps.isNotEmpty() || includedApps.isNotEmpty()) { - emitTunnelAllApplicationsDisabled() - determineAppInclusionState(excludedApps, includedApps) - } else { - emitTunnelAllApplicationsEnabled() - } - } - - private suspend fun determineAppInclusionState( - excludedApps: Set, - includedApps: Set - ) { - if (excludedApps.isEmpty()) { - emitIncludedAppsExist() - emitCheckedApps(includedApps) - } else { - emitExcludedAppsExist() - emitCheckedApps(excludedApps) - } - } - - private suspend fun emitIncludedAppsExist() { - _include.emit(true) - } - - private suspend fun emitExcludedAppsExist() { - _include.emit(false) - } - - private suspend fun emitCheckedApps(apps: Set) { - _checkedPackages.emit(apps.toMutableStateList()) - } - - private suspend fun emitTunnelAllApplicationsEnabled() { - _isAllApplicationsEnabled.emit(true) - } - - private suspend fun emitTunnelAllApplicationsDisabled() { - _isAllApplicationsEnabled.emit(false) - } - - private fun emitCurrentPackageConfigurations() { - viewModelScope.launch(Dispatchers.IO) { - val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick) - emitSplitTunnelConfiguration(config) - } - } - - fun emitQueriedPackages(query: String) { - viewModelScope.launch(Dispatchers.IO) { - val packages = - getAllInternetCapablePackages().filter { - getPackageLabel(it).lowercase().contains(query.lowercase()) - } - _packages.emit(packages) + private fun getQueriedPackages(query: String) : List { + return getAllInternetCapablePackages().filter { + getPackageLabel(it).lowercase().contains(query.lowercase()) } } @@ -241,14 +129,14 @@ constructor( } private fun isAllApplicationsEnabled(): Boolean { - return _isAllApplicationsEnabled.value + return _uiState.value.isAllApplicationsEnabled } - private suspend fun saveConfig(tunnelConfig: TunnelConfig) { - tunnelRepo.save(tunnelConfig) + private fun saveConfig(tunnelConfig: TunnelConfig) = viewModelScope.launch { + tunnelConfigRepository.save(tunnelConfig) } - private suspend fun updateTunnelConfig(tunnelConfig: TunnelConfig?) { + private fun updateTunnelConfig(tunnelConfig: TunnelConfig?) = viewModelScope.launch { if (tunnelConfig != null) { saveConfig(tunnelConfig) updateSettingsDefaultTunnel(tunnelConfig) @@ -256,23 +144,20 @@ constructor( } private suspend fun updateSettingsDefaultTunnel(tunnelConfig: TunnelConfig) { - val settings = settingsRepo.getAll() - if (settings.isNotEmpty()) { - val setting = settings[0] - if (setting.defaultTunnel != null) { - if (tunnelConfig.id == TunnelConfig.from(setting.defaultTunnel!!).id) { - settingsRepo.save( - setting.copy( - defaultTunnel = tunnelConfig.toString() - ) + val settings = settingsRepository.getSettingsFlow().first() + if (settings.defaultTunnel != null) { + if (tunnelConfig.id == TunnelConfig.from(settings.defaultTunnel!!).id) { + settingsRepository.save( + settings.copy( + defaultTunnel = tunnelConfig.toString() ) - } + ) } } } private fun buildPeerListFromProxyPeers(): List { - return _proxyPeers.value.map { + return _uiState.value.proxyPeers.map { val builder = Peer.Builder() if (it.allowedIps.isNotEmpty()) builder.parseAllowedIPs(it.allowedIps.trim()) if (it.publicKey.isNotEmpty()) builder.parsePublicKey(it.publicKey.trim()) @@ -287,31 +172,37 @@ constructor( } } + fun emptyCheckedPackagesList() { + _uiState.value = _uiState.value.copy( + checkedPackageNames = emptyList() + ) + } + private fun buildInterfaceListFromProxyInterface(): Interface { val builder = Interface.Builder() - builder.parsePrivateKey(_interface.value.privateKey.trim()) - builder.parseAddresses(_interface.value.addresses.trim()) - builder.parseDnsServers(_interface.value.dnsServers.trim()) - if (_interface.value.mtu.isNotEmpty()) builder.parseMtu(_interface.value.mtu.trim()) - if (_interface.value.listenPort.isNotEmpty()) { + builder.parsePrivateKey(_uiState.value.interfaceProxy.privateKey.trim()) + builder.parseAddresses(_uiState.value.interfaceProxy.privateKey.trim()) + builder.parseDnsServers(_uiState.value.interfaceProxy.privateKey.trim()) + if (_uiState.value.interfaceProxy.mtu.isNotEmpty()) builder.parseMtu(_uiState.value.interfaceProxy.privateKey.trim()) + if (_uiState.value.interfaceProxy.listenPort.isNotEmpty()) { builder.parseListenPort( - _interface.value.listenPort.trim() + _uiState.value.interfaceProxy.listenPort.trim() ) } - if (isAllApplicationsEnabled()) _checkedPackages.value.clear() - if (_include.value) builder.includeApplications(_checkedPackages.value) - if (!_include.value) builder.excludeApplications(_checkedPackages.value) + if (isAllApplicationsEnabled()) emptyCheckedPackagesList() + if (_uiState.value.include) builder.includeApplications(_uiState.value.checkedPackageNames) + if (!_uiState.value.include) builder.excludeApplications(_uiState.value.checkedPackageNames) return builder.build() } - suspend fun onSaveAllChanges() { + fun onSaveAllChanges() = viewModelScope.launch { try { val peerList = buildPeerListFromProxyPeers() val wgInterface = buildInterfaceListFromProxyInterface() val config = Config.Builder().addPeers(peerList).setInterface(wgInterface).build() val tunnelConfig = - _tunnel.value?.copy( - name = _tunnelName.value, + _uiState.value.tunnel?.copy( + name = _uiState.value.tunnelName, wgQuick = config.toWgQuickString() ) updateTunnelConfig(tunnelConfig) @@ -324,111 +215,134 @@ constructor( fun onPeerPublicKeyChange( index: Int, - publicKey: String + value: String ) { - _proxyPeers.value[index] = - _proxyPeers.value[index].copy( - publicKey = publicKey + _uiState.value = _uiState.value.copy( + proxyPeers = _uiState.value.proxyPeers.update(index, + _uiState.value.proxyPeers[index].copy( + publicKey = value + ) ) + ) } fun onPreSharedKeyChange( index: Int, value: String ) { - _proxyPeers.value[index] = - _proxyPeers.value[index].copy( - preSharedKey = value + _uiState.value = _uiState.value.copy( + proxyPeers = _uiState.value.proxyPeers.update(index, + _uiState.value.proxyPeers.get(index).copy( + preSharedKey = value + ) ) + ) } fun onEndpointChange( index: Int, value: String ) { - _proxyPeers.value[index] = - _proxyPeers.value[index].copy( - endpoint = value + _uiState.value = _uiState.value.copy( + proxyPeers = _uiState.value.proxyPeers.update(index, + _uiState.value.proxyPeers.get(index).copy( + endpoint = value + ) ) + ) } fun onAllowedIpsChange( index: Int, value: String ) { - _proxyPeers.value[index] = - _proxyPeers.value[index].copy( - allowedIps = value + _uiState.value = _uiState.value.copy( + proxyPeers = _uiState.value.proxyPeers.update(index, + _uiState.value.proxyPeers.get(index).copy( + allowedIps = value + ) ) + ) } fun onPersistentKeepaliveChanged( index: Int, value: String ) { - _proxyPeers.value[index] = - _proxyPeers.value[index].copy( - persistentKeepalive = value + _uiState.value = _uiState.value.copy( + proxyPeers = _uiState.value.proxyPeers.update(index, + _uiState.value.proxyPeers[index].copy( + persistentKeepalive = value + ) ) + ) } fun onDeletePeer(index: Int) { - proxyPeers.value.removeAt(index) + _uiState.value.proxyPeers.removeAt(index) } fun addEmptyPeer() { - _proxyPeers.value.add(PeerProxy()) + _uiState.value.proxyPeers + PeerProxy() } fun generateKeyPair() { val keyPair = KeyPair() - _interface.value = - _interface.value.copy( + _uiState.value = _uiState.value.copy( + interfaceProxy = _uiState.value.interfaceProxy.copy( privateKey = keyPair.privateKey.toBase64(), publicKey = keyPair.publicKey.toBase64() ) + ) } fun onAddressesChanged(value: String) { - _interface.value = - _interface.value.copy( + _uiState.value = _uiState.value.copy( + interfaceProxy = _uiState.value.interfaceProxy.copy( addresses = value ) + ) } fun onListenPortChanged(value: String) { - _interface.value = - _interface.value.copy( + _uiState.value = _uiState.value.copy( + interfaceProxy = _uiState.value.interfaceProxy.copy( listenPort = value ) + ) } fun onDnsServersChanged(value: String) { - _interface.value = - _interface.value.copy( + _uiState.value = _uiState.value.copy( + interfaceProxy = _uiState.value.interfaceProxy.copy( dnsServers = value ) + ) } fun onMtuChanged(value: String) { - _interface.value = - _interface.value.copy( + _uiState.value = _uiState.value.copy( + interfaceProxy = _uiState.value.interfaceProxy.copy( mtu = value ) + ) } private fun onInterfacePublicKeyChange(value: String) { - _interface.value = - _interface.value.copy( - publicKey = value + _uiState.value = + _uiState.value.copy( + interfaceProxy = _uiState.value.interfaceProxy.copy( + publicKey = value + ) ) } fun onPrivateKeyChange(value: String) { - _interface.value = - _interface.value.copy( + _uiState.value = _uiState.value.copy( + interfaceProxy = _uiState.value.interfaceProxy.copy( privateKey = value ) + ) if (NumberUtils.isValidKey(value)) { val pair = KeyPair(Key.fromBase64(value)) onInterfacePublicKeyChange(pair.publicKey.toBase64()) @@ -436,4 +350,16 @@ constructor( onInterfacePublicKeyChange("") } } + + fun emitQueriedPackages(query: String) { + val packages = + getAllInternetCapablePackages().filter { + getPackageLabel(it).lowercase().contains(query.lowercase()) + } + _uiState.value = _uiState.value.copy( + packages = packages + ) + } } + + diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModelFactory.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModelFactory.kt new file mode 100644 index 0000000..6f9a662 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModelFactory.kt @@ -0,0 +1,8 @@ +package com.zaneschepke.wireguardautotunnel.ui.screens.config + +import dagger.assisted.AssistedFactory + +@AssistedFactory +interface ConfigViewModelFactory { + fun create(configId: String): ConfigViewModel +} \ No newline at end of file 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 7ed0adf..cb370bf 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 @@ -52,6 +52,7 @@ import androidx.compose.material3.TextButton import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -85,14 +86,15 @@ import com.zaneschepke.wireguardautotunnel.Constants import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig +import com.zaneschepke.wireguardautotunnel.handshakeStatus +import com.zaneschepke.wireguardautotunnel.mapPeerStats import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus import com.zaneschepke.wireguardautotunnel.ui.CaptureActivityPortrait import com.zaneschepke.wireguardautotunnel.ui.Routes import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem -import com.zaneschepke.wireguardautotunnel.ui.theme.brickRed import com.zaneschepke.wireguardautotunnel.ui.theme.corn import com.zaneschepke.wireguardautotunnel.ui.theme.mint -import com.zaneschepke.wireguardautotunnel.util.WgTunnelException +import com.zaneschepke.wireguardautotunnel.util.Error import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -102,7 +104,7 @@ import kotlinx.coroutines.launch fun MainScreen( viewModel: MainViewModel = hiltViewModel(), padding: PaddingValues, - showSnackbarMessage: (String) -> Unit, + showSnackbarMessage: (Int) -> Unit, navController: NavController ) { val haptic = LocalHapticFeedback.current @@ -113,15 +115,16 @@ fun MainScreen( val sheetState = rememberModalBottomSheetState() var showBottomSheet by remember { mutableStateOf(false) } var showPrimaryChangeAlertDialog by remember { mutableStateOf(false) } - val tunnels by viewModel.tunnels.collectAsStateWithLifecycle(mutableListOf()) - val handshakeStatus by viewModel.handshakeStatus.collectAsStateWithLifecycle( - HandshakeStatus.NOT_STARTED - ) var selectedTunnel by remember { mutableStateOf(null) } - val state by viewModel.state.collectAsStateWithLifecycle(Tunnel.State.DOWN) - val tunnelName by viewModel.tunnelName.collectAsStateWithLifecycle("") - val settings by viewModel.settings.collectAsStateWithLifecycle() - val statistics by viewModel.statistics.collectAsStateWithLifecycle(null) + + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + LaunchedEffect(uiState.errorEvent){ + if(uiState.errorEvent != Error.NONE) { + showSnackbarMessage(uiState.errorEvent.getMessage()) + viewModel.emitErrorEventConsumed() + } + } // Nested scroll for control FAB val nestedScrollConnection = @@ -176,41 +179,21 @@ fun MainScreen( ) } ) { - throw WgTunnelException(context.getString(R.string.no_file_explorer)) + showSnackbarMessage(R.string.error_no_file_explorer) } return intent } } ) { data -> if (data == null) return@rememberLauncherForActivityResult - scope.launch(Dispatchers.IO) { - try { - viewModel.onTunnelFileSelected(data) - } catch (e: WgTunnelException) { - showSnackbarMessage(e.message) - } - } + viewModel.onTunnelFileSelected(data) } val scanLauncher = rememberLauncherForActivityResult( contract = ScanContract(), onResult = { - scope.launch { - try { - viewModel.onTunnelQrResult(it.contents) - } catch (e: Exception) { - when (e) { - is WgTunnelException -> { - showSnackbarMessage(e.message) - } - - else -> { - showSnackbarMessage("No QR code scanned") - } - } - } - } + viewModel.onTunnelQrResult(it.contents) } ) @@ -242,11 +225,7 @@ fun MainScreen( checked: Boolean, tunnel: TunnelConfig ) { - try { - if (checked) viewModel.onTunnelStart(tunnel) else viewModel.onTunnelStop() - } catch (e: Exception) { - showSnackbarMessage(e.message!!) - } + if (checked) viewModel.onTunnelStart(tunnel) else viewModel.onTunnelStop() } Scaffold( @@ -290,7 +269,7 @@ fun MainScreen( } } ) { - AnimatedVisibility(tunnels.isEmpty(), exit = fadeOut(), enter = fadeIn()) { + AnimatedVisibility(uiState.tunnels.isEmpty() && !uiState.loading, exit = fadeOut(), enter = fadeIn()) { Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, @@ -316,11 +295,7 @@ fun MainScreen( .fillMaxWidth() .clickable { showBottomSheet = false - try { - tunnelFileImportResultLauncher.launch(Constants.ALLOWED_FILE_TYPES) - } catch (e: Exception) { - showSnackbarMessage(e.message!!) - } + tunnelFileImportResultLauncher.launch(Constants.ALLOWED_FILE_TYPES) } .padding(10.dp) ) { @@ -406,20 +381,24 @@ fun MainScreen( .padding(top = 10.dp) .nestedScroll(nestedScrollConnection) ) { - items(tunnels, key = { tunnel -> tunnel.id }) { tunnel -> + items(uiState.tunnels, key = { tunnel -> tunnel.id }) { tunnel -> val leadingIconColor = ( - if (tunnelName == tunnel.name) { - when (handshakeStatus) { - HandshakeStatus.HEALTHY -> mint - HandshakeStatus.UNHEALTHY -> brickRed - HandshakeStatus.STALE -> corn - HandshakeStatus.NOT_STARTED -> Color.Gray - HandshakeStatus.NEVER_CONNECTED -> brickRed + if (uiState.vpnState.name == tunnel.name && uiState.vpnState.status == Tunnel.State.UP) { + uiState.vpnState.statistics?.mapPeerStats()?.map { + it.value?.handshakeStatus() + }.let { + when { + it?.all { it == HandshakeStatus.HEALTHY } == true -> mint + it?.any { it == HandshakeStatus.STALE } == true -> corn + it?.all { it == HandshakeStatus.NOT_STARTED } == true -> Color.Gray + else -> { + Color.Gray + } + } } } else { Color.Gray - } - ) + }) val focusRequester = remember { FocusRequester() } val expanded = remember { @@ -427,7 +406,7 @@ fun MainScreen( } RowListItem( icon = { - if (settings.isTunnelConfigDefault(tunnel)) { + if (uiState.settings.isTunnelConfigDefault(tunnel)) { Icon( Icons.Rounded.Star, stringResource(R.string.status), @@ -451,10 +430,9 @@ fun MainScreen( }, text = tunnel.name, onHold = { - if ((state == Tunnel.State.UP) && (tunnel.name == tunnelName)) { + if ((uiState.vpnState.status == Tunnel.State.UP) && (tunnel.name == uiState.vpnState.name)) { showSnackbarMessage( - context.resources.getString(R.string.turn_off_tunnel) - ) + R.string.turn_off_tunnel) return@RowListItem } haptic.performHapticFeedback(HapticFeedbackType.LongPress) @@ -462,7 +440,7 @@ fun MainScreen( }, onClick = { if (!WireGuardAutoTunnel.isRunningOnAndroidTv(context)) { - if (state == Tunnel.State.UP && (tunnelName == tunnel.name)) { + if (uiState.vpnState.status == Tunnel.State.UP && (uiState.vpnState.name == tunnel.name)) { expanded.value = !expanded.value } } else { @@ -470,7 +448,7 @@ fun MainScreen( focusRequester.requestFocus() } }, - statistics = statistics, + statistics = uiState.vpnState.statistics, expanded = expanded.value, rowButton = { if (tunnel.id == selectedTunnel?.id && !WireGuardAutoTunnel.isRunningOnAndroidTv( @@ -478,14 +456,11 @@ fun MainScreen( ) ) { Row { - if (!settings.isTunnelConfigDefault(tunnel)) { + if (!uiState.settings.isTunnelConfigDefault(tunnel)) { IconButton(onClick = { - if (settings.isAutoTunnelEnabled) { + if (uiState.settings.isAutoTunnelEnabled) { showSnackbarMessage( - context.resources.getString( - R.string.turn_off_auto - ) - ) + R.string.turn_off_auto) } else { showPrimaryChangeAlertDialog = true } @@ -514,7 +489,7 @@ fun MainScreen( } } } else { - val checked = state == Tunnel.State.UP && tunnel.name == tunnelName + val checked = uiState.vpnState.status == Tunnel.State.UP && tunnel.name == uiState.vpnState.name if (!checked) expanded.value = false @Composable @@ -529,14 +504,11 @@ fun MainScreen( ) if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) { Row { - if (!settings.isTunnelConfigDefault(tunnel)) { + if (!uiState.settings.isTunnelConfigDefault(tunnel)) { IconButton(onClick = { - if (settings.isAutoTunnelEnabled) { + if (uiState.settings.isAutoTunnelEnabled) { showSnackbarMessage( - context.resources.getString( - R.string.turn_off_auto - ) - ) + R.string.turn_off_auto) } else { showPrimaryChangeAlertDialog = true } @@ -550,7 +522,7 @@ fun MainScreen( IconButton( modifier = Modifier.focusRequester(focusRequester), onClick = { - if (state == Tunnel.State.UP && (tunnelName == tunnel.name)) { + if (uiState.vpnState.status == Tunnel.State.UP && (uiState.vpnState.name == tunnel.name)) { expanded.value = !expanded.value } } @@ -558,12 +530,9 @@ fun MainScreen( Icon(Icons.Rounded.Info, stringResource(R.string.info)) } IconButton(onClick = { - if (state == Tunnel.State.UP && tunnel.name == tunnelName) { + if (uiState.vpnState.status == Tunnel.State.UP && tunnel.name == uiState.vpnState.name) { showSnackbarMessage( - context.resources.getString( - R.string.turn_off_tunnel - ) - ) + R.string.turn_off_tunnel) } else { navController.navigate( "${Routes.Config.name}/${tunnel.id}" @@ -576,12 +545,9 @@ fun MainScreen( ) } IconButton(onClick = { - if (state == Tunnel.State.UP && tunnel.name == tunnelName) { + if (uiState.vpnState.status == Tunnel.State.UP && tunnel.name == uiState.vpnState.name) { showSnackbarMessage( - context.resources.getString( - R.string.turn_off_tunnel - ) - ) + R.string.turn_off_tunnel) } else { viewModel.onDelete(tunnel) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainUiState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainUiState.kt new file mode 100644 index 0000000..239e7af --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainUiState.kt @@ -0,0 +1,14 @@ +package com.zaneschepke.wireguardautotunnel.ui.screens.main + +import com.zaneschepke.wireguardautotunnel.TunnelConfigs +import com.zaneschepke.wireguardautotunnel.data.model.Settings +import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState +import com.zaneschepke.wireguardautotunnel.util.Error + +data class MainUiState( + val settings : Settings = Settings(), + val tunnels : TunnelConfigs = emptyList(), + val vpnState: VpnState = VpnState(), + val loading : Boolean = true, + val errorEvent : Error = Error.NONE +) 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 a9f3c01..962650a 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 @@ -10,16 +10,16 @@ import androidx.lifecycle.viewModelScope import com.wireguard.config.Config import com.zaneschepke.wireguardautotunnel.Constants import com.zaneschepke.wireguardautotunnel.R -import com.zaneschepke.wireguardautotunnel.data.SettingsDao -import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao import com.zaneschepke.wireguardautotunnel.data.model.Settings import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository +import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceState import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService +import com.zaneschepke.wireguardautotunnel.util.Error import com.zaneschepke.wireguardautotunnel.util.NumberUtils import com.zaneschepke.wireguardautotunnel.util.WgTunnelException import dagger.hilt.android.lifecycle.HiltViewModel @@ -29,8 +29,9 @@ import javax.inject.Inject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -39,27 +40,24 @@ class MainViewModel @Inject constructor( private val application: Application, - private val tunnelRepo: TunnelConfigDao, + private val tunnelConfigRepository: TunnelConfigRepository, private val settingsRepository: SettingsRepository, private val vpnService: VpnService ) : ViewModel() { - val tunnels get() = tunnelRepo.getAllFlow() - val state get() = vpnService.state - val handshakeStatus get() = vpnService.handshakeStatus - val tunnelName get() = vpnService.tunnelName - private val _settings = MutableStateFlow(Settings()) - val settings get() = _settings.asStateFlow() - val statistics get() = vpnService.statistics + private val _errorState = MutableStateFlow(Error.NONE) - init { - viewModelScope.launch(Dispatchers.IO) { - settingsRepository.getSettings().collect { - validateWatcherServiceState(it) - _settings.emit(it) - } - } - } + val uiState = combine( + settingsRepository.getSettingsFlow(), + tunnelConfigRepository.getTunnelConfigsFlow(), + vpnService.vpnState, + _errorState, + ){ settings, tunnels, vpnState, errorState -> + validateWatcherServiceState(settings) + MainUiState(settings, tunnels, vpnState, false, errorState) + }.stateIn(viewModelScope, + SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT), MainUiState() + ) private fun validateWatcherServiceState(settings: Settings) { val watcherState = @@ -77,7 +75,7 @@ constructor( fun onDelete(tunnel: TunnelConfig) { viewModelScope.launch { - if (tunnelRepo.count() == 1L) { + if (tunnelConfigRepository.count() == 1) { ServiceManager.stopWatcherService(application.applicationContext) val settings = settingsRepository.getAll() if (settings.isNotEmpty()) { @@ -88,22 +86,20 @@ constructor( saveSettings(setting) } } - tunnelRepo.delete(tunnel) + tunnelConfigRepository.delete(tunnel) } } - fun onTunnelStart(tunnelConfig: TunnelConfig) { - viewModelScope.launch { - stopActiveTunnel() - startTunnel(tunnelConfig) - } + fun onTunnelStart(tunnelConfig: TunnelConfig) = viewModelScope.launch { + stopActiveTunnel() + startTunnel(tunnelConfig) } private fun startTunnel(tunnelConfig: TunnelConfig) { ServiceManager.startVpnService(application.applicationContext, tunnelConfig.toString()) } - private suspend fun stopActiveTunnel() { + private fun stopActiveTunnel() = viewModelScope.launch { if (ServiceManager.getServiceState( application.applicationContext, WireGuardTunnelService::class.java @@ -122,14 +118,14 @@ constructor( TunnelConfig.configFromQuick(config) } - suspend fun onTunnelQrResult(result: String) { + fun onTunnelQrResult(result: String) = viewModelScope.launch { try { validateConfigString(result) val tunnelConfig = TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = result) addTunnel(tunnelConfig) } catch (e: Exception) { - throw WgTunnelException(e) + emitErrorEvent(Error.INVALID_QR) } } @@ -151,19 +147,16 @@ constructor( ?: throw WgTunnelException(application.getString(R.string.stream_failed)) } - suspend fun onTunnelFileSelected(uri: Uri) { + fun onTunnelFileSelected(uri: Uri) = viewModelScope.launch { try { val fileName = getFileName(application.applicationContext, uri) - val fileExtension = getFileExtensionFromFileName(fileName) - when (fileExtension) { + when (getFileExtensionFromFileName(fileName)) { Constants.CONF_FILE_EXTENSION -> saveTunnelFromConfUri(fileName, uri) Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri) - else -> throw WgTunnelException( - application.getString(R.string.file_extension_message) - ) + else -> emitErrorEvent(Error.FILE_EXTENSION) } } catch (e: Exception) { - throw WgTunnelException(e) + emitErrorEvent(Error.FILE_EXTENSION) } } @@ -197,7 +190,7 @@ constructor( } private suspend fun saveTunnel(tunnelConfig: TunnelConfig) { - tunnelRepo.save(tunnelConfig) + tunnelConfigRepository.save(tunnelConfig) } private fun getFileNameByCursor( @@ -233,13 +226,16 @@ constructor( private fun validateUriContentScheme(uri: Uri) { if (uri.scheme != Constants.URI_CONTENT_SCHEME) { - throw WgTunnelException(application.getString(R.string.file_extension_message)) + emitErrorEvent(Error.FILE_EXTENSION) } } - private suspend fun saveSettings(settings: Settings) { - //TODO handle error if fails - settingsRepository.save(settings) + fun emitErrorEventConsumed() { + _errorState.tryEmit(Error.NONE) + } + + private fun emitErrorEvent(error : Error) { + _errorState.tryEmit(error) } private fun getFileName( @@ -266,14 +262,17 @@ constructor( } } - suspend fun onDefaultTunnelChange(selectedTunnel: TunnelConfig?) { + private fun saveSettings(settings: Settings) = viewModelScope.launch { + settingsRepository.save(settings) + } + + fun onDefaultTunnelChange(selectedTunnel: TunnelConfig?) { if (selectedTunnel != null) { - _settings.emit( - _settings.value.copy( + saveSettings( + uiState.value.settings.copy( defaultTunnel = selectedTunnel.toString() ) ) - settingsRepository.save(_settings.value) } } } 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 8ff4823..8c09053 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 @@ -7,6 +7,7 @@ import android.os.Build import android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.clickable +import androidx.compose.foundation.focusable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -38,6 +39,7 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -74,6 +76,7 @@ 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.Error import com.zaneschepke.wireguardautotunnel.util.FileUtils import java.io.File import kotlinx.coroutines.Dispatchers @@ -88,7 +91,7 @@ import kotlinx.coroutines.launch fun SettingsScreen( viewModel: SettingsViewModel = hiltViewModel(), padding: PaddingValues, - showSnackbarMessage: (String) -> Unit, + showSnackbarMessage: (Int) -> Unit, focusRequester: FocusRequester ) { val scope = rememberCoroutineScope { Dispatchers.IO } @@ -109,8 +112,49 @@ fun SettingsScreen( val screenPadding = 5.dp val fillMaxWidth = .85f + LaunchedEffect(uiState.errorEvent) { + if (uiState.errorEvent != Error.NONE) { + showSnackbarMessage(uiState.errorEvent.getMessage()) + viewModel.emitErrorEventConsumed() + } + } + + if(uiState.loading) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top, + modifier = + Modifier.fillMaxSize() + .verticalScroll(rememberScrollState()) + .focusable() + .padding(padding)) { + Surface( + tonalElevation = 2.dp, + shadowElevation = 2.dp, + shape = RoundedCornerShape(12.dp), + color = MaterialTheme.colorScheme.surface, + modifier = + (if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) { + Modifier.height(IntrinsicSize.Min) + .fillMaxWidth(fillMaxWidth) + .padding(top = 10.dp) + } else { + Modifier.fillMaxWidth(fillMaxWidth).padding(top = 20.dp) + }) + .padding(bottom = 25.dp) + ) { + Column(modifier = Modifier.padding(20.dp)) { + Text( + stringResource(R.string.thank_you), + textAlign = TextAlign.Start, + modifier = Modifier.padding(bottom = 20.dp), + fontSize = 16.sp + ) + } + } + } + } - //TODO add error collecting and displaying for WGTunnelErrors fun exportAllConfigs() { try { @@ -122,22 +166,16 @@ fun SettingsScreen( } FileUtils.saveFilesToZip(context, files) didExportFiles = true - showSnackbarMessage(context.getString(R.string.exported_configs_message)) + showSnackbarMessage(R.string.exported_configs_message) } catch (e: Exception) { - showSnackbarMessage(e.message!!) + showSnackbarMessage(Error.GENERAL.getMessage()) } } fun saveTrustedSSID() { if (currentText.isNotEmpty()) { - scope.launch { - try { - viewModel.onSaveTrustedSSID(currentText) - currentText = "" - } catch (e: Exception) { - showSnackbarMessage(e.message ?: context.getString(R.string.unknown_error)) - } - } + viewModel.onSaveTrustedSSID(currentText) + currentText = "" } } @@ -163,9 +201,7 @@ fun SettingsScreen( isBackgroundLocationGranted = if (!fineLocationState.status.isGranted) { false } else { - scope.launch { - viewModel.setLocationDisclosureShown() - } + viewModel.setLocationDisclosureShown() true } } @@ -194,101 +230,101 @@ fun SettingsScreen( } } - AnimatedVisibility(!uiState.isLocationDisclosureShown) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Top, + AnimatedVisibility(!uiState.isLocationDisclosureShown && !uiState.loading) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top, + modifier = + Modifier + .fillMaxSize() + .verticalScroll(scrollState) + .padding(padding) + ) { + Icon( + Icons.Rounded.LocationOff, + contentDescription = stringResource(id = R.string.map), modifier = Modifier - .fillMaxSize() - .verticalScroll(scrollState) - .padding(padding) - ) { - Icon( - Icons.Rounded.LocationOff, - contentDescription = stringResource(id = R.string.map), - modifier = + .padding(30.dp) + .size(128.dp) + ) + Text( + stringResource(R.string.prominent_background_location_title), + textAlign = TextAlign.Center, + modifier = Modifier.padding(30.dp), + fontSize = 20.sp + ) + Text( + stringResource(R.string.prominent_background_location_message), + textAlign = TextAlign.Center, + modifier = Modifier.padding(30.dp), + fontSize = 15.sp + ) + Row( + modifier = + if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) { Modifier + .fillMaxWidth() + .padding(10.dp) + } else { + Modifier + .fillMaxWidth() .padding(30.dp) - .size(128.dp) - ) - Text( - stringResource(R.string.prominent_background_location_title), - textAlign = TextAlign.Center, - modifier = Modifier.padding(30.dp), - fontSize = 20.sp - ) - Text( - stringResource(R.string.prominent_background_location_message), - textAlign = TextAlign.Center, - modifier = Modifier.padding(30.dp), - fontSize = 15.sp - ) - Row( - modifier = - if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) { - Modifier - .fillMaxWidth() - .padding(10.dp) - } else { - Modifier - .fillMaxWidth() - .padding(30.dp) - }, - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceEvenly - ) { - TextButton(onClick = { - viewModel.setLocationDisclosureShown() - }) { - Text(stringResource(id = R.string.no_thanks)) - } - TextButton(modifier = Modifier.focusRequester(focusRequester), onClick = { - openSettings() - viewModel.setLocationDisclosureShown() - }) { - Text(stringResource(id = R.string.turn_on)) - } + }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceEvenly + ) { + TextButton(onClick = { + viewModel.setLocationDisclosureShown() + }) { + Text(stringResource(id = R.string.no_thanks)) + } + TextButton(modifier = Modifier.focusRequester(focusRequester), onClick = { + openSettings() + viewModel.setLocationDisclosureShown() + }) { + Text(stringResource(id = R.string.turn_on)) } } } + } - AnimatedVisibility(showAuthPrompt) { - AuthorizationPrompt( - onSuccess = { - showAuthPrompt = false - exportAllConfigs() - }, - onError = { error -> - showSnackbarMessage(error) - showAuthPrompt = false - }, - onFailure = { - showAuthPrompt = false - showSnackbarMessage(context.getString(R.string.authentication_failed)) - } + AnimatedVisibility(showAuthPrompt) { + AuthorizationPrompt( + onSuccess = { + showAuthPrompt = false + exportAllConfigs() + }, + onError = { error -> + showAuthPrompt = false + showSnackbarMessage(R.string.error_authentication_failed) + showAuthPrompt = false + }, + onFailure = { + showAuthPrompt = false + showSnackbarMessage(R.string.error_authentication_failed) + } + ) + } + + if (uiState.tunnels.isEmpty() && !uiState.loading && uiState.isLocationDisclosureShown) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = + Modifier + .fillMaxSize() + .padding(padding) + ) { + Text( + stringResource(R.string.one_tunnel_required), + textAlign = TextAlign.Center, + modifier = Modifier.padding(15.dp), + fontStyle = FontStyle.Italic ) } - - if (uiState.tunnels.isEmpty()) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - modifier = - Modifier - .fillMaxSize() - .padding(padding) - ) { - Text( - stringResource(R.string.one_tunnel_required), - textAlign = TextAlign.Center, - modifier = Modifier.padding(15.dp), - fontStyle = FontStyle.Italic - ) - } - return - } - + } + if (!uiState.loading && uiState.isLocationDisclosureShown && uiState.tunnels.isNotEmpty()) { Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Top, @@ -453,11 +489,11 @@ fun SettingsScreen( if (!isAllAutoTunnelPermissionsEnabled() && uiState.settings.isTunnelOnWifiEnabled) { val message = if (!isBackgroundLocationGranted) { - context.getString(R.string.background_location_required) + R.string.background_location_required } else if (viewModel.isLocationServicesNeeded()) { - context.getString(R.string.location_services_required) + R.string.location_services_required } else { - context.getString(R.string.precise_location_required) + R.string.precise_location_required } showSnackbarMessage(message) } else { @@ -499,7 +535,7 @@ fun SettingsScreen( stringResource(R.string.use_kernel), enabled = !( uiState.settings.isAutoTunnelEnabled || uiState.settings.isAlwaysOnVpnEnabled || - (uiState.tunnelState == Tunnel.State.UP) + (uiState.vpnState.status == Tunnel.State.UP) ), checked = uiState.settings.isKernelEnabled, padding = screenPadding, @@ -536,9 +572,7 @@ fun SettingsScreen( checked = uiState.settings.isAlwaysOnVpnEnabled, padding = screenPadding, onCheckChanged = { - scope.launch { - viewModel.onToggleAlwaysOnVPN() - } + viewModel.onToggleAlwaysOnVPN() } ) ConfigurationToggle( @@ -547,9 +581,7 @@ fun SettingsScreen( checked = uiState.settings.isShortcutsEnabled, padding = screenPadding, onCheckChanged = { - scope.launch { - viewModel.onToggleShortcutsEnabled() - } + viewModel.onToggleShortcutsEnabled() } ) Row( @@ -577,3 +609,4 @@ fun SettingsScreen( } } } +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsUiState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsUiState.kt index 9a40f73..056eec5 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsUiState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsUiState.kt @@ -1,13 +1,14 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings -import com.wireguard.android.backend.Tunnel import com.zaneschepke.wireguardautotunnel.data.model.Settings import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig - +import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState +import com.zaneschepke.wireguardautotunnel.util.Error data class SettingsUiState( val settings : Settings = Settings(), val tunnels : List = emptyList(), - val tunnelState : Tunnel.State = Tunnel.State.DOWN, + val vpnState: VpnState = VpnState(), val isLocationDisclosureShown : Boolean = true, val loading : Boolean = true, + val errorEvent: Error = Error.NONE ) 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 08a3b1e..40a9b95 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 @@ -7,14 +7,16 @@ import android.os.Build import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wireguard.android.util.RootShell -import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao +import com.zaneschepke.wireguardautotunnel.Constants import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager import com.zaneschepke.wireguardautotunnel.data.model.Settings import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository +import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService -import com.zaneschepke.wireguardautotunnel.util.WgTunnelException +import com.zaneschepke.wireguardautotunnel.util.Error import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow import javax.inject.Inject import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine @@ -27,22 +29,25 @@ class SettingsViewModel @Inject constructor( private val application: Application, - private val tunnelRepo: TunnelConfigDao, + private val tunnelConfigRepository: TunnelConfigRepository, private val settingsRepository: SettingsRepository, private val dataStoreManager: DataStoreManager, private val rootShell: RootShell, private val vpnService: VpnService ) : ViewModel() { + private val _errorState = MutableStateFlow(Error.NONE) + val uiState = combine( - settingsRepository.getSettings(), - tunnelRepo.getAllFlow(), - vpnService.state, + settingsRepository.getSettingsFlow(), + tunnelConfigRepository.getTunnelConfigsFlow(), + vpnService.vpnState, dataStoreManager.locationDisclosureFlow, - ){ settings, tunnels, tunnelState, locationDisclosure -> - SettingsUiState(settings, tunnels, tunnelState, locationDisclosure ?: false, false) + _errorState + ){ settings, tunnels, tunnelState, locationDisclosure, errorState -> + SettingsUiState(settings, tunnels, tunnelState, locationDisclosure ?: false, false, errorState) }.stateIn(viewModelScope, - SharingStarted.WhileSubscribed(5_000L), SettingsUiState()) + SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT), SettingsUiState()) fun onSaveTrustedSSID(ssid: String) { val trimmed = ssid.trim() @@ -50,10 +55,17 @@ constructor( uiState.value.settings.trustedNetworkSSIDs.add(trimmed) saveSettings(uiState.value.settings) } else { - throw WgTunnelException("SSID already exists.") + emitErrorEvent(Error.SSID_EXISTS) } } + fun emitErrorEventConsumed() { + _errorState.tryEmit(Error.NONE) + } + + private fun emitErrorEvent(error : Error) { + _errorState.tryEmit(error) + } suspend fun isLocationDisclosureShown() : Boolean { return dataStoreManager.getFromStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN) ?: false } @@ -76,7 +88,7 @@ constructor( } private suspend fun getDefaultTunnelOrFirst() : String { - return uiState.value.settings.defaultTunnel ?: tunnelRepo.getAll().first().wgQuick + return uiState.value.settings.defaultTunnel ?: tunnelConfigRepository.getAll().first().wgQuick } fun toggleAutoTunnel() = viewModelScope.launch { @@ -94,9 +106,8 @@ constructor( ) } - suspend fun onToggleAlwaysOnVPN() { - val updatedSettings = - uiState.value.settings.copy( + fun onToggleAlwaysOnVPN() = viewModelScope.launch { + val updatedSettings = uiState.value.settings.copy( isAlwaysOnVpnEnabled = !uiState.value.settings.isAlwaysOnVpnEnabled, defaultTunnel = getDefaultTunnelOrFirst() ) @@ -163,7 +174,7 @@ constructor( saveKernelMode(on = true) } catch (e: RootShell.RootShellException) { saveKernelMode(on = false) - throw WgTunnelException("Root shell denied!") + emitErrorEvent(Error.ROOT_DENIED) } } else { saveKernelMode(on = false) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Error.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Error.kt new file mode 100644 index 0000000..153c8c5 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Error.kt @@ -0,0 +1,23 @@ +package com.zaneschepke.wireguardautotunnel.util +import com.zaneschepke.wireguardautotunnel.R + +enum class Error(val code : Int,) { + NONE(0), + SSID_EXISTS(2), + ROOT_DENIED(3), + FILE_EXTENSION(4), + NO_FILE_EXPLORER(5), + INVALID_QR(6), + GENERAL(1); + fun getMessage() : Int { + return when(this.code) { + 1 -> R.string.unknown_error + 2 -> R.string.error_ssid_exists + 3 -> R.string.error_root_denied + 4 -> R.string.error_file_extension + 5 -> R.string.error_no_file_explorer + 6 -> R.string.error_invalid_code + else -> R.string.unknown_error + } + } +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 80f453e..33962f3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -9,7 +9,7 @@ https://github.com/zaneschepke/wgtunnel/issues https://zaneschepke.com/wgtunnel-docs/overview.html https://zaneschepke.com/wgtunnel-docs/privacypolicy.html - File is not a .conf or .zip + File is not a .conf or .zip Turn off tunnel before editing No tunnels added yet! Tunnel name already exists @@ -96,8 +96,6 @@ No trusted wifi names Never Failed to open file stream. - An unknown error occurred. - No file app installed. Other Auto-tunneling Select tunnel to use @@ -128,7 +126,7 @@ Cancel Primary tunnel change Would you like to make this your primary tunnel? - Authentication failed + Authentication failed Enable app shortcuts Export configs Battery saver (beta) @@ -137,7 +135,6 @@ Precise location required Unknown error occurred Exported configs to downloads - No file explorer installed status Tunnel on untrusted wifi zanecschepke@gmail.com @@ -154,4 +151,9 @@ If you are experiencing issues, have improvement ideas, or just want to engage, the following resources are available: Kernel Use kernel module + SSID already exists + Root shell denied + No file explorer installed + No code scanned + Invalid QR code \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/Constants.kt b/buildSrc/src/main/kotlin/Constants.kt index 17ec898..915b421 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.2.5" + const val VERSION_NAME = "3.2.6" const val JVM_TARGET = "17" - const val VERSION_CODE = 32500 + const val VERSION_CODE = 32600 const val TARGET_SDK = 34 const val MIN_SDK = 26 const val APP_ID = "com.zaneschepke.wireguardautotunnel"