mirror of
https://github.com/wgtunnel/android.git
synced 2026-07-03 14:07:49 +02:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c8634c71a3 | |||
| 3894efa066 | |||
| 3c60913917 |
@@ -76,7 +76,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Set up JDK 17
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@v5
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
@@ -118,11 +118,6 @@ jobs:
|
|||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: android_artifacts_${{ inputs.flavor }}
|
name: android_artifacts_${{ inputs.flavor }}
|
||||||
path: >-
|
path: app/build/outputs/apk/${{ inputs.flavor }}/${{ inputs.build_type }}/wgtunnel-${{ inputs.flavor }}${{ inputs.flavor == 'fdroid' && '-release' || '' }}-*.apk
|
||||||
app/build/outputs/apk/${{ inputs.flavor }}/${{ inputs.build_type }}/${{
|
|
||||||
inputs.flavor == 'fdroid' && inputs.build_type == 'release'
|
|
||||||
&& 'wgtunnel-fdroid-release-*.apk'
|
|
||||||
|| format('wgtunnel-{0}-v*.apk', inputs.flavor)
|
|
||||||
}}
|
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
if-no-files-found: warn
|
if-no-files-found: warn
|
||||||
@@ -69,7 +69,7 @@ jobs:
|
|||||||
run: mkdir ${{ github.workspace }}/temp
|
run: mkdir ${{ github.workspace }}/temp
|
||||||
|
|
||||||
- name: Download artifacts
|
- name: Download artifacts
|
||||||
uses: actions/download-artifact@v5
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
pattern: android_artifacts_*
|
pattern: android_artifacts_*
|
||||||
path: ${{ github.workspace }}/temp
|
path: ${{ github.workspace }}/temp
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Set up JDK 17
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@v5
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ jobs:
|
|||||||
run: mkdir ${{ github.workspace }}/temp
|
run: mkdir ${{ github.workspace }}/temp
|
||||||
|
|
||||||
- name: Download artifacts
|
- name: Download artifacts
|
||||||
uses: actions/download-artifact@v5
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
pattern: android_artifacts_*
|
pattern: android_artifacts_*
|
||||||
path: ${{ github.workspace }}/temp
|
path: ${{ github.workspace }}/temp
|
||||||
@@ -191,7 +191,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Set up JDK 17
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@v5
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
|
|||||||
@@ -47,10 +47,6 @@
|
|||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent>
|
</intent>
|
||||||
<intent>
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
|
||||||
</intent>
|
|
||||||
</queries>
|
</queries>
|
||||||
<application
|
<application
|
||||||
android:name=".WireGuardAutoTunnel"
|
android:name=".WireGuardAutoTunnel"
|
||||||
|
|||||||
@@ -301,7 +301,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
val args = backStack.toRoute<Route.Config>()
|
val args = backStack.toRoute<Route.Config>()
|
||||||
val config =
|
val config =
|
||||||
appUiState.tunnels.firstOrNull { it.id == args.id }
|
appUiState.tunnels.firstOrNull { it.id == args.id }
|
||||||
ConfigScreen(config, appUiState, viewModel)
|
ConfigScreen(config, viewModel)
|
||||||
}
|
}
|
||||||
composable<Route.TunnelOptions> { backStack ->
|
composable<Route.TunnelOptions> { backStack ->
|
||||||
val args = backStack.toRoute<Route.TunnelOptions>()
|
val args = backStack.toRoute<Route.TunnelOptions>()
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import com.zaneschepke.wireguardautotunnel.core.worker.ServiceWorker
|
|||||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||||
import com.zaneschepke.wireguardautotunnel.di.MainDispatcher
|
import com.zaneschepke.wireguardautotunnel.di.MainDispatcher
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendStatus
|
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
|
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
|
||||||
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
|
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
|
||||||
@@ -91,7 +91,7 @@ class WireGuardAutoTunnel : Application(), Configuration.Provider {
|
|||||||
|
|
||||||
override fun onTerminate() {
|
override fun onTerminate() {
|
||||||
applicationScope.cancel()
|
applicationScope.cancel()
|
||||||
tunnelManager.setBackendStatus(BackendStatus.Inactive)
|
tunnelManager.setBackendState(BackendState.INACTIVE, emptyList())
|
||||||
super.onTerminate()
|
super.onTerminate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+5
-5
@@ -16,7 +16,7 @@ import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
|||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelMonitor
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelMonitor
|
||||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendStatus
|
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus.StopReason.Ping
|
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus.StopReason.Ping
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
|
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
|
||||||
@@ -109,13 +109,13 @@ class AutoTunnelService : LifecycleService() {
|
|||||||
with(autoTunnelStateFlow.value) {
|
with(autoTunnelStateFlow.value) {
|
||||||
if (
|
if (
|
||||||
settings.isVpnKillSwitchEnabled &&
|
settings.isVpnKillSwitchEnabled &&
|
||||||
tunnelManager.getBackendStatus() !is BackendStatus.KillSwitch
|
tunnelManager.getBackendState() != BackendState.KILL_SWITCH_ACTIVE
|
||||||
) {
|
) {
|
||||||
eventHandlerJob?.cancel()
|
eventHandlerJob?.cancel()
|
||||||
val allowedIps =
|
val allowedIps =
|
||||||
if (settings.isLanOnKillSwitchEnabled) TunnelConf.LAN_BYPASS_ALLOWED_IPS
|
if (settings.isLanOnKillSwitchEnabled) TunnelConf.LAN_BYPASS_ALLOWED_IPS
|
||||||
else emptyList()
|
else emptyList()
|
||||||
tunnelManager.setBackendStatus(BackendStatus.KillSwitch(allowedIps))
|
tunnelManager.setBackendState(BackendState.KILL_SWITCH_ACTIVE, allowedIps)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -402,11 +402,11 @@ class AutoTunnelService : LifecycleService() {
|
|||||||
handleBounceWithBackoff(event.configsPeerKeyResolvedMap)
|
handleBounceWithBackoff(event.configsPeerKeyResolvedMap)
|
||||||
is AutoTunnelEvent.StartKillSwitch -> {
|
is AutoTunnelEvent.StartKillSwitch -> {
|
||||||
Timber.d("Starting kill switch")
|
Timber.d("Starting kill switch")
|
||||||
tunnelManager.setBackendStatus(BackendStatus.KillSwitch(event.allowedIps))
|
tunnelManager.setBackendState(BackendState.KILL_SWITCH_ACTIVE, event.allowedIps)
|
||||||
}
|
}
|
||||||
AutoTunnelEvent.StopKillSwitch -> {
|
AutoTunnelEvent.StopKillSwitch -> {
|
||||||
Timber.d("Stopping kill switch")
|
Timber.d("Stopping kill switch")
|
||||||
tunnelManager.setBackendStatus(BackendStatus.Active)
|
tunnelManager.setBackendState(BackendState.SERVICE_ACTIVE, emptySet())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import com.wireguard.android.backend.BackendException
|
|||||||
import com.wireguard.android.backend.Tunnel
|
import com.wireguard.android.backend.Tunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendStatus
|
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.events.BackendError
|
import com.zaneschepke.wireguardautotunnel.domain.events.BackendError
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
||||||
@@ -59,12 +59,12 @@ constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setBackendStatus(backendStatus: BackendStatus) {
|
override fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
|
||||||
Timber.w("Not yet implemented for kernel")
|
Timber.w("Not yet implemented for kernel")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getBackendStatus(): BackendStatus {
|
override fun getBackendState(): BackendState {
|
||||||
return BackendStatus.Inactive
|
return BackendState.INACTIVE
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun runningTunnelNames(): Set<String> {
|
override suspend fun runningTunnelNames(): Set<String> {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendStatus
|
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.events.BackendError
|
import com.zaneschepke.wireguardautotunnel.domain.events.BackendError
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage
|
import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage
|
||||||
@@ -34,8 +34,7 @@ constructor(
|
|||||||
appDataRepository.settings.flow
|
appDataRepository.settings.flow
|
||||||
.filterNotNull()
|
.filterNotNull()
|
||||||
.flatMapLatest { settings ->
|
.flatMapLatest { settings ->
|
||||||
val backend = if (settings.isKernelEnabled) kernelTunnel else userspaceTunnel
|
MutableStateFlow(if (settings.isKernelEnabled) kernelTunnel else userspaceTunnel)
|
||||||
MutableStateFlow(backend)
|
|
||||||
}
|
}
|
||||||
.stateIn(
|
.stateIn(
|
||||||
scope = applicationScope.plus(ioDispatcher),
|
scope = applicationScope.plus(ioDispatcher),
|
||||||
@@ -90,12 +89,12 @@ constructor(
|
|||||||
tunnelProviderFlow.value.bounceTunnel(tunnelConf, reason)
|
tunnelProviderFlow.value.bounceTunnel(tunnelConf, reason)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setBackendStatus(backendStatus: BackendStatus) {
|
override fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
|
||||||
tunnelProviderFlow.value.setBackendStatus(backendStatus)
|
tunnelProviderFlow.value.setBackendState(backendState, allowedIps)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getBackendStatus(): BackendStatus {
|
override fun getBackendState(): BackendState {
|
||||||
return tunnelProviderFlow.value.getBackendStatus()
|
return tunnelProviderFlow.value.getBackendState()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun runningTunnelNames(): Set<String> {
|
override suspend fun runningTunnelNames(): Set<String> {
|
||||||
|
|||||||
+11
-14
@@ -138,22 +138,19 @@ constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val host =
|
val host =
|
||||||
tunnelConf.pingTarget
|
{
|
||||||
?: {
|
val parts = allowedIpStr.split("/")
|
||||||
val parts = allowedIpStr.split("/")
|
val internalIp = if (parts.size == 2) parts[0] else allowedIpStr
|
||||||
val internalIp =
|
|
||||||
if (parts.size == 2) parts[0] else allowedIpStr
|
|
||||||
|
|
||||||
val prefix =
|
val prefix =
|
||||||
if (parts.size == 2) parts[1].toIntOrNull() ?: 32
|
if (parts.size == 2) parts[1].toIntOrNull() ?: 32 else 32
|
||||||
else 32
|
if (prefix <= 1) {
|
||||||
if (prefix <= 1) {
|
tunnelConf.pingTarget ?: CLOUDFLARE_IPV4_IP
|
||||||
CLOUDFLARE_IPV4_IP
|
} else {
|
||||||
} else {
|
internalIp.removeSurrounding("[", "]")
|
||||||
internalIp.removeSurrounding("[", "]")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.invoke()
|
}
|
||||||
|
.invoke()
|
||||||
|
|
||||||
val attemptTime = System.currentTimeMillis()
|
val attemptTime = System.currentTimeMillis()
|
||||||
runCatching {
|
runCatching {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendStatus
|
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.events.BackendError
|
import com.zaneschepke.wireguardautotunnel.domain.events.BackendError
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage
|
import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage
|
||||||
@@ -41,9 +41,9 @@ interface TunnelProvider {
|
|||||||
reason: TunnelStatus.StopReason = TunnelStatus.StopReason.User,
|
reason: TunnelStatus.StopReason = TunnelStatus.StopReason.User,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun setBackendStatus(backendStatus: BackendStatus)
|
fun setBackendState(backendState: BackendState, allowedIps: Collection<String>)
|
||||||
|
|
||||||
fun getBackendStatus(): BackendStatus
|
fun getBackendState(): BackendState
|
||||||
|
|
||||||
suspend fun runningTunnelNames(): Set<String>
|
suspend fun runningTunnelNames(): Set<String>
|
||||||
|
|
||||||
|
|||||||
+39
-18
@@ -1,15 +1,15 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendStatus
|
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.events.BackendError
|
import com.zaneschepke.wireguardautotunnel.domain.events.BackendError
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.AmneziaStatistics
|
import com.zaneschepke.wireguardautotunnel.domain.state.AmneziaStatistics
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asAmBackendStatus
|
import com.zaneschepke.wireguardautotunnel.util.extensions.asAmBackendState
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asBackendStatus
|
import com.zaneschepke.wireguardautotunnel.util.extensions.asBackendState
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendError
|
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendError
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.jvm.optionals.getOrNull
|
import kotlin.jvm.optionals.getOrNull
|
||||||
@@ -17,6 +17,7 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import org.amnezia.awg.backend.Backend
|
import org.amnezia.awg.backend.Backend
|
||||||
import org.amnezia.awg.backend.BackendException
|
import org.amnezia.awg.backend.BackendException
|
||||||
import org.amnezia.awg.backend.Tunnel
|
import org.amnezia.awg.backend.Tunnel
|
||||||
|
import org.amnezia.awg.config.Config
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
class UserspaceTunnel
|
class UserspaceTunnel
|
||||||
@@ -28,21 +29,14 @@ constructor(
|
|||||||
private val backend: Backend,
|
private val backend: Backend,
|
||||||
) : BaseTunnel(applicationScope, appDataRepository, serviceManager) {
|
) : BaseTunnel(applicationScope, appDataRepository, serviceManager) {
|
||||||
|
|
||||||
|
private var previousBackendState: Pair<BackendState, Boolean>? = null
|
||||||
|
|
||||||
override suspend fun startBackend(tunnel: TunnelConf) {
|
override suspend fun startBackend(tunnel: TunnelConf) {
|
||||||
try {
|
try {
|
||||||
updateTunnelStatus(tunnel, TunnelStatus.Starting)
|
updateTunnelStatus(tunnel, TunnelStatus.Starting)
|
||||||
val amConfig = tunnel.toAmConfig()
|
val amConfig = tunnel.toAmConfig()
|
||||||
var previousKillSwitch: Backend.BackendStatus? = null
|
handleVpnKillSwitchWithDomainEndpoints(amConfig)
|
||||||
// prevent dns failures from bringing tuns up when vpn kill switch active
|
|
||||||
if (
|
|
||||||
amConfig.peers.any { it.endpoint.getOrNull()?.toString()?.isUrl() == true } &&
|
|
||||||
backend.backendStatus is Backend.BackendStatus.KillSwitchActive
|
|
||||||
) {
|
|
||||||
previousKillSwitch = backend.backendStatus
|
|
||||||
setBackendStatus(BackendStatus.Active)
|
|
||||||
}
|
|
||||||
backend.setState(tunnel, Tunnel.State.UP, amConfig)
|
backend.setState(tunnel, Tunnel.State.UP, amConfig)
|
||||||
previousKillSwitch?.let { backend.backendStatus = it }
|
|
||||||
} catch (e: BackendException) {
|
} catch (e: BackendException) {
|
||||||
Timber.e(e, "Failed to start up backend for tunnel ${tunnel.name}")
|
Timber.e(e, "Failed to start up backend for tunnel ${tunnel.name}")
|
||||||
throw e.toBackendError()
|
throw e.toBackendError()
|
||||||
@@ -59,20 +53,47 @@ constructor(
|
|||||||
} catch (e: BackendException) {
|
} catch (e: BackendException) {
|
||||||
Timber.e(e, "Failed to stop tunnel ${tunnel.id}")
|
Timber.e(e, "Failed to stop tunnel ${tunnel.id}")
|
||||||
throw e.toBackendError()
|
throw e.toBackendError()
|
||||||
|
} finally {
|
||||||
|
handlePreviouslyEnabledVpnKillSwitch()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setBackendStatus(backendStatus: BackendStatus) {
|
// stop vpn kill switch if we need to resolve DNS for peer endpoints
|
||||||
Timber.d("Setting backend state: $backendStatus")
|
private suspend fun handleVpnKillSwitchWithDomainEndpoints(config: Config) {
|
||||||
|
if (
|
||||||
|
config.peers.any { it.endpoint.getOrNull()?.toString()?.isUrl() == true } &&
|
||||||
|
backend.backendState.asBackendState() == BackendState.KILL_SWITCH_ACTIVE
|
||||||
|
) {
|
||||||
|
val bypassLan = appDataRepository.settings.get().isLanOnKillSwitchEnabled
|
||||||
|
previousBackendState = Pair(BackendState.KILL_SWITCH_ACTIVE, bypassLan)
|
||||||
|
setBackendState(BackendState.SERVICE_ACTIVE, emptyList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// restore vpn kill switch if needed
|
||||||
|
private fun handlePreviouslyEnabledVpnKillSwitch() {
|
||||||
|
// let auto tunnel handle this if it is active
|
||||||
|
if (serviceManager.autoTunnelService.value == null) {
|
||||||
|
previousBackendState?.let { (state, lanEnabled) ->
|
||||||
|
Timber.d("Restoring kill switch configuration")
|
||||||
|
val lan = if (lanEnabled) TunnelConf.LAN_BYPASS_ALLOWED_IPS else emptyList()
|
||||||
|
backend.setBackendState(state.asAmBackendState(), lan)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
previousBackendState = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
|
||||||
|
Timber.d("Setting backend state: $backendState with allowedIps: $allowedIps")
|
||||||
try {
|
try {
|
||||||
backend.backendStatus = backendStatus.asAmBackendStatus()
|
backend.setBackendState(backendState.asAmBackendState(), allowedIps)
|
||||||
} catch (e: BackendException) {
|
} catch (e: BackendException) {
|
||||||
throw e.toBackendError()
|
throw e.toBackendError()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getBackendStatus(): BackendStatus {
|
override fun getBackendState(): BackendState {
|
||||||
return backend.backendStatus.asBackendStatus()
|
return backend.backendState.asBackendState()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun runningTunnelNames(): Set<String> {
|
override suspend fun runningTunnelNames(): Set<String> {
|
||||||
|
|||||||
+5
-7
@@ -38,13 +38,13 @@ class GitHubUpdateRepository(
|
|||||||
gitHubApi.getLatestRelease(githubOwner, githubRepo).onFailure(Timber::e)
|
gitHubApi.getLatestRelease(githubOwner, githubRepo).onFailure(Timber::e)
|
||||||
}
|
}
|
||||||
release.map { release ->
|
release.map { release ->
|
||||||
val standaloneApkAsset =
|
val apkAsset =
|
||||||
release.assets.find { asset ->
|
release.assets.find { asset ->
|
||||||
asset.name.startsWith("wgtunnel-${Constants.STANDALONE_FLAVOR}-v") &&
|
asset.name.startsWith("wgtunnel-${Constants.STANDALONE_FLAVOR}-v") &&
|
||||||
asset.name.endsWith(".apk")
|
asset.name.endsWith(".apk")
|
||||||
}
|
}
|
||||||
val newVersion =
|
val newVersion =
|
||||||
standaloneApkAsset
|
apkAsset
|
||||||
?.name
|
?.name
|
||||||
?.removePrefix("wgtunnel-${Constants.STANDALONE_FLAVOR}-v")
|
?.removePrefix("wgtunnel-${Constants.STANDALONE_FLAVOR}-v")
|
||||||
?.removeSuffix(".apk") ?: return@map null
|
?.removeSuffix(".apk") ?: return@map null
|
||||||
@@ -53,9 +53,7 @@ class GitHubUpdateRepository(
|
|||||||
if (isNightly && newVersion != currentVersion)
|
if (isNightly && newVersion != currentVersion)
|
||||||
return@map GitHubReleaseMapper.toAppUpdate(release, newVersion)
|
return@map GitHubReleaseMapper.toAppUpdate(release, newVersion)
|
||||||
if (NumberUtils.compareVersions(newVersion, currentVersion) > 0) {
|
if (NumberUtils.compareVersions(newVersion, currentVersion) > 0) {
|
||||||
GitHubReleaseMapper.toAppUpdate(release.copy(
|
GitHubReleaseMapper.toAppUpdate(release, newVersion)
|
||||||
assets = listOf(standaloneApkAsset)
|
|
||||||
), newVersion)
|
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
@@ -65,7 +63,7 @@ class GitHubUpdateRepository(
|
|||||||
override suspend fun downloadApk(
|
override suspend fun downloadApk(
|
||||||
apkUrl: String,
|
apkUrl: String,
|
||||||
fileName: String,
|
fileName: String,
|
||||||
onProgress: suspend (Float) -> Unit,
|
onProgress: (Float) -> Unit,
|
||||||
): Result<File> =
|
): Result<File> =
|
||||||
withContext(ioDispatcher) {
|
withContext(ioDispatcher) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.domain.enums
|
package com.zaneschepke.wireguardautotunnel.domain.enums
|
||||||
|
|
||||||
sealed class BackendStatus {
|
enum class BackendState {
|
||||||
data object Inactive : BackendStatus()
|
KILL_SWITCH_ACTIVE,
|
||||||
|
SERVICE_ACTIVE,
|
||||||
data object Active : BackendStatus()
|
INACTIVE,
|
||||||
|
|
||||||
data class KillSwitch(val allowedIps: List<String>) : BackendStatus()
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -9,6 +9,6 @@ interface UpdateRepository {
|
|||||||
suspend fun downloadApk(
|
suspend fun downloadApk(
|
||||||
apkUrl: String,
|
apkUrl: String,
|
||||||
fileName: String,
|
fileName: String,
|
||||||
onProgress: suspend (Float) -> Unit,
|
onProgress: (Float) -> Unit,
|
||||||
): Result<File>
|
): Result<File>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.domain.state
|
package com.zaneschepke.wireguardautotunnel.domain.state
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendStatus
|
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
||||||
import org.amnezia.awg.crypto.Key
|
import org.amnezia.awg.crypto.Key
|
||||||
|
|
||||||
data class TunnelState(
|
data class TunnelState(
|
||||||
val status: TunnelStatus = TunnelStatus.Down,
|
val status: TunnelStatus = TunnelStatus.Down,
|
||||||
val backendState: BackendStatus = BackendStatus.Inactive,
|
val backendState: BackendState = BackendState.INACTIVE,
|
||||||
val statistics: TunnelStatistics? = null,
|
val statistics: TunnelStatistics? = null,
|
||||||
val pingStates: Map<Key, PingState>? = null,
|
val pingStates: Map<Key, PingState>? = null,
|
||||||
val handshakeSuccessLogs: Boolean? = null,
|
val handshakeSuccessLogs: Boolean? = null,
|
||||||
|
|||||||
+19
-37
@@ -7,7 +7,10 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
@@ -22,7 +25,6 @@ import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
|
|||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.components.AddPeerButton
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.components.AddPeerButton
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.components.InterfaceSection
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.components.InterfaceSection
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.components.PeersSection
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.components.PeersSection
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||||
@@ -30,7 +32,6 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
|||||||
@Composable
|
@Composable
|
||||||
fun ConfigScreen(
|
fun ConfigScreen(
|
||||||
tunnelConf: TunnelConf?,
|
tunnelConf: TunnelConf?,
|
||||||
appUiState: AppUiState,
|
|
||||||
appViewModel: AppViewModel,
|
appViewModel: AppViewModel,
|
||||||
viewModel: ConfigViewModel = hiltViewModel(),
|
viewModel: ConfigViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
@@ -41,17 +42,6 @@ fun ConfigScreen(
|
|||||||
|
|
||||||
val activity = context as? MainActivity
|
val activity = context as? MainActivity
|
||||||
|
|
||||||
var save by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
val isTunnelNameTaken by
|
|
||||||
remember(uiState.tunnelName, appUiState.tunnels) {
|
|
||||||
derivedStateOf {
|
|
||||||
appUiState.tunnels
|
|
||||||
.filter { it.id != tunnelConf?.id }
|
|
||||||
.any { it.name == uiState.tunnelName }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Secure screen due to sensitive information
|
// Secure screen due to sensitive information
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
activity
|
activity
|
||||||
@@ -68,34 +58,26 @@ fun ConfigScreen(
|
|||||||
appViewModel.handleEvent(
|
appViewModel.handleEvent(
|
||||||
AppEvent.SetScreenAction {
|
AppEvent.SetScreenAction {
|
||||||
keyboardController?.hide()
|
keyboardController?.hide()
|
||||||
if (!isTunnelNameTaken) {
|
viewModel.save(tunnelConf)
|
||||||
save = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(tunnelConf) { viewModel.initFromTunnel(tunnelConf) }
|
LaunchedEffect(tunnelConf) { viewModel.initFromTunnel(tunnelConf) }
|
||||||
|
|
||||||
// TODO improve error messages
|
LaunchedEffect(uiState.success) {
|
||||||
LaunchedEffect(save) {
|
if (uiState.success == true) {
|
||||||
if (save) {
|
appViewModel.handleEvent(
|
||||||
try {
|
AppEvent.ShowMessage(StringValue.StringResource(R.string.config_changes_saved))
|
||||||
appViewModel.handleEvent(
|
)
|
||||||
AppEvent.SaveTunnel(
|
appViewModel.handleEvent(AppEvent.PopBackStack(true))
|
||||||
uiState.configProxy.buildTunnelConfFromState(uiState.tunnelName, tunnelConf)
|
}
|
||||||
)
|
}
|
||||||
)
|
|
||||||
appViewModel.handleEvent(
|
LaunchedEffect(uiState.message) {
|
||||||
AppEvent.ShowMessage(StringValue.StringResource(R.string.config_changes_saved))
|
uiState.message?.let { message ->
|
||||||
)
|
appViewModel.handleEvent(AppEvent.ShowMessage(message))
|
||||||
appViewModel.handleEvent(AppEvent.PopBackStack(true))
|
viewModel.setMessage(null)
|
||||||
} catch (e: Exception) {
|
|
||||||
val message = e.message ?: context.resources.getString(R.string.unknown_error)
|
|
||||||
appViewModel.handleEvent(AppEvent.ShowMessage(StringValue.DynamicString(message)))
|
|
||||||
} finally {
|
|
||||||
save = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,7 +111,7 @@ fun ConfigScreen(
|
|||||||
modifier =
|
modifier =
|
||||||
Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(horizontal = 24.dp),
|
Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(horizontal = 24.dp),
|
||||||
) {
|
) {
|
||||||
InterfaceSection(isTunnelNameTaken, uiState, viewModel)
|
InterfaceSection(uiState, viewModel)
|
||||||
PeersSection(uiState, viewModel)
|
PeersSection(uiState, viewModel)
|
||||||
AddPeerButton(viewModel)
|
AddPeerButton(viewModel)
|
||||||
}
|
}
|
||||||
|
|||||||
+48
-1
@@ -1,20 +1,32 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.main.config
|
package com.zaneschepke.wireguardautotunnel.ui.screens.main.config
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.state.ConfigUiState
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.state.ConfigUiState
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.state.ConfigProxy
|
import com.zaneschepke.wireguardautotunnel.ui.state.ConfigProxy
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.state.InterfaceProxy
|
import com.zaneschepke.wireguardautotunnel.ui.state.InterfaceProxy
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.state.PeerProxy
|
import com.zaneschepke.wireguardautotunnel.ui.state.PeerProxy
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class ConfigViewModel @Inject constructor() : ViewModel() {
|
class ConfigViewModel
|
||||||
|
@Inject
|
||||||
|
constructor(
|
||||||
|
private val tunnelRepository: TunnelRepository,
|
||||||
|
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _uiState = MutableStateFlow(ConfigUiState())
|
private val _uiState = MutableStateFlow(ConfigUiState())
|
||||||
val uiState: StateFlow<ConfigUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<ConfigUiState> = _uiState.asStateFlow()
|
||||||
@@ -109,6 +121,41 @@ class ConfigViewModel @Inject constructor() : ViewModel() {
|
|||||||
updatePeer(index, updated)
|
updatePeer(index, updated)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setMessage(message: StringValue?) {
|
||||||
|
_uiState.update { it.copy(message = message) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO improve error messaging
|
||||||
|
fun save(tunnelConf: TunnelConf?) =
|
||||||
|
viewModelScope.launch(ioDispatcher) {
|
||||||
|
val message =
|
||||||
|
try {
|
||||||
|
val saveConfig = buildTunnelConfFromState(tunnelConf)
|
||||||
|
tunnelRepository.save(saveConfig)
|
||||||
|
_uiState.update { it.copy(success = true) }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
setMessage(
|
||||||
|
e.message?.let { message -> (StringValue.DynamicString(message)) }
|
||||||
|
?: StringValue.StringResource(R.string.unknown_error)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildTunnelConfFromState(tunnelConf: TunnelConf?): TunnelConf {
|
||||||
|
val (wg, am) = _uiState.value.configProxy.buildConfigs()
|
||||||
|
val name = _uiState.value.tunnelName
|
||||||
|
return tunnelConf?.copyWithCallback(
|
||||||
|
tunName = name,
|
||||||
|
amQuick = am.toAwgQuickString(true),
|
||||||
|
wgQuick = wg.toWgQuickString(true),
|
||||||
|
)
|
||||||
|
?: TunnelConf(
|
||||||
|
tunName = name,
|
||||||
|
amQuick = am.toAwgQuickString(true),
|
||||||
|
wgQuick = wg.toWgQuickString(true),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun onAuthenticated() {
|
fun onAuthenticated() {
|
||||||
_uiState.update { it.copy(isAuthenticated = true) }
|
_uiState.update { it.copy(isAuthenticated = true) }
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-6
@@ -17,11 +17,7 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.ConfigViewMode
|
|||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.state.ConfigUiState
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.state.ConfigUiState
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun InterfaceSection(
|
fun InterfaceSection(uiState: ConfigUiState, viewModel: ConfigViewModel) {
|
||||||
isTunnelNameTaken: Boolean,
|
|
||||||
uiState: ConfigUiState,
|
|
||||||
viewModel: ConfigViewModel,
|
|
||||||
) {
|
|
||||||
var isDropDownExpanded by remember { mutableStateOf(false) }
|
var isDropDownExpanded by remember { mutableStateOf(false) }
|
||||||
val isAmneziaCompatibilitySet =
|
val isAmneziaCompatibilitySet =
|
||||||
remember(uiState.configProxy.`interface`) {
|
remember(uiState.configProxy.`interface`) {
|
||||||
@@ -54,7 +50,6 @@ fun InterfaceSection(
|
|||||||
value = uiState.tunnelName,
|
value = uiState.tunnelName,
|
||||||
onValueChange = viewModel::updateTunnelName,
|
onValueChange = viewModel::updateTunnelName,
|
||||||
label = stringResource(R.string.name),
|
label = stringResource(R.string.name),
|
||||||
isError = isTunnelNameTaken,
|
|
||||||
hint = stringResource(R.string.tunnel_name).lowercase(),
|
hint = stringResource(R.string.tunnel_name).lowercase(),
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
|
|||||||
+3
-1
@@ -3,6 +3,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.main.config.state
|
|||||||
import com.zaneschepke.wireguardautotunnel.ui.state.ConfigProxy
|
import com.zaneschepke.wireguardautotunnel.ui.state.ConfigProxy
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.state.InterfaceProxy
|
import com.zaneschepke.wireguardautotunnel.ui.state.InterfaceProxy
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.state.PeerProxy
|
import com.zaneschepke.wireguardautotunnel.ui.state.PeerProxy
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||||
|
|
||||||
data class ConfigUiState(
|
data class ConfigUiState(
|
||||||
val tunnelName: String = "",
|
val tunnelName: String = "",
|
||||||
@@ -12,5 +13,6 @@ data class ConfigUiState(
|
|||||||
val showScripts: Boolean = false,
|
val showScripts: Boolean = false,
|
||||||
val isAuthenticated: Boolean = true,
|
val isAuthenticated: Boolean = true,
|
||||||
val showAuthPrompt: Boolean = false,
|
val showAuthPrompt: Boolean = false,
|
||||||
val saveChanges: Boolean = false,
|
val message: StringValue? = null,
|
||||||
|
val success: Boolean? = null,
|
||||||
)
|
)
|
||||||
|
|||||||
+2
-8
@@ -4,25 +4,21 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.di.MainDispatcher
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.AppUpdate
|
import com.zaneschepke.wireguardautotunnel.domain.model.AppUpdate
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.UpdateRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.UpdateRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import jakarta.inject.Inject
|
import jakarta.inject.Inject
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class SupportViewModel
|
class SupportViewModel
|
||||||
@Inject
|
@Inject
|
||||||
constructor(private val updateRepository: UpdateRepository, private val fileUtils: FileUtils,
|
constructor(private val updateRepository: UpdateRepository, private val fileUtils: FileUtils) :
|
||||||
@MainDispatcher private val mainDispatcher: CoroutineDispatcher) :
|
|
||||||
ViewModel() {
|
ViewModel() {
|
||||||
|
|
||||||
private val _uiState = MutableStateFlow(SupportUiState())
|
private val _uiState = MutableStateFlow(SupportUiState())
|
||||||
@@ -66,9 +62,7 @@ constructor(private val updateRepository: UpdateRepository, private val fileUtil
|
|||||||
_uiState.update { it.copy(isLoading = true) }
|
_uiState.update { it.copy(isLoading = true) }
|
||||||
updateRepository
|
updateRepository
|
||||||
.downloadApk(appUpdate.apkUrl, appUpdate.apkFileName) { progress ->
|
.downloadApk(appUpdate.apkUrl, appUpdate.apkFileName) { progress ->
|
||||||
withContext(mainDispatcher) {
|
_uiState.update { it.copy(downloadProgress = progress) }
|
||||||
_uiState.update { it.copy(downloadProgress = progress) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.onSuccess { apk ->
|
.onSuccess { apk ->
|
||||||
_uiState.update { it.copy(isLoading = false) }
|
_uiState.update { it.copy(isLoading = false) }
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.state
|
package com.zaneschepke.wireguardautotunnel.ui.state
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
|
||||||
import org.amnezia.awg.config.Config
|
import org.amnezia.awg.config.Config
|
||||||
|
|
||||||
data class ConfigProxy(val peers: List<PeerProxy>, val `interface`: InterfaceProxy) {
|
data class ConfigProxy(val peers: List<PeerProxy>, val `interface`: InterfaceProxy) {
|
||||||
@@ -29,20 +28,6 @@ data class ConfigProxy(val peers: List<PeerProxy>, val `interface`: InterfacePro
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun buildTunnelConfFromState(name: String, tunnelConf: TunnelConf?): TunnelConf {
|
|
||||||
val (wg, am) = buildConfigs()
|
|
||||||
return tunnelConf?.copyWithCallback(
|
|
||||||
tunName = name,
|
|
||||||
amQuick = am.toAwgQuickString(true),
|
|
||||||
wgQuick = wg.toWgQuickString(true),
|
|
||||||
)
|
|
||||||
?: TunnelConf(
|
|
||||||
tunName = name,
|
|
||||||
amQuick = am.toAwgQuickString(true),
|
|
||||||
wgQuick = wg.toWgQuickString(true),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun from(amConfig: Config): ConfigProxy {
|
fun from(amConfig: Config): ConfigProxy {
|
||||||
return ConfigProxy(
|
return ConfigProxy(
|
||||||
|
|||||||
+1
-1
@@ -21,10 +21,10 @@ import com.zaneschepke.wireguardautotunnel.R
|
|||||||
import com.zaneschepke.wireguardautotunnel.core.service.tile.AutoTunnelControlTile
|
import com.zaneschepke.wireguardautotunnel.core.service.tile.AutoTunnelControlTile
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.tile.TunnelControlTile
|
import com.zaneschepke.wireguardautotunnel.core.service.tile.TunnelControlTile
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
fun Context.openWebUrl(url: String): Result<Unit> {
|
fun Context.openWebUrl(url: String): Result<Unit> {
|
||||||
return kotlin
|
return kotlin
|
||||||
|
|||||||
+5
-14
@@ -2,7 +2,7 @@ package com.zaneschepke.wireguardautotunnel.util.extensions
|
|||||||
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import com.wireguard.android.backend.BackendException
|
import com.wireguard.android.backend.BackendException
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendStatus
|
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.HandshakeStatus
|
import com.zaneschepke.wireguardautotunnel.domain.enums.HandshakeStatus
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.events.BackendError
|
import com.zaneschepke.wireguardautotunnel.domain.events.BackendError
|
||||||
@@ -75,21 +75,12 @@ fun Config.defaultName(): String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Backend.BackendStatus.asBackendStatus(): BackendStatus {
|
fun Backend.BackendState.asBackendState(): BackendState {
|
||||||
return when (val status = this) {
|
return BackendState.valueOf(this.name)
|
||||||
is Backend.BackendStatus.KillSwitchActive ->
|
|
||||||
BackendStatus.KillSwitch(status.allowedIps.toList())
|
|
||||||
is Backend.BackendStatus.ServiceActive -> BackendStatus.Active
|
|
||||||
else -> BackendStatus.Inactive
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun BackendStatus.asAmBackendStatus(): Backend.BackendStatus {
|
fun BackendState.asAmBackendState(): Backend.BackendState {
|
||||||
return when (val status = this) {
|
return Backend.BackendState.valueOf(this.name)
|
||||||
is BackendStatus.Active -> Backend.BackendStatus.ServiceActive.INSTANCE
|
|
||||||
is BackendStatus.Inactive -> Backend.BackendStatus.Inactive.INSTANCE
|
|
||||||
is BackendStatus.KillSwitch -> Backend.BackendStatus.KillSwitchActive(status.allowedIps)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Tunnel.State.asTunnelState(): TunnelStatus {
|
fun Tunnel.State.asTunnelState(): TunnelStatus {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
|||||||
import com.zaneschepke.wireguardautotunnel.di.AppShell
|
import com.zaneschepke.wireguardautotunnel.di.AppShell
|
||||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||||
import com.zaneschepke.wireguardautotunnel.di.MainDispatcher
|
import com.zaneschepke.wireguardautotunnel.di.MainDispatcher
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendStatus
|
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.ConfigType
|
import com.zaneschepke.wireguardautotunnel.domain.enums.ConfigType
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.events.BackendError
|
import com.zaneschepke.wireguardautotunnel.domain.events.BackendError
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.AppSettings
|
import com.zaneschepke.wireguardautotunnel.domain.model.AppSettings
|
||||||
@@ -35,12 +35,6 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.withFirstState
|
|||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.UiEvent
|
import com.zaneschepke.wireguardautotunnel.viewmodel.event.UiEvent
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import java.io.IOException
|
|
||||||
import java.net.URL
|
|
||||||
import java.time.Instant
|
|
||||||
import java.util.*
|
|
||||||
import javax.inject.Inject
|
|
||||||
import javax.inject.Provider
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
@@ -51,6 +45,12 @@ import org.amnezia.awg.config.Config
|
|||||||
import rikka.shizuku.Shizuku
|
import rikka.shizuku.Shizuku
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||||
|
import java.io.IOException
|
||||||
|
import java.net.URL
|
||||||
|
import java.time.Instant
|
||||||
|
import java.util.*
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Provider
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class AppViewModel
|
class AppViewModel
|
||||||
@@ -148,7 +148,8 @@ constructor(
|
|||||||
is AppEvent.ImportTunnelFromClipboard ->
|
is AppEvent.ImportTunnelFromClipboard ->
|
||||||
handleClipboardImport(event.text, state.tunnels)
|
handleClipboardImport(event.text, state.tunnels)
|
||||||
|
|
||||||
is AppEvent.ImportTunnelFromFile -> handleImportTunnelFromFile(event.data)
|
is AppEvent.ImportTunnelFromFile ->
|
||||||
|
handleImportTunnelFromFile(event.data, state.tunnels)
|
||||||
|
|
||||||
is AppEvent.ImportTunnelFromUrl ->
|
is AppEvent.ImportTunnelFromUrl ->
|
||||||
handleImportTunnelFromUrl(event.url, state.tunnels)
|
handleImportTunnelFromUrl(event.url, state.tunnels)
|
||||||
@@ -263,45 +264,11 @@ constructor(
|
|||||||
saveSettings(
|
saveSettings(
|
||||||
state.appSettings.copy(tunnelPingTimeoutSeconds = event.timeout)
|
state.appSettings.copy(tunnelPingTimeoutSeconds = event.timeout)
|
||||||
)
|
)
|
||||||
is AppEvent.SaveTunnel -> saveTunnel(event.tunnel)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun saveTunnelsUniquely(tunnels: List<TunnelConf>) {
|
|
||||||
withContext(ioDispatcher) {
|
|
||||||
tunnelMutex.withLock {
|
|
||||||
val existingTunnels = appDataRepository.tunnels.getAll()
|
|
||||||
val uniqueTuns = generateUniquelyNamedConfigs(tunnels, existingTunnels)
|
|
||||||
appDataRepository.tunnels.saveAll(uniqueTuns)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun generateUniquelyNamedConfigs(
|
|
||||||
incoming: List<TunnelConf>,
|
|
||||||
existing: List<TunnelConf>,
|
|
||||||
): List<TunnelConf> {
|
|
||||||
val usedNames = existing.map { it.tunName }.toMutableSet()
|
|
||||||
val result = mutableListOf<TunnelConf>()
|
|
||||||
|
|
||||||
for (tun in incoming) {
|
|
||||||
var uniqueName = tun.tunName
|
|
||||||
var counter = 1
|
|
||||||
|
|
||||||
while (uniqueName in usedNames) {
|
|
||||||
uniqueName = "${tun.tunName} ($counter)"
|
|
||||||
counter++
|
|
||||||
}
|
|
||||||
|
|
||||||
usedNames.add(uniqueName)
|
|
||||||
result.add(tun.copy(tunName = uniqueName))
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
fun handleUiEvent(event: UiEvent): Job =
|
fun handleUiEvent(event: UiEvent): Job =
|
||||||
viewModelScope.launch(mainDispatcher) { _uiEvent.emit(event) }
|
viewModelScope.launch(mainDispatcher) { _uiEvent.emit(event) }
|
||||||
|
|
||||||
@@ -452,7 +419,7 @@ constructor(
|
|||||||
_appViewState.update { it.copy(bottomSheet = bottomSheet) }
|
_appViewState.update { it.copy(bottomSheet = bottomSheet) }
|
||||||
|
|
||||||
private suspend fun handleTunnelPingTargetChange(tunnelConf: TunnelConf, target: String) =
|
private suspend fun handleTunnelPingTargetChange(tunnelConf: TunnelConf, target: String) =
|
||||||
saveTunnel(tunnelConf.copy(pingTarget = target.ifBlank { null }))
|
saveTunnel(tunnelConf.copy(pingTarget = target))
|
||||||
|
|
||||||
private suspend fun handleTogglePingTunnel(tunnel: TunnelConf) =
|
private suspend fun handleTogglePingTunnel(tunnel: TunnelConf) =
|
||||||
saveTunnel(tunnel.copy(restartOnPingFailure = !tunnel.restartOnPingFailure))
|
saveTunnel(tunnel.copy(restartOnPingFailure = !tunnel.restartOnPingFailure))
|
||||||
@@ -548,10 +515,17 @@ constructor(
|
|||||||
_appViewState.update { it.copy(popBackStack = true) }
|
_appViewState.update { it.copy(popBackStack = true) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun handleImportTunnelFromFile(uri: Uri) {
|
private suspend fun handleImportTunnelFromFile(uri: Uri, tunnels: List<TunnelConf>) {
|
||||||
runCatching {
|
runCatching {
|
||||||
val tunnelConfigs = fileUtils.buildTunnelsFromUri(uri)
|
val tunnelConfigs = fileUtils.buildTunnelsFromUri(uri)
|
||||||
saveTunnelsUniquely(tunnelConfigs)
|
val existingNames = tunnels.map { it.tunName }.toMutableList()
|
||||||
|
val uniqueTunnelConfigs =
|
||||||
|
tunnelConfigs.map { config ->
|
||||||
|
val uniqueName = config.generateUniqueName(existingNames)
|
||||||
|
existingNames.add(uniqueName)
|
||||||
|
config.copy(tunName = uniqueName)
|
||||||
|
}
|
||||||
|
appDataRepository.tunnels.saveAll(uniqueTunnelConfigs)
|
||||||
}
|
}
|
||||||
.onFailure {
|
.onFailure {
|
||||||
when (it) {
|
when (it) {
|
||||||
@@ -570,7 +544,11 @@ constructor(
|
|||||||
runCatching {
|
runCatching {
|
||||||
val amConfig = TunnelConf.configFromAmQuick(config)
|
val amConfig = TunnelConf.configFromAmQuick(config)
|
||||||
val tunnelConf = TunnelConf.tunnelConfigFromAmConfig(amConfig)
|
val tunnelConf = TunnelConf.tunnelConfigFromAmConfig(amConfig)
|
||||||
saveTunnelsUniquely(listOf(tunnelConf))
|
saveTunnel(
|
||||||
|
tunnelConf.copy(
|
||||||
|
tunName = tunnelConf.generateUniqueName(tunnels.map { it.tunName })
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.onFailure {
|
.onFailure {
|
||||||
Timber.e(it)
|
Timber.e(it)
|
||||||
@@ -588,7 +566,11 @@ constructor(
|
|||||||
url.openStream().use { stream ->
|
url.openStream().use { stream ->
|
||||||
val amConfig = Config.parse(stream)
|
val amConfig = Config.parse(stream)
|
||||||
val tunnelConf = TunnelConf.tunnelConfigFromAmConfig(amConfig)
|
val tunnelConf = TunnelConf.tunnelConfigFromAmConfig(amConfig)
|
||||||
saveTunnelsUniquely(listOf(tunnelConf))
|
saveTunnel(
|
||||||
|
tunnelConf.copy(
|
||||||
|
tunName = tunnelConf.generateUniqueName(tunnels.map { it.tunName })
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onFailure {
|
.onFailure {
|
||||||
@@ -666,19 +648,19 @@ constructor(
|
|||||||
val updatedSettings =
|
val updatedSettings =
|
||||||
appSettings.copy(isLanOnKillSwitchEnabled = !appSettings.isLanOnKillSwitchEnabled)
|
appSettings.copy(isLanOnKillSwitchEnabled = !appSettings.isLanOnKillSwitchEnabled)
|
||||||
saveSettings(updatedSettings)
|
saveSettings(updatedSettings)
|
||||||
handleKillSwitchChange(updatedSettings)
|
handleKillSwitchChange(appSettings)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleKillSwitchChange(appSettings: AppSettings) {
|
private fun handleKillSwitchChange(appSettings: AppSettings) {
|
||||||
// let auto tunnel handle kill switch changes if running
|
// let auto tunnel handle kill switch changes if running
|
||||||
if (uiState.value.isAutoTunnelActive) return
|
if (uiState.value.isAutoTunnelActive) return
|
||||||
if (!appSettings.isVpnKillSwitchEnabled)
|
if (!appSettings.isVpnKillSwitchEnabled)
|
||||||
return tunnelManager.setBackendStatus(BackendStatus.Active)
|
return tunnelManager.setBackendState(BackendState.SERVICE_ACTIVE, emptyList())
|
||||||
Timber.d("Starting kill switch")
|
Timber.d("Starting kill switch")
|
||||||
val allowedIps =
|
val allowedIps =
|
||||||
if (appSettings.isLanOnKillSwitchEnabled) TunnelConf.LAN_BYPASS_ALLOWED_IPS
|
if (appSettings.isLanOnKillSwitchEnabled) TunnelConf.LAN_BYPASS_ALLOWED_IPS
|
||||||
else emptyList()
|
else emptyList()
|
||||||
tunnelManager.setBackendStatus(BackendStatus.KillSwitch(allowedIps))
|
tunnelManager.setBackendState(BackendState.KILL_SWITCH_ACTIVE, allowedIps)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun handleToggleAppShortcuts(appSettings: AppSettings) {
|
private suspend fun handleToggleAppShortcuts(appSettings: AppSettings) {
|
||||||
@@ -713,7 +695,7 @@ constructor(
|
|||||||
}
|
}
|
||||||
if (enabled && !requestRoot()) return
|
if (enabled && !requestRoot()) return
|
||||||
// disable kill switch feature in kernel mode
|
// disable kill switch feature in kernel mode
|
||||||
tunnelManager.setBackendStatus(BackendStatus.Inactive)
|
tunnelManager.setBackendState(BackendState.INACTIVE, emptyList())
|
||||||
saveSettings(
|
saveSettings(
|
||||||
appSettings.copy(
|
appSettings.copy(
|
||||||
isKernelEnabled = enabled,
|
isKernelEnabled = enabled,
|
||||||
|
|||||||
@@ -77,8 +77,6 @@ sealed class AppEvent {
|
|||||||
|
|
||||||
data class SetTheme(val theme: Theme) : AppEvent()
|
data class SetTheme(val theme: Theme) : AppEvent()
|
||||||
|
|
||||||
data class SaveTunnel(val tunnel: TunnelConf) : AppEvent()
|
|
||||||
|
|
||||||
data class SaveMonitoringSettings(
|
data class SaveMonitoringSettings(
|
||||||
val pingInterval: Int,
|
val pingInterval: Int,
|
||||||
val tunnelPingAttempts: Int,
|
val tunnelPingAttempts: Int,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
object Constants {
|
object Constants {
|
||||||
const val VERSION_NAME = "3.9.5"
|
const val VERSION_NAME = "3.9.4"
|
||||||
const val VERSION_CODE = 39500
|
const val VERSION_CODE = 39400
|
||||||
const val TARGET_SDK = 35
|
const val TARGET_SDK = 35
|
||||||
const val MIN_SDK = 26
|
const val MIN_SDK = 26
|
||||||
const val APP_ID = "com.zaneschepke.wireguardautotunnel"
|
const val APP_ID = "com.zaneschepke.wireguardautotunnel"
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
What's new:
|
|
||||||
- Fix for tunnel sort bug
|
|
||||||
- Improved location permissions flow
|
|
||||||
- Location permission detection and notifications
|
|
||||||
- Fix for AndroidTV apps detection for split tunneling
|
|
||||||
- Improved tunnel monitoring and reboot recovery
|
|
||||||
- Fix tunnel slow reconnect from sleep
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
[versions]
|
[versions]
|
||||||
accompanist = "0.37.3"
|
accompanist = "0.37.3"
|
||||||
activityCompose = "1.10.1"
|
activityCompose = "1.10.1"
|
||||||
amneziawgAndroid = "1.6.2"
|
amneziawgAndroid = "1.4.0"
|
||||||
androidx-junit = "1.3.0"
|
androidx-junit = "1.3.0"
|
||||||
icmp4a = "1.0.0"
|
icmp4a = "1.0.0"
|
||||||
roomdatabasebackup = "1.1.0"
|
roomdatabasebackup = "1.1.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user