Compare commits

..

15 Commits

Author SHA1 Message Date
zaneschepke b641539af8 chore: release v4.2.2 2025-12-26 00:58:08 -05:00
Weblate (bot) 9f9a15a97c feat(lang): translations from Weblate (#1115)
Co-authored-by: Fill read-only add-on <noreply-addon-fill@weblate.org>
Co-authored-by: solokot <solokot@gmail.com>
Co-authored-by: Matthaiks <kitynska@gmail.com>
Co-authored-by: Qotsa1984 <carlominzi@inwind.it>
Co-authored-by: jaime-grj <weblate.4ljj9@aleeas.com>
Co-authored-by: catelixor <catelixor+weblate@proton.me>
Co-authored-by: Priit Jõerüüt <jrthwlate@users.noreply.hosted.weblate.org>
Co-authored-by: Prefill add-on <noreply-addon-prefill@weblate.org>
Co-authored-by: Kachelkaiser <kachelkaiser@htpst.de>
Co-authored-by: Henrik Sozzi <henrik_sozzi@hotmail.com>
Co-authored-by: EESF-2 <eesf-2@users.noreply.hosted.weblate.org>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: Aleksandre Ghvineria <Ghvinerias@gmail.com>
Co-authored-by: lateweb <weblate@techkoala.net>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Co-authored-by: Alvar Kusma <kaabuta@gmail.com>
Co-authored-by: Denny Schwender <denny.schwender@gmail.com>
Co-authored-by: Salizan <sohrab.sy1@gmail.com>
Co-authored-by: Jacob <jacob.venborg@gmail.com>
Co-authored-by: CyanWolf <hydemr@pm.me>
Co-authored-by: Mostafa Kian <mostafakian77@gmail.com>
Co-authored-by: Tomáš Pernekr <leostreamer@gmail.com>
Co-authored-by: Zane Schepke <zanecschepke@gmail.com>
2025-12-26 00:38:26 -05:00
zaneschepke eeeec5613f fix: fdroid check update, in app messages bug 2025-12-26 00:31:13 -05:00
zaneschepke af21a6a3cf refactor: remove redundant dispatchers 2025-12-25 22:57:21 -05:00
Zane Schepke 0bf52ad378 fix: koin refactor param bug 2025-12-25 12:47:25 -05:00
Zane Schepke 0cf39fed68 fix: multiple profiles endpoint updates and lockdown bug
closes #962
2025-12-25 03:30:13 -05:00
Zane Schepke 590985d5cd refactor: make clear kernel is wg only
closes #1103
2025-12-25 02:05:21 -05:00
Zane Schepke c16a1b9b55 fix: pop backstack crash in certain scenarios 2025-12-25 01:18:06 -05:00
Zane Schepke 679f6abbcb fix: race after recent tunnel manager refactor
Optimize restore logic
2025-12-25 00:52:04 -05:00
Zane Schepke bbc62a26e7 refactor: ui state optimizations 2025-12-25 00:06:53 -05:00
Zane Schepke e475fd27d9 fix: race in vpn activation after amnezia 2.0 changes
Refactor to tunnel jobs handlers to make them more modular and efficient.

closes #1113
2025-12-24 02:59:14 -05:00
Zane Schepke e2dd27e70c refactor: dagger/hilt to koin for kmp 2025-12-22 12:47:57 -05:00
Zane Schepke a994e8e2c1 chore: bump agp 2025-12-19 13:52:04 -05:00
Zane Schepke 16d0642a51 chore: release 4.2.1 2025-12-19 11:35:43 -05:00
Zane Schepke eac674c996 fix: auto-tunnel screen not loading without wifi
Fixes auto tunnel screen failing to load if you haven't connected to wifi once.

Fixes import via url.

Closes #1108
Closes #1105
2025-12-19 11:30:39 -05:00
133 changed files with 2052 additions and 2217 deletions
+10 -6
View File
@@ -4,7 +4,6 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins { plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.android)
alias(libs.plugins.hilt.android)
alias(libs.plugins.kotlinxSerialization) alias(libs.plugins.kotlinxSerialization)
alias(libs.plugins.ksp) alias(libs.plugins.ksp)
alias(libs.plugins.compose.compiler) alias(libs.plugins.compose.compiler)
@@ -189,7 +188,6 @@ dependencies {
// Navigation // Navigation
implementation(libs.bundles.androidx.navigation3) implementation(libs.bundles.androidx.navigation3)
implementation(libs.bundles.navigation.lifecycle) implementation(libs.bundles.navigation.lifecycle)
implementation(libs.bundles.androidx.hilt)
// Material and icons // Material and icons
implementation(libs.bundles.google.material) implementation(libs.bundles.google.material)
@@ -200,11 +198,7 @@ dependencies {
implementation(libs.bundles.androidx.datastore) implementation(libs.bundles.androidx.datastore)
ksp(libs.androidx.room.compiler) ksp(libs.androidx.room.compiler)
// DI and work
implementation(libs.bundles.hilt.android)
implementation(libs.bundles.androidx.work) implementation(libs.bundles.androidx.work)
ksp(libs.hilt.android.compiler)
ksp(libs.androidx.hilt.compiler)
// Networking and serialization // Networking and serialization
implementation(libs.bundles.ktor.client) implementation(libs.bundles.ktor.client)
@@ -250,6 +244,16 @@ dependencies {
implementation(libs.roomdatabasebackup) { implementation(libs.roomdatabasebackup) {
exclude(group = "org.reactivestreams", module = "reactive-streams") exclude(group = "org.reactivestreams", module = "reactive-streams")
} }
// DI
implementation(platform(libs.koin.bom))
implementation(libs.koin.core)
implementation(libs.koin.android)
implementation(libs.koin.compose.viewmodel)
implementation(libs.koin.androidx.compose)
implementation(libs.koin.androidx.navigation)
implementation(libs.koin.lazy)
implementation(libs.koin.worker)
} }
tasks.register<Copy>("copyLicenseeJsonToAssets") { tasks.register<Copy>("copyLicenseeJsonToAssets") {
+10 -6
View File
@@ -103,12 +103,16 @@
android:resource="@xml/file_paths" /> android:resource="@xml/file_paths" />
</provider> </provider>
<provider <provider
android:name="androidx.startup.InitializationProvider" android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup" android:authorities="${applicationId}.androidx-startup"
android:multiprocess="true" android:exported="false"
tools:node="remove"> tools:node="merge">
</provider> <meta-data
android:name="androidx.work.WorkManagerInitializer"
android:value="androidx.startup"
tools:node="remove" />
</provider>
<service <service
android:name=".core.service.tile.TunnelControlTile" android:name=".core.service.tile.TunnelControlTile"
android:exported="true" android:exported="true"
@@ -11,13 +11,33 @@ import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.* import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.Box
import androidx.compose.material3.* import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.* import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@@ -32,18 +52,15 @@ import androidx.compose.ui.text.withLink
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
import androidx.navigation3.ui.NavDisplay import androidx.navigation3.ui.NavDisplay
import com.zaneschepke.networkmonitor.NetworkMonitor import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.data.AppDatabase import com.zaneschepke.wireguardautotunnel.data.AppDatabase
import com.zaneschepke.wireguardautotunnel.data.DataStoreManager.Companion.shouldShowDonationSnackbar
import com.zaneschepke.wireguardautotunnel.data.model.AppMode import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
@@ -51,7 +68,6 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm
import com.zaneschepke.wireguardautotunnel.ui.common.banner.AppAlertBanner import com.zaneschepke.wireguardautotunnel.ui.common.banner.AppAlertBanner
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.VpnDeniedDialog import com.zaneschepke.wireguardautotunnel.ui.common.dialog.VpnDeniedDialog
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.CustomSnackBar import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.CustomSnackBar
@@ -94,26 +110,32 @@ import com.zaneschepke.wireguardautotunnel.ui.theme.AlertRed
import com.zaneschepke.wireguardautotunnel.ui.theme.OffWhite import com.zaneschepke.wireguardautotunnel.ui.theme.OffWhite
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
import com.zaneschepke.wireguardautotunnel.util.extensions.* import com.zaneschepke.wireguardautotunnel.util.extensions.installApk
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
import com.zaneschepke.wireguardautotunnel.util.extensions.restartApp
import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
import com.zaneschepke.wireguardautotunnel.viewmodel.ConfigViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.ConfigViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.SplitTunnelViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.SplitTunnelViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelViewModel
import dagger.hilt.android.AndroidEntryPoint
import de.raphaelebner.roomdatabasebackup.core.RoomBackup import de.raphaelebner.roomdatabasebackup.core.RoomBackup
import javax.inject.Inject
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import org.koin.androidx.compose.koinViewModel
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
import xyz.teamgravity.pin_lock_compose.PinManager import xyz.teamgravity.pin_lock_compose.PinManager
@AndroidEntryPoint
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
@Inject lateinit var appStateRepository: AppStateRepository private val appStateRepository: AppStateRepository by inject()
@Inject lateinit var tunnelRepository: TunnelRepository private val tunnelRepository: TunnelRepository by inject()
@Inject lateinit var appDatabase: AppDatabase private val appDatabase: AppDatabase by inject()
@Inject lateinit var networkMonitor: NetworkMonitor private val networkMonitor: NetworkMonitor by inject()
val viewModel by viewModel<SharedAppViewModel>()
private lateinit var roomBackup: RoomBackup private lateinit var roomBackup: RoomBackup
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -129,8 +151,6 @@ class MainActivity : AppCompatActivity() {
roomBackup = RoomBackup(this) roomBackup = RoomBackup(this)
val viewModel by viewModels<SharedAppViewModel>()
installSplashScreen().apply { installSplashScreen().apply {
setKeepOnScreenCondition { !viewModel.container.stateFlow.value.isAppLoaded } setKeepOnScreenCondition { !viewModel.container.stateFlow.value.isAppLoaded }
} }
@@ -164,10 +184,12 @@ class MainActivity : AppCompatActivity() {
var previousRoute by remember { mutableStateOf<Route?>(null) } var previousRoute by remember { mutableStateOf<Route?>(null) }
val navController = val navController =
rememberNavController<NavKey>(backStack, uiState.isLocationDisclosureShown) { rememberNavController(
previousKey -> backStack,
previousRoute = previousKey as? Route uiState.isLocationDisclosureShown,
} onChange = { previousKey -> previousRoute = previousKey as? Route },
onExitApp = { finish() },
)
val vpnActivity = val vpnActivity =
rememberLauncherForActivityResult( rememberLauncherForActivityResult(
@@ -233,7 +255,6 @@ class MainActivity : AppCompatActivity() {
CompositionLocalProvider( CompositionLocalProvider(
LocalIsAndroidTV provides isTv, LocalIsAndroidTV provides isTv,
LocalSharedVm provides viewModel,
LocalNavController provides navController, LocalNavController provides navController,
) { ) {
WireguardAutoTunnelTheme(theme = uiState.theme) { WireguardAutoTunnelTheme(theme = uiState.theme) {
@@ -280,9 +301,7 @@ class MainActivity : AppCompatActivity() {
} }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
if ( if (uiState.shouldShowDonationSnackbar && !uiState.alreadyDonated) {
uiState.shouldShowDonationSnackbar && !uiState.settings.alreadyDonated
) {
viewModel.setShouldShowDonationSnackbar(false) viewModel.setShouldShowDonationSnackbar(false)
snackbarState.showSnackbar( snackbarState.showSnackbar(
SnackbarInfo( SnackbarInfo(
@@ -313,7 +332,7 @@ class MainActivity : AppCompatActivity() {
) )
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
if (uiState.settings.appMode == AppMode.LOCK_DOWN) { if (uiState.appMode == AppMode.LOCK_DOWN) {
AppAlertBanner( AppAlertBanner(
stringResource(R.string.locked_down) stringResource(R.string.locked_down)
.uppercase(Locale.current.platformLocale), .uppercase(Locale.current.platformLocale),
@@ -413,38 +432,23 @@ class MainActivity : AppCompatActivity() {
entry<Route.Tunnels> { TunnelsScreen() } entry<Route.Tunnels> { TunnelsScreen() }
entry<Route.Sort> { SortScreen() } entry<Route.Sort> { SortScreen() }
entry<Route.TunnelSettings> { key -> entry<Route.TunnelSettings> { key ->
val viewModel = val viewModel: TunnelViewModel =
hiltViewModel< koinViewModel(
TunnelViewModel, parameters = { parametersOf(key.id) }
TunnelViewModel.Factory,
>(
creationCallback = { factory ->
factory.create(key.id)
}
) )
TunnelSettingsScreen(viewModel) TunnelSettingsScreen(viewModel)
} }
entry<Route.SplitTunnel> { key -> entry<Route.SplitTunnel> { key ->
val viewModel = val viewModel: SplitTunnelViewModel =
hiltViewModel< koinViewModel(
SplitTunnelViewModel, parameters = { parametersOf(key.id) }
SplitTunnelViewModel.Factory,
>(
creationCallback = { factory ->
factory.create(key.id)
}
) )
SplitTunnelScreen(viewModel) SplitTunnelScreen(viewModel)
} }
entry<Route.Config> { key -> entry<Route.Config> { key ->
val viewModel = val viewModel: ConfigViewModel =
hiltViewModel< koinViewModel(
ConfigViewModel, parameters = { parametersOf(key.id) }
ConfigViewModel.Factory,
>(
creationCallback = { factory ->
factory.create(key.id)
}
) )
ConfigScreen(viewModel) ConfigScreen(viewModel)
} }
@@ -470,26 +474,16 @@ class MainActivity : AppCompatActivity() {
} }
entry<Route.Dns> { DnsSettingsScreen() } entry<Route.Dns> { DnsSettingsScreen() }
entry<Route.ConfigGlobal> { key -> entry<Route.ConfigGlobal> { key ->
val viewModel = val viewModel: ConfigViewModel =
hiltViewModel< koinViewModel(
ConfigViewModel, parameters = { parametersOf(key.id) }
ConfigViewModel.Factory,
>(
creationCallback = { factory ->
factory.create(key.id)
}
) )
ConfigScreen(viewModel) ConfigScreen(viewModel)
} }
entry<Route.SplitTunnelGlobal> { key -> entry<Route.SplitTunnelGlobal> { key ->
val viewModel = val viewModel: SplitTunnelViewModel =
hiltViewModel< koinViewModel(
SplitTunnelViewModel, parameters = { parametersOf(key.id) }
SplitTunnelViewModel.Factory,
>(
creationCallback = { factory ->
factory.create(key.id)
}
) )
SplitTunnelScreen(viewModel) SplitTunnelScreen(viewModel)
} }
@@ -2,18 +2,18 @@ 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 androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration
import com.zaneschepke.logcatter.LogReader import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationMonitor import com.zaneschepke.wireguardautotunnel.core.notification.NotificationMonitor
import com.zaneschepke.wireguardautotunnel.core.worker.ServiceWorker import com.zaneschepke.wireguardautotunnel.di.Dispatcher
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope import com.zaneschepke.wireguardautotunnel.di.Scope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher import com.zaneschepke.wireguardautotunnel.di.appModule
import com.zaneschepke.wireguardautotunnel.di.databaseModule
import com.zaneschepke.wireguardautotunnel.di.dispatchersModule
import com.zaneschepke.wireguardautotunnel.di.networkModule
import com.zaneschepke.wireguardautotunnel.di.tunnelModule
import com.zaneschepke.wireguardautotunnel.di.workerModule
import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
import dagger.hilt.android.HiltAndroidApp
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@@ -21,39 +21,47 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChangedBy import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.androidx.workmanager.koin.workManagerFactory
import org.koin.core.component.KoinComponent
import org.koin.core.context.GlobalContext.startKoin
import org.koin.core.lazyModules
import org.koin.core.option.viewModelScopeFactory
import org.koin.core.qualifier.named
import timber.log.Timber import timber.log.Timber
@HiltAndroidApp class WireGuardAutoTunnel : Application(), KoinComponent {
class WireGuardAutoTunnel : Application(), Configuration.Provider {
@Inject lateinit var workerFactory: HiltWorkerFactory private val applicationScope: CoroutineScope by inject(named(Scope.APPLICATION))
private val ioDispatcher: CoroutineDispatcher by inject(named(Dispatcher.IO))
private val logReader: LogReader by inject()
override val workManagerConfiguration: Configuration private val monitoringRepository: MonitoringSettingsRepository by inject()
get() = Configuration.Builder().setWorkerFactory(workerFactory).build() private val notificationMonitor: NotificationMonitor by inject()
@Inject @ApplicationScope lateinit var applicationScope: CoroutineScope
@Inject lateinit var logReader: LogReader
@Inject @IoDispatcher lateinit var ioDispatcher: CoroutineDispatcher
@Inject lateinit var monitoringRepository: MonitoringSettingsRepository
@Inject lateinit var notificationMonitor: NotificationMonitor
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
startKoin {
androidContext(this@WireGuardAutoTunnel)
if (BuildConfig.DEBUG) androidLogger()
workManagerFactory()
modules(dispatchersModule, appModule, databaseModule, tunnelModule, workerModule)
options(viewModelScopeFactory())
lazyModules(networkModule)
}
instance = this instance = this
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree()) Timber.plant(Timber.DebugTree())
StrictMode.setThreadPolicy( StrictMode.setThreadPolicy(
ThreadPolicy.Builder() StrictMode.ThreadPolicy.Builder()
.detectDiskReads() .detectAll()
.detectDiskWrites()
.detectNetwork()
.penaltyLog() .penaltyLog()
.penaltyFlashScreen()
.build() .build()
) )
StrictMode.setVmPolicy(StrictMode.VmPolicy.Builder().detectAll().penaltyLog().build())
} else { } else {
Timber.plant(ReleaseTree()) Timber.plant(ReleaseTree())
} }
@@ -72,12 +80,9 @@ class WireGuardAutoTunnel : Application(), Configuration.Provider {
} }
launch { notificationMonitor.handleApplicationNotifications() } launch { notificationMonitor.handleApplicationNotifications() }
} }
ServiceWorker.start(this)
} }
companion object { companion object {
private val _uiActive = MutableStateFlow(false) private val _uiActive = MutableStateFlow(false)
val uiActive: StateFlow<Boolean> val uiActive: StateFlow<Boolean>
@@ -4,21 +4,19 @@ import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope import com.zaneschepke.wireguardautotunnel.di.Scope
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.koin.core.qualifier.named
@AndroidEntryPoint class KernelReceiver : BroadcastReceiver(), KoinComponent {
class KernelReceiver : BroadcastReceiver() {
@Inject @ApplicationScope lateinit var applicationScope: CoroutineScope private val applicationScope: CoroutineScope by inject(named(Scope.APPLICATION))
private val tunnelRepository: TunnelRepository by inject()
@Inject lateinit var tunnelRepository: TunnelRepository private val tunnelManager: TunnelManager by inject()
@Inject lateinit var tunnelManager: TunnelManager
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
val action = intent.action ?: return val action = intent.action ?: return
@@ -5,25 +5,21 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope import com.zaneschepke.wireguardautotunnel.di.Scope
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import org.koin.core.component.inject
import org.koin.core.qualifier.named
@AndroidEntryPoint class NotificationActionReceiver : BroadcastReceiver(), KoinComponent {
class NotificationActionReceiver : BroadcastReceiver() {
@Inject lateinit var tunnelManager: TunnelManager private val tunnelManager: TunnelManager by inject()
private val autoTunnelRepository: AutoTunnelSettingsRepository by inject()
@Inject lateinit var tunnelRepository: TunnelRepository private val applicationScope: CoroutineScope = get(named(Scope.APPLICATION))
@Inject lateinit var autoTunnelRepository: AutoTunnelSettingsRepository
@Inject @ApplicationScope lateinit var applicationScope: CoroutineScope
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
applicationScope.launch { applicationScope.launch {
@@ -4,27 +4,25 @@ import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope import com.zaneschepke.wireguardautotunnel.di.Scope
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.koin.core.qualifier.named
import timber.log.Timber import timber.log.Timber
@AndroidEntryPoint class RemoteControlReceiver : BroadcastReceiver(), KoinComponent {
class RemoteControlReceiver : BroadcastReceiver() {
@Inject @ApplicationScope lateinit var applicationScope: CoroutineScope private val applicationScope: CoroutineScope by inject(named(Scope.APPLICATION))
private val settingsRepository: GeneralSettingRepository by inject()
@Inject lateinit var settingsRepository: GeneralSettingRepository private val tunnelsRepository: TunnelRepository by inject()
@Inject lateinit var tunnelsRepository: TunnelRepository private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository by inject()
@Inject lateinit var autoTunnelSettingsRepository: AutoTunnelSettingsRepository private val tunnelManager: TunnelManager by inject()
@Inject lateinit var tunnelManager: TunnelManager
enum class Action(private val suffix: String) { enum class Action(private val suffix: String) {
START_TUNNEL("START_TUNNEL"), START_TUNNEL("START_TUNNEL"),
@@ -5,24 +5,25 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import com.zaneschepke.logcatter.LogReader import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope import com.zaneschepke.wireguardautotunnel.di.Scope
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import org.koin.core.component.inject
import org.koin.core.qualifier.named
import timber.log.Timber import timber.log.Timber
@AndroidEntryPoint class RestartReceiver : BroadcastReceiver(), KoinComponent {
class RestartReceiver : BroadcastReceiver() {
@Inject @ApplicationScope lateinit var applicationScope: CoroutineScope private val applicationScope: CoroutineScope = get(named(Scope.APPLICATION))
@Inject lateinit var tunnelManager: TunnelManager private val tunnelManager: TunnelManager by inject()
@Inject lateinit var appStateRepository: AppStateRepository private val appStateRepository: AppStateRepository by inject()
@Inject lateinit var logReader: LogReader private val logReader: LogReader by inject()
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
Timber.d("RestartReceiver triggered with action: ${intent.action}") Timber.d("RestartReceiver triggered with action: ${intent.action}")
@@ -4,14 +4,11 @@ import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.util.StringValue import com.zaneschepke.wireguardautotunnel.util.StringValue
import jakarta.inject.Inject
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class NotificationMonitor class NotificationMonitor(
@Inject
constructor(
private val tunnelManager: TunnelManager, private val tunnelManager: TunnelManager,
private val notificationManager: NotificationManager, private val notificationManager: NotificationManager,
) { ) {
@@ -16,11 +16,8 @@ import com.zaneschepke.wireguardautotunnel.core.broadcast.NotificationActionRece
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager.Companion.EXTRA_ID import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager.Companion.EXTRA_ID
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
import com.zaneschepke.wireguardautotunnel.util.StringValue import com.zaneschepke.wireguardautotunnel.util.StringValue
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
class WireGuardNotification @Inject constructor(@ApplicationContext override val context: Context) : class WireGuardNotification(override val context: Context) : NotificationManager {
NotificationManager {
enum class NotificationChannels { enum class NotificationChannels {
VPN, VPN,
@@ -10,35 +10,31 @@ import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelMonitor import com.zaneschepke.wireguardautotunnel.di.Dispatcher
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.util.extensions.distinctByKeys import com.zaneschepke.wireguardautotunnel.util.extensions.distinctByKeys
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import org.koin.core.qualifier.named
import timber.log.Timber import timber.log.Timber
@AndroidEntryPoint
abstract class BaseTunnelForegroundService : LifecycleService(), TunnelService { abstract class BaseTunnelForegroundService : LifecycleService(), TunnelService {
@Inject lateinit var notificationManager: NotificationManager private val notificationManager: NotificationManager by inject()
@Inject lateinit var serviceManager: ServiceManager private val serviceManager: ServiceManager by inject()
@Inject lateinit var tunnelManager: TunnelManager private val tunnelManager: TunnelManager by inject()
@Inject lateinit var tunnelMonitor: TunnelMonitor private val ioDispatcher: CoroutineDispatcher by inject(named(Dispatcher.IO))
@Inject @IoDispatcher lateinit var ioDispatcher: CoroutineDispatcher private val settingsRepository: GeneralSettingRepository by inject()
@Inject lateinit var settingsRepository: GeneralSettingRepository private val tunnelsRepository: TunnelRepository by inject()
@Inject lateinit var tunnelsRepository: TunnelRepository
protected abstract val fgsType: Int protected abstract val fgsType: Int
@@ -8,15 +8,20 @@ import android.net.VpnService
import android.os.IBinder import android.os.IBinder
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelService import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelService
import com.zaneschepke.wireguardautotunnel.data.model.AppMode import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
import jakarta.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
@@ -24,12 +29,10 @@ import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.withTimeoutOrNull
import timber.log.Timber import timber.log.Timber
class ServiceManager class ServiceManager(
@Inject
constructor(
private val context: Context, private val context: Context,
@IoDispatcher ioDispatcher: CoroutineDispatcher, ioDispatcher: CoroutineDispatcher,
@ApplicationScope applicationScope: CoroutineScope, applicationScope: CoroutineScope,
private val mainDispatcher: CoroutineDispatcher, private val mainDispatcher: CoroutineDispatcher,
private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository, private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
) { ) {
@@ -1,8 +1,6 @@
package com.zaneschepke.wireguardautotunnel.core.service package com.zaneschepke.wireguardautotunnel.core.service
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class TunnelForegroundService(override val fgsType: Int = Constants.SPECIAL_USE_SERVICE_TYPE_ID) : class TunnelForegroundService(override val fgsType: Int = Constants.SPECIAL_USE_SERVICE_TYPE_ID) :
BaseTunnelForegroundService() BaseTunnelForegroundService()
@@ -1,8 +1,6 @@
package com.zaneschepke.wireguardautotunnel.core.service package com.zaneschepke.wireguardautotunnel.core.service
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class VpnForegroundService(override val fgsType: Int = Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID) : class VpnForegroundService(override val fgsType: Int = Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID) :
BaseTunnelForegroundService() BaseTunnelForegroundService()
@@ -15,7 +15,7 @@ import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotificati
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.data.model.AppMode import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher import com.zaneschepke.wireguardautotunnel.di.Dispatcher
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
import com.zaneschepke.wireguardautotunnel.domain.model.AutoTunnelSettings import com.zaneschepke.wireguardautotunnel.domain.model.AutoTunnelSettings
@@ -28,32 +28,45 @@ import com.zaneschepke.wireguardautotunnel.domain.state.toDomain
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.to import com.zaneschepke.wireguardautotunnel.util.extensions.to
import com.zaneschepke.wireguardautotunnel.util.extensions.toMillis import com.zaneschepke.wireguardautotunnel.util.extensions.toMillis
import dagger.hilt.android.AndroidEntryPoint
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher
import javax.inject.Provider import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.* import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.* import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import org.koin.android.ext.android.inject
import org.koin.core.qualifier.named
import timber.log.Timber import timber.log.Timber
@AndroidEntryPoint
class AutoTunnelService : LifecycleService() { class AutoTunnelService : LifecycleService() {
@Inject lateinit var networkMonitor: NetworkMonitor private val networkMonitor: NetworkMonitor by inject()
@Inject lateinit var notificationManager: NotificationManager private val notificationManager: NotificationManager by inject()
@Inject @IoDispatcher lateinit var ioDispatcher: CoroutineDispatcher private val ioDispatcher: CoroutineDispatcher by inject(named(Dispatcher.IO))
@Inject lateinit var serviceManager: ServiceManager private val serviceManager: ServiceManager by inject()
@Inject lateinit var tunnelManager: TunnelManager private val tunnelManager: TunnelManager by inject()
@Inject lateinit var autoTunnelRepository: Provider<AutoTunnelSettingsRepository> private val autoTunnelRepository: AutoTunnelSettingsRepository by inject()
@Inject lateinit var settingsRepository: GeneralSettingRepository private val settingsRepository: GeneralSettingRepository by inject()
@Inject lateinit var tunnelsRepository: TunnelRepository private val tunnelsRepository: TunnelRepository by inject()
private val defaultState = AutoTunnelState() private val defaultState = AutoTunnelState()
@@ -235,7 +248,7 @@ class AutoTunnelService : LifecycleService() {
private fun combineSettings(): Flow<Triple<AppMode, AutoTunnelSettings, List<TunnelConfig>>> { private fun combineSettings(): Flow<Triple<AppMode, AutoTunnelSettings, List<TunnelConfig>>> {
return combine( return combine(
settingsRepository.flow.map { it.appMode }.distinctUntilChanged(), settingsRepository.flow.map { it.appMode }.distinctUntilChanged(),
autoTunnelRepository.get().flow, autoTunnelRepository.flow,
tunnelsRepository.userTunnelsFlow.map { tunnels -> tunnelsRepository.userTunnelsFlow.map { tunnels ->
// isActive is ignored for equality checks so user can manually toggle off // isActive is ignored for equality checks so user can manually toggle off
// tunnel with auto-tunnel // tunnel with auto-tunnel
@@ -352,7 +365,10 @@ class AutoTunnelService : LifecycleService() {
) { ) {
is AutoTunnelEvent.Start -> is AutoTunnelEvent.Start ->
(event.tunnelConfig ?: tunnelsRepository.getDefaultTunnel())?.let { (event.tunnelConfig ?: tunnelsRepository.getDefaultTunnel())?.let {
tunnelManager.startTunnel(it) tunnelManager.startTunnel(it).onFailure { e ->
Timber.e(e, "Auto-tunnel start failed for ${it.name}")
// TODO notify or retry
}
} }
is AutoTunnelEvent.Stop -> tunnelManager.stopActiveTunnels() is AutoTunnelEvent.Stop -> tunnelManager.stopActiveTunnels()
AutoTunnelEvent.DoNothing -> Timber.i("Auto-tunneling: nothing to do") AutoTunnelEvent.DoNothing -> Timber.i("Auto-tunneling: nothing to do")
@@ -363,9 +379,7 @@ class AutoTunnelService : LifecycleService() {
// restart network flow on debounce changes // restart network flow on debounce changes
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
private val debouncedConnectivityStateFlow: Flow<ConnectivityState> by lazy { private val debouncedConnectivityStateFlow: Flow<ConnectivityState> by lazy {
autoTunnelRepository autoTunnelRepository.flow
.get()
.flow
.map { it.debounceDelaySeconds.toMillis() } .map { it.debounceDelaySeconds.toMillis() }
.distinctUntilChanged() .distinctUntilChanged()
.flatMapLatest { debounceMillis -> .flatMapLatest { debounceMillis ->
@@ -7,19 +7,17 @@ import android.service.quicksettings.TileService
import androidx.lifecycle.* import androidx.lifecycle.*
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlin.concurrent.atomics.AtomicBoolean import kotlin.concurrent.atomics.AtomicBoolean
import kotlin.concurrent.atomics.ExperimentalAtomicApi import kotlin.concurrent.atomics.ExperimentalAtomicApi
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import timber.log.Timber import timber.log.Timber
@AndroidEntryPoint
class AutoTunnelControlTile : TileService(), LifecycleOwner { class AutoTunnelControlTile : TileService(), LifecycleOwner {
@Inject lateinit var autoTunnelSettingsRepository: AutoTunnelSettingsRepository private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository by inject()
@Inject lateinit var serviceManager: ServiceManager private val serviceManager: ServiceManager by inject()
@OptIn(ExperimentalAtomicApi::class) val isCollecting = AtomicBoolean(false) @OptIn(ExperimentalAtomicApi::class) val isCollecting = AtomicBoolean(false)
@@ -5,30 +5,32 @@ import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.service.quicksettings.Tile import android.service.quicksettings.Tile
import android.service.quicksettings.TileService import android.service.quicksettings.TileService
import androidx.lifecycle.* import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlin.concurrent.atomics.AtomicBoolean import kotlin.concurrent.atomics.AtomicBoolean
import kotlin.concurrent.atomics.ExperimentalAtomicApi import kotlin.concurrent.atomics.ExperimentalAtomicApi
import kotlinx.coroutines.flow.distinctUntilChangedBy import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import org.koin.android.ext.android.inject
import timber.log.Timber import timber.log.Timber
@AndroidEntryPoint
class TunnelControlTile : TileService(), LifecycleOwner { class TunnelControlTile : TileService(), LifecycleOwner {
@Inject lateinit var tunnelsRepository: TunnelRepository private val tunnelsRepository: TunnelRepository by inject()
@Inject lateinit var serviceManager: ServiceManager private val serviceManager: ServiceManager by inject()
@Inject lateinit var tunnelManager: TunnelManager private val tunnelManager: TunnelManager by inject()
@OptIn(ExperimentalAtomicApi::class) val isCollecting = AtomicBoolean(false) @OptIn(ExperimentalAtomicApi::class) val isCollecting = AtomicBoolean(false)
@@ -6,13 +6,12 @@ import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
class DynamicShortcutManager( class DynamicShortcutManager(
private val context: Context, private val context: Context,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher, private val ioDispatcher: CoroutineDispatcher,
) : ShortcutManager { ) : ShortcutManager {
override suspend fun addShortcuts() { override suspend fun addShortcuts() {
withContext(ioDispatcher) { withContext(ioDispatcher) {
@@ -5,26 +5,23 @@ import androidx.activity.ComponentActivity
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelService import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelService
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope import com.zaneschepke.wireguardautotunnel.di.Scope
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import org.koin.core.qualifier.named
import timber.log.Timber import timber.log.Timber
@AndroidEntryPoint
class ShortcutsActivity : ComponentActivity() { class ShortcutsActivity : ComponentActivity() {
@Inject lateinit var settingsRepository: GeneralSettingRepository private val settingsRepository: GeneralSettingRepository by inject()
@Inject lateinit var autoTunnelSettingsRepository: AutoTunnelSettingsRepository private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository by inject()
@Inject lateinit var tunnelsRepository: TunnelRepository private val tunnelsRepository: TunnelRepository by inject()
private val tunnelManager: TunnelManager by inject()
@Inject lateinit var tunnelManager: TunnelManager private val applicationScope: CoroutineScope by inject(named(Scope.APPLICATION))
@Inject @ApplicationScope lateinit var applicationScope: CoroutineScope
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -1,155 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException
import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.state.LogHealthState
import com.zaneschepke.wireguardautotunnel.domain.state.PingState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import java.util.concurrent.ConcurrentHashMap
import kotlin.coroutines.cancellation.CancellationException
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import timber.log.Timber
abstract class BaseTunnel(
@ApplicationScope protected val applicationScope: CoroutineScope,
@IoDispatcher protected val ioDispatcher: CoroutineDispatcher,
) : TunnelProvider {
protected val errors = MutableSharedFlow<Pair<String, BackendCoreException>>()
override val errorEvents = errors.asSharedFlow()
private val _messageEvents = MutableSharedFlow<Pair<String, BackendMessage>>()
override val messageEvents = _messageEvents.asSharedFlow()
protected val activeTuns = MutableStateFlow<Map<Int, TunnelState>>(emptyMap())
override val activeTunnels = activeTuns.asStateFlow()
protected val tunJobs = ConcurrentHashMap<Int, Job>()
private val tunMutex = Mutex()
private val tunStatusMutex = Mutex()
abstract fun tunnelStateFlow(tunnelConfig: TunnelConfig): Flow<TunnelStatus>
abstract override fun setBackendMode(backendMode: BackendMode)
abstract override fun getBackendMode(): BackendMode
abstract override suspend fun forceStopTunnel(tunnelId: Int)
abstract override fun handleDnsReresolve(tunnelConfig: TunnelConfig): Boolean
abstract override fun getStatistics(tunnelId: Int): TunnelStatistics?
override suspend fun updateTunnelStatus(
tunnelId: Int,
status: TunnelStatus?,
stats: TunnelStatistics?,
pingStates: Map<String, PingState>?,
logHealthState: LogHealthState?,
) {
tunStatusMutex.withLock {
activeTuns.update { currentTuns ->
if (!currentTuns.containsKey(tunnelId) && status != TunnelStatus.Starting) {
Timber.d("Ignoring update for inactive tunnel $tunnelId")
return@update currentTuns
}
val existingState = currentTuns[tunnelId] ?: TunnelState()
val newStatus = status ?: existingState.status
if (newStatus == TunnelStatus.Down) {
Timber.d("Removing tunnel $tunnelId from activeTunnels as state is DOWN")
cleanUpTunJob(tunnelId)
currentTuns - tunnelId
} else if (
existingState.status == newStatus &&
stats == null &&
pingStates == null &&
logHealthState == null
) {
Timber.d("Skipping redundant state update for ${tunnelId}: $newStatus")
currentTuns
} else {
val updated =
existingState.copy(
status = newStatus,
statistics = stats ?: existingState.statistics,
pingStates = pingStates ?: existingState.pingStates,
logHealthState = logHealthState ?: existingState.logHealthState,
)
currentTuns + (tunnelId to updated)
}
}
}
}
override suspend fun stopActiveTunnels() {
activeTunnels.value.forEach { (config, state) ->
if (state.status.isUpOrStarting()) {
stopTunnel(config)
}
}
}
override suspend fun startTunnel(tunnelConfig: TunnelConfig) {
tunMutex.withLock {
if (
activeTuns.value.containsKey(tunnelConfig.id) ||
tunJobs.containsKey(tunnelConfig.id)
) {
return Timber.w("Tunnel is already running: ${tunnelConfig.name}")
}
val job =
applicationScope.launch(ioDispatcher) {
try {
tunnelStateFlow(tunnelConfig).collect { status ->
updateTunnelStatus(tunnelConfig.id, status)
}
} catch (e: BackendCoreException) {
errors.emit(tunnelConfig.name to e)
updateTunnelStatus(tunnelConfig.id, TunnelStatus.Down)
} catch (_: CancellationException) {}
}
tunJobs[tunnelConfig.id] = job
job.invokeOnCompletion {
tunJobs.remove(tunnelConfig.id)
activeTuns.update { it - tunnelConfig.id }
}
}
}
override suspend fun stopTunnel(tunnelId: Int) {
tunMutex.withLock {
val currentState = activeTuns.value[tunnelId]?.status ?: return@withLock
updateTunnelStatus(tunnelId, TunnelStatus.Stopping)
tunJobs[tunnelId]?.cancel()
withTimeoutOrNull(STOP_TIMEOUT_MS) {
activeTunnels.first {
!it.containsKey(tunnelId) || it[tunnelId]!!.status == TunnelStatus.Down
}
}
?: run {
Timber.w("Stop timeout for $tunnelId (was $currentState); forcing kill")
forceStopTunnel(tunnelId)
}
}
}
private fun cleanUpTunJob(tunnelId: Int) {
Timber.d("Removing job for $tunnelId")
tunJobs -= tunnelId
}
companion object {
const val STARTUP_TIMEOUT_MS: Long = 15_000L
const val STOP_TIMEOUT_MS: Long = 5_000L
}
}
@@ -0,0 +1,187 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.core.tunnel.backend.TunnelBackend
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException
import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage
import com.zaneschepke.wireguardautotunnel.domain.events.UnknownError
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.state.LogHealthState
import com.zaneschepke.wireguardautotunnel.domain.state.PingState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import java.util.concurrent.ConcurrentHashMap
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withTimeoutOrNull
import timber.log.Timber
class TunnelLifecycleManager(
private val backend: TunnelBackend,
private val applicationScope: CoroutineScope,
private val ioDispatcher: CoroutineDispatcher,
private val sharedActiveTunnels: MutableStateFlow<Map<Int, TunnelState>>,
) : TunnelProvider {
override val activeTunnels: StateFlow<Map<Int, TunnelState>> = sharedActiveTunnels.asStateFlow()
private val _errorEvents = MutableSharedFlow<Pair<String?, BackendCoreException>>()
override val errorEvents: SharedFlow<Pair<String?, BackendCoreException>> =
_errorEvents.asSharedFlow()
private val _messageEvents = MutableSharedFlow<Pair<String?, BackendMessage>>()
override val messageEvents: SharedFlow<Pair<String?, BackendMessage>> =
_messageEvents.asSharedFlow()
private val tunnelJobs = ConcurrentHashMap<Int, Job>()
private val tunMutex = Mutex()
private val tunStatusMutex = Mutex()
override suspend fun startTunnel(tunnelConfig: TunnelConfig): Result<Unit> =
tunMutex.withLock {
val id = tunnelConfig.id
if (sharedActiveTunnels.value.containsKey(id)) {
Timber.w("Tunnel is already running: ${tunnelConfig.name}")
return Result.failure(IllegalStateException("Tunnel already running"))
}
val startupCompleted = CompletableDeferred<Result<Unit>>()
val job =
applicationScope.launch(ioDispatcher) {
try {
updateTunnelStatus(id, TunnelStatus.Starting)
backend.tunnelStateFlow(tunnelConfig).collect { status ->
updateTunnelStatus(id, status)
if (status != TunnelStatus.Starting && !startupCompleted.isCompleted) {
if (status is TunnelStatus.Up) {
startupCompleted.complete(Result.success(Unit))
} else {
startupCompleted.complete(Result.failure(UnknownError()))
}
}
}
} catch (e: BackendCoreException) {
_errorEvents.emit(tunnelConfig.name to e)
updateTunnelStatus(id, TunnelStatus.Down)
startupCompleted.complete(Result.failure(e))
} catch (_: CancellationException) {} finally {
tunnelJobs.remove(id)
sharedActiveTunnels.update { it - id }
}
}
tunnelJobs[id] = job
job.invokeOnCompletion { tunnelJobs.remove(id) }
try {
startupCompleted.await()
} catch (e: Throwable) {
job.cancel()
Result.failure(e)
}
}
override suspend fun stopTunnel(tunnelId: Int) =
tunMutex.withLock {
val currentState = sharedActiveTunnels.value[tunnelId]?.status ?: return@withLock
updateTunnelStatus(tunnelId, TunnelStatus.Stopping)
tunnelJobs[tunnelId]?.cancel()
withTimeoutOrNull(STOP_TIMEOUT_MS) {
activeTunnels.first {
!it.containsKey(tunnelId) || it[tunnelId]!!.status == TunnelStatus.Down
}
}
?: run {
Timber.w("Stop timeout for $tunnelId (was $currentState); forcing kill")
forceStopTunnel(tunnelId)
}
}
override suspend fun forceStopTunnel(tunnelId: Int) {
backend.forceStopTunnel(tunnelId)
tunnelJobs[tunnelId]?.cancel()
tunnelJobs.remove(tunnelId)
sharedActiveTunnels.update { it - tunnelId }
updateTunnelStatus(tunnelId, TunnelStatus.Down)
}
override suspend fun stopActiveTunnels() {
sharedActiveTunnels.value.forEach { (id, state) ->
if (state.status.isUpOrStarting()) {
stopTunnel(id)
}
}
}
override suspend fun updateTunnelStatus(
tunnelId: Int,
status: TunnelStatus?,
stats: TunnelStatistics?,
pingStates: Map<String, PingState>?,
logHealthState: LogHealthState?,
) =
tunStatusMutex.withLock {
sharedActiveTunnels.update { currentTuns ->
if (!currentTuns.containsKey(tunnelId) && status != TunnelStatus.Starting) {
Timber.d("Ignoring update for inactive tunnel $tunnelId")
return@update currentTuns
}
val existingState = currentTuns[tunnelId] ?: TunnelState()
val newStatus = status ?: existingState.status
if (newStatus == TunnelStatus.Down) {
Timber.d("Removing tunnel $tunnelId from activeTunnels as state is DOWN")
currentTuns - tunnelId
} else if (
existingState.status == newStatus &&
stats == null &&
pingStates == null &&
logHealthState == null
) {
Timber.d("Skipping redundant state update for ${tunnelId}: $newStatus")
currentTuns
} else {
val updated =
existingState.copy(
status = newStatus,
statistics = stats ?: existingState.statistics,
pingStates = pingStates ?: existingState.pingStates,
logHealthState = logHealthState ?: existingState.logHealthState,
)
currentTuns + (tunnelId to updated)
}
}
}
override fun setBackendMode(backendMode: BackendMode) = backend.setBackendMode(backendMode)
override fun getBackendMode(): BackendMode = backend.getBackendMode()
override suspend fun runningTunnelNames(): Set<String> = backend.runningTunnelNames()
override fun handleDnsReresolve(tunnelConfig: TunnelConfig): Boolean =
backend.handleDnsReresolve(tunnelConfig)
override fun getStatistics(tunnelId: Int): TunnelStatistics? = backend.getStatistics(tunnelId)
companion object {
const val STOP_TIMEOUT_MS: Long = 5_000L
}
}
@@ -1,8 +1,15 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel package com.zaneschepke.wireguardautotunnel.core.tunnel
import android.os.PowerManager
import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.backend.TunnelBackend
import com.zaneschepke.wireguardautotunnel.core.tunnel.handler.DynamicDnsHandler
import com.zaneschepke.wireguardautotunnel.core.tunnel.handler.TunnelActiveStatePersister
import com.zaneschepke.wireguardautotunnel.core.tunnel.handler.TunnelMonitorHandler
import com.zaneschepke.wireguardautotunnel.core.tunnel.handler.TunnelServiceHandler
import com.zaneschepke.wireguardautotunnel.data.model.AppMode import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.di.*
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException
@@ -14,250 +21,117 @@ import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.LockdownSettingsRepository import com.zaneschepke.wireguardautotunnel.domain.repository.LockdownSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.state.LogHealthState import com.zaneschepke.wireguardautotunnel.domain.state.LogHealthState
import com.zaneschepke.wireguardautotunnel.domain.state.PingState import com.zaneschepke.wireguardautotunnel.domain.state.PingState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import java.util.concurrent.ConcurrentHashMap import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils
import javax.inject.Inject
import kotlin.concurrent.atomics.AtomicBoolean import kotlin.concurrent.atomics.AtomicBoolean
import kotlin.concurrent.atomics.AtomicReference import kotlin.concurrent.atomics.AtomicReference
import kotlin.concurrent.atomics.ExperimentalAtomicApi import kotlin.concurrent.atomics.ExperimentalAtomicApi
import kotlinx.coroutines.* import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.* import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import kotlinx.coroutines.supervisorScope
import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
@OptIn(ExperimentalCoroutinesApi::class, ExperimentalAtomicApi::class) @OptIn(ExperimentalCoroutinesApi::class, ExperimentalAtomicApi::class)
class TunnelManager class TunnelManager(
@Inject kernelBackend: TunnelBackend,
constructor( userspaceBackend: TunnelBackend,
@Kernel private val kernelTunnel: TunnelProvider, proxyUserspaceBackend: TunnelBackend,
@Userspace private val userspaceTunnel: TunnelProvider, networkMonitor: NetworkMonitor,
@ProxyUserspace private val proxyUserspaceTunnel: TunnelProvider, networkUtils: NetworkUtils,
powerManager: PowerManager,
logReader: LogReader,
monitoringSettingsRepository: MonitoringSettingsRepository,
private val serviceManager: ServiceManager, private val serviceManager: ServiceManager,
private val settingsRepository: GeneralSettingRepository, private val settingsRepository: GeneralSettingRepository,
private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository, private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
private val lockdownSettingsRepository: LockdownSettingsRepository, private val lockdownSettingsRepository: LockdownSettingsRepository,
private val tunnelsRepository: TunnelRepository, private val tunnelsRepository: TunnelRepository,
private val tunnelMonitor: TunnelMonitor, private val applicationScope: CoroutineScope,
@ApplicationScope private val applicationScope: CoroutineScope, private val ioDispatcher: CoroutineDispatcher,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : TunnelProvider { ) : TunnelProvider {
@OptIn(ExperimentalCoroutinesApi::class) private val _activeTunnels = MutableStateFlow<Map<Int, TunnelState>>(emptyMap())
private val localErrorEvents = MutableSharedFlow<Pair<String?, BackendCoreException>>() override val activeTunnels: StateFlow<Map<Int, TunnelState>> = _activeTunnels.asStateFlow()
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalAtomicApi::class) val currentAppMode = AtomicReference(AppMode.VPN)
private val localMessageEvents = MutableSharedFlow<Pair<String?, BackendMessage>>()
private val monitoringMutex = Mutex() private val defaultManager =
private val monitoringJobs = ConcurrentHashMap<Int, Job>() TunnelLifecycleManager(userspaceBackend, applicationScope, ioDispatcher, _activeTunnels)
private val ddnsMutex = Mutex() private val lifecycleManagers: Map<AppMode, TunnelLifecycleManager> =
private val ddnsJobs = ConcurrentHashMap<Int, Job>() mapOf(
AppMode.KERNEL to
TunnelLifecycleManager(
kernelBackend,
applicationScope,
ioDispatcher,
_activeTunnels,
),
AppMode.VPN to defaultManager,
AppMode.PROXY to
TunnelLifecycleManager(
proxyUserspaceBackend,
applicationScope,
ioDispatcher,
_activeTunnels,
),
AppMode.LOCK_DOWN to
TunnelLifecycleManager(
proxyUserspaceBackend,
applicationScope,
ioDispatcher,
_activeTunnels,
),
)
private data class SideEffectState( @OptIn(ExperimentalAtomicApi::class)
val activeTuns: Map<Int, TunnelState>, private fun getProvider(): TunnelProvider {
val tuns: List<TunnelConfig>, return lifecycleManagers[currentAppMode.load()] ?: defaultManager
val settings: GeneralSettings,
val previouslyActive: Map<Int, TunnelState>,
)
private data class SideEffectWithCondition(
val effect: suspend (SideEffectState) -> Unit,
val condition: (SideEffectState) -> Boolean,
)
private val tunnelProviderFlow: StateFlow<TunnelProvider> = run {
val currentBackend = AtomicReference(userspaceTunnel)
val currentSettings = AtomicReference(GeneralSettings())
val initialEmit = AtomicBoolean(true)
settingsRepository.flow
.filterNotNull()
// ignore default state
.filterNot { it == GeneralSettings() }
.distinctUntilChangedBy { it.appMode }
.map { settings ->
Timber.d("App mode changes with ${settings.appMode}")
val backend =
when (settings.appMode) {
AppMode.VPN -> userspaceTunnel
AppMode.PROXY -> proxyUserspaceTunnel
AppMode.LOCK_DOWN -> proxyUserspaceTunnel
AppMode.KERNEL -> kernelTunnel
}
settings to backend
}
.onEach { (settings, newBackend) ->
val isInitialEmit = initialEmit.exchange(false)
val previousBackend = currentBackend.exchange(newBackend)
val previousSettings = currentSettings.exchange(settings)
if ((previousSettings.appMode != settings.appMode) && !isInitialEmit) {
handleModeChangeCleanup(previousBackend, previousSettings.appMode)
}
if (settings.appMode == AppMode.LOCK_DOWN) {
handleLockDownModeInit()
}
}
.map { (_, backend) -> backend }
.stateIn(
scope = applicationScope.plus(ioDispatcher),
started = SharingStarted.Eagerly,
initialValue = userspaceTunnel,
)
} }
override val activeTunnels: StateFlow<Map<Int, TunnelState>> = run { override suspend fun startTunnel(tunnelConfig: TunnelConfig): Result<Unit> =
val activeTunsReference: AtomicReference<Map<Int, TunnelState>> = getProvider().startTunnel(tunnelConfig)
AtomicReference(emptyMap())
tunnelProviderFlow override suspend fun stopTunnel(tunnelId: Int) = getProvider().stopTunnel(tunnelId)
.flatMapLatest { backend ->
combine(
backend.activeTunnels,
tunnelsRepository.flow,
settingsRepository.flow.filterNotNull(),
) { activeTuns, tuns, settings ->
Triple(activeTuns, tuns, settings)
}
}
.onStart { handleRestore() }
.onEach { (activeTuns, tuns, settings) ->
val previouslyActive = activeTunsReference.exchange(activeTuns)
val state = SideEffectState(activeTuns, tuns, settings, previouslyActive)
applicationScope.launch(ioDispatcher) { override suspend fun forceStopTunnel(tunnelId: Int) = getProvider().forceStopTunnel(tunnelId)
supervisorScope {
val sideEffects =
listOf(
SideEffectWithCondition(
effect = { s ->
handleTunnelServiceChange(s.settings.appMode, s.activeTuns)
},
condition = { s ->
s.activeTuns.size != s.previouslyActive.size
},
),
SideEffectWithCondition(
effect = { s ->
handleTunnelsActiveChange(
s.previouslyActive,
s.activeTuns,
s.tuns,
)
},
condition = { s ->
s.activeTuns.size != s.previouslyActive.size
},
),
SideEffectWithCondition(
effect = { s ->
handleDynamicDnsMonitoring(s.activeTuns, s.tuns, s.settings)
},
condition = { s ->
s.activeTuns.keys != s.previouslyActive.keys
},
),
SideEffectWithCondition(
effect = { s ->
handleFullTunnelMonitoring(s.activeTuns, s.tuns, s.settings)
},
condition = { s ->
s.activeTuns.keys != s.previouslyActive.keys
},
),
)
sideEffects override suspend fun stopActiveTunnels() = getProvider().stopActiveTunnels()
.filter { it.condition(state) }
.forEach { sideEffect ->
launch {
try {
sideEffect.effect(state)
} catch (e: Exception) {
Timber.e(e, "Side effect failed")
}
}
}
}
}
}
.map { (activeTuns, _, _) -> activeTuns }
.stateIn(
scope = applicationScope,
started = SharingStarted.Eagerly,
initialValue = emptyMap(),
)
}
@OptIn(ExperimentalCoroutinesApi::class) override fun setBackendMode(backendMode: BackendMode) =
override val errorEvents: SharedFlow<Pair<String?, BackendCoreException>> = getProvider().setBackendMode(backendMode)
merge(localErrorEvents, tunnelProviderFlow.flatMapLatest { it.errorEvents })
.shareIn(
scope = applicationScope + ioDispatcher,
started = SharingStarted.Eagerly,
replay = 0,
)
@OptIn(ExperimentalCoroutinesApi::class) override fun getBackendMode(): BackendMode = getProvider().getBackendMode()
override val messageEvents: SharedFlow<Pair<String?, BackendMessage>> =
merge(localMessageEvents, tunnelProviderFlow.flatMapLatest { it.messageEvents })
.shareIn(
scope = applicationScope.plus(ioDispatcher),
started = SharingStarted.Eagerly,
replay = 0,
)
override fun getStatistics(tunnelId: Int): TunnelStatistics? { override suspend fun runningTunnelNames(): Set<String> = getProvider().runningTunnelNames()
return tunnelProviderFlow.value.getStatistics(tunnelId)
}
override suspend fun startTunnel(tunnelConfig: TunnelConfig) { override fun handleDnsReresolve(tunnelConfig: TunnelConfig): Boolean =
if (activeTunnels.value.containsKey(tunnelConfig.id)) return getProvider().handleDnsReresolve(tunnelConfig)
val provider = tunnelProviderFlow.value
val isKernel = provider is KernelTunnel
if (!isKernel && activeTunnels.value.isNotEmpty()) { override fun getStatistics(tunnelId: Int): TunnelStatistics? =
stopActiveTunnels() getProvider().getStatistics(tunnelId)
withTimeoutOrNull(BaseTunnel.STARTUP_TIMEOUT_MS) {
activeTunnels.first { it.isEmpty() }
} ?: run { activeTunnels.value.keys.forEach { id -> provider.forceStopTunnel(id) } }
}
tunnelProviderFlow.value.startTunnel(tunnelConfig)
}
override suspend fun stopTunnel(tunnelId: Int) {
tunnelProviderFlow.value.stopTunnel(tunnelId)
}
override suspend fun forceStopTunnel(tunnelId: Int) {
tunnelProviderFlow.value.forceStopTunnel(tunnelId)
}
override suspend fun stopActiveTunnels() {
tunnelProviderFlow.value.stopActiveTunnels()
}
override fun setBackendMode(backendMode: BackendMode) {
tunnelProviderFlow.value.setBackendMode(backendMode)
}
override fun getBackendMode(): BackendMode {
return tunnelProviderFlow.value.getBackendMode()
}
override suspend fun runningTunnelNames(): Set<String> {
return tunnelProviderFlow.value.runningTunnelNames()
}
override fun handleDnsReresolve(tunnelConfig: TunnelConfig): Boolean {
return tunnelProviderFlow.value.handleDnsReresolve(tunnelConfig)
}
override suspend fun updateTunnelStatus( override suspend fun updateTunnelStatus(
tunnelId: Int, tunnelId: Int,
@@ -265,24 +139,99 @@ constructor(
stats: TunnelStatistics?, stats: TunnelStatistics?,
pingStates: Map<String, PingState>?, pingStates: Map<String, PingState>?,
logHealthState: LogHealthState?, logHealthState: LogHealthState?,
) { ) = getProvider().updateTunnelStatus(tunnelId, status, stats, pingStates, logHealthState)
tunnelProviderFlow.value.updateTunnelStatus(
tunnelId,
status,
stats,
pingStates,
logHealthState,
)
}
private suspend fun handleTunnelServiceChange( @OptIn(ExperimentalCoroutinesApi::class)
appMode: AppMode, private val localErrorEvents = MutableSharedFlow<Pair<String?, BackendCoreException>>()
activeTuns: Map<Int, TunnelState>,
) { @OptIn(ExperimentalCoroutinesApi::class)
if (activeTuns.isEmpty()) serviceManager.stopTunnelService() private val localMessageEvents = MutableSharedFlow<Pair<String?, BackendMessage>>()
if (activeTuns.isNotEmpty() && serviceManager.tunnelService.value == null)
serviceManager.startTunnelService(appMode) override val errorEvents: SharedFlow<Pair<String?, BackendCoreException>> =
serviceManager.updateTunnelTile() merge(localErrorEvents, *lifecycleManagers.values.map { it.errorEvents }.toTypedArray())
.shareIn(
scope = applicationScope + ioDispatcher,
started = SharingStarted.Eagerly,
replay = 0,
)
override val messageEvents: SharedFlow<Pair<String?, BackendMessage>> =
merge(localMessageEvents, *lifecycleManagers.values.map { it.messageEvents }.toTypedArray())
.shareIn(
scope = applicationScope.plus(ioDispatcher),
started = SharingStarted.Eagerly,
replay = 0,
)
private val tunnelServiceHandler =
TunnelServiceHandler(
activeTunnels = activeTunnels,
settingsRepository = settingsRepository,
serviceManager = serviceManager,
applicationScope = applicationScope,
ioDispatcher = ioDispatcher,
)
private val tunnelActiveStatePersister =
TunnelActiveStatePersister(
activeTunnels = activeTunnels,
tunnelsRepository = tunnelsRepository,
applicationScope = applicationScope,
ioDispatcher = ioDispatcher,
)
private val dynamicDnsHandler =
DynamicDnsHandler(
activeTunnels = activeTunnels,
tunnelsRepository = tunnelsRepository,
settingsRepository = settingsRepository,
localMessageEvents = localMessageEvents,
handleDnsReresolve = { config -> handleDnsReresolve(config) },
applicationScope = applicationScope,
ioDispatcher = ioDispatcher,
)
private val fullTunnelMonitorHandler =
TunnelMonitorHandler(
activeTunnels = activeTunnels,
tunnelsRepository = tunnelsRepository,
settingsRepository = settingsRepository,
monitoringSettingsRepository = monitoringSettingsRepository,
networkMonitor = networkMonitor,
networkUtils = networkUtils,
powerManager = powerManager,
logReader = logReader,
getStatistics = { id -> getStatistics(id) },
updateTunnelStatus = { id, status, stats, pings, logHealth ->
updateTunnelStatus(id, status, stats, pings, logHealth)
},
applicationScope = applicationScope,
ioDispatcher = ioDispatcher,
)
init {
applicationScope.launch(ioDispatcher) {
val initialEmit = AtomicBoolean(true)
settingsRepository.flow
.filterNotNull()
.filterNot { it == GeneralSettings() }
.distinctUntilChangedBy { it.appMode }
.collect { settings ->
val isInitialEmit = initialEmit.exchange(false)
val previousMode = currentAppMode.exchange(settings.appMode)
if (isInitialEmit) {
return@collect handleRestore(settings)
}
if (previousMode != settings.appMode) {
handleModeChangeCleanup(previousMode)
}
if (settings.appMode == AppMode.LOCK_DOWN) {
handleLockDownModeInit()
}
}
}
} }
// TODO this can crash if we haven't started foreground service yet, especially for // TODO this can crash if we haven't started foreground service yet, especially for
@@ -293,7 +242,7 @@ constructor(
if (lockdownSettings.bypassLan) TunnelConfig.IPV4_PUBLIC_NETWORKS else emptySet() if (lockdownSettings.bypassLan) TunnelConfig.IPV4_PUBLIC_NETWORKS else emptySet()
try { try {
if (serviceManager.hasVpnPermission()) { if (serviceManager.hasVpnPermission()) {
proxyUserspaceTunnel.setBackendMode( setBackendMode(
BackendMode.KillSwitch( BackendMode.KillSwitch(
allowedIps, allowedIps,
lockdownSettings.metered, lockdownSettings.metered,
@@ -308,28 +257,25 @@ constructor(
} }
} }
private suspend fun handleModeChangeCleanup( private suspend fun handleModeChangeCleanup(previousAppMode: AppMode) {
previousBackend: TunnelProvider, lifecycleManagers[previousAppMode]?.stopActiveTunnels()
previousAppMode: AppMode, if (previousAppMode == AppMode.LOCK_DOWN) {
) { lifecycleManagers[previousAppMode]?.setBackendMode(BackendMode.Inactive)
previousBackend.stopActiveTunnels() }
// stop lockdown if we switch from that mode
if (previousAppMode == AppMode.LOCK_DOWN)
proxyUserspaceTunnel.setBackendMode(BackendMode.Inactive)
} }
suspend fun handleRestore() = suspend fun handleRestore(settings: GeneralSettings? = null) =
withContext(ioDispatcher) { withContext(ioDispatcher) {
val settings = settingsRepository.getGeneralSettings() val currentSettings = settings ?: settingsRepository.getGeneralSettings()
val autoTunnelSettings = autoTunnelSettingsRepository.getAutoTunnelSettings() val autoTunnelSettings = autoTunnelSettingsRepository.getAutoTunnelSettings()
val tunnels = tunnelsRepository.userTunnelsFlow.firstOrNull() val tunnels = tunnelsRepository.userTunnelsFlow.firstOrNull()
if (autoTunnelSettings.isAutoTunnelEnabled) if (autoTunnelSettings.isAutoTunnelEnabled)
return@withContext restoreAutoTunnel(autoTunnelSettings) return@withContext restoreAutoTunnel(autoTunnelSettings)
if (settings.appMode == AppMode.LOCK_DOWN) handleLockDownModeInit() if (currentSettings.appMode == AppMode.LOCK_DOWN) handleLockDownModeInit()
if (tunnels?.any { it.isActive } == true) { if (tunnels?.any { it.isActive } == true) {
if (settings.appMode == AppMode.VPN && !serviceManager.hasVpnPermission()) if (currentSettings.appMode == AppMode.VPN && !serviceManager.hasVpnPermission())
return@withContext localErrorEvents.emit(null to NotAuthorized()) return@withContext localErrorEvents.emit(null to NotAuthorized())
when (settings.appMode) { when (currentSettings.appMode) {
AppMode.VPN, AppMode.VPN,
AppMode.PROXY, AppMode.PROXY,
AppMode.LOCK_DOWN -> { AppMode.LOCK_DOWN -> {
@@ -367,36 +313,6 @@ constructor(
} }
} }
private suspend fun handleTunnelsActiveChange(
previousActiveTuns: Map<Int, TunnelState>,
activeTuns: Map<Int, TunnelState>,
tuns: List<TunnelConfig>,
) {
val relevantTunnels = previousActiveTuns.keys + activeTuns.keys
relevantTunnels.forEach { tunnelId ->
val wasActive = previousActiveTuns.containsKey(tunnelId)
val isActiveNow = activeTuns.containsKey(tunnelId)
when {
!wasActive && isActiveNow -> {
tuns
.find { it.id == tunnelId }
?.let { dbTunnelConf ->
tunnelsRepository.save(dbTunnelConf.copy(isActive = true))
}
}
wasActive && !isActiveNow -> {
tuns
.find { it.id == tunnelId }
?.let { dbTunnelConf ->
tunnelsRepository.save(dbTunnelConf.copy(isActive = false))
}
}
}
}
}
suspend fun restartActiveTunnel(id: Int) = suspend fun restartActiveTunnel(id: Int) =
withContext(ioDispatcher) { withContext(ioDispatcher) {
val activeIds = activeTunnels.value.keys.toList() val activeIds = activeTunnels.value.keys.toList()
@@ -437,122 +353,7 @@ constructor(
.onFailure { e -> Timber.e(e, "Failed to restart tunnel ${tunnel.id}") } .onFailure { e -> Timber.e(e, "Failed to restart tunnel ${tunnel.id}") }
} }
private suspend fun handleDynamicDnsMonitoring(
activeTuns: Map<Int, TunnelState>,
configs: List<TunnelConfig>,
settings: GeneralSettings,
) =
ddnsMutex.withLock {
val activeIds =
activeTuns.keys
.filter { id ->
configs.find { it.id == id }?.restartOnPingFailure == true &&
settings.appMode != AppMode.KERNEL
}
.toSet()
val currentJobs = ddnsJobs.keys.toSet()
val obsoleteIds = currentJobs - activeIds
Timber.d(
"DDNS Monitoring: Active IDs: $activeIds, Obsolete IDs: $obsoleteIds, Total jobs before: ${ddnsJobs.size}"
)
obsoleteIds.forEach { id ->
ddnsJobs[id]?.cancel()
ddnsJobs.remove(id)
}
activeIds.forEach { id ->
if (ddnsJobs.containsKey(id)) return@forEach // Skip if already monitored
val conf = configs.find { it.id == id } ?: return@forEach
val tunStateFlow =
activeTunnels.map { it[id] }.stateIn(applicationScope + ioDispatcher)
val newJob =
applicationScope.launch(ioDispatcher) {
var backoff = 30_000L
while (isActive) {
val state = tunStateFlow.value ?: break
if (state.health() != TunnelState.Health.UNHEALTHY) {
backoff = BASE_BACKOFF
tunStateFlow.first {
it?.health() == TunnelState.Health.UNHEALTHY || it == null
}
continue
}
runCatching {
val updated = handleDnsReresolve(conf)
if (updated) {
localMessageEvents.emit(
conf.name to BackendMessage.DynamicDnsSuccess
)
backoff = BASE_BACKOFF
} else {
Timber.i(
"Dynamic DNS check completed, current endpoint address is already up to date."
)
}
}
.onFailure {
Timber.e(
it,
"Failed to handle dns re-resolution for ${conf.name}",
)
}
delay(backoff)
backoff = (backoff * 1.5).toLong().coerceAtMost(MAX_BACKOFF_TIME)
}
}
ddnsJobs[id] = newJob
}
}
private suspend fun handleFullTunnelMonitoring(
activeTuns: Map<Int, TunnelState>,
configs: List<TunnelConfig>,
settings: GeneralSettings,
) =
monitoringMutex.withLock {
val activeIds = activeTuns.keys.toSet()
val currentJobs = monitoringJobs.keys.toSet()
val obsoleteIds = currentJobs - activeIds
Timber.d(
"Monitoring: Active IDs: $activeIds, Obsolete IDs: $obsoleteIds, Total jobs before: ${monitoringJobs.size}"
)
obsoleteIds.forEach { id ->
monitoringJobs[id]?.cancel()
monitoringJobs.remove(id)
}
activeIds.forEach { id ->
if (monitoringJobs.containsKey(id)) return@forEach // Skip if already monitored
configs.find { it.id == id } ?: return@forEach
val tunStateFlow =
activeTunnels.map { it[id] }.stateIn(applicationScope + ioDispatcher)
val newJob =
applicationScope.launch(ioDispatcher) {
tunnelMonitor.startMonitoring(
id,
withLogs = settings.appMode != AppMode.KERNEL,
tunStateFlow = tunStateFlow,
getStatistics = { tunnelId -> getStatistics(tunnelId) },
updateTunnelStatus = { tid, _, stats, pings, logHealth ->
updateTunnelStatus(tid, null, stats, pings, logHealth)
},
)
}
monitoringJobs[id] = newJob
}
}
companion object { companion object {
const val BASE_BACKOFF = 30_000L
const val MAX_BACKOFF_TIME = 300_000L
const val RESTART_TUNNEL_DELAY = 300L const val RESTART_TUNNEL_DELAY = 300L
} }
} }
@@ -13,19 +13,12 @@ import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
interface TunnelProvider { interface TunnelProvider {
/** Starts the specified tunnel configuration. */ suspend fun startTunnel(tunnelConfig: TunnelConfig): Result<Unit>
suspend fun startTunnel(tunnelConfig: TunnelConfig)
/**
* Stops the specified tunnel.
*
* @param tunnelId The tunnelConf to stop.
*/
suspend fun stopTunnel(tunnelId: Int) suspend fun stopTunnel(tunnelId: Int)
suspend fun forceStopTunnel(tunnelId: Int) suspend fun forceStopTunnel(tunnelId: Int)
/** Stops all active tunnels. */
suspend fun stopActiveTunnels() suspend fun stopActiveTunnels()
fun setBackendMode(backendMode: BackendMode) fun setBackendMode(backendMode: BackendMode)
@@ -39,9 +32,7 @@ interface TunnelProvider {
fun getStatistics(tunnelId: Int): TunnelStatistics? fun getStatistics(tunnelId: Int): TunnelStatistics?
val activeTunnels: StateFlow<Map<Int, TunnelState>> val activeTunnels: StateFlow<Map<Int, TunnelState>>
val errorEvents: SharedFlow<Pair<String?, BackendCoreException>> val errorEvents: SharedFlow<Pair<String?, BackendCoreException>>
val messageEvents: SharedFlow<Pair<String?, BackendMessage>> val messageEvents: SharedFlow<Pair<String?, BackendMessage>>
suspend fun updateTunnelStatus( suspend fun updateTunnelStatus(
@@ -1,13 +1,10 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel package com.zaneschepke.wireguardautotunnel.core.tunnel.backend
import com.wireguard.android.backend.Backend import com.wireguard.android.backend.Backend
import com.wireguard.android.backend.BackendException import com.wireguard.android.backend.BackendException
import com.wireguard.android.backend.Tunnel as WgTunnel import com.wireguard.android.backend.Tunnel
import com.wireguard.android.backend.WgQuickBackend import com.wireguard.android.backend.WgQuickBackend
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.di.Kernel
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.events.DnsFailure import com.zaneschepke.wireguardautotunnel.domain.events.DnsFailure
@@ -22,26 +19,19 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendCoreException import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendCoreException
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.regex.Pattern import java.util.regex.Pattern
import javax.inject.Inject import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
class KernelTunnel class KernelTunnel(private val runConfigHelper: RunConfigHelper, private val backend: Backend) :
@Inject TunnelBackend {
constructor(
@ApplicationScope applicationScope: CoroutineScope,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
private val runConfigHelper: RunConfigHelper,
@Kernel private val backend: Backend,
) : BaseTunnel(applicationScope, ioDispatcher) {
private val runtimeTunnels = ConcurrentHashMap<Int, WgTunnel>() private val runtimeTunnels = ConcurrentHashMap<Int, Tunnel>()
private fun validateWireGuardInterfaceName(name: String): Result<Unit> { private fun validateWireGuardInterfaceName(name: String): Result<Unit> {
if (name.isEmpty() || name.length > 15) if (name.isEmpty() || name.length > 15)
@@ -57,10 +47,10 @@ constructor(
} }
override fun tunnelStateFlow(tunnelConfig: TunnelConfig): Flow<TunnelStatus> = callbackFlow { override fun tunnelStateFlow(tunnelConfig: TunnelConfig): Flow<TunnelStatus> = callbackFlow {
if (!WgQuickBackend.hasKernelSupport()) close(KernelWireguardNotSupported()) if (!WgQuickBackend.hasKernelSupport()) throw KernelWireguardNotSupported()
validateWireGuardInterfaceName(tunnelConfig.name).onFailure { close(it) } validateWireGuardInterfaceName(tunnelConfig.name).onFailure { throw it }
val stateChannel = Channel<WgTunnel.State>() val stateChannel = Channel<Tunnel.State>()
val runtimeTunnel = RuntimeWgTunnel(tunnelConfig, stateChannel) val runtimeTunnel = RuntimeWgTunnel(tunnelConfig, stateChannel)
runtimeTunnels[tunnelConfig.id] = runtimeTunnel runtimeTunnels[tunnelConfig.id] = runtimeTunnel
@@ -70,37 +60,31 @@ constructor(
} }
try { try {
withTimeout(STARTUP_TIMEOUT_MS) { val runConfig = runConfigHelper.buildWgRunConfig(tunnelConfig)
updateTunnelStatus(tunnelConfig.id, TunnelStatus.Starting) backend.setState(runtimeTunnel, Tunnel.State.UP, runConfig)
val runConfig = runConfigHelper.buildWgRunConfig(tunnelConfig)
backend.setState(runtimeTunnel, WgTunnel.State.UP, runConfig)
}
} catch (e: TimeoutCancellationException) { } catch (e: TimeoutCancellationException) {
Timber.e("Startup timed out for ${tunnelConfig.name}") Timber.Forest.e("Startup timed out for ${tunnelConfig.name}")
errors.emit(tunnelConfig.name to DnsFailure()) throw DnsFailure()
forceStopTunnel(tunnelConfig.id)
close()
} catch (e: BackendException) { } catch (e: BackendException) {
close(e.toBackendCoreException()) throw e.toBackendCoreException()
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
Timber.e(e, "Invalid backend arguments") Timber.Forest.e(e, "Invalid backend arguments")
close(InvalidConfig()) throw InvalidConfig()
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "Error while setting tunnel state") Timber.Forest.e(e, "Error while setting tunnel state")
close(UnknownError()) throw UnknownError()
} }
awaitClose { awaitClose {
try { try {
backend.setState(runtimeTunnel, WgTunnel.State.DOWN, null) backend.setState(runtimeTunnel, Tunnel.State.DOWN, null)
} catch (e: BackendException) { } catch (e: BackendException) {
errors.tryEmit(tunnelConfig.name to e.toBackendCoreException()) // Errors are emitted by caller (lifecycle manager)
} finally { } finally {
consumerJob.cancel() consumerJob.cancel()
stateChannel.close() stateChannel.close()
runtimeTunnels.remove(tunnelConfig.id) runtimeTunnels.remove(tunnelConfig.id)
trySend(TunnelStatus.Down) trySend(TunnelStatus.Down)
close()
} }
} }
} }
@@ -110,13 +94,13 @@ constructor(
val runtimeTunnel = runtimeTunnels[tunnelId] ?: return null val runtimeTunnel = runtimeTunnels[tunnelId] ?: return null
WireGuardStatistics(backend.getStatistics(runtimeTunnel)) WireGuardStatistics(backend.getStatistics(runtimeTunnel))
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "Failed to get stats for $tunnelId") Timber.Forest.e(e, "Failed to get stats for $tunnelId")
null null
} }
} }
override fun setBackendMode(backendMode: BackendMode) { override fun setBackendMode(backendMode: BackendMode) {
Timber.w("Not yet implemented for kernel") Timber.Forest.w("Not yet implemented for kernel")
} }
override fun getBackendMode(): BackendMode { override fun getBackendMode(): BackendMode {
@@ -134,15 +118,11 @@ constructor(
override suspend fun forceStopTunnel(tunnelId: Int) { override suspend fun forceStopTunnel(tunnelId: Int) {
val runtimeTunnel = runtimeTunnels[tunnelId] ?: return val runtimeTunnel = runtimeTunnels[tunnelId] ?: return
try { try {
backend.setState(runtimeTunnel, WgTunnel.State.DOWN, null) backend.setState(runtimeTunnel, Tunnel.State.DOWN, null)
} catch (e: BackendException) { } catch (e: BackendException) {
Timber.e(e, "Force stop failed for $tunnelId") Timber.Forest.e(e, "Force stop failed for $tunnelId")
} finally { } finally {
tunJobs[tunnelId]?.cancel()
runtimeTunnels.remove(tunnelId) runtimeTunnels.remove(tunnelId)
tunJobs.remove(tunnelId)
activeTuns.update { it - tunnelId }
updateTunnelStatus(tunnelId, TunnelStatus.Down)
} }
} }
} }
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel package com.zaneschepke.wireguardautotunnel.core.tunnel.backend
import com.zaneschepke.wireguardautotunnel.data.model.AppMode import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.data.model.DnsProtocol import com.zaneschepke.wireguardautotunnel.data.model.DnsProtocol
@@ -11,16 +11,13 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.DnsSettingsReposito
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.ProxySettingsRepository import com.zaneschepke.wireguardautotunnel.domain.repository.ProxySettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import java.util.* import java.util.Optional
import javax.inject.Inject
import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.firstOrNull
import org.amnezia.awg.config.Config import org.amnezia.awg.config.Config
import org.amnezia.awg.config.proxy.HttpProxy import org.amnezia.awg.config.proxy.HttpProxy
import org.amnezia.awg.config.proxy.Socks5Proxy import org.amnezia.awg.config.proxy.Socks5Proxy
class RunConfigHelper class RunConfigHelper(
@Inject
constructor(
private val settingsRepository: GeneralSettingRepository, private val settingsRepository: GeneralSettingRepository,
private val proxySettingsRepository: ProxySettingsRepository, private val proxySettingsRepository: ProxySettingsRepository,
private val dnsSettingsRepository: DnsSettingsRepository, private val dnsSettingsRepository: DnsSettingsRepository,
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel package com.zaneschepke.wireguardautotunnel.core.tunnel.backend
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel package com.zaneschepke.wireguardautotunnel.core.tunnel.backend
import com.wireguard.android.backend.Tunnel import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
@@ -0,0 +1,23 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel.backend
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import kotlinx.coroutines.flow.Flow
interface TunnelBackend {
fun tunnelStateFlow(tunnelConfig: TunnelConfig): Flow<TunnelStatus>
fun getStatistics(tunnelId: Int): TunnelStatistics?
fun setBackendMode(backendMode: BackendMode)
fun getBackendMode(): BackendMode
fun handleDnsReresolve(tunnelConfig: TunnelConfig): Boolean
suspend fun runningTunnelNames(): Set<String>
suspend fun forceStopTunnel(tunnelId: Int)
}
@@ -1,10 +1,12 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel package com.zaneschepke.wireguardautotunnel.core.tunnel.backend
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.events.* import com.zaneschepke.wireguardautotunnel.domain.events.DnsFailure
import com.zaneschepke.wireguardautotunnel.domain.events.InvalidConfig
import com.zaneschepke.wireguardautotunnel.domain.events.ServiceNotRunning
import com.zaneschepke.wireguardautotunnel.domain.events.UnknownError
import com.zaneschepke.wireguardautotunnel.domain.events.VpnUnauthorized
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.state.AmneziaStatistics import com.zaneschepke.wireguardautotunnel.domain.state.AmneziaStatistics
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
@@ -14,32 +16,25 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendCoreException import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendCoreException
import java.io.IOException import java.io.IOException
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch
import org.amnezia.awg.backend.Backend import org.amnezia.awg.backend.Backend
import org.amnezia.awg.backend.BackendException import org.amnezia.awg.backend.BackendException
import org.amnezia.awg.backend.Tunnel as AwgTunnel import org.amnezia.awg.backend.Tunnel
import timber.log.Timber import timber.log.Timber
class UserspaceTunnel class UserspaceTunnel(private val backend: Backend, private val runConfigHelper: RunConfigHelper) :
@Inject TunnelBackend {
constructor(
@ApplicationScope applicationScope: CoroutineScope,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
private val backend: Backend,
private val runConfigHelper: RunConfigHelper,
) : BaseTunnel(applicationScope, ioDispatcher) {
private val runtimeTunnels = ConcurrentHashMap<Int, AwgTunnel>() private val runtimeTunnels = ConcurrentHashMap<Int, Tunnel>()
override fun tunnelStateFlow(tunnelConfig: TunnelConfig): Flow<TunnelStatus> = callbackFlow { override fun tunnelStateFlow(tunnelConfig: TunnelConfig): Flow<TunnelStatus> = callbackFlow {
val stateChannel = Channel<AwgTunnel.State>() val stateChannel = Channel<Tunnel.State>()
val runtimeTunnel = RuntimeAwgTunnel(tunnelConfig, stateChannel) val runtimeTunnel = RuntimeAwgTunnel(tunnelConfig, stateChannel)
runtimeTunnels[tunnelConfig.id] = runtimeTunnel runtimeTunnels[tunnelConfig.id] = runtimeTunnel
@@ -49,36 +44,30 @@ constructor(
} }
try { try {
withTimeout(STARTUP_TIMEOUT_MS) { val runConfig = runConfigHelper.buildAmRunConfig(tunnelConfig)
updateTunnelStatus(tunnelConfig.id, TunnelStatus.Starting) backend.setState(runtimeTunnel, Tunnel.State.UP, runConfig)
val runConfig = runConfigHelper.buildAmRunConfig(tunnelConfig)
backend.setState(runtimeTunnel, AwgTunnel.State.UP, runConfig)
}
} catch (_: TimeoutCancellationException) { } catch (_: TimeoutCancellationException) {
Timber.e("Startup timed out for ${tunnelConfig.name} (likely DNS hang)") Timber.e("Startup timed out for ${tunnelConfig.name} (likely DNS hang)")
errors.emit(tunnelConfig.name to DnsFailure()) throw DnsFailure()
forceStopTunnel(tunnelConfig.id)
close()
} catch (e: BackendException) { } catch (e: BackendException) {
close(e.toBackendCoreException()) throw e.toBackendCoreException()
} catch (_: IllegalArgumentException) { } catch (_: IllegalArgumentException) {
close(InvalidConfig()) throw InvalidConfig()
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "Error while setting tunnel state") Timber.e(e, "Error while setting tunnel state")
close(UnknownError()) throw UnknownError()
} }
awaitClose { awaitClose {
try { try {
backend.setState(runtimeTunnel, AwgTunnel.State.DOWN, null) backend.setState(runtimeTunnel, Tunnel.State.DOWN, null)
} catch (e: BackendException) { } catch (e: BackendException) {
errors.tryEmit(tunnelConfig.name to e.toBackendCoreException()) // Errors emitted by caller
} finally { } finally {
consumerJob.cancel() consumerJob.cancel()
stateChannel.close() stateChannel.close()
runtimeTunnels.remove(tunnelConfig.id) runtimeTunnels.remove(tunnelConfig.id)
trySend(TunnelStatus.Down) trySend(TunnelStatus.Down)
close()
} }
} }
} }
@@ -89,7 +78,6 @@ constructor(
backend.backendMode = backendMode.asAmBackendMode() backend.backendMode = backendMode.asAmBackendMode()
} catch (e: BackendException) { } catch (e: BackendException) {
throw e.toBackendCoreException() throw e.toBackendCoreException()
// TODO this should be mapped to BackendException in the lib
} catch (_: IOException) { } catch (_: IOException) {
throw VpnUnauthorized() throw VpnUnauthorized()
} }
@@ -121,15 +109,11 @@ constructor(
override suspend fun forceStopTunnel(tunnelId: Int) { override suspend fun forceStopTunnel(tunnelId: Int) {
val runtimeTunnel = runtimeTunnels[tunnelId] ?: return val runtimeTunnel = runtimeTunnels[tunnelId] ?: return
try { try {
backend.setState(runtimeTunnel, AwgTunnel.State.DOWN, null) backend.setState(runtimeTunnel, Tunnel.State.DOWN, null)
} catch (e: BackendException) { } catch (e: BackendException) {
Timber.e(e, "Force stop failed for $tunnelId") Timber.e(e, "Force stop failed for $tunnelId")
} finally { } finally {
tunJobs[tunnelId]?.cancel()
runtimeTunnels.remove(tunnelId) runtimeTunnels.remove(tunnelId)
tunJobs.remove(tunnelId)
activeTuns.update { it - tunnelId }
updateTunnelStatus(tunnelId, TunnelStatus.Down)
} }
} }
} }
@@ -0,0 +1,114 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel.handler
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import java.util.concurrent.ConcurrentHashMap
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import timber.log.Timber
class DynamicDnsHandler(
private val activeTunnels: StateFlow<Map<Int, TunnelState>>,
private val tunnelsRepository: TunnelRepository,
private val settingsRepository: GeneralSettingRepository,
private val localMessageEvents: MutableSharedFlow<Pair<String?, BackendMessage>>,
private val handleDnsReresolve: (TunnelConfig) -> Boolean,
private val applicationScope: CoroutineScope,
private val ioDispatcher: CoroutineDispatcher,
) {
private val mutex = Mutex()
private val jobs = ConcurrentHashMap<Int, Job>()
init {
applicationScope.launch(ioDispatcher) {
combine(activeTunnels, settingsRepository.flow.filterNotNull()) { active, settings ->
active to settings
}
.collect { (activeTuns, settings) ->
mutex.withLock {
val activeIds =
activeTuns.keys
.filter { id ->
val config =
tunnelsRepository.getById(id) ?: return@filter false
config.restartOnPingFailure &&
settings.appMode != AppMode.KERNEL
}
.toSet()
(jobs.keys - activeIds).forEach { id ->
Timber.d("Shutting down Dynamic DNS monitoring job for tunnelId: $id")
jobs.remove(id)?.cancel()
}
activeIds.forEach { id ->
if (jobs.containsKey(id)) return@forEach
val config = tunnelsRepository.getById(id) ?: return@forEach
val tunStateFlow =
activeTunnels
.map { it[id] }
.stateIn(applicationScope + ioDispatcher)
Timber.d("Starting Dynamic DNS monitoring job for tunnelId: $id")
jobs[id] =
applicationScope.launch(ioDispatcher) {
monitorDynamicDns(config, tunStateFlow)
}
}
}
}
}
}
private suspend fun monitorDynamicDns(
config: TunnelConfig,
tunStateFlow: StateFlow<TunnelState?>,
) {
var backoff = BASE_BACKOFF
while (true) {
val state = tunStateFlow.value ?: break
if (state.health() != TunnelState.Health.UNHEALTHY) {
backoff = BASE_BACKOFF
tunStateFlow.first { it?.health() == TunnelState.Health.UNHEALTHY || it == null }
continue
}
runCatching {
val updated = handleDnsReresolve(config)
if (updated) {
localMessageEvents.emit(config.name to BackendMessage.DynamicDnsSuccess)
backoff = BASE_BACKOFF
} else {
Timber.i(
"Dynamic DNS check completed, current endpoint address is already up to date."
)
}
}
.onFailure { Timber.e(it, "Failed to handle dns re-resolution for ${config.name}") }
delay(backoff)
backoff = (backoff * 1.5).toLong().coerceAtMost(MAX_BACKOFF_TIME)
}
}
companion object {
const val BASE_BACKOFF = 30_000L
const val MAX_BACKOFF_TIME = 300_000L
}
}
@@ -0,0 +1,47 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel.handler
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
class TunnelActiveStatePersister(
private val activeTunnels: StateFlow<Map<Int, TunnelState>>,
private val tunnelsRepository: TunnelRepository,
applicationScope: CoroutineScope,
ioDispatcher: CoroutineDispatcher,
) {
private var previousActiveIds: Set<Int> = emptySet()
init {
applicationScope.launch(ioDispatcher) {
activeTunnels.collect { currentActive ->
val currentActiveIds = currentActive.keys
if (currentActiveIds == previousActiveIds) return@collect
val tunnels = tunnelsRepository.userTunnelsFlow.firstOrNull() ?: return@collect
val tunnelsById = tunnels.associateBy { it.id }
val relevantIds = previousActiveIds + currentActiveIds
supervisorScope {
relevantIds.forEach { id ->
launch {
val config = tunnelsById[id] ?: return@launch
val wasActive = previousActiveIds.contains(id)
val isActive = currentActiveIds.contains(id)
if (wasActive != isActive) {
tunnelsRepository.save(config.copy(isActive = isActive))
}
}
}
}
previousActiveIds = currentActiveIds.toSet()
}
}
}
}
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel package com.zaneschepke.wireguardautotunnel.core.tunnel.handler
import android.os.PowerManager import android.os.PowerManager
import com.zaneschepke.logcatter.LogReader import com.zaneschepke.logcatter.LogReader
@@ -9,35 +9,107 @@ import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.state.* import com.zaneschepke.wireguardautotunnel.domain.state.FailureReason
import com.zaneschepke.wireguardautotunnel.domain.state.LogHealthState
import com.zaneschepke.wireguardautotunnel.domain.state.PingState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.util.extensions.toMillis import com.zaneschepke.wireguardautotunnel.util.extensions.toMillis
import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils
import inet.ipaddr.AddressValueException import inet.ipaddr.AddressValueException
import inet.ipaddr.IPAddress import inet.ipaddr.IPAddress
import inet.ipaddr.IPAddressString import inet.ipaddr.IPAddressString
import io.ktor.util.collections.* import io.ktor.util.collections.ConcurrentMap
import javax.inject.Inject import java.util.concurrent.ConcurrentHashMap
import javax.inject.Singleton import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.* import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.* import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withTimeout
import timber.log.Timber import timber.log.Timber
@Singleton class TunnelMonitorHandler(
class TunnelMonitor private val activeTunnels: StateFlow<Map<Int, TunnelState>>,
@Inject
constructor(
private val settingsRepository: GeneralSettingRepository,
private val tunnelsRepository: TunnelRepository, private val tunnelsRepository: TunnelRepository,
private val settingsRepository: GeneralSettingRepository,
private val monitoringSettingsRepository: MonitoringSettingsRepository, private val monitoringSettingsRepository: MonitoringSettingsRepository,
private val networkMonitor: NetworkMonitor, private val networkMonitor: NetworkMonitor,
private val networkUtils: NetworkUtils, private val networkUtils: NetworkUtils,
private val logReader: LogReader, private val logReader: LogReader,
private val powerManager: PowerManager, private val powerManager: PowerManager,
private val getStatistics: (Int) -> TunnelStatistics?,
private val updateTunnelStatus:
suspend (
Int, TunnelStatus?, TunnelStatistics?, Map<String, PingState>?, LogHealthState?,
) -> Unit,
private val applicationScope: CoroutineScope,
private val ioDispatcher: CoroutineDispatcher,
) { ) {
private val mutex = Mutex()
private val jobs = ConcurrentHashMap<Int, Job>()
init {
applicationScope.launch(ioDispatcher) {
activeTunnels.collect { activeTuns ->
mutex.withLock {
val activeIds = activeTuns.keys.toSet()
(jobs.keys - activeIds).forEach { id ->
Timber.d("Shutting down tunnel monitoring job for tunnelId: $id")
jobs.remove(id)?.cancel()
}
val tunnels = tunnelsRepository.flow.firstOrNull() ?: return@collect
val tunnelsById = tunnels.associateBy { it.id }
activeIds.forEach { id ->
if (jobs.containsKey(id)) return@forEach
val config = tunnelsById[id] ?: return@forEach
val settings = settingsRepository.flow.filterNotNull().first()
val tunStateFlow =
activeTunnels.map { it[id] }.stateIn(applicationScope + ioDispatcher)
jobs[id] =
applicationScope.launch(ioDispatcher) {
Timber.d("Starting tunnel monitoring job for tunnelId: $id")
startMonitoring(
config = config,
withLogs = settings.appMode != AppMode.KERNEL,
tunStateFlow = tunStateFlow,
getStatistics = { tunnelId -> getStatistics(tunnelId) },
updateTunnelStatus = { tid, _, stats, pings, logHealth ->
updateTunnelStatus(tid, null, stats, pings, logHealth)
},
)
}
}
}
}
}
}
@OptIn(FlowPreview::class) @OptIn(FlowPreview::class)
suspend fun startMonitoring( private suspend fun startMonitoring(
tunnelId: Int, config: TunnelConfig,
withLogs: Boolean, withLogs: Boolean,
tunStateFlow: StateFlow<TunnelState?>, tunStateFlow: StateFlow<TunnelState?>,
getStatistics: suspend (Int) -> TunnelStatistics?, getStatistics: suspend (Int) -> TunnelStatistics?,
@@ -45,13 +117,10 @@ constructor(
suspend ( suspend (
Int, TunnelStatus?, TunnelStatistics?, Map<String, PingState>?, LogHealthState?, Int, TunnelStatus?, TunnelStatistics?, Map<String, PingState>?, LogHealthState?,
) -> Unit, ) -> Unit,
): Job = coroutineScope { ) = coroutineScope {
launch { launch { startPingMonitor(config, tunStateFlow, updateTunnelStatus) }
val config = tunnelsRepository.getById(tunnelId) ?: return@launch launch { startWgStatsPoll(config.id, getStatistics, updateTunnelStatus) }
launch { startPingMonitor(config, tunStateFlow, updateTunnelStatus) } if (withLogs) launch { startLogsMonitor(config, updateTunnelStatus) }
launch { startWgStatsPoll(tunnelId, getStatistics, updateTunnelStatus) }
if (withLogs) launch { startLogsMonitor(config, updateTunnelStatus) }
}
} }
private suspend fun startLogsMonitor( private suspend fun startLogsMonitor(
@@ -250,7 +319,7 @@ constructor(
tunStateFlow.filter { state -> state?.status is TunnelStatus.Up }.first() tunStateFlow.filter { state -> state?.status is TunnelStatus.Up }.first()
// small delay to make sure tunnel is fully up before we actively monitor // small delay to make sure tunnel is fully up before we actively monitor
delay(3_000L) delay(PING_MONITOR_START_DELAY)
while (isActive) { while (isActive) {
ensureActive() ensureActive()
@@ -302,7 +371,6 @@ constructor(
} }
companion object { companion object {
private val successLogRegex = private val successLogRegex =
Regex("Received handshake response|Receiving keepalive packet", RegexOption.IGNORE_CASE) Regex("Received handshake response|Receiving keepalive packet", RegexOption.IGNORE_CASE)
@@ -317,5 +385,6 @@ constructor(
const val CLOUDFLARE_IPV6_IP = "2606:4700:4700::1111" const val CLOUDFLARE_IPV6_IP = "2606:4700:4700::1111"
const val CLOUDFLARE_IPV4_IP = "1.1.1.1" const val CLOUDFLARE_IPV4_IP = "1.1.1.1"
const val STATS_DELAY = 1_000L const val STATS_DELAY = 1_000L
const val PING_MONITOR_START_DELAY = 5_000L
} }
} }
@@ -0,0 +1,36 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel.handler
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
import timber.log.Timber
class TunnelServiceHandler(
private val activeTunnels: StateFlow<Map<Int, TunnelState>>,
private val settingsRepository: GeneralSettingRepository,
private val serviceManager: ServiceManager,
applicationScope: CoroutineScope,
ioDispatcher: CoroutineDispatcher,
) {
init {
applicationScope.launch(ioDispatcher) {
activeTunnels.collect { activeTuns ->
if (activeTuns.isEmpty()) {
Timber.d("Stopping tunnel service, no tunnels active.")
serviceManager.stopTunnelService()
} else if (serviceManager.tunnelService.value == null) {
val settings = settingsRepository.flow.firstOrNull() ?: GeneralSettings()
Timber.d("Starting tunnel foreground service for active tunnel.")
serviceManager.startTunnelService(settings.appMode)
}
serviceManager.updateTunnelTile()
}
}
}
}
@@ -1,31 +1,25 @@
package com.zaneschepke.wireguardautotunnel.core.worker package com.zaneschepke.wireguardautotunnel.core.worker
import android.content.Context import android.content.Context
import androidx.hilt.work.HiltWorker import androidx.work.CoroutineWorker
import androidx.work.* 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.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
@HiltWorker class ServiceWorker(
class ServiceWorker context: Context,
@AssistedInject params: WorkerParameters,
constructor(
@Assisted private val context: Context,
@Assisted private val params: WorkerParameters,
private val serviceManager: ServiceManager, private val serviceManager: ServiceManager,
private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository, private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : CoroutineWorker(context, params) { ) : CoroutineWorker(context, params) {
companion object { companion object {
private const val TAG = "service_worker" private const val TAG = "auto_tunnel_service_monitor"
fun stop(context: Context) { fun stop(context: Context) {
WorkManager.getInstance(context).cancelAllWorkByTag(TAG) WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
@@ -47,16 +41,15 @@ constructor(
} }
} }
override suspend fun doWork(): Result = override suspend fun doWork(): Result {
withContext(ioDispatcher) { Timber.i("Service worker started")
Timber.i("Service worker started") with(autoTunnelSettingsRepository.getAutoTunnelSettings()) {
with(autoTunnelSettingsRepository.getAutoTunnelSettings()) { Timber.i("Checking to see if auto-tunnel has been killed by system")
Timber.i("Checking to see if auto-tunnel has been killed by system") if (isAutoTunnelEnabled && serviceManager.autoTunnelService.value == null) {
if (isAutoTunnelEnabled && serviceManager.autoTunnelService.value == null) { Timber.i("Service has been killed by system, restoring.")
Timber.i("Service has been killed by system, restoring.") serviceManager.startAutoTunnelService()
serviceManager.startAutoTunnelService()
}
} }
Result.success() return Result.success()
} }
}
} }
@@ -5,7 +5,6 @@ import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.preferencesDataStore import androidx.datastore.preferences.preferencesDataStore
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import java.io.IOException import java.io.IOException
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@@ -17,7 +16,7 @@ import timber.log.Timber
class DataStoreManager( class DataStoreManager(
private val context: Context, private val context: Context,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher, private val ioDispatcher: CoroutineDispatcher,
) { ) {
private val preferencesKey = "preferences" private val preferencesKey = "preferences"
val Context.dataStore by preferencesDataStore(name = preferencesKey) val Context.dataStore by preferencesDataStore(name = preferencesKey)
@@ -2,12 +2,9 @@ package com.zaneschepke.wireguardautotunnel.data
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteDatabase
import javax.inject.Inject
import javax.inject.Provider
import timber.log.Timber import timber.log.Timber
class DatabaseCallback @Inject constructor(private val databaseProvider: Provider<AppDatabase>) : class DatabaseCallback(private val databaseProvider: Lazy<AppDatabase>) : RoomDatabase.Callback() {
RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) { override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db) super.onCreate(db)
Timber.d("Database created, inserting default rows") Timber.d("Database created, inserting default rows")
@@ -4,6 +4,7 @@ import androidx.room.Dao
import androidx.room.Query import androidx.room.Query
import androidx.room.Upsert import androidx.room.Upsert
import com.zaneschepke.wireguardautotunnel.data.entity.GeneralSettings import com.zaneschepke.wireguardautotunnel.data.entity.GeneralSettings
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@Dao @Dao
@@ -15,4 +16,16 @@ interface GeneralSettingsDao {
@Query("SELECT * FROM general_settings LIMIT 1") @Query("SELECT * FROM general_settings LIMIT 1")
fun getGeneralSettingsFlow(): Flow<GeneralSettings?> fun getGeneralSettingsFlow(): Flow<GeneralSettings?>
@Query("UPDATE general_settings SET theme = :theme WHERE id = 1")
suspend fun updateTheme(theme: String)
@Query("UPDATE general_settings SET locale = :locale WHERE id = 1")
suspend fun updateLocale(locale: String)
@Query("UPDATE general_settings SET is_pin_lock_enabled = :enabled WHERE id = 1")
suspend fun updatePinLockEnabled(enabled: Boolean)
@Query("UPDATE general_settings SET app_mode = :appMode WHERE id = 1")
suspend fun updateAppMode(appMode: AppMode)
} }
@@ -3,8 +3,6 @@ package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.DataStoreManager import com.zaneschepke.wireguardautotunnel.data.DataStoreManager
import com.zaneschepke.wireguardautotunnel.data.entity.AppState as Entity import com.zaneschepke.wireguardautotunnel.data.entity.AppState as Entity
import com.zaneschepke.wireguardautotunnel.data.mapper.toDomain import com.zaneschepke.wireguardautotunnel.data.mapper.toDomain
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.model.AppState as Domain import com.zaneschepke.wireguardautotunnel.domain.model.AppState as Domain
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
@@ -18,8 +16,8 @@ import timber.log.Timber
class DataStoreAppStateRepository( class DataStoreAppStateRepository(
private val dataStoreManager: DataStoreManager, private val dataStoreManager: DataStoreManager,
@ApplicationScope private val applicationScope: CoroutineScope, applicationScope: CoroutineScope,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher, ioDispatcher: CoroutineDispatcher,
) : AppStateRepository { ) : AppStateRepository {
override suspend fun isLocationDisclosureShown(): Boolean { override suspend fun isLocationDisclosureShown(): Boolean {
return dataStoreManager.getFromStore(DataStoreManager.locationDisclosureShown) ?: false return dataStoreManager.getFromStore(DataStoreManager.locationDisclosureShown) ?: false
@@ -4,16 +4,17 @@ import android.content.Context
import com.zaneschepke.wireguardautotunnel.BuildConfig import com.zaneschepke.wireguardautotunnel.BuildConfig
import com.zaneschepke.wireguardautotunnel.data.mapper.GitHubReleaseMapper import com.zaneschepke.wireguardautotunnel.data.mapper.GitHubReleaseMapper
import com.zaneschepke.wireguardautotunnel.data.network.GitHubApi import com.zaneschepke.wireguardautotunnel.data.network.GitHubApi
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.model.AppUpdate import com.zaneschepke.wireguardautotunnel.domain.model.AppUpdate
import com.zaneschepke.wireguardautotunnel.domain.repository.UpdateRepository import com.zaneschepke.wireguardautotunnel.domain.repository.UpdateRepository
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.NumberUtils import com.zaneschepke.wireguardautotunnel.util.NumberUtils
import io.ktor.client.* import io.ktor.client.HttpClient
import io.ktor.client.request.* import io.ktor.client.request.get
import io.ktor.client.statement.* import io.ktor.client.statement.HttpResponse
import io.ktor.http.* import io.ktor.client.statement.bodyAsChannel
import io.ktor.utils.io.* import io.ktor.http.contentLength
import io.ktor.utils.io.ByteReadChannel
import io.ktor.utils.io.readAvailable
import java.io.File import java.io.File
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -25,8 +26,9 @@ class GitHubUpdateRepository(
private val githubOwner: String, private val githubOwner: String,
private val githubRepo: String, private val githubRepo: String,
private val context: Context, private val context: Context,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher, private val ioDispatcher: CoroutineDispatcher,
) : UpdateRepository { ) : UpdateRepository {
override suspend fun checkForUpdate(currentVersion: String): Result<AppUpdate?> = override suspend fun checkForUpdate(currentVersion: String): Result<AppUpdate?> =
withContext(ioDispatcher) { withContext(ioDispatcher) {
Timber.i("Checking for update") Timber.i("Checking for update")
@@ -2,22 +2,16 @@ package com.zaneschepke.wireguardautotunnel.data.repository
import android.content.Context import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.model.InstalledPackage import com.zaneschepke.wireguardautotunnel.domain.model.InstalledPackage
import com.zaneschepke.wireguardautotunnel.domain.repository.InstalledPackageRepository import com.zaneschepke.wireguardautotunnel.domain.repository.InstalledPackageRepository
import com.zaneschepke.wireguardautotunnel.util.extensions.getFriendlyAppName import com.zaneschepke.wireguardautotunnel.util.extensions.getFriendlyAppName
import javax.inject.Singleton
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
@Singleton
class InstalledAndroidPackageRepository( class InstalledAndroidPackageRepository(
private val context: Context, private val context: Context,
@ApplicationScope val applicationScope: CoroutineScope, private val ioDispatcher: CoroutineDispatcher,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : InstalledPackageRepository { ) : InstalledPackageRepository {
private var cachedPackages: List<InstalledPackage>? = null private var cachedPackages: List<InstalledPackage>? = null
@@ -4,28 +4,20 @@ import com.zaneschepke.wireguardautotunnel.data.dao.AutoTunnelSettingsDao
import com.zaneschepke.wireguardautotunnel.data.entity.AutoTunnelSettings as Entity import com.zaneschepke.wireguardautotunnel.data.entity.AutoTunnelSettings as Entity
import com.zaneschepke.wireguardautotunnel.data.mapper.toDomain import com.zaneschepke.wireguardautotunnel.data.mapper.toDomain
import com.zaneschepke.wireguardautotunnel.data.mapper.toEntity import com.zaneschepke.wireguardautotunnel.data.mapper.toEntity
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.model.AutoTunnelSettings as Domain import com.zaneschepke.wireguardautotunnel.domain.model.AutoTunnelSettings as Domain
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
class RoomAutoTunnelSettingsRepository( class RoomAutoTunnelSettingsRepository(private val autoTunnelSettingsDao: AutoTunnelSettingsDao) :
private val autoTunnelSettingsDao: AutoTunnelSettingsDao, AutoTunnelSettingsRepository {
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : AutoTunnelSettingsRepository {
override suspend fun upsert(autoTunnelSettings: Domain) { override suspend fun upsert(autoTunnelSettings: Domain) {
autoTunnelSettingsDao.upsert(autoTunnelSettings.toEntity()) autoTunnelSettingsDao.upsert(autoTunnelSettings.toEntity())
} }
override val flow: Flow<Domain> override val flow: Flow<Domain>
get() = get() =
autoTunnelSettingsDao autoTunnelSettingsDao.getAutoTunnelSettingsFlow().map { (it ?: Entity()).toDomain() }
.getAutoTunnelSettingsFlow()
.map { (it ?: Entity()).toDomain() }
.flowOn(ioDispatcher)
override suspend fun getAutoTunnelSettings(): Domain { override suspend fun getAutoTunnelSettings(): Domain {
return (autoTunnelSettingsDao.getAutoTunnelSettings() ?: Entity()).toDomain() return (autoTunnelSettingsDao.getAutoTunnelSettings() ?: Entity()).toDomain()
@@ -4,28 +4,19 @@ import com.zaneschepke.wireguardautotunnel.data.dao.DnsSettingsDao
import com.zaneschepke.wireguardautotunnel.data.entity.DnsSettings as Entity import com.zaneschepke.wireguardautotunnel.data.entity.DnsSettings as Entity
import com.zaneschepke.wireguardautotunnel.data.mapper.toDomain import com.zaneschepke.wireguardautotunnel.data.mapper.toDomain
import com.zaneschepke.wireguardautotunnel.data.mapper.toEntity import com.zaneschepke.wireguardautotunnel.data.mapper.toEntity
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.model.DnsSettings as Domain import com.zaneschepke.wireguardautotunnel.domain.model.DnsSettings as Domain
import com.zaneschepke.wireguardautotunnel.domain.repository.DnsSettingsRepository import com.zaneschepke.wireguardautotunnel.domain.repository.DnsSettingsRepository
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
class RoomDnsSettingsRepository( class RoomDnsSettingsRepository(private val dnsSettingsDao: DnsSettingsDao) :
private val dnsSettingsDao: DnsSettingsDao, DnsSettingsRepository {
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : DnsSettingsRepository {
override suspend fun upsert(dnsSettings: Domain) { override suspend fun upsert(dnsSettings: Domain) {
dnsSettingsDao.upsert(dnsSettings.toEntity()) dnsSettingsDao.upsert(dnsSettings.toEntity())
} }
override val flow: Flow<Domain> override val flow: Flow<Domain>
get() = get() = dnsSettingsDao.getDnsSettingsFlow().map { (it ?: Entity()).toDomain() }
dnsSettingsDao
.getDnsSettingsFlow()
.map { (it ?: Entity()).toDomain() }
.flowOn(ioDispatcher)
override suspend fun getDnsSettings(): Domain { override suspend fun getDnsSettings(): Domain {
return (dnsSettingsDao.getDnsSettings() ?: Entity()).toDomain() return (dnsSettingsDao.getDnsSettings() ?: Entity()).toDomain()
@@ -4,31 +4,20 @@ import com.zaneschepke.wireguardautotunnel.data.dao.LockdownSettingsDao
import com.zaneschepke.wireguardautotunnel.data.entity.LockdownSettings as Entity import com.zaneschepke.wireguardautotunnel.data.entity.LockdownSettings as Entity
import com.zaneschepke.wireguardautotunnel.data.mapper.toDomain import com.zaneschepke.wireguardautotunnel.data.mapper.toDomain
import com.zaneschepke.wireguardautotunnel.data.mapper.toEntity import com.zaneschepke.wireguardautotunnel.data.mapper.toEntity
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.model.LockdownSettings as Domain import com.zaneschepke.wireguardautotunnel.domain.model.LockdownSettings as Domain
import com.zaneschepke.wireguardautotunnel.domain.repository.LockdownSettingsRepository import com.zaneschepke.wireguardautotunnel.domain.repository.LockdownSettingsRepository
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
class RoomLockdownSettingsRepository( class RoomLockdownSettingsRepository(private val lockdownSettingsDao: LockdownSettingsDao) :
private val lockdownSettingsDao: LockdownSettingsDao, LockdownSettingsRepository {
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : LockdownSettingsRepository {
override suspend fun upsert(lockdownSettings: Domain) { override suspend fun upsert(lockdownSettings: Domain) {
withContext(ioDispatcher) { lockdownSettingsDao.upsert(lockdownSettings.toEntity()) } lockdownSettingsDao.upsert(lockdownSettings.toEntity())
} }
override val flow = override val flow =
lockdownSettingsDao lockdownSettingsDao.getLockdownSettingsFlow().map { (it ?: Entity()).toDomain() }
.getLockdownSettingsFlow()
.map { (it ?: Entity()).toDomain() }
.flowOn(ioDispatcher)
override suspend fun getLockdownSettings(): Domain { override suspend fun getLockdownSettings(): Domain {
return withContext(ioDispatcher) { return (lockdownSettingsDao.getLockdownSettings() ?: Entity()).toDomain()
(lockdownSettingsDao.getLockdownSettings() ?: Entity()).toDomain()
}
} }
} }
@@ -4,28 +4,20 @@ import com.zaneschepke.wireguardautotunnel.data.dao.MonitoringSettingsDao
import com.zaneschepke.wireguardautotunnel.data.entity.MonitoringSettings as Entity import com.zaneschepke.wireguardautotunnel.data.entity.MonitoringSettings as Entity
import com.zaneschepke.wireguardautotunnel.data.mapper.toDomain import com.zaneschepke.wireguardautotunnel.data.mapper.toDomain
import com.zaneschepke.wireguardautotunnel.data.mapper.toEntity import com.zaneschepke.wireguardautotunnel.data.mapper.toEntity
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.model.MonitoringSettings as Domain import com.zaneschepke.wireguardautotunnel.domain.model.MonitoringSettings as Domain
import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
class RoomMonitoringSettingsRepository( class RoomMonitoringSettingsRepository(private val monitoringSettingsDao: MonitoringSettingsDao) :
private val monitoringSettingsDao: MonitoringSettingsDao, MonitoringSettingsRepository {
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : MonitoringSettingsRepository {
override suspend fun upsert(monitoringSettings: Domain) { override suspend fun upsert(monitoringSettings: Domain) {
monitoringSettingsDao.upsert(monitoringSettings.toEntity()) monitoringSettingsDao.upsert(monitoringSettings.toEntity())
} }
override val flow: Flow<Domain> override val flow: Flow<Domain>
get() = get() =
monitoringSettingsDao monitoringSettingsDao.getMonitoringSettingsFlow().map { (it ?: Entity()).toDomain() }
.getMonitoringSettingsFlow()
.map { (it ?: Entity()).toDomain() }
.flowOn(ioDispatcher)
override suspend fun getMonitoringSettings(): Domain { override suspend fun getMonitoringSettings(): Domain {
return (monitoringSettingsDao.getMonitoringSettings() ?: Entity()).toDomain() return (monitoringSettingsDao.getMonitoringSettings() ?: Entity()).toDomain()
@@ -4,31 +4,20 @@ import com.zaneschepke.wireguardautotunnel.data.dao.ProxySettingsDao
import com.zaneschepke.wireguardautotunnel.data.entity.ProxySettings as Entity import com.zaneschepke.wireguardautotunnel.data.entity.ProxySettings as Entity
import com.zaneschepke.wireguardautotunnel.data.mapper.toDomain import com.zaneschepke.wireguardautotunnel.data.mapper.toDomain
import com.zaneschepke.wireguardautotunnel.data.mapper.toEntity import com.zaneschepke.wireguardautotunnel.data.mapper.toEntity
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.model.ProxySettings as Domain import com.zaneschepke.wireguardautotunnel.domain.model.ProxySettings as Domain
import com.zaneschepke.wireguardautotunnel.domain.repository.ProxySettingsRepository import com.zaneschepke.wireguardautotunnel.domain.repository.ProxySettingsRepository
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
class RoomProxySettingsRepository( class RoomProxySettingsRepository(private val proxySettingsDao: ProxySettingsDao) :
private val proxySettingsDao: ProxySettingsDao, ProxySettingsRepository {
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : ProxySettingsRepository {
override suspend fun upsert(proxySettings: Domain) { override suspend fun upsert(proxySettings: Domain) {
withContext(ioDispatcher) { proxySettingsDao.upsert(proxySettings.toEntity()) } proxySettingsDao.upsert(proxySettings.toEntity())
} }
override val flow = override val flow = proxySettingsDao.getProxySettingsFlow().map { (it ?: Entity()).toDomain() }
proxySettingsDao
.getProxySettingsFlow()
.map { (it ?: Entity()).toDomain() }
.flowOn(ioDispatcher)
override suspend fun getProxySettings(): Domain { override suspend fun getProxySettings(): Domain {
return withContext(ioDispatcher) { return (proxySettingsDao.getProxySettings() ?: Entity()).toDomain()
(proxySettingsDao.getProxySettings() ?: Entity()).toDomain()
}
} }
} }
@@ -4,32 +4,37 @@ import com.zaneschepke.wireguardautotunnel.data.dao.GeneralSettingsDao
import com.zaneschepke.wireguardautotunnel.data.entity.GeneralSettings as Entity import com.zaneschepke.wireguardautotunnel.data.entity.GeneralSettings as Entity
import com.zaneschepke.wireguardautotunnel.data.mapper.toDomain import com.zaneschepke.wireguardautotunnel.data.mapper.toDomain
import com.zaneschepke.wireguardautotunnel.data.mapper.toEntity import com.zaneschepke.wireguardautotunnel.data.mapper.toEntity
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings as Domain import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings as Domain
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import kotlinx.coroutines.CoroutineDispatcher import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
class RoomSettingsRepository(
private val settingsDoa: GeneralSettingsDao,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : GeneralSettingRepository {
class RoomSettingsRepository(private val settingsDao: GeneralSettingsDao) :
GeneralSettingRepository {
override suspend fun upsert(generalSettings: Domain) { override suspend fun upsert(generalSettings: Domain) {
withContext(ioDispatcher) { settingsDoa.upsert(generalSettings.toEntity()) } settingsDao.upsert(generalSettings.toEntity())
} }
override val flow = override val flow = settingsDao.getGeneralSettingsFlow().map { (it ?: Entity()).toDomain() }
settingsDoa
.getGeneralSettingsFlow()
.map { (it ?: Entity()).toDomain() }
.flowOn(ioDispatcher)
override suspend fun getGeneralSettings(): Domain { override suspend fun getGeneralSettings(): Domain {
return withContext(ioDispatcher) { return (settingsDao.getGeneralSettings() ?: Entity()).toDomain()
(settingsDoa.getGeneralSettings() ?: Entity()).toDomain() }
}
override suspend fun updateTheme(theme: Theme) {
settingsDao.updateTheme(theme.name)
}
override suspend fun updateLocale(locale: String) {
settingsDao.updateLocale(locale)
}
override suspend fun updatePinLockEnabled(enabled: Boolean) {
settingsDao.updatePinLockEnabled(enabled)
}
override suspend fun updateAppMode(appMode: AppMode) {
settingsDao.updateAppMode(appMode)
} }
} }
@@ -3,119 +3,96 @@ package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.dao.TunnelConfigDao import com.zaneschepke.wireguardautotunnel.data.dao.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.data.mapper.toDomain import com.zaneschepke.wireguardautotunnel.data.mapper.toDomain
import com.zaneschepke.wireguardautotunnel.data.mapper.toEntity import com.zaneschepke.wireguardautotunnel.data.mapper.toEntity
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig as Domain import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig as Domain
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
class RoomTunnelRepository( class RoomTunnelRepository(private val tunnelConfigDao: TunnelConfigDao) : TunnelRepository {
private val tunnelConfigDao: TunnelConfigDao,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : TunnelRepository {
override val flow = override val flow =
tunnelConfigDao.getAllFlow().flowOn(ioDispatcher).map { tunnelConfigDao.getAllFlow().map { it.map { tunnelConfig -> tunnelConfig.toDomain() } }
it.map { tunnelConfig -> tunnelConfig.toDomain() }
}
override val userTunnelsFlow = override val userTunnelsFlow =
tunnelConfigDao.getAllTunnelsExceptGlobal().flowOn(ioDispatcher).map { tunnelConfigDao.getAllTunnelsExceptGlobal().map {
it.map { tunnelConfig -> tunnelConfig.toDomain() } it.map { tunnelConfig -> tunnelConfig.toDomain() }
} }
override val globalTunnelFlow: Flow<Domain?> = override val globalTunnelFlow: Flow<Domain?> =
tunnelConfigDao.getGlobalTunnel().flowOn(ioDispatcher).map { it?.toDomain() } tunnelConfigDao.getGlobalTunnel().map { it?.toDomain() }
override suspend fun getAll(): List<Domain> { override suspend fun getAll(): List<Domain> {
return withContext(ioDispatcher) { tunnelConfigDao.getAll().map { it.toDomain() } } return tunnelConfigDao.getAll().map { it.toDomain() }
} }
override suspend fun save(tunnelConfig: Domain) { override suspend fun save(tunnelConfig: Domain) {
withContext(ioDispatcher) { tunnelConfigDao.upsert(tunnelConfig.toEntity()) } tunnelConfigDao.upsert(tunnelConfig.toEntity())
} }
override suspend fun saveAll(tunnelConfigList: List<Domain>) { override suspend fun saveAll(tunnelConfigList: List<Domain>) {
withContext(ioDispatcher) { tunnelConfigDao.saveAll(tunnelConfigList.map { tunnelConfig -> tunnelConfig.toEntity() })
tunnelConfigDao.saveAll(
tunnelConfigList.map { tunnelConfig -> tunnelConfig.toEntity() }
)
}
} }
override suspend fun updatePrimaryTunnel(tunnelConfig: Domain?) { override suspend fun updatePrimaryTunnel(tunnelConfig: Domain?) {
withContext(ioDispatcher) { tunnelConfigDao.resetPrimaryTunnel()
tunnelConfigDao.resetPrimaryTunnel() tunnelConfig?.let { save(it.copy(isPrimaryTunnel = true)) }
tunnelConfig?.let { save(it.copy(isPrimaryTunnel = true)) }
}
} }
override suspend fun resetActiveTunnels() { override suspend fun resetActiveTunnels() {
withContext(ioDispatcher) { tunnelConfigDao.resetActiveTunnels() } tunnelConfigDao.resetActiveTunnels()
} }
override suspend fun updateMobileDataTunnel(tunnelConfig: Domain?) { override suspend fun updateMobileDataTunnel(tunnelConfig: Domain?) {
withContext(ioDispatcher) { tunnelConfigDao.resetMobileDataTunnel()
tunnelConfigDao.resetMobileDataTunnel() tunnelConfig?.let { save(it.copy(isMobileDataTunnel = true)) }
tunnelConfig?.let { save(it.copy(isMobileDataTunnel = true)) }
}
} }
override suspend fun updateEthernetTunnel(tunnelConfig: Domain?) { override suspend fun updateEthernetTunnel(tunnelConfig: Domain?) {
withContext(ioDispatcher) { tunnelConfigDao.resetEthernetTunnel()
tunnelConfigDao.resetEthernetTunnel() tunnelConfig?.let { save(it.copy(isEthernetTunnel = true)) }
tunnelConfig?.let { save(it.copy(isEthernetTunnel = true)) }
}
} }
override suspend fun delete(tunnelConfig: Domain) { override suspend fun delete(tunnelConfig: Domain) {
withContext(ioDispatcher) { tunnelConfigDao.delete(tunnelConfig.toEntity()) } tunnelConfigDao.delete(tunnelConfig.toEntity())
} }
override suspend fun getById(id: Int): Domain? { override suspend fun getById(id: Int): Domain? {
return withContext(ioDispatcher) { tunnelConfigDao.getById(id.toLong())?.toDomain() } return tunnelConfigDao.getById(id.toLong())?.toDomain()
} }
override suspend fun getActive(): List<Domain> { override suspend fun getActive(): List<Domain> {
return withContext(ioDispatcher) { tunnelConfigDao.getActive().map { it.toDomain() } } return tunnelConfigDao.getActive().map { it.toDomain() }
} }
override suspend fun getDefaultTunnel(): Domain? { override suspend fun getDefaultTunnel(): Domain? {
return withContext(ioDispatcher) { tunnelConfigDao.getDefaultTunnel()?.toDomain() } return tunnelConfigDao.getDefaultTunnel()?.toDomain()
} }
override suspend fun getStartTunnel(): Domain? { override suspend fun getStartTunnel(): Domain? {
return withContext(ioDispatcher) { tunnelConfigDao.getStartTunnel()?.toDomain() } return tunnelConfigDao.getStartTunnel()?.toDomain()
} }
override suspend fun count(): Int { override suspend fun count(): Int {
return withContext(ioDispatcher) { tunnelConfigDao.count().toInt() } return tunnelConfigDao.count().toInt()
} }
override suspend fun findByTunnelName(name: String): Domain? { override suspend fun findByTunnelName(name: String): Domain? {
return withContext(ioDispatcher) { tunnelConfigDao.getByName(name)?.toDomain() } return tunnelConfigDao.getByName(name)?.toDomain()
} }
override suspend fun findByTunnelNetworksName(name: String): List<Domain> { override suspend fun findByTunnelNetworksName(name: String): List<Domain> {
return withContext(ioDispatcher) { return tunnelConfigDao.findByTunnelNetworkName(name).map { it.toDomain() }
tunnelConfigDao.findByTunnelNetworkName(name).map { it.toDomain() }
}
} }
override suspend fun findByMobileDataTunnel(): List<Domain> { override suspend fun findByMobileDataTunnel(): List<Domain> {
return withContext(ioDispatcher) { return tunnelConfigDao.findByMobileDataTunnel().map { it.toDomain() }
tunnelConfigDao.findByMobileDataTunnel().map { it.toDomain() }
}
} }
override suspend fun findPrimary(): List<Domain> { override suspend fun findPrimary(): List<Domain> {
return withContext(ioDispatcher) { tunnelConfigDao.findByPrimary().map { it.toDomain() } } return tunnelConfigDao.findByPrimary().map { it.toDomain() }
} }
override suspend fun delete(tunnels: List<Domain>) { override suspend fun delete(tunnels: List<Domain>) {
withContext(ioDispatcher) { tunnelConfigDao.delete(tunnels.map { it.toEntity() }) } tunnelConfigDao.delete(tunnels.map { it.toEntity() })
} }
} }
@@ -7,69 +7,86 @@ import com.zaneschepke.logcatter.LogcatReader
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationMonitor import com.zaneschepke.wireguardautotunnel.core.notification.NotificationMonitor
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.shortcut.DynamicShortcutManager import com.zaneschepke.wireguardautotunnel.core.shortcut.DynamicShortcutManager
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.domain.repository.GlobalEffectRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.SelectedTunnelsRepository
import com.zaneschepke.wireguardautotunnel.util.FileUtils
import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils
import dagger.Module import com.zaneschepke.wireguardautotunnel.viewmodel.AutoTunnelViewModel
import dagger.Provides import com.zaneschepke.wireguardautotunnel.viewmodel.ConfigViewModel
import dagger.hilt.InstallIn import com.zaneschepke.wireguardautotunnel.viewmodel.DnsViewModel
import dagger.hilt.android.qualifiers.ApplicationContext import com.zaneschepke.wireguardautotunnel.viewmodel.LicenseViewModel
import dagger.hilt.components.SingletonComponent import com.zaneschepke.wireguardautotunnel.viewmodel.LockdownViewModel
import javax.inject.Singleton import com.zaneschepke.wireguardautotunnel.viewmodel.LoggerViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.MonitoringViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.ProxySettingsViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.SettingsViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.SplitTunnelViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.SupportViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelViewModel
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import org.koin.android.ext.koin.androidContext
import org.koin.core.annotation.KoinExperimentalAPI
import org.koin.core.module.dsl.scopedOf
import org.koin.core.module.dsl.singleOf
import org.koin.core.module.dsl.viewModel
import org.koin.core.module.dsl.viewModelOf
import org.koin.core.qualifier.named
import org.koin.dsl.bind
import org.koin.dsl.module
import org.koin.viewmodel.scope.viewModelScope
@Module @OptIn(KoinExperimentalAPI::class)
@InstallIn(SingletonComponent::class) val appModule = module {
class AppModule { single<CoroutineScope>(named(Scope.APPLICATION)) {
CoroutineScope(SupervisorJob() + get<CoroutineDispatcher>(named(Dispatcher.DEFAULT)))
@Singleton
@ApplicationScope
@Provides
fun providesApplicationScope(
@DefaultDispatcher defaultDispatcher: CoroutineDispatcher
): CoroutineScope = CoroutineScope(SupervisorJob() + defaultDispatcher)
@Singleton
@Provides
fun provideLogCollect(@ApplicationContext context: Context): LogReader {
return LogcatReader.init(storageDir = context.filesDir.absolutePath)
} }
@Singleton single<LogReader> { LogcatReader.init(storageDir = androidContext().filesDir.absolutePath) }
@Provides
fun provideNotificationService(@ApplicationContext context: Context): NotificationManager { single<PowerManager> {
return WireGuardNotification(context) androidContext().getSystemService(Context.POWER_SERVICE) as PowerManager
}
singleOf(::NotificationMonitor)
singleOf(::WireGuardNotification) bind NotificationManager::class
single {
ServiceManager(
androidContext(),
get(named(Dispatcher.IO)),
get(named(Scope.APPLICATION)),
get(named(Dispatcher.MAIN)),
get(),
)
} }
@Singleton singleOf(::GlobalEffectRepository)
@Provides
fun provideShortcutManager( viewModelScope {
@ApplicationContext context: Context, scoped { FileUtils(androidContext(), get(named(Dispatcher.IO))) }
@IoDispatcher ioDispatcher: CoroutineDispatcher, scoped<ShortcutManager> {
): ShortcutManager { DynamicShortcutManager(androidContext(), get(named(Dispatcher.IO)))
return DynamicShortcutManager(context, ioDispatcher) }
scopedOf(::SelectedTunnelsRepository)
} }
@Singleton single { NetworkUtils(get(named(Dispatcher.IO))) }
@Provides
fun provideNetworkUtils(@IoDispatcher ioDispatcher: CoroutineDispatcher): NetworkUtils {
return NetworkUtils(ioDispatcher)
}
@Singleton viewModelOf(::AutoTunnelViewModel)
@Provides viewModel { (id: Int) -> ConfigViewModel(get(), get(), get(), id) }
fun provideNotificationMonitor( viewModelOf(::DnsViewModel)
tunnelManager: TunnelManager, viewModelOf(::LicenseViewModel)
notificationManager: NotificationManager, viewModelOf(::LockdownViewModel)
): NotificationMonitor { viewModelOf(::LoggerViewModel)
return NotificationMonitor(tunnelManager, notificationManager) viewModelOf(::MonitoringViewModel)
} viewModelOf(::ProxySettingsViewModel)
viewModelOf(::SettingsViewModel)
@Provides viewModelOf(::SharedAppViewModel)
fun providePowerManager(@ApplicationContext context: Context): PowerManager { viewModel { (id: Int) -> SplitTunnelViewModel(get(), get(), get(), id) }
return context.getSystemService(Context.POWER_SERVICE) as PowerManager viewModel { SupportViewModel(get(), get(named(Dispatcher.MAIN)), get()) }
} viewModel { (id: Int) -> TunnelViewModel(get(), get(), id) }
} }
@@ -1,13 +0,0 @@
package com.zaneschepke.wireguardautotunnel.di
import javax.inject.Qualifier
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class TunnelShell
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class AppShell
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class Kernel
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class Userspace
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class ProxyUserspace
@@ -1,15 +0,0 @@
package com.zaneschepke.wireguardautotunnel.di
import javax.inject.Qualifier
@Retention(AnnotationRetention.BINARY) @Qualifier annotation class DefaultDispatcher
@Retention(AnnotationRetention.BINARY) @Qualifier annotation class IoDispatcher
@Retention(AnnotationRetention.BINARY) @Qualifier annotation class MainDispatcher
@Retention(AnnotationRetention.BINARY) @Qualifier annotation class MainImmediateDispatcher
@Retention(AnnotationRetention.BINARY) @Qualifier annotation class ApplicationScope
@Retention(AnnotationRetention.BINARY) @Qualifier annotation class ServiceScope
@@ -1,24 +0,0 @@
package com.zaneschepke.wireguardautotunnel.di
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
@Module
@InstallIn(SingletonComponent::class)
object CoroutinesDispatchersModule {
@DefaultDispatcher
@Provides
fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
@IoDispatcher @Provides fun providesIoDispatcher(): CoroutineDispatcher = Dispatchers.IO
@MainDispatcher @Provides fun providesMainDispatcher(): CoroutineDispatcher = Dispatchers.Main
@MainImmediateDispatcher
@Provides
fun providesMainImmediateDispatcher(): CoroutineDispatcher = Dispatchers.Main.immediate
}
@@ -0,0 +1,79 @@
package com.zaneschepke.wireguardautotunnel.di
import android.content.Context
import androidx.room.Room
import androidx.room.RoomDatabase
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
import com.zaneschepke.wireguardautotunnel.data.DataStoreManager
import com.zaneschepke.wireguardautotunnel.data.DatabaseCallback
import com.zaneschepke.wireguardautotunnel.data.migrations.MIGRATION_23_24
import com.zaneschepke.wireguardautotunnel.data.migrations.MIGRATION_25_26
import com.zaneschepke.wireguardautotunnel.data.migrations.MIGRATION_28_29
import com.zaneschepke.wireguardautotunnel.data.repository.DataStoreAppStateRepository
import com.zaneschepke.wireguardautotunnel.data.repository.InstalledAndroidPackageRepository
import com.zaneschepke.wireguardautotunnel.data.repository.RoomAutoTunnelSettingsRepository
import com.zaneschepke.wireguardautotunnel.data.repository.RoomDnsSettingsRepository
import com.zaneschepke.wireguardautotunnel.data.repository.RoomLockdownSettingsRepository
import com.zaneschepke.wireguardautotunnel.data.repository.RoomMonitoringSettingsRepository
import com.zaneschepke.wireguardautotunnel.data.repository.RoomProxySettingsRepository
import com.zaneschepke.wireguardautotunnel.data.repository.RoomSettingsRepository
import com.zaneschepke.wireguardautotunnel.data.repository.RoomTunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.DnsSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.InstalledPackageRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.LockdownSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.ProxySettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import org.koin.android.ext.koin.androidContext
import org.koin.core.module.dsl.singleOf
import org.koin.core.qualifier.named
import org.koin.dsl.bind
import org.koin.dsl.module
val databaseModule = module {
single<RoomDatabase.Callback> { DatabaseCallback(lazy { get() }) }
single {
Room.databaseBuilder(
androidContext(),
AppDatabase::class.java,
get<Context>().getString(R.string.db_name),
)
.addMigrations(
MIGRATION_23_24(get<DataStoreManager>().dataStore),
MIGRATION_25_26,
MIGRATION_28_29,
)
.fallbackToDestructiveMigration(true)
.addCallback(get())
.build()
}
single { get<AppDatabase>().generalSettingsDao() }
single { get<AppDatabase>().lockdownSettingsDao() }
single { get<AppDatabase>().dnsSettingsDao() }
single { get<AppDatabase>().autoTunnelSettingsDao() }
single { get<AppDatabase>().monitoringSettingsDao() }
single { get<AppDatabase>().proxySettingsDoa() }
single { get<AppDatabase>().tunnelConfigDoa() }
single { DataStoreManager(androidContext(), get(named(Dispatcher.IO))) }
single<AppStateRepository> {
DataStoreAppStateRepository(get(), get(named(Scope.APPLICATION)), get(named(Dispatcher.IO)))
}
singleOf(::RoomAutoTunnelSettingsRepository) bind AutoTunnelSettingsRepository::class
singleOf(::RoomDnsSettingsRepository) bind DnsSettingsRepository::class
singleOf(::RoomLockdownSettingsRepository) bind LockdownSettingsRepository::class
singleOf(::RoomMonitoringSettingsRepository) bind MonitoringSettingsRepository::class
singleOf(::RoomProxySettingsRepository) bind ProxySettingsRepository::class
singleOf(::RoomSettingsRepository) bind GeneralSettingRepository::class
singleOf(::RoomTunnelRepository) bind TunnelRepository::class
single<InstalledPackageRepository> {
InstalledAndroidPackageRepository(androidContext(), get(named(Dispatcher.IO)))
}
}
@@ -0,0 +1,13 @@
package com.zaneschepke.wireguardautotunnel.di
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import org.koin.core.qualifier.named
import org.koin.dsl.module
val dispatchersModule = module {
single<CoroutineDispatcher>(named(Dispatcher.DEFAULT)) { Dispatchers.Default }
single<CoroutineDispatcher>(named(Dispatcher.IO)) { Dispatchers.IO }
single<CoroutineDispatcher>(named(Dispatcher.MAIN)) { Dispatchers.Main }
single<CoroutineDispatcher>(named(Dispatcher.MAIN_IMMEDIATE)) { Dispatchers.Main.immediate }
}
@@ -0,0 +1,29 @@
package com.zaneschepke.wireguardautotunnel.di
import com.zaneschepke.wireguardautotunnel.data.network.GitHubApi
import com.zaneschepke.wireguardautotunnel.data.network.KtorClient
import com.zaneschepke.wireguardautotunnel.data.network.KtorGitHubApi
import com.zaneschepke.wireguardautotunnel.data.repository.GitHubUpdateRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.UpdateRepository
import org.koin.android.ext.koin.androidContext
import org.koin.core.module.dsl.singleOf
import org.koin.core.qualifier.named
import org.koin.dsl.bind
import org.koin.dsl.lazyModule
val networkModule = lazyModule {
single { KtorClient.create() }
singleOf(::KtorGitHubApi) bind GitHubApi::class
single<UpdateRepository> {
val appName = "wgtunnel"
GitHubUpdateRepository(
get(),
get(),
appName,
appName,
androidContext(),
get(named(Dispatcher.IO)),
)
}
}
@@ -0,0 +1,25 @@
package com.zaneschepke.wireguardautotunnel.di
// Dispatchers
enum class Dispatcher {
MAIN,
IO,
DEFAULT,
MAIN_IMMEDIATE,
}
// Scopes
enum class Scope {
APPLICATION
}
enum class Shell {
APP,
TUNNEL,
}
enum class Core {
KERNEL,
PROXY_USERSPACE,
USERSPACE,
}
@@ -1,223 +0,0 @@
package com.zaneschepke.wireguardautotunnel.di
import android.content.Context
import androidx.room.Room
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
import com.zaneschepke.wireguardautotunnel.data.DataStoreManager
import com.zaneschepke.wireguardautotunnel.data.DatabaseCallback
import com.zaneschepke.wireguardautotunnel.data.dao.*
import com.zaneschepke.wireguardautotunnel.data.migrations.MIGRATION_23_24
import com.zaneschepke.wireguardautotunnel.data.migrations.MIGRATION_25_26
import com.zaneschepke.wireguardautotunnel.data.migrations.MIGRATION_28_29
import com.zaneschepke.wireguardautotunnel.data.network.GitHubApi
import com.zaneschepke.wireguardautotunnel.data.network.KtorClient
import com.zaneschepke.wireguardautotunnel.data.network.KtorGitHubApi
import com.zaneschepke.wireguardautotunnel.data.repository.*
import com.zaneschepke.wireguardautotunnel.domain.repository.*
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import io.ktor.client.*
import javax.inject.Singleton
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
@Module
@InstallIn(SingletonComponent::class)
class RepositoryModule {
@Provides
@Singleton
fun provideGlobalEffectRepository(): GlobalEffectRepository {
return GlobalEffectRepository()
}
@Provides
@Singleton
fun provideInstalledPackageRepository(
@ApplicationContext context: Context,
@ApplicationScope applicationScope: CoroutineScope,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): InstalledPackageRepository {
return InstalledAndroidPackageRepository(context, applicationScope, ioDispatcher)
}
@Provides
@Singleton
fun provideDatabase(
@ApplicationContext context: Context,
callback: DatabaseCallback,
dataStoreManager: DataStoreManager,
): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
context.getString(R.string.db_name),
)
.addMigrations(
MIGRATION_23_24(dataStoreManager.dataStore),
MIGRATION_25_26,
MIGRATION_28_29,
)
.fallbackToDestructiveMigration(true)
.addCallback(callback)
.build()
}
@Singleton
@Provides
fun provideSettingsDoa(appDatabase: AppDatabase): GeneralSettingsDao {
return appDatabase.generalSettingsDao()
}
@Singleton
@Provides
fun provideLockdownDoa(appDatabase: AppDatabase): LockdownSettingsDao {
return appDatabase.lockdownSettingsDao()
}
@Singleton
@Provides
fun provideDnsSettingsDao(appDatabase: AppDatabase): DnsSettingsDao {
return appDatabase.dnsSettingsDao()
}
@Singleton
@Provides
fun provideAutoTunnelDao(appDatabase: AppDatabase): AutoTunnelSettingsDao {
return appDatabase.autoTunnelSettingsDao()
}
@Singleton
@Provides
fun provideMonitoringDao(appDatabase: AppDatabase): MonitoringSettingsDao {
return appDatabase.monitoringSettingsDao()
}
@Singleton
@Provides
fun provideProxyDoa(appDatabase: AppDatabase): ProxySettingsDao {
return appDatabase.proxySettingsDoa()
}
@Singleton
@Provides
fun provideTunnelConfigDoa(appDatabase: AppDatabase): TunnelConfigDao {
return appDatabase.tunnelConfigDoa()
}
@Singleton
@Provides
fun provideTunnelConfigRepository(
tunnelConfigDao: TunnelConfigDao,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): TunnelRepository {
return RoomTunnelRepository(tunnelConfigDao, ioDispatcher)
}
@Singleton
@Provides
fun provideLockdownSettingsRepository(
lockdownSettingsDao: LockdownSettingsDao,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): LockdownSettingsRepository {
return RoomLockdownSettingsRepository(lockdownSettingsDao, ioDispatcher)
}
@Singleton
@Provides
fun provideGeneralSettingsRepository(
settingsDao: GeneralSettingsDao,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): GeneralSettingRepository {
return RoomSettingsRepository(settingsDao, ioDispatcher)
}
@Singleton
@Provides
fun provideMonitoringSettingsRepository(
monitoringSettingsDao: MonitoringSettingsDao,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): MonitoringSettingsRepository {
return RoomMonitoringSettingsRepository(monitoringSettingsDao, ioDispatcher)
}
@Singleton
@Provides
fun provideDnsSettingsRepository(
dnsSettingsDao: DnsSettingsDao,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): DnsSettingsRepository {
return RoomDnsSettingsRepository(dnsSettingsDao, ioDispatcher)
}
@Singleton
@Provides
fun provideAutoTunnelSettingsRepository(
autoTunnelSettingsDao: AutoTunnelSettingsDao,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): AutoTunnelSettingsRepository {
return RoomAutoTunnelSettingsRepository(autoTunnelSettingsDao, ioDispatcher)
}
@Singleton
@Provides
fun provideProxySettingsRepository(
proxySettingsDao: ProxySettingsDao,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): ProxySettingsRepository {
return RoomProxySettingsRepository(proxySettingsDao, ioDispatcher)
}
@Singleton
@Provides
fun providePreferencesDataStore(
@ApplicationContext context: Context,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): DataStoreManager {
return DataStoreManager(context, ioDispatcher)
}
@Provides
@Singleton
fun provideGeneralStateRepository(
dataStoreManager: DataStoreManager,
@ApplicationScope applicationScope: CoroutineScope,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): AppStateRepository {
return DataStoreAppStateRepository(dataStoreManager, applicationScope, ioDispatcher)
}
@Provides
@Singleton
fun provideHttpClient(): HttpClient {
return KtorClient.create()
}
@Provides
@Singleton
fun provideGitHubApi(client: HttpClient): GitHubApi {
return KtorGitHubApi(client)
}
@Provides
@Singleton
fun provideUpdateRepository(
gitHubApi: GitHubApi,
client: HttpClient,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
@ApplicationContext context: Context,
): UpdateRepository {
return GitHubUpdateRepository(
gitHubApi,
client,
"wgtunnel",
"wgtunnel",
context,
ioDispatcher,
)
}
}
@@ -1,227 +1,108 @@
package com.zaneschepke.wireguardautotunnel.di package com.zaneschepke.wireguardautotunnel.di
import android.content.Context
import android.os.PowerManager
import com.wireguard.android.backend.WgQuickBackend import com.wireguard.android.backend.WgQuickBackend
import com.wireguard.android.util.RootShell import com.wireguard.android.util.RootShell
import com.wireguard.android.util.ToolsInstaller import com.wireguard.android.util.ToolsInstaller
import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
import com.zaneschepke.networkmonitor.NetworkMonitor import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.* import com.zaneschepke.wireguardautotunnel.core.tunnel.backend.KernelTunnel
import com.zaneschepke.wireguardautotunnel.domain.repository.* import com.zaneschepke.wireguardautotunnel.core.tunnel.backend.RunConfigHelper
import com.zaneschepke.wireguardautotunnel.core.tunnel.backend.TunnelBackend
import com.zaneschepke.wireguardautotunnel.core.tunnel.backend.UserspaceTunnel
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
import com.zaneschepke.wireguardautotunnel.util.RootShellUtils
import com.zaneschepke.wireguardautotunnel.util.extensions.to import com.zaneschepke.wireguardautotunnel.util.extensions.to
import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChangedBy import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import org.amnezia.awg.backend.Backend import org.amnezia.awg.backend.Backend
import org.amnezia.awg.backend.GoBackend import org.amnezia.awg.backend.GoBackend
import org.amnezia.awg.backend.ProxyGoBackend import org.amnezia.awg.backend.ProxyGoBackend
import org.amnezia.awg.backend.RootTunnelActionHandler import org.amnezia.awg.backend.RootTunnelActionHandler
import org.koin.android.ext.koin.androidContext
import org.koin.core.module.dsl.singleOf
import org.koin.core.qualifier.named
import org.koin.dsl.module
@Module val tunnelModule = module {
@InstallIn(SingletonComponent::class) single(named(Shell.TUNNEL)) { RootShell(androidContext()) }
class TunnelModule { single(named(Shell.APP)) { RootShell(androidContext()) }
@Provides single { RootShellUtils(get(named(Shell.APP)), get(named(Dispatcher.IO))) }
@Singleton
@TunnelShell
fun provideTunnelRootShell(@ApplicationContext context: Context): RootShell {
return RootShell(context)
}
@Provides singleOf(::RunConfigHelper)
@Singleton
@AppShell
fun provideAppRootShell(@ApplicationContext context: Context): RootShell {
return RootShell(context)
}
@Provides single<Backend>(named(Core.USERSPACE)) {
@Singleton GoBackend(
@Userspace androidContext(),
fun provideAmneziaBackend(@ApplicationContext context: Context): Backend { RootTunnelActionHandler(org.amnezia.awg.util.RootShell(androidContext())),
return GoBackend(context, RootTunnelActionHandler(org.amnezia.awg.util.RootShell(context)))
}
@Provides
@Singleton
@ProxyUserspace
fun provideAmneziaProxyBackend(@ApplicationContext context: Context): Backend {
return ProxyGoBackend(
context,
RootTunnelActionHandler(org.amnezia.awg.util.RootShell(context)),
) )
} }
@Provides single<Backend>(named(Core.PROXY_USERSPACE)) {
@Singleton ProxyGoBackend(
fun provideKernelBackend( androidContext(),
@ApplicationContext context: Context, RootTunnelActionHandler(org.amnezia.awg.util.RootShell(androidContext())),
@TunnelShell shell: RootShell, )
): com.wireguard.android.backend.Backend { }
return WgQuickBackend(
context, single<com.wireguard.android.backend.Backend> {
val shell = get<RootShell>(named(Shell.TUNNEL))
WgQuickBackend(
androidContext(),
shell, shell,
ToolsInstaller(context, shell), ToolsInstaller(androidContext(), shell),
com.wireguard.android.backend.RootTunnelActionHandler(shell), com.wireguard.android.backend.RootTunnelActionHandler(shell),
) )
.also { it.setMultipleTunnels(true) } .apply { setMultipleTunnels(true) }
} }
@Provides single<TunnelBackend>(named(Core.KERNEL)) {
@Singleton KernelTunnel(get(), get<com.wireguard.android.backend.Backend>())
@Kernel
fun provideKernelProvider(
@ApplicationScope applicationScope: CoroutineScope,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
backend: com.wireguard.android.backend.Backend,
runConfigHelper: RunConfigHelper,
): TunnelProvider {
return KernelTunnel(applicationScope, ioDispatcher, runConfigHelper, backend)
} }
@Provides single<TunnelBackend>(qualifier = named(Core.USERSPACE)) {
@Singleton UserspaceTunnel(get<Backend>(named(Core.USERSPACE)), get())
@Userspace
fun provideUserspaceProvider(
@ApplicationScope applicationScope: CoroutineScope,
runConfigHelper: RunConfigHelper,
@Userspace backend: Backend,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): TunnelProvider {
return UserspaceTunnel(applicationScope, ioDispatcher, backend, runConfigHelper)
} }
@Provides single<TunnelBackend>(qualifier = named(Core.PROXY_USERSPACE)) {
@Singleton UserspaceTunnel(get<Backend>(named(Core.PROXY_USERSPACE)), get())
@ProxyUserspace
fun provideProxyUserspaceProvider(
@ApplicationScope applicationScope: CoroutineScope,
runConfigHelper: RunConfigHelper,
@ProxyUserspace backend: Backend,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): TunnelProvider {
return UserspaceTunnel(applicationScope, ioDispatcher, backend, runConfigHelper)
} }
@Provides single<NetworkMonitor> {
@Singleton AndroidNetworkMonitor(
fun provideTunnelManager( androidContext(),
@Kernel kernelTunnel: TunnelProvider,
@Userspace userspaceTunnel: TunnelProvider,
@ProxyUserspace proxyTunnel: TunnelProvider,
serviceManager: ServiceManager,
tunnelRepository: TunnelRepository,
lockdownSettingsRepository: LockdownSettingsRepository,
settingsRepository: GeneralSettingRepository,
autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
tunnelMonitor: TunnelMonitor,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
@ApplicationScope applicationScope: CoroutineScope,
): TunnelManager {
return TunnelManager(
kernelTunnel,
userspaceTunnel,
proxyTunnel,
serviceManager,
settingsRepository,
autoTunnelSettingsRepository,
lockdownSettingsRepository,
tunnelRepository,
tunnelMonitor,
applicationScope,
ioDispatcher,
)
}
@Provides
@Singleton
fun provideTunnelConfigHelper(
settingsRepository: GeneralSettingRepository,
proxySettingsRepository: ProxySettingsRepository,
dnsSettingsRepository: DnsSettingsRepository,
tunnelRepository: TunnelRepository,
): RunConfigHelper {
return RunConfigHelper(
settingsRepository,
proxySettingsRepository,
dnsSettingsRepository,
tunnelRepository,
)
}
@Singleton
@Provides
fun provideServiceManager(
@ApplicationContext context: Context,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
@MainDispatcher mainCoroutineDispatcher: CoroutineDispatcher,
@ApplicationScope applicationScope: CoroutineScope,
autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
): ServiceManager {
return ServiceManager(
context,
ioDispatcher,
applicationScope,
mainCoroutineDispatcher,
autoTunnelSettingsRepository,
)
}
@Singleton
@Provides
fun provideTunnelMonitor(
powerManager: PowerManager,
networkMonitor: NetworkMonitor,
networkUtils: NetworkUtils,
logReader: LogReader,
tunnelsRepository: TunnelRepository,
settingsRepository: GeneralSettingRepository,
monitoringSettingsRepository: MonitoringSettingsRepository,
): TunnelMonitor {
return TunnelMonitor(
settingsRepository,
tunnelsRepository,
monitoringSettingsRepository,
networkMonitor,
networkUtils,
logReader,
powerManager,
)
}
@Provides
@Singleton
fun provideNetworkMonitor(
@ApplicationContext context: Context,
autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
@ApplicationScope applicationScope: CoroutineScope,
@AppShell appShell: RootShell,
): NetworkMonitor {
return AndroidNetworkMonitor(
context,
object : AndroidNetworkMonitor.ConfigurationListener { object : AndroidNetworkMonitor.ConfigurationListener {
override val detectionMethod: Flow<AndroidNetworkMonitor.WifiDetectionMethod> override val detectionMethod =
get() = get<AutoTunnelSettingsRepository>()
autoTunnelSettingsRepository.flow .flow
.distinctUntilChangedBy { it.wifiDetectionMethod } .distinctUntilChangedBy { it.wifiDetectionMethod }
.map { it.wifiDetectionMethod.to() } .map { it.wifiDetectionMethod.to() }
override val rootShell: RootShell override val rootShell = get<RootShell>(named(Shell.APP))
get() = appShell
}, },
applicationScope, get<CoroutineScope>(named(Scope.APPLICATION)),
)
}
single {
TunnelManager(
get(named(Core.KERNEL)),
get(named(Core.USERSPACE)),
get(named(Core.PROXY_USERSPACE)),
get(),
get(),
get(),
get(),
get(),
get(),
get(),
get(),
get(),
get(),
get(named(Scope.APPLICATION)),
get(named(Dispatcher.IO)),
) )
} }
} }
@@ -1,36 +0,0 @@
package com.zaneschepke.wireguardautotunnel.di
import android.content.Context
import com.wireguard.android.util.RootShell
import com.zaneschepke.wireguardautotunnel.util.FileUtils
import com.zaneschepke.wireguardautotunnel.util.RootShellUtils
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ViewModelComponent
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.scopes.ViewModelScoped
import javax.inject.Provider
import kotlinx.coroutines.CoroutineDispatcher
@Module
@InstallIn(ViewModelComponent::class)
class ViewModelModule {
@ViewModelScoped
@Provides
fun provideFileUtils(
@ApplicationContext context: Context,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): FileUtils {
return FileUtils(context, ioDispatcher)
}
@ViewModelScoped
@Provides
fun provideRootShellUtils(
@AppShell rootShell: Provider<RootShell>,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): RootShellUtils {
return RootShellUtils(rootShell, ioDispatcher)
}
}
@@ -0,0 +1,7 @@
package com.zaneschepke.wireguardautotunnel.di
import com.zaneschepke.wireguardautotunnel.core.worker.ServiceWorker
import org.koin.androidx.workmanager.dsl.workerOf
import org.koin.dsl.module
val workerModule = module { workerOf(::ServiceWorker) }
@@ -1,6 +1,8 @@
package com.zaneschepke.wireguardautotunnel.domain.repository package com.zaneschepke.wireguardautotunnel.domain.repository
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface GeneralSettingRepository { interface GeneralSettingRepository {
@@ -9,4 +11,12 @@ interface GeneralSettingRepository {
val flow: Flow<GeneralSettings> val flow: Flow<GeneralSettings>
suspend fun getGeneralSettings(): GeneralSettings suspend fun getGeneralSettings(): GeneralSettings
suspend fun updateTheme(theme: Theme)
suspend fun updateLocale(locale: String)
suspend fun updatePinLockEnabled(enabled: Boolean)
suspend fun updateAppMode(appMode: AppMode)
} }
@@ -1,11 +1,9 @@
package com.zaneschepke.wireguardautotunnel.domain.repository package com.zaneschepke.wireguardautotunnel.domain.repository
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
import javax.inject.Singleton
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
@Singleton
class GlobalEffectRepository { class GlobalEffectRepository {
private val _globalEffectFlow = private val _globalEffectFlow =
@@ -0,0 +1,27 @@
package com.zaneschepke.wireguardautotunnel.domain.repository
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
class SelectedTunnelsRepository {
private val _selectedTunnelsFlow = MutableStateFlow<List<TunnelConfig>>(emptyList())
val flow = _selectedTunnelsFlow.asStateFlow()
fun add(tunnelConfig: TunnelConfig) {
_selectedTunnelsFlow.update { it.toMutableList().apply { add(tunnelConfig) } }
}
fun remove(tunnelConfig: TunnelConfig) {
_selectedTunnelsFlow.update { it.toMutableList().apply { remove(tunnelConfig) } }
}
fun clear() {
_selectedTunnelsFlow.update { emptyList() }
}
fun set(tunnelConfigs: List<TunnelConfig>) {
_selectedTunnelsFlow.update { tunnelConfigs }
}
}
@@ -1,6 +1,6 @@
package com.zaneschepke.wireguardautotunnel.domain.state package com.zaneschepke.wireguardautotunnel.domain.state
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelMonitor.Companion.CLOUDFLARE_IPV4_IP import com.zaneschepke.wireguardautotunnel.core.tunnel.handler.TunnelMonitorHandler.Companion.CLOUDFLARE_IPV4_IP
enum class FailureReason { enum class FailureReason {
NoConnectivity, NoConnectivity,
@@ -4,13 +4,9 @@ import androidx.compose.runtime.staticCompositionLocalOf
import androidx.navigation3.runtime.NavBackStack import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.NavKey
import com.zaneschepke.wireguardautotunnel.ui.navigation.NavController import com.zaneschepke.wireguardautotunnel.ui.navigation.NavController
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
val LocalIsAndroidTV = staticCompositionLocalOf { false } val LocalIsAndroidTV = staticCompositionLocalOf { false }
val LocalSharedVm =
staticCompositionLocalOf<SharedAppViewModel> { error("No shared viewmodel provided") }
val LocalNavController = staticCompositionLocalOf<NavController> { error("No backstack provided") } val LocalNavController = staticCompositionLocalOf<NavController> { error("No backstack provided") }
typealias BackStack = NavBackStack<NavKey> typealias BackStack = NavBackStack<NavKey>
@@ -1,52 +1,20 @@
package com.zaneschepke.wireguardautotunnel.ui.common.sheet package com.zaneschepke.wireguardautotunnel.ui.common.sheet
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
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.Icons
import androidx.compose.material.icons.outlined.Check import androidx.compose.material.icons.outlined.Check
import androidx.compose.material3.* import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
@Composable import com.zaneschepke.wireguardautotunnel.ui.common.text.DescriptionText
fun SheetOption(
label: String,
leadingIcon: ImageVector? = null,
onClick: () -> Unit,
selected: Boolean,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier.fillMaxWidth().clickable(onClick = onClick).padding(10.dp),
horizontalArrangement = Arrangement.SpaceBetween,
) {
Row {
leadingIcon?.let {
Icon(
imageVector = it,
contentDescription = null,
modifier = Modifier.padding(10.dp),
)
}
Text(text = label, modifier = Modifier.padding(10.dp))
}
if (selected) {
Icon(
imageVector = Icons.Outlined.Check,
contentDescription = stringResource(R.string.selected),
modifier = Modifier.padding(10.dp),
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -59,7 +27,21 @@ fun CustomBottomSheet(options: List<SheetOption>, onDismiss: () -> Unit) {
sheetState = sheetState, sheetState = sheetState,
) { ) {
options.forEachIndexed { index, option -> options.forEachIndexed { index, option ->
SheetOption(option.label, option.leadingIcon, option.onClick, option.selected) SurfaceRow(
title = option.label,
onClick = option.onClick,
leading = { Icon(imageVector = option.leadingIcon, contentDescription = null) },
trailing =
if (option.selected) {
{
Icon(
imageVector = Icons.Outlined.Check,
contentDescription = stringResource(R.string.selected),
)
}
} else null,
description = option.description?.let { { DescriptionText(it) } },
)
if (index != options.size - 1) HorizontalDivider() if (index != options.size - 1) HorizontalDivider()
} }
} }
@@ -70,4 +52,5 @@ data class SheetOption(
val label: String, val label: String,
val onClick: () -> Unit, val onClick: () -> Unit,
val selected: Boolean = false, val selected: Boolean = false,
val description: String? = null,
) )
@@ -7,6 +7,7 @@ class NavController(
private val backStack: NavBackStack<NavKey>, private val backStack: NavBackStack<NavKey>,
private val isDisclosureShown: Boolean, private val isDisclosureShown: Boolean,
private val onChange: (previous: NavKey?) -> Unit = {}, private val onChange: (previous: NavKey?) -> Unit = {},
private val onExitApp: () -> Unit = {},
) { ) {
fun push(route: NavKey) { fun push(route: NavKey) {
onChange(currentRoute) onChange(currentRoute)
@@ -14,12 +15,13 @@ class NavController(
} }
fun pop(): Boolean { fun pop(): Boolean {
if (currentRoute != null) { if (!canPop) {
onChange(currentRoute) onExitApp()
backStack.removeLastOrNull()
return true return true
} }
return false onChange(currentRoute)
backStack.removeLastOrNull()
return true
} }
fun popUpTo(route: NavKey) { fun popUpTo(route: NavKey) {
@@ -23,13 +23,13 @@ import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route.* import com.zaneschepke.wireguardautotunnel.ui.navigation.Route.*
import com.zaneschepke.wireguardautotunnel.ui.navigation.TunnelNetwork import com.zaneschepke.wireguardautotunnel.ui.navigation.TunnelNetwork
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
import com.zaneschepke.wireguardautotunnel.ui.state.GlobalAppUiState
import com.zaneschepke.wireguardautotunnel.ui.state.NavbarState import com.zaneschepke.wireguardautotunnel.ui.state.NavbarState
import com.zaneschepke.wireguardautotunnel.ui.state.SharedAppUiState
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
@Composable @Composable
fun currentRouteAsNavbarState( fun currentRouteAsNavbarState(
sharedState: SharedAppUiState, globalState: GlobalAppUiState,
sharedViewModel: SharedAppViewModel, sharedViewModel: SharedAppViewModel,
route: Route?, route: Route?,
navController: NavController, navController: NavController,
@@ -37,7 +37,7 @@ fun currentRouteAsNavbarState(
val keyboardController = LocalSoftwareKeyboardController.current val keyboardController = LocalSoftwareKeyboardController.current
val context = LocalContext.current val context = LocalContext.current
return remember(route, sharedState) { return remember(route, globalState) {
derivedStateOf { derivedStateOf {
when (route) { when (route) {
AdvancedAutoTunnel -> AdvancedAutoTunnel ->
@@ -70,7 +70,7 @@ fun currentRouteAsNavbarState(
NavbarState( NavbarState(
showBottomItems = true, showBottomItems = true,
topTitle = topTitle =
if (!sharedState.isLocationDisclosureShown) null if (!globalState.isLocationDisclosureShown) null
else { else {
context.getString(R.string.auto_tunnel) context.getString(R.string.auto_tunnel)
}, },
@@ -238,7 +238,7 @@ fun currentRouteAsNavbarState(
is Config, is Config,
is ConfigGlobal -> { is ConfigGlobal -> {
val tunnelName = val tunnelName =
if (route is Config) sharedState.tunnels.find { it.id == route.id }?.name if (route is Config) globalState.tunnelNames[route.id]
else context.getString(R.string.global_dns_servers) else context.getString(R.string.global_dns_servers)
NavbarState( NavbarState(
topLeading = { topLeading = {
@@ -266,8 +266,7 @@ fun currentRouteAsNavbarState(
is SplitTunnel, is SplitTunnel,
is SplitTunnelGlobal -> { is SplitTunnelGlobal -> {
val tunnelName = val tunnelName =
if (route is SplitTunnel) if (route is SplitTunnel) globalState.tunnelNames[route.id]
sharedState.tunnels.find { it.id == route.id }?.name
else context.getString(R.string.global_split_tunneling) else context.getString(R.string.global_split_tunneling)
NavbarState( NavbarState(
topLeading = { topLeading = {
@@ -337,7 +336,7 @@ fun currentRouteAsNavbarState(
showBottomItems = true, showBottomItems = true,
) )
is TunnelSettings -> { is TunnelSettings -> {
val tunnelName = sharedState.tunnels.find { it.id == route.id }?.name val tunnelName = globalState.tunnelNames[route.id]
NavbarState( NavbarState(
topLeading = { topLeading = {
IconButton(onClick = { navController.pop() }) { IconButton(onClick = { navController.pop() }) {
@@ -369,7 +368,7 @@ fun currentRouteAsNavbarState(
NavbarState( NavbarState(
topTitle = context.getString(R.string.tunnels), topTitle = context.getString(R.string.tunnels),
topTrailing = { topTrailing = {
when (sharedState.selectedTunnels.size) { when (globalState.selectedTunnelCount) {
0 -> 0 ->
Row { Row {
IconButton(onClick = { navController.push(Sort) }) { IconButton(onClick = { navController.push(Sort) }) {
@@ -423,7 +422,7 @@ fun currentRouteAsNavbarState(
} }
} }
if (sharedState.selectedTunnels.size == 1) { if (globalState.selectedTunnelCount == 1) {
IconButton( IconButton(
onClick = { onClick = {
sharedViewModel.postSideEffect( sharedViewModel.postSideEffect(
@@ -7,12 +7,13 @@ import androidx.navigation3.runtime.NavKey
import com.zaneschepke.wireguardautotunnel.ui.navigation.NavController import com.zaneschepke.wireguardautotunnel.ui.navigation.NavController
@Composable @Composable
fun <T : NavKey> rememberNavController( fun rememberNavController(
backStack: NavBackStack<NavKey>, backStack: NavBackStack<NavKey>,
isDisclosureShown: Boolean, isDisclosureShown: Boolean,
onChange: (NavKey?) -> Unit = {}, onChange: (NavKey?) -> Unit = {},
onExitApp: () -> Unit = {},
): NavController { ): NavController {
return remember(backStack, onChange, isDisclosureShown) { return remember(backStack, isDisclosureShown, onChange, onExitApp) {
NavController(backStack, isDisclosureShown, onChange) NavController(backStack, isDisclosureShown, onChange, onExitApp)
} }
} }
@@ -12,7 +12,15 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.* import androidx.compose.material.icons.outlined.CheckCircle
import androidx.compose.material.icons.outlined.ContentCopy
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material.icons.outlined.PublicOff
import androidx.compose.material.icons.outlined.RestartAlt
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.outlined.SettingsEthernet
import androidx.compose.material.icons.outlined.SignalCellular4Bar
import androidx.compose.material.icons.outlined.Wifi
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -33,13 +41,11 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withStyle import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.zaneschepke.networkmonitor.ActiveNetwork import com.zaneschepke.networkmonitor.ActiveNetwork
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.common.button.SwitchWithDivider import com.zaneschepke.wireguardautotunnel.ui.common.button.SwitchWithDivider
import com.zaneschepke.wireguardautotunnel.ui.common.button.ThemedSwitch import com.zaneschepke.wireguardautotunnel.ui.common.button.ThemedSwitch
@@ -49,23 +55,28 @@ import com.zaneschepke.wireguardautotunnel.ui.common.text.DescriptionText
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
import com.zaneschepke.wireguardautotunnel.ui.navigation.TunnelNetwork import com.zaneschepke.wireguardautotunnel.ui.navigation.TunnelNetwork
import com.zaneschepke.wireguardautotunnel.viewmodel.AutoTunnelViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.AutoTunnelViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
import org.koin.androidx.compose.koinViewModel
import org.koin.compose.viewmodel.koinActivityViewModel
@OptIn(ExperimentalPermissionsApi::class) @OptIn(ExperimentalPermissionsApi::class)
@Composable @Composable
fun AutoTunnelScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) { fun AutoTunnelScreen(
viewModel: AutoTunnelViewModel = koinViewModel(),
sharedViewModel: SharedAppViewModel = koinActivityViewModel(),
) {
val context = LocalContext.current val context = LocalContext.current
val navController = LocalNavController.current val navController = LocalNavController.current
val shareViewModel = LocalSharedVm.current
val clipboard = rememberClipboardHelper() val clipboard = rememberClipboardHelper()
val sharedUiState by shareViewModel.container.stateFlow.collectAsStateWithLifecycle() val globalUiState by sharedViewModel.container.stateFlow.collectAsStateWithLifecycle()
val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle() val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
if (uiState.isLoading) return if (uiState.isLoading) return
val batteryActivity = val batteryActivity =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
shareViewModel.disableBatteryOptimizationsShown() sharedViewModel.disableBatteryOptimizationsShown()
} }
@SuppressLint("BatteryLife") @SuppressLint("BatteryLife")
@@ -112,9 +123,9 @@ fun AutoTunnelScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
} }
fun onAutoTunnelClick() { fun onAutoTunnelClick() {
if (!sharedUiState.isBatteryOptimizationShown) if (!globalUiState.isBatteryOptimizationShown)
return requestDisableBatteryOptimizations() return requestDisableBatteryOptimizations()
viewModel.toggleAutoTunnel(sharedUiState.settings.appMode) viewModel.toggleAutoTunnel(globalUiState.appMode)
} }
SurfaceRow( SurfaceRow(
@@ -15,16 +15,16 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.dropdown.LabelledDropdown import com.zaneschepke.wireguardautotunnel.ui.common.dropdown.LabelledDropdown
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.common.text.DescriptionText import com.zaneschepke.wireguardautotunnel.ui.common.text.DescriptionText
import com.zaneschepke.wireguardautotunnel.viewmodel.AutoTunnelViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.AutoTunnelViewModel
import org.koin.androidx.compose.koinViewModel
@Composable @Composable
fun AutoTunnelAdvancedScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) { fun AutoTunnelAdvancedScreen(viewModel: AutoTunnelViewModel = koinViewModel()) {
val autoTunnelState by viewModel.container.stateFlow.collectAsStateWithLifecycle() val autoTunnelState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
Column( Column(
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.Start,
@@ -10,16 +10,16 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.data.model.WifiDetectionMethod import com.zaneschepke.wireguardautotunnel.data.model.WifiDetectionMethod
import com.zaneschepke.wireguardautotunnel.ui.common.button.IconSurfaceButton import com.zaneschepke.wireguardautotunnel.ui.common.button.IconSurfaceButton
import com.zaneschepke.wireguardautotunnel.util.extensions.asDescriptionString import com.zaneschepke.wireguardautotunnel.util.extensions.asDescriptionString
import com.zaneschepke.wireguardautotunnel.util.extensions.asTitleString import com.zaneschepke.wireguardautotunnel.util.extensions.asTitleString
import com.zaneschepke.wireguardautotunnel.viewmodel.AutoTunnelViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.AutoTunnelViewModel
import org.koin.androidx.compose.koinViewModel
@Composable @Composable
fun WifiDetectionMethodScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) { fun WifiDetectionMethodScreen(viewModel: AutoTunnelViewModel = koinViewModel()) {
val context = LocalContext.current val context = LocalContext.current
val autoTunnelState by viewModel.container.stateFlow.collectAsStateWithLifecycle() val autoTunnelState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
@@ -23,16 +23,16 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.disclosure.components.LocationDisclosureHeader import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.disclosure.components.LocationDisclosureHeader
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
import org.koin.compose.viewmodel.koinActivityViewModel
@Composable @Composable
fun LocationDisclosureScreen() { fun LocationDisclosureScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel()) {
val context = LocalContext.current val context = LocalContext.current
val navController = LocalNavController.current val navController = LocalNavController.current
val viewModel = LocalSharedVm.current
fun goToAutoTunnel() { fun goToAutoTunnel() {
navController.popUpTo(Route.AutoTunnel) navController.popUpTo(Route.AutoTunnel)
@@ -43,7 +43,7 @@ fun LocationDisclosureScreen() {
goToAutoTunnel() goToAutoTunnel()
} }
LaunchedEffect(Unit) { viewModel.setLocationDisclosureShown() } LaunchedEffect(Unit) { sharedViewModel.setLocationDisclosureShown() }
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
@@ -2,7 +2,11 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.preferred
import androidx.compose.foundation.gestures.ScrollableDefaults import androidx.compose.foundation.gestures.ScrollableDefaults
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
@@ -18,7 +22,12 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.* import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
@@ -33,7 +42,6 @@ import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.withStyle import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
@@ -45,11 +53,12 @@ import com.zaneschepke.wireguardautotunnel.ui.common.textbox.CustomTextField
import com.zaneschepke.wireguardautotunnel.ui.navigation.TunnelNetwork import com.zaneschepke.wireguardautotunnel.ui.navigation.TunnelNetwork
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components.WildcardsLabel import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components.WildcardsLabel
import com.zaneschepke.wireguardautotunnel.viewmodel.AutoTunnelViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.AutoTunnelViewModel
import org.koin.androidx.compose.koinViewModel
@Composable @Composable
fun PreferredTunnelScreen( fun PreferredTunnelScreen(
tunnelNetwork: TunnelNetwork, tunnelNetwork: TunnelNetwork,
viewModel: AutoTunnelViewModel = hiltViewModel(), viewModel: AutoTunnelViewModel = koinViewModel(),
) { ) {
val navController = LocalNavController.current val navController = LocalNavController.current
@@ -14,14 +14,19 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.* import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
@@ -41,9 +46,10 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.launchAppSettings
import com.zaneschepke.wireguardautotunnel.util.extensions.launchLocationServicesSettings import com.zaneschepke.wireguardautotunnel.util.extensions.launchLocationServicesSettings
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
import com.zaneschepke.wireguardautotunnel.viewmodel.AutoTunnelViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.AutoTunnelViewModel
import org.koin.androidx.compose.koinViewModel
@Composable @Composable
fun WifiSettingsScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) { fun WifiSettingsScreen(viewModel: AutoTunnelViewModel = koinViewModel()) {
val context = LocalContext.current val context = LocalContext.current
val navController = LocalNavController.current val navController = LocalNavController.current
@@ -11,15 +11,15 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
import com.zaneschepke.wireguardautotunnel.util.StringValue import com.zaneschepke.wireguardautotunnel.util.StringValue
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
import org.koin.compose.viewmodel.koinActivityViewModel
import xyz.teamgravity.pin_lock_compose.PinLock import xyz.teamgravity.pin_lock_compose.PinLock
import xyz.teamgravity.pin_lock_compose.PinManager import xyz.teamgravity.pin_lock_compose.PinManager
@Composable @Composable
fun PinLockScreen() { fun PinLockScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel()) {
val sharedViewModel = LocalSharedVm.current
val navController = LocalNavController.current val navController = LocalNavController.current
val pinAlreadyExists by rememberSaveable { mutableStateOf(PinManager.pinExists()) } val pinAlreadyExists by rememberSaveable { mutableStateOf(PinManager.pinExists()) }
var pinCreated by rememberSaveable { mutableStateOf(false) } var pinCreated by rememberSaveable { mutableStateOf(false) }
@@ -9,12 +9,23 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.CallSplit import androidx.compose.material.icons.automirrored.outlined.CallSplit
import androidx.compose.material.icons.automirrored.outlined.ViewQuilt import androidx.compose.material.icons.automirrored.outlined.ViewQuilt
import androidx.compose.material.icons.outlined.* import androidx.compose.material.icons.outlined.Android
import androidx.compose.material.icons.outlined.Dns
import androidx.compose.material.icons.outlined.ExpandMore
import androidx.compose.material.icons.outlined.NetworkPing
import androidx.compose.material.icons.outlined.Pin
import androidx.compose.material.icons.outlined.SettingsBackupRestore
import androidx.compose.material.icons.outlined.ViewHeadline
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.* import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
@@ -23,13 +34,11 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.MainActivity import com.zaneschepke.wireguardautotunnel.MainActivity
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.model.AppMode import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm
import com.zaneschepke.wireguardautotunnel.ui.common.button.SheetButtonWithDivider import com.zaneschepke.wireguardautotunnel.ui.common.button.SheetButtonWithDivider
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.common.button.SwitchWithDivider import com.zaneschepke.wireguardautotunnel.ui.common.button.SwitchWithDivider
@@ -46,16 +55,21 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.asTitleString
import com.zaneschepke.wireguardautotunnel.util.extensions.capitalize import com.zaneschepke.wireguardautotunnel.util.extensions.capitalize
import com.zaneschepke.wireguardautotunnel.util.extensions.showToast import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
import com.zaneschepke.wireguardautotunnel.viewmodel.SettingsViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.SettingsViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
import org.koin.androidx.compose.koinViewModel
import org.koin.compose.viewmodel.koinActivityViewModel
@Composable @Composable
fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) { fun SettingsScreen(
viewModel: SettingsViewModel = koinViewModel(),
sharedViewModel: SharedAppViewModel = koinActivityViewModel(),
) {
val context = LocalContext.current val context = LocalContext.current
val navController = LocalNavController.current val navController = LocalNavController.current
val sharedViewModel = LocalSharedVm.current
val locale = Locale.current.platformLocale val locale = Locale.current.platformLocale
val sharedUiState by sharedViewModel.container.stateFlow.collectAsStateWithLifecycle() val globalUiState by sharedViewModel.container.stateFlow.collectAsStateWithLifecycle()
val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle() val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
if (uiState.isLoading) return if (uiState.isLoading) return
@@ -72,7 +86,7 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
} }
fun performBackupRestore(action: () -> Unit) { fun performBackupRestore(action: () -> Unit) {
if (sharedUiState.activeTunnels.isNotEmpty() || sharedUiState.isAutoTunnelActive) if (uiState.tunnelActive || globalUiState.isAutoTunnelActive)
return context.showToast(R.string.all_services_disabled) return context.showToast(R.string.all_services_disabled)
showBackupSheet = false showBackupSheet = false
action() action()
@@ -151,22 +165,22 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
Icons.AutoMirrored.Outlined.CallSplit, Icons.AutoMirrored.Outlined.CallSplit,
contentDescription = null, contentDescription = null,
tint = tint =
if (sharedUiState.proxyEnabled) Disabled if (globalUiState.appMode == AppMode.PROXY) Disabled
else MaterialTheme.colorScheme.onSurface, else MaterialTheme.colorScheme.onSurface,
) )
}, },
enabled = !sharedUiState.proxyEnabled, enabled = globalUiState.appMode != AppMode.PROXY,
title = stringResource(R.string.global_split_tunneling), title = stringResource(R.string.global_split_tunneling),
trailing = { modifier -> trailing = { modifier ->
SwitchWithDivider( SwitchWithDivider(
checked = uiState.settings.isGlobalSplitTunnelEnabled, checked = uiState.settings.isGlobalSplitTunnelEnabled,
onClick = { viewModel.setGlobalSplitTunneling(it) }, onClick = { viewModel.setGlobalSplitTunneling(it) },
modifier = modifier, modifier = modifier,
enabled = !sharedUiState.proxyEnabled, enabled = globalUiState.appMode != AppMode.PROXY,
) )
}, },
description = description =
if (sharedUiState.proxyEnabled) { if (globalUiState.appMode == AppMode.PROXY) {
{ {
DescriptionText( DescriptionText(
stringResource(R.string.unavailable_in_mode), stringResource(R.string.unavailable_in_mode),
@@ -197,14 +211,15 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
Icons.Outlined.NetworkPing, Icons.Outlined.NetworkPing,
contentDescription = null, contentDescription = null,
tint = tint =
if (!sharedUiState.proxyEnabled) MaterialTheme.colorScheme.onSurface if (globalUiState.appMode != AppMode.PROXY)
MaterialTheme.colorScheme.onSurface
else Disabled, else Disabled,
) )
}, },
title = stringResource(R.string.ping_monitor), title = stringResource(R.string.ping_monitor),
enabled = !sharedUiState.proxyEnabled, enabled = globalUiState.appMode != AppMode.PROXY,
description = description =
if (sharedUiState.proxyEnabled) { if (globalUiState.appMode == AppMode.PROXY) {
{ {
DescriptionText( DescriptionText(
stringResource(R.string.unavailable_in_mode), stringResource(R.string.unavailable_in_mode),
@@ -216,7 +231,7 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
SwitchWithDivider( SwitchWithDivider(
checked = uiState.monitoring.isPingEnabled, checked = uiState.monitoring.isPingEnabled,
onClick = { viewModel.setPingEnabled(it) }, onClick = { viewModel.setPingEnabled(it) },
enabled = !sharedUiState.proxyEnabled, enabled = globalUiState.appMode != AppMode.PROXY,
modifier = modifier, modifier = modifier,
) )
}, },
@@ -14,14 +14,13 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
import org.koin.compose.viewmodel.koinActivityViewModel
@Composable @Composable
fun DisplayScreen() { fun DisplayScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel()) {
val sharedViewModel = LocalSharedVm.current
val appState by sharedViewModel.container.stateFlow.collectAsStateWithLifecycle() val appState by sharedViewModel.container.stateFlow.collectAsStateWithLifecycle()
@@ -17,15 +17,15 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.intl.Locale
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
import java.text.Collator import java.text.Collator
import org.koin.compose.viewmodel.koinActivityViewModel
@Composable @Composable
fun LanguageScreen() { fun LanguageScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel()) {
val sharedViewModel = LocalSharedVm.current
val appState by sharedViewModel.container.stateFlow.collectAsStateWithLifecycle() val appState by sharedViewModel.container.stateFlow.collectAsStateWithLifecycle()
val collator = Collator.getInstance(Locale.current.platformLocale) val collator = Collator.getInstance(Locale.current.platformLocale)
@@ -21,7 +21,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.model.DnsProtocol import com.zaneschepke.wireguardautotunnel.data.model.DnsProtocol
@@ -34,9 +33,10 @@ import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
import com.zaneschepke.wireguardautotunnel.util.extensions.capitalize import com.zaneschepke.wireguardautotunnel.util.extensions.capitalize
import com.zaneschepke.wireguardautotunnel.viewmodel.DnsViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.DnsViewModel
import org.koin.androidx.compose.koinViewModel
@Composable @Composable
fun DnsSettingsScreen(viewModel: DnsViewModel = hiltViewModel()) { fun DnsSettingsScreen(viewModel: DnsViewModel = koinViewModel()) {
val context = LocalContext.current val context = LocalContext.current
val navController = LocalNavController.current val navController = LocalNavController.current
val dnsUiState by viewModel.container.stateFlow.collectAsStateWithLifecycle() val dnsUiState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
@@ -1,26 +1,40 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.integrations package com.zaneschepke.wireguardautotunnel.ui.screens.settings.integrations
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.Launch import androidx.compose.material.icons.automirrored.outlined.Launch
import androidx.compose.material.icons.filled.AppShortcut import androidx.compose.material.icons.filled.AppShortcut
import androidx.compose.material.icons.filled.SmartToy import androidx.compose.material.icons.filled.SmartToy
import androidx.compose.material.icons.outlined.* import androidx.compose.material.icons.outlined.AdminPanelSettings
import androidx.compose.material.icons.outlined.ContentCopy
import androidx.compose.material.icons.outlined.Key
import androidx.compose.material.icons.outlined.RemoveRedEye
import androidx.compose.material.icons.outlined.Restore
import androidx.compose.material.icons.outlined.VpnLock
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.* import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
@@ -32,9 +46,10 @@ import com.zaneschepke.wireguardautotunnel.ui.common.security.SecureScreenFromRe
import com.zaneschepke.wireguardautotunnel.ui.common.text.DescriptionText import com.zaneschepke.wireguardautotunnel.ui.common.text.DescriptionText
import com.zaneschepke.wireguardautotunnel.util.extensions.launchVpnSettings import com.zaneschepke.wireguardautotunnel.util.extensions.launchVpnSettings
import com.zaneschepke.wireguardautotunnel.viewmodel.SettingsViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.SettingsViewModel
import org.koin.androidx.compose.koinViewModel
@Composable @Composable
fun AndroidIntegrationsScreen(viewModel: SettingsViewModel = hiltViewModel()) { fun AndroidIntegrationsScreen(viewModel: SettingsViewModel = koinViewModel()) {
val context = LocalContext.current val context = LocalContext.current
val isTv = LocalIsAndroidTV.current val isTv = LocalIsAndroidTV.current
@@ -24,10 +24,8 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.common.button.ThemedSwitch import com.zaneschepke.wireguardautotunnel.ui.common.button.ThemedSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
@@ -35,12 +33,16 @@ import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.common.text.DescriptionText import com.zaneschepke.wireguardautotunnel.ui.common.text.DescriptionText
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
import com.zaneschepke.wireguardautotunnel.viewmodel.LockdownViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.LockdownViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
import org.koin.androidx.compose.koinViewModel
import org.koin.compose.viewmodel.koinActivityViewModel
import org.orbitmvi.orbit.compose.collectSideEffect import org.orbitmvi.orbit.compose.collectSideEffect
@Composable @Composable
fun LockdownSettingsScreen(viewModel: LockdownViewModel = hiltViewModel()) { fun LockdownSettingsScreen(
viewModel: LockdownViewModel = koinViewModel(),
val sharedViewModel = LocalSharedVm.current sharedViewModel: SharedAppViewModel = koinActivityViewModel(),
) {
val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle() val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
@@ -7,7 +7,11 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.* import androidx.compose.material.icons.outlined.Adjust
import androidx.compose.material.icons.outlined.QueryStats
import androidx.compose.material.icons.outlined.Replay
import androidx.compose.material.icons.outlined.Timer
import androidx.compose.material.icons.outlined.TimerOff
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -17,7 +21,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
@@ -27,9 +30,10 @@ import com.zaneschepke.wireguardautotunnel.ui.common.dropdown.LabelledDropdown
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
import com.zaneschepke.wireguardautotunnel.viewmodel.MonitoringViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.MonitoringViewModel
import org.koin.androidx.compose.koinViewModel
@Composable @Composable
fun TunnelMonitoringScreen(viewModel: MonitoringViewModel = hiltViewModel()) { fun TunnelMonitoringScreen(viewModel: MonitoringViewModel = koinViewModel()) {
val navController = LocalNavController.current val navController = LocalNavController.current
val monitoringUiState by viewModel.container.stateFlow.collectAsStateWithLifecycle() val monitoringUiState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
@@ -5,25 +5,34 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.* import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.logs.components.LogList import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.logs.components.LogList
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.logs.components.LogsBottomSheet import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.logs.components.LogsBottomSheet
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
import com.zaneschepke.wireguardautotunnel.viewmodel.LoggerViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.LoggerViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
import org.koin.androidx.compose.koinViewModel
import org.koin.compose.viewmodel.koinActivityViewModel
import org.orbitmvi.orbit.compose.collectSideEffect import org.orbitmvi.orbit.compose.collectSideEffect
@Composable @Composable
fun LogsScreen(viewModel: LoggerViewModel = hiltViewModel()) { fun LogsScreen(
val sharedAppViewModel = LocalSharedVm.current viewModel: LoggerViewModel = koinViewModel(),
sharedViewModel: SharedAppViewModel = koinActivityViewModel(),
) {
val loggerState by viewModel.container.stateFlow.collectAsStateWithLifecycle() val loggerState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
val lazyColumnListState = rememberLazyListState() val lazyColumnListState = rememberLazyListState()
@@ -31,7 +40,7 @@ fun LogsScreen(viewModel: LoggerViewModel = hiltViewModel()) {
var lastScrollPosition by rememberSaveable { mutableIntStateOf(0) } var lastScrollPosition by rememberSaveable { mutableIntStateOf(0) }
var showLogsSheet by rememberSaveable { mutableStateOf(false) } var showLogsSheet by rememberSaveable { mutableStateOf(false) }
sharedAppViewModel.collectSideEffect { sideEffect -> sharedViewModel.collectSideEffect { sideEffect ->
if (sideEffect is LocalSideEffect.Sheet.LoggerActions) showLogsSheet = true if (sideEffect is LocalSideEffect.Sheet.LoggerActions) showLogsSheet = true
} }
@@ -19,7 +19,12 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.* import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
@@ -29,7 +34,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
@@ -39,9 +43,10 @@ import com.zaneschepke.wireguardautotunnel.ui.common.text.DescriptionText
import com.zaneschepke.wireguardautotunnel.ui.common.textbox.CustomTextField import com.zaneschepke.wireguardautotunnel.ui.common.textbox.CustomTextField
import com.zaneschepke.wireguardautotunnel.util.extensions.isValidIpv4orIpv6Address import com.zaneschepke.wireguardautotunnel.util.extensions.isValidIpv4orIpv6Address
import com.zaneschepke.wireguardautotunnel.viewmodel.MonitoringViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.MonitoringViewModel
import org.koin.androidx.compose.koinViewModel
@Composable @Composable
fun PingTargetScreen(viewModel: MonitoringViewModel = hiltViewModel()) { fun PingTargetScreen(viewModel: MonitoringViewModel = koinViewModel()) {
val settingsState by viewModel.container.stateFlow.collectAsStateWithLifecycle() val settingsState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
@@ -21,11 +21,9 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.domain.model.ProxySettings import com.zaneschepke.wireguardautotunnel.domain.model.ProxySettings
import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.common.button.ThemedSwitch import com.zaneschepke.wireguardautotunnel.ui.common.button.ThemedSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
@@ -34,12 +32,16 @@ import com.zaneschepke.wireguardautotunnel.ui.common.security.SecureScreenFromRe
import com.zaneschepke.wireguardautotunnel.ui.common.textbox.ConfigurationTextBox import com.zaneschepke.wireguardautotunnel.ui.common.textbox.ConfigurationTextBox
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
import com.zaneschepke.wireguardautotunnel.viewmodel.ProxySettingsViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.ProxySettingsViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
import org.koin.androidx.compose.koinViewModel
import org.koin.compose.viewmodel.koinActivityViewModel
import org.orbitmvi.orbit.compose.collectSideEffect import org.orbitmvi.orbit.compose.collectSideEffect
@Composable @Composable
fun ProxySettingsScreen(viewModel: ProxySettingsViewModel = hiltViewModel()) { fun ProxySettingsScreen(
val sharedViewModel = LocalSharedVm.current viewModel: ProxySettingsViewModel = koinViewModel(),
sharedViewModel: SharedAppViewModel = koinActivityViewModel(),
) {
val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle() val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
if (uiState.isLoading) return if (uiState.isLoading) return
@@ -8,6 +8,7 @@ import com.zaneschepke.wireguardautotunnel.ui.common.sheet.CustomBottomSheet
import com.zaneschepke.wireguardautotunnel.ui.common.sheet.SheetOption import com.zaneschepke.wireguardautotunnel.ui.common.sheet.SheetOption
import com.zaneschepke.wireguardautotunnel.util.extensions.asIcon import com.zaneschepke.wireguardautotunnel.util.extensions.asIcon
import com.zaneschepke.wireguardautotunnel.util.extensions.asTitleString import com.zaneschepke.wireguardautotunnel.util.extensions.asTitleString
import com.zaneschepke.wireguardautotunnel.util.extensions.description
@Composable @Composable
fun AppModeBottomSheet( fun AppModeBottomSheet(
@@ -31,6 +32,7 @@ fun AppModeBottomSheet(
onAppModeChange(it) onAppModeChange(it)
}, },
selected = appMode == it, selected = appMode == it,
description = it.description(context),
) )
} }
) { ) {
@@ -17,7 +17,6 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.BuildConfig import com.zaneschepke.wireguardautotunnel.BuildConfig
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
@@ -32,9 +31,10 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.support.components.UpdateD
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.* import com.zaneschepke.wireguardautotunnel.util.extensions.*
import com.zaneschepke.wireguardautotunnel.viewmodel.SupportViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.SupportViewModel
import org.koin.androidx.compose.koinViewModel
@Composable @Composable
fun SupportScreen(viewModel: SupportViewModel = hiltViewModel()) { fun SupportScreen(viewModel: SupportViewModel = koinViewModel()) {
val context = LocalContext.current val context = LocalContext.current
val navController = LocalNavController.current val navController = LocalNavController.current
@@ -191,7 +191,8 @@ fun SupportScreen(viewModel: SupportViewModel = hiltViewModel()) {
return@SurfaceRow context.showToast(R.string.update_check_unsupported) return@SurfaceRow context.showToast(R.string.update_check_unsupported)
when (BuildConfig.FLAVOR) { when (BuildConfig.FLAVOR) {
Constants.GOOGLE_PLAY_FLAVOR -> context.launchPlayStoreListing() Constants.GOOGLE_PLAY_FLAVOR -> context.launchPlayStoreListing()
Constants.FDROID_FLAVOR -> context.launchFDroidListing() Constants.FDROID_FLAVOR ->
context.openWebUrl(context.getString(R.string.fdroid_url))
else -> viewModel.checkForStandaloneUpdate() else -> viewModel.checkForStandaloneUpdate()
} }
}, },
@@ -18,7 +18,6 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.BuildConfig import com.zaneschepke.wireguardautotunnel.BuildConfig
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
@@ -33,9 +32,10 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.support.donate.components.
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
import com.zaneschepke.wireguardautotunnel.viewmodel.SettingsViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.SettingsViewModel
import org.koin.androidx.compose.koinViewModel
@Composable @Composable
fun DonateScreen(viewModel: SettingsViewModel = hiltViewModel()) { fun DonateScreen(viewModel: SettingsViewModel = koinViewModel()) {
val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle() val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
if (uiState.isLoading) return if (uiState.isLoading) return
val context = LocalContext.current val context = LocalContext.current
@@ -10,14 +10,14 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.ui.screens.support.license.components.LicenseList import com.zaneschepke.wireguardautotunnel.ui.screens.support.license.components.LicenseList
import com.zaneschepke.wireguardautotunnel.viewmodel.LicenseViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.LicenseViewModel
import org.koin.androidx.compose.koinViewModel
@OptIn(ExperimentalMaterial3ExpressiveApi::class) @OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable @Composable
fun LicenseScreen(viewModel: LicenseViewModel = hiltViewModel()) { fun LicenseScreen(viewModel: LicenseViewModel = koinViewModel()) {
val licenseUiState by viewModel.container.stateFlow.collectAsStateWithLifecycle() val licenseUiState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
if (licenseUiState.isLoading) { if (licenseUiState.isLoading) {
@@ -14,7 +14,6 @@ import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberClipboardHelper import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberClipboardHelper
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberFileImportLauncherForResult import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberFileImportLauncherForResult
@@ -26,31 +25,34 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.components.UrlImpo
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
import com.zaneschepke.wireguardautotunnel.util.FileUtils import com.zaneschepke.wireguardautotunnel.util.FileUtils
import com.zaneschepke.wireguardautotunnel.util.StringValue import com.zaneschepke.wireguardautotunnel.util.StringValue
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
import io.github.g00fy2.quickie.QRResult import io.github.g00fy2.quickie.QRResult
import io.github.g00fy2.quickie.ScanQRCode import io.github.g00fy2.quickie.ScanQRCode
import org.koin.compose.viewmodel.koinActivityViewModel
import org.orbitmvi.orbit.compose.collectSideEffect import org.orbitmvi.orbit.compose.collectSideEffect
import timber.log.Timber import timber.log.Timber
@Composable @Composable
fun TunnelsScreen() { fun TunnelsScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel()) {
val viewModel = LocalSharedVm.current
val navController = LocalNavController.current val navController = LocalNavController.current
val clipboard = rememberClipboardHelper() val clipboard = rememberClipboardHelper()
val sharedState by viewModel.container.stateFlow.collectAsStateWithLifecycle() val uiState by sharedViewModel.tunnelsUiState.collectAsStateWithLifecycle()
if (uiState.isLoading) return
var showExportSheet by rememberSaveable { mutableStateOf(false) } var showExportSheet by rememberSaveable { mutableStateOf(false) }
var showImportSheet by rememberSaveable { mutableStateOf(false) } var showImportSheet by rememberSaveable { mutableStateOf(false) }
var showDeleteModal by rememberSaveable { mutableStateOf(false) } var showDeleteModal by rememberSaveable { mutableStateOf(false) }
var showUrlDialog by rememberSaveable { mutableStateOf(false) } var showUrlDialog by rememberSaveable { mutableStateOf(false) }
viewModel.collectSideEffect { sideEffect -> sharedViewModel.collectSideEffect { sideEffect ->
when (sideEffect) { when (sideEffect) {
LocalSideEffect.Sheet.ImportTunnels -> showImportSheet = true LocalSideEffect.Sheet.ImportTunnels -> showImportSheet = true
LocalSideEffect.Modal.DeleteTunnels -> showDeleteModal = true LocalSideEffect.Modal.DeleteTunnels -> showDeleteModal = true
LocalSideEffect.Sheet.ExportTunnels -> showExportSheet = true LocalSideEffect.Sheet.ExportTunnels -> showExportSheet = true
LocalSideEffect.SelectedTunnels.Copy -> viewModel.copySelectedTunnel() LocalSideEffect.SelectedTunnels.Copy -> sharedViewModel.copySelectedTunnel()
LocalSideEffect.SelectedTunnels.SelectAll -> viewModel.toggleSelectAllTunnels() LocalSideEffect.SelectedTunnels.SelectAll -> sharedViewModel.toggleSelectAllTunnels()
else -> Unit else -> Unit
} }
} }
@@ -58,11 +60,11 @@ fun TunnelsScreen() {
val tunnelFileImportResultLauncher = val tunnelFileImportResultLauncher =
rememberFileImportLauncherForResult( rememberFileImportLauncherForResult(
onNoFileExplorer = { onNoFileExplorer = {
viewModel.showSnackMessage( sharedViewModel.showSnackMessage(
StringValue.StringResource(R.string.error_no_file_explorer) StringValue.StringResource(R.string.error_no_file_explorer)
) )
}, },
onData = { data -> viewModel.importFromUri(data) }, onData = { data -> sharedViewModel.importFromUri(data) },
) )
val scanQrCodeLauncher = val scanQrCodeLauncher =
@@ -72,13 +74,13 @@ fun TunnelsScreen() {
Timber.e(result.exception, "QR Code") Timber.e(result.exception, "QR Code")
} }
QRResult.QRMissingPermission -> { QRResult.QRMissingPermission -> {
viewModel.showSnackMessage( sharedViewModel.showSnackMessage(
StringValue.StringResource(R.string.camera_permission_required) StringValue.StringResource(R.string.camera_permission_required)
) )
} }
is QRResult.QRSuccess -> { is QRResult.QRSuccess -> {
result.content.rawValue?.let { viewModel.importFromQr(it) } result.content.rawValue?.let { sharedViewModel.importFromQr(it) }
?: viewModel.showSnackMessage( ?: sharedViewModel.showSnackMessage(
StringValue.StringResource(R.string.config_error) StringValue.StringResource(R.string.config_error)
) )
} }
@@ -90,7 +92,7 @@ fun TunnelsScreen() {
rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted
-> ->
if (!isGranted) { if (!isGranted) {
viewModel.showSnackMessage( sharedViewModel.showSnackMessage(
StringValue.StringResource(R.string.camera_permission_required) StringValue.StringResource(R.string.camera_permission_required)
) )
return@rememberLauncherForActivityResult return@rememberLauncherForActivityResult
@@ -102,7 +104,7 @@ fun TunnelsScreen() {
InfoDialog( InfoDialog(
onDismiss = { showDeleteModal = false }, onDismiss = { showDeleteModal = false },
onAttest = { onAttest = {
viewModel.deleteSelectedTunnels() sharedViewModel.deleteSelectedTunnels()
showDeleteModal = false showDeleteModal = false
}, },
title = stringResource(R.string.delete_tunnel), title = stringResource(R.string.delete_tunnel),
@@ -113,11 +115,11 @@ fun TunnelsScreen() {
if (showExportSheet) { if (showExportSheet) {
ExportTunnelsBottomSheet({ type, uri -> ExportTunnelsBottomSheet({ type, uri ->
viewModel.exportSelectedTunnels(type, uri) sharedViewModel.exportSelectedTunnels(type, uri)
showExportSheet = false showExportSheet = false
}) { }) {
showExportSheet = false showExportSheet = false
viewModel.clearSelectedTunnels() sharedViewModel.clearSelectedTunnels()
} }
} }
@@ -130,7 +132,7 @@ fun TunnelsScreen() {
onQrClick = { requestPermissionLauncher.launch(android.Manifest.permission.CAMERA) }, onQrClick = { requestPermissionLauncher.launch(android.Manifest.permission.CAMERA) },
onClipboardClick = { onClipboardClick = {
clipboard.paste { result -> clipboard.paste { result ->
if (result != null) viewModel.importFromClipboard(result) if (result != null) sharedViewModel.importFromClipboard(result)
} }
}, },
onManualImportClick = { navController.push(Route.Config(null)) }, onManualImportClick = { navController.push(Route.Config(null)) },
@@ -142,11 +144,11 @@ fun TunnelsScreen() {
UrlImportDialog( UrlImportDialog(
onDismiss = { showUrlDialog = false }, onDismiss = { showUrlDialog = false },
onConfirm = { url -> onConfirm = { url ->
viewModel.importFromUrl(url) sharedViewModel.importFromUrl(url)
showUrlDialog = false showUrlDialog = false
}, },
) )
} }
TunnelList(sharedState, Modifier.fillMaxSize(), viewModel) TunnelList(uiState, Modifier.fillMaxSize(), sharedViewModel)
} }
@@ -12,7 +12,11 @@ import androidx.compose.foundation.rememberOverscrollEffect
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Circle import androidx.compose.material.icons.rounded.Circle
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.runtime.* import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
@@ -25,7 +29,7 @@ import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.common.button.SwitchWithDivider import com.zaneschepke.wireguardautotunnel.ui.common.button.SwitchWithDivider
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
import com.zaneschepke.wireguardautotunnel.ui.state.SharedAppUiState import com.zaneschepke.wireguardautotunnel.ui.state.TunnelsUiState
import com.zaneschepke.wireguardautotunnel.util.extensions.asColor import com.zaneschepke.wireguardautotunnel.util.extensions.asColor
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
@@ -33,7 +37,7 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun TunnelList( fun TunnelList(
sharedState: SharedAppUiState, uiState: TunnelsUiState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: SharedAppViewModel, viewModel: SharedAppViewModel,
) { ) {
@@ -48,7 +52,7 @@ fun TunnelList(
modifier modifier
.pointerInput(Unit) { .pointerInput(Unit) {
detectTapGestures { detectTapGestures {
if (sharedState.tunnels.isEmpty()) return@detectTapGestures if (uiState.tunnels.isEmpty()) return@detectTapGestures
viewModel.clearSelectedTunnels() viewModel.clearSelectedTunnels()
} }
} }
@@ -58,7 +62,7 @@ fun TunnelList(
reverseLayout = false, reverseLayout = false,
flingBehavior = ScrollableDefaults.flingBehavior(), flingBehavior = ScrollableDefaults.flingBehavior(),
) { ) {
if (sharedState.tunnels.isEmpty()) { if (uiState.tunnels.isEmpty()) {
item { item {
GettingStartedLabel( GettingStartedLabel(
onClick = { context.openWebUrl(it) }, onClick = { context.openWebUrl(it) },
@@ -66,14 +70,14 @@ fun TunnelList(
) )
} }
} }
items(sharedState.tunnels, key = { it.id }) { tunnel -> items(uiState.tunnels, key = { it.id }) { tunnel ->
val tunnelState = val tunnelState =
remember(sharedState.activeTunnels) { remember(uiState.activeTunnels) {
sharedState.activeTunnels[tunnel.id] ?: TunnelState() uiState.activeTunnels[tunnel.id] ?: TunnelState()
} }
val selected = val selected =
remember(sharedState.selectedTunnels) { remember(uiState.selectedTunnels) {
sharedState.selectedTunnels.any { it.id == tunnel.id } uiState.selectedTunnels.any { it.id == tunnel.id }
} }
var leadingIconColor by var leadingIconColor by
remember( remember(
@@ -97,7 +101,7 @@ fun TunnelList(
}, },
title = tunnel.name, title = tunnel.name,
onClick = { onClick = {
if (sharedState.selectedTunnels.isNotEmpty()) { if (uiState.selectedTunnels.isNotEmpty()) {
viewModel.toggleSelectedTunnel(tunnel.id) viewModel.toggleSelectedTunnel(tunnel.id)
} else { } else {
navController.push(Route.TunnelSettings(tunnel.id)) navController.push(Route.TunnelSettings(tunnel.id))
@@ -111,8 +115,8 @@ fun TunnelList(
TunnelStatisticsRow( TunnelStatisticsRow(
tunnel, tunnel,
tunnelState, tunnelState,
sharedState.isPingEnabled, uiState.isPingEnabled,
sharedState.showPingStats, uiState.showPingStats,
) )
} }
} else null, } else null,
@@ -1,42 +1,47 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.components package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.MaterialTheme
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
import com.zaneschepke.wireguardautotunnel.ui.common.textbox.ConfigurationTextBox
@Composable @Composable
fun UrlImportDialog(onDismiss: () -> Unit, onConfirm: (String) -> Unit) { fun UrlImportDialog(onDismiss: () -> Unit, onConfirm: (String) -> Unit) {
var url by remember { mutableStateOf("") } var url by remember { mutableStateOf("") }
var isError by remember { mutableStateOf(false) }
AlertDialog( LaunchedEffect(url) { isError = false }
onDismissRequest = onDismiss,
title = { Text(stringResource(R.string.add_from_url)) }, InfoDialog(
text = { onDismiss = onDismiss,
Column(modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp)) { title = stringResource(R.string.add_from_url),
OutlinedTextField( body = {
Column(verticalArrangement = Arrangement.spacedBy(24.dp)) {
Text(
stringResource(R.string.import_url_description),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
)
ConfigurationTextBox(
value = url, value = url,
label = stringResource(R.string.enter_config_url),
hint = stringResource(R.string.example_import_url),
onValueChange = { url = it }, onValueChange = { url = it },
label = { Text(stringResource(R.string.enter_config_url)) }, isError = isError,
modifier = Modifier.fillMaxWidth(),
) )
} }
}, },
confirmButton = { confirmText = stringResource(R.string.okay),
TextButton(onClick = { onConfirm(url) }, enabled = url.isNotBlank()) { onAttest = {
Text(stringResource(R.string.okay)) if (url.isNotBlank() && url.startsWith("https://")) {
} onConfirm(url)
}, } else isError = true
dismissButton = {
TextButton(onClick = onDismiss) { Text(stringResource(R.string.cancel)) }
}, },
) )
} }
@@ -6,8 +6,13 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.* import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@@ -16,7 +21,6 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
import com.zaneschepke.wireguardautotunnel.ui.common.security.SecureScreenFromRecording import com.zaneschepke.wireguardautotunnel.ui.common.security.SecureScreenFromRecording
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.config.components.AddPeerButton import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.config.components.AddPeerButton
@@ -26,11 +30,15 @@ import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
import com.zaneschepke.wireguardautotunnel.ui.state.ConfigProxy import com.zaneschepke.wireguardautotunnel.ui.state.ConfigProxy
import com.zaneschepke.wireguardautotunnel.ui.state.PeerProxy import com.zaneschepke.wireguardautotunnel.ui.state.PeerProxy
import com.zaneschepke.wireguardautotunnel.viewmodel.ConfigViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.ConfigViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
import org.koin.compose.viewmodel.koinActivityViewModel
import org.orbitmvi.orbit.compose.collectSideEffect import org.orbitmvi.orbit.compose.collectSideEffect
@Composable @Composable
fun ConfigScreen(viewModel: ConfigViewModel) { fun ConfigScreen(
val sharedViewModel = LocalSharedVm.current viewModel: ConfigViewModel,
sharedViewModel: SharedAppViewModel = koinActivityViewModel(),
) {
val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle() val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
@@ -28,8 +28,8 @@ import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.common.button.ThemedSwitch import com.zaneschepke.wireguardautotunnel.ui.common.button.ThemedSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
@@ -38,13 +38,17 @@ import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.settings.components.QrCodeDialog import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.settings.components.QrCodeDialog
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
import com.zaneschepke.wireguardautotunnel.ui.theme.Disabled import com.zaneschepke.wireguardautotunnel.ui.theme.Disabled
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelViewModel
import org.koin.compose.viewmodel.koinActivityViewModel
import org.orbitmvi.orbit.compose.collectSideEffect import org.orbitmvi.orbit.compose.collectSideEffect
@Composable @Composable
fun TunnelSettingsScreen(viewModel: TunnelViewModel) { fun TunnelSettingsScreen(
viewModel: TunnelViewModel,
sharedViewModel: SharedAppViewModel = koinActivityViewModel(),
) {
val navController = LocalNavController.current val navController = LocalNavController.current
val sharedViewModel = LocalSharedVm.current
val sharedUiState by sharedViewModel.container.stateFlow.collectAsStateWithLifecycle() val sharedUiState by sharedViewModel.container.stateFlow.collectAsStateWithLifecycle()
@@ -99,14 +103,14 @@ fun TunnelSettingsScreen(viewModel: TunnelViewModel) {
Icons.AutoMirrored.Outlined.CallSplit, Icons.AutoMirrored.Outlined.CallSplit,
contentDescription = null, contentDescription = null,
tint = tint =
if (sharedUiState.proxyEnabled) Disabled if (sharedUiState.appMode == AppMode.PROXY) Disabled
else MaterialTheme.colorScheme.onSurface, else MaterialTheme.colorScheme.onSurface,
) )
}, },
enabled = !sharedUiState.proxyEnabled, enabled = sharedUiState.appMode != AppMode.PROXY,
title = stringResource(R.string.splt_tunneling), title = stringResource(R.string.splt_tunneling),
description = description =
if (sharedUiState.proxyEnabled) { if (sharedUiState.appMode == AppMode.PROXY) {
{ {
DescriptionText( DescriptionText(
stringResource(R.string.unavailable_in_mode), stringResource(R.string.unavailable_in_mode),
@@ -156,14 +160,14 @@ fun TunnelSettingsScreen(viewModel: TunnelViewModel) {
Icons.Outlined.DataUsage, Icons.Outlined.DataUsage,
contentDescription = null, contentDescription = null,
tint = tint =
if (sharedUiState.proxyEnabled) Disabled if (sharedUiState.appMode == AppMode.PROXY) Disabled
else MaterialTheme.colorScheme.onSurface, else MaterialTheme.colorScheme.onSurface,
) )
}, },
title = stringResource(R.string.metered_tunnel), title = stringResource(R.string.metered_tunnel),
enabled = !sharedUiState.proxyEnabled, enabled = sharedUiState.appMode != AppMode.PROXY,
description = description =
if (sharedUiState.proxyEnabled) { if (sharedUiState.appMode == AppMode.PROXY) {
{ {
DescriptionText( DescriptionText(
stringResource(R.string.unavailable_in_mode), stringResource(R.string.unavailable_in_mode),
@@ -175,7 +179,7 @@ fun TunnelSettingsScreen(viewModel: TunnelViewModel) {
ThemedSwitch( ThemedSwitch(
checked = tunnel.isMetered, checked = tunnel.isMetered,
onClick = { viewModel.setMetered(it) }, onClick = { viewModel.setMetered(it) },
enabled = !sharedUiState.proxyEnabled, enabled = sharedUiState.appMode != AppMode.PROXY,
) )
}, },
onClick = { viewModel.setMetered(!tunnel.isMetered) }, onClick = { viewModel.setMetered(!tunnel.isMetered) },
@@ -28,29 +28,29 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm
import com.zaneschepke.wireguardautotunnel.ui.common.ExpandingRowListItem import com.zaneschepke.wireguardautotunnel.ui.common.ExpandingRowListItem
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
import com.zaneschepke.wireguardautotunnel.util.extensions.isSortedBy import com.zaneschepke.wireguardautotunnel.util.extensions.isSortedBy
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
import org.koin.compose.viewmodel.koinActivityViewModel
import org.orbitmvi.orbit.compose.collectSideEffect import org.orbitmvi.orbit.compose.collectSideEffect
import sh.calvin.reorderable.DragGestureDetector import sh.calvin.reorderable.DragGestureDetector
import sh.calvin.reorderable.ReorderableItem import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.rememberReorderableLazyListState import sh.calvin.reorderable.rememberReorderableLazyListState
@Composable @Composable
fun SortScreen() { fun SortScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel()) {
val viewModel = LocalSharedVm.current val tunnelsUiState by sharedViewModel.tunnelsUiState.collectAsStateWithLifecycle()
val sharedState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
val hapticFeedback = LocalHapticFeedback.current val hapticFeedback = LocalHapticFeedback.current
val isTv = LocalIsAndroidTV.current val isTv = LocalIsAndroidTV.current
var sortAscending by rememberSaveable { mutableStateOf<Boolean?>(null) } var sortAscending by rememberSaveable { mutableStateOf<Boolean?>(null) }
var editableTunnels by rememberSaveable { mutableStateOf(sharedState.tunnels) } var editableTunnels by rememberSaveable { mutableStateOf(tunnelsUiState.tunnels) }
viewModel.collectSideEffect { sideEffect -> sharedViewModel.collectSideEffect { sideEffect ->
when (sideEffect) { when (sideEffect) {
LocalSideEffect.SaveChanges -> { LocalSideEffect.SaveChanges -> {
viewModel.saveSortChanges(editableTunnels) sharedViewModel.saveSortChanges(editableTunnels)
} }
LocalSideEffect.Sort -> { LocalSideEffect.Sort -> {
sortAscending = sortAscending =
@@ -63,7 +63,7 @@ fun SortScreen() {
when (sortAscending) { when (sortAscending) {
true -> editableTunnels.sortedBy { it.name } true -> editableTunnels.sortedBy { it.name }
false -> editableTunnels.sortedByDescending { it.name } false -> editableTunnels.sortedByDescending { it.name }
null -> sharedState.tunnels null -> tunnelsUiState.tunnels
} }
} }
else -> Unit else -> Unit
@@ -86,7 +86,9 @@ fun SortScreen() {
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(5.dp, Alignment.Top), verticalArrangement = Arrangement.spacedBy(5.dp, Alignment.Top),
modifier = modifier =
Modifier.pointerInput(Unit) { if (sharedState.tunnels.isEmpty()) return@pointerInput } Modifier.pointerInput(Unit) {
if (tunnelsUiState.tunnels.isEmpty()) return@pointerInput
}
.overscroll(rememberOverscrollEffect()), .overscroll(rememberOverscrollEffect()),
state = lazyListState, state = lazyListState,
userScrollEnabled = true, userScrollEnabled = true,
@@ -5,25 +5,33 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material3.CircularWavyProgressIndicator import androidx.compose.material3.CircularWavyProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.runtime.* import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.splittunnel.components.SelectTunnelModal import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.splittunnel.components.SelectTunnelModal
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.splittunnel.components.SplitTunnelContent import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.splittunnel.components.SplitTunnelContent
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.splittunnel.state.SplitOption import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.splittunnel.state.SplitOption
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.SplitTunnelViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.SplitTunnelViewModel
import org.koin.compose.viewmodel.koinActivityViewModel
import org.orbitmvi.orbit.compose.collectSideEffect import org.orbitmvi.orbit.compose.collectSideEffect
@OptIn(ExperimentalMaterial3ExpressiveApi::class) @OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable @Composable
fun SplitTunnelScreen(viewModel: SplitTunnelViewModel) { fun SplitTunnelScreen(
val sharedViewModel = LocalSharedVm.current viewModel: SplitTunnelViewModel,
sharedViewModel: SharedAppViewModel = koinActivityViewModel(),
) {
val sharedUiState by sharedViewModel.container.stateFlow.collectAsStateWithLifecycle() val tunnelsUiState by sharedViewModel.tunnelsUiState.collectAsStateWithLifecycle()
val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle() val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
var showDialog by remember { mutableStateOf(false) } var showDialog by remember { mutableStateOf(false) }
@@ -61,7 +69,7 @@ fun SplitTunnelScreen(viewModel: SplitTunnelViewModel) {
SelectTunnelModal( SelectTunnelModal(
showDialog, showDialog,
sharedUiState.tunnels, tunnelsUiState.tunnels,
onAttest = { conf -> onAttest = { conf ->
if (conf == null) return@SelectTunnelModal if (conf == null) return@SelectTunnelModal
effectiveTunnel = conf effectiveTunnel = conf
@@ -1,26 +1,21 @@
package com.zaneschepke.wireguardautotunnel.ui.state package com.zaneschepke.wireguardautotunnel.ui.state
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
data class SharedAppUiState( data class GlobalAppUiState(
val isAppLoaded: Boolean = false, val isAppLoaded: Boolean = false,
val theme: Theme = Theme.AUTOMATIC, val theme: Theme = Theme.AUTOMATIC,
val locale: String = LocaleUtil.OPTION_PHONE_LANGUAGE, val locale: String = LocaleUtil.OPTION_PHONE_LANGUAGE,
val pinLockEnabled: Boolean = false, val pinLockEnabled: Boolean = false,
val appMode: AppMode = AppMode.VPN,
val shouldShowDonationSnackbar: Boolean = false, val shouldShowDonationSnackbar: Boolean = false,
val tunnels: List<TunnelConfig> = emptyList(),
val selectedTunnels: List<TunnelConfig> = emptyList(),
val activeTunnels: Map<Int, TunnelState> = emptyMap(),
val isPingEnabled: Boolean = false,
val showPingStats: Boolean = false,
val isPinVerified: Boolean = false,
val isAutoTunnelActive: Boolean = false,
val isLocationDisclosureShown: Boolean = false, val isLocationDisclosureShown: Boolean = false,
val isBatteryOptimizationShown: Boolean = false, val isBatteryOptimizationShown: Boolean = false,
val proxyEnabled: Boolean = false, val isAutoTunnelActive: Boolean = false,
val settings: GeneralSettings = GeneralSettings(), val tunnelNames: Map<Int, String> = emptyMap(),
val selectedTunnelCount: Int = 0,
val alreadyDonated: Boolean = false,
val isPinVerified: Boolean = false,
) )
@@ -15,4 +15,5 @@ data class SettingUiState(
val globalTunnelConfig: TunnelConfig? = null, val globalTunnelConfig: TunnelConfig? = null,
val tunnels: List<TunnelConfig> = emptyList(), val tunnels: List<TunnelConfig> = emptyList(),
val monitoring: MonitoringSettings = MonitoringSettings(), val monitoring: MonitoringSettings = MonitoringSettings(),
val tunnelActive: Boolean = false,
) )
@@ -1,8 +1,13 @@
package com.zaneschepke.wireguardautotunnel.ui.state package com.zaneschepke.wireguardautotunnel.ui.state
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
data class TunnelsUiState( data class TunnelsUiState(
val tunnels: List<TunnelConfig> = emptyList(), val tunnels: List<TunnelConfig> = emptyList(),
val activeTunnels: Map<Int, TunnelState> = emptyMap(),
val selectedTunnels: List<TunnelConfig> = emptyList(),
val isPingEnabled: Boolean = false,
val showPingStats: Boolean = false,
val isLoading: Boolean = true, val isLoading: Boolean = true,
) )
@@ -9,7 +9,6 @@ import android.os.Build
import android.provider.MediaStore import android.provider.MediaStore
import android.provider.OpenableColumns import android.provider.OpenableColumns
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.util.extensions.QuickConfig import com.zaneschepke.wireguardautotunnel.util.extensions.QuickConfig
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelName import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelName
import com.zaneschepke.wireguardautotunnel.util.extensions.getInputStreamFromUri import com.zaneschepke.wireguardautotunnel.util.extensions.getInputStreamFromUri
@@ -22,10 +21,7 @@ import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import timber.log.Timber import timber.log.Timber
class FileUtils( class FileUtils(private val context: Context, private val ioDispatcher: CoroutineDispatcher) {
private val context: Context,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) {
/** /**
* Creates a configuration file for a given tunnel if the config string is valid. * Creates a configuration file for a given tunnel if the config string is valid.

Some files were not shown because too many files have changed in this diff Show More