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_CONNECTIVITY_CHECK_INTERVAL = 3000L
|
||||||
const val VPN_STATISTIC_CHECK_INTERVAL = 1000L
|
const val VPN_STATISTIC_CHECK_INTERVAL = 1000L
|
||||||
const val TOGGLE_TUNNEL_DELAY = 500L
|
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 CONF_FILE_EXTENSION = ".conf"
|
||||||
const val ZIP_FILE_EXTENSION = ".zip"
|
const val ZIP_FILE_EXTENSION = ".zip"
|
||||||
const val URI_CONTENT_SCHEME = "content"
|
const val URI_CONTENT_SCHEME = "content"
|
||||||
|
@ -18,4 +15,6 @@ object Constants {
|
||||||
const val ANDROID_TV_EXPLORER_STUB = "com.android.tv.frameworkpackagestubs"
|
const val ANDROID_TV_EXPLORER_STUB = "com.android.tv.frameworkpackagestubs"
|
||||||
const val EMAIL_MIME_TYPE = "message/rfc822"
|
const val EMAIL_MIME_TYPE = "message/rfc822"
|
||||||
const val SYSTEM_EXEMPT_SERVICE_TYPE_ID = 1024
|
const val SYSTEM_EXEMPT_SERVICE_TYPE_ID = 1024
|
||||||
|
|
||||||
|
const val SUBSCRIPTION_TIMEOUT = 5_000L
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,13 @@
|
||||||
package com.zaneschepke.wireguardautotunnel
|
package com.zaneschepke.wireguardautotunnel
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
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.math.BigDecimal
|
||||||
import java.text.DecimalFormat
|
import java.text.DecimalFormat
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
@ -30,3 +37,32 @@ fun BigDecimal.toThreeDecimalPlaceString(): String {
|
||||||
return df.format(this)
|
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 {
|
interface SettingsRepository {
|
||||||
suspend fun save(settings : Settings)
|
suspend fun save(settings : Settings)
|
||||||
fun getSettings() : Flow<Settings>
|
fun getSettingsFlow() : Flow<Settings>
|
||||||
|
|
||||||
suspend fun getAll() : List<Settings>
|
suspend fun getAll() : List<Settings>
|
||||||
}
|
}
|
|
@ -10,7 +10,7 @@ class SettingsRepositoryImpl(private val settingsDoa: SettingsDao) : SettingsRep
|
||||||
settingsDoa.save(settings)
|
settingsDoa.save(settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getSettings(): Flow<Settings> {
|
override fun getSettingsFlow(): Flow<Settings> {
|
||||||
return settingsDoa.getSettingsFlow()
|
return settingsDoa.getSettingsFlow()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,14 @@
|
||||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
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 {
|
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
|
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||||
|
|
||||||
|
import com.zaneschepke.wireguardautotunnel.TunnelConfigs
|
||||||
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
|
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 {
|
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 {
|
//TODO fix connected notification
|
||||||
var didShowConnected = false
|
// launch {
|
||||||
var didShowFailedHandshakeNotification = false
|
// var didShowConnected = false
|
||||||
vpnService.handshakeStatus.collect {
|
// var didShowFailedHandshakeNotification = false
|
||||||
when (it) {
|
// vpnService.handshakeStatus.collect {
|
||||||
HandshakeStatus.NOT_STARTED -> {
|
// when (it) {
|
||||||
}
|
// HandshakeStatus.NOT_STARTED -> {
|
||||||
HandshakeStatus.NEVER_CONNECTED -> {
|
// }
|
||||||
if (!didShowFailedHandshakeNotification) {
|
// HandshakeStatus.NEVER_CONNECTED -> {
|
||||||
launchVpnConnectionFailedNotification(
|
// if (!didShowFailedHandshakeNotification) {
|
||||||
getString(R.string.initial_connection_failure_message)
|
// launchVpnConnectionFailedNotification(
|
||||||
)
|
// getString(R.string.initial_connection_failure_message)
|
||||||
didShowFailedHandshakeNotification = true
|
// )
|
||||||
didShowConnected = false
|
// didShowFailedHandshakeNotification = true
|
||||||
}
|
// didShowConnected = false
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
HandshakeStatus.HEALTHY -> {
|
//
|
||||||
if (!didShowConnected) {
|
// HandshakeStatus.HEALTHY -> {
|
||||||
launchVpnConnectedNotification()
|
// if (!didShowConnected) {
|
||||||
didShowConnected = true
|
// launchVpnConnectedNotification()
|
||||||
}
|
// didShowConnected = true
|
||||||
}
|
// }
|
||||||
HandshakeStatus.STALE -> {}
|
// }
|
||||||
HandshakeStatus.UNHEALTHY -> {
|
// HandshakeStatus.STALE -> {}
|
||||||
if (!didShowFailedHandshakeNotification) {
|
// HandshakeStatus.UNHEALTHY -> {
|
||||||
launchVpnConnectionFailedNotification(
|
// if (!didShowFailedHandshakeNotification) {
|
||||||
getString(R.string.lost_connection_failure_message)
|
// launchVpnConnectionFailedNotification(
|
||||||
)
|
// getString(R.string.lost_connection_failure_message)
|
||||||
didShowFailedHandshakeNotification = true
|
// )
|
||||||
didShowConnected = false
|
// didShowFailedHandshakeNotification = true
|
||||||
}
|
// didShowConnected = false
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -116,21 +116,13 @@ class TunnelControlTile : TileService() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun updateTileState() {
|
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 {
|
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()
|
val config = determineTileTunnel()
|
||||||
setTileDescription(
|
setTileDescription(
|
||||||
config?.name ?: this.resources.getString(R.string.no_tunnel_available)
|
config?.name ?: this.resources.getString(R.string.no_tunnel_available)
|
||||||
|
|
|
@ -3,8 +3,7 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel
|
||||||
enum class HandshakeStatus {
|
enum class HandshakeStatus {
|
||||||
HEALTHY,
|
HEALTHY,
|
||||||
STALE,
|
STALE,
|
||||||
UNHEALTHY,
|
UNKNOWN,
|
||||||
NEVER_CONNECTED,
|
|
||||||
NOT_STARTED
|
NOT_STARTED
|
||||||
;
|
;
|
||||||
|
|
||||||
|
|
|
@ -5,17 +5,14 @@ import com.wireguard.android.backend.Tunnel
|
||||||
import com.wireguard.crypto.Key
|
import com.wireguard.crypto.Key
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
interface VpnService : Tunnel {
|
interface VpnService : Tunnel {
|
||||||
suspend fun startTunnel(tunnelConfig: TunnelConfig): Tunnel.State
|
suspend fun startTunnel(tunnelConfig: TunnelConfig): Tunnel.State
|
||||||
|
|
||||||
suspend fun stopTunnel()
|
suspend fun stopTunnel()
|
||||||
|
|
||||||
val state: SharedFlow<Tunnel.State>
|
val vpnState: StateFlow<VpnState>
|
||||||
val tunnelName: SharedFlow<String>
|
|
||||||
val statistics: SharedFlow<Statistics>
|
|
||||||
val lastHandshake: SharedFlow<Map<Key, Long>>
|
|
||||||
val handshakeStatus: SharedFlow<HandshakeStatus>
|
|
||||||
|
|
||||||
fun getState(): Tunnel.State
|
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.Backend
|
||||||
import com.wireguard.android.backend.BackendException
|
import com.wireguard.android.backend.BackendException
|
||||||
import com.wireguard.android.backend.Statistics
|
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.config.Config
|
||||||
import com.wireguard.crypto.Key
|
|
||||||
import com.zaneschepke.wireguardautotunnel.Constants
|
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.SettingsDao
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
import com.zaneschepke.wireguardautotunnel.module.Kernel
|
||||||
import javax.inject.Inject
|
import com.zaneschepke.wireguardautotunnel.module.Userspace
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.channels.BufferOverflow
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
class WireGuardTunnel
|
class WireGuardTunnel
|
||||||
@Inject
|
@Inject
|
||||||
|
@ -33,30 +28,8 @@ constructor(
|
||||||
@Kernel private val kernelBackend: Backend,
|
@Kernel private val kernelBackend: Backend,
|
||||||
private val settingsRepo: SettingsDao
|
private val settingsRepo: SettingsDao
|
||||||
) : VpnService {
|
) : VpnService {
|
||||||
private val _tunnelName = MutableStateFlow("")
|
private val _vpnState = MutableStateFlow(VpnState())
|
||||||
override val tunnelName get() = _tunnelName.asStateFlow()
|
override val vpnState: StateFlow<VpnState> = _vpnState.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 scope = CoroutineScope(Dispatchers.IO)
|
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 {
|
return try {
|
||||||
stopTunnelOnConfigChange(tunnelConfig)
|
stopTunnelOnConfigChange(tunnelConfig)
|
||||||
emitTunnelName(tunnelConfig.name)
|
emitTunnelName(tunnelConfig.name)
|
||||||
|
@ -93,95 +66,83 @@ constructor(
|
||||||
val state =
|
val state =
|
||||||
backend.setState(
|
backend.setState(
|
||||||
this,
|
this,
|
||||||
Tunnel.State.UP,
|
State.UP,
|
||||||
config
|
config
|
||||||
)
|
)
|
||||||
_state.emit(state)
|
emitTunnelState(state)
|
||||||
state
|
state
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e("Failed to start tunnel with error: ${e.message}")
|
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) {
|
private suspend fun emitTunnelName(name: String) {
|
||||||
_tunnelName.emit(name)
|
_vpnState.emit(
|
||||||
|
_vpnState.value.copy(
|
||||||
|
name = name
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun stopTunnelOnConfigChange(tunnelConfig: TunnelConfig) {
|
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()
|
stopTunnel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getName(): String {
|
override fun getName(): String {
|
||||||
return _tunnelName.value
|
return _vpnState.value.name
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun stopTunnel() {
|
override suspend fun stopTunnel() {
|
||||||
try {
|
try {
|
||||||
if (getState() == Tunnel.State.UP) {
|
if (getState() == State.UP) {
|
||||||
val state = backend.setState(this, Tunnel.State.DOWN, null)
|
val state = backend.setState(this, State.DOWN, null)
|
||||||
_state.emit(state)
|
emitTunnelState(state)
|
||||||
}
|
}
|
||||||
} catch (e: BackendException) {
|
} catch (e: BackendException) {
|
||||||
Timber.e("Failed to stop tunnel with error: ${e.message}")
|
Timber.e("Failed to stop tunnel with error: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getState(): Tunnel.State {
|
override fun getState(): State {
|
||||||
return backend.getState(this)
|
return backend.getState(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStateChange(state: Tunnel.State) {
|
override fun onStateChange(state: State) {
|
||||||
val tunnel = this
|
val tunnel = this
|
||||||
_state.tryEmit(state)
|
emitTunnelState(state)
|
||||||
if (state == Tunnel.State.UP) {
|
if (state == State.UP) {
|
||||||
statsJob =
|
statsJob =
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val handshakeMap = HashMap<Key, Long>()
|
|
||||||
var neverHadHandshakeCounter = 0
|
|
||||||
while (true) {
|
while (true) {
|
||||||
val statistics = backend.getStatistics(tunnel)
|
val statistics = backend.getStatistics(tunnel)
|
||||||
_statistics.emit(statistics)
|
emitBackendStatistics(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)
|
|
||||||
delay(Constants.VPN_STATISTIC_CHECK_INTERVAL)
|
delay(Constants.VPN_STATISTIC_CHECK_INTERVAL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (state == Tunnel.State.DOWN) {
|
if (state == State.DOWN) {
|
||||||
if (this::statsJob.isInitialized) {
|
if (this::statsJob.isInitialized) {
|
||||||
statsJob.cancel()
|
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.rememberLauncherForActivityResult
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
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.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.SnackbarData
|
import androidx.compose.material3.SnackbarData
|
||||||
|
@ -32,7 +29,6 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.input.key.onKeyEvent
|
import androidx.compose.ui.input.key.onKeyEvent
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
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.navigation.BottomNavBar
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar
|
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen
|
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.main.MainScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.TransparentSystemBars
|
import com.zaneschepke.wireguardautotunnel.ui.theme.TransparentSystemBars
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
|
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import dagger.hilt.android.lifecycle.withCreationCallback
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
@ -66,6 +65,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContent {
|
setContent {
|
||||||
// val activityViewModel = hiltViewModel<ActivityViewModel>()
|
// val activityViewModel = hiltViewModel<ActivityViewModel>()
|
||||||
|
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
|
@ -102,20 +102,16 @@ class MainActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showSnackBarMessage(message: String) {
|
fun showSnackBarMessage(message: Int) {
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
val result =
|
val result =
|
||||||
snackbarHostState.showSnackbar(
|
snackbarHostState.showSnackbar(
|
||||||
message = message,
|
message = getString(message),
|
||||||
actionLabel = applicationContext.getString(R.string.okay),
|
actionLabel = applicationContext.getString(R.string.okay),
|
||||||
duration = SnackbarDuration.Short
|
duration = SnackbarDuration.Short
|
||||||
)
|
)
|
||||||
when (result) {
|
when (result) {
|
||||||
SnackbarResult.ActionPerformed -> {
|
SnackbarResult.ActionPerformed, SnackbarResult.Dismissed -> {
|
||||||
snackbarHostState.currentSnackbarData?.dismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
SnackbarResult.Dismissed -> {
|
|
||||||
snackbarHostState.currentSnackbarData?.dismiss()
|
snackbarHostState.currentSnackbarData?.dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -192,86 +188,38 @@ class MainActivity : AppCompatActivity() {
|
||||||
)
|
)
|
||||||
return@Scaffold
|
return@Scaffold
|
||||||
}
|
}
|
||||||
|
|
||||||
NavHost(navController, startDestination = Routes.Main.name) {
|
NavHost(navController, startDestination = Routes.Main.name) {
|
||||||
composable(
|
composable(
|
||||||
Routes.Main.name,
|
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 ->
|
MainScreen(padding = padding, showSnackbarMessage = { message ->
|
||||||
showSnackBarMessage(message)
|
showSnackBarMessage(message)
|
||||||
}, navController = navController)
|
}, navController = navController)
|
||||||
}
|
}
|
||||||
composable(Routes.Settings.name, enterTransition = {
|
composable(Routes.Settings.name,
|
||||||
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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
SettingsScreen(padding = padding, showSnackbarMessage = { message ->
|
SettingsScreen(padding = padding, showSnackbarMessage = { message ->
|
||||||
showSnackBarMessage(message)
|
showSnackBarMessage(message)
|
||||||
}, focusRequester = focusRequester)
|
}, focusRequester = focusRequester)
|
||||||
}
|
}
|
||||||
composable(Routes.Support.name, enterTransition = {
|
composable(Routes.Support.name,
|
||||||
when (initialState.destination.route) {
|
) {
|
||||||
Routes.Settings.name, Routes.Main.name ->
|
SupportScreen(padding = padding, focusRequester = focusRequester)
|
||||||
slideInHorizontally(
|
|
||||||
initialOffsetX = { Constants.SLIDE_IN_ANIMATION_DURATION },
|
|
||||||
animationSpec = tween(Constants.SLIDE_IN_ANIMATION_DURATION)
|
|
||||||
)
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
fadeIn(
|
|
||||||
animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
composable("${Routes.Config.name}/{id}") {
|
||||||
}) { SupportScreen(padding = padding, focusRequester = focusRequester) }
|
|
||||||
composable("${Routes.Config.name}/{id}", enterTransition = {
|
|
||||||
fadeIn(animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION))
|
|
||||||
}) {
|
|
||||||
val id = it.arguments?.getString("id")
|
val id = it.arguments?.getString("id")
|
||||||
if (!id.isNullOrBlank()) {
|
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(
|
ConfigScreen(
|
||||||
|
viewModel = configViewModel,
|
||||||
navController = navController,
|
navController = navController,
|
||||||
id = id,
|
id = id,
|
||||||
showSnackbarMessage = { message ->
|
showSnackbarMessage = { message ->
|
||||||
|
|
|
@ -93,25 +93,16 @@ import timber.log.Timber
|
||||||
)
|
)
|
||||||
@Composable
|
@Composable
|
||||||
fun ConfigScreen(
|
fun ConfigScreen(
|
||||||
viewModel: ConfigViewModel = hiltViewModel(),
|
viewModel: ConfigViewModel,
|
||||||
focusRequester: FocusRequester,
|
focusRequester: FocusRequester,
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
showSnackbarMessage: (String) -> Unit,
|
showSnackbarMessage: (Int) -> Unit,
|
||||||
id: String
|
id: String
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val clipboardManager: ClipboardManager = LocalClipboardManager.current
|
val clipboardManager: ClipboardManager = LocalClipboardManager.current
|
||||||
val keyboardController = LocalSoftwareKeyboardController.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 showApplicationsDialog by remember { mutableStateOf(false) }
|
||||||
var showAuthPrompt by remember { mutableStateOf(false) }
|
var showAuthPrompt by remember { mutableStateOf(false) }
|
||||||
var isAuthenticated by remember { mutableStateOf(false) }
|
var isAuthenticated by remember { mutableStateOf(false) }
|
||||||
|
@ -122,6 +113,8 @@ fun ConfigScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
val keyboardActions =
|
val keyboardActions =
|
||||||
KeyboardActions(
|
KeyboardActions(
|
||||||
onDone = {
|
onDone = {
|
||||||
|
@ -139,23 +132,12 @@ fun ConfigScreen(
|
||||||
val fillMaxWidth = .85f
|
val fillMaxWidth = .85f
|
||||||
val screenPadding = 5.dp
|
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 = {
|
val applicationButtonText = {
|
||||||
"Tunneling apps: " +
|
"Tunneling apps: " +
|
||||||
if (isAllApplicationsEnabled) {
|
if (uiState.isAllApplicationsEnabled) {
|
||||||
"all"
|
"all"
|
||||||
} else {
|
} 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
|
isAuthenticated = true
|
||||||
},
|
},
|
||||||
onError = { error ->
|
onError = { error ->
|
||||||
showSnackbarMessage(error)
|
|
||||||
showAuthPrompt = false
|
showAuthPrompt = false
|
||||||
|
showSnackbarMessage(R.string.error_authentication_failed)
|
||||||
},
|
},
|
||||||
onFailure = {
|
onFailure = {
|
||||||
showAuthPrompt = false
|
showAuthPrompt = false
|
||||||
showSnackbarMessage(context.getString(R.string.authentication_failed))
|
showSnackbarMessage(R.string.error_authentication_failed)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showApplicationsDialog) {
|
if (showApplicationsDialog) {
|
||||||
val sortedPackages =
|
val sortedPackages =
|
||||||
remember(packages) {
|
remember(uiState.packages) {
|
||||||
packages.sortedBy { viewModel.getPackageLabel(it) }
|
uiState.packages.sortedBy { viewModel.getPackageLabel(it) }
|
||||||
}
|
}
|
||||||
AlertDialog(onDismissRequest = {
|
AlertDialog(onDismissRequest = {
|
||||||
showApplicationsDialog = false
|
showApplicationsDialog = false
|
||||||
|
@ -192,7 +174,7 @@ fun ConfigScreen(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.fillMaxHeight(if (isAllApplicationsEnabled) 1 / 5f else 4 / 5f)
|
.fillMaxHeight(if (uiState.isAllApplicationsEnabled) 1 / 5f else 4 / 5f)
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
@ -207,13 +189,13 @@ fun ConfigScreen(
|
||||||
) {
|
) {
|
||||||
Text(stringResource(id = R.string.tunnel_all))
|
Text(stringResource(id = R.string.tunnel_all))
|
||||||
Switch(
|
Switch(
|
||||||
checked = isAllApplicationsEnabled,
|
checked = uiState.isAllApplicationsEnabled,
|
||||||
onCheckedChange = {
|
onCheckedChange = {
|
||||||
viewModel.onAllApplicationsChange(it)
|
viewModel.onAllApplicationsChange(it)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (!isAllApplicationsEnabled) {
|
if (!uiState.isAllApplicationsEnabled) {
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
|
@ -231,9 +213,9 @@ fun ConfigScreen(
|
||||||
) {
|
) {
|
||||||
Text(stringResource(id = R.string.include))
|
Text(stringResource(id = R.string.include))
|
||||||
Checkbox(
|
Checkbox(
|
||||||
checked = include,
|
checked = uiState.include,
|
||||||
onCheckedChange = {
|
onCheckedChange = {
|
||||||
viewModel.onIncludeChange(!include)
|
viewModel.onIncludeChange(!uiState.include)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -243,9 +225,9 @@ fun ConfigScreen(
|
||||||
) {
|
) {
|
||||||
Text(stringResource(id = R.string.exclude))
|
Text(stringResource(id = R.string.exclude))
|
||||||
Checkbox(
|
Checkbox(
|
||||||
checked = !include,
|
checked = !uiState.include,
|
||||||
onCheckedChange = {
|
onCheckedChange = {
|
||||||
viewModel.onIncludeChange(!include)
|
viewModel.onIncludeChange(!uiState.include)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -324,7 +306,7 @@ fun ConfigScreen(
|
||||||
}
|
}
|
||||||
Checkbox(
|
Checkbox(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
checked = (checkedPackages.contains(pack.packageName)),
|
checked = (uiState.checkedPackageNames.contains(pack.packageName)),
|
||||||
onCheckedChange = {
|
onCheckedChange = {
|
||||||
if (it) {
|
if (it) {
|
||||||
viewModel.onAddCheckedPackage(
|
viewModel.onAddCheckedPackage(
|
||||||
|
@ -362,7 +344,7 @@ fun ConfigScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tunnel != null) {
|
if (uiState.tunnel != null) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
floatingActionButtonPosition = FabPosition.End,
|
floatingActionButtonPosition = FabPosition.End,
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
|
@ -371,7 +353,9 @@ fun ConfigScreen(
|
||||||
var fobColor by remember { mutableStateOf(secondaryColor) }
|
var fobColor by remember { mutableStateOf(secondaryColor) }
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.padding(bottom = 90.dp).onFocusChanged {
|
Modifier
|
||||||
|
.padding(bottom = 90.dp)
|
||||||
|
.onFocusChanged {
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||||
fobColor = if (it.isFocused) hoverColor else secondaryColor
|
fobColor = if (it.isFocused) hoverColor else secondaryColor
|
||||||
}
|
}
|
||||||
|
@ -382,11 +366,12 @@ fun ConfigScreen(
|
||||||
viewModel.onSaveAllChanges()
|
viewModel.onSaveAllChanges()
|
||||||
navController.navigate(Routes.Main.name)
|
navController.navigate(Routes.Main.name)
|
||||||
showSnackbarMessage(
|
showSnackbarMessage(
|
||||||
context.resources.getString(R.string.config_changes_saved)
|
R.string.config_changes_saved
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e.message)
|
Timber.e(e.message)
|
||||||
showSnackbarMessage(e.message!!)
|
//TODO fix error handling
|
||||||
|
//showSnackbarMessage(e.message!!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -433,30 +418,36 @@ fun ConfigScreen(
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier = Modifier.padding(15.dp).focusGroup()
|
modifier = Modifier
|
||||||
|
.padding(15.dp)
|
||||||
|
.focusGroup()
|
||||||
) {
|
) {
|
||||||
SectionTitle(
|
SectionTitle(
|
||||||
stringResource(R.string.interface_),
|
stringResource(R.string.interface_),
|
||||||
padding = screenPadding
|
padding = screenPadding
|
||||||
)
|
)
|
||||||
ConfigurationTextBox(
|
ConfigurationTextBox(
|
||||||
value = tunnelName.value,
|
value = uiState.tunnelName,
|
||||||
onValueChange = { value ->
|
onValueChange = { value ->
|
||||||
viewModel.onTunnelNameChange(value)
|
viewModel.onTunnelNameChange(value)
|
||||||
},
|
},
|
||||||
keyboardActions = keyboardActions,
|
keyboardActions = keyboardActions,
|
||||||
label = stringResource(R.string.name),
|
label = stringResource(R.string.name),
|
||||||
hint = stringResource(R.string.tunnel_name).lowercase(),
|
hint = stringResource(R.string.tunnel_name).lowercase(),
|
||||||
modifier = baseTextBoxModifier.fillMaxWidth().focusRequester(
|
modifier = baseTextBoxModifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.focusRequester(
|
||||||
focusRequester
|
focusRequester
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
modifier =
|
modifier =
|
||||||
baseTextBoxModifier.fillMaxWidth().clickable {
|
baseTextBoxModifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable {
|
||||||
showAuthPrompt = true
|
showAuthPrompt = true
|
||||||
},
|
},
|
||||||
value = proxyInterface.privateKey,
|
value = uiState.interfaceProxy.privateKey,
|
||||||
visualTransformation = if ((id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated) VisualTransformation.None else PasswordVisualTransformation(),
|
visualTransformation = if ((id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated) VisualTransformation.None else PasswordVisualTransformation(),
|
||||||
enabled = (id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated,
|
enabled = (id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated,
|
||||||
onValueChange = { value ->
|
onValueChange = { value ->
|
||||||
|
@ -483,10 +474,12 @@ fun ConfigScreen(
|
||||||
keyboardActions = keyboardActions
|
keyboardActions = keyboardActions
|
||||||
)
|
)
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
modifier = baseTextBoxModifier.fillMaxWidth().focusRequester(
|
modifier = baseTextBoxModifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.focusRequester(
|
||||||
FocusRequester.Default
|
FocusRequester.Default
|
||||||
),
|
),
|
||||||
value = proxyInterface.publicKey,
|
value = uiState.interfaceProxy.publicKey,
|
||||||
enabled = false,
|
enabled = false,
|
||||||
onValueChange = {},
|
onValueChange = {},
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
|
@ -494,7 +487,7 @@ fun ConfigScreen(
|
||||||
modifier = Modifier.focusRequester(FocusRequester.Default),
|
modifier = Modifier.focusRequester(FocusRequester.Default),
|
||||||
onClick = {
|
onClick = {
|
||||||
clipboardManager.setText(
|
clipboardManager.setText(
|
||||||
AnnotatedString(proxyInterface.publicKey)
|
AnnotatedString(uiState.interfaceProxy.publicKey)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
@ -513,7 +506,7 @@ fun ConfigScreen(
|
||||||
)
|
)
|
||||||
Row(modifier = Modifier.fillMaxWidth()) {
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
ConfigurationTextBox(
|
ConfigurationTextBox(
|
||||||
value = proxyInterface.addresses,
|
value = uiState.interfaceProxy.addresses,
|
||||||
onValueChange = { value ->
|
onValueChange = { value ->
|
||||||
viewModel.onAddressesChanged(value)
|
viewModel.onAddressesChanged(value)
|
||||||
},
|
},
|
||||||
|
@ -526,7 +519,7 @@ fun ConfigScreen(
|
||||||
.padding(end = 5.dp)
|
.padding(end = 5.dp)
|
||||||
)
|
)
|
||||||
ConfigurationTextBox(
|
ConfigurationTextBox(
|
||||||
value = proxyInterface.listenPort,
|
value = uiState.interfaceProxy.listenPort,
|
||||||
onValueChange = { value -> viewModel.onListenPortChanged(value) },
|
onValueChange = { value -> viewModel.onListenPortChanged(value) },
|
||||||
keyboardActions = keyboardActions,
|
keyboardActions = keyboardActions,
|
||||||
label = stringResource(R.string.listen_port),
|
label = stringResource(R.string.listen_port),
|
||||||
|
@ -536,7 +529,7 @@ fun ConfigScreen(
|
||||||
}
|
}
|
||||||
Row(modifier = Modifier.fillMaxWidth()) {
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
ConfigurationTextBox(
|
ConfigurationTextBox(
|
||||||
value = proxyInterface.dnsServers,
|
value = uiState.interfaceProxy.dnsServers,
|
||||||
onValueChange = { value -> viewModel.onDnsServersChanged(value) },
|
onValueChange = { value -> viewModel.onDnsServersChanged(value) },
|
||||||
keyboardActions = keyboardActions,
|
keyboardActions = keyboardActions,
|
||||||
label = stringResource(R.string.dns_servers),
|
label = stringResource(R.string.dns_servers),
|
||||||
|
@ -547,7 +540,7 @@ fun ConfigScreen(
|
||||||
.padding(end = 5.dp)
|
.padding(end = 5.dp)
|
||||||
)
|
)
|
||||||
ConfigurationTextBox(
|
ConfigurationTextBox(
|
||||||
value = proxyInterface.mtu,
|
value = uiState.interfaceProxy.mtu,
|
||||||
onValueChange = { value -> viewModel.onMtuChanged(value) },
|
onValueChange = { value -> viewModel.onMtuChanged(value) },
|
||||||
keyboardActions = keyboardActions,
|
keyboardActions = keyboardActions,
|
||||||
label = stringResource(R.string.mtu),
|
label = stringResource(R.string.mtu),
|
||||||
|
@ -573,7 +566,7 @@ fun ConfigScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
proxyPeers.forEachIndexed { index, peer ->
|
uiState.proxyPeers.forEachIndexed { index, peer ->
|
||||||
Surface(
|
Surface(
|
||||||
tonalElevation = 2.dp,
|
tonalElevation = 2.dp,
|
||||||
shadowElevation = 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.PackageInfo
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
|
||||||
import androidx.compose.runtime.toMutableStateList
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.wireguard.config.Config
|
import com.wireguard.config.Config
|
||||||
|
@ -14,211 +12,101 @@ import com.wireguard.config.Interface
|
||||||
import com.wireguard.config.Peer
|
import com.wireguard.config.Peer
|
||||||
import com.wireguard.crypto.Key
|
import com.wireguard.crypto.Key
|
||||||
import com.wireguard.crypto.KeyPair
|
import com.wireguard.crypto.KeyPair
|
||||||
import com.zaneschepke.wireguardautotunnel.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.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.InterfaceProxy
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.models.PeerProxy
|
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.NumberUtils
|
||||||
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
|
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel(assistedFactory = ConfigViewModelFactory::class)
|
||||||
class ConfigViewModel
|
class ConfigViewModel
|
||||||
@Inject
|
@AssistedInject
|
||||||
constructor(
|
constructor(
|
||||||
private val application: Application,
|
private val application: Application,
|
||||||
private val tunnelRepo: TunnelConfigDao,
|
private val tunnelConfigRepository: TunnelConfigRepository,
|
||||||
private val settingsRepo: SettingsDao
|
private val settingsRepository: SettingsRepository,
|
||||||
|
@Assisted val tunnelId : String
|
||||||
) : ViewModel() {
|
) : 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 packageManager = application.packageManager
|
||||||
|
|
||||||
private val _checkedPackages = MutableStateFlow(mutableStateListOf<String>())
|
private val _uiState = MutableStateFlow(ConfigUiState())
|
||||||
val checkedPackages get() = _checkedPackages.asStateFlow()
|
val uiState = _uiState.asStateFlow()
|
||||||
private val _include = MutableStateFlow(true)
|
|
||||||
val include get() = _include.asStateFlow()
|
|
||||||
|
|
||||||
private val _isAllApplicationsEnabled = MutableStateFlow(false)
|
init {
|
||||||
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 = "")
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
emitTunnelConfig()
|
val packages = getQueriedPackages("")
|
||||||
emitPeerProxy(PeerProxy())
|
val tunnelConfig = tunnelConfigRepository.getAll().firstOrNull{ it.id.toString() == tunnelId }
|
||||||
emitInterfaceProxy(InterfaceProxy())
|
val state = if(tunnelConfig != null) {
|
||||||
emitTunnelConfigName()
|
|
||||||
emitDefaultTunnelStatus()
|
|
||||||
emitQueriedPackages("")
|
|
||||||
emitTunnelAllApplicationsEnabled()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
||||||
_interface.value = InterfaceProxy.from(config.`interface`)
|
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(),
|
||||||
private fun emitPeersFromConfig() {
|
include, isAllApplicationsEnabled, false, tunnelConfig, tunnelConfig.name, Error.NONE)
|
||||||
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
} else {
|
||||||
config.peers.forEach {
|
ConfigUiState(isLoading = false, packages = packages)
|
||||||
_proxyPeers.value.add(PeerProxy.from(it))
|
|
||||||
}
|
}
|
||||||
|
_uiState.value = state
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
fun onTunnelNameChange(name: String) {
|
||||||
_tunnelName.value = name
|
_uiState.value = _uiState.value.copy(
|
||||||
|
tunnelName = name
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onIncludeChange(include: Boolean) {
|
fun onIncludeChange(include: Boolean) {
|
||||||
_include.value = include
|
_uiState.value = _uiState.value.copy(
|
||||||
|
include = include
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onAddCheckedPackage(packageName: String) {
|
fun onAddCheckedPackage(packageName: String) {
|
||||||
_checkedPackages.value.add(packageName)
|
_uiState.value = _uiState.value.copy(
|
||||||
|
checkedPackageNames = _uiState.value.checkedPackageNames + packageName
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onAllApplicationsChange(isAllApplicationsEnabled: Boolean) {
|
fun onAllApplicationsChange(isAllApplicationsEnabled: Boolean) {
|
||||||
_isAllApplicationsEnabled.value = isAllApplicationsEnabled
|
_uiState.value = _uiState.value.copy(
|
||||||
|
isAllApplicationsEnabled = isAllApplicationsEnabled
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onRemoveCheckedPackage(packageName: String) {
|
fun onRemoveCheckedPackage(packageName: String) {
|
||||||
_checkedPackages.value.remove(packageName)
|
_uiState.value = _uiState.value.copy(
|
||||||
|
checkedPackageNames = _uiState.value.checkedPackageNames - packageName
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun emitSplitTunnelConfiguration(config: Config) {
|
private fun getQueriedPackages(query: String) : List<PackageInfo> {
|
||||||
val excludedApps = config.`interface`.excludedApplications
|
return getAllInternetCapablePackages().filter {
|
||||||
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())
|
getPackageLabel(it).lowercase().contains(query.lowercase())
|
||||||
}
|
}
|
||||||
_packages.emit(packages)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getPackageLabel(packageInfo: PackageInfo): String {
|
fun getPackageLabel(packageInfo: PackageInfo): String {
|
||||||
|
@ -241,14 +129,14 @@ constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isAllApplicationsEnabled(): Boolean {
|
private fun isAllApplicationsEnabled(): Boolean {
|
||||||
return _isAllApplicationsEnabled.value
|
return _uiState.value.isAllApplicationsEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun saveConfig(tunnelConfig: TunnelConfig) {
|
private fun saveConfig(tunnelConfig: TunnelConfig) = viewModelScope.launch {
|
||||||
tunnelRepo.save(tunnelConfig)
|
tunnelConfigRepository.save(tunnelConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun updateTunnelConfig(tunnelConfig: TunnelConfig?) {
|
private fun updateTunnelConfig(tunnelConfig: TunnelConfig?) = viewModelScope.launch {
|
||||||
if (tunnelConfig != null) {
|
if (tunnelConfig != null) {
|
||||||
saveConfig(tunnelConfig)
|
saveConfig(tunnelConfig)
|
||||||
updateSettingsDefaultTunnel(tunnelConfig)
|
updateSettingsDefaultTunnel(tunnelConfig)
|
||||||
|
@ -256,23 +144,20 @@ constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun updateSettingsDefaultTunnel(tunnelConfig: TunnelConfig) {
|
private suspend fun updateSettingsDefaultTunnel(tunnelConfig: TunnelConfig) {
|
||||||
val settings = settingsRepo.getAll()
|
val settings = settingsRepository.getSettingsFlow().first()
|
||||||
if (settings.isNotEmpty()) {
|
if (settings.defaultTunnel != null) {
|
||||||
val setting = settings[0]
|
if (tunnelConfig.id == TunnelConfig.from(settings.defaultTunnel!!).id) {
|
||||||
if (setting.defaultTunnel != null) {
|
settingsRepository.save(
|
||||||
if (tunnelConfig.id == TunnelConfig.from(setting.defaultTunnel!!).id) {
|
settings.copy(
|
||||||
settingsRepo.save(
|
|
||||||
setting.copy(
|
|
||||||
defaultTunnel = tunnelConfig.toString()
|
defaultTunnel = tunnelConfig.toString()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildPeerListFromProxyPeers(): List<Peer> {
|
private fun buildPeerListFromProxyPeers(): List<Peer> {
|
||||||
return _proxyPeers.value.map {
|
return _uiState.value.proxyPeers.map {
|
||||||
val builder = Peer.Builder()
|
val builder = Peer.Builder()
|
||||||
if (it.allowedIps.isNotEmpty()) builder.parseAllowedIPs(it.allowedIps.trim())
|
if (it.allowedIps.isNotEmpty()) builder.parseAllowedIPs(it.allowedIps.trim())
|
||||||
if (it.publicKey.isNotEmpty()) builder.parsePublicKey(it.publicKey.trim())
|
if (it.publicKey.isNotEmpty()) builder.parsePublicKey(it.publicKey.trim())
|
||||||
|
@ -287,31 +172,37 @@ constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildInterfaceListFromProxyInterface(): Interface {
|
fun emptyCheckedPackagesList() {
|
||||||
val builder = Interface.Builder()
|
_uiState.value = _uiState.value.copy(
|
||||||
builder.parsePrivateKey(_interface.value.privateKey.trim())
|
checkedPackageNames = emptyList()
|
||||||
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.parseListenPort(
|
|
||||||
_interface.value.listenPort.trim()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (isAllApplicationsEnabled()) _checkedPackages.value.clear()
|
|
||||||
if (_include.value) builder.includeApplications(_checkedPackages.value)
|
private fun buildInterfaceListFromProxyInterface(): Interface {
|
||||||
if (!_include.value) builder.excludeApplications(_checkedPackages.value)
|
val builder = Interface.Builder()
|
||||||
|
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(
|
||||||
|
_uiState.value.interfaceProxy.listenPort.trim()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (isAllApplicationsEnabled()) emptyCheckedPackagesList()
|
||||||
|
if (_uiState.value.include) builder.includeApplications(_uiState.value.checkedPackageNames)
|
||||||
|
if (!_uiState.value.include) builder.excludeApplications(_uiState.value.checkedPackageNames)
|
||||||
return builder.build()
|
return builder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun onSaveAllChanges() {
|
fun onSaveAllChanges() = viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
val peerList = buildPeerListFromProxyPeers()
|
val peerList = buildPeerListFromProxyPeers()
|
||||||
val wgInterface = buildInterfaceListFromProxyInterface()
|
val wgInterface = buildInterfaceListFromProxyInterface()
|
||||||
val config = Config.Builder().addPeers(peerList).setInterface(wgInterface).build()
|
val config = Config.Builder().addPeers(peerList).setInterface(wgInterface).build()
|
||||||
val tunnelConfig =
|
val tunnelConfig =
|
||||||
_tunnel.value?.copy(
|
_uiState.value.tunnel?.copy(
|
||||||
name = _tunnelName.value,
|
name = _uiState.value.tunnelName,
|
||||||
wgQuick = config.toWgQuickString()
|
wgQuick = config.toWgQuickString()
|
||||||
)
|
)
|
||||||
updateTunnelConfig(tunnelConfig)
|
updateTunnelConfig(tunnelConfig)
|
||||||
|
@ -324,11 +215,14 @@ constructor(
|
||||||
|
|
||||||
fun onPeerPublicKeyChange(
|
fun onPeerPublicKeyChange(
|
||||||
index: Int,
|
index: Int,
|
||||||
publicKey: String
|
value: String
|
||||||
) {
|
) {
|
||||||
_proxyPeers.value[index] =
|
_uiState.value = _uiState.value.copy(
|
||||||
_proxyPeers.value[index].copy(
|
proxyPeers = _uiState.value.proxyPeers.update(index,
|
||||||
publicKey = publicKey
|
_uiState.value.proxyPeers[index].copy(
|
||||||
|
publicKey = value
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -336,99 +230,119 @@ constructor(
|
||||||
index: Int,
|
index: Int,
|
||||||
value: String
|
value: String
|
||||||
) {
|
) {
|
||||||
_proxyPeers.value[index] =
|
_uiState.value = _uiState.value.copy(
|
||||||
_proxyPeers.value[index].copy(
|
proxyPeers = _uiState.value.proxyPeers.update(index,
|
||||||
|
_uiState.value.proxyPeers.get(index).copy(
|
||||||
preSharedKey = value
|
preSharedKey = value
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onEndpointChange(
|
fun onEndpointChange(
|
||||||
index: Int,
|
index: Int,
|
||||||
value: String
|
value: String
|
||||||
) {
|
) {
|
||||||
_proxyPeers.value[index] =
|
_uiState.value = _uiState.value.copy(
|
||||||
_proxyPeers.value[index].copy(
|
proxyPeers = _uiState.value.proxyPeers.update(index,
|
||||||
|
_uiState.value.proxyPeers.get(index).copy(
|
||||||
endpoint = value
|
endpoint = value
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onAllowedIpsChange(
|
fun onAllowedIpsChange(
|
||||||
index: Int,
|
index: Int,
|
||||||
value: String
|
value: String
|
||||||
) {
|
) {
|
||||||
_proxyPeers.value[index] =
|
_uiState.value = _uiState.value.copy(
|
||||||
_proxyPeers.value[index].copy(
|
proxyPeers = _uiState.value.proxyPeers.update(index,
|
||||||
|
_uiState.value.proxyPeers.get(index).copy(
|
||||||
allowedIps = value
|
allowedIps = value
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onPersistentKeepaliveChanged(
|
fun onPersistentKeepaliveChanged(
|
||||||
index: Int,
|
index: Int,
|
||||||
value: String
|
value: String
|
||||||
) {
|
) {
|
||||||
_proxyPeers.value[index] =
|
_uiState.value = _uiState.value.copy(
|
||||||
_proxyPeers.value[index].copy(
|
proxyPeers = _uiState.value.proxyPeers.update(index,
|
||||||
|
_uiState.value.proxyPeers[index].copy(
|
||||||
persistentKeepalive = value
|
persistentKeepalive = value
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onDeletePeer(index: Int) {
|
fun onDeletePeer(index: Int) {
|
||||||
proxyPeers.value.removeAt(index)
|
_uiState.value.proxyPeers.removeAt(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addEmptyPeer() {
|
fun addEmptyPeer() {
|
||||||
_proxyPeers.value.add(PeerProxy())
|
_uiState.value.proxyPeers + PeerProxy()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun generateKeyPair() {
|
fun generateKeyPair() {
|
||||||
val keyPair = KeyPair()
|
val keyPair = KeyPair()
|
||||||
_interface.value =
|
_uiState.value = _uiState.value.copy(
|
||||||
_interface.value.copy(
|
interfaceProxy = _uiState.value.interfaceProxy.copy(
|
||||||
privateKey = keyPair.privateKey.toBase64(),
|
privateKey = keyPair.privateKey.toBase64(),
|
||||||
publicKey = keyPair.publicKey.toBase64()
|
publicKey = keyPair.publicKey.toBase64()
|
||||||
)
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onAddressesChanged(value: String) {
|
fun onAddressesChanged(value: String) {
|
||||||
_interface.value =
|
_uiState.value = _uiState.value.copy(
|
||||||
_interface.value.copy(
|
interfaceProxy = _uiState.value.interfaceProxy.copy(
|
||||||
addresses = value
|
addresses = value
|
||||||
)
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onListenPortChanged(value: String) {
|
fun onListenPortChanged(value: String) {
|
||||||
_interface.value =
|
_uiState.value = _uiState.value.copy(
|
||||||
_interface.value.copy(
|
interfaceProxy = _uiState.value.interfaceProxy.copy(
|
||||||
listenPort = value
|
listenPort = value
|
||||||
)
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onDnsServersChanged(value: String) {
|
fun onDnsServersChanged(value: String) {
|
||||||
_interface.value =
|
_uiState.value = _uiState.value.copy(
|
||||||
_interface.value.copy(
|
interfaceProxy = _uiState.value.interfaceProxy.copy(
|
||||||
dnsServers = value
|
dnsServers = value
|
||||||
)
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onMtuChanged(value: String) {
|
fun onMtuChanged(value: String) {
|
||||||
_interface.value =
|
_uiState.value = _uiState.value.copy(
|
||||||
_interface.value.copy(
|
interfaceProxy = _uiState.value.interfaceProxy.copy(
|
||||||
mtu = value
|
mtu = value
|
||||||
)
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onInterfacePublicKeyChange(value: String) {
|
private fun onInterfacePublicKeyChange(value: String) {
|
||||||
_interface.value =
|
_uiState.value =
|
||||||
_interface.value.copy(
|
_uiState.value.copy(
|
||||||
|
interfaceProxy = _uiState.value.interfaceProxy.copy(
|
||||||
publicKey = value
|
publicKey = value
|
||||||
)
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onPrivateKeyChange(value: String) {
|
fun onPrivateKeyChange(value: String) {
|
||||||
_interface.value =
|
_uiState.value = _uiState.value.copy(
|
||||||
_interface.value.copy(
|
interfaceProxy = _uiState.value.interfaceProxy.copy(
|
||||||
privateKey = value
|
privateKey = value
|
||||||
)
|
)
|
||||||
|
)
|
||||||
if (NumberUtils.isValidKey(value)) {
|
if (NumberUtils.isValidKey(value)) {
|
||||||
val pair = KeyPair(Key.fromBase64(value))
|
val pair = KeyPair(Key.fromBase64(value))
|
||||||
onInterfacePublicKeyChange(pair.publicKey.toBase64())
|
onInterfacePublicKeyChange(pair.publicKey.toBase64())
|
||||||
|
@ -436,4 +350,16 @@ constructor(
|
||||||
onInterfacePublicKeyChange("")
|
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.rememberModalBottomSheetState
|
||||||
import androidx.compose.material3.surfaceColorAtElevation
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
@ -85,14 +86,15 @@ import com.zaneschepke.wireguardautotunnel.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
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.service.tunnel.HandshakeStatus
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.CaptureActivityPortrait
|
import com.zaneschepke.wireguardautotunnel.ui.CaptureActivityPortrait
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.Routes
|
import com.zaneschepke.wireguardautotunnel.ui.Routes
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem
|
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.corn
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.mint
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@ -102,7 +104,7 @@ import kotlinx.coroutines.launch
|
||||||
fun MainScreen(
|
fun MainScreen(
|
||||||
viewModel: MainViewModel = hiltViewModel(),
|
viewModel: MainViewModel = hiltViewModel(),
|
||||||
padding: PaddingValues,
|
padding: PaddingValues,
|
||||||
showSnackbarMessage: (String) -> Unit,
|
showSnackbarMessage: (Int) -> Unit,
|
||||||
navController: NavController
|
navController: NavController
|
||||||
) {
|
) {
|
||||||
val haptic = LocalHapticFeedback.current
|
val haptic = LocalHapticFeedback.current
|
||||||
|
@ -113,15 +115,16 @@ fun MainScreen(
|
||||||
val sheetState = rememberModalBottomSheetState()
|
val sheetState = rememberModalBottomSheetState()
|
||||||
var showBottomSheet by remember { mutableStateOf(false) }
|
var showBottomSheet by remember { mutableStateOf(false) }
|
||||||
var showPrimaryChangeAlertDialog 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) }
|
var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) }
|
||||||
val state by viewModel.state.collectAsStateWithLifecycle(Tunnel.State.DOWN)
|
|
||||||
val tunnelName by viewModel.tunnelName.collectAsStateWithLifecycle("")
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
val settings by viewModel.settings.collectAsStateWithLifecycle()
|
|
||||||
val statistics by viewModel.statistics.collectAsStateWithLifecycle(null)
|
LaunchedEffect(uiState.errorEvent){
|
||||||
|
if(uiState.errorEvent != Error.NONE) {
|
||||||
|
showSnackbarMessage(uiState.errorEvent.getMessage())
|
||||||
|
viewModel.emitErrorEventConsumed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Nested scroll for control FAB
|
// Nested scroll for control FAB
|
||||||
val nestedScrollConnection =
|
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
|
return intent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) { data ->
|
) { data ->
|
||||||
if (data == null) return@rememberLauncherForActivityResult
|
if (data == null) return@rememberLauncherForActivityResult
|
||||||
scope.launch(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
viewModel.onTunnelFileSelected(data)
|
viewModel.onTunnelFileSelected(data)
|
||||||
} catch (e: WgTunnelException) {
|
|
||||||
showSnackbarMessage(e.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val scanLauncher =
|
val scanLauncher =
|
||||||
rememberLauncherForActivityResult(
|
rememberLauncherForActivityResult(
|
||||||
contract = ScanContract(),
|
contract = ScanContract(),
|
||||||
onResult = {
|
onResult = {
|
||||||
scope.launch {
|
|
||||||
try {
|
|
||||||
viewModel.onTunnelQrResult(it.contents)
|
viewModel.onTunnelQrResult(it.contents)
|
||||||
} catch (e: Exception) {
|
|
||||||
when (e) {
|
|
||||||
is WgTunnelException -> {
|
|
||||||
showSnackbarMessage(e.message)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
showSnackbarMessage("No QR code scanned")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -242,11 +225,7 @@ fun MainScreen(
|
||||||
checked: Boolean,
|
checked: Boolean,
|
||||||
tunnel: TunnelConfig
|
tunnel: TunnelConfig
|
||||||
) {
|
) {
|
||||||
try {
|
|
||||||
if (checked) viewModel.onTunnelStart(tunnel) else viewModel.onTunnelStop()
|
if (checked) viewModel.onTunnelStart(tunnel) else viewModel.onTunnelStop()
|
||||||
} catch (e: Exception) {
|
|
||||||
showSnackbarMessage(e.message!!)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
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(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
|
@ -316,11 +295,7 @@ fun MainScreen(
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable {
|
.clickable {
|
||||||
showBottomSheet = false
|
showBottomSheet = false
|
||||||
try {
|
|
||||||
tunnelFileImportResultLauncher.launch(Constants.ALLOWED_FILE_TYPES)
|
tunnelFileImportResultLauncher.launch(Constants.ALLOWED_FILE_TYPES)
|
||||||
} catch (e: Exception) {
|
|
||||||
showSnackbarMessage(e.message!!)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.padding(10.dp)
|
.padding(10.dp)
|
||||||
) {
|
) {
|
||||||
|
@ -406,20 +381,24 @@ fun MainScreen(
|
||||||
.padding(top = 10.dp)
|
.padding(top = 10.dp)
|
||||||
.nestedScroll(nestedScrollConnection)
|
.nestedScroll(nestedScrollConnection)
|
||||||
) {
|
) {
|
||||||
items(tunnels, key = { tunnel -> tunnel.id }) { tunnel ->
|
items(uiState.tunnels, key = { tunnel -> tunnel.id }) { tunnel ->
|
||||||
val leadingIconColor = (
|
val leadingIconColor = (
|
||||||
if (tunnelName == tunnel.name) {
|
if (uiState.vpnState.name == tunnel.name && uiState.vpnState.status == Tunnel.State.UP) {
|
||||||
when (handshakeStatus) {
|
uiState.vpnState.statistics?.mapPeerStats()?.map {
|
||||||
HandshakeStatus.HEALTHY -> mint
|
it.value?.handshakeStatus()
|
||||||
HandshakeStatus.UNHEALTHY -> brickRed
|
}.let {
|
||||||
HandshakeStatus.STALE -> corn
|
when {
|
||||||
HandshakeStatus.NOT_STARTED -> Color.Gray
|
it?.all { it == HandshakeStatus.HEALTHY } == true -> mint
|
||||||
HandshakeStatus.NEVER_CONNECTED -> brickRed
|
it?.any { it == HandshakeStatus.STALE } == true -> corn
|
||||||
|
it?.all { it == HandshakeStatus.NOT_STARTED } == true -> Color.Gray
|
||||||
|
else -> {
|
||||||
|
Color.Gray
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Color.Gray
|
Color.Gray
|
||||||
}
|
})
|
||||||
)
|
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
val expanded =
|
val expanded =
|
||||||
remember {
|
remember {
|
||||||
|
@ -427,7 +406,7 @@ fun MainScreen(
|
||||||
}
|
}
|
||||||
RowListItem(
|
RowListItem(
|
||||||
icon = {
|
icon = {
|
||||||
if (settings.isTunnelConfigDefault(tunnel)) {
|
if (uiState.settings.isTunnelConfigDefault(tunnel)) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Rounded.Star,
|
Icons.Rounded.Star,
|
||||||
stringResource(R.string.status),
|
stringResource(R.string.status),
|
||||||
|
@ -451,10 +430,9 @@ fun MainScreen(
|
||||||
},
|
},
|
||||||
text = tunnel.name,
|
text = tunnel.name,
|
||||||
onHold = {
|
onHold = {
|
||||||
if ((state == Tunnel.State.UP) && (tunnel.name == tunnelName)) {
|
if ((uiState.vpnState.status == Tunnel.State.UP) && (tunnel.name == uiState.vpnState.name)) {
|
||||||
showSnackbarMessage(
|
showSnackbarMessage(
|
||||||
context.resources.getString(R.string.turn_off_tunnel)
|
R.string.turn_off_tunnel)
|
||||||
)
|
|
||||||
return@RowListItem
|
return@RowListItem
|
||||||
}
|
}
|
||||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
|
@ -462,7 +440,7 @@ fun MainScreen(
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
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
|
expanded.value = !expanded.value
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -470,7 +448,7 @@ fun MainScreen(
|
||||||
focusRequester.requestFocus()
|
focusRequester.requestFocus()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
statistics = statistics,
|
statistics = uiState.vpnState.statistics,
|
||||||
expanded = expanded.value,
|
expanded = expanded.value,
|
||||||
rowButton = {
|
rowButton = {
|
||||||
if (tunnel.id == selectedTunnel?.id && !WireGuardAutoTunnel.isRunningOnAndroidTv(
|
if (tunnel.id == selectedTunnel?.id && !WireGuardAutoTunnel.isRunningOnAndroidTv(
|
||||||
|
@ -478,14 +456,11 @@ fun MainScreen(
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Row {
|
Row {
|
||||||
if (!settings.isTunnelConfigDefault(tunnel)) {
|
if (!uiState.settings.isTunnelConfigDefault(tunnel)) {
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
if (settings.isAutoTunnelEnabled) {
|
if (uiState.settings.isAutoTunnelEnabled) {
|
||||||
showSnackbarMessage(
|
showSnackbarMessage(
|
||||||
context.resources.getString(
|
R.string.turn_off_auto)
|
||||||
R.string.turn_off_auto
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
showPrimaryChangeAlertDialog = true
|
showPrimaryChangeAlertDialog = true
|
||||||
}
|
}
|
||||||
|
@ -514,7 +489,7 @@ fun MainScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} 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
|
if (!checked) expanded.value = false
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -529,14 +504,11 @@ fun MainScreen(
|
||||||
)
|
)
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||||
Row {
|
Row {
|
||||||
if (!settings.isTunnelConfigDefault(tunnel)) {
|
if (!uiState.settings.isTunnelConfigDefault(tunnel)) {
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
if (settings.isAutoTunnelEnabled) {
|
if (uiState.settings.isAutoTunnelEnabled) {
|
||||||
showSnackbarMessage(
|
showSnackbarMessage(
|
||||||
context.resources.getString(
|
R.string.turn_off_auto)
|
||||||
R.string.turn_off_auto
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
showPrimaryChangeAlertDialog = true
|
showPrimaryChangeAlertDialog = true
|
||||||
}
|
}
|
||||||
|
@ -550,7 +522,7 @@ fun MainScreen(
|
||||||
IconButton(
|
IconButton(
|
||||||
modifier = Modifier.focusRequester(focusRequester),
|
modifier = Modifier.focusRequester(focusRequester),
|
||||||
onClick = {
|
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
|
expanded.value = !expanded.value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -558,12 +530,9 @@ fun MainScreen(
|
||||||
Icon(Icons.Rounded.Info, stringResource(R.string.info))
|
Icon(Icons.Rounded.Info, stringResource(R.string.info))
|
||||||
}
|
}
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
if (state == Tunnel.State.UP && tunnel.name == tunnelName) {
|
if (uiState.vpnState.status == Tunnel.State.UP && tunnel.name == uiState.vpnState.name) {
|
||||||
showSnackbarMessage(
|
showSnackbarMessage(
|
||||||
context.resources.getString(
|
R.string.turn_off_tunnel)
|
||||||
R.string.turn_off_tunnel
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
navController.navigate(
|
navController.navigate(
|
||||||
"${Routes.Config.name}/${tunnel.id}"
|
"${Routes.Config.name}/${tunnel.id}"
|
||||||
|
@ -576,12 +545,9 @@ fun MainScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
if (state == Tunnel.State.UP && tunnel.name == tunnelName) {
|
if (uiState.vpnState.status == Tunnel.State.UP && tunnel.name == uiState.vpnState.name) {
|
||||||
showSnackbarMessage(
|
showSnackbarMessage(
|
||||||
context.resources.getString(
|
R.string.turn_off_tunnel)
|
||||||
R.string.turn_off_tunnel
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
viewModel.onDelete(tunnel)
|
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.wireguard.config.Config
|
||||||
import com.zaneschepke.wireguardautotunnel.Constants
|
import com.zaneschepke.wireguardautotunnel.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
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.Settings
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
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.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceState
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceState
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService
|
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
|
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Error
|
||||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||||
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
|
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
@ -29,8 +29,9 @@ import javax.inject.Inject
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
@ -39,27 +40,24 @@ class MainViewModel
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
private val application: Application,
|
private val application: Application,
|
||||||
private val tunnelRepo: TunnelConfigDao,
|
private val tunnelConfigRepository: TunnelConfigRepository,
|
||||||
private val settingsRepository: SettingsRepository,
|
private val settingsRepository: SettingsRepository,
|
||||||
private val vpnService: VpnService
|
private val vpnService: VpnService
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
val tunnels get() = tunnelRepo.getAllFlow()
|
|
||||||
val state get() = vpnService.state
|
|
||||||
|
|
||||||
val handshakeStatus get() = vpnService.handshakeStatus
|
private val _errorState = MutableStateFlow(Error.NONE)
|
||||||
val tunnelName get() = vpnService.tunnelName
|
|
||||||
private val _settings = MutableStateFlow(Settings())
|
|
||||||
val settings get() = _settings.asStateFlow()
|
|
||||||
val statistics get() = vpnService.statistics
|
|
||||||
|
|
||||||
init {
|
val uiState = combine(
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
settingsRepository.getSettingsFlow(),
|
||||||
settingsRepository.getSettings().collect {
|
tunnelConfigRepository.getTunnelConfigsFlow(),
|
||||||
validateWatcherServiceState(it)
|
vpnService.vpnState,
|
||||||
_settings.emit(it)
|
_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) {
|
private fun validateWatcherServiceState(settings: Settings) {
|
||||||
val watcherState =
|
val watcherState =
|
||||||
|
@ -77,7 +75,7 @@ constructor(
|
||||||
|
|
||||||
fun onDelete(tunnel: TunnelConfig) {
|
fun onDelete(tunnel: TunnelConfig) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
if (tunnelRepo.count() == 1L) {
|
if (tunnelConfigRepository.count() == 1) {
|
||||||
ServiceManager.stopWatcherService(application.applicationContext)
|
ServiceManager.stopWatcherService(application.applicationContext)
|
||||||
val settings = settingsRepository.getAll()
|
val settings = settingsRepository.getAll()
|
||||||
if (settings.isNotEmpty()) {
|
if (settings.isNotEmpty()) {
|
||||||
|
@ -88,22 +86,20 @@ constructor(
|
||||||
saveSettings(setting)
|
saveSettings(setting)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tunnelRepo.delete(tunnel)
|
tunnelConfigRepository.delete(tunnel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onTunnelStart(tunnelConfig: TunnelConfig) {
|
fun onTunnelStart(tunnelConfig: TunnelConfig) = viewModelScope.launch {
|
||||||
viewModelScope.launch {
|
|
||||||
stopActiveTunnel()
|
stopActiveTunnel()
|
||||||
startTunnel(tunnelConfig)
|
startTunnel(tunnelConfig)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun startTunnel(tunnelConfig: TunnelConfig) {
|
private fun startTunnel(tunnelConfig: TunnelConfig) {
|
||||||
ServiceManager.startVpnService(application.applicationContext, tunnelConfig.toString())
|
ServiceManager.startVpnService(application.applicationContext, tunnelConfig.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun stopActiveTunnel() {
|
private fun stopActiveTunnel() = viewModelScope.launch {
|
||||||
if (ServiceManager.getServiceState(
|
if (ServiceManager.getServiceState(
|
||||||
application.applicationContext,
|
application.applicationContext,
|
||||||
WireGuardTunnelService::class.java
|
WireGuardTunnelService::class.java
|
||||||
|
@ -122,14 +118,14 @@ constructor(
|
||||||
TunnelConfig.configFromQuick(config)
|
TunnelConfig.configFromQuick(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun onTunnelQrResult(result: String) {
|
fun onTunnelQrResult(result: String) = viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
validateConfigString(result)
|
validateConfigString(result)
|
||||||
val tunnelConfig =
|
val tunnelConfig =
|
||||||
TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = result)
|
TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = result)
|
||||||
addTunnel(tunnelConfig)
|
addTunnel(tunnelConfig)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw WgTunnelException(e)
|
emitErrorEvent(Error.INVALID_QR)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,19 +147,16 @@ constructor(
|
||||||
?: throw WgTunnelException(application.getString(R.string.stream_failed))
|
?: throw WgTunnelException(application.getString(R.string.stream_failed))
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun onTunnelFileSelected(uri: Uri) {
|
fun onTunnelFileSelected(uri: Uri) = viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
val fileName = getFileName(application.applicationContext, uri)
|
val fileName = getFileName(application.applicationContext, uri)
|
||||||
val fileExtension = getFileExtensionFromFileName(fileName)
|
when (getFileExtensionFromFileName(fileName)) {
|
||||||
when (fileExtension) {
|
|
||||||
Constants.CONF_FILE_EXTENSION -> saveTunnelFromConfUri(fileName, uri)
|
Constants.CONF_FILE_EXTENSION -> saveTunnelFromConfUri(fileName, uri)
|
||||||
Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri)
|
Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri)
|
||||||
else -> throw WgTunnelException(
|
else -> emitErrorEvent(Error.FILE_EXTENSION)
|
||||||
application.getString(R.string.file_extension_message)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw WgTunnelException(e)
|
emitErrorEvent(Error.FILE_EXTENSION)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -197,7 +190,7 @@ constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun saveTunnel(tunnelConfig: TunnelConfig) {
|
private suspend fun saveTunnel(tunnelConfig: TunnelConfig) {
|
||||||
tunnelRepo.save(tunnelConfig)
|
tunnelConfigRepository.save(tunnelConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getFileNameByCursor(
|
private fun getFileNameByCursor(
|
||||||
|
@ -233,13 +226,16 @@ constructor(
|
||||||
|
|
||||||
private fun validateUriContentScheme(uri: Uri) {
|
private fun validateUriContentScheme(uri: Uri) {
|
||||||
if (uri.scheme != Constants.URI_CONTENT_SCHEME) {
|
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) {
|
fun emitErrorEventConsumed() {
|
||||||
//TODO handle error if fails
|
_errorState.tryEmit(Error.NONE)
|
||||||
settingsRepository.save(settings)
|
}
|
||||||
|
|
||||||
|
private fun emitErrorEvent(error : Error) {
|
||||||
|
_errorState.tryEmit(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getFileName(
|
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) {
|
if (selectedTunnel != null) {
|
||||||
_settings.emit(
|
saveSettings(
|
||||||
_settings.value.copy(
|
uiState.value.settings.copy(
|
||||||
defaultTunnel = selectedTunnel.toString()
|
defaultTunnel = selectedTunnel.toString()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
settingsRepository.save(_settings.value)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import android.os.Build
|
||||||
import android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS
|
import android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.focusable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
@ -38,6 +39,7 @@ import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.SideEffect
|
import androidx.compose.runtime.SideEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
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.config.ConfigurationToggle
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
|
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
|
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Error
|
||||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@ -88,7 +91,7 @@ import kotlinx.coroutines.launch
|
||||||
fun SettingsScreen(
|
fun SettingsScreen(
|
||||||
viewModel: SettingsViewModel = hiltViewModel(),
|
viewModel: SettingsViewModel = hiltViewModel(),
|
||||||
padding: PaddingValues,
|
padding: PaddingValues,
|
||||||
showSnackbarMessage: (String) -> Unit,
|
showSnackbarMessage: (Int) -> Unit,
|
||||||
focusRequester: FocusRequester
|
focusRequester: FocusRequester
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope { Dispatchers.IO }
|
val scope = rememberCoroutineScope { Dispatchers.IO }
|
||||||
|
@ -109,8 +112,49 @@ fun SettingsScreen(
|
||||||
val screenPadding = 5.dp
|
val screenPadding = 5.dp
|
||||||
val fillMaxWidth = .85f
|
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() {
|
fun exportAllConfigs() {
|
||||||
try {
|
try {
|
||||||
|
@ -122,22 +166,16 @@ fun SettingsScreen(
|
||||||
}
|
}
|
||||||
FileUtils.saveFilesToZip(context, files)
|
FileUtils.saveFilesToZip(context, files)
|
||||||
didExportFiles = true
|
didExportFiles = true
|
||||||
showSnackbarMessage(context.getString(R.string.exported_configs_message))
|
showSnackbarMessage(R.string.exported_configs_message)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
showSnackbarMessage(e.message!!)
|
showSnackbarMessage(Error.GENERAL.getMessage())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveTrustedSSID() {
|
fun saveTrustedSSID() {
|
||||||
if (currentText.isNotEmpty()) {
|
if (currentText.isNotEmpty()) {
|
||||||
scope.launch {
|
|
||||||
try {
|
|
||||||
viewModel.onSaveTrustedSSID(currentText)
|
viewModel.onSaveTrustedSSID(currentText)
|
||||||
currentText = ""
|
currentText = ""
|
||||||
} catch (e: Exception) {
|
|
||||||
showSnackbarMessage(e.message ?: context.getString(R.string.unknown_error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -163,9 +201,7 @@ fun SettingsScreen(
|
||||||
isBackgroundLocationGranted = if (!fineLocationState.status.isGranted) {
|
isBackgroundLocationGranted = if (!fineLocationState.status.isGranted) {
|
||||||
false
|
false
|
||||||
} else {
|
} else {
|
||||||
scope.launch {
|
|
||||||
viewModel.setLocationDisclosureShown()
|
viewModel.setLocationDisclosureShown()
|
||||||
}
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -194,7 +230,7 @@ fun SettingsScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AnimatedVisibility(!uiState.isLocationDisclosureShown) {
|
AnimatedVisibility(!uiState.isLocationDisclosureShown && !uiState.loading) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
|
@ -260,17 +296,18 @@ fun SettingsScreen(
|
||||||
exportAllConfigs()
|
exportAllConfigs()
|
||||||
},
|
},
|
||||||
onError = { error ->
|
onError = { error ->
|
||||||
showSnackbarMessage(error)
|
showAuthPrompt = false
|
||||||
|
showSnackbarMessage(R.string.error_authentication_failed)
|
||||||
showAuthPrompt = false
|
showAuthPrompt = false
|
||||||
},
|
},
|
||||||
onFailure = {
|
onFailure = {
|
||||||
showAuthPrompt = false
|
showAuthPrompt = false
|
||||||
showSnackbarMessage(context.getString(R.string.authentication_failed))
|
showSnackbarMessage(R.string.error_authentication_failed)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (uiState.tunnels.isEmpty()) {
|
if (uiState.tunnels.isEmpty() && !uiState.loading && uiState.isLocationDisclosureShown) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
|
@ -286,9 +323,8 @@ fun SettingsScreen(
|
||||||
fontStyle = FontStyle.Italic
|
fontStyle = FontStyle.Italic
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
if (!uiState.loading && uiState.isLocationDisclosureShown && uiState.tunnels.isNotEmpty()) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
|
@ -453,11 +489,11 @@ fun SettingsScreen(
|
||||||
if (!isAllAutoTunnelPermissionsEnabled() && uiState.settings.isTunnelOnWifiEnabled) {
|
if (!isAllAutoTunnelPermissionsEnabled() && uiState.settings.isTunnelOnWifiEnabled) {
|
||||||
val message =
|
val message =
|
||||||
if (!isBackgroundLocationGranted) {
|
if (!isBackgroundLocationGranted) {
|
||||||
context.getString(R.string.background_location_required)
|
R.string.background_location_required
|
||||||
} else if (viewModel.isLocationServicesNeeded()) {
|
} else if (viewModel.isLocationServicesNeeded()) {
|
||||||
context.getString(R.string.location_services_required)
|
R.string.location_services_required
|
||||||
} else {
|
} else {
|
||||||
context.getString(R.string.precise_location_required)
|
R.string.precise_location_required
|
||||||
}
|
}
|
||||||
showSnackbarMessage(message)
|
showSnackbarMessage(message)
|
||||||
} else {
|
} else {
|
||||||
|
@ -499,7 +535,7 @@ fun SettingsScreen(
|
||||||
stringResource(R.string.use_kernel),
|
stringResource(R.string.use_kernel),
|
||||||
enabled = !(
|
enabled = !(
|
||||||
uiState.settings.isAutoTunnelEnabled || uiState.settings.isAlwaysOnVpnEnabled ||
|
uiState.settings.isAutoTunnelEnabled || uiState.settings.isAlwaysOnVpnEnabled ||
|
||||||
(uiState.tunnelState == Tunnel.State.UP)
|
(uiState.vpnState.status == Tunnel.State.UP)
|
||||||
),
|
),
|
||||||
checked = uiState.settings.isKernelEnabled,
|
checked = uiState.settings.isKernelEnabled,
|
||||||
padding = screenPadding,
|
padding = screenPadding,
|
||||||
|
@ -536,10 +572,8 @@ fun SettingsScreen(
|
||||||
checked = uiState.settings.isAlwaysOnVpnEnabled,
|
checked = uiState.settings.isAlwaysOnVpnEnabled,
|
||||||
padding = screenPadding,
|
padding = screenPadding,
|
||||||
onCheckChanged = {
|
onCheckChanged = {
|
||||||
scope.launch {
|
|
||||||
viewModel.onToggleAlwaysOnVPN()
|
viewModel.onToggleAlwaysOnVPN()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
)
|
)
|
||||||
ConfigurationToggle(
|
ConfigurationToggle(
|
||||||
stringResource(R.string.enabled_app_shortcuts),
|
stringResource(R.string.enabled_app_shortcuts),
|
||||||
|
@ -547,10 +581,8 @@ fun SettingsScreen(
|
||||||
checked = uiState.settings.isShortcutsEnabled,
|
checked = uiState.settings.isShortcutsEnabled,
|
||||||
padding = screenPadding,
|
padding = screenPadding,
|
||||||
onCheckChanged = {
|
onCheckChanged = {
|
||||||
scope.launch {
|
|
||||||
viewModel.onToggleShortcutsEnabled()
|
viewModel.onToggleShortcutsEnabled()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
)
|
)
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
@ -577,3 +609,4 @@ fun SettingsScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.settings
|
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.Settings
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Error
|
||||||
data class SettingsUiState(
|
data class SettingsUiState(
|
||||||
val settings : Settings = Settings(),
|
val settings : Settings = Settings(),
|
||||||
val tunnels : List<TunnelConfig> = emptyList(),
|
val tunnels : List<TunnelConfig> = emptyList(),
|
||||||
val tunnelState : Tunnel.State = Tunnel.State.DOWN,
|
val vpnState: VpnState = VpnState(),
|
||||||
val isLocationDisclosureShown : Boolean = true,
|
val isLocationDisclosureShown : Boolean = true,
|
||||||
val loading : 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.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.wireguard.android.util.RootShell
|
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.datastore.DataStoreManager
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
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.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
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 dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
|
@ -27,22 +29,25 @@ class SettingsViewModel
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
private val application: Application,
|
private val application: Application,
|
||||||
private val tunnelRepo: TunnelConfigDao,
|
private val tunnelConfigRepository: TunnelConfigRepository,
|
||||||
private val settingsRepository: SettingsRepository,
|
private val settingsRepository: SettingsRepository,
|
||||||
private val dataStoreManager: DataStoreManager,
|
private val dataStoreManager: DataStoreManager,
|
||||||
private val rootShell: RootShell,
|
private val rootShell: RootShell,
|
||||||
private val vpnService: VpnService
|
private val vpnService: VpnService
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val _errorState = MutableStateFlow(Error.NONE)
|
||||||
|
|
||||||
val uiState = combine(
|
val uiState = combine(
|
||||||
settingsRepository.getSettings(),
|
settingsRepository.getSettingsFlow(),
|
||||||
tunnelRepo.getAllFlow(),
|
tunnelConfigRepository.getTunnelConfigsFlow(),
|
||||||
vpnService.state,
|
vpnService.vpnState,
|
||||||
dataStoreManager.locationDisclosureFlow,
|
dataStoreManager.locationDisclosureFlow,
|
||||||
){ settings, tunnels, tunnelState, locationDisclosure ->
|
_errorState
|
||||||
SettingsUiState(settings, tunnels, tunnelState, locationDisclosure ?: false, false)
|
){ settings, tunnels, tunnelState, locationDisclosure, errorState ->
|
||||||
|
SettingsUiState(settings, tunnels, tunnelState, locationDisclosure ?: false, false, errorState)
|
||||||
}.stateIn(viewModelScope,
|
}.stateIn(viewModelScope,
|
||||||
SharingStarted.WhileSubscribed(5_000L), SettingsUiState())
|
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT), SettingsUiState())
|
||||||
|
|
||||||
fun onSaveTrustedSSID(ssid: String) {
|
fun onSaveTrustedSSID(ssid: String) {
|
||||||
val trimmed = ssid.trim()
|
val trimmed = ssid.trim()
|
||||||
|
@ -50,10 +55,17 @@ constructor(
|
||||||
uiState.value.settings.trustedNetworkSSIDs.add(trimmed)
|
uiState.value.settings.trustedNetworkSSIDs.add(trimmed)
|
||||||
saveSettings(uiState.value.settings)
|
saveSettings(uiState.value.settings)
|
||||||
} else {
|
} 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 {
|
suspend fun isLocationDisclosureShown() : Boolean {
|
||||||
return dataStoreManager.getFromStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN) ?: false
|
return dataStoreManager.getFromStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN) ?: false
|
||||||
}
|
}
|
||||||
|
@ -76,7 +88,7 @@ constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getDefaultTunnelOrFirst() : String {
|
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 {
|
fun toggleAutoTunnel() = viewModelScope.launch {
|
||||||
|
@ -94,9 +106,8 @@ constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun onToggleAlwaysOnVPN() {
|
fun onToggleAlwaysOnVPN() = viewModelScope.launch {
|
||||||
val updatedSettings =
|
val updatedSettings = uiState.value.settings.copy(
|
||||||
uiState.value.settings.copy(
|
|
||||||
isAlwaysOnVpnEnabled = !uiState.value.settings.isAlwaysOnVpnEnabled,
|
isAlwaysOnVpnEnabled = !uiState.value.settings.isAlwaysOnVpnEnabled,
|
||||||
defaultTunnel = getDefaultTunnelOrFirst()
|
defaultTunnel = getDefaultTunnelOrFirst()
|
||||||
)
|
)
|
||||||
|
@ -163,7 +174,7 @@ constructor(
|
||||||
saveKernelMode(on = true)
|
saveKernelMode(on = true)
|
||||||
} catch (e: RootShell.RootShellException) {
|
} catch (e: RootShell.RootShellException) {
|
||||||
saveKernelMode(on = false)
|
saveKernelMode(on = false)
|
||||||
throw WgTunnelException("Root shell denied!")
|
emitErrorEvent(Error.ROOT_DENIED)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
saveKernelMode(on = false)
|
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="github_url">https://github.com/zaneschepke/wgtunnel/issues</string>
|
||||||
<string name="docs_url">https://zaneschepke.com/wgtunnel-docs/overview.html</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="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="turn_off_tunnel">Turn off tunnel before editing</string>
|
||||||
<string name="no_tunnels">No tunnels added yet!</string>
|
<string name="no_tunnels">No tunnels added yet!</string>
|
||||||
<string name="tunnel_exists">Tunnel name already exists</string>
|
<string name="tunnel_exists">Tunnel name already exists</string>
|
||||||
|
@ -96,8 +96,6 @@
|
||||||
<string name="none">No trusted wifi names</string>
|
<string name="none">No trusted wifi names</string>
|
||||||
<string name="never">Never</string>
|
<string name="never">Never</string>
|
||||||
<string name="stream_failed">Failed to open file stream.</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="other">Other</string>
|
||||||
<string name="auto_tunneling">Auto-tunneling</string>
|
<string name="auto_tunneling">Auto-tunneling</string>
|
||||||
<string name="select_tunnel">Select tunnel to use</string>
|
<string name="select_tunnel">Select tunnel to use</string>
|
||||||
|
@ -128,7 +126,7 @@
|
||||||
<string name="cancel">Cancel</string>
|
<string name="cancel">Cancel</string>
|
||||||
<string name="primary_tunnel_change">Primary tunnel change</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="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="enabled_app_shortcuts">Enable app shortcuts</string>
|
||||||
<string name="export_configs">Export configs</string>
|
<string name="export_configs">Export configs</string>
|
||||||
<string name="battery_saver">Battery saver (beta)</string>
|
<string name="battery_saver">Battery saver (beta)</string>
|
||||||
|
@ -137,7 +135,6 @@
|
||||||
<string name="precise_location_required">Precise location required</string>
|
<string name="precise_location_required">Precise location required</string>
|
||||||
<string name="unknown_error">Unknown error occurred</string>
|
<string name="unknown_error">Unknown error occurred</string>
|
||||||
<string name="exported_configs_message">Exported configs to downloads</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="status">status</string>
|
||||||
<string name="tunnel_on_wifi">Tunnel on untrusted wifi</string>
|
<string name="tunnel_on_wifi">Tunnel on untrusted wifi</string>
|
||||||
<string name="my_email">zanecschepke@gmail.com</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="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="kernel">Kernel</string>
|
||||||
<string name="use_kernel">Use kernel module</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>
|
</resources>
|
|
@ -1,7 +1,7 @@
|
||||||
object Constants {
|
object Constants {
|
||||||
const val VERSION_NAME = "3.2.5"
|
const val VERSION_NAME = "3.2.6"
|
||||||
const val JVM_TARGET = "17"
|
const val JVM_TARGET = "17"
|
||||||
const val VERSION_CODE = 32500
|
const val VERSION_CODE = 32600
|
||||||
const val TARGET_SDK = 34
|
const val TARGET_SDK = 34
|
||||||
const val MIN_SDK = 26
|
const val MIN_SDK = 26
|
||||||
const val APP_ID = "com.zaneschepke.wireguardautotunnel"
|
const val APP_ID = "com.zaneschepke.wireguardautotunnel"
|
||||||
|
|
Loading…
Reference in New Issue