Compare commits

...

12 Commits

Author SHA1 Message Date
Zane Schepke c153be6f9f more fixes 2025-02-23 17:17:31 -05:00
Zane Schepke b2f8384862 another 2025-02-23 17:14:30 -05:00
Zane Schepke 4415834b64 another 2025-02-23 17:06:21 -05:00
Zane Schepke cbdfef5daf fix: r8 2025-02-23 16:58:19 -05:00
Zane Schepke 9da9e3cc65 try again 2025-02-23 16:24:05 -05:00
Zane Schepke ad379bd9a9 fix: proguard 2025-02-23 16:13:51 -05:00
Zane Schepke 8021c133a5 fix: proguard 2025-02-23 15:58:08 -05:00
Zane Schepke 6009445a15 bump tunnel deps 2025-02-23 15:16:12 -05:00
Zane Schepke f80af9dd5e fix: tunnel state bug 2025-02-22 23:21:25 -05:00
Zane Schepke 3f912ed532 fix: tunnel change restart logic 2025-02-22 13:07:15 -05:00
Zane Schepke 13bb300f20 feat: add worker to restart killed services 2025-02-21 21:16:10 -05:00
Zane Schepke 1375a468fd feat: add export as amnezia or wg 2025-02-21 17:26:51 -05:00
29 changed files with 339 additions and 103 deletions
+6 -2
View File
@@ -172,8 +172,7 @@ dependencies {
debugImplementation(libs.androidx.compose.ui.tooling) debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.manifest) debugImplementation(libs.androidx.compose.manifest)
// get tunnel lib from github packages or mavenLocal // tunnel
// implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar"))))
implementation(libs.tunnel) implementation(libs.tunnel)
implementation(libs.amneziawg.android) implementation(libs.amneziawg.android)
coreLibraryDesugaring(libs.desugar.jdk.libs) coreLibraryDesugaring(libs.desugar.jdk.libs)
@@ -188,6 +187,7 @@ dependencies {
// hilt // hilt
implementation(libs.hilt.android) implementation(libs.hilt.android)
ksp(libs.hilt.android.compiler) ksp(libs.hilt.android.compiler)
ksp(libs.androidx.hilt.compiler)
// accompanist // accompanist
implementation(libs.accompanist.permissions) implementation(libs.accompanist.permissions)
@@ -221,6 +221,10 @@ dependencies {
// splash // splash
implementation(libs.androidx.core.splashscreen) implementation(libs.androidx.core.splashscreen)
// worker
implementation(libs.androidx.work.runtime)
implementation(libs.androidx.hilt.work)
} }
fun determineVersionName(): String { fun determineVersionName(): String {
+35 -1
View File
@@ -2,4 +2,38 @@
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite { -keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
<fields>; <fields>;
} }
# Keep all classes in the org.xbill.DNS package and subpackages
-keep class org.xbill.DNS.** { *; }
-dontwarn org.xbill.DNS.**
# Preserve JNA classes if used (e.g., for IPHlpAPI on Windows)
-keep class com.sun.jna.** { *; }
-dontwarn com.sun.jna.**
# Keep DNS resolver configuration classes that might be loaded dynamically
-keep class org.xbill.DNS.config.** { *; }
-dontwarn org.xbill.DNS.config.**
-keep class org.xbill.DNS.** { *; }
# Prevent optimization issues with native or reflection-based calls
-dontoptimize
-dontshrink
# Uncomment the above if errors persist, but use sparingly as theyre broad
# Suppress warnings about missing classes if not all features are used
-dontwarn java.lang.management.**
-dontwarn sun.nio.ch.**
-keep class com.google.api.client.http.** { *; }
-dontwarn com.google.api.client.http.**
# Keep Joda-Time classes used by Tink
-keep class org.joda.time.** { *; }
-dontwarn org.joda.time.**
-keep class org.slf4j.** { *; }
-dontwarn org.slf4j.**
+34 -1
View File
@@ -21,4 +21,37 @@
#-renamesourcefileattribute SourceFile #-renamesourcefileattribute SourceFile
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite { -keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
<fields>; <fields>;
} }
# Keep all classes in the org.xbill.DNS package and subpackages
-keep class org.xbill.DNS.** { *; }
-dontwarn org.xbill.DNS.**
# Preserve JNA classes if used (e.g., for IPHlpAPI on Windows)
-keep class com.sun.jna.** { *; }
-dontwarn com.sun.jna.**
# Keep DNS resolver configuration classes that might be loaded dynamically
-keep class org.xbill.DNS.config.** { *; }
-dontwarn org.xbill.DNS.config.**
-keep class org.xbill.DNS.** { *; }
# Prevent optimization issues with native or reflection-based calls
-dontoptimize
-dontshrink
# Uncomment the above if errors persist, but use sparingly as theyre broad
# Suppress warnings about missing classes if not all features are used
-dontwarn java.lang.management.**
-dontwarn sun.nio.ch.**
-keep class com.google.api.client.http.** { *; }
-dontwarn com.google.api.client.http.**
# Keep Joda-Time classes used by Tink
-keep class org.joda.time.** { *; }
-dontwarn org.joda.time.**
-keep class org.slf4j.** { *; }
-dontwarn org.slf4j.**
+7
View File
@@ -106,6 +106,13 @@
android:resource="@xml/file_paths" /> android:resource="@xml/file_paths" />
</provider> </provider>
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:multiprocess="true"
tools:node="remove">
</provider>
<service <service
android:name=".core.service.tile.TunnelControlTile" android:name=".core.service.tile.TunnelControlTile"
android:exported="true" android:exported="true"
@@ -40,6 +40,7 @@ import androidx.navigation.compose.rememberNavController
import androidx.navigation.toRoute import androidx.navigation.toRoute
import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.core.worker.ServiceWorker
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.ui.Route import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
@@ -66,7 +67,6 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.support.LogsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject import javax.inject.Inject
@@ -120,20 +120,16 @@ class MainActivity : AppCompatActivity() {
viewModel.getEmitSplitTunnelApps(this@MainActivity) viewModel.getEmitSplitTunnelApps(this@MainActivity)
} }
LaunchedEffect(appUiState.autoTunnelActive) {
requestAutoTunnelTileServiceUpdate()
}
with(appUiState.appSettings) { with(appUiState.appSettings) {
LaunchedEffect(isAutoTunnelEnabled) {
this@MainActivity.requestAutoTunnelTileServiceUpdate()
}
LaunchedEffect(isShortcutsEnabled) { LaunchedEffect(isShortcutsEnabled) {
if (!isShortcutsEnabled) return@LaunchedEffect shortcutManager.removeShortcuts() if (!isShortcutsEnabled) return@LaunchedEffect shortcutManager.removeShortcuts()
shortcutManager.addShortcuts() shortcutManager.addShortcuts()
} }
} }
// TODO could improve this to cancel when no tuns or autotun on
ServiceWorker.start(this)
CompositionLocalProvider(LocalNavController provides navController) { CompositionLocalProvider(LocalNavController provides navController) {
SnackbarControllerProvider { host -> SnackbarControllerProvider { host ->
WireguardAutoTunnelTheme(theme = appUiState.generalState.theme) { WireguardAutoTunnelTheme(theme = appUiState.generalState.theme) {
@@ -218,14 +214,14 @@ class MainActivity : AppCompatActivity() {
composable<Route.Logs> { composable<Route.Logs> {
LogsScreen() LogsScreen()
} }
composable<Route.Config> { composable<Route.Config> { backStack ->
val args = it.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, viewModel) ConfigScreen(config, viewModel)
} }
composable<Route.TunnelOptions> { composable<Route.TunnelOptions> { backStack ->
val args = it.toRoute<Route.TunnelOptions>() val args = backStack.toRoute<Route.TunnelOptions>()
val config = appUiState.tunnels.first { it.id == args.id } val config = appUiState.tunnels.first { it.id == args.id }
OptionsScreen(config) OptionsScreen(config)
} }
@@ -238,13 +234,13 @@ class MainActivity : AppCompatActivity() {
composable<Route.KillSwitch> { composable<Route.KillSwitch> {
KillSwitchScreen(appUiState, viewModel) KillSwitchScreen(appUiState, viewModel)
} }
composable<Route.SplitTunnel> { composable<Route.SplitTunnel> { backStack ->
val args = it.toRoute<Route.SplitTunnel>() val args = backStack.toRoute<Route.SplitTunnel>()
val config = appUiState.tunnels.first { it.id == args.id } val config = appUiState.tunnels.first { it.id == args.id }
SplitTunnelScreen(config, viewModel) SplitTunnelScreen(config, viewModel)
} }
composable<Route.TunnelAutoTunnel> { composable<Route.TunnelAutoTunnel> { backStack ->
val args = it.toRoute<Route.TunnelOptions>() val args = backStack.toRoute<Route.TunnelOptions>()
val config = appUiState.tunnels.first { it.id == args.id } val config = appUiState.tunnels.first { it.id == args.id }
TunnelAutoTunnelScreen(config, appUiState.appSettings) TunnelAutoTunnelScreen(config, appUiState.appSettings)
} }
@@ -3,9 +3,11 @@ package com.zaneschepke.wireguardautotunnel
import android.app.Application import android.app.Application
import android.os.StrictMode import android.os.StrictMode
import android.os.StrictMode.ThreadPolicy import android.os.StrictMode.ThreadPolicy
import androidx.hilt.work.HiltWorkerFactory
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner
import androidx.work.Configuration
import com.wireguard.android.backend.GoBackend import com.wireguard.android.backend.GoBackend
import com.zaneschepke.logcatter.LogReader import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
@@ -26,7 +28,15 @@ import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@HiltAndroidApp @HiltAndroidApp
class WireGuardAutoTunnel : Application() { class WireGuardAutoTunnel : Application(), Configuration.Provider {
@Inject
lateinit var workerFactory: HiltWorkerFactory
override val workManagerConfiguration: Configuration
get() = Configuration.Builder()
.setWorkerFactory(workerFactory)
.build()
@Inject @Inject
@ApplicationScope @ApplicationScope
@@ -36,7 +36,7 @@ class AppUpdateReceiver : BroadcastReceiver() {
with(appDataRepository.settings.get()) { with(appDataRepository.settings.get()) {
if (isRestoreOnBootEnabled) { if (isRestoreOnBootEnabled) {
// If auto tunnel is enabled, just start it and let auto tunnel start appropriate tun // If auto tunnel is enabled, just start it and let auto tunnel start appropriate tun
if (isAutoTunnelEnabled) return@launch serviceManager.startAutoTunnel(true) if (isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) return@launch serviceManager.startAutoTunnel(true)
tunnelManager.restorePreviousState() tunnelManager.restorePreviousState()
} }
} }
@@ -35,7 +35,7 @@ class BootReceiver : BroadcastReceiver() {
with(appDataRepository.settings.get()) { with(appDataRepository.settings.get()) {
if (isRestoreOnBootEnabled) { if (isRestoreOnBootEnabled) {
// If auto tunnel is enabled, just start it and let auto tunnel start appropriate tun // If auto tunnel is enabled, just start it and let auto tunnel start appropriate tun
if (isAutoTunnelEnabled) return@launch serviceManager.startAutoTunnel(true) if (isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) return@launch serviceManager.startAutoTunnel(true)
tunnelManager.restorePreviousState() tunnelManager.restorePreviousState()
} }
} }
@@ -32,8 +32,8 @@ class KernelReceiver : BroadcastReceiver() {
val action = intent.action ?: return val action = intent.action ?: return
applicationScope.launch { applicationScope.launch {
if (action == REFRESH_TUNNELS_ACTION) { if (action == REFRESH_TUNNELS_ACTION) {
tunnelManager.runningTunnelNames().forEach { tunnelManager.runningTunnelNames().forEach { name ->
val tunnel = tunnelRepository.findByTunnelName(it) val tunnel = tunnelRepository.findByTunnelName(name)
tunnel?.let { tunnel?.let {
tunnelRepository.save(it.copy(isActive = true)) tunnelRepository.save(it.copy(isActive = true))
} }
@@ -187,7 +187,7 @@ class AutoTunnelService : LifecycleService() {
buildNetworkState(it) buildNetworkState(it)
}.distinctUntilChanged(), }.distinctUntilChanged(),
) { double, networkState -> ) { double, networkState ->
AutoTunnelState(tunnelManager.activeTunnels().value, networkState, double.first, double.second) AutoTunnelState(tunnelManager.activeTunnels.value, networkState, double.first, double.second)
}.collect { state -> }.collect { state ->
autoTunnelStateFlow.update { autoTunnelStateFlow.update {
it.copy(activeTunnels = state.activeTunnels, networkState = state.networkState, settings = state.settings, tunnels = state.tunnels) it.copy(activeTunnels = state.activeTunnels, networkState = state.networkState, settings = state.settings, tunnels = state.tunnels)
@@ -52,9 +52,14 @@ class TunnelControlTile : TileService() {
} }
fun updateTileState() = applicationScope.launch { fun updateTileState() = applicationScope.launch {
if (appDataRepository.tunnels.getAll().isEmpty()) return@launch setUnavailable() val tunnels = appDataRepository.tunnels.getAll()
with(tunnelManager.activeTunnels().value) { if (tunnels.isEmpty()) return@launch setUnavailable()
if (isNotEmpty()) return@launch updateTile(if (size == 1) first().tunName else getString(R.string.multiple), true) with(tunnelManager.activeTunnels.value) {
if (isNotEmpty()) if (size == 1) {
tunnels.firstOrNull { it.id == keys.first() }?.let { return@launch updateTile(it.tunName, true) }
} else {
return@launch updateTile(getString(R.string.multiple), true)
}
} }
appDataRepository.getStartTunnelConfig()?.let { appDataRepository.getStartTunnelConfig()?.let {
updateTile(it.tunName, false) updateTile(it.tunName, false)
@@ -65,7 +70,7 @@ class TunnelControlTile : TileService() {
super.onClick() super.onClick()
unlockAndRun { unlockAndRun {
applicationScope.launch { applicationScope.launch {
if (tunnelManager.activeTunnels().value.isNotEmpty()) return@launch tunnelManager.stopTunnel() if (tunnelManager.activeTunnels.value.isNotEmpty()) return@launch tunnelManager.stopTunnel()
appDataRepository.getStartTunnelConfig()?.let { appDataRepository.getStartTunnelConfig()?.let {
tunnelManager.startTunnel(it) tunnelManager.startTunnel(it)
} }
@@ -14,6 +14,7 @@ import com.zaneschepke.wireguardautotunnel.domain.enums.BackendError
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState 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.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
@@ -45,6 +46,8 @@ open class BaseTunnel(
internal val tunnels = MutableStateFlow<List<TunnelConf>>(emptyList()) internal val tunnels = MutableStateFlow<List<TunnelConf>>(emptyList())
private val _activeTunnels = MutableStateFlow<Map<Int, TunnelState>>(emptyMap())
private val tunnelJobs = mutableMapOf<TunnelConf, Job>() private val tunnelJobs = mutableMapOf<TunnelConf, Job>()
private val isNetworkAvailable = AtomicBoolean(false) private val isNetworkAvailable = AtomicBoolean(false)
@@ -67,8 +70,9 @@ open class BaseTunnel(
removedItems.forEach { tun -> removedItems.forEach { tun ->
tunnelJobs[tun]?.cancelWithMessage("Canceling tunnel jobs for tunnel: ${tun.name}") tunnelJobs[tun]?.cancelWithMessage("Canceling tunnel jobs for tunnel: ${tun.name}")
tunnelJobs.remove(tun) tunnelJobs.remove(tun)
_activeTunnels.update { it - tun.id }
serviceManager.updateTunnelTile()
} }
serviceManager.updateTunnelTile()
} }
} }
} }
@@ -83,6 +87,9 @@ open class BaseTunnel(
launch { launch {
startTunnelConfigChangeJob(tunnel) startTunnelConfigChangeJob(tunnel)
} }
launch {
startStateJob(tunnel)
}
} }
override suspend fun bounceTunnel(tunnelConf: TunnelConf) { override suspend fun bounceTunnel(tunnelConf: TunnelConf) {
@@ -99,14 +106,13 @@ open class BaseTunnel(
return emptySet() return emptySet()
} }
override suspend fun activeTunnels(): StateFlow<List<TunnelConf>> { override val activeTunnels: StateFlow<Map<Int, TunnelState>>
return tunnels.asStateFlow() get() = _activeTunnels.asStateFlow()
}
override suspend fun startTunnel(tunnelConf: TunnelConf) { override suspend fun startTunnel(tunnelConf: TunnelConf) {
if (tunnels.value.any { it.id == tunnelConf.id }) return Timber.w("Tunnel already running")
serviceManager.startBackgroundService(tunnelConf) serviceManager.startBackgroundService(tunnelConf)
appDataRepository.tunnels.save(tunnelConf.copy(isActive = true)) appDataRepository.tunnels.save(tunnelConf.copy(isActive = true))
addToActiveTunnels(tunnelConf)
} }
override suspend fun stopTunnel(tunnelConf: TunnelConf?) { override suspend fun stopTunnel(tunnelConf: TunnelConf?) {
@@ -131,7 +137,7 @@ open class BaseTunnel(
} }
} }
internal fun addToActiveTunnels(conf: TunnelConf) { private fun addToActiveTunnels(conf: TunnelConf) {
tunnels.update { tunnels.update {
it.toMutableList().apply { it.toMutableList().apply {
add(conf) add(conf)
@@ -153,13 +159,22 @@ open class BaseTunnel(
} }
} }
private suspend fun startStateJob(tunnel: TunnelConf) {
tunnel.state.collect { state ->
_activeTunnels.update {
it + (tunnel.id to state)
}
serviceManager.updateTunnelTile()
}
}
private suspend fun startPingJob(tunnel: TunnelConf) = coroutineScope { private suspend fun startPingJob(tunnel: TunnelConf) = coroutineScope {
while (isActive) { while (isActive) {
if (isNetworkAvailable.get() && tunnel.isActive) { if (isNetworkAvailable.get() && tunnel.isActive) {
val pingResult = tunnel.pingTunnel(ioDispatcher) val pingResult = tunnel.pingTunnel(ioDispatcher)
handlePingResult(tunnel, pingResult) handlePingResult(tunnel, pingResult)
} }
delay(CHECK_INTERVAL) delay(tunnel.pingInterval ?: Constants.PING_INTERVAL)
} }
} }
@@ -200,7 +215,7 @@ open class BaseTunnel(
private suspend fun startTunnelConfigChangeJob(tunnel: TunnelConf) = coroutineScope { private suspend fun startTunnelConfigChangeJob(tunnel: TunnelConf) = coroutineScope {
appDataRepository.tunnels.flow.collect { storageTuns -> appDataRepository.tunnels.flow.collect { storageTuns ->
storageTuns.firstOrNull { it.id == tunnel.id }?.let { storageTun -> storageTuns.firstOrNull { it.id == tunnel.id }?.let { storageTun ->
if (tunnel.isQuickConfigChanged(storageTun) || tunnel.isPingConfigMatching(storageTun)) { if (!tunnel.isQuickConfigMatching(storageTun) || !tunnel.isPingConfigMatching(storageTun)) {
bounceTunnel(tunnel) bounceTunnel(tunnel)
} }
} }
@@ -33,10 +33,10 @@ class KernelTunnel @Inject constructor(
override suspend fun startTunnel(tunnelConf: TunnelConf) { override suspend fun startTunnel(tunnelConf: TunnelConf) {
withContext(ioDispatcher) { withContext(ioDispatcher) {
super.startTunnel(tunnelConf) if (tunnels.value.any { it.id == tunnelConf.id }) return@withContext Timber.w("Tunnel already running")
runCatching { runCatching {
backend.setState(tunnelConf, Tunnel.State.UP, tunnelConf.toWgConfig()) backend.setState(tunnelConf, Tunnel.State.UP, tunnelConf.toWgConfig())
addToActiveTunnels(tunnelConf) super.startTunnel(tunnelConf)
}.onFailure { }.onFailure {
onTunnelStop(tunnelConf) onTunnelStop(tunnelConf)
if (it is BackendException) { if (it is BackendException) {
@@ -66,8 +66,8 @@ class KernelTunnel @Inject constructor(
} }
} }
override suspend fun toggleTunnel(tunnelConf: TunnelConf, status: TunnelStatus) { override suspend fun toggleTunnel(tunnelConf: TunnelConf, state: TunnelStatus) {
when (status) { when (state) {
TunnelStatus.UP -> backend.setState(tunnelConf, Tunnel.State.UP, tunnelConf.toWgConfig()) TunnelStatus.UP -> backend.setState(tunnelConf, Tunnel.State.UP, tunnelConf.toWgConfig())
TunnelStatus.DOWN -> backend.setState(tunnelConf, Tunnel.State.DOWN, tunnelConf.toWgConfig()) TunnelStatus.DOWN -> backend.setState(tunnelConf, Tunnel.State.DOWN, tunnelConf.toWgConfig())
} }
@@ -11,10 +11,12 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.util.extensions.withData import com.zaneschepke.wireguardautotunnel.util.extensions.withData
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -34,14 +36,21 @@ class TunnelManager @Inject constructor(
initialValue = null, initialValue = null,
) )
override suspend fun activeTunnels(): StateFlow<List<TunnelConf>> { @OptIn(ExperimentalCoroutinesApi::class)
return withContext(ioDispatcher) { override val activeTunnels = appSettings
appSettings.filterNotNull().first().let { .filterNotNull()
if (it.isKernelEnabled) return@withContext kernelTunnel.activeTunnels() .flatMapLatest { settings ->
userspaceTunnel.activeTunnels() if (settings.isKernelEnabled) {
kernelTunnel.activeTunnels
} else {
userspaceTunnel.activeTunnels
} }
} }
} .stateIn(
scope = applicationScope,
started = SharingStarted.Eagerly,
initialValue = emptyMap(),
)
override suspend fun startTunnel(tunnelConf: TunnelConf) { override suspend fun startTunnel(tunnelConf: TunnelConf) {
appSettings.withData { appSettings.withData {
@@ -84,14 +93,14 @@ class TunnelManager @Inject constructor(
if (isRestoreOnBootEnabled) { if (isRestoreOnBootEnabled) {
val previouslyActiveTuns = appDataRepository.tunnels.getActive() val previouslyActiveTuns = appDataRepository.tunnels.getActive()
// handle kernel mode // handle kernel mode
val tunsToStart = previouslyActiveTuns.filterNot { tun -> activeTunnels().value.any { tun.id == it.id } } val tunsToStart = previouslyActiveTuns.filterNot { tun -> activeTunnels.value.any { tun.id == it.key } }
if (isKernelEnabled) { if (isKernelEnabled) {
return@withContext tunsToStart.forEach { return@withContext tunsToStart.forEach {
startTunnel(it) startTunnel(it)
} }
} }
// handle userspace // handle userspace
if (activeTunnels().value.isEmpty()) tunsToStart.firstOrNull()?.let { startTunnel(it) } if (activeTunnels.value.isEmpty()) tunsToStart.firstOrNull()?.let { startTunnel(it) }
} }
} }
} }
@@ -2,11 +2,12 @@ package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
interface TunnelProvider { interface TunnelProvider {
suspend fun activeTunnels(): StateFlow<List<TunnelConf>> val activeTunnels: StateFlow<Map<Int, TunnelState>>
suspend fun startTunnel(tunnelConf: TunnelConf) suspend fun startTunnel(tunnelConf: TunnelConf)
@@ -34,10 +34,13 @@ class UserspaceTunnel @Inject constructor(
override suspend fun startTunnel(tunnelConf: TunnelConf) { override suspend fun startTunnel(tunnelConf: TunnelConf) {
withContext(ioDispatcher) { withContext(ioDispatcher) {
super.startTunnel(tunnelConf) if (tunnels.value.any { it.id == tunnelConf.id }) return@withContext Timber.w("Tunnel already running")
if (tunnels.value.isNotEmpty()) {
stopAllTunnels()
}
runCatching { runCatching {
backend.setState(tunnelConf, Tunnel.State.UP, tunnelConf.toAmConfig()) backend.setState(tunnelConf, Tunnel.State.UP, tunnelConf.toAmConfig())
addToActiveTunnels(tunnelConf) super.startTunnel(tunnelConf)
}.onFailure { }.onFailure {
onTunnelStop(tunnelConf) onTunnelStop(tunnelConf)
if (it is BackendException) { if (it is BackendException) {
@@ -0,0 +1,60 @@
package com.zaneschepke.wireguardautotunnel.core.worker
import android.content.Context
import androidx.hilt.work.HiltWorker
import androidx.work.CoroutineWorker
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.util.concurrent.TimeUnit
@HiltWorker
class ServiceWorker @AssistedInject constructor(
@Assisted private val context: Context,
@Assisted private val params: WorkerParameters,
private val serviceManager: ServiceManager,
private val appDataRepository: AppDataRepository,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
private val tunnelManager: TunnelManager,
) : CoroutineWorker(context, params) {
companion object {
private const val TAG = "service_worker"
fun stop(context: Context) {
WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
}
fun start(context: Context) {
val periodicWorkRequest = PeriodicWorkRequestBuilder<ServiceWorker>(
repeatInterval = 15,
repeatIntervalTimeUnit = TimeUnit.MINUTES,
).build()
WorkManager.getInstance(context)
.enqueueUniquePeriodicWork(
TAG,
ExistingPeriodicWorkPolicy.KEEP,
periodicWorkRequest,
)
}
}
override suspend fun doWork(): Result = withContext(ioDispatcher) {
Timber.i("Service worker started")
with(appDataRepository.settings.get()) {
if (isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) return@with serviceManager.startAutoTunnel(true)
if (tunnelManager.activeTunnels.value.isEmpty()) tunnelManager.restorePreviousState()
}
Result.success()
}
}
@@ -62,9 +62,9 @@ data class TunnelConf(
} }
} }
fun isQuickConfigChanged(updatedConf: TunnelConf): Boolean { fun isQuickConfigMatching(updatedConf: TunnelConf): Boolean {
return updatedConf.wgQuick != wgQuick || return updatedConf.wgQuick == wgQuick ||
updatedConf.amQuick != amQuick updatedConf.amQuick == amQuick
} }
fun isPingConfigMatching(updatedConf: TunnelConf): Boolean { fun isPingConfigMatching(updatedConf: TunnelConf): Boolean {
@@ -0,0 +1,6 @@
package com.zaneschepke.wireguardautotunnel.domain.enums
enum class ConfigType {
AMNEZIA,
WG,
}
@@ -7,7 +7,7 @@ import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
import com.zaneschepke.wireguardautotunnel.util.extensions.isMatchingToWildcardList import com.zaneschepke.wireguardautotunnel.util.extensions.isMatchingToWildcardList
data class AutoTunnelState( data class AutoTunnelState(
val activeTunnels: List<TunnelConf> = emptyList(), val activeTunnels: Map<Int, TunnelState> = emptyMap(),
val networkState: NetworkState = NetworkState(), val networkState: NetworkState = NetworkState(),
val settings: AppSettings = AppSettings(), val settings: AppSettings = AppSettings(),
val tunnels: List<TunnelConf> = emptyList(), val tunnels: List<TunnelConf> = emptyList(),
@@ -20,12 +20,12 @@ data class AutoTunnelState(
private fun isMobileTunnelDataChangeNeeded(): Boolean { private fun isMobileTunnelDataChangeNeeded(): Boolean {
val preferredTunnel = preferredMobileDataTunnel() val preferredTunnel = preferredMobileDataTunnel()
return preferredTunnel != null && return preferredTunnel != null &&
activeTunnels.isNotEmpty() && !activeTunnels.any { it.id == preferredTunnel.id } activeTunnels.isNotEmpty() && !activeTunnels.any { it.key == preferredTunnel.id }
} }
private fun isEthernetTunnelChangeNeeded(): Boolean { private fun isEthernetTunnelChangeNeeded(): Boolean {
val preferredTunnel = preferredEthernetTunnel() val preferredTunnel = preferredEthernetTunnel()
return preferredTunnel != null && activeTunnels.isNotEmpty() && !activeTunnels.any { it.id == preferredTunnel.id } return preferredTunnel != null && activeTunnels.isNotEmpty() && !activeTunnels.any { it.key == preferredTunnel.id }
} }
private fun preferredMobileDataTunnel(): TunnelConf? { private fun preferredMobileDataTunnel(): TunnelConf? {
@@ -62,7 +62,7 @@ data class AutoTunnelState(
return settings.isVpnKillSwitchEnabled && (!settings.isDisableKillSwitchOnTrustedEnabled || !isCurrentSSIDTrusted()) return settings.isVpnKillSwitchEnabled && (!settings.isDisableKillSwitchOnTrustedEnabled || !isCurrentSSIDTrusted())
} }
fun isNoConnectivity(): Boolean { private fun isNoConnectivity(): Boolean {
return !networkState.isEthernetConnected && !networkState.isWifiConnected && !networkState.isMobileDataConnected return !networkState.isEthernetConnected && !networkState.isWifiConnected && !networkState.isMobileDataConnected
} }
@@ -100,7 +100,7 @@ data class AutoTunnelState(
private fun isWifiTunnelPreferred(): Boolean { private fun isWifiTunnelPreferred(): Boolean {
val preferred = preferredWifiTunnel() val preferred = preferredWifiTunnel()
return activeTunnels.any { it.id == preferred?.id } return activeTunnels.any { it.key == preferred?.id }
} }
fun asAutoTunnelEvent(): AutoTunnelEvent { fun asAutoTunnelEvent(): AutoTunnelEvent {
@@ -77,7 +77,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
var selectedTunnel by remember { mutableStateOf<TunnelConf?>(null) } var selectedTunnel by remember { mutableStateOf<TunnelConf?>(null) }
val isRunningOnTv = remember { context.isRunningOnTv() } val isRunningOnTv = remember { context.isRunningOnTv() }
val activeTunnels by viewModel.activeTunnels.collectAsStateWithLifecycle(emptyList()) val activeTunnels by viewModel.tunnelManager.activeTunnels.collectAsStateWithLifecycle(emptyMap())
val collator = Collator.getInstance(Locale.getDefault()) val collator = Collator.getInstance(Locale.getDefault())
@@ -89,6 +89,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
val startTunnel = withVpnPermission<TunnelConf> { val startTunnel = withVpnPermission<TunnelConf> {
viewModel.onTunnelStart(it) viewModel.onTunnelStart(it)
} }
val autoTunnelToggleBattery = withIgnoreBatteryOpt(uiState.generalState.isBatteryOptimizationDisableShown) { val autoTunnelToggleBattery = withIgnoreBatteryOpt(uiState.generalState.isBatteryOptimizationDisableShown) {
if (!uiState.generalState.isBatteryOptimizationDisableShown) viewModel.setBatteryOptimizeDisableShown() if (!uiState.generalState.isBatteryOptimizationDisableShown) viewModel.setBatteryOptimizeDisableShown()
if (uiState.appSettings.isKernelEnabled) { if (uiState.appSettings.isKernelEnabled) {
@@ -132,15 +133,8 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
} }
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConf) { fun onTunnelToggle(checked: Boolean, tunnel: TunnelConf) {
if (!checked) { if (!checked) return viewModel.onTunnelStop(tunnel).let { }
viewModel.onTunnelStop(tunnel) if (uiState.appSettings.isKernelEnabled) viewModel.onTunnelStart(tunnel) else startTunnel(tunnel)
return
}
if (uiState.appSettings.isKernelEnabled) {
viewModel.onTunnelStart(tunnel)
} else {
startTunnel.invoke(tunnel)
}
} }
Scaffold( Scaffold(
@@ -233,13 +227,13 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
key = { tunnel -> tunnel.id }, key = { tunnel -> tunnel.id },
) { tunnel -> ) { tunnel ->
val expanded = uiState.generalState.isTunnelStatsExpanded val expanded = uiState.generalState.isTunnelStatsExpanded
val tunnelState = activeTunnels.firstOrNull { it.id == tunnel.id }?.state?.collectAsStateWithLifecycle() val tunnelState = activeTunnels.getOrDefault(tunnel.id, TunnelState())
TunnelRowItem( TunnelRowItem(
tunnel.isActive, tunnelState.state.isUp(),
expanded, expanded,
selectedTunnel?.id == tunnel.id, selectedTunnel?.id == tunnel.id,
tunnel, tunnel,
tunnelState = tunnelState?.value ?: TunnelState(), tunnelState = tunnelState,
{ selectedTunnel = tunnel }, { selectedTunnel = tunnel },
{ viewModel.onExpandedChanged(!expanded) }, { viewModel.onExpandedChanged(!expanded) },
onDelete = { showDeleteTunnelAlertDialog = true }, onDelete = { showDeleteTunnelAlertDialog = true },
@@ -5,8 +5,9 @@ import androidx.compose.foundation.focusable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.layout.systemBarsPadding
@@ -15,6 +16,7 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ViewQuilt import androidx.compose.material.icons.automirrored.outlined.ViewQuilt
import androidx.compose.material.icons.filled.AppShortcut import androidx.compose.material.icons.filled.AppShortcut
import androidx.compose.material.icons.filled.FolderZip
import androidx.compose.material.icons.outlined.Bolt import androidx.compose.material.icons.outlined.Bolt
import androidx.compose.material.icons.outlined.Code import androidx.compose.material.icons.outlined.Code
import androidx.compose.material.icons.outlined.FolderZip import androidx.compose.material.icons.outlined.FolderZip
@@ -23,7 +25,11 @@ import androidx.compose.material.icons.outlined.Pin
import androidx.compose.material.icons.outlined.Restore import androidx.compose.material.icons.outlined.Restore
import androidx.compose.material.icons.outlined.VpnKeyOff import androidx.compose.material.icons.outlined.VpnKeyOff
import androidx.compose.material.icons.outlined.VpnLock import androidx.compose.material.icons.outlined.VpnLock
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -38,6 +44,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.domain.enums.ConfigType
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.Route import com.zaneschepke.wireguardautotunnel.ui.Route
@@ -56,7 +63,7 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
import com.zaneschepke.wireguardautotunnel.viewmodel.SettingsViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.SettingsViewModel
import xyz.teamgravity.pin_lock_compose.PinManager import xyz.teamgravity.pin_lock_compose.PinManager
@OptIn(ExperimentalLayoutApi::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel: AppViewModel, uiState: AppUiState) { fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel: AppViewModel, uiState: AppUiState) {
val context = LocalContext.current val context = LocalContext.current
@@ -68,11 +75,13 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
var showAuthPrompt by remember { mutableStateOf(false) } var showAuthPrompt by remember { mutableStateOf(false) }
var showExportSheet by remember { mutableStateOf(false) }
if (showAuthPrompt) { if (showAuthPrompt) {
AuthorizationPrompt( AuthorizationPrompt(
onSuccess = { onSuccess = {
showAuthPrompt = false showAuthPrompt = false
viewModel.exportAllConfigs(context) showExportSheet = true
}, },
onError = { _ -> onError = { _ ->
showAuthPrompt = false showAuthPrompt = false
@@ -89,6 +98,52 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
) )
} }
if (showExportSheet) {
ModalBottomSheet(onDismissRequest = { showExportSheet = false }) {
Row(
modifier =
Modifier
.fillMaxWidth()
.clickable {
showExportSheet = false
viewModel.exportAllConfigs(context, ConfigType.AMNEZIA)
}
.padding(10.dp),
) {
Icon(
Icons.Filled.FolderZip,
contentDescription = stringResource(id = R.string.export_amnezia),
modifier = Modifier.padding(10.dp),
)
Text(
stringResource(id = R.string.export_amnezia),
modifier = Modifier.padding(10.dp),
)
}
HorizontalDivider()
Row(
modifier =
Modifier
.fillMaxWidth()
.clickable {
showExportSheet = false
viewModel.exportAllConfigs(context, ConfigType.WG)
}
.padding(10.dp),
) {
Icon(
Icons.Filled.FolderZip,
contentDescription = stringResource(id = R.string.export_wireguard),
modifier = Modifier.padding(10.dp),
)
Text(
stringResource(id = R.string.export_wireguard),
modifier = Modifier.padding(10.dp),
)
}
}
}
Column( Column(
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top), verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top),
@@ -46,9 +46,9 @@ fun TunnelStatistics.PeerStats.handshakeStatus(): HandshakeStatus {
fun Peer.isReachable(preferIpv4: Boolean): Boolean { fun Peer.isReachable(preferIpv4: Boolean): Boolean {
val host = val host =
if (this.endpoint.isPresent && if (this.endpoint.isPresent &&
this.endpoint.get().getResolved(preferIpv4).isPresent this.endpoint.get().resolved.isPresent
) { ) {
this.endpoint.get().getResolved(preferIpv4).get().host this.endpoint.get().resolved.get().host
} else { } else {
Constants.DEFAULT_PING_IP Constants.DEFAULT_PING_IP
} }
@@ -109,8 +109,8 @@ constructor(
} }
private suspend fun initTunnels() { private suspend fun initTunnels() {
tunnels.withData { tunnels.withData { tunnels ->
it.filter { it.isActive }.forEach { tunnels.filter { it.isActive }.forEach {
tunnelManager.startTunnel(it) tunnelManager.startTunnel(it)
} }
} }
@@ -24,8 +24,6 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.toWgQuickString
import com.zaneschepke.wireguardautotunnel.util.extensions.withData import com.zaneschepke.wireguardautotunnel.util.extensions.withData
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.amnezia.awg.config.Config import org.amnezia.awg.config.Config
@@ -44,15 +42,6 @@ constructor(
appDataRepository: AppDataRepository, appDataRepository: AppDataRepository,
) : BaseViewModel(appDataRepository) { ) : BaseViewModel(appDataRepository) {
private val _activeTunnels = MutableStateFlow<List<TunnelConf>>(emptyList())
val activeTunnels = _activeTunnels.asStateFlow()
init {
viewModelScope.launch {
tunnelManager.activeTunnels().collect(_activeTunnels::emit)
}
}
fun onDelete(tunnel: TunnelConf) = viewModelScope.launch { fun onDelete(tunnel: TunnelConf) = viewModelScope.launch {
appSettings.withData { settings -> appSettings.withData { settings ->
tunnels.withData { tunnels.withData {
@@ -5,11 +5,13 @@ import androidx.core.content.FileProvider
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.domain.enums.ConfigType
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.util.FileUtils import com.zaneschepke.wireguardautotunnel.util.FileUtils
import com.zaneschepke.wireguardautotunnel.util.extensions.launchShareFile import com.zaneschepke.wireguardautotunnel.util.extensions.launchShareFile
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber
import java.time.Instant import java.time.Instant
import javax.inject.Inject import javax.inject.Inject
@@ -21,16 +23,23 @@ constructor(
private val fileUtils: FileUtils, private val fileUtils: FileUtils,
) : ViewModel() { ) : ViewModel() {
fun exportAllConfigs(context: Context) = viewModelScope.launch { fun exportAllConfigs(context: Context, configType: ConfigType) = viewModelScope.launch {
runCatching { runCatching {
val shareFile = fileUtils.createNewShareFile("wg-export_${Instant.now().epochSecond}.zip")
val tunnels = appDataRepository.tunnels.getAll() val tunnels = appDataRepository.tunnels.getAll()
val wgFiles = fileUtils.createWgFiles(tunnels) val (files, shareFileName) = when (configType) {
val amFiles = fileUtils.createAmFiles(tunnels) ConfigType.AMNEZIA -> {
val allFiles = wgFiles + amFiles Pair(fileUtils.createAmFiles(tunnels), "am-export_${Instant.now().epochSecond}.zip")
fileUtils.zipAll(shareFile, allFiles) }
ConfigType.WG -> {
Pair(fileUtils.createWgFiles(tunnels), "wg-export_${Instant.now().epochSecond}.zip")
}
}
val shareFile = fileUtils.createNewShareFile(shareFileName)
fileUtils.zipAll(shareFile, files)
val uri = FileProvider.getUriForFile(context, context.getString(R.string.provider), shareFile) val uri = FileProvider.getUriForFile(context, context.getString(R.string.provider), shareFile)
context.launchShareFile(uri) context.launchShareFile(uri)
}.onFailure {
Timber.e(it)
} }
} }
} }
+2
View File
@@ -213,4 +213,6 @@
<string name="unauthorized">Failed to start tunnel, unauthorized.</string> <string name="unauthorized">Failed to start tunnel, unauthorized.</string>
<string name="tunne_start_failed_title">Tunnel failure</string> <string name="tunne_start_failed_title">Tunnel failure</string>
<string name="multiple">Multiple</string> <string name="multiple">Multiple</string>
<string name="export_amnezia">Export as Amnezia</string>
<string name="export_wireguard">Export as WireGuard</string>
</resources> </resources>
+8 -4
View File
@@ -1,7 +1,7 @@
[versions] [versions]
accompanist = "0.37.0" accompanist = "0.37.0"
activityCompose = "1.10.0" activityCompose = "1.10.0"
amneziawgAndroid = "1.2.6" amneziawgAndroid = "1.2.8"
androidx-junit = "1.2.1" androidx-junit = "1.2.1"
appcompat = "1.7.0" appcompat = "1.7.0"
biometricKtx = "1.2.0-alpha05" biometricKtx = "1.2.0-alpha05"
@@ -10,7 +10,7 @@ datastorePreferences = "1.1.2"
desugar_jdk_libs = "2.1.4" desugar_jdk_libs = "2.1.4"
espressoCore = "3.6.1" espressoCore = "3.6.1"
hiltAndroid = "2.55" hiltAndroid = "2.55"
hiltNavigationCompose = "1.2.0" hiltCompiler = "1.2.0"
junit = "4.13.2" junit = "4.13.2"
kotlinx-serialization-json = "1.8.0" kotlinx-serialization-json = "1.8.0"
lifecycle-runtime-compose = "2.8.7" lifecycle-runtime-compose = "2.8.7"
@@ -19,12 +19,13 @@ navigationCompose = "2.8.7"
pinLockCompose = "1.0.4" pinLockCompose = "1.0.4"
roomVersion = "2.6.1" roomVersion = "2.6.1"
timber = "5.0.1" timber = "5.0.1"
tunnel = "1.2.2" tunnel = "1.2.4"
androidGradlePlugin = "8.8.0-alpha05" androidGradlePlugin = "8.8.0-alpha05"
kotlin = "2.1.10" kotlin = "2.1.10"
ksp = "2.1.10-1.0.30" ksp = "2.1.10-1.0.30"
composeBom = "2025.02.00" composeBom = "2025.02.00"
compose = "1.7.8" compose = "1.7.8"
workRuntimeKtxVersion = "2.10.0"
zxingAndroidEmbedded = "4.3.0" zxingAndroidEmbedded = "4.3.0"
coreSplashscreen = "1.0.1" coreSplashscreen = "1.0.1"
gradlePlugins-grgit = "5.3.0" gradlePlugins-grgit = "5.3.0"
@@ -45,6 +46,7 @@ amneziawg-android = { module = "com.zaneschepke:amneziawg-android", version.ref
androidx-biometric-ktx = { module = "androidx.biometric:biometric-ktx", version.ref = "biometricKtx" } androidx-biometric-ktx = { module = "androidx.biometric:biometric-ktx", version.ref = "biometricKtx" }
androidx-core = { module = "androidx.core:core", version.ref = "coreKtx" } androidx-core = { module = "androidx.core:core", version.ref = "coreKtx" }
androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" } androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" }
androidx-hilt-work = { module = "androidx.hilt:hilt-work", version.ref = "hiltCompiler" }
androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle-runtime-compose" } androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle-runtime-compose" }
androidx-lifecycle-service = { module = "androidx.lifecycle:lifecycle-service", version.ref = "lifecycle-runtime-compose" } androidx-lifecycle-service = { module = "androidx.lifecycle:lifecycle-service", version.ref = "lifecycle-runtime-compose" }
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomVersion" } androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomVersion" }
@@ -62,15 +64,17 @@ androidx-compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compos
#hilt #hilt
androidx-room-testing = { module = "androidx.room:room-testing", version.ref = "roomVersion" } androidx-room-testing = { module = "androidx.room:room-testing", version.ref = "roomVersion" }
androidx-work-runtime = { module = "androidx.work:work-runtime-ktx", version.ref = "workRuntimeKtxVersion" }
desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" } desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" }
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroid" } hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroid" }
hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hiltAndroid" } hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hiltAndroid" }
androidx-hilt-compiler = { module = "androidx.hilt:hilt-compiler", version.ref = "hiltCompiler" }
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" } androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" }
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" } androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" }
androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espressoCore" } androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espressoCore" }
androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationCompose" } androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltCompiler" }
androidx-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-junit" } androidx-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-junit" }
androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coreSplashscreen" } androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coreSplashscreen" }
androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle-runtime-compose" } androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle-runtime-compose" }
+1 -1
View File
@@ -1 +1 @@
1 2