Compare commits

...

41 Commits

Author SHA1 Message Date
zaneschepke cbef4600af fix: version conflict 2026-03-09 20:52:25 -04:00
zaneschepke c24c33c95c chore: release 4.3.1
Fixes google version collision
2026-03-09 20:31:35 -04:00
zaneschepke c130247df1 chore: release 4.3.0 2026-03-09 19:54:14 -04:00
zaneschepke 074229b6b4 fix: doze mode tunnel connectivity bug 2026-03-09 16:30:03 -04:00
zaneschepke 585176f08d chore: fmt 2026-03-07 22:13:38 -05:00
Max Grakov 2ed06728e3 feat: show consumed traffic in notification (#1165) 2026-03-07 21:58:32 -05:00
Naonak 2d9c5ece4a fix(core): always poll WireGuard stats regardless of Doze mode (#1177) 2026-03-07 18:48:15 -05:00
Weblate (bot) 3b69f620fb Translations update from Hosted Weblate (#1124)
Co-authored-by: Prefill add-on <noreply-addon-prefill@weblate.org>
Co-authored-by: Matthaiks <kitynska@gmail.com>
Co-authored-by: Priit Jõerüüt <jrthwlate@users.noreply.hosted.weblate.org>
Co-authored-by: EESF-2 <eesf-2@users.noreply.hosted.weblate.org>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: Kachelkaiser <kachelkaiser@htpst.de>
Co-authored-by: Aleksandre Ghvineria <Ghvinerias@gmail.com>
Co-authored-by: Fill read-only add-on <noreply-addon-fill@weblate.org>
Co-authored-by: solokot <solokot@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: PhillyMay <mein.alias@outlook.com>
Co-authored-by: Vitaliy Bilyk <vitaliy.bilyk@gmail.com>
Co-authored-by: SanctumSaturn <oraculogithub@gmail.com>
Co-authored-by: Saeid Sadat <9773heeytgege@gmail.com>
Co-authored-by: catelixor <catelixor+weblate@proton.me>
Co-authored-by: HalfMolar <weblate@reitsmas.nl>
Co-authored-by: apemay <aperezmayol@gmail.com>
Co-authored-by: Quang cai Boong <a0974284912@gmail.com>
Co-authored-by: jaime-grj <weblate.4ljj9@aleeas.com>
Co-authored-by: Zaparojine <passerby.feiz@yahoo.com>
Co-authored-by: ojiouigyutycghjbknljioui <weblate@victordrijkoningen.nl>
Co-authored-by: กวาง ครับ <mr.krisda2542@gmail.com>
Co-authored-by: Arif Budiman <arifpedia@gmail.com>
Co-authored-by: justcontributor <kty5663@gmail.com>
Co-authored-by: spreedawd <cosmos895@proton.me>
Co-authored-by: Zane Schepke <zanecschepke@gmail.com>
2026-03-07 12:32:13 -05:00
Henry Essinghigh 6369d8975c feat: support for filtering by endpoint latency (#1155) 2026-03-07 12:22:30 -05:00
zaneschepke 0c57bea2ff Merge branch 'master' of github.com:wgtunnel/wgtunnel 2026-03-07 12:02:45 -05:00
zaneschepke 5f8f699ab5 chore: bump deps 2026-03-07 12:02:31 -05:00
dependabot[bot] d0f58615b0 chore(deps): bump actions/upload-artifact from 6 to 7 (#1178)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-07 01:36:54 -05:00
dependabot[bot] 35982aa345 chore(deps): bump actions/download-artifact from 7 to 8 (#1179)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-07 01:36:43 -05:00
zaneschepke 0344b8fde8 fix: optional I2-5 params and socks5/lockdown crash on heavy load 2026-03-07 01:36:14 -05:00
zaneschepke bdd7c9689c fix: create from scratch bug, proxy mode UDP crash
Bump versions

closes #1149
closes #1141
2026-01-24 21:26:04 -05:00
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
Zane Schepke 394188b55f chore: release 4.2.0 2025-12-19 01:10:11 -05:00
Weblate (bot) d61a4213cf Translations update from Hosted Weblate (#1030)
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>
2025-12-18 22:05:31 -05:00
Zane Schepke 90a693abb4 feat: copy split config from existing 2025-12-18 16:28:44 -05:00
Patryk 05b950fbb3 fix: add check if kernel supports WG (#993) 2025-12-18 10:25:31 -05:00
Zane Schepke 5310cf17e6 chore: bump deps 2025-12-17 23:19:55 -05:00
dependabot[bot] 3ffc8c5e39 chore(deps): bump actions/upload-artifact from 5 to 6 (#1096)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-16 22:50:36 -05:00
dependabot[bot] e538ec80d0 chore(deps): bump actions/download-artifact from 6 to 7 (#1097)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-16 22:50:12 -05:00
Zane Schepke 6b24cca551 feat: improve amnezia label names
closes #1089
2025-12-15 12:36:50 -05:00
Zane Schepke f0c885708b fix: amnezia 2.0 header ranges 2025-12-14 22:30:54 -05:00
Zane Schepke c028b9b5b6 fix: logger start and clear 2025-12-09 10:45:34 -05:00
Zane Schepke de8b39d848 fix: amnezia 2.0 backwards compat, bump deps 2025-12-08 11:02:08 -05:00
543 changed files with 13625 additions and 3014 deletions
+1 -1
View File
@@ -122,7 +122,7 @@ jobs:
echo "path=$AAB_PATH" >> $GITHUB_OUTPUT
- name: Upload AAB Artifact
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v7
with:
name: google-play-aab
path: ${{ steps.aab-path.outputs.path }}
+1 -1
View File
@@ -115,7 +115,7 @@ jobs:
id: apk-path
run: echo "path=$(find . -regex '^.*/build/outputs/apk/${{ inputs.flavor }}/${{ inputs.build_type }}/.*\.apk$' -type f | head -1 | tail -c+2)" >> $GITHUB_OUTPUT
- name: Upload All APK Artifacts
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v7
with:
name: android_artifacts_${{ inputs.flavor }}
path: >-
+1 -1
View File
@@ -71,7 +71,7 @@ jobs:
run: mkdir ${{ github.workspace }}/temp
- name: Download artifacts
uses: actions/download-artifact@v6
uses: actions/download-artifact@v8
with:
pattern: android_artifacts_*
path: ${{ github.workspace }}/temp
+1 -1
View File
@@ -115,7 +115,7 @@ jobs:
run: mkdir ${{ github.workspace }}/temp
- name: Download artifacts
uses: actions/download-artifact@v6
uses: actions/download-artifact@v8
with:
pattern: android_artifacts_*
path: ${{ github.workspace }}/temp
+10 -6
View File
@@ -4,7 +4,6 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.hilt.android)
alias(libs.plugins.kotlinxSerialization)
alias(libs.plugins.ksp)
alias(libs.plugins.compose.compiler)
@@ -189,7 +188,6 @@ dependencies {
// Navigation
implementation(libs.bundles.androidx.navigation3)
implementation(libs.bundles.navigation.lifecycle)
implementation(libs.bundles.androidx.hilt)
// Material and icons
implementation(libs.bundles.google.material)
@@ -200,11 +198,7 @@ dependencies {
implementation(libs.bundles.androidx.datastore)
ksp(libs.androidx.room.compiler)
// DI and work
implementation(libs.bundles.hilt.android)
implementation(libs.bundles.androidx.work)
ksp(libs.hilt.android.compiler)
ksp(libs.androidx.hilt.compiler)
// Networking and serialization
implementation(libs.bundles.ktor.client)
@@ -250,6 +244,16 @@ dependencies {
implementation(libs.roomdatabasebackup) {
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") {
+10 -6
View File
@@ -103,12 +103,16 @@
android:resource="@xml/file_paths" />
</provider>
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:multiprocess="true"
tools:node="remove">
</provider>
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="androidx.work.WorkManagerInitializer"
android:value="androidx.startup"
tools:node="remove" />
</provider>
<service
android:name=".core.service.tile.TunnelControlTile"
android:exported="true"
@@ -11,13 +11,33 @@ import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
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.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
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.Modifier
import androidx.compose.ui.platform.LocalContext
@@ -26,23 +46,21 @@ import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextLinkStyles
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withLink
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
import androidx.navigation3.ui.NavDisplay
import com.zaneschepke.networkmonitor.NetworkMonitor
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.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
@@ -50,7 +68,6 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
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.dialog.VpnDeniedDialog
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.CustomSnackBar
@@ -93,27 +110,32 @@ import com.zaneschepke.wireguardautotunnel.ui.theme.AlertRed
import com.zaneschepke.wireguardautotunnel.ui.theme.OffWhite
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
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.SharedAppViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.SplitTunnelViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelViewModel
import dagger.hilt.android.AndroidEntryPoint
import de.raphaelebner.roomdatabasebackup.core.RoomBackup
import java.util.*
import javax.inject.Inject
import kotlinx.coroutines.flow.collectLatest
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
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit var appStateRepository: AppStateRepository
@Inject lateinit var tunnelRepository: TunnelRepository
@Inject lateinit var appDatabase: AppDatabase
@Inject lateinit var networkMonitor: NetworkMonitor
private val appStateRepository: AppStateRepository by inject()
private val tunnelRepository: TunnelRepository by inject()
private val appDatabase: AppDatabase by inject()
private val networkMonitor: NetworkMonitor by inject()
val viewModel by viewModel<SharedAppViewModel>()
private lateinit var roomBackup: RoomBackup
@OptIn(ExperimentalMaterial3Api::class)
@@ -129,8 +151,6 @@ class MainActivity : AppCompatActivity() {
roomBackup = RoomBackup(this)
val viewModel by viewModels<SharedAppViewModel>()
installSplashScreen().apply {
setKeepOnScreenCondition { !viewModel.container.stateFlow.value.isAppLoaded }
}
@@ -164,10 +184,12 @@ class MainActivity : AppCompatActivity() {
var previousRoute by remember { mutableStateOf<Route?>(null) }
val navController =
rememberNavController<NavKey>(backStack, uiState.isLocationDisclosureShown) {
previousKey ->
previousRoute = previousKey as? Route
}
rememberNavController(
backStack,
uiState.isLocationDisclosureShown,
onChange = { previousKey -> previousRoute = previousKey as? Route },
onExitApp = { finish() },
)
val vpnActivity =
rememberLauncherForActivityResult(
@@ -233,7 +255,6 @@ class MainActivity : AppCompatActivity() {
CompositionLocalProvider(
LocalIsAndroidTV provides isTv,
LocalSharedVm provides viewModel,
LocalNavController provides navController,
) {
WireguardAutoTunnelTheme(theme = uiState.theme) {
@@ -280,9 +301,7 @@ class MainActivity : AppCompatActivity() {
}
LaunchedEffect(Unit) {
if (
uiState.shouldShowDonationSnackbar && !uiState.settings.alreadyDonated
) {
if (uiState.shouldShowDonationSnackbar && !uiState.alreadyDonated) {
viewModel.setShouldShowDonationSnackbar(false)
snackbarState.showSnackbar(
SnackbarInfo(
@@ -313,10 +332,10 @@ class MainActivity : AppCompatActivity() {
)
Box(modifier = Modifier.fillMaxSize()) {
if (uiState.settings.appMode == AppMode.LOCK_DOWN) {
if (uiState.appMode == AppMode.LOCK_DOWN) {
AppAlertBanner(
stringResource(R.string.locked_down)
.uppercase(Locale.getDefault()),
.uppercase(Locale.current.platformLocale),
OffWhite,
AlertRed,
modifier = Modifier.fillMaxWidth().zIndex(2f),
@@ -413,38 +432,23 @@ class MainActivity : AppCompatActivity() {
entry<Route.Tunnels> { TunnelsScreen() }
entry<Route.Sort> { SortScreen() }
entry<Route.TunnelSettings> { key ->
val viewModel =
hiltViewModel<
TunnelViewModel,
TunnelViewModel.Factory,
>(
creationCallback = { factory ->
factory.create(key.id)
}
val viewModel: TunnelViewModel =
koinViewModel(
parameters = { parametersOf(key.id) }
)
TunnelSettingsScreen(viewModel)
}
entry<Route.SplitTunnel> { key ->
val viewModel =
hiltViewModel<
SplitTunnelViewModel,
SplitTunnelViewModel.Factory,
>(
creationCallback = { factory ->
factory.create(key.id)
}
val viewModel: SplitTunnelViewModel =
koinViewModel(
parameters = { parametersOf(key.id) }
)
SplitTunnelScreen(viewModel)
}
entry<Route.Config> { key ->
val viewModel =
hiltViewModel<
ConfigViewModel,
ConfigViewModel.Factory,
>(
creationCallback = { factory ->
factory.create(key.id)
}
val viewModel: ConfigViewModel =
koinViewModel(
parameters = { parametersOf(key.id) }
)
ConfigScreen(viewModel)
}
@@ -470,26 +474,16 @@ class MainActivity : AppCompatActivity() {
}
entry<Route.Dns> { DnsSettingsScreen() }
entry<Route.ConfigGlobal> { key ->
val viewModel =
hiltViewModel<
ConfigViewModel,
ConfigViewModel.Factory,
>(
creationCallback = { factory ->
factory.create(key.id)
}
val viewModel: ConfigViewModel =
koinViewModel(
parameters = { parametersOf(key.id) }
)
ConfigScreen(viewModel)
}
entry<Route.SplitTunnelGlobal> { key ->
val viewModel =
hiltViewModel<
SplitTunnelViewModel,
SplitTunnelViewModel.Factory,
>(
creationCallback = { factory ->
factory.create(key.id)
}
val viewModel: SplitTunnelViewModel =
koinViewModel(
parameters = { parametersOf(key.id) }
)
SplitTunnelScreen(viewModel)
}
@@ -2,74 +2,87 @@ package com.zaneschepke.wireguardautotunnel
import android.app.Application
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.wireguardautotunnel.core.notification.NotificationMonitor
import com.zaneschepke.wireguardautotunnel.core.worker.ServiceWorker
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.di.Dispatcher
import com.zaneschepke.wireguardautotunnel.di.Scope
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.util.ReleaseTree
import dagger.hilt.android.HiltAndroidApp
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.update
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
@HiltAndroidApp
class WireGuardAutoTunnel : Application(), Configuration.Provider {
class WireGuardAutoTunnel : Application(), KoinComponent {
@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
get() = Configuration.Builder().setWorkerFactory(workerFactory).build()
@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
private val monitoringRepository: MonitoringSettingsRepository by inject()
private val notificationMonitor: NotificationMonitor by inject()
override fun onCreate() {
super.onCreate()
startKoin {
androidContext(this@WireGuardAutoTunnel)
if (BuildConfig.DEBUG) androidLogger()
workManagerFactory()
modules(dispatchersModule, appModule, databaseModule, tunnelModule, workerModule)
options(viewModelScopeFactory())
lazyModules(networkModule)
}
instance = this
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
StrictMode.setThreadPolicy(
ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()
StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyLog()
.penaltyFlashScreen()
.build()
)
StrictMode.setVmPolicy(StrictMode.VmPolicy.Builder().detectAll().penaltyLog().build())
} else {
Timber.plant(ReleaseTree())
}
applicationScope.launch(ioDispatcher) {
launch {
val monitoringSettings = monitoringRepository.getMonitoringSettings()
if (monitoringSettings.isLocalLogsEnabled) logReader.start()
monitoringRepository.flow
.distinctUntilChangedBy { it.isLocalLogsEnabled }
.collect { settings ->
if (settings.isLocalLogsEnabled) {
logReader.start()
} else {
logReader.stop()
}
}
}
launch { notificationMonitor.handleApplicationNotifications() }
}
ServiceWorker.start(this)
}
companion object {
private val _uiActive = MutableStateFlow(false)
val uiActive: StateFlow<Boolean>
@@ -4,21 +4,19 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
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 dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
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() {
class KernelReceiver : BroadcastReceiver(), KoinComponent {
@Inject @ApplicationScope lateinit var applicationScope: CoroutineScope
@Inject lateinit var tunnelRepository: TunnelRepository
@Inject lateinit var tunnelManager: TunnelManager
private val applicationScope: CoroutineScope by inject(named(Scope.APPLICATION))
private val tunnelRepository: TunnelRepository by inject()
private val tunnelManager: TunnelManager by inject()
override fun onReceive(context: Context, intent: Intent) {
val action = intent.action ?: return
@@ -5,25 +5,21 @@ import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
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.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.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() {
class NotificationActionReceiver : BroadcastReceiver(), KoinComponent {
@Inject lateinit var tunnelManager: TunnelManager
@Inject lateinit var tunnelRepository: TunnelRepository
@Inject lateinit var autoTunnelRepository: AutoTunnelSettingsRepository
@Inject @ApplicationScope lateinit var applicationScope: CoroutineScope
private val tunnelManager: TunnelManager by inject()
private val autoTunnelRepository: AutoTunnelSettingsRepository by inject()
private val applicationScope: CoroutineScope = get(named(Scope.APPLICATION))
override fun onReceive(context: Context, intent: Intent) {
applicationScope.launch {
@@ -4,27 +4,25 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
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.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.util.Constants
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
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
@AndroidEntryPoint
class RemoteControlReceiver : BroadcastReceiver() {
class RemoteControlReceiver : BroadcastReceiver(), KoinComponent {
@Inject @ApplicationScope lateinit var applicationScope: CoroutineScope
@Inject lateinit var settingsRepository: GeneralSettingRepository
@Inject lateinit var tunnelsRepository: TunnelRepository
@Inject lateinit var autoTunnelSettingsRepository: AutoTunnelSettingsRepository
@Inject lateinit var tunnelManager: TunnelManager
private val applicationScope: CoroutineScope by inject(named(Scope.APPLICATION))
private val settingsRepository: GeneralSettingRepository by inject()
private val tunnelsRepository: TunnelRepository by inject()
private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository by inject()
private val tunnelManager: TunnelManager by inject()
enum class Action(private val suffix: String) {
START_TUNNEL("START_TUNNEL"),
@@ -5,24 +5,25 @@ import android.content.Context
import android.content.Intent
import com.zaneschepke.logcatter.LogReader
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 dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
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
@AndroidEntryPoint
class RestartReceiver : BroadcastReceiver() {
class RestartReceiver : BroadcastReceiver(), KoinComponent {
@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) {
Timber.d("RestartReceiver triggered with action: ${intent.action}")
@@ -14,6 +14,7 @@ interface NotificationManager {
fun createNotification(
channel: NotificationChannels,
title: String = "",
subText: String? = null,
actions: Collection<NotificationCompat.Action> = emptyList(),
description: String = "",
showTimestamp: Boolean = true,
@@ -27,6 +28,7 @@ interface NotificationManager {
fun createNotification(
channel: NotificationChannels,
title: StringValue,
subText: String? = null,
actions: Collection<NotificationCompat.Action> = emptyList(),
description: StringValue,
showTimestamp: Boolean = true,
@@ -4,14 +4,11 @@ import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.util.StringValue
import jakarta.inject.Inject
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
class NotificationMonitor
@Inject
constructor(
class NotificationMonitor(
private val tunnelManager: TunnelManager,
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.domain.enums.NotificationAction
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) :
NotificationManager {
class WireGuardNotification(override val context: Context) : NotificationManager {
enum class NotificationChannels {
VPN,
@@ -32,6 +29,7 @@ class WireGuardNotification @Inject constructor(@ApplicationContext override val
override fun createNotification(
channel: NotificationChannels,
title: String,
subText: String?,
actions: Collection<NotificationCompat.Action>,
description: String,
showTimestamp: Boolean,
@@ -47,6 +45,7 @@ class WireGuardNotification @Inject constructor(@ApplicationContext override val
.apply {
actions.forEach { addAction(it) }
setContentTitle(title)
setSubText(subText)
setContentIntent(
PendingIntent.getActivity(
context,
@@ -75,6 +74,7 @@ class WireGuardNotification @Inject constructor(@ApplicationContext override val
override fun createNotification(
channel: NotificationChannels,
title: StringValue,
subText: String?,
actions: Collection<NotificationCompat.Action>,
description: StringValue,
showTimestamp: Boolean,
@@ -87,6 +87,7 @@ class WireGuardNotification @Inject constructor(@ApplicationContext override val
return createNotification(
channel,
title.asString(context),
subText,
actions,
description.asString(context),
showTimestamp,
@@ -112,7 +113,7 @@ class WireGuardNotification @Inject constructor(@ApplicationContext override val
)
return NotificationCompat.Action.Builder(
R.drawable.ic_notification,
notificationAction.title(context).uppercase(),
notificationAction.title(context),
pendingIntent,
)
.build()
@@ -144,6 +145,7 @@ class WireGuardNotification @Inject constructor(@ApplicationContext override val
context.getString(R.string.auto_tunnel_channel_id),
)
}
NotificationChannels.VPN -> {
NotificationCompat.Builder(context, context.getString(R.string.vpn_channel_id))
}
@@ -160,6 +162,7 @@ class WireGuardNotification @Inject constructor(@ApplicationContext override val
)
.apply { description = context.getString(R.string.vpn_channel_description) }
}
NotificationChannels.AUTO_TUNNEL -> {
NotificationChannel(
context.getString(R.string.auto_tunnel_channel_id),
@@ -3,6 +3,7 @@ package com.zaneschepke.wireguardautotunnel.core.service
import android.app.Notification
import android.content.Intent
import android.os.IBinder
import android.text.format.Formatter
import androidx.core.app.ServiceCompat
import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope
@@ -10,38 +11,36 @@ import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelMonitor
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.di.Dispatcher
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
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.util.extensions.distinctByKeys
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import org.koin.core.qualifier.named
import timber.log.Timber
@AndroidEntryPoint
abstract class BaseTunnelForegroundService : LifecycleService(), TunnelService {
@Inject lateinit var notificationManager: NotificationManager
@Inject lateinit var serviceManager: ServiceManager
@Inject lateinit var tunnelManager: TunnelManager
@Inject lateinit var tunnelMonitor: TunnelMonitor
@Inject @IoDispatcher lateinit var ioDispatcher: CoroutineDispatcher
@Inject lateinit var settingsRepository: GeneralSettingRepository
@Inject lateinit var tunnelsRepository: TunnelRepository
private val notificationManager: NotificationManager by inject()
private val serviceManager: ServiceManager by inject()
private val tunnelManager: TunnelManager by inject()
private val ioDispatcher: CoroutineDispatcher by inject(named(Dispatcher.IO))
private val settingsRepository: GeneralSettingRepository by inject()
private val tunnelsRepository: TunnelRepository by inject()
protected abstract val fgsType: Int
private var currentSingleTunnelId: Int? = null
private var statsJob: Job? = null
override fun onBind(intent: Intent): IBinder {
super.onBind(intent)
return LocalBinder(this)
@@ -59,12 +58,14 @@ abstract class BaseTunnelForegroundService : LifecycleService(), TunnelService {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
ServiceCompat.startForeground(
this,
NotificationManager.VPN_NOTIFICATION_ID,
onCreateNotification(),
fgsType,
)
if (
intent == null ||
intent.component == null ||
@@ -83,28 +84,68 @@ abstract class BaseTunnelForegroundService : LifecycleService(), TunnelService {
} else {
start()
}
return START_STICKY
}
override fun start() {
lifecycleScope.launch(ioDispatcher) {
tunnelManager.activeTunnels.distinctByKeys().collect { activeTunnels ->
val activeTunConfigs = activeTunnels.keys
val activeTunIds = activeTunnels.keys
val tunnels = tunnelsRepository.getAll()
val activeConfigs = tunnels.filter { activeTunConfigs.contains(it.id) }
val activeConfigs = tunnels.filter { activeTunIds.contains(it.id) }
updateServiceNotification(activeConfigs)
restartStatsUpdaterIfNeeded(activeConfigs)
}
}
}
// TODO Would be cool to have this include kill switch
private fun restartStatsUpdaterIfNeeded(activeConfigs: List<TunnelConfig>) {
val single = activeConfigs.singleOrNull()
if (single == null) {
statsJob?.cancel()
statsJob = null
currentSingleTunnelId = null
return
}
if (currentSingleTunnelId == single.id && statsJob?.isActive == true) return
statsJob?.cancel()
statsJob = null
currentSingleTunnelId = single.id
statsJob =
lifecycleScope.launch(ioDispatcher) {
while (isActive) {
val traffic = readTraffic(single.id)
notificationManager.show(
NotificationManager.VPN_NOTIFICATION_ID,
createTunnelNotification(single, consumedTraffic = traffic),
)
delay(1000)
}
}
}
private fun readTraffic(tunnelId: Int): Pair<Long, Long>? {
val active = tunnelManager.activeTunnels.value[tunnelId] ?: return null
val stats = active.statistics ?: return null
return stats.rx() to stats.tx()
}
private fun updateServiceNotification(activeConfigs: List<TunnelConfig>) {
val notification =
when (activeConfigs.size) {
0 -> onCreateNotification()
1 -> createTunnelNotification(activeConfigs.first())
1 -> createTunnelNotification(activeConfigs.first(), consumedTraffic = null)
else -> createTunnelsNotification()
}
ServiceCompat.startForeground(
this,
NotificationManager.VPN_NOTIFICATION_ID,
@@ -115,21 +156,43 @@ abstract class BaseTunnelForegroundService : LifecycleService(), TunnelService {
override fun stop() {
Timber.d("Stop called")
statsJob?.cancel()
statsJob = null
currentSingleTunnelId = null
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
stopSelf()
}
override fun onDestroy() {
serviceManager.handleTunnelServiceDestroy()
statsJob?.cancel()
statsJob = null
currentSingleTunnelId = null
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
Timber.d("onDestroy")
super.onDestroy()
}
private fun createTunnelNotification(tunnelConfig: TunnelConfig): Notification {
private fun createTunnelNotification(
tunnelConfig: TunnelConfig,
consumedTraffic: Pair<Long, Long>?,
): Notification {
val subText =
consumedTraffic?.let { traffic ->
val formattedRx = "${formatBytes(traffic.first)}"
val formattedTx = "${formatBytes(traffic.second)}"
"$formattedRx $formattedTx"
}
return notificationManager.createNotification(
WireGuardNotification.NotificationChannels.VPN,
title = "${getString(R.string.tunnel_running)} - ${tunnelConfig.name}",
title = tunnelConfig.name,
description = getString(R.string.tunnel_running),
subText = subText,
actions =
listOf(
notificationManager.createNotificationAction(
@@ -164,4 +227,6 @@ abstract class BaseTunnelForegroundService : LifecycleService(), TunnelService {
isGroupSummary = true,
)
}
private fun formatBytes(bytes: Long) = Formatter.formatFileSize(this, bytes)
}
@@ -8,15 +8,20 @@ import android.net.VpnService
import android.os.IBinder
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelService
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.util.extensions.requestAutoTunnelTileServiceUpdate
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
import jakarta.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
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.sync.Mutex
import kotlinx.coroutines.sync.withLock
@@ -24,12 +29,10 @@ import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import timber.log.Timber
class ServiceManager
@Inject
constructor(
class ServiceManager(
private val context: Context,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
@ApplicationScope applicationScope: CoroutineScope,
ioDispatcher: CoroutineDispatcher,
applicationScope: CoroutineScope,
private val mainDispatcher: CoroutineDispatcher,
private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
) {
@@ -1,8 +1,6 @@
package com.zaneschepke.wireguardautotunnel.core.service
import com.zaneschepke.wireguardautotunnel.util.Constants
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class TunnelForegroundService(override val fgsType: Int = Constants.SPECIAL_USE_SERVICE_TYPE_ID) :
BaseTunnelForegroundService()
@@ -1,8 +1,6 @@
package com.zaneschepke.wireguardautotunnel.core.service
import com.zaneschepke.wireguardautotunnel.util.Constants
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class VpnForegroundService(override val fgsType: Int = Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID) :
BaseTunnelForegroundService()
@@ -15,7 +15,7 @@ import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotificati
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
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.events.AutoTunnelEvent
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.extensions.to
import com.zaneschepke.wireguardautotunnel.util.extensions.toMillis
import dagger.hilt.android.AndroidEntryPoint
import java.lang.ref.WeakReference
import javax.inject.Inject
import javax.inject.Provider
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
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.withLock
import org.koin.android.ext.android.inject
import org.koin.core.qualifier.named
import timber.log.Timber
@AndroidEntryPoint
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>
@Inject lateinit var settingsRepository: GeneralSettingRepository
@Inject lateinit var tunnelsRepository: TunnelRepository
private val autoTunnelRepository: AutoTunnelSettingsRepository by inject()
private val settingsRepository: GeneralSettingRepository by inject()
private val tunnelsRepository: TunnelRepository by inject()
private val defaultState = AutoTunnelState()
@@ -235,7 +248,7 @@ class AutoTunnelService : LifecycleService() {
private fun combineSettings(): Flow<Triple<AppMode, AutoTunnelSettings, List<TunnelConfig>>> {
return combine(
settingsRepository.flow.map { it.appMode }.distinctUntilChanged(),
autoTunnelRepository.get().flow,
autoTunnelRepository.flow,
tunnelsRepository.userTunnelsFlow.map { tunnels ->
// isActive is ignored for equality checks so user can manually toggle off
// tunnel with auto-tunnel
@@ -352,7 +365,10 @@ class AutoTunnelService : LifecycleService() {
) {
is AutoTunnelEvent.Start ->
(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()
AutoTunnelEvent.DoNothing -> Timber.i("Auto-tunneling: nothing to do")
@@ -363,9 +379,7 @@ class AutoTunnelService : LifecycleService() {
// restart network flow on debounce changes
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
private val debouncedConnectivityStateFlow: Flow<ConnectivityState> by lazy {
autoTunnelRepository
.get()
.flow
autoTunnelRepository.flow
.map { it.debounceDelaySeconds.toMillis() }
.distinctUntilChanged()
.flatMapLatest { debounceMillis ->
@@ -7,19 +7,17 @@ import android.service.quicksettings.TileService
import androidx.lifecycle.*
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
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.ExperimentalAtomicApi
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import timber.log.Timber
@AndroidEntryPoint
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)
@@ -5,30 +5,32 @@ import android.os.Build
import android.os.IBinder
import android.service.quicksettings.Tile
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.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
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.ExperimentalAtomicApi
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.koin.android.ext.android.inject
import timber.log.Timber
@AndroidEntryPoint
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)
@@ -6,13 +6,12 @@ import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
class DynamicShortcutManager(
private val context: Context,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
private val ioDispatcher: CoroutineDispatcher,
) : ShortcutManager {
override suspend fun addShortcuts() {
withContext(ioDispatcher) {
@@ -5,26 +5,23 @@ import androidx.activity.ComponentActivity
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelService
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
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.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import org.koin.core.qualifier.named
import timber.log.Timber
@AndroidEntryPoint
class ShortcutsActivity : ComponentActivity() {
@Inject lateinit var settingsRepository: GeneralSettingRepository
@Inject lateinit var autoTunnelSettingsRepository: AutoTunnelSettingsRepository
@Inject lateinit var tunnelsRepository: TunnelRepository
@Inject lateinit var tunnelManager: TunnelManager
@Inject @ApplicationScope lateinit var applicationScope: CoroutineScope
private val settingsRepository: GeneralSettingRepository by inject()
private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository by inject()
private val tunnelsRepository: TunnelRepository by inject()
private val tunnelManager: TunnelManager by inject()
private val applicationScope: CoroutineScope by inject(named(Scope.APPLICATION))
override fun onCreate(savedInstanceState: Bundle?) {
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
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.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.di.*
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
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.GeneralSettingRepository
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.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 javax.inject.Inject
import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils
import kotlin.concurrent.atomics.AtomicBoolean
import kotlin.concurrent.atomics.AtomicReference
import kotlin.concurrent.atomics.ExperimentalAtomicApi
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
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
@OptIn(ExperimentalCoroutinesApi::class, ExperimentalAtomicApi::class)
class TunnelManager
@Inject
constructor(
@Kernel private val kernelTunnel: TunnelProvider,
@Userspace private val userspaceTunnel: TunnelProvider,
@ProxyUserspace private val proxyUserspaceTunnel: TunnelProvider,
class TunnelManager(
kernelBackend: TunnelBackend,
userspaceBackend: TunnelBackend,
proxyUserspaceBackend: TunnelBackend,
networkMonitor: NetworkMonitor,
networkUtils: NetworkUtils,
powerManager: PowerManager,
logReader: LogReader,
monitoringSettingsRepository: MonitoringSettingsRepository,
private val serviceManager: ServiceManager,
private val settingsRepository: GeneralSettingRepository,
private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
private val lockdownSettingsRepository: LockdownSettingsRepository,
private val tunnelsRepository: TunnelRepository,
private val tunnelMonitor: TunnelMonitor,
@ApplicationScope private val applicationScope: CoroutineScope,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
private val applicationScope: CoroutineScope,
private val ioDispatcher: CoroutineDispatcher,
) : TunnelProvider {
@OptIn(ExperimentalCoroutinesApi::class)
private val localErrorEvents = MutableSharedFlow<Pair<String?, BackendCoreException>>()
private val _activeTunnels = MutableStateFlow<Map<Int, TunnelState>>(emptyMap())
override val activeTunnels: StateFlow<Map<Int, TunnelState>> = _activeTunnels.asStateFlow()
@OptIn(ExperimentalCoroutinesApi::class)
private val localMessageEvents = MutableSharedFlow<Pair<String?, BackendMessage>>()
@OptIn(ExperimentalAtomicApi::class) val currentAppMode = AtomicReference(AppMode.VPN)
private val monitoringMutex = Mutex()
private val monitoringJobs = ConcurrentHashMap<Int, Job>()
private val defaultManager =
TunnelLifecycleManager(userspaceBackend, applicationScope, ioDispatcher, _activeTunnels)
private val ddnsMutex = Mutex()
private val ddnsJobs = ConcurrentHashMap<Int, Job>()
private val lifecycleManagers: Map<AppMode, TunnelLifecycleManager> =
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(
val activeTuns: Map<Int, TunnelState>,
val tuns: List<TunnelConfig>,
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,
)
@OptIn(ExperimentalAtomicApi::class)
private fun getProvider(): TunnelProvider {
return lifecycleManagers[currentAppMode.load()] ?: defaultManager
}
override val activeTunnels: StateFlow<Map<Int, TunnelState>> = run {
val activeTunsReference: AtomicReference<Map<Int, TunnelState>> =
AtomicReference(emptyMap())
override suspend fun startTunnel(tunnelConfig: TunnelConfig): Result<Unit> =
getProvider().startTunnel(tunnelConfig)
tunnelProviderFlow
.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)
override suspend fun stopTunnel(tunnelId: Int) = getProvider().stopTunnel(tunnelId)
applicationScope.launch(ioDispatcher) {
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
},
),
)
override suspend fun forceStopTunnel(tunnelId: Int) = getProvider().forceStopTunnel(tunnelId)
sideEffects
.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(),
)
}
override suspend fun stopActiveTunnels() = getProvider().stopActiveTunnels()
@OptIn(ExperimentalCoroutinesApi::class)
override val errorEvents: SharedFlow<Pair<String?, BackendCoreException>> =
merge(localErrorEvents, tunnelProviderFlow.flatMapLatest { it.errorEvents })
.shareIn(
scope = applicationScope + ioDispatcher,
started = SharingStarted.Eagerly,
replay = 0,
)
override fun setBackendMode(backendMode: BackendMode) =
getProvider().setBackendMode(backendMode)
@OptIn(ExperimentalCoroutinesApi::class)
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 getBackendMode(): BackendMode = getProvider().getBackendMode()
override fun getStatistics(tunnelId: Int): TunnelStatistics? {
return tunnelProviderFlow.value.getStatistics(tunnelId)
}
override suspend fun runningTunnelNames(): Set<String> = getProvider().runningTunnelNames()
override suspend fun startTunnel(tunnelConfig: TunnelConfig) {
if (activeTunnels.value.containsKey(tunnelConfig.id)) return
val provider = tunnelProviderFlow.value
val isKernel = provider is KernelTunnel
override fun handleDnsReresolve(tunnelConfig: TunnelConfig): Boolean =
getProvider().handleDnsReresolve(tunnelConfig)
if (!isKernel && activeTunnels.value.isNotEmpty()) {
stopActiveTunnels()
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 fun getStatistics(tunnelId: Int): TunnelStatistics? =
getProvider().getStatistics(tunnelId)
override suspend fun updateTunnelStatus(
tunnelId: Int,
@@ -265,24 +139,99 @@ constructor(
stats: TunnelStatistics?,
pingStates: Map<String, PingState>?,
logHealthState: LogHealthState?,
) {
tunnelProviderFlow.value.updateTunnelStatus(
tunnelId,
status,
stats,
pingStates,
logHealthState,
)
}
) = getProvider().updateTunnelStatus(tunnelId, status, stats, pingStates, logHealthState)
private suspend fun handleTunnelServiceChange(
appMode: AppMode,
activeTuns: Map<Int, TunnelState>,
) {
if (activeTuns.isEmpty()) serviceManager.stopTunnelService()
if (activeTuns.isNotEmpty() && serviceManager.tunnelService.value == null)
serviceManager.startTunnelService(appMode)
serviceManager.updateTunnelTile()
@OptIn(ExperimentalCoroutinesApi::class)
private val localErrorEvents = MutableSharedFlow<Pair<String?, BackendCoreException>>()
@OptIn(ExperimentalCoroutinesApi::class)
private val localMessageEvents = MutableSharedFlow<Pair<String?, BackendMessage>>()
override val errorEvents: SharedFlow<Pair<String?, BackendCoreException>> =
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
@@ -293,7 +242,7 @@ constructor(
if (lockdownSettings.bypassLan) TunnelConfig.IPV4_PUBLIC_NETWORKS else emptySet()
try {
if (serviceManager.hasVpnPermission()) {
proxyUserspaceTunnel.setBackendMode(
setBackendMode(
BackendMode.KillSwitch(
allowedIps,
lockdownSettings.metered,
@@ -308,28 +257,25 @@ constructor(
}
}
private suspend fun handleModeChangeCleanup(
previousBackend: TunnelProvider,
previousAppMode: AppMode,
) {
previousBackend.stopActiveTunnels()
// stop lockdown if we switch from that mode
if (previousAppMode == AppMode.LOCK_DOWN)
proxyUserspaceTunnel.setBackendMode(BackendMode.Inactive)
private suspend fun handleModeChangeCleanup(previousAppMode: AppMode) {
lifecycleManagers[previousAppMode]?.stopActiveTunnels()
if (previousAppMode == AppMode.LOCK_DOWN) {
lifecycleManagers[previousAppMode]?.setBackendMode(BackendMode.Inactive)
}
}
suspend fun handleRestore() =
suspend fun handleRestore(settings: GeneralSettings? = null) =
withContext(ioDispatcher) {
val settings = settingsRepository.getGeneralSettings()
val currentSettings = settings ?: settingsRepository.getGeneralSettings()
val autoTunnelSettings = autoTunnelSettingsRepository.getAutoTunnelSettings()
val tunnels = tunnelsRepository.userTunnelsFlow.firstOrNull()
if (autoTunnelSettings.isAutoTunnelEnabled)
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 (settings.appMode == AppMode.VPN && !serviceManager.hasVpnPermission())
if (currentSettings.appMode == AppMode.VPN && !serviceManager.hasVpnPermission())
return@withContext localErrorEvents.emit(null to NotAuthorized())
when (settings.appMode) {
when (currentSettings.appMode) {
AppMode.VPN,
AppMode.PROXY,
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) =
withContext(ioDispatcher) {
val activeIds = activeTunnels.value.keys.toList()
@@ -437,122 +353,7 @@ constructor(
.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 {
const val BASE_BACKOFF = 30_000L
const val MAX_BACKOFF_TIME = 300_000L
const val RESTART_TUNNEL_DELAY = 300L
}
}
@@ -13,19 +13,12 @@ import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
interface TunnelProvider {
/** Starts the specified tunnel configuration. */
suspend fun startTunnel(tunnelConfig: TunnelConfig)
suspend fun startTunnel(tunnelConfig: TunnelConfig): Result<Unit>
/**
* Stops the specified tunnel.
*
* @param tunnelId The tunnelConf to stop.
*/
suspend fun stopTunnel(tunnelId: Int)
suspend fun forceStopTunnel(tunnelId: Int)
/** Stops all active tunnels. */
suspend fun stopActiveTunnels()
fun setBackendMode(backendMode: BackendMode)
@@ -39,9 +32,7 @@ interface TunnelProvider {
fun getStatistics(tunnelId: Int): TunnelStatistics?
val activeTunnels: StateFlow<Map<Int, TunnelState>>
val errorEvents: SharedFlow<Pair<String?, BackendCoreException>>
val messageEvents: SharedFlow<Pair<String?, BackendMessage>>
suspend fun updateTunnelStatus(
@@ -1,17 +1,16 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
package com.zaneschepke.wireguardautotunnel.core.tunnel.backend
import com.wireguard.android.backend.Backend
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.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.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.events.DnsFailure
import com.zaneschepke.wireguardautotunnel.domain.events.InvalidConfig
import com.zaneschepke.wireguardautotunnel.domain.events.KernelTunnelName
import com.zaneschepke.wireguardautotunnel.domain.events.KernelWireguardNotSupported
import com.zaneschepke.wireguardautotunnel.domain.events.UnknownError
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
@@ -20,26 +19,19 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendCoreException
import java.util.concurrent.ConcurrentHashMap
import java.util.regex.Pattern
import javax.inject.Inject
import kotlinx.coroutines.*
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
class KernelTunnel
@Inject
constructor(
@ApplicationScope applicationScope: CoroutineScope,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
private val runConfigHelper: RunConfigHelper,
@Kernel private val backend: Backend,
) : BaseTunnel(applicationScope, ioDispatcher) {
class KernelTunnel(private val runConfigHelper: RunConfigHelper, private val backend: Backend) :
TunnelBackend {
private val runtimeTunnels = ConcurrentHashMap<Int, WgTunnel>()
private val runtimeTunnels = ConcurrentHashMap<Int, Tunnel>()
private fun validateWireGuardInterfaceName(name: String): Result<Unit> {
if (name.isEmpty() || name.length > 15)
@@ -55,9 +47,10 @@ constructor(
}
override fun tunnelStateFlow(tunnelConfig: TunnelConfig): Flow<TunnelStatus> = callbackFlow {
validateWireGuardInterfaceName(tunnelConfig.name).onFailure { close(it) }
if (!WgQuickBackend.hasKernelSupport()) throw KernelWireguardNotSupported()
validateWireGuardInterfaceName(tunnelConfig.name).onFailure { throw it }
val stateChannel = Channel<WgTunnel.State>()
val stateChannel = Channel<Tunnel.State>()
val runtimeTunnel = RuntimeWgTunnel(tunnelConfig, stateChannel)
runtimeTunnels[tunnelConfig.id] = runtimeTunnel
@@ -67,37 +60,31 @@ constructor(
}
try {
withTimeout(STARTUP_TIMEOUT_MS) {
updateTunnelStatus(tunnelConfig.id, TunnelStatus.Starting)
val runConfig = runConfigHelper.buildWgRunConfig(tunnelConfig)
backend.setState(runtimeTunnel, WgTunnel.State.UP, runConfig)
}
val runConfig = runConfigHelper.buildWgRunConfig(tunnelConfig)
backend.setState(runtimeTunnel, Tunnel.State.UP, runConfig)
} catch (e: TimeoutCancellationException) {
Timber.e("Startup timed out for ${tunnelConfig.name}")
errors.emit(tunnelConfig.name to DnsFailure())
forceStopTunnel(tunnelConfig.id)
close()
Timber.Forest.e("Startup timed out for ${tunnelConfig.name}")
throw DnsFailure()
} catch (e: BackendException) {
close(e.toBackendCoreException())
throw e.toBackendCoreException()
} catch (e: IllegalArgumentException) {
Timber.e(e, "Invalid backend arguments")
close(InvalidConfig())
Timber.Forest.e(e, "Invalid backend arguments")
throw InvalidConfig()
} catch (e: Exception) {
Timber.e(e, "Error while setting tunnel state")
close(UnknownError())
Timber.Forest.e(e, "Error while setting tunnel state")
throw UnknownError()
}
awaitClose {
try {
backend.setState(runtimeTunnel, WgTunnel.State.DOWN, null)
backend.setState(runtimeTunnel, Tunnel.State.DOWN, null)
} catch (e: BackendException) {
errors.tryEmit(tunnelConfig.name to e.toBackendCoreException())
// Errors are emitted by caller (lifecycle manager)
} finally {
consumerJob.cancel()
stateChannel.close()
runtimeTunnels.remove(tunnelConfig.id)
trySend(TunnelStatus.Down)
close()
}
}
}
@@ -107,13 +94,13 @@ constructor(
val runtimeTunnel = runtimeTunnels[tunnelId] ?: return null
WireGuardStatistics(backend.getStatistics(runtimeTunnel))
} catch (e: Exception) {
Timber.e(e, "Failed to get stats for $tunnelId")
Timber.Forest.e(e, "Failed to get stats for $tunnelId")
null
}
}
override fun setBackendMode(backendMode: BackendMode) {
Timber.w("Not yet implemented for kernel")
Timber.Forest.w("Not yet implemented for kernel")
}
override fun getBackendMode(): BackendMode {
@@ -131,15 +118,11 @@ constructor(
override suspend fun forceStopTunnel(tunnelId: Int) {
val runtimeTunnel = runtimeTunnels[tunnelId] ?: return
try {
backend.setState(runtimeTunnel, WgTunnel.State.DOWN, null)
backend.setState(runtimeTunnel, Tunnel.State.DOWN, null)
} catch (e: BackendException) {
Timber.e(e, "Force stop failed for $tunnelId")
Timber.Forest.e(e, "Force stop failed for $tunnelId")
} finally {
tunJobs[tunnelId]?.cancel()
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.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.ProxySettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import java.util.*
import javax.inject.Inject
import java.util.Optional
import kotlinx.coroutines.flow.firstOrNull
import org.amnezia.awg.config.Config
import org.amnezia.awg.config.proxy.HttpProxy
import org.amnezia.awg.config.proxy.Socks5Proxy
class RunConfigHelper
@Inject
constructor(
class RunConfigHelper(
private val settingsRepository: GeneralSettingRepository,
private val proxySettingsRepository: ProxySettingsRepository,
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 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.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.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.state.AmneziaStatistics
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 java.io.IOException
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
import kotlinx.coroutines.*
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
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.BackendException
import org.amnezia.awg.backend.Tunnel as AwgTunnel
import org.amnezia.awg.backend.Tunnel
import timber.log.Timber
class UserspaceTunnel
@Inject
constructor(
@ApplicationScope applicationScope: CoroutineScope,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
private val backend: Backend,
private val runConfigHelper: RunConfigHelper,
) : BaseTunnel(applicationScope, ioDispatcher) {
class UserspaceTunnel(private val backend: Backend, private val runConfigHelper: RunConfigHelper) :
TunnelBackend {
private val runtimeTunnels = ConcurrentHashMap<Int, AwgTunnel>()
private val runtimeTunnels = ConcurrentHashMap<Int, Tunnel>()
override fun tunnelStateFlow(tunnelConfig: TunnelConfig): Flow<TunnelStatus> = callbackFlow {
val stateChannel = Channel<AwgTunnel.State>()
val stateChannel = Channel<Tunnel.State>()
val runtimeTunnel = RuntimeAwgTunnel(tunnelConfig, stateChannel)
runtimeTunnels[tunnelConfig.id] = runtimeTunnel
@@ -49,36 +44,30 @@ constructor(
}
try {
withTimeout(STARTUP_TIMEOUT_MS) {
updateTunnelStatus(tunnelConfig.id, TunnelStatus.Starting)
val runConfig = runConfigHelper.buildAmRunConfig(tunnelConfig)
backend.setState(runtimeTunnel, AwgTunnel.State.UP, runConfig)
}
val runConfig = runConfigHelper.buildAmRunConfig(tunnelConfig)
backend.setState(runtimeTunnel, Tunnel.State.UP, runConfig)
} catch (_: TimeoutCancellationException) {
Timber.e("Startup timed out for ${tunnelConfig.name} (likely DNS hang)")
errors.emit(tunnelConfig.name to DnsFailure())
forceStopTunnel(tunnelConfig.id)
close()
throw DnsFailure()
} catch (e: BackendException) {
close(e.toBackendCoreException())
throw e.toBackendCoreException()
} catch (_: IllegalArgumentException) {
close(InvalidConfig())
throw InvalidConfig()
} catch (e: Exception) {
Timber.e(e, "Error while setting tunnel state")
close(UnknownError())
throw UnknownError()
}
awaitClose {
try {
backend.setState(runtimeTunnel, AwgTunnel.State.DOWN, null)
backend.setState(runtimeTunnel, Tunnel.State.DOWN, null)
} catch (e: BackendException) {
errors.tryEmit(tunnelConfig.name to e.toBackendCoreException())
// Errors emitted by caller
} finally {
consumerJob.cancel()
stateChannel.close()
runtimeTunnels.remove(tunnelConfig.id)
trySend(TunnelStatus.Down)
close()
}
}
}
@@ -89,7 +78,6 @@ constructor(
backend.backendMode = backendMode.asAmBackendMode()
} catch (e: BackendException) {
throw e.toBackendCoreException()
// TODO this should be mapped to BackendException in the lib
} catch (_: IOException) {
throw VpnUnauthorized()
}
@@ -121,15 +109,11 @@ constructor(
override suspend fun forceStopTunnel(tunnelId: Int) {
val runtimeTunnel = runtimeTunnels[tunnelId] ?: return
try {
backend.setState(runtimeTunnel, AwgTunnel.State.DOWN, null)
backend.setState(runtimeTunnel, Tunnel.State.DOWN, null)
} catch (e: BackendException) {
Timber.e(e, "Force stop failed for $tunnelId")
} finally {
tunJobs[tunnelId]?.cancel()
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 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.MonitoringSettingsRepository
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.network.NetworkUtils
import inet.ipaddr.AddressValueException
import inet.ipaddr.IPAddress
import inet.ipaddr.IPAddressString
import io.ktor.util.collections.*
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import io.ktor.util.collections.ConcurrentMap
import java.util.concurrent.ConcurrentHashMap
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
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
@Singleton
class TunnelMonitor
@Inject
constructor(
private val settingsRepository: GeneralSettingRepository,
class TunnelMonitorHandler(
private val activeTunnels: StateFlow<Map<Int, TunnelState>>,
private val tunnelsRepository: TunnelRepository,
private val settingsRepository: GeneralSettingRepository,
private val monitoringSettingsRepository: MonitoringSettingsRepository,
private val networkMonitor: NetworkMonitor,
private val networkUtils: NetworkUtils,
private val logReader: LogReader,
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)
suspend fun startMonitoring(
tunnelId: Int,
private suspend fun startMonitoring(
config: TunnelConfig,
withLogs: Boolean,
tunStateFlow: StateFlow<TunnelState?>,
getStatistics: suspend (Int) -> TunnelStatistics?,
@@ -45,13 +117,10 @@ constructor(
suspend (
Int, TunnelStatus?, TunnelStatistics?, Map<String, PingState>?, LogHealthState?,
) -> Unit,
): Job = coroutineScope {
launch {
val config = tunnelsRepository.getById(tunnelId) ?: return@launch
launch { startPingMonitor(config, tunStateFlow, updateTunnelStatus) }
launch { startWgStatsPoll(tunnelId, getStatistics, updateTunnelStatus) }
if (withLogs) launch { startLogsMonitor(config, updateTunnelStatus) }
}
) = coroutineScope {
launch { startPingMonitor(config, tunStateFlow, updateTunnelStatus) }
launch { startWgStatsPoll(config.id, getStatistics, updateTunnelStatus) }
if (withLogs) launch { startLogsMonitor(config, updateTunnelStatus) }
}
private suspend fun startLogsMonitor(
@@ -250,7 +319,7 @@ constructor(
tunStateFlow.filter { state -> state?.status is TunnelStatus.Up }.first()
// small delay to make sure tunnel is fully up before we actively monitor
delay(3_000L)
delay(PING_MONITOR_START_DELAY)
while (isActive) {
ensureActive()
@@ -292,17 +361,14 @@ constructor(
) = coroutineScope {
while (isActive) {
ensureActive()
if (!powerManager.isDeviceIdleMode) {
val stats = getStatistics(tunnelId)
ensureActive()
updateTunnelStatus(tunnelId, null, stats, null, null)
}
val stats = getStatistics(tunnelId)
ensureActive()
updateTunnelStatus(tunnelId, null, stats, null, null)
delay(STATS_DELAY)
}
}
companion object {
private val successLogRegex =
Regex("Received handshake response|Receiving keepalive packet", RegexOption.IGNORE_CASE)
@@ -317,5 +383,6 @@ constructor(
const val CLOUDFLARE_IPV6_IP = "2606:4700:4700::1111"
const val CLOUDFLARE_IPV4_IP = "1.1.1.1"
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
import android.content.Context
import androidx.hilt.work.HiltWorker
import androidx.work.*
import androidx.work.CoroutineWorker
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import timber.log.Timber
@HiltWorker
class ServiceWorker
@AssistedInject
constructor(
@Assisted private val context: Context,
@Assisted private val params: WorkerParameters,
class ServiceWorker(
context: Context,
params: WorkerParameters,
private val serviceManager: ServiceManager,
private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : CoroutineWorker(context, params) {
companion object {
private const val TAG = "service_worker"
private const val TAG = "auto_tunnel_service_monitor"
fun stop(context: Context) {
WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
@@ -47,16 +41,15 @@ constructor(
}
}
override suspend fun doWork(): Result =
withContext(ioDispatcher) {
Timber.i("Service worker started")
with(autoTunnelSettingsRepository.getAutoTunnelSettings()) {
Timber.i("Checking to see if auto-tunnel has been killed by system")
if (isAutoTunnelEnabled && serviceManager.autoTunnelService.value == null) {
Timber.i("Service has been killed by system, restoring.")
serviceManager.startAutoTunnelService()
}
override suspend fun doWork(): Result {
Timber.i("Service worker started")
with(autoTunnelSettingsRepository.getAutoTunnelSettings()) {
Timber.i("Checking to see if auto-tunnel has been killed by system")
if (isAutoTunnelEnabled && serviceManager.autoTunnelService.value == null) {
Timber.i("Service has been killed by system, restoring.")
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.edit
import androidx.datastore.preferences.preferencesDataStore
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import java.io.IOException
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
@@ -17,7 +16,7 @@ import timber.log.Timber
class DataStoreManager(
private val context: Context,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
private val ioDispatcher: CoroutineDispatcher,
) {
private val preferencesKey = "preferences"
val Context.dataStore by preferencesDataStore(name = preferencesKey)
@@ -2,12 +2,9 @@ package com.zaneschepke.wireguardautotunnel.data
import androidx.room.RoomDatabase
import androidx.sqlite.db.SupportSQLiteDatabase
import javax.inject.Inject
import javax.inject.Provider
import timber.log.Timber
class DatabaseCallback @Inject constructor(private val databaseProvider: Provider<AppDatabase>) :
RoomDatabase.Callback() {
class DatabaseCallback(private val databaseProvider: Lazy<AppDatabase>) : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
Timber.d("Database created, inserting default rows")
@@ -4,6 +4,7 @@ import androidx.room.Dao
import androidx.room.Query
import androidx.room.Upsert
import com.zaneschepke.wireguardautotunnel.data.entity.GeneralSettings
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import kotlinx.coroutines.flow.Flow
@Dao
@@ -15,4 +16,16 @@ interface GeneralSettingsDao {
@Query("SELECT * FROM general_settings LIMIT 1")
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.entity.AppState as Entity
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.repository.AppStateRepository
import kotlinx.coroutines.CoroutineDispatcher
@@ -18,8 +16,8 @@ import timber.log.Timber
class DataStoreAppStateRepository(
private val dataStoreManager: DataStoreManager,
@ApplicationScope private val applicationScope: CoroutineScope,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
applicationScope: CoroutineScope,
ioDispatcher: CoroutineDispatcher,
) : AppStateRepository {
override suspend fun isLocationDisclosureShown(): Boolean {
return dataStoreManager.getFromStore(DataStoreManager.locationDisclosureShown) ?: false
@@ -4,16 +4,17 @@ import android.content.Context
import com.zaneschepke.wireguardautotunnel.BuildConfig
import com.zaneschepke.wireguardautotunnel.data.mapper.GitHubReleaseMapper
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.repository.UpdateRepository
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.utils.io.*
import io.ktor.client.HttpClient
import io.ktor.client.request.get
import io.ktor.client.statement.HttpResponse
import io.ktor.client.statement.bodyAsChannel
import io.ktor.http.contentLength
import io.ktor.utils.io.ByteReadChannel
import io.ktor.utils.io.readAvailable
import java.io.File
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
@@ -25,8 +26,9 @@ class GitHubUpdateRepository(
private val githubOwner: String,
private val githubRepo: String,
private val context: Context,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
private val ioDispatcher: CoroutineDispatcher,
) : UpdateRepository {
override suspend fun checkForUpdate(currentVersion: String): Result<AppUpdate?> =
withContext(ioDispatcher) {
Timber.i("Checking for update")
@@ -2,22 +2,16 @@ package com.zaneschepke.wireguardautotunnel.data.repository
import android.content.Context
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.repository.InstalledPackageRepository
import com.zaneschepke.wireguardautotunnel.util.extensions.getFriendlyAppName
import javax.inject.Singleton
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.withContext
import timber.log.Timber
@Singleton
class InstalledAndroidPackageRepository(
private val context: Context,
@ApplicationScope val applicationScope: CoroutineScope,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
private val ioDispatcher: CoroutineDispatcher,
) : InstalledPackageRepository {
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.mapper.toDomain
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.repository.AutoTunnelSettingsRepository
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
class RoomAutoTunnelSettingsRepository(
private val autoTunnelSettingsDao: AutoTunnelSettingsDao,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : AutoTunnelSettingsRepository {
class RoomAutoTunnelSettingsRepository(private val autoTunnelSettingsDao: AutoTunnelSettingsDao) :
AutoTunnelSettingsRepository {
override suspend fun upsert(autoTunnelSettings: Domain) {
autoTunnelSettingsDao.upsert(autoTunnelSettings.toEntity())
}
override val flow: Flow<Domain>
get() =
autoTunnelSettingsDao
.getAutoTunnelSettingsFlow()
.map { (it ?: Entity()).toDomain() }
.flowOn(ioDispatcher)
autoTunnelSettingsDao.getAutoTunnelSettingsFlow().map { (it ?: Entity()).toDomain() }
override suspend fun getAutoTunnelSettings(): Domain {
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.mapper.toDomain
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.repository.DnsSettingsRepository
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
class RoomDnsSettingsRepository(
private val dnsSettingsDao: DnsSettingsDao,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : DnsSettingsRepository {
class RoomDnsSettingsRepository(private val dnsSettingsDao: DnsSettingsDao) :
DnsSettingsRepository {
override suspend fun upsert(dnsSettings: Domain) {
dnsSettingsDao.upsert(dnsSettings.toEntity())
}
override val flow: Flow<Domain>
get() =
dnsSettingsDao
.getDnsSettingsFlow()
.map { (it ?: Entity()).toDomain() }
.flowOn(ioDispatcher)
get() = dnsSettingsDao.getDnsSettingsFlow().map { (it ?: Entity()).toDomain() }
override suspend fun getDnsSettings(): Domain {
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.mapper.toDomain
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.repository.LockdownSettingsRepository
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
class RoomLockdownSettingsRepository(
private val lockdownSettingsDao: LockdownSettingsDao,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : LockdownSettingsRepository {
class RoomLockdownSettingsRepository(private val lockdownSettingsDao: LockdownSettingsDao) :
LockdownSettingsRepository {
override suspend fun upsert(lockdownSettings: Domain) {
withContext(ioDispatcher) { lockdownSettingsDao.upsert(lockdownSettings.toEntity()) }
lockdownSettingsDao.upsert(lockdownSettings.toEntity())
}
override val flow =
lockdownSettingsDao
.getLockdownSettingsFlow()
.map { (it ?: Entity()).toDomain() }
.flowOn(ioDispatcher)
lockdownSettingsDao.getLockdownSettingsFlow().map { (it ?: Entity()).toDomain() }
override suspend fun getLockdownSettings(): Domain {
return withContext(ioDispatcher) {
(lockdownSettingsDao.getLockdownSettings() ?: Entity()).toDomain()
}
return (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.mapper.toDomain
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.repository.MonitoringSettingsRepository
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
class RoomMonitoringSettingsRepository(
private val monitoringSettingsDao: MonitoringSettingsDao,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : MonitoringSettingsRepository {
class RoomMonitoringSettingsRepository(private val monitoringSettingsDao: MonitoringSettingsDao) :
MonitoringSettingsRepository {
override suspend fun upsert(monitoringSettings: Domain) {
monitoringSettingsDao.upsert(monitoringSettings.toEntity())
}
override val flow: Flow<Domain>
get() =
monitoringSettingsDao
.getMonitoringSettingsFlow()
.map { (it ?: Entity()).toDomain() }
.flowOn(ioDispatcher)
monitoringSettingsDao.getMonitoringSettingsFlow().map { (it ?: Entity()).toDomain() }
override suspend fun getMonitoringSettings(): Domain {
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.mapper.toDomain
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.repository.ProxySettingsRepository
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
class RoomProxySettingsRepository(
private val proxySettingsDao: ProxySettingsDao,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : ProxySettingsRepository {
class RoomProxySettingsRepository(private val proxySettingsDao: ProxySettingsDao) :
ProxySettingsRepository {
override suspend fun upsert(proxySettings: Domain) {
withContext(ioDispatcher) { proxySettingsDao.upsert(proxySettings.toEntity()) }
proxySettingsDao.upsert(proxySettings.toEntity())
}
override val flow =
proxySettingsDao
.getProxySettingsFlow()
.map { (it ?: Entity()).toDomain() }
.flowOn(ioDispatcher)
override val flow = proxySettingsDao.getProxySettingsFlow().map { (it ?: Entity()).toDomain() }
override suspend fun getProxySettings(): Domain {
return withContext(ioDispatcher) {
(proxySettingsDao.getProxySettings() ?: Entity()).toDomain()
}
return (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.mapper.toDomain
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.repository.GeneralSettingRepository
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.flowOn
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
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) {
withContext(ioDispatcher) { settingsDoa.upsert(generalSettings.toEntity()) }
settingsDao.upsert(generalSettings.toEntity())
}
override val flow =
settingsDoa
.getGeneralSettingsFlow()
.map { (it ?: Entity()).toDomain() }
.flowOn(ioDispatcher)
override val flow = settingsDao.getGeneralSettingsFlow().map { (it ?: Entity()).toDomain() }
override suspend fun getGeneralSettings(): Domain {
return withContext(ioDispatcher) {
(settingsDoa.getGeneralSettings() ?: Entity()).toDomain()
}
return (settingsDao.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.mapper.toDomain
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.repository.TunnelRepository
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
class RoomTunnelRepository(
private val tunnelConfigDao: TunnelConfigDao,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : TunnelRepository {
class RoomTunnelRepository(private val tunnelConfigDao: TunnelConfigDao) : TunnelRepository {
override val flow =
tunnelConfigDao.getAllFlow().flowOn(ioDispatcher).map {
it.map { tunnelConfig -> tunnelConfig.toDomain() }
}
tunnelConfigDao.getAllFlow().map { it.map { tunnelConfig -> tunnelConfig.toDomain() } }
override val userTunnelsFlow =
tunnelConfigDao.getAllTunnelsExceptGlobal().flowOn(ioDispatcher).map {
tunnelConfigDao.getAllTunnelsExceptGlobal().map {
it.map { tunnelConfig -> tunnelConfig.toDomain() }
}
override val globalTunnelFlow: Flow<Domain?> =
tunnelConfigDao.getGlobalTunnel().flowOn(ioDispatcher).map { it?.toDomain() }
tunnelConfigDao.getGlobalTunnel().map { it?.toDomain() }
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) {
withContext(ioDispatcher) { tunnelConfigDao.upsert(tunnelConfig.toEntity()) }
tunnelConfigDao.upsert(tunnelConfig.toEntity())
}
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?) {
withContext(ioDispatcher) {
tunnelConfigDao.resetPrimaryTunnel()
tunnelConfig?.let { save(it.copy(isPrimaryTunnel = true)) }
}
tunnelConfigDao.resetPrimaryTunnel()
tunnelConfig?.let { save(it.copy(isPrimaryTunnel = true)) }
}
override suspend fun resetActiveTunnels() {
withContext(ioDispatcher) { tunnelConfigDao.resetActiveTunnels() }
tunnelConfigDao.resetActiveTunnels()
}
override suspend fun updateMobileDataTunnel(tunnelConfig: Domain?) {
withContext(ioDispatcher) {
tunnelConfigDao.resetMobileDataTunnel()
tunnelConfig?.let { save(it.copy(isMobileDataTunnel = true)) }
}
tunnelConfigDao.resetMobileDataTunnel()
tunnelConfig?.let { save(it.copy(isMobileDataTunnel = true)) }
}
override suspend fun updateEthernetTunnel(tunnelConfig: Domain?) {
withContext(ioDispatcher) {
tunnelConfigDao.resetEthernetTunnel()
tunnelConfig?.let { save(it.copy(isEthernetTunnel = true)) }
}
tunnelConfigDao.resetEthernetTunnel()
tunnelConfig?.let { save(it.copy(isEthernetTunnel = true)) }
}
override suspend fun delete(tunnelConfig: Domain) {
withContext(ioDispatcher) { tunnelConfigDao.delete(tunnelConfig.toEntity()) }
tunnelConfigDao.delete(tunnelConfig.toEntity())
}
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> {
return withContext(ioDispatcher) { tunnelConfigDao.getActive().map { it.toDomain() } }
return tunnelConfigDao.getActive().map { it.toDomain() }
}
override suspend fun getDefaultTunnel(): Domain? {
return withContext(ioDispatcher) { tunnelConfigDao.getDefaultTunnel()?.toDomain() }
return tunnelConfigDao.getDefaultTunnel()?.toDomain()
}
override suspend fun getStartTunnel(): Domain? {
return withContext(ioDispatcher) { tunnelConfigDao.getStartTunnel()?.toDomain() }
return tunnelConfigDao.getStartTunnel()?.toDomain()
}
override suspend fun count(): Int {
return withContext(ioDispatcher) { tunnelConfigDao.count().toInt() }
return tunnelConfigDao.count().toInt()
}
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> {
return withContext(ioDispatcher) {
tunnelConfigDao.findByTunnelNetworkName(name).map { it.toDomain() }
}
return tunnelConfigDao.findByTunnelNetworkName(name).map { it.toDomain() }
}
override suspend fun findByMobileDataTunnel(): List<Domain> {
return withContext(ioDispatcher) {
tunnelConfigDao.findByMobileDataTunnel().map { it.toDomain() }
}
return tunnelConfigDao.findByMobileDataTunnel().map { it.toDomain() }
}
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>) {
withContext(ioDispatcher) { tunnelConfigDao.delete(tunnels.map { it.toEntity() }) }
tunnelConfigDao.delete(tunnels.map { it.toEntity() })
}
}
@@ -7,69 +7,68 @@ import com.zaneschepke.logcatter.LogcatReader
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationMonitor
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.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 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 com.zaneschepke.wireguardautotunnel.viewmodel.*
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import org.koin.android.ext.koin.androidContext
import org.koin.core.annotation.KoinExperimentalAPI
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
@Module
@InstallIn(SingletonComponent::class)
class AppModule {
@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)
@OptIn(KoinExperimentalAPI::class)
val appModule = module {
single<CoroutineScope>(named(Scope.APPLICATION)) {
CoroutineScope(SupervisorJob() + get<CoroutineDispatcher>(named(Dispatcher.DEFAULT)))
}
@Singleton
@Provides
fun provideNotificationService(@ApplicationContext context: Context): NotificationManager {
return WireGuardNotification(context)
single<LogReader> { LogcatReader.init(storageDir = androidContext().filesDir.absolutePath) }
single<PowerManager> {
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
@Provides
fun provideShortcutManager(
@ApplicationContext context: Context,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): ShortcutManager {
return DynamicShortcutManager(context, ioDispatcher)
}
singleOf(::GlobalEffectRepository)
@Singleton
@Provides
fun provideNetworkUtils(@IoDispatcher ioDispatcher: CoroutineDispatcher): NetworkUtils {
return NetworkUtils(ioDispatcher)
}
single { FileUtils(androidContext(), get(named(Dispatcher.IO))) }
single<ShortcutManager> { DynamicShortcutManager(androidContext(), get(named(Dispatcher.IO))) }
singleOf(::SelectedTunnelsRepository)
@Singleton
@Provides
fun provideNotificationMonitor(
tunnelManager: TunnelManager,
notificationManager: NotificationManager,
): NotificationMonitor {
return NotificationMonitor(tunnelManager, notificationManager)
}
single { NetworkUtils(get(named(Dispatcher.IO))) }
@Provides
fun providePowerManager(@ApplicationContext context: Context): PowerManager {
return context.getSystemService(Context.POWER_SERVICE) as PowerManager
}
viewModelOf(::AutoTunnelViewModel)
viewModel { (id: Int?) -> ConfigViewModel(get(), get(), get(), id) }
viewModelOf(::DnsViewModel)
viewModelOf(::LicenseViewModel)
viewModelOf(::LockdownViewModel)
viewModelOf(::LoggerViewModel)
viewModelOf(::MonitoringViewModel)
viewModelOf(::ProxySettingsViewModel)
viewModelOf(::SettingsViewModel)
viewModelOf(::SharedAppViewModel)
viewModel { (id: Int) -> SplitTunnelViewModel(get(), get(), get(), id) }
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
import android.content.Context
import android.os.PowerManager
import com.wireguard.android.backend.WgQuickBackend
import com.wireguard.android.util.RootShell
import com.wireguard.android.util.ToolsInstaller
import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.*
import com.zaneschepke.wireguardautotunnel.domain.repository.*
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.backend.KernelTunnel
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.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.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.map
import org.amnezia.awg.backend.Backend
import org.amnezia.awg.backend.GoBackend
import org.amnezia.awg.backend.ProxyGoBackend
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
@InstallIn(SingletonComponent::class)
class TunnelModule {
val tunnelModule = module {
single(named(Shell.TUNNEL)) { RootShell(androidContext()) }
single(named(Shell.APP)) { RootShell(androidContext()) }
@Provides
@Singleton
@TunnelShell
fun provideTunnelRootShell(@ApplicationContext context: Context): RootShell {
return RootShell(context)
}
single { RootShellUtils(get(named(Shell.APP)), get(named(Dispatcher.IO))) }
@Provides
@Singleton
@AppShell
fun provideAppRootShell(@ApplicationContext context: Context): RootShell {
return RootShell(context)
}
singleOf(::RunConfigHelper)
@Provides
@Singleton
@Userspace
fun provideAmneziaBackend(@ApplicationContext context: Context): Backend {
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)),
single<Backend>(named(Core.USERSPACE)) {
GoBackend(
androidContext(),
RootTunnelActionHandler(org.amnezia.awg.util.RootShell(androidContext())),
)
}
@Provides
@Singleton
fun provideKernelBackend(
@ApplicationContext context: Context,
@TunnelShell shell: RootShell,
): com.wireguard.android.backend.Backend {
return WgQuickBackend(
context,
single<Backend>(named(Core.PROXY_USERSPACE)) {
ProxyGoBackend(
androidContext(),
RootTunnelActionHandler(org.amnezia.awg.util.RootShell(androidContext())),
)
}
single<com.wireguard.android.backend.Backend> {
val shell = get<RootShell>(named(Shell.TUNNEL))
WgQuickBackend(
androidContext(),
shell,
ToolsInstaller(context, shell),
ToolsInstaller(androidContext(), shell),
com.wireguard.android.backend.RootTunnelActionHandler(shell),
)
.also { it.setMultipleTunnels(true) }
.apply { setMultipleTunnels(true) }
}
@Provides
@Singleton
@Kernel
fun provideKernelProvider(
@ApplicationScope applicationScope: CoroutineScope,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
backend: com.wireguard.android.backend.Backend,
runConfigHelper: RunConfigHelper,
): TunnelProvider {
return KernelTunnel(applicationScope, ioDispatcher, runConfigHelper, backend)
single<TunnelBackend>(named(Core.KERNEL)) {
KernelTunnel(get(), get<com.wireguard.android.backend.Backend>())
}
@Provides
@Singleton
@Userspace
fun provideUserspaceProvider(
@ApplicationScope applicationScope: CoroutineScope,
runConfigHelper: RunConfigHelper,
@Userspace backend: Backend,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): TunnelProvider {
return UserspaceTunnel(applicationScope, ioDispatcher, backend, runConfigHelper)
single<TunnelBackend>(qualifier = named(Core.USERSPACE)) {
UserspaceTunnel(get<Backend>(named(Core.USERSPACE)), get())
}
@Provides
@Singleton
@ProxyUserspace
fun provideProxyUserspaceProvider(
@ApplicationScope applicationScope: CoroutineScope,
runConfigHelper: RunConfigHelper,
@ProxyUserspace backend: Backend,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): TunnelProvider {
return UserspaceTunnel(applicationScope, ioDispatcher, backend, runConfigHelper)
single<TunnelBackend>(qualifier = named(Core.PROXY_USERSPACE)) {
UserspaceTunnel(get<Backend>(named(Core.PROXY_USERSPACE)), get())
}
@Provides
@Singleton
fun provideTunnelManager(
@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,
single<NetworkMonitor> {
AndroidNetworkMonitor(
androidContext(),
object : AndroidNetworkMonitor.ConfigurationListener {
override val detectionMethod: Flow<AndroidNetworkMonitor.WifiDetectionMethod>
get() =
autoTunnelSettingsRepository.flow
.distinctUntilChangedBy { it.wifiDetectionMethod }
.map { it.wifiDetectionMethod.to() }
override val detectionMethod =
get<AutoTunnelSettingsRepository>()
.flow
.distinctUntilChangedBy { it.wifiDetectionMethod }
.map { it.wifiDetectionMethod.to() }
override val rootShell: RootShell
get() = appShell
override val rootShell = get<RootShell>(named(Shell.APP))
},
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) }
@@ -40,3 +40,7 @@ class UnknownError : BackendCoreException() {
class UapiUpdateFailed : BackendCoreException() {
override val stringRes = R.string.active_tunnel_update_failed
}
class KernelWireguardNotSupported : BackendCoreException() {
override val stringRes = R.string.kernel_wireguard_unsupported
}
@@ -1,6 +1,8 @@
package com.zaneschepke.wireguardautotunnel.domain.repository
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
import kotlinx.coroutines.flow.Flow
interface GeneralSettingRepository {
@@ -9,4 +11,12 @@ interface GeneralSettingRepository {
val flow: Flow<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
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
import javax.inject.Singleton
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
@Singleton
class GlobalEffectRepository {
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
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 {
NoConnectivity,
@@ -4,13 +4,9 @@ import androidx.compose.runtime.staticCompositionLocalOf
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavKey
import com.zaneschepke.wireguardautotunnel.ui.navigation.NavController
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
val LocalIsAndroidTV = staticCompositionLocalOf { false }
val LocalSharedVm =
staticCompositionLocalOf<SharedAppViewModel> { error("No shared viewmodel provided") }
val LocalNavController = staticCompositionLocalOf<NavController> { error("No backstack provided") }
typealias BackStack = NavBackStack<NavKey>
@@ -10,6 +10,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@@ -17,7 +18,7 @@ import androidx.compose.ui.unit.dp
@Composable
fun ExpandingRowListItem(
leading: @Composable () -> Unit,
text: String,
text: AnnotatedString,
trailing: @Composable () -> Unit,
isSelected: Boolean,
expanded: @Composable () -> Unit,
@@ -1,11 +1,10 @@
package com.zaneschepke.wireguardautotunnel.ui.common.label
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import java.util.*
import androidx.compose.ui.text.intl.Locale
@Composable
fun lowercaseLabel(text: String): String {
val locale = remember { Locale.getDefault() }
val locale = Locale.current.platformLocale
return text.lowercase(locale)
}
@@ -1,52 +1,20 @@
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.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.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
@Composable
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),
)
}
}
}
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.common.text.DescriptionText
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -59,7 +27,21 @@ fun CustomBottomSheet(options: List<SheetOption>, onDismiss: () -> Unit) {
sheetState = sheetState,
) {
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()
}
}
@@ -70,4 +52,5 @@ data class SheetOption(
val label: String,
val onClick: () -> Unit,
val selected: Boolean = false,
val description: String? = null,
)
@@ -12,6 +12,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@Composable
@@ -50,6 +51,8 @@ fun ConfigurationTextBox(
label,
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.labelMedium,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
},
containerColor = MaterialTheme.colorScheme.surface,
@@ -58,6 +61,8 @@ fun ConfigurationTextBox(
hint,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.outline,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
},
keyboardOptions = keyboardOptions,
@@ -7,6 +7,7 @@ class NavController(
private val backStack: NavBackStack<NavKey>,
private val isDisclosureShown: Boolean,
private val onChange: (previous: NavKey?) -> Unit = {},
private val onExitApp: () -> Unit = {},
) {
fun push(route: NavKey) {
onChange(currentRoute)
@@ -14,12 +15,13 @@ class NavController(
}
fun pop(): Boolean {
if (currentRoute != null) {
onChange(currentRoute)
backStack.removeLastOrNull()
if (!canPop) {
onExitApp()
return true
}
return false
onChange(currentRoute)
backStack.removeLastOrNull()
return true
}
fun popUpTo(route: NavKey) {
@@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.automirrored.rounded.Sort
import androidx.compose.material.icons.outlined.ContentPasteGo
import androidx.compose.material.icons.outlined.CopyAll
import androidx.compose.material.icons.rounded.*
import androidx.compose.material3.Icon
@@ -22,13 +23,13 @@ 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.sideeffect.LocalSideEffect
import com.zaneschepke.wireguardautotunnel.ui.state.GlobalAppUiState
import com.zaneschepke.wireguardautotunnel.ui.state.NavbarState
import com.zaneschepke.wireguardautotunnel.ui.state.SharedAppUiState
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
@Composable
fun currentRouteAsNavbarState(
sharedState: SharedAppUiState,
globalState: GlobalAppUiState,
sharedViewModel: SharedAppViewModel,
route: Route?,
navController: NavController,
@@ -36,7 +37,7 @@ fun currentRouteAsNavbarState(
val keyboardController = LocalSoftwareKeyboardController.current
val context = LocalContext.current
return remember(route, sharedState) {
return remember(route, globalState) {
derivedStateOf {
when (route) {
AdvancedAutoTunnel ->
@@ -69,7 +70,7 @@ fun currentRouteAsNavbarState(
NavbarState(
showBottomItems = true,
topTitle =
if (!sharedState.isLocationDisclosureShown) null
if (!globalState.isLocationDisclosureShown) null
else {
context.getString(R.string.auto_tunnel)
},
@@ -217,6 +218,18 @@ fun currentRouteAsNavbarState(
topTitle = context.getString(R.string.sort),
topTrailing = {
Row {
IconButton(
onClick = {
sharedViewModel.postSideEffect(
LocalSideEffect.SortByLatency
)
}
) {
Icon(
Icons.Rounded.NetworkCheck,
stringResource(R.string.sort_by_latency),
)
}
IconButton(
onClick = {
sharedViewModel.postSideEffect(LocalSideEffect.Sort)
@@ -237,7 +250,7 @@ fun currentRouteAsNavbarState(
is Config,
is ConfigGlobal -> {
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)
NavbarState(
topLeading = {
@@ -265,8 +278,7 @@ fun currentRouteAsNavbarState(
is SplitTunnel,
is SplitTunnelGlobal -> {
val tunnelName =
if (route is SplitTunnel)
sharedState.tunnels.find { it.id == route.id }?.name
if (route is SplitTunnel) globalState.tunnelNames[route.id]
else context.getString(R.string.global_split_tunneling)
NavbarState(
topLeading = {
@@ -279,12 +291,26 @@ fun currentRouteAsNavbarState(
},
topTitle = tunnelName ?: "",
topTrailing = {
IconButton(
onClick = {
sharedViewModel.postSideEffect(LocalSideEffect.SaveChanges)
Row {
IconButton(
onClick = {
sharedViewModel.postSideEffect(
LocalSideEffect.Modal.SelectTunnel
)
}
) {
Icon(
Icons.Outlined.ContentPasteGo,
stringResource(R.string.copy_from),
)
}
IconButton(
onClick = {
sharedViewModel.postSideEffect(LocalSideEffect.SaveChanges)
}
) {
Icon(Icons.Rounded.Save, stringResource(R.string.save))
}
) {
Icon(Icons.Rounded.Save, stringResource(R.string.save))
}
},
showBottomItems = true,
@@ -322,7 +348,7 @@ fun currentRouteAsNavbarState(
showBottomItems = true,
)
is TunnelSettings -> {
val tunnelName = sharedState.tunnels.find { it.id == route.id }?.name
val tunnelName = globalState.tunnelNames[route.id]
NavbarState(
topLeading = {
IconButton(onClick = { navController.pop() }) {
@@ -354,7 +380,7 @@ fun currentRouteAsNavbarState(
NavbarState(
topTitle = context.getString(R.string.tunnels),
topTrailing = {
when (sharedState.selectedTunnels.size) {
when (globalState.selectedTunnelCount) {
0 ->
Row {
IconButton(onClick = { navController.push(Sort) }) {
@@ -408,7 +434,7 @@ fun currentRouteAsNavbarState(
}
}
if (sharedState.selectedTunnels.size == 1) {
if (globalState.selectedTunnelCount == 1) {
IconButton(
onClick = {
sharedViewModel.postSideEffect(
@@ -7,12 +7,13 @@ import androidx.navigation3.runtime.NavKey
import com.zaneschepke.wireguardautotunnel.ui.navigation.NavController
@Composable
fun <T : NavKey> rememberNavController(
fun rememberNavController(
backStack: NavBackStack<NavKey>,
isDisclosureShown: Boolean,
onChange: (NavKey?) -> Unit = {},
onExitApp: () -> Unit = {},
): NavController {
return remember(backStack, onChange, isDisclosureShown) {
NavController(backStack, isDisclosureShown, onChange)
return remember(backStack, isDisclosureShown, onChange, onExitApp) {
NavController(backStack, isDisclosureShown, onChange, onExitApp)
}
}
@@ -12,7 +12,15 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
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.Icon
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.unit.dp
import androidx.core.net.toUri
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.zaneschepke.networkmonitor.ActiveNetwork
import com.zaneschepke.wireguardautotunnel.R
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.SwitchWithDivider
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.TunnelNetwork
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)
@Composable
fun AutoTunnelScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
fun AutoTunnelScreen(
viewModel: AutoTunnelViewModel = koinViewModel(),
sharedViewModel: SharedAppViewModel = koinActivityViewModel(),
) {
val context = LocalContext.current
val navController = LocalNavController.current
val shareViewModel = LocalSharedVm.current
val clipboard = rememberClipboardHelper()
val sharedUiState by shareViewModel.container.stateFlow.collectAsStateWithLifecycle()
val globalUiState by sharedViewModel.container.stateFlow.collectAsStateWithLifecycle()
val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
if (uiState.isLoading) return
val batteryActivity =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
shareViewModel.disableBatteryOptimizationsShown()
sharedViewModel.disableBatteryOptimizationsShown()
}
@SuppressLint("BatteryLife")
@@ -112,9 +123,9 @@ fun AutoTunnelScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
}
fun onAutoTunnelClick() {
if (!sharedUiState.isBatteryOptimizationShown)
if (!globalUiState.isBatteryOptimizationShown)
return requestDisableBatteryOptimizations()
viewModel.toggleAutoTunnel(sharedUiState.settings.appMode)
viewModel.toggleAutoTunnel(globalUiState.appMode)
}
SurfaceRow(
@@ -15,16 +15,16 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.dropdown.LabelledDropdown
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.common.text.DescriptionText
import com.zaneschepke.wireguardautotunnel.viewmodel.AutoTunnelViewModel
import org.koin.androidx.compose.koinViewModel
@Composable
fun AutoTunnelAdvancedScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
fun AutoTunnelAdvancedScreen(viewModel: AutoTunnelViewModel = koinViewModel()) {
val autoTunnelState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
Column(
horizontalAlignment = Alignment.Start,
@@ -10,16 +10,16 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.data.model.WifiDetectionMethod
import com.zaneschepke.wireguardautotunnel.ui.common.button.IconSurfaceButton
import com.zaneschepke.wireguardautotunnel.util.extensions.asDescriptionString
import com.zaneschepke.wireguardautotunnel.util.extensions.asTitleString
import com.zaneschepke.wireguardautotunnel.viewmodel.AutoTunnelViewModel
import org.koin.androidx.compose.koinViewModel
@Composable
fun WifiDetectionMethodScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
fun WifiDetectionMethodScreen(viewModel: AutoTunnelViewModel = koinViewModel()) {
val context = LocalContext.current
val autoTunnelState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
@@ -23,16 +23,16 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
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.navigation.Route
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.disclosure.components.LocationDisclosureHeader
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
import org.koin.compose.viewmodel.koinActivityViewModel
@Composable
fun LocationDisclosureScreen() {
fun LocationDisclosureScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel()) {
val context = LocalContext.current
val navController = LocalNavController.current
val viewModel = LocalSharedVm.current
fun goToAutoTunnel() {
navController.popUpTo(Route.AutoTunnel)
@@ -43,7 +43,7 @@ fun LocationDisclosureScreen() {
goToAutoTunnel()
}
LaunchedEffect(Unit) { viewModel.setLocationDisclosureShown() }
LaunchedEffect(Unit) { sharedViewModel.setLocationDisclosureShown() }
Column(
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.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.items
import androidx.compose.foundation.lazy.rememberLazyListState
@@ -18,7 +22,12 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
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.Modifier
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.withStyle
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R
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.screens.autotunnel.components.WildcardsLabel
import com.zaneschepke.wireguardautotunnel.viewmodel.AutoTunnelViewModel
import org.koin.androidx.compose.koinViewModel
@Composable
fun PreferredTunnelScreen(
tunnelNetwork: TunnelNetwork,
viewModel: AutoTunnelViewModel = hiltViewModel(),
viewModel: AutoTunnelViewModel = koinViewModel(),
) {
val navController = LocalNavController.current
@@ -14,14 +14,19 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
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.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R
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.openWebUrl
import com.zaneschepke.wireguardautotunnel.viewmodel.AutoTunnelViewModel
import org.koin.androidx.compose.koinViewModel
@Composable
fun WifiSettingsScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
fun WifiSettingsScreen(viewModel: AutoTunnelViewModel = koinViewModel()) {
val context = LocalContext.current
val navController = LocalNavController.current
@@ -11,15 +11,15 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
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.PinManager
@Composable
fun PinLockScreen() {
val sharedViewModel = LocalSharedVm.current
fun PinLockScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel()) {
val navController = LocalNavController.current
val pinAlreadyExists by rememberSaveable { mutableStateOf(PinManager.pinExists()) }
var pinCreated by rememberSaveable { mutableStateOf(false) }
@@ -9,26 +9,36 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.CallSplit
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.IconButton
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.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.MainActivity
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
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.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.common.button.SwitchWithDivider
@@ -45,17 +55,21 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.asTitleString
import com.zaneschepke.wireguardautotunnel.util.extensions.capitalize
import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
import com.zaneschepke.wireguardautotunnel.viewmodel.SettingsViewModel
import java.util.*
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
import org.koin.androidx.compose.koinViewModel
import org.koin.compose.viewmodel.koinActivityViewModel
@Composable
fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
fun SettingsScreen(
viewModel: SettingsViewModel = koinViewModel(),
sharedViewModel: SharedAppViewModel = koinActivityViewModel(),
) {
val context = LocalContext.current
val navController = LocalNavController.current
val sharedViewModel = LocalSharedVm.current
val locale = remember { Locale.getDefault() }
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()
if (uiState.isLoading) return
@@ -72,7 +86,7 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
}
fun performBackupRestore(action: () -> Unit) {
if (sharedUiState.activeTunnels.isNotEmpty() || sharedUiState.isAutoTunnelActive)
if (uiState.tunnelActive || globalUiState.isAutoTunnelActive)
return context.showToast(R.string.all_services_disabled)
showBackupSheet = false
action()
@@ -151,22 +165,22 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
Icons.AutoMirrored.Outlined.CallSplit,
contentDescription = null,
tint =
if (sharedUiState.proxyEnabled) Disabled
if (globalUiState.appMode == AppMode.PROXY) Disabled
else MaterialTheme.colorScheme.onSurface,
)
},
enabled = !sharedUiState.proxyEnabled,
enabled = globalUiState.appMode != AppMode.PROXY,
title = stringResource(R.string.global_split_tunneling),
trailing = { modifier ->
SwitchWithDivider(
checked = uiState.settings.isGlobalSplitTunnelEnabled,
onClick = { viewModel.setGlobalSplitTunneling(it) },
modifier = modifier,
enabled = !sharedUiState.proxyEnabled,
enabled = globalUiState.appMode != AppMode.PROXY,
)
},
description =
if (sharedUiState.proxyEnabled) {
if (globalUiState.appMode == AppMode.PROXY) {
{
DescriptionText(
stringResource(R.string.unavailable_in_mode),
@@ -197,14 +211,15 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
Icons.Outlined.NetworkPing,
contentDescription = null,
tint =
if (!sharedUiState.proxyEnabled) MaterialTheme.colorScheme.onSurface
if (globalUiState.appMode != AppMode.PROXY)
MaterialTheme.colorScheme.onSurface
else Disabled,
)
},
title = stringResource(R.string.ping_monitor),
enabled = !sharedUiState.proxyEnabled,
enabled = globalUiState.appMode != AppMode.PROXY,
description =
if (sharedUiState.proxyEnabled) {
if (globalUiState.appMode == AppMode.PROXY) {
{
DescriptionText(
stringResource(R.string.unavailable_in_mode),
@@ -216,7 +231,7 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
SwitchWithDivider(
checked = uiState.monitoring.isPingEnabled,
onClick = { viewModel.setPingEnabled(it) },
enabled = !sharedUiState.proxyEnabled,
enabled = globalUiState.appMode != AppMode.PROXY,
modifier = modifier,
)
},
@@ -14,14 +14,13 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
import org.koin.compose.viewmodel.koinActivityViewModel
@Composable
fun DisplayScreen() {
val sharedViewModel = LocalSharedVm.current
fun DisplayScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel()) {
val appState by sharedViewModel.container.stateFlow.collectAsStateWithLifecycle()
@@ -14,25 +14,25 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.intl.Locale
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
import java.text.Collator
import java.util.*
import org.koin.compose.viewmodel.koinActivityViewModel
@Composable
fun LanguageScreen() {
fun LanguageScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel()) {
val sharedViewModel = LocalSharedVm.current
val appState by sharedViewModel.container.stateFlow.collectAsStateWithLifecycle()
val collator = Collator.getInstance(Locale.getDefault())
val collator = Collator.getInstance(Locale.current.platformLocale)
val locales =
LocaleUtil.supportedLocales.map {
val tag = it.replace("_", "-")
Locale.forLanguageTag(tag)
java.util.Locale.forLanguageTag(tag)
}
val sortedLocales =
@@ -46,7 +46,7 @@ fun LanguageScreen() {
remember(appState.locale, sortedLocales) {
if (appState.locale == LocaleUtil.OPTION_PHONE_LANGUAGE) 0
else {
val selectedLocale = Locale.forLanguageTag(appState.locale)
val selectedLocale = java.util.Locale.forLanguageTag(appState.locale)
sortedLocales.indexOfFirst {
it.toLanguageTag() == selectedLocale.toLanguageTag()
} + 1
@@ -13,15 +13,14 @@ import androidx.compose.material.icons.outlined.Dns
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.model.DnsProtocol
@@ -34,16 +33,16 @@ import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
import com.zaneschepke.wireguardautotunnel.util.extensions.capitalize
import com.zaneschepke.wireguardautotunnel.viewmodel.DnsViewModel
import java.util.*
import org.koin.androidx.compose.koinViewModel
@Composable
fun DnsSettingsScreen(viewModel: DnsViewModel = hiltViewModel()) {
fun DnsSettingsScreen(viewModel: DnsViewModel = koinViewModel()) {
val context = LocalContext.current
val navController = LocalNavController.current
val dnsUiState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
if (dnsUiState.isLoading) return
val locale = remember { Locale.getDefault() }
val locale = Locale.current.platformLocale
Column(
horizontalAlignment = Alignment.Start,
@@ -1,26 +1,40 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.integrations
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.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.Launch
import androidx.compose.material.icons.filled.AppShortcut
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.IconButton
import androidx.compose.material3.MaterialTheme
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.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R
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.util.extensions.launchVpnSettings
import com.zaneschepke.wireguardautotunnel.viewmodel.SettingsViewModel
import org.koin.androidx.compose.koinViewModel
@Composable
fun AndroidIntegrationsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
fun AndroidIntegrationsScreen(viewModel: SettingsViewModel = koinViewModel()) {
val context = LocalContext.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.vectorResource
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
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.ThemedSwitch
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.sideeffect.LocalSideEffect
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
@Composable
fun LockdownSettingsScreen(viewModel: LockdownViewModel = hiltViewModel()) {
val sharedViewModel = LocalSharedVm.current
fun LockdownSettingsScreen(
viewModel: LockdownViewModel = koinViewModel(),
sharedViewModel: SharedAppViewModel = koinActivityViewModel(),
) {
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.verticalScroll
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.MaterialTheme
import androidx.compose.material3.Text
@@ -17,7 +21,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R
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.navigation.Route
import com.zaneschepke.wireguardautotunnel.viewmodel.MonitoringViewModel
import org.koin.androidx.compose.koinViewModel
@Composable
fun TunnelMonitoringScreen(viewModel: MonitoringViewModel = hiltViewModel()) {
fun TunnelMonitoringScreen(viewModel: MonitoringViewModel = koinViewModel()) {
val navController = LocalNavController.current
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.material3.MaterialTheme
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.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
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.LogsBottomSheet
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
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
@Composable
fun LogsScreen(viewModel: LoggerViewModel = hiltViewModel()) {
val sharedAppViewModel = LocalSharedVm.current
fun LogsScreen(
viewModel: LoggerViewModel = koinViewModel(),
sharedViewModel: SharedAppViewModel = koinActivityViewModel(),
) {
val loggerState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
val lazyColumnListState = rememberLazyListState()
@@ -31,7 +40,7 @@ fun LogsScreen(viewModel: LoggerViewModel = hiltViewModel()) {
var lastScrollPosition by rememberSaveable { mutableIntStateOf(0) }
var showLogsSheet by rememberSaveable { mutableStateOf(false) }
sharedAppViewModel.collectSideEffect { sideEffect ->
sharedViewModel.collectSideEffect { sideEffect ->
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.MaterialTheme
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.Modifier
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.KeyboardCapitalization
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R
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.util.extensions.isValidIpv4orIpv6Address
import com.zaneschepke.wireguardautotunnel.viewmodel.MonitoringViewModel
import org.koin.androidx.compose.koinViewModel
@Composable
fun PingTargetScreen(viewModel: MonitoringViewModel = hiltViewModel()) {
fun PingTargetScreen(viewModel: MonitoringViewModel = koinViewModel()) {
val settingsState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
@@ -19,12 +19,11 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R
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.ThemedSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
@@ -33,18 +32,21 @@ import com.zaneschepke.wireguardautotunnel.ui.common.security.SecureScreenFromRe
import com.zaneschepke.wireguardautotunnel.ui.common.textbox.ConfigurationTextBox
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
import com.zaneschepke.wireguardautotunnel.viewmodel.ProxySettingsViewModel
import java.util.Locale
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
import org.koin.androidx.compose.koinViewModel
import org.koin.compose.viewmodel.koinActivityViewModel
import org.orbitmvi.orbit.compose.collectSideEffect
@Composable
fun ProxySettingsScreen(viewModel: ProxySettingsViewModel = hiltViewModel()) {
val sharedViewModel = LocalSharedVm.current
fun ProxySettingsScreen(
viewModel: ProxySettingsViewModel = koinViewModel(),
sharedViewModel: SharedAppViewModel = koinActivityViewModel(),
) {
val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
if (uiState.isLoading) return
val locale = remember { Locale.getDefault() }
val locale = Locale.current.platformLocale
val proxySettings by remember(uiState) { mutableStateOf(uiState.proxySettings) }
@@ -8,6 +8,7 @@ import com.zaneschepke.wireguardautotunnel.ui.common.sheet.CustomBottomSheet
import com.zaneschepke.wireguardautotunnel.ui.common.sheet.SheetOption
import com.zaneschepke.wireguardautotunnel.util.extensions.asIcon
import com.zaneschepke.wireguardautotunnel.util.extensions.asTitleString
import com.zaneschepke.wireguardautotunnel.util.extensions.description
@Composable
fun AppModeBottomSheet(
@@ -31,6 +32,7 @@ fun AppModeBottomSheet(
onAppModeChange(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.vectorResource
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.BuildConfig
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.extensions.*
import com.zaneschepke.wireguardautotunnel.viewmodel.SupportViewModel
import org.koin.androidx.compose.koinViewModel
@Composable
fun SupportScreen(viewModel: SupportViewModel = hiltViewModel()) {
fun SupportScreen(viewModel: SupportViewModel = koinViewModel()) {
val context = LocalContext.current
val navController = LocalNavController.current
@@ -191,7 +191,8 @@ fun SupportScreen(viewModel: SupportViewModel = hiltViewModel()) {
return@SurfaceRow context.showToast(R.string.update_check_unsupported)
when (BuildConfig.FLAVOR) {
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()
}
},
@@ -18,7 +18,6 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.BuildConfig
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.extensions.openWebUrl
import com.zaneschepke.wireguardautotunnel.viewmodel.SettingsViewModel
import org.koin.androidx.compose.koinViewModel
@Composable
fun DonateScreen(viewModel: SettingsViewModel = hiltViewModel()) {
fun DonateScreen(viewModel: SettingsViewModel = koinViewModel()) {
val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
if (uiState.isLoading) return
val context = LocalContext.current
@@ -10,14 +10,14 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.ui.screens.support.license.components.LicenseList
import com.zaneschepke.wireguardautotunnel.viewmodel.LicenseViewModel
import org.koin.androidx.compose.koinViewModel
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun LicenseScreen(viewModel: LicenseViewModel = hiltViewModel()) {
fun LicenseScreen(viewModel: LicenseViewModel = koinViewModel()) {
val licenseUiState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
if (licenseUiState.isLoading) {
@@ -14,7 +14,6 @@ import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R
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.functions.rememberClipboardHelper
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.util.FileUtils
import com.zaneschepke.wireguardautotunnel.util.StringValue
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
import io.github.g00fy2.quickie.QRResult
import io.github.g00fy2.quickie.ScanQRCode
import org.koin.compose.viewmodel.koinActivityViewModel
import org.orbitmvi.orbit.compose.collectSideEffect
import timber.log.Timber
@Composable
fun TunnelsScreen() {
val viewModel = LocalSharedVm.current
fun TunnelsScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel()) {
val navController = LocalNavController.current
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 showImportSheet by rememberSaveable { mutableStateOf(false) }
var showDeleteModal by rememberSaveable { mutableStateOf(false) }
var showUrlDialog by rememberSaveable { mutableStateOf(false) }
viewModel.collectSideEffect { sideEffect ->
sharedViewModel.collectSideEffect { sideEffect ->
when (sideEffect) {
LocalSideEffect.Sheet.ImportTunnels -> showImportSheet = true
LocalSideEffect.Modal.DeleteTunnels -> showDeleteModal = true
LocalSideEffect.Sheet.ExportTunnels -> showExportSheet = true
LocalSideEffect.SelectedTunnels.Copy -> viewModel.copySelectedTunnel()
LocalSideEffect.SelectedTunnels.SelectAll -> viewModel.toggleSelectAllTunnels()
LocalSideEffect.SelectedTunnels.Copy -> sharedViewModel.copySelectedTunnel()
LocalSideEffect.SelectedTunnels.SelectAll -> sharedViewModel.toggleSelectAllTunnels()
else -> Unit
}
}
@@ -58,11 +60,11 @@ fun TunnelsScreen() {
val tunnelFileImportResultLauncher =
rememberFileImportLauncherForResult(
onNoFileExplorer = {
viewModel.showSnackMessage(
sharedViewModel.showSnackMessage(
StringValue.StringResource(R.string.error_no_file_explorer)
)
},
onData = { data -> viewModel.importFromUri(data) },
onData = { data -> sharedViewModel.importFromUri(data) },
)
val scanQrCodeLauncher =
@@ -72,13 +74,13 @@ fun TunnelsScreen() {
Timber.e(result.exception, "QR Code")
}
QRResult.QRMissingPermission -> {
viewModel.showSnackMessage(
sharedViewModel.showSnackMessage(
StringValue.StringResource(R.string.camera_permission_required)
)
}
is QRResult.QRSuccess -> {
result.content.rawValue?.let { viewModel.importFromQr(it) }
?: viewModel.showSnackMessage(
result.content.rawValue?.let { sharedViewModel.importFromQr(it) }
?: sharedViewModel.showSnackMessage(
StringValue.StringResource(R.string.config_error)
)
}
@@ -90,7 +92,7 @@ fun TunnelsScreen() {
rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted
->
if (!isGranted) {
viewModel.showSnackMessage(
sharedViewModel.showSnackMessage(
StringValue.StringResource(R.string.camera_permission_required)
)
return@rememberLauncherForActivityResult
@@ -102,7 +104,7 @@ fun TunnelsScreen() {
InfoDialog(
onDismiss = { showDeleteModal = false },
onAttest = {
viewModel.deleteSelectedTunnels()
sharedViewModel.deleteSelectedTunnels()
showDeleteModal = false
},
title = stringResource(R.string.delete_tunnel),
@@ -113,11 +115,11 @@ fun TunnelsScreen() {
if (showExportSheet) {
ExportTunnelsBottomSheet({ type, uri ->
viewModel.exportSelectedTunnels(type, uri)
sharedViewModel.exportSelectedTunnels(type, uri)
showExportSheet = false
}) {
showExportSheet = false
viewModel.clearSelectedTunnels()
sharedViewModel.clearSelectedTunnels()
}
}
@@ -130,7 +132,7 @@ fun TunnelsScreen() {
onQrClick = { requestPermissionLauncher.launch(android.Manifest.permission.CAMERA) },
onClipboardClick = {
clipboard.paste { result ->
if (result != null) viewModel.importFromClipboard(result)
if (result != null) sharedViewModel.importFromClipboard(result)
}
},
onManualImportClick = { navController.push(Route.Config(null)) },
@@ -142,11 +144,11 @@ fun TunnelsScreen() {
UrlImportDialog(
onDismiss = { showUrlDialog = false },
onConfirm = { url ->
viewModel.importFromUrl(url)
sharedViewModel.importFromUrl(url)
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.rounded.Circle
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.Modifier
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.SwitchWithDivider
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.openWebUrl
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
@@ -33,7 +37,7 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun TunnelList(
sharedState: SharedAppUiState,
uiState: TunnelsUiState,
modifier: Modifier = Modifier,
viewModel: SharedAppViewModel,
) {
@@ -48,7 +52,7 @@ fun TunnelList(
modifier
.pointerInput(Unit) {
detectTapGestures {
if (sharedState.tunnels.isEmpty()) return@detectTapGestures
if (uiState.tunnels.isEmpty()) return@detectTapGestures
viewModel.clearSelectedTunnels()
}
}
@@ -58,7 +62,7 @@ fun TunnelList(
reverseLayout = false,
flingBehavior = ScrollableDefaults.flingBehavior(),
) {
if (sharedState.tunnels.isEmpty()) {
if (uiState.tunnels.isEmpty()) {
item {
GettingStartedLabel(
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 =
remember(sharedState.activeTunnels) {
sharedState.activeTunnels[tunnel.id] ?: TunnelState()
remember(uiState.activeTunnels) {
uiState.activeTunnels[tunnel.id] ?: TunnelState()
}
val selected =
remember(sharedState.selectedTunnels) {
sharedState.selectedTunnels.any { it.id == tunnel.id }
remember(uiState.selectedTunnels) {
uiState.selectedTunnels.any { it.id == tunnel.id }
}
var leadingIconColor by
remember(
@@ -97,7 +101,7 @@ fun TunnelList(
},
title = tunnel.name,
onClick = {
if (sharedState.selectedTunnels.isNotEmpty()) {
if (uiState.selectedTunnels.isNotEmpty()) {
viewModel.toggleSelectedTunnel(tunnel.id)
} else {
navController.push(Route.TunnelSettings(tunnel.id))
@@ -111,8 +115,8 @@ fun TunnelList(
TunnelStatisticsRow(
tunnel,
tunnelState,
sharedState.isPingEnabled,
sharedState.showPingStats,
uiState.isPingEnabled,
uiState.showPingStats,
)
}
} else null,

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