Compare commits

..

15 Commits

Author SHA1 Message Date
dependabot[bot] 4b7af214fe chore(deps): bump androidGradlePlugin from 8.11.0 to 8.12.0
Bumps `androidGradlePlugin` from 8.11.0 to 8.12.0.

Updates `com.android.application` from 8.11.0 to 8.12.0

Updates `com.android.library` from 8.11.0 to 8.12.0

---
updated-dependencies:
- dependency-name: com.android.application
  dependency-version: 8.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: com.android.library
  dependency-version: 8.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-11 18:43:41 +00:00
Zane Schepke 5d8fb38906 fix: uapi socket connection with support for dynamic packages 2025-08-09 19:21:40 -04:00
Zane Schepke 6d100a2f46 fix: kill switch stuck on bug 2025-08-09 02:19:30 -04:00
Zane Schepke 5feb2827fd refactor: share import save logic 2025-08-08 19:37:01 -04:00
Zane Schepke 0336c2ac9f fix: duplicate tunnel names overwrite bug
closes #886
2025-08-08 19:33:49 -04:00
Zane Schepke 96d8114d37 fix: split tunneling for AndroidTV only apps typo
#805
2025-08-08 17:20:53 -04:00
Zane Schepke e78469c730 fix: split tunneling for AndroidTV only apps
closes #805
2025-08-08 16:46:58 -04:00
dependabot[bot] 6f365a4490 chore(deps): bump actions/download-artifact from 4 to 5 (#884)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-08 16:20:16 -04:00
Zane Schepke 2885d1a539 fix: ping target empty bug 2025-08-08 16:03:19 -04:00
Zane Schepke c56b11599f ci: fix debug build path 2025-08-08 15:28:51 -04:00
Zane Schepke 753575c50d chore: gradle checksum 2025-08-08 15:12:15 -04:00
Zane Schepke 78b419dc6e chore: bump deps 2025-08-08 04:46:32 -04:00
Zane Schepke e8681af273 feat: app database backup and restore
closes #541
2025-08-08 04:07:04 -04:00
Zane Schepke cb92c9605f fix: startup splash bug 2025-08-08 02:35:58 -04:00
Zane Schepke 38ecb0b66b feat!: tun monitoring, move ping restarts to auto-tunnel w/recovery (#885)
This is a big one.. oops.

Main changes:
- Make ping monitor more robust and global, with ping target overrides of the default cloudflare fallback target per tunnel (for full tunnels, otherwise we ping the internal tun ip)
- Include ping restart recovery to prevent tun being down if dns failures happen after a bounce
- Ping monitoring itself remains per tunnel and works without auto tunnel active, but moves the restart feature back to be managed by and integrated with auto tunnel to prevent inconsistencies and conflicts
- Ping statistics can be optionally included to be displayed with tun statistics
- Adds the beginnings of monitoring logs for handshake and data packet failures for userspace tuns (to be incorporated with restarts/tun status later)
- Improve tun error notifications, adds ping restart notifications
- Major refactor of auto tunnel logic to make it more modular and extensible for new auto tunnel conditions
- A bunch of other stuff..
2025-08-07 18:19:36 -04:00
33 changed files with 412 additions and 229 deletions
+6 -1
View File
@@ -118,6 +118,11 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: android_artifacts_${{ inputs.flavor }}
path: app/build/outputs/apk/${{ inputs.flavor }}/${{ inputs.build_type }}/wgtunnel-${{ inputs.flavor }}${{ inputs.flavor == 'fdroid' && '-release' || '' }}-*.apk
path: >-
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
if-no-files-found: warn
+1 -1
View File
@@ -69,7 +69,7 @@ jobs:
run: mkdir ${{ github.workspace }}/temp
- name: Download artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
pattern: android_artifacts_*
path: ${{ github.workspace }}/temp
+1 -1
View File
@@ -108,7 +108,7 @@ jobs:
run: mkdir ${{ github.workspace }}/temp
- name: Download artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
pattern: android_artifacts_*
path: ${{ github.workspace }}/temp
+5 -2
View File
@@ -121,8 +121,8 @@ android {
packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } }
licensee {
Constants.allowedLicenses.forEach { allow(it) }
Constants.allowedLicenseUrls.forEach { allowUrl(it) }
allowedLicenses().forEach { allow(it) }
allowedLicenseUrls().forEach { allowUrl(it) }
}
applicationVariants.all {
@@ -225,6 +225,9 @@ dependencies {
implementation(libs.shizuku.provider)
implementation(libs.reorderable)
implementation(libs.roomdatabasebackup) {
exclude(group = "org.reactivestreams", module = "reactive-streams")
}
}
tasks.register<Copy>("copyLicenseeJsonToAssets") {
+4
View File
@@ -47,6 +47,10 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent>
<intent>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent>
</queries>
<application
android:name=".WireGuardAutoTunnel"
@@ -28,6 +28,7 @@ import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
@@ -35,6 +36,9 @@ import androidx.navigation.compose.rememberNavController
import androidx.navigation.toRoute
import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.di.MainDispatcher
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.VpnDeniedDialog
@@ -67,11 +71,16 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.license.LicenseScreen
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import com.zaneschepke.wireguardautotunnel.util.extensions.restartApp
import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
import dagger.hilt.android.AndroidEntryPoint
import de.raphaelebner.roomdatabasebackup.core.RoomBackup
import javax.inject.Inject
import kotlin.system.exitProcess
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.launch
import org.amnezia.awg.backend.GoBackend.VpnService
@AndroidEntryPoint
@@ -83,8 +92,16 @@ class MainActivity : AppCompatActivity() {
@Inject lateinit var networkMonitor: NetworkMonitor
@Inject @IoDispatcher lateinit var ioDispatcher: CoroutineDispatcher
@Inject @MainDispatcher lateinit var mainDispatcher: CoroutineDispatcher
@Inject lateinit var appDatabase: AppDatabase
private var lastLocationPermissionState: Boolean? = null
private lateinit var roomBackup: RoomBackup
val REQUEST_CODE = 123
@SuppressLint("BatteryLife")
@@ -97,6 +114,7 @@ class MainActivity : AppCompatActivity() {
window.isNavigationBarContrastEnforced = false
}
super.onCreate(savedInstanceState)
roomBackup = RoomBackup(this)
val viewModel by viewModels<AppViewModel>()
@@ -250,7 +268,7 @@ class MainActivity : AppCompatActivity() {
MainScreen(appUiState, appViewState, viewModel)
}
composable<Route.Settings> {
SettingsScreen(appUiState, viewModel)
SettingsScreen(appUiState, appViewState, viewModel)
}
composable<Route.SettingsAdvanced> {
SettingsAdvancedScreen(appUiState, viewModel)
@@ -283,7 +301,7 @@ class MainActivity : AppCompatActivity() {
val args = backStack.toRoute<Route.Config>()
val config =
appUiState.tunnels.firstOrNull { it.id == args.id }
ConfigScreen(config, viewModel)
ConfigScreen(config, appUiState, viewModel)
}
composable<Route.TunnelOptions> { backStack ->
val args = backStack.toRoute<Route.TunnelOptions>()
@@ -338,4 +356,53 @@ class MainActivity : AppCompatActivity() {
super.onPause()
WireGuardAutoTunnel.setUiActive(false)
}
fun performBackup() =
lifecycleScope.launch(ioDispatcher) {
roomBackup
.database(appDatabase)
.backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG)
.enableLogDebug(true)
.maxFileCount(5)
.apply {
onCompleteListener { success, message, exitCode ->
lifecycleScope.launch(mainDispatcher) {
if (success) {
showToast(
getString(
R.string.backup_success,
getString(R.string.restarting_app),
)
)
restartApp()
} else showToast(R.string.backup_failed)
}
}
}
.backup()
}
fun performRestore() =
lifecycleScope.launch {
roomBackup
.database(appDatabase)
.enableLogDebug(true)
.backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG)
.apply {
onCompleteListener { success, message, exitCode ->
lifecycleScope.launch(mainDispatcher) {
if (success) {
showToast(
getString(
R.string.restore_success,
getString(R.string.restarting_app),
)
)
restartApp()
} else showToast(R.string.restore_failed)
}
}
}
.restore()
}
}
@@ -13,7 +13,7 @@ import com.zaneschepke.wireguardautotunnel.core.worker.ServiceWorker
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.di.MainDispatcher
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendStatus
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
@@ -91,7 +91,7 @@ class WireGuardAutoTunnel : Application(), Configuration.Provider {
override fun onTerminate() {
applicationScope.cancel()
tunnelManager.setBackendState(BackendState.INACTIVE, emptyList())
tunnelManager.setBackendStatus(BackendStatus.Inactive)
super.onTerminate()
}
@@ -16,7 +16,7 @@ import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelMonitor
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendStatus
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus.StopReason.Ping
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
@@ -109,13 +109,13 @@ class AutoTunnelService : LifecycleService() {
with(autoTunnelStateFlow.value) {
if (
settings.isVpnKillSwitchEnabled &&
tunnelManager.getBackendState() != BackendState.KILL_SWITCH_ACTIVE
tunnelManager.getBackendStatus() !is BackendStatus.KillSwitch
) {
eventHandlerJob?.cancel()
val allowedIps =
if (settings.isLanOnKillSwitchEnabled) TunnelConf.LAN_BYPASS_ALLOWED_IPS
else emptyList()
tunnelManager.setBackendState(BackendState.KILL_SWITCH_ACTIVE, allowedIps)
tunnelManager.setBackendStatus(BackendStatus.KillSwitch(allowedIps))
}
}
}
@@ -402,11 +402,11 @@ class AutoTunnelService : LifecycleService() {
handleBounceWithBackoff(event.configsPeerKeyResolvedMap)
is AutoTunnelEvent.StartKillSwitch -> {
Timber.d("Starting kill switch")
tunnelManager.setBackendState(BackendState.KILL_SWITCH_ACTIVE, event.allowedIps)
tunnelManager.setBackendStatus(BackendStatus.KillSwitch(event.allowedIps))
}
AutoTunnelEvent.StopKillSwitch -> {
Timber.d("Stopping kill switch")
tunnelManager.setBackendState(BackendState.SERVICE_ACTIVE, emptySet())
tunnelManager.setBackendStatus(BackendStatus.Active)
}
}
}
@@ -5,7 +5,7 @@ import com.wireguard.android.backend.BackendException
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendStatus
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.events.BackendError
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
@@ -59,12 +59,12 @@ constructor(
}
}
override fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
override fun setBackendStatus(backendStatus: BackendStatus) {
Timber.w("Not yet implemented for kernel")
}
override fun getBackendState(): BackendState {
return BackendState.INACTIVE
override fun getBackendStatus(): BackendStatus {
return BackendStatus.Inactive
}
override suspend fun runningTunnelNames(): Set<String> {
@@ -1,6 +1,6 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendStatus
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.events.BackendError
import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage
@@ -34,7 +34,8 @@ constructor(
appDataRepository.settings.flow
.filterNotNull()
.flatMapLatest { settings ->
MutableStateFlow(if (settings.isKernelEnabled) kernelTunnel else userspaceTunnel)
val backend = if (settings.isKernelEnabled) kernelTunnel else userspaceTunnel
MutableStateFlow(backend)
}
.stateIn(
scope = applicationScope.plus(ioDispatcher),
@@ -89,12 +90,12 @@ constructor(
tunnelProviderFlow.value.bounceTunnel(tunnelConf, reason)
}
override fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
tunnelProviderFlow.value.setBackendState(backendState, allowedIps)
override fun setBackendStatus(backendStatus: BackendStatus) {
tunnelProviderFlow.value.setBackendStatus(backendStatus)
}
override fun getBackendState(): BackendState {
return tunnelProviderFlow.value.getBackendState()
override fun getBackendStatus(): BackendStatus {
return tunnelProviderFlow.value.getBackendStatus()
}
override suspend fun runningTunnelNames(): Set<String> {
@@ -1,6 +1,6 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendStatus
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.events.BackendError
import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage
@@ -41,9 +41,9 @@ interface TunnelProvider {
reason: TunnelStatus.StopReason = TunnelStatus.StopReason.User,
)
fun setBackendState(backendState: BackendState, allowedIps: Collection<String>)
fun setBackendStatus(backendStatus: BackendStatus)
fun getBackendState(): BackendState
fun getBackendStatus(): BackendStatus
suspend fun runningTunnelNames(): Set<String>
@@ -1,15 +1,15 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendStatus
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.events.BackendError
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.state.AmneziaStatistics
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.util.extensions.asAmBackendState
import com.zaneschepke.wireguardautotunnel.util.extensions.asBackendState
import com.zaneschepke.wireguardautotunnel.util.extensions.asAmBackendStatus
import com.zaneschepke.wireguardautotunnel.util.extensions.asBackendStatus
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendError
import javax.inject.Inject
import kotlin.jvm.optionals.getOrNull
@@ -17,7 +17,6 @@ import kotlinx.coroutines.CoroutineScope
import org.amnezia.awg.backend.Backend
import org.amnezia.awg.backend.BackendException
import org.amnezia.awg.backend.Tunnel
import org.amnezia.awg.config.Config
import timber.log.Timber
class UserspaceTunnel
@@ -29,14 +28,21 @@ constructor(
private val backend: Backend,
) : BaseTunnel(applicationScope, appDataRepository, serviceManager) {
private var previousBackendState: Pair<BackendState, Boolean>? = null
override suspend fun startBackend(tunnel: TunnelConf) {
try {
updateTunnelStatus(tunnel, TunnelStatus.Starting)
val amConfig = tunnel.toAmConfig()
handleVpnKillSwitchWithDomainEndpoints(amConfig)
var previousKillSwitch: Backend.BackendStatus? = null
// 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)
previousKillSwitch?.let { backend.backendStatus = it }
} catch (e: BackendException) {
Timber.e(e, "Failed to start up backend for tunnel ${tunnel.name}")
throw e.toBackendError()
@@ -53,47 +59,20 @@ constructor(
} catch (e: BackendException) {
Timber.e(e, "Failed to stop tunnel ${tunnel.id}")
throw e.toBackendError()
} finally {
handlePreviouslyEnabledVpnKillSwitch()
}
}
// stop vpn kill switch if we need to resolve DNS for peer endpoints
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")
override fun setBackendStatus(backendStatus: BackendStatus) {
Timber.d("Setting backend state: $backendStatus")
try {
backend.setBackendState(backendState.asAmBackendState(), allowedIps)
backend.backendStatus = backendStatus.asAmBackendStatus()
} catch (e: BackendException) {
throw e.toBackendError()
}
}
override fun getBackendState(): BackendState {
return backend.backendState.asBackendState()
override fun getBackendStatus(): BackendStatus {
return backend.backendStatus.asBackendStatus()
}
override suspend fun runningTunnelNames(): Set<String> {
@@ -1,7 +1,9 @@
package com.zaneschepke.wireguardautotunnel.domain.enums
enum class BackendState {
KILL_SWITCH_ACTIVE,
SERVICE_ACTIVE,
INACTIVE,
sealed class BackendStatus {
data object Inactive : BackendStatus()
data object Active : BackendStatus()
data class KillSwitch(val allowedIps: List<String>) : BackendStatus()
}
@@ -1,12 +1,12 @@
package com.zaneschepke.wireguardautotunnel.domain.state
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendStatus
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import org.amnezia.awg.crypto.Key
data class TunnelState(
val status: TunnelStatus = TunnelStatus.Down,
val backendState: BackendState = BackendState.INACTIVE,
val backendState: BackendStatus = BackendStatus.Inactive,
val statistics: TunnelStatistics? = null,
val pingStates: Map<Key, PingState>? = null,
val handshakeSuccessLogs: Boolean? = null,
@@ -143,6 +143,15 @@ fun currentNavBackStackEntryAsNavBarState(
NavBarState(
topTitle = { Text(stringResource(R.string.settings)) },
route = Route.Settings,
topTrailing = {
ActionIconButton(Icons.Rounded.Menu, R.string.quick_actions) {
viewModel.handleEvent(
AppEvent.SetBottomSheet(
AppViewState.BottomSheet.BACKUP_AND_RESTORE
)
)
}
},
)
backStackEntry.isCurrentRoute(Route.Appearance::class) ->
@@ -7,10 +7,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
@@ -25,6 +22,7 @@ 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.InterfaceSection
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.viewmodel.AppViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
@@ -32,6 +30,7 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
@Composable
fun ConfigScreen(
tunnelConf: TunnelConf?,
appUiState: AppUiState,
appViewModel: AppViewModel,
viewModel: ConfigViewModel = hiltViewModel(),
) {
@@ -42,6 +41,17 @@ fun ConfigScreen(
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
DisposableEffect(Unit) {
activity
@@ -58,26 +68,34 @@ fun ConfigScreen(
appViewModel.handleEvent(
AppEvent.SetScreenAction {
keyboardController?.hide()
viewModel.save(tunnelConf)
if (!isTunnelNameTaken) {
save = true
}
}
)
}
LaunchedEffect(tunnelConf) { viewModel.initFromTunnel(tunnelConf) }
LaunchedEffect(uiState.success) {
if (uiState.success == true) {
appViewModel.handleEvent(
AppEvent.ShowMessage(StringValue.StringResource(R.string.config_changes_saved))
)
appViewModel.handleEvent(AppEvent.PopBackStack(true))
}
}
LaunchedEffect(uiState.message) {
uiState.message?.let { message ->
appViewModel.handleEvent(AppEvent.ShowMessage(message))
viewModel.setMessage(null)
// TODO improve error messages
LaunchedEffect(save) {
if (save) {
try {
appViewModel.handleEvent(
AppEvent.SaveTunnelUniquely(
uiState.configProxy.buildTunnelConfFromState(uiState.tunnelName, tunnelConf)
)
)
appViewModel.handleEvent(
AppEvent.ShowMessage(StringValue.StringResource(R.string.config_changes_saved))
)
appViewModel.handleEvent(AppEvent.PopBackStack(true))
} catch (e: Exception) {
val message = e.message ?: context.resources.getString(R.string.unknown_error)
appViewModel.handleEvent(AppEvent.ShowMessage(StringValue.DynamicString(message)))
} finally {
save = false
}
}
}
@@ -111,7 +129,7 @@ fun ConfigScreen(
modifier =
Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(horizontal = 24.dp),
) {
InterfaceSection(uiState, viewModel)
InterfaceSection(isTunnelNameTaken, uiState, viewModel)
PeersSection(uiState, viewModel)
AddPeerButton(viewModel)
}
@@ -1,32 +1,20 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main.config
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.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.state.ConfigUiState
import com.zaneschepke.wireguardautotunnel.ui.state.ConfigProxy
import com.zaneschepke.wireguardautotunnel.ui.state.InterfaceProxy
import com.zaneschepke.wireguardautotunnel.ui.state.PeerProxy
import com.zaneschepke.wireguardautotunnel.util.StringValue
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
@HiltViewModel
class ConfigViewModel
@Inject
constructor(
private val tunnelRepository: TunnelRepository,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : ViewModel() {
class ConfigViewModel @Inject constructor() : ViewModel() {
private val _uiState = MutableStateFlow(ConfigUiState())
val uiState: StateFlow<ConfigUiState> = _uiState.asStateFlow()
@@ -121,41 +109,6 @@ constructor(
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() {
_uiState.update { it.copy(isAuthenticated = true) }
}
@@ -17,7 +17,11 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.ConfigViewMode
import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.state.ConfigUiState
@Composable
fun InterfaceSection(uiState: ConfigUiState, viewModel: ConfigViewModel) {
fun InterfaceSection(
isTunnelNameTaken: Boolean,
uiState: ConfigUiState,
viewModel: ConfigViewModel,
) {
var isDropDownExpanded by remember { mutableStateOf(false) }
val isAmneziaCompatibilitySet =
remember(uiState.configProxy.`interface`) {
@@ -50,6 +54,7 @@ fun InterfaceSection(uiState: ConfigUiState, viewModel: ConfigViewModel) {
value = uiState.tunnelName,
onValueChange = viewModel::updateTunnelName,
label = stringResource(R.string.name),
isError = isTunnelNameTaken,
hint = stringResource(R.string.tunnel_name).lowercase(),
modifier = Modifier.fillMaxWidth(),
)
@@ -3,7 +3,6 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.main.config.state
import com.zaneschepke.wireguardautotunnel.ui.state.ConfigProxy
import com.zaneschepke.wireguardautotunnel.ui.state.InterfaceProxy
import com.zaneschepke.wireguardautotunnel.ui.state.PeerProxy
import com.zaneschepke.wireguardautotunnel.util.StringValue
data class ConfigUiState(
val tunnelName: String = "",
@@ -13,6 +12,5 @@ data class ConfigUiState(
val showScripts: Boolean = false,
val isAuthenticated: Boolean = true,
val showAuthPrompt: Boolean = false,
val message: StringValue? = null,
val success: Boolean? = null,
val saveChanges: Boolean = false,
)
@@ -22,16 +22,24 @@ import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components.AdvancedSettingsItem
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.*
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
import com.zaneschepke.wireguardautotunnel.ui.state.AppViewState
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
@Composable
fun SettingsScreen(uiState: AppUiState, viewModel: AppViewModel) {
fun SettingsScreen(uiState: AppUiState, appViewState: AppViewState, viewModel: AppViewModel) {
val isTv = LocalIsAndroidTV.current
val focusManager = LocalFocusManager.current
val navController = LocalNavController.current
val interactionSource = remember { MutableInteractionSource() }
when (appViewState.bottomSheet) {
AppViewState.BottomSheet.BACKUP_AND_RESTORE -> {
SettingsBottomSheet(viewModel)
}
else -> Unit
}
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.Top),
@@ -0,0 +1,78 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Restore
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.MainActivity
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.state.AppViewState
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsBottomSheet(viewModel: AppViewModel) {
val context = LocalContext.current
ModalBottomSheet(
containerColor = MaterialTheme.colorScheme.surface,
onDismissRequest = {
viewModel.handleEvent(AppEvent.SetBottomSheet(AppViewState.BottomSheet.NONE))
},
) {
Row(
modifier =
Modifier.fillMaxWidth()
.clickable {
viewModel.handleEvent(
AppEvent.SetBottomSheet(AppViewState.BottomSheet.NONE)
)
(context as? MainActivity)?.performBackup()
}
.padding(10.dp)
) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.database),
contentDescription = null,
modifier = Modifier.padding(10.dp),
)
Text(
text = stringResource(R.string.backup_application),
modifier = Modifier.padding(10.dp),
)
}
HorizontalDivider()
Row(
modifier =
Modifier.fillMaxWidth()
.clickable {
viewModel.handleEvent(
AppEvent.SetBottomSheet(AppViewState.BottomSheet.NONE)
)
(context as? MainActivity)?.performRestore()
}
.padding(10.dp)
) {
Icon(
imageVector = Icons.Outlined.Restore,
contentDescription = null,
modifier = Modifier.padding(10.dp),
)
Text(
text = stringResource(R.string.restore_application),
modifier = Modifier.padding(10.dp),
)
}
}
}
@@ -22,6 +22,7 @@ data class AppViewState(
}
enum class BottomSheet {
BACKUP_AND_RESTORE,
EXPORT_TUNNELS,
IMPORT_TUNNELS,
LOGS,
@@ -1,5 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.state
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import org.amnezia.awg.config.Config
data class ConfigProxy(val peers: List<PeerProxy>, val `interface`: InterfaceProxy) {
@@ -28,6 +29,20 @@ 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 {
fun from(amConfig: Config): ConfigProxy {
return ConfigProxy(
@@ -8,7 +8,6 @@ import android.content.Context.POWER_SERVICE
import android.content.Intent
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.location.LocationManager
import android.net.Uri
import android.os.Build
import android.os.PowerManager
@@ -16,17 +15,15 @@ import android.provider.Settings
import android.service.quicksettings.TileService
import android.widget.Toast
import androidx.core.content.FileProvider
import androidx.core.location.LocationManagerCompat
import androidx.core.net.toUri
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import com.zaneschepke.wireguardautotunnel.MainActivity
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.core.service.tile.AutoTunnelControlTile
import com.zaneschepke.wireguardautotunnel.core.service.tile.TunnelControlTile
import com.zaneschepke.wireguardautotunnel.util.Constants
import java.io.File
import java.io.InputStream
import kotlin.system.exitProcess
import timber.log.Timber
fun Context.openWebUrl(url: String): Result<Unit> {
@@ -113,15 +110,14 @@ fun Context.launchShareFile(file: Uri) {
this.startActivity(chooserIntent)
}
fun Context.isLocationServicesEnabled(): Boolean {
val locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager
return LocationManagerCompat.isLocationEnabled(locationManager)
}
fun Context.showToast(resId: Int) {
Toast.makeText(this, this.getString(resId), Toast.LENGTH_LONG).show()
}
fun Context.showToast(message: String) {
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
}
fun Context.launchSupportEmail() {
val intent =
Intent(Intent.ACTION_SENDTO).apply {
@@ -239,17 +235,9 @@ fun Activity.setScreenBrightness(brightness: Float) {
window.attributes = window.attributes.apply { screenBrightness = brightness }
}
fun Activity.enableImmersiveMode() {
WindowCompat.setDecorFitsSystemWindows(window, false)
val controller = WindowCompat.getInsetsController(window, window.decorView)
controller.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
controller.hide(WindowInsetsCompat.Type.systemBars())
}
fun Activity.disableImmersiveMode() {
WindowCompat.setDecorFitsSystemWindows(window, true)
val controller = WindowCompat.getInsetsController(window, window.decorView)
controller.show(WindowInsetsCompat.Type.systemBars())
window.statusBarColor = android.graphics.Color.TRANSPARENT
fun MainActivity.restartApp() {
Intent(this, MainActivity::class.java).also {
startActivity(it)
exitProcess(0)
}
}
@@ -2,7 +2,7 @@ package com.zaneschepke.wireguardautotunnel.util.extensions
import androidx.compose.ui.graphics.Color
import com.wireguard.android.backend.BackendException
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendStatus
import com.zaneschepke.wireguardautotunnel.domain.enums.HandshakeStatus
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.events.BackendError
@@ -75,12 +75,21 @@ fun Config.defaultName(): String {
}
}
fun Backend.BackendState.asBackendState(): BackendState {
return BackendState.valueOf(this.name)
fun Backend.BackendStatus.asBackendStatus(): BackendStatus {
return when (val status = this) {
is Backend.BackendStatus.KillSwitchActive ->
BackendStatus.KillSwitch(status.allowedIps.toList())
is Backend.BackendStatus.ServiceActive -> BackendStatus.Active
else -> BackendStatus.Inactive
}
}
fun BackendState.asAmBackendState(): Backend.BackendState {
return Backend.BackendState.valueOf(this.name)
fun BackendStatus.asAmBackendStatus(): Backend.BackendStatus {
return when (val status = this) {
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 {
@@ -19,7 +19,7 @@ import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.AppShell
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.di.MainDispatcher
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendStatus
import com.zaneschepke.wireguardautotunnel.domain.enums.ConfigType
import com.zaneschepke.wireguardautotunnel.domain.events.BackendError
import com.zaneschepke.wireguardautotunnel.domain.model.AppSettings
@@ -42,7 +42,7 @@ import java.util.*
import javax.inject.Inject
import javax.inject.Provider
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@@ -70,12 +70,7 @@ constructor(
private var logsJob: Job? = null
private val _eventFlow =
MutableSharedFlow<AppEvent>(
replay = 0,
extraBufferCapacity = 10,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
private val _eventChannel = Channel<AppEvent>(Channel.BUFFERED)
private val tunnelMutex = Mutex()
private val settingsMutex = Mutex()
@@ -139,7 +134,7 @@ constructor(
if (state.appState.isLocalLogsEnabled) logsJob = startCollectingLogs()
handleTunnelMessages()
}
_eventFlow.collect { event ->
for (event in _eventChannel) {
val state = uiState.value
when (event) {
AppEvent.ToggleLocalLogging ->
@@ -153,8 +148,7 @@ constructor(
is AppEvent.ImportTunnelFromClipboard ->
handleClipboardImport(event.text, state.tunnels)
is AppEvent.ImportTunnelFromFile ->
handleImportTunnelFromFile(event.data, state.tunnels)
is AppEvent.ImportTunnelFromFile -> handleImportTunnelFromFile(event.data)
is AppEvent.ImportTunnelFromUrl ->
handleImportTunnelFromUrl(event.url, state.tunnels)
@@ -247,6 +241,7 @@ constructor(
is AppEvent.SaveAllConfigs -> saveAllTunnels(event.tunnels)
AppEvent.ToggleShowDetailedPingStats ->
handleToggleShowDetailedPingStats(state.appState)
is AppEvent.SaveMonitoringSettings ->
handleMonitoringSaveChanges(
state.appSettings,
@@ -258,24 +253,60 @@ constructor(
AppEvent.TogglePingMonitoring -> handleTogglePingMonitoring(state.appSettings)
is AppEvent.SetPingAttempts ->
saveSettings(state.appSettings.copy(tunnelPingAttempts = event.count))
is AppEvent.SetPingInterval ->
saveSettings(
state.appSettings.copy(tunnelPingIntervalSeconds = event.interval)
)
is AppEvent.SetPingTimeout ->
saveSettings(
state.appSettings.copy(tunnelPingTimeoutSeconds = event.timeout)
)
is AppEvent.SaveTunnelUniquely -> saveTunnelsUniquely(listOf(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 =
viewModelScope.launch(mainDispatcher) { _uiEvent.emit(event) }
fun handleEvent(event: AppEvent) {
_eventFlow.tryEmit(event)
_eventChannel.trySend(event)
}
private suspend fun handleTogglePingMonitoring(appSettings: AppSettings) {
@@ -421,7 +452,7 @@ constructor(
_appViewState.update { it.copy(bottomSheet = bottomSheet) }
private suspend fun handleTunnelPingTargetChange(tunnelConf: TunnelConf, target: String) =
saveTunnel(tunnelConf.copy(pingTarget = target))
saveTunnel(tunnelConf.copy(pingTarget = target.ifBlank { null }))
private suspend fun handleTogglePingTunnel(tunnel: TunnelConf) =
saveTunnel(tunnel.copy(restartOnPingFailure = !tunnel.restartOnPingFailure))
@@ -517,17 +548,10 @@ constructor(
_appViewState.update { it.copy(popBackStack = true) }
}
private suspend fun handleImportTunnelFromFile(uri: Uri, tunnels: List<TunnelConf>) {
private suspend fun handleImportTunnelFromFile(uri: Uri) {
runCatching {
val tunnelConfigs = fileUtils.buildTunnelsFromUri(uri)
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)
saveTunnelsUniquely(tunnelConfigs)
}
.onFailure {
when (it) {
@@ -546,11 +570,7 @@ constructor(
runCatching {
val amConfig = TunnelConf.configFromAmQuick(config)
val tunnelConf = TunnelConf.tunnelConfigFromAmConfig(amConfig)
saveTunnel(
tunnelConf.copy(
tunName = tunnelConf.generateUniqueName(tunnels.map { it.tunName })
)
)
saveTunnelsUniquely(listOf(tunnelConf))
}
.onFailure {
Timber.e(it)
@@ -568,11 +588,7 @@ constructor(
url.openStream().use { stream ->
val amConfig = Config.parse(stream)
val tunnelConf = TunnelConf.tunnelConfigFromAmConfig(amConfig)
saveTunnel(
tunnelConf.copy(
tunName = tunnelConf.generateUniqueName(tunnels.map { it.tunName })
)
)
saveTunnelsUniquely(listOf(tunnelConf))
}
}
.onFailure {
@@ -650,19 +666,19 @@ constructor(
val updatedSettings =
appSettings.copy(isLanOnKillSwitchEnabled = !appSettings.isLanOnKillSwitchEnabled)
saveSettings(updatedSettings)
handleKillSwitchChange(appSettings)
handleKillSwitchChange(updatedSettings)
}
private fun handleKillSwitchChange(appSettings: AppSettings) {
// let auto tunnel handle kill switch changes if running
if (uiState.value.isAutoTunnelActive) return
if (!appSettings.isVpnKillSwitchEnabled)
return tunnelManager.setBackendState(BackendState.SERVICE_ACTIVE, emptyList())
return tunnelManager.setBackendStatus(BackendStatus.Active)
Timber.d("Starting kill switch")
val allowedIps =
if (appSettings.isLanOnKillSwitchEnabled) TunnelConf.LAN_BYPASS_ALLOWED_IPS
else emptyList()
tunnelManager.setBackendState(BackendState.KILL_SWITCH_ACTIVE, allowedIps)
tunnelManager.setBackendStatus(BackendStatus.KillSwitch(allowedIps))
}
private suspend fun handleToggleAppShortcuts(appSettings: AppSettings) {
@@ -697,7 +713,7 @@ constructor(
}
if (enabled && !requestRoot()) return
// disable kill switch feature in kernel mode
tunnelManager.setBackendState(BackendState.INACTIVE, emptyList())
tunnelManager.setBackendStatus(BackendStatus.Inactive)
saveSettings(
appSettings.copy(
isKernelEnabled = enabled,
@@ -77,6 +77,8 @@ sealed class AppEvent {
data class SetTheme(val theme: Theme) : AppEvent()
data class SaveTunnelUniquely(val tunnel: TunnelConf) : AppEvent()
data class SaveMonitoringSettings(
val pingInterval: Int,
val tunnelPingAttempts: Int,
+9
View File
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M480,840q-151,0 -255.5,-46.5T120,680v-400q0,-66 105.5,-113T480,120q149,0 254.5,47T840,280v400q0,67 -104.5,113.5T480,840ZM480,361q89,0 179,-25.5T760,281q-11,-29 -100.5,-55T480,200q-91,0 -178.5,25.5T200,281q14,30 101.5,55T480,361ZM480,560q42,0 81,-4t74.5,-11.5q35.5,-7.5 67,-18.5t57.5,-25v-120q-26,14 -57.5,25t-67,18.5Q600,432 561,436t-81,4q-42,0 -82,-4t-75.5,-11.5Q287,417 256,406t-56,-25v120q25,14 56,25t66.5,18.5Q358,552 398,556t82,4ZM480,760q46,0 93.5,-7t87.5,-18.5q40,-11.5 67,-26t32,-29.5v-98q-26,14 -57.5,25t-67,18.5Q600,632 561,636t-81,4q-42,0 -82,-4t-75.5,-11.5Q287,617 256,606t-56,-25v99q5,15 31.5,29t66.5,25.5q40,11.5 88,18.5t94,7Z"
android:fillColor="#e3e3e3"/>
</vector>
+7
View File
@@ -327,4 +327,11 @@
<string name="ping_target_template">Ping target: %1$s</string>
<string name="_true">True</string>
<string name="_false">False</string>
<string name="backup_success">Backup success. %1$s</string>
<string name="restore_success">Restore success. %1$s</string>
<string name="restarting_app">Restarting app to apply changes…</string>
<string name="restore_failed">Failed to restore from backup.</string>
<string name="backup_failed">Failed to create backup.</string>
<string name="backup_application">Backup application data</string>
<string name="restore_application">Restore from backup</string>
</resources>
-5
View File
@@ -1,6 +1,5 @@
object Constants {
const val VERSION_NAME = "3.9.4"
const val JVM_TARGET = "17"
const val VERSION_CODE = 39400
const val TARGET_SDK = 35
const val MIN_SDK = 26
@@ -10,8 +9,4 @@ object Constants {
// build types
const val RELEASE = "release"
const val NIGHTLY = "nightly"
val allowedLicenses = listOf("MIT", "Apache-2.0", "BSD-3-Clause")
val allowedLicenseUrls = listOf("https://github.com/journeyapps/zxing-android-embedded/blob/master/COPYING",
"https://github.com/RikkaApps/Shizuku-API/blob/master/LICENSE")
}
+9
View File
@@ -15,6 +15,15 @@ fun Project.languageList(): List<String> {
.toList() + "en"
}
fun allowedLicenses(): List<String> {
return listOf("MIT", "Apache-2.0", "BSD-3-Clause")
}
fun allowedLicenseUrls(): List<String> {
return listOf("https://github.com/journeyapps/zxing-android-embedded/blob/master/COPYING",
"https://github.com/RikkaApps/Shizuku-API/blob/master/LICENSE", "https://github.com/rafi0101/Android-Room-Database-Backup/blob/master/LICENSE",
"https://opensource.org/license/mit")
}
fun buildLanguagesArray(languages: List<String>): String {
return languages.joinToString(separator = ", ") { "\"$it\"" }
+16 -14
View File
@@ -1,38 +1,39 @@
[versions]
accompanist = "0.37.3"
activityCompose = "1.10.1"
amneziawgAndroid = "1.5.0"
androidx-junit = "1.2.1"
amneziawgAndroid = "1.6.1"
androidx-junit = "1.3.0"
icmp4a = "1.0.0"
roomdatabasebackup = "1.1.0"
shizuku = "13.1.5"
appcompat = "1.7.1"
biometricKtx = "1.2.0-alpha05"
coreKtx = "1.16.0"
datastorePreferences = "1.2.0-alpha02"
desugar_jdk_libs = "2.1.5"
espressoCore = "3.6.1"
hiltAndroid = "2.56.2"
espressoCore = "3.7.0"
hiltAndroid = "2.57"
hiltCompiler = "1.2.0"
junit = "4.13.2"
kotlinx-serialization-json = "1.8.1"
ktorClientCore = "3.1.3"
kotlinx-serialization-json = "1.9.0"
ktorClientCore = "3.2.3"
lifecycle-runtime-compose = "2.9.2"
material3 = "1.3.2"
navigationCompose = "2.9.0"
navigationCompose = "2.9.3"
pinLockCompose = "1.0.4"
qrose = "1.0.1"
roomVersion = "2.7.1"
roomVersion = "2.7.2"
semver4j = "3.1.0"
slf4jAndroid = "1.7.36"
timber = "5.0.1"
tunnel = "1.4.0"
androidGradlePlugin = "8.10.1"
androidGradlePlugin = "8.12.0"
kotlin = "2.2.0"
ksp = "2.2.0-2.0.2"
composeBom = "2025.06.00"
compose = "1.8.2"
composeBom = "2025.07.00"
compose = "1.8.3"
icons = "1.7.8"
workRuntimeKtxVersion = "2.10.1"
workRuntimeKtxVersion = "2.10.3"
zxingAndroidEmbedded = "4.3.0"
coreSplashscreen = "1.0.1"
gradlePlugins-grgit = "5.3.2"
@@ -40,8 +41,8 @@ reorderable = "2.5.1"
#plugins
material = "1.12.0"
storage = "1.5.0"
ktfmt = "0.22.0"
storage = "1.6.0"
ktfmt = "0.23.0"
licensee = "1.13.0"
@@ -104,6 +105,7 @@ material-icons-extended = { module = "androidx.compose.material:material-icons-e
# util
pin-lock-compose = { module = "com.zaneschepke:pin_lock_compose", version.ref = "pinLockCompose" }
roomdatabasebackup = { module = "de.raphaelebner:roomdatabasebackup", version.ref = "roomdatabasebackup" }
shizuku-api = { module = "dev.rikka.shizuku:api", version.ref = "shizuku" }
shizuku-provider = { module = "dev.rikka.shizuku:provider", version.ref = "shizuku" }
qrose = { module = "io.github.alexzhirkevich:qrose", version.ref = "qrose" }
+2 -2
View File
@@ -1,8 +1,8 @@
#Wed Oct 11 22:39:21 EDT 2023
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
distributionSha256Sum=f397b287023acdba1e9f6fc5ea72d22dd63669d59ed4a289a29b1a76eee151c6
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
distributionSha256Sum=20f1b1176237254a6fc204d8434196fa11a4cfb387567519c61556e8710aed78
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists