refactor state, errors, nav
This commit is contained in:
parent
a3386552d5
commit
12a9e849a6
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 <T> List<T>.update(index: Int, item: T): List<T> = toMutableList().apply { this[index] = item }
|
||||
fun <T> List<T>.removeAt(index: Int): List<T> = toMutableList().apply { this.removeAt(index) }
|
||||
|
||||
typealias TunnelConfigs = List<TunnelConfig>
|
||||
typealias Packages = List<PackageInfo>
|
||||
|
||||
fun Statistics.mapPeerStats(): Map<Key, PeerStats?> {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import kotlinx.coroutines.flow.Flow
|
|||
|
||||
interface SettingsRepository {
|
||||
suspend fun save(settings : Settings)
|
||||
fun getSettings() : Flow<Settings>
|
||||
fun getSettingsFlow() : Flow<Settings>
|
||||
|
||||
suspend fun getAll() : List<Settings>
|
||||
}
|
|
@ -10,7 +10,7 @@ class SettingsRepositoryImpl(private val settingsDoa: SettingsDao) : SettingsRep
|
|||
settingsDoa.save(settings)
|
||||
}
|
||||
|
||||
override fun getSettings(): Flow<Settings> {
|
||||
override fun getSettingsFlow(): Flow<Settings> {
|
||||
return settingsDoa.getSettingsFlow()
|
||||
}
|
||||
|
||||
|
|
|
@ -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<TunnelConfigs>
|
||||
suspend fun getAll() : TunnelConfigs
|
||||
suspend fun save(tunnelConfig: TunnelConfig)
|
||||
suspend fun delete(tunnelConfig: TunnelConfig)
|
||||
suspend fun count() : Int
|
||||
}
|
|
@ -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<TunnelConfigs> {
|
||||
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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,8 +3,7 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel
|
|||
enum class HandshakeStatus {
|
||||
HEALTHY,
|
||||
STALE,
|
||||
UNHEALTHY,
|
||||
NEVER_CONNECTED,
|
||||
UNKNOWN,
|
||||
NOT_STARTED
|
||||
;
|
||||
|
||||
|
|
|
@ -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<Tunnel.State>
|
||||
val tunnelName: SharedFlow<String>
|
||||
val statistics: SharedFlow<Statistics>
|
||||
val lastHandshake: SharedFlow<Map<Key, Long>>
|
||||
val handshakeStatus: SharedFlow<HandshakeStatus>
|
||||
val vpnState: StateFlow<VpnState>
|
||||
|
||||
fun getState(): Tunnel.State
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -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<Tunnel.State>(
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||
replay = 1
|
||||
)
|
||||
|
||||
private val _handshakeStatus =
|
||||
MutableSharedFlow<HandshakeStatus>(
|
||||
replay = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||
)
|
||||
override val state get() = _state.asSharedFlow()
|
||||
|
||||
private val _statistics = MutableSharedFlow<Statistics>(replay = 1)
|
||||
override val statistics get() = _statistics.asSharedFlow()
|
||||
|
||||
private val _lastHandshake = MutableSharedFlow<Map<Key, Long>>(replay = 1)
|
||||
override val lastHandshake get() = _lastHandshake.asSharedFlow()
|
||||
|
||||
override val handshakeStatus: SharedFlow<HandshakeStatus>
|
||||
get() = _handshakeStatus.asSharedFlow()
|
||||
private val _vpnState = MutableStateFlow(VpnState())
|
||||
override val vpnState: StateFlow<VpnState> = _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<Key, Long>()
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<ActivityViewModel>()
|
||||
|
||||
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<ConfigViewModel>(
|
||||
extrasProducer = {
|
||||
defaultViewModelCreationExtras.withCreationCallback<
|
||||
ConfigViewModelFactory> { factory ->
|
||||
factory.create(id)
|
||||
}
|
||||
}
|
||||
)
|
||||
ConfigScreen(
|
||||
viewModel = configViewModel,
|
||||
navController = navController,
|
||||
id = id,
|
||||
showSnackbarMessage = { message ->
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<PeerProxy> = arrayListOf(PeerProxy()),
|
||||
val interfaceProxy: InterfaceProxy = InterfaceProxy(),
|
||||
val packages: Packages = emptyList(),
|
||||
val checkedPackageNames: List<String> = 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
|
||||
)
|
|
@ -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<TunnelConfig?>(null)
|
||||
private val _tunnelName = MutableStateFlow("")
|
||||
val tunnelName get() = _tunnelName.asStateFlow()
|
||||
val tunnel get() = _tunnel.asStateFlow()
|
||||
|
||||
private var _proxyPeers = MutableStateFlow(mutableStateListOf<PeerProxy>())
|
||||
val proxyPeers get() = _proxyPeers.asStateFlow()
|
||||
|
||||
private var _interface = MutableStateFlow(InterfaceProxy())
|
||||
val interfaceProxy = _interface.asStateFlow()
|
||||
|
||||
private val _packages = MutableStateFlow(emptyList<PackageInfo>())
|
||||
val packages get() = _packages.asStateFlow()
|
||||
private val packageManager = application.packageManager
|
||||
|
||||
private val _checkedPackages = MutableStateFlow(mutableStateListOf<String>())
|
||||
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<String>,
|
||||
includedApps: Set<String>
|
||||
) {
|
||||
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<String>) {
|
||||
_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<PackageInfo> {
|
||||
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<Peer> {
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
package com.zaneschepke.wireguardautotunnel.ui.screens.config
|
||||
|
||||
import dagger.assisted.AssistedFactory
|
||||
|
||||
@AssistedFactory
|
||||
interface ConfigViewModelFactory {
|
||||
fun create(configId: String): ConfigViewModel
|
||||
}
|
|
@ -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<TunnelConfig?>(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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<TunnelConfig> = 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
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,7 +9,7 @@
|
|||
<string name="github_url">https://github.com/zaneschepke/wgtunnel/issues</string>
|
||||
<string name="docs_url">https://zaneschepke.com/wgtunnel-docs/overview.html</string>
|
||||
<string name="privacy_policy_url">https://zaneschepke.com/wgtunnel-docs/privacypolicy.html</string>
|
||||
<string name="file_extension_message">File is not a .conf or .zip</string>
|
||||
<string name="error_file_extension">File is not a .conf or .zip</string>
|
||||
<string name="turn_off_tunnel">Turn off tunnel before editing</string>
|
||||
<string name="no_tunnels">No tunnels added yet!</string>
|
||||
<string name="tunnel_exists">Tunnel name already exists</string>
|
||||
|
@ -96,8 +96,6 @@
|
|||
<string name="none">No trusted wifi names</string>
|
||||
<string name="never">Never</string>
|
||||
<string name="stream_failed">Failed to open file stream.</string>
|
||||
<string name="unknown_error_message">An unknown error occurred.</string>
|
||||
<string name="no_file_app">No file app installed.</string>
|
||||
<string name="other">Other</string>
|
||||
<string name="auto_tunneling">Auto-tunneling</string>
|
||||
<string name="select_tunnel">Select tunnel to use</string>
|
||||
|
@ -128,7 +126,7 @@
|
|||
<string name="cancel">Cancel</string>
|
||||
<string name="primary_tunnel_change">Primary tunnel change</string>
|
||||
<string name="primary_tunnel_change_question">Would you like to make this your primary tunnel?</string>
|
||||
<string name="authentication_failed">Authentication failed</string>
|
||||
<string name="error_authentication_failed">Authentication failed</string>
|
||||
<string name="enabled_app_shortcuts">Enable app shortcuts</string>
|
||||
<string name="export_configs">Export configs</string>
|
||||
<string name="battery_saver">Battery saver (beta)</string>
|
||||
|
@ -137,7 +135,6 @@
|
|||
<string name="precise_location_required">Precise location required</string>
|
||||
<string name="unknown_error">Unknown error occurred</string>
|
||||
<string name="exported_configs_message">Exported configs to downloads</string>
|
||||
<string name="no_file_explorer">No file explorer installed</string>
|
||||
<string name="status">status</string>
|
||||
<string name="tunnel_on_wifi">Tunnel on untrusted wifi</string>
|
||||
<string name="my_email">zanecschepke@gmail.com</string>
|
||||
|
@ -154,4 +151,9 @@
|
|||
<string name="support_help_text">If you are experiencing issues, have improvement ideas, or just want to engage, the following resources are available:</string>
|
||||
<string name="kernel">Kernel</string>
|
||||
<string name="use_kernel">Use kernel module</string>
|
||||
<string name="error_ssid_exists">SSID already exists</string>
|
||||
<string name="error_root_denied">Root shell denied</string>
|
||||
<string name="error_no_file_explorer">No file explorer installed</string>
|
||||
<string name="error_no_scan">No code scanned</string>
|
||||
<string name="error_invalid_code">Invalid QR code</string>
|
||||
</resources>
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue