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