mirror of
https://github.com/wgtunnel/android.git
synced 2026-07-03 14:07:49 +02:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 01e15099ca | |||
| 8f2fd93e77 | |||
| 94197c9943 | |||
| 39fc9014d8 | |||
| 8860b45230 | |||
| e220b26d88 | |||
| 2302b473b4 | |||
| b01473073f | |||
| 3ae9a24ca4 | |||
| dc3f7fa736 | |||
| 93d6f8aa45 | |||
| 3ea4aea5cf | |||
| 68b41c8925 | |||
| 06de1f24c2 | |||
| a39feeeea6 | |||
| c1619ff012 | |||
| 2534b86005 | |||
| 15c550737c | |||
| e1e7e27bb5 | |||
| 8021c133a5 | |||
| 6009445a15 | |||
| f80af9dd5e | |||
| 3f912ed532 |
@@ -144,8 +144,8 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
implementation(project(":logcatter"))
|
implementation(project(":logcatter"))
|
||||||
|
implementation(project(":networkmonitor"))
|
||||||
|
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
|
|||||||
@@ -3,3 +3,40 @@
|
|||||||
-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.**
|
||||||
|
|
||||||
|
-dontwarn com.google.api.client.http.GenericUrl
|
||||||
|
-dontwarn com.google.api.client.http.HttpHeaders
|
||||||
|
-dontwarn com.google.api.client.http.HttpRequest
|
||||||
|
-dontwarn com.google.api.client.http.HttpRequestFactory
|
||||||
|
-dontwarn com.google.api.client.http.HttpResponse
|
||||||
|
-dontwarn com.google.api.client.http.HttpTransport
|
||||||
|
-dontwarn com.google.api.client.http.javanet.NetHttpTransport$Builder
|
||||||
|
-dontwarn com.google.api.client.http.javanet.NetHttpTransport
|
||||||
|
-dontwarn javax.lang.model.element.Modifier
|
||||||
|
-dontwarn org.joda.time.Instant
|
||||||
|
-dontwarn org.slf4j.impl.StaticLoggerBinder
|
||||||
|
-dontwarn org.slf4j.impl.StaticMDCBinder
|
||||||
|
-dontwarn org.slf4j.impl.StaticMarkerBinder
|
||||||
|
|||||||
Vendored
+37
@@ -22,3 +22,40 @@
|
|||||||
-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.**
|
||||||
|
|
||||||
|
-dontwarn com.google.api.client.http.GenericUrl
|
||||||
|
-dontwarn com.google.api.client.http.HttpHeaders
|
||||||
|
-dontwarn com.google.api.client.http.HttpRequest
|
||||||
|
-dontwarn com.google.api.client.http.HttpRequestFactory
|
||||||
|
-dontwarn com.google.api.client.http.HttpResponse
|
||||||
|
-dontwarn com.google.api.client.http.HttpTransport
|
||||||
|
-dontwarn com.google.api.client.http.javanet.NetHttpTransport$Builder
|
||||||
|
-dontwarn com.google.api.client.http.javanet.NetHttpTransport
|
||||||
|
-dontwarn javax.lang.model.element.Modifier
|
||||||
|
-dontwarn org.joda.time.Instant
|
||||||
|
-dontwarn org.slf4j.impl.StaticLoggerBinder
|
||||||
|
-dontwarn org.slf4j.impl.StaticMDCBinder
|
||||||
|
-dontwarn org.slf4j.impl.StaticMarkerBinder
|
||||||
|
|||||||
@@ -3,13 +3,8 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
|
||||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
|
||||||
<!--foreground service exempt android 14-->
|
<!--foreground service exempt android 14-->
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
|
||||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"
|
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"
|
||||||
@@ -168,22 +163,14 @@
|
|||||||
</service>
|
</service>
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".core.broadcast.BootReceiver"
|
android:name=".core.broadcast.RestartReceiver"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
<action android:name="android.intent.action.ACTION_BOOT_COMPLETED" />
|
|
||||||
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
|
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
|
||||||
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
|
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
<receiver
|
|
||||||
android:name=".core.broadcast.AppUpdateReceiver"
|
|
||||||
android:exported="false">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ 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
|
||||||
@@ -67,7 +66,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
|
||||||
@@ -121,22 +119,13 @@ 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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
@@ -221,16 +210,17 @@ 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 }
|
appUiState.tunnels.firstOrNull { it.id == args.id }?.let { config ->
|
||||||
OptionsScreen(config)
|
OptionsScreen(config, appUiState)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
composable<Route.Lock> {
|
composable<Route.Lock> {
|
||||||
PinLockScreen(viewModel)
|
PinLockScreen(viewModel)
|
||||||
@@ -241,15 +231,17 @@ 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 }
|
appUiState.tunnels.firstOrNull { it.id == args.id }?.let {
|
||||||
SplitTunnelScreen(config, viewModel)
|
SplitTunnelScreen(it, viewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
composable<Route.TunnelAutoTunnel> { backStack ->
|
||||||
|
val args = backStack.toRoute<Route.TunnelOptions>()
|
||||||
|
appUiState.tunnels.firstOrNull { it.id == args.id }?.let {
|
||||||
|
TunnelAutoTunnelScreen(it, appUiState.appSettings)
|
||||||
}
|
}
|
||||||
composable<Route.TunnelAutoTunnel> {
|
|
||||||
val args = it.toRoute<Route.TunnelOptions>()
|
|
||||||
val config = appUiState.tunnels.first { it.id == args.id }
|
|
||||||
TunnelAutoTunnelScreen(config, appUiState.appSettings)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BackHandler {
|
BackHandler {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ 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
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.worker.ServiceWorker
|
||||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||||
import com.zaneschepke.wireguardautotunnel.di.MainDispatcher
|
import com.zaneschepke.wireguardautotunnel.di.MainDispatcher
|
||||||
@@ -91,9 +92,11 @@ class WireGuardAutoTunnel : Application(), Configuration.Provider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ServiceWorker.start(this)
|
||||||
|
|
||||||
applicationScope.launch {
|
applicationScope.launch {
|
||||||
withContext(mainDispatcher) {
|
withContext(mainDispatcher) {
|
||||||
if (appDataRepository.appState.isLocalLogsEnabled() && !isRunningOnTv()) logReader.initialize()
|
if (appDataRepository.appState.isLocalLogsEnabled() && !isRunningOnTv()) logReader.start()
|
||||||
}
|
}
|
||||||
if (!appDataRepository.settings.get().isKernelEnabled) {
|
if (!appDataRepository.settings.get().isKernelEnabled) {
|
||||||
tunnelManager.setBackendState(BackendState.SERVICE_ACTIVE, emptyList())
|
tunnelManager.setBackendState(BackendState.SERVICE_ACTIVE, emptyList())
|
||||||
|
|||||||
-45
@@ -1,45 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.broadcast
|
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class AppUpdateReceiver : BroadcastReceiver() {
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
@ApplicationScope
|
|
||||||
lateinit var applicationScope: CoroutineScope
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var appDataRepository: AppDataRepository
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var serviceManager: ServiceManager
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var tunnelManager: TunnelManager
|
|
||||||
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
if (intent.action != Intent.ACTION_MY_PACKAGE_REPLACED) return
|
|
||||||
serviceManager.updateTunnelTile()
|
|
||||||
serviceManager.updateAutoTunnelTile()
|
|
||||||
applicationScope.launch {
|
|
||||||
with(appDataRepository.settings.get()) {
|
|
||||||
if (isRestoreOnBootEnabled) {
|
|
||||||
// If auto tunnel is enabled, just start it and let auto tunnel start appropriate tun
|
|
||||||
if (isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) return@launch serviceManager.startAutoTunnel(true)
|
|
||||||
tunnelManager.restorePreviousState()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.broadcast
|
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class BootReceiver : BroadcastReceiver() {
|
|
||||||
@Inject
|
|
||||||
lateinit var appDataRepository: AppDataRepository
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
@ApplicationScope
|
|
||||||
lateinit var applicationScope: CoroutineScope
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var serviceManager: ServiceManager
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var tunnelManager: TunnelManager
|
|
||||||
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
if (Intent.ACTION_BOOT_COMPLETED != intent.action) return
|
|
||||||
serviceManager.updateTunnelTile()
|
|
||||||
serviceManager.updateAutoTunnelTile()
|
|
||||||
applicationScope.launch {
|
|
||||||
with(appDataRepository.settings.get()) {
|
|
||||||
if (isRestoreOnBootEnabled) {
|
|
||||||
// If auto tunnel is enabled, just start it and let auto tunnel start appropriate tun
|
|
||||||
if (isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) return@launch serviceManager.startAutoTunnel(true)
|
|
||||||
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))
|
||||||
}
|
}
|
||||||
|
|||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.core.broadcast
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class RestartReceiver : BroadcastReceiver() {
|
||||||
|
@Inject
|
||||||
|
lateinit var appDataRepository: AppDataRepository
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@ApplicationScope
|
||||||
|
lateinit var applicationScope: CoroutineScope
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var serviceManager: ServiceManager
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var tunnelManager: TunnelManager
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@IoDispatcher
|
||||||
|
lateinit var ioDispatcher: CoroutineDispatcher
|
||||||
|
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
val action = intent.action ?: return
|
||||||
|
if (action != Intent.ACTION_BOOT_COMPLETED &&
|
||||||
|
action != Intent.ACTION_MY_PACKAGE_REPLACED &&
|
||||||
|
action != "com.htc.intent.action.QUICKBOOT_POWERON"
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Timber.d("RestartReceiver triggered with action: ${intent.action}")
|
||||||
|
applicationScope.launch(ioDispatcher) {
|
||||||
|
serviceManager.updateTunnelTile()
|
||||||
|
serviceManager.updateAutoTunnelTile()
|
||||||
|
val settings = appDataRepository.settings.get()
|
||||||
|
if (settings.isRestoreOnBootEnabled) {
|
||||||
|
if (settings.isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) {
|
||||||
|
Timber.d("Starting auto-tunnel on boot/update")
|
||||||
|
serviceManager.startAutoTunnel(true)
|
||||||
|
} else {
|
||||||
|
Timber.d("Restoring previous tunnel state")
|
||||||
|
tunnelManager.restorePreviousState()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Timber.d("Restore on boot disabled, skipping")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
-161
@@ -1,161 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.network
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.ConnectivityManager
|
|
||||||
import android.net.Network
|
|
||||||
import android.net.NetworkCapabilities
|
|
||||||
import android.net.NetworkRequest
|
|
||||||
import android.net.wifi.SupplicantState
|
|
||||||
import android.net.wifi.WifiManager
|
|
||||||
import android.os.Build
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.ConnectivityState
|
|
||||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.channels.awaitClose
|
|
||||||
import kotlinx.coroutines.flow.callbackFlow
|
|
||||||
import kotlinx.coroutines.flow.flowOn
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
class InternetConnectivityMonitor
|
|
||||||
@Inject
|
|
||||||
constructor(
|
|
||||||
@ApplicationContext private val context: Context,
|
|
||||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
|
||||||
) : NetworkMonitor {
|
|
||||||
|
|
||||||
private val connectivityManager =
|
|
||||||
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
|
||||||
|
|
||||||
@get:Synchronized @set:Synchronized
|
|
||||||
private var wifiCapabilities: NetworkCapabilities? = null
|
|
||||||
|
|
||||||
@get:Synchronized @set:Synchronized
|
|
||||||
private var wifiNetworkChanged: Boolean = false
|
|
||||||
|
|
||||||
override val didWifiChangeSinceLastCapabilitiesQuery: Boolean
|
|
||||||
get() = wifiNetworkChanged
|
|
||||||
|
|
||||||
override val status = callbackFlow {
|
|
||||||
|
|
||||||
var wifiState: Boolean = false
|
|
||||||
var ethernetState: Boolean = false
|
|
||||||
var cellularState: Boolean = false
|
|
||||||
|
|
||||||
fun emitState() {
|
|
||||||
trySend(ConnectivityState(wifiState, ethernetState, cellularState))
|
|
||||||
}
|
|
||||||
|
|
||||||
val currentNetwork = connectivityManager.activeNetwork
|
|
||||||
if (currentNetwork == null) {
|
|
||||||
emitState()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateCapabilityState(up: Boolean, network: Network) {
|
|
||||||
with(connectivityManager.getNetworkCapabilities(network)) {
|
|
||||||
when {
|
|
||||||
this == null -> return
|
|
||||||
hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> wifiState = up
|
|
||||||
hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ->
|
|
||||||
cellularState = up
|
|
||||||
|
|
||||||
hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) ->
|
|
||||||
ethernetState = up
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onWifiChange(network: Network, callback: () -> Unit) {
|
|
||||||
if (connectivityManager.getNetworkCapabilities(network)?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true) {
|
|
||||||
callback()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onAvailable(network: Network) {
|
|
||||||
onWifiChange(network) {
|
|
||||||
wifiNetworkChanged = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
|
|
||||||
onWifiChange(network) {
|
|
||||||
wifiCapabilities = networkCapabilities
|
|
||||||
}
|
|
||||||
updateCapabilityState(true, network)
|
|
||||||
emitState()
|
|
||||||
}
|
|
||||||
|
|
||||||
val networkStatusCallback =
|
|
||||||
when (Build.VERSION.SDK_INT) {
|
|
||||||
in Build.VERSION_CODES.S..Int.MAX_VALUE -> {
|
|
||||||
object :
|
|
||||||
ConnectivityManager.NetworkCallback(
|
|
||||||
FLAG_INCLUDE_LOCATION_INFO,
|
|
||||||
) {
|
|
||||||
override fun onAvailable(network: Network) {
|
|
||||||
onAvailable(network)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLost(network: Network) {
|
|
||||||
updateCapabilityState(false, network)
|
|
||||||
emitState()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
|
|
||||||
onCapabilitiesChanged(network, networkCapabilities)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
object : ConnectivityManager.NetworkCallback() {
|
|
||||||
override fun onAvailable(network: Network) {
|
|
||||||
onAvailable(network)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLost(network: Network) {
|
|
||||||
updateCapabilityState(false, network)
|
|
||||||
emitState()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
|
|
||||||
onCapabilitiesChanged(network, networkCapabilities)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val request =
|
|
||||||
NetworkRequest.Builder()
|
|
||||||
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
|
|
||||||
.addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
|
|
||||||
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
|
|
||||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
|
||||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
|
||||||
.build()
|
|
||||||
connectivityManager.registerNetworkCallback(request, networkStatusCallback)
|
|
||||||
|
|
||||||
awaitClose { connectivityManager.unregisterNetworkCallback(networkStatusCallback) }
|
|
||||||
}.flowOn(ioDispatcher)
|
|
||||||
|
|
||||||
override fun getWifiCapabilities(): NetworkCapabilities? {
|
|
||||||
wifiNetworkChanged = false
|
|
||||||
return wifiCapabilities
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun getNetworkName(networkCapabilities: NetworkCapabilities, context: Context): String? {
|
|
||||||
var ssid = networkCapabilities.getWifiName()
|
|
||||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S) {
|
|
||||||
val wifiManager =
|
|
||||||
context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
val info = wifiManager.connectionInfo
|
|
||||||
if (info.supplicantState === SupplicantState.COMPLETED) {
|
|
||||||
ssid = info.ssid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ssid?.trim('"')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-16
@@ -1,16 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.network
|
|
||||||
|
|
||||||
import android.net.NetworkCapabilities
|
|
||||||
import android.net.wifi.WifiInfo
|
|
||||||
import android.os.Build
|
|
||||||
|
|
||||||
fun NetworkCapabilities.getWifiName(): String? {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
||||||
val info: WifiInfo
|
|
||||||
if (transportInfo is WifiInfo) {
|
|
||||||
info = transportInfo as WifiInfo
|
|
||||||
return info.ssid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.network
|
|
||||||
|
|
||||||
import android.net.NetworkCapabilities
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.ConnectivityState
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
|
|
||||||
interface NetworkMonitor {
|
|
||||||
val status: Flow<ConnectivityState>
|
|
||||||
|
|
||||||
// util to help limit location queries
|
|
||||||
val didWifiChangeSinceLastCapabilitiesQuery: Boolean
|
|
||||||
fun getWifiCapabilities(): NetworkCapabilities?
|
|
||||||
}
|
|
||||||
+78
-37
@@ -3,29 +3,35 @@ package com.zaneschepke.wireguardautotunnel.core.service
|
|||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelService
|
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelService
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.tile.AutoTunnelControlTile
|
import com.zaneschepke.wireguardautotunnel.core.service.tile.AutoTunnelControlTile
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.tile.TunnelControlTile
|
import com.zaneschepke.wireguardautotunnel.core.service.tile.TunnelControlTile
|
||||||
|
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||||
|
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
|
import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
|
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
|
||||||
import jakarta.inject.Inject
|
import jakarta.inject.Inject
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
class ServiceManager @Inject constructor(
|
||||||
class ServiceManager
|
private val context: Context,
|
||||||
@Inject constructor(private val context: Context, private val ioDispatcher: CoroutineDispatcher, private val appDataRepository: AppDataRepository) {
|
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||||
|
@ApplicationScope private val applicationScope: CoroutineScope,
|
||||||
|
private val appDataRepository: AppDataRepository,
|
||||||
|
) {
|
||||||
|
|
||||||
private val _autoTunnelActive = MutableStateFlow(false)
|
private val _autoTunnelActive = MutableStateFlow(false)
|
||||||
|
|
||||||
val autoTunnelActive = _autoTunnelActive.asStateFlow()
|
val autoTunnelActive = _autoTunnelActive.asStateFlow()
|
||||||
|
|
||||||
var autoTunnelService = CompletableDeferred<AutoTunnelService>()
|
var autoTunnelService = CompletableDeferred<AutoTunnelService>()
|
||||||
@@ -44,76 +50,111 @@ class ServiceManager
|
|||||||
}.onFailure { Timber.e(it) }
|
}.onFailure { Timber.e(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun startAutoTunnel(background: Boolean) {
|
fun startAutoTunnel(background: Boolean) {
|
||||||
|
applicationScope.launch(ioDispatcher) {
|
||||||
val settings = appDataRepository.settings.get()
|
val settings = appDataRepository.settings.get()
|
||||||
appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = true))
|
appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = true))
|
||||||
if (autoTunnelService.isCompleted) return _autoTunnelActive.update { true }
|
if (autoTunnelService.isCompleted) {
|
||||||
|
_autoTunnelActive.update { true }
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
runCatching {
|
runCatching {
|
||||||
|
autoTunnelService = CompletableDeferred()
|
||||||
startService(AutoTunnelService::class.java, background)
|
startService(AutoTunnelService::class.java, background)
|
||||||
autoTunnelService.await()
|
val service = withTimeoutOrNull(SERVICE_START_TIMEOUT) { autoTunnelService.await() }
|
||||||
autoTunnelService.getCompleted().start()
|
?: throw IllegalStateException("AutoTunnelService start timed out")
|
||||||
|
service.start()
|
||||||
_autoTunnelActive.update { true }
|
_autoTunnelActive.update { true }
|
||||||
updateAutoTunnelTile()
|
updateAutoTunnelTile()
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
Timber.e(it)
|
Timber.e(it)
|
||||||
|
_autoTunnelActive.update { false }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun startBackgroundService(tunnelConf: TunnelConf) {
|
fun startBackgroundService(tunnelConf: TunnelConf) {
|
||||||
if (backgroundService.isCompleted) return
|
applicationScope.launch(ioDispatcher) {
|
||||||
|
if (backgroundService.isCompleted) return@launch
|
||||||
runCatching {
|
runCatching {
|
||||||
|
backgroundService = CompletableDeferred()
|
||||||
startService(TunnelForegroundService::class.java, true)
|
startService(TunnelForegroundService::class.java, true)
|
||||||
backgroundService.await()
|
val service = withTimeoutOrNull(SERVICE_START_TIMEOUT) { backgroundService.await() }
|
||||||
backgroundService.getCompleted().start(tunnelConf)
|
?: throw IllegalStateException("Background service start timed out")
|
||||||
|
service.start(tunnelConf)
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
Timber.e(it)
|
Timber.e(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun stopBackgroundService() {
|
fun stopBackgroundService() {
|
||||||
if (!backgroundService.isCompleted) return
|
applicationScope.launch(ioDispatcher) {
|
||||||
|
if (!backgroundService.isCompleted) return@launch
|
||||||
runCatching {
|
runCatching {
|
||||||
backgroundService.getCompleted().stop()
|
val service = backgroundService.await()
|
||||||
|
service.stop()
|
||||||
|
backgroundService = CompletableDeferred()
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
Timber.e(it)
|
Timber.e(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun toggleAutoTunnel(background: Boolean) {
|
fun toggleAutoTunnel(background: Boolean) {
|
||||||
|
applicationScope.launch(ioDispatcher) {
|
||||||
|
if (_autoTunnelActive.value) stopAutoTunnel() else startAutoTunnel(background)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateAutoTunnelTile() {
|
||||||
withContext(ioDispatcher) {
|
withContext(ioDispatcher) {
|
||||||
if (_autoTunnelActive.value) return@withContext stopAutoTunnel()
|
runCatching {
|
||||||
startAutoTunnel(background)
|
val service = withTimeoutOrNull(SERVICE_START_TIMEOUT) { autoTunnelTile.await() }
|
||||||
}
|
?: run {
|
||||||
}
|
|
||||||
|
|
||||||
fun updateAutoTunnelTile() {
|
|
||||||
if (autoTunnelTile.isCompleted) {
|
|
||||||
autoTunnelTile.getCompleted().updateTileState()
|
|
||||||
} else {
|
|
||||||
context.requestAutoTunnelTileServiceUpdate()
|
context.requestAutoTunnelTileServiceUpdate()
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
service.updateTileState()
|
||||||
|
}.onFailure {
|
||||||
|
Timber.e(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateTunnelTile() {
|
suspend fun updateTunnelTile() {
|
||||||
if (tunnelControlTile.isCompleted) {
|
|
||||||
tunnelControlTile.getCompleted().updateTileState()
|
|
||||||
} else {
|
|
||||||
context.requestTunnelTileServiceStateUpdate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun stopAutoTunnel() {
|
|
||||||
withContext(ioDispatcher) {
|
withContext(ioDispatcher) {
|
||||||
|
runCatching {
|
||||||
|
val service = withTimeoutOrNull(SERVICE_START_TIMEOUT) { tunnelControlTile.await() }
|
||||||
|
?: run {
|
||||||
|
context.requestTunnelTileServiceStateUpdate()
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
service.updateTileState()
|
||||||
|
}.onFailure {
|
||||||
|
Timber.e(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stopAutoTunnel() {
|
||||||
|
applicationScope.launch(ioDispatcher) {
|
||||||
val settings = appDataRepository.settings.get()
|
val settings = appDataRepository.settings.get()
|
||||||
appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = false))
|
appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = false))
|
||||||
if (!autoTunnelService.isCompleted) return@withContext
|
if (!autoTunnelService.isCompleted) return@launch
|
||||||
runCatching {
|
runCatching {
|
||||||
autoTunnelService.getCompleted().stop()
|
val service = autoTunnelService.await()
|
||||||
|
service.stop()
|
||||||
_autoTunnelActive.update { false }
|
_autoTunnelActive.update { false }
|
||||||
|
autoTunnelService = CompletableDeferred()
|
||||||
updateAutoTunnelTile()
|
updateAutoTunnelTile()
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
Timber.e(it)
|
Timber.e(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val SERVICE_START_TIMEOUT = 5_000L
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-2
@@ -35,9 +35,8 @@ class TunnelForegroundService : LifecycleService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
super.onStartCommand(intent, flags, startId)
|
|
||||||
serviceManager.backgroundService.complete(this)
|
serviceManager.backgroundService.complete(this)
|
||||||
return START_NOT_STICKY
|
return super.onStartCommand(intent, flags, startId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun start(tunnelConf: TunnelConf) {
|
fun start(tunnelConf: TunnelConf) {
|
||||||
|
|||||||
+43
-45
@@ -1,44 +1,42 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.service.autotunnel
|
package com.zaneschepke.wireguardautotunnel.core.service.autotunnel
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.NetworkCapabilities
|
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import androidx.core.app.ServiceCompat
|
import androidx.core.app.ServiceCompat
|
||||||
import androidx.lifecycle.LifecycleService
|
import androidx.lifecycle.LifecycleService
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.wireguard.android.util.RootShell
|
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||||
|
import com.zaneschepke.networkmonitor.NetworkStatus
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.di.AppShell
|
|
||||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
|
||||||
import com.zaneschepke.wireguardautotunnel.di.MainImmediateDispatcher
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.AutoTunnelState
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.NetworkState
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.network.InternetConnectivityMonitor
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.network.NetworkMonitor
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.ConnectivityState
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
|
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||||
|
import com.zaneschepke.wireguardautotunnel.di.MainImmediateDispatcher
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
|
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
|
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.events.KillSwitchEvent
|
import com.zaneschepke.wireguardautotunnel.domain.events.KillSwitchEvent
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.state.AutoTunnelState
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.state.NetworkState
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.Tunnels
|
import com.zaneschepke.wireguardautotunnel.util.extensions.Tunnels
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.getCurrentWifiName
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.debounce
|
import kotlinx.coroutines.flow.debounce
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -49,10 +47,6 @@ import javax.inject.Provider
|
|||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class AutoTunnelService : LifecycleService() {
|
class AutoTunnelService : LifecycleService() {
|
||||||
|
|
||||||
@Inject
|
|
||||||
@AppShell
|
|
||||||
lateinit var rootShell: Provider<RootShell>
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var networkMonitor: NetworkMonitor
|
lateinit var networkMonitor: NetworkMonitor
|
||||||
|
|
||||||
@@ -100,10 +94,9 @@ class AutoTunnelService : LifecycleService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
super.onStartCommand(intent, flags, startId)
|
|
||||||
Timber.d("onStartCommand executed with startId: $startId")
|
Timber.d("onStartCommand executed with startId: $startId")
|
||||||
serviceManager.autoTunnelService.complete(this)
|
serviceManager.autoTunnelService.complete(this)
|
||||||
return START_NOT_STICKY
|
return super.onStartCommand(intent, flags, startId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun start() {
|
fun start() {
|
||||||
@@ -162,49 +155,54 @@ class AutoTunnelService : LifecycleService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun buildNetworkState(connectivityState: ConnectivityState): NetworkState {
|
private fun buildNetworkState(networkStatus: NetworkStatus): NetworkState {
|
||||||
return with(autoTunnelStateFlow.value.networkState) {
|
return with(autoTunnelStateFlow.value.networkState) {
|
||||||
val wifiName = when {
|
val wifiName = when (networkStatus) {
|
||||||
connectivityState.wifiAvailable &&
|
is NetworkStatus.Connected -> {
|
||||||
(wifiName == null || wifiName == Constants.UNREADABLE_SSID || networkMonitor.didWifiChangeSinceLastCapabilitiesQuery) -> {
|
networkStatus.wifiSsid
|
||||||
networkMonitor.getWifiCapabilities()?.let { getWifiName(it) } ?: wifiName
|
|
||||||
}
|
}
|
||||||
!connectivityState.wifiAvailable -> null
|
else -> null
|
||||||
else -> wifiName
|
|
||||||
}
|
}
|
||||||
copy(
|
copy(
|
||||||
isWifiConnected = connectivityState.wifiAvailable,
|
isWifiConnected = networkStatus.wifiConnected,
|
||||||
isMobileDataConnected = connectivityState.cellularAvailable,
|
isMobileDataConnected = networkStatus.cellularConnected,
|
||||||
isEthernetConnected = isEthernetConnected,
|
isEthernetConnected = networkStatus.ethernetConnected,
|
||||||
wifiName = wifiName,
|
wifiName = wifiName,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
private fun startAutoTunnelStateJob() = lifecycleScope.launch(ioDispatcher) {
|
private fun startAutoTunnelStateJob() = lifecycleScope.launch(ioDispatcher) {
|
||||||
combine(
|
combine(
|
||||||
combineSettings(),
|
combineSettings(),
|
||||||
networkMonitor.status.map {
|
appDataRepository.get().settings.flow
|
||||||
buildNetworkState(it)
|
.distinctUntilChanged { old, new -> old.isKernelEnabled == new.isKernelEnabled } // Only emit when isKernelEnabled changes
|
||||||
}.distinctUntilChanged(),
|
.flatMapLatest { settings ->
|
||||||
|
networkMonitor.getNetworkStatusFlow(true, settings.isKernelEnabled)
|
||||||
|
.flowOn(ioDispatcher)
|
||||||
|
.map { buildNetworkState(it) }
|
||||||
|
}
|
||||||
|
.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,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getWifiName(wifiCapabilities: NetworkCapabilities): String? {
|
|
||||||
val setting = appDataRepository.get().settings.get()
|
|
||||||
return if (setting.isWifiNameByShellEnabled) {
|
|
||||||
rootShell.get().getCurrentWifiName()
|
|
||||||
} else {
|
|
||||||
InternetConnectivityMonitor.getNetworkName(wifiCapabilities, this@AutoTunnelService)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun combineSettings(): Flow<Pair<AppSettings, Tunnels>> {
|
private fun combineSettings(): Flow<Pair<AppSettings, Tunnels>> {
|
||||||
return combine(
|
return combine(
|
||||||
appDataRepository.get().settings.flow,
|
appDataRepository.get().settings.flow,
|
||||||
@@ -241,7 +239,7 @@ class AutoTunnelService : LifecycleService() {
|
|||||||
Timber.d("Starting with debounce delay of: ${settings.debounceDelaySeconds} seconds")
|
Timber.d("Starting with debounce delay of: ${settings.debounceDelaySeconds} seconds")
|
||||||
autoTunnelStateFlow.debounce(settings.debounceDelayMillis()).collect { watcherState ->
|
autoTunnelStateFlow.debounce(settings.debounceDelayMillis()).collect { watcherState ->
|
||||||
if (watcherState == defaultState) return@collect
|
if (watcherState == defaultState) return@collect
|
||||||
Timber.d("New auto tunnel state emitted")
|
Timber.d("New auto tunnel state emitted ${watcherState.networkState}")
|
||||||
when (val event = watcherState.asAutoTunnelEvent()) {
|
when (val event = watcherState.asAutoTunnelEvent()) {
|
||||||
is AutoTunnelEvent.Start -> (event.tunnelConf ?: appDataRepository.get().getPrimaryOrFirstTunnel())?.let {
|
is AutoTunnelEvent.Start -> (event.tunnelConf ?: appDataRepository.get().getPrimaryOrFirstTunnel())?.let {
|
||||||
tunnelManager.startTunnel(it)
|
tunnelManager.startTunnel(it)
|
||||||
|
|||||||
+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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||||
|
|
||||||
|
import com.wireguard.android.backend.Tunnel
|
||||||
|
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||||
|
import com.zaneschepke.networkmonitor.NetworkStatus
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.core.network.NetworkMonitor
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
|
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||||
@@ -14,10 +16,12 @@ 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
|
||||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.cancelWithMessage
|
import com.zaneschepke.wireguardautotunnel.util.extensions.cancelWithMessage
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -27,11 +31,12 @@ import kotlinx.coroutines.delay
|
|||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
open class BaseTunnel(
|
open class BaseTunnel(
|
||||||
@@ -45,93 +50,112 @@ open class BaseTunnel(
|
|||||||
|
|
||||||
internal val tunnels = MutableStateFlow<List<TunnelConf>>(emptyList())
|
internal val tunnels = MutableStateFlow<List<TunnelConf>>(emptyList())
|
||||||
|
|
||||||
private val tunnelJobs = mutableMapOf<TunnelConf, Job>()
|
private val _tunnelStates = MutableStateFlow<Map<Int, TunnelState>>(emptyMap())
|
||||||
|
|
||||||
|
private val tunnelJobs = ConcurrentHashMap<Int, Job>()
|
||||||
|
|
||||||
private val isNetworkAvailable = AtomicBoolean(false)
|
private val isNetworkAvailable = AtomicBoolean(false)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
applicationScope.launch(ioDispatcher) {
|
applicationScope.launch(ioDispatcher) {
|
||||||
launch {
|
launch { startNetworkJob() }
|
||||||
startNetworkJob()
|
launch { monitorTunnelConfigChanges() }
|
||||||
}
|
|
||||||
tunnels.collect { tuns ->
|
tunnels.collect { tuns ->
|
||||||
val previousTuns = tunnelJobs.keys.toSet()
|
val previousTunIds = tunnelJobs.keys.toSet()
|
||||||
val newTuns = tuns - previousTuns
|
val currentTunIds = tuns.map { it.id }.toSet()
|
||||||
val removedItems = previousTuns - tuns.toSet()
|
val newTuns = tuns.filter { it.id !in previousTunIds }
|
||||||
|
val removedTunIds = previousTunIds - currentTunIds
|
||||||
|
|
||||||
newTuns.forEach { tun ->
|
newTuns.forEach { tun ->
|
||||||
Timber.d("Starting tunnel jobs for tun ${tun.name}")
|
Timber.d("Starting tunnel jobs for tun ${tun.name} (ID: ${tun.id})")
|
||||||
tunnelJobs[tun] = startTunnelJobs(tun)
|
tunnelJobs[tun.id] = startTunnelJobs(tun)
|
||||||
}
|
}
|
||||||
|
|
||||||
removedItems.forEach { tun ->
|
removedTunIds.forEach { tunId ->
|
||||||
tunnelJobs[tun]?.cancelWithMessage("Canceling tunnel jobs for tunnel: ${tun.name}")
|
tunnelJobs[tunId]?.cancelWithMessage("Canceling tunnel jobs for tunnel ID: $tunId")
|
||||||
tunnelJobs.remove(tun)
|
tunnelJobs.remove(tunId)
|
||||||
}
|
_tunnelStates.update { it - tunId }
|
||||||
serviceManager.updateTunnelTile()
|
serviceManager.updateTunnelTile()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun startTunnelJobs(tunnel: TunnelConf) = applicationScope.launch(ioDispatcher) {
|
private fun startTunnelJobs(tunnel: TunnelConf) = applicationScope.launch(ioDispatcher) {
|
||||||
launch {
|
launch { startTunnelStatisticsJob(tunnel) }
|
||||||
startTunnelStatisticsJob(tunnel)
|
if (tunnel.isPingEnabled) launch { startPingJob(tunnel) }
|
||||||
}
|
}
|
||||||
launch {
|
|
||||||
startPingJob(tunnel)
|
private fun updateTunnelState(tunnelId: Int, newState: TunnelStatus) {
|
||||||
|
Timber.d("Updating tunnel state for ID $tunnelId to $newState")
|
||||||
|
_tunnelStates.update { current ->
|
||||||
|
val currentState = current[tunnelId]
|
||||||
|
val updatedState = currentState?.copy(state = newState) ?: TunnelState(state = newState)
|
||||||
|
val newMap = current + (tunnelId to updatedState)
|
||||||
|
Timber.d("New tunnel states: $newMap")
|
||||||
|
newMap
|
||||||
}
|
}
|
||||||
launch {
|
|
||||||
startTunnelConfigChangeJob(tunnel)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun beforeStartTunnel(tunnelConf: TunnelConf) {
|
||||||
|
tunnelConf.setStateChangeCallback { state ->
|
||||||
|
Timber.d("New tunnel state $state")
|
||||||
|
when (state) {
|
||||||
|
is Tunnel.State -> updateTunnelState(tunnelConf.id, state.asTunnelState())
|
||||||
|
is org.amnezia.awg.backend.Tunnel.State -> updateTunnelState(tunnelConf.id, state.asTunnelState())
|
||||||
|
}
|
||||||
|
applicationScope.launch(ioDispatcher) {
|
||||||
|
serviceManager.updateTunnelTile()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun startTunnel(tunnelConf: TunnelConf) {
|
||||||
|
applicationScope.launch(ioDispatcher) {
|
||||||
|
serviceManager.startBackgroundService(tunnelConf)
|
||||||
|
appDataRepository.tunnels.save(tunnelConf.copy(isActive = true))
|
||||||
|
addToActiveTunnels(tunnelConf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stopTunnel(tunnelConf: TunnelConf?) {
|
||||||
|
// Default empty implementation; subclasses override
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun bounceTunnel(tunnelConf: TunnelConf) {
|
override suspend fun bounceTunnel(tunnelConf: TunnelConf) {
|
||||||
if (tunnels.value.any { it.id == tunnelConf.id }) {
|
stopTunnel(tunnelConf)
|
||||||
toggleTunnel(tunnelConf, TunnelStatus.DOWN)
|
delay(1000)
|
||||||
toggleTunnel(tunnelConf, TunnelStatus.UP)
|
startTunnel(tunnelConf)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
|
override suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
|
||||||
|
// Default empty implementation
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun runningTunnelNames(): Set<String> {
|
override suspend fun runningTunnelNames(): Set<String> {
|
||||||
return emptySet()
|
return emptySet()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun activeTunnels(): StateFlow<List<TunnelConf>> {
|
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics {
|
||||||
return tunnels.asStateFlow()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun startTunnel(tunnelConf: TunnelConf) {
|
|
||||||
if (tunnels.value.any { it.id == tunnelConf.id }) return Timber.w("Tunnel already running")
|
|
||||||
serviceManager.startBackgroundService(tunnelConf)
|
|
||||||
appDataRepository.tunnels.save(tunnelConf.copy(isActive = true))
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun stopTunnel(tunnelConf: TunnelConf?) {
|
|
||||||
}
|
|
||||||
|
|
||||||
open suspend fun toggleTunnel(tunnelConf: TunnelConf, state: TunnelStatus) {
|
|
||||||
}
|
|
||||||
|
|
||||||
open suspend fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics {
|
|
||||||
throw NotImplementedError("Get statistics not implemented in base class")
|
throw NotImplementedError("Get statistics not implemented in base class")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override val activeTunnels: StateFlow<Map<Int, TunnelState>>
|
||||||
|
get() = _tunnelStates.asStateFlow()
|
||||||
|
|
||||||
internal suspend fun onTunnelStop(tunnelConf: TunnelConf) {
|
internal suspend fun onTunnelStop(tunnelConf: TunnelConf) {
|
||||||
appDataRepository.tunnels.save(tunnelConf.copy(isActive = false))
|
appDataRepository.tunnels.save(tunnelConf.copy(isActive = false))
|
||||||
removeFromActiveTunnels(tunnelConf)
|
removeFromActiveTunnels(tunnelConf)
|
||||||
if (tunnels.value.isEmpty()) serviceManager.stopBackgroundService()
|
if (tunnels.value.isEmpty()) serviceManager.stopBackgroundService()
|
||||||
}
|
}
|
||||||
|
|
||||||
internal suspend fun stopAllTunnels() {
|
internal fun stopAllTunnels() {
|
||||||
tunnels.value.forEach {
|
tunnels.value.forEach {
|
||||||
stopTunnel(it)
|
stopTunnel(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun addToActiveTunnels(conf: TunnelConf) {
|
private fun addToActiveTunnels(conf: TunnelConf) {
|
||||||
tunnels.update {
|
tunnels.update {
|
||||||
it.toMutableList().apply {
|
it.toMutableList().apply {
|
||||||
add(conf)
|
add(conf)
|
||||||
@@ -148,23 +172,26 @@ open class BaseTunnel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun startNetworkJob() = coroutineScope {
|
private suspend fun startNetworkJob() = coroutineScope {
|
||||||
networkMonitor.status.distinctUntilChanged().collect {
|
networkMonitor.getNetworkStatusFlow(includeWifiSsid = false, useRootShell = false)
|
||||||
isNetworkAvailable.set(!it.allOffline)
|
.flowOn(ioDispatcher).collect {
|
||||||
|
isNetworkAvailable.set(it !is NetworkStatus.Disconnected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun startPingJob(tunnel: TunnelConf) = coroutineScope {
|
private suspend fun startPingJob(tunnel: TunnelConf) = coroutineScope {
|
||||||
while (isActive) {
|
while (isActive) {
|
||||||
|
runCatching {
|
||||||
if (isNetworkAvailable.get() && tunnel.isActive) {
|
if (isNetworkAvailable.get() && tunnel.isActive) {
|
||||||
val pingResult = tunnel.pingTunnel(ioDispatcher)
|
val pingSuccess = tunnel.isTunnelPingable(ioDispatcher)
|
||||||
handlePingResult(tunnel, pingResult)
|
handlePingResult(tunnel, pingSuccess)
|
||||||
|
}
|
||||||
|
delay(tunnel.pingInterval ?: Constants.PING_INTERVAL)
|
||||||
}
|
}
|
||||||
delay(CHECK_INTERVAL)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun handlePingResult(tunnel: TunnelConf, pingResult: List<Boolean>) {
|
private suspend fun handlePingResult(tunnel: TunnelConf, pingSuccess: Boolean) {
|
||||||
if (pingResult.contains(false)) {
|
if (!pingSuccess) {
|
||||||
if (isNetworkAvailable.get()) {
|
if (isNetworkAvailable.get()) {
|
||||||
Timber.i("Ping result: target was not reachable, bouncing the tunnel")
|
Timber.i("Ping result: target was not reachable, bouncing the tunnel")
|
||||||
bounceTunnel(tunnel)
|
bounceTunnel(tunnel)
|
||||||
@@ -197,23 +224,33 @@ open class BaseTunnel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun startTunnelConfigChangeJob(tunnel: TunnelConf) = coroutineScope {
|
private suspend fun monitorTunnelConfigChanges() = coroutineScope {
|
||||||
appDataRepository.tunnels.flow.collect { storageTuns ->
|
appDataRepository.tunnels.flow.collect { storageTuns ->
|
||||||
storageTuns.firstOrNull { it.id == tunnel.id }?.let { storageTun ->
|
storageTuns.forEach { storageTun ->
|
||||||
if (tunnel.isQuickConfigChanged(storageTun) || tunnel.isPingConfigMatching(storageTun)) {
|
val currentTun = tunnels.value.firstOrNull { it.id == storageTun.id }
|
||||||
bounceTunnel(tunnel)
|
if (currentTun != null) {
|
||||||
|
if (!currentTun.isQuickConfigMatching(storageTun)) {
|
||||||
|
Timber.d("Tunnel config changed for ID $storageTun, bouncing tunnel")
|
||||||
|
bounceTunnel(storageTun)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun startTunnelStatisticsJob(tunnel: TunnelConf) = coroutineScope {
|
private suspend fun startTunnelStatisticsJob(tunnel: TunnelConf) = coroutineScope {
|
||||||
while (isActive) {
|
while (this.isActive) {
|
||||||
|
runCatching {
|
||||||
val stats = getStatistics(tunnel)
|
val stats = getStatistics(tunnel)
|
||||||
tunnel.state.update {
|
_tunnelStates.update { currentStates ->
|
||||||
it.copy(statistics = stats)
|
val updatedState = currentStates[tunnel.id]?.copy(statistics = stats)
|
||||||
|
?: TunnelState(statistics = stats)
|
||||||
|
currentStates + (tunnel.id to updatedState)
|
||||||
}
|
}
|
||||||
delay(CHECK_INTERVAL)
|
delay(CHECK_INTERVAL)
|
||||||
|
}.onFailure { exception ->
|
||||||
|
Timber.e(exception, "Failed to update tunnel statistics for ${tunnel.tunName}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,21 +3,20 @@ package com.zaneschepke.wireguardautotunnel.core.tunnel
|
|||||||
import com.wireguard.android.backend.Backend
|
import com.wireguard.android.backend.Backend
|
||||||
import com.wireguard.android.backend.BackendException
|
import com.wireguard.android.backend.BackendException
|
||||||
import com.wireguard.android.backend.Tunnel
|
import com.wireguard.android.backend.Tunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.core.network.NetworkMonitor
|
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||||
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.enums.TunnelStatus
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.WireGuardStatistics
|
import com.zaneschepke.wireguardautotunnel.domain.state.WireGuardStatistics
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendError
|
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendError
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -31,13 +30,18 @@ class KernelTunnel @Inject constructor(
|
|||||||
networkMonitor: NetworkMonitor,
|
networkMonitor: NetworkMonitor,
|
||||||
) : BaseTunnel(ioDispatcher, applicationScope, networkMonitor, appDataRepository, serviceManager, notificationManager) {
|
) : BaseTunnel(ioDispatcher, applicationScope, networkMonitor, appDataRepository, serviceManager, notificationManager) {
|
||||||
|
|
||||||
override suspend fun startTunnel(tunnelConf: TunnelConf) {
|
override fun startTunnel(tunnelConf: TunnelConf) {
|
||||||
withContext(ioDispatcher) {
|
Timber.d("Starting tunnel ${tunnelConf.id} kernel")
|
||||||
super.startTunnel(tunnelConf)
|
applicationScope.launch(ioDispatcher) {
|
||||||
|
if (tunnels.value.any { it.id == tunnelConf.id }) return@launch Timber.w("Tunnel already running")
|
||||||
runCatching {
|
runCatching {
|
||||||
|
Timber.d("Setting backend state UP")
|
||||||
|
super.beforeStartTunnel(tunnelConf)
|
||||||
backend.setState(tunnelConf, Tunnel.State.UP, tunnelConf.toWgConfig())
|
backend.setState(tunnelConf, Tunnel.State.UP, tunnelConf.toWgConfig())
|
||||||
addToActiveTunnels(tunnelConf)
|
Timber.d("Calling super.startTunnel")
|
||||||
|
super.startTunnel(tunnelConf)
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
|
Timber.e(it, "Failed to start tunnel ${tunnelConf.id} kernel")
|
||||||
onTunnelStop(tunnelConf)
|
onTunnelStop(tunnelConf)
|
||||||
if (it is BackendException) {
|
if (it is BackendException) {
|
||||||
handleBackendThrowable(it.toBackendError())
|
handleBackendThrowable(it.toBackendError())
|
||||||
@@ -48,15 +52,14 @@ class KernelTunnel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics {
|
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics {
|
||||||
return WireGuardStatistics(backend.getStatistics(tunnelConf))
|
return WireGuardStatistics(backend.getStatistics(tunnelConf))
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun stopTunnel(tunnelConf: TunnelConf?) {
|
override fun stopTunnel(tunnelConf: TunnelConf?) {
|
||||||
withContext(ioDispatcher) {
|
applicationScope.launch(ioDispatcher) {
|
||||||
val tunnel = tunnels.value.firstOrNull { it.id == tunnelConf?.id }
|
|
||||||
runCatching {
|
runCatching {
|
||||||
tunnel?.let {
|
tunnels.value.firstOrNull { it.id == tunnelConf?.id }?.let {
|
||||||
backend.setState(it, Tunnel.State.DOWN, it.toWgConfig())
|
backend.setState(it, Tunnel.State.DOWN, it.toWgConfig())
|
||||||
onTunnelStop(it)
|
onTunnelStop(it)
|
||||||
} ?: stopAllTunnels()
|
} ?: stopAllTunnels()
|
||||||
@@ -66,13 +69,6 @@ class KernelTunnel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun toggleTunnel(tunnelConf: TunnelConf, status: TunnelStatus) {
|
|
||||||
when (status) {
|
|
||||||
TunnelStatus.UP -> backend.setState(tunnelConf, Tunnel.State.UP, tunnelConf.toWgConfig())
|
|
||||||
TunnelStatus.DOWN -> backend.setState(tunnelConf, Tunnel.State.DOWN, tunnelConf.toWgConfig())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
|
override suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
|
||||||
Timber.w("Not yet implemented for kernel")
|
Timber.w("Not yet implemented for kernel")
|
||||||
}
|
}
|
||||||
|
|||||||
+46
-48
@@ -4,20 +4,20 @@ import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
|||||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||||
import com.zaneschepke.wireguardautotunnel.di.Kernel
|
import com.zaneschepke.wireguardautotunnel.di.Kernel
|
||||||
import com.zaneschepke.wireguardautotunnel.di.Userspace
|
import com.zaneschepke.wireguardautotunnel.di.Userspace
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
|
|
||||||
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.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.withData
|
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.plus
|
import kotlinx.coroutines.plus
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class TunnelManager @Inject constructor(
|
class TunnelManager @Inject constructor(
|
||||||
@@ -28,71 +28,69 @@ class TunnelManager @Inject constructor(
|
|||||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||||
) : TunnelProvider {
|
) : TunnelProvider {
|
||||||
|
|
||||||
val appSettings: StateFlow<AppSettings?> = appDataRepository.settings.flow.stateIn(
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
private val tunnelProviderFlow = appDataRepository.settings.flow
|
||||||
|
.filterNotNull()
|
||||||
|
.flatMapLatest { settings ->
|
||||||
|
MutableStateFlow(if (settings.isKernelEnabled) kernelTunnel else userspaceTunnel)
|
||||||
|
}
|
||||||
|
.stateIn(
|
||||||
scope = applicationScope.plus(ioDispatcher),
|
scope = applicationScope.plus(ioDispatcher),
|
||||||
started = SharingStarted.Eagerly,
|
started = SharingStarted.Eagerly,
|
||||||
initialValue = null,
|
initialValue = userspaceTunnel,
|
||||||
)
|
)
|
||||||
|
|
||||||
override suspend fun activeTunnels(): StateFlow<List<TunnelConf>> {
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
return withContext(ioDispatcher) {
|
override val activeTunnels = appDataRepository.settings.flow
|
||||||
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.plus(ioDispatcher),
|
||||||
|
started = SharingStarted.Eagerly,
|
||||||
|
initialValue = emptyMap(),
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun startTunnel(tunnelConf: TunnelConf) {
|
||||||
|
tunnelProviderFlow.value.startTunnel(tunnelConf)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun startTunnel(tunnelConf: TunnelConf) {
|
override fun stopTunnel(tunnelConf: TunnelConf?) {
|
||||||
appSettings.withData {
|
tunnelProviderFlow.value.stopTunnel(tunnelConf)
|
||||||
if (it.isKernelEnabled) return@withData kernelTunnel.startTunnel(tunnelConf)
|
|
||||||
userspaceTunnel.startTunnel(tunnelConf)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun stopTunnel(tunnelConf: TunnelConf?) {
|
|
||||||
appSettings.withData {
|
|
||||||
if (it.isKernelEnabled) return@withData kernelTunnel.stopTunnel(tunnelConf)
|
|
||||||
userspaceTunnel.stopTunnel(tunnelConf)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun bounceTunnel(tunnelConf: TunnelConf) {
|
override suspend fun bounceTunnel(tunnelConf: TunnelConf) {
|
||||||
appSettings.withData {
|
tunnelProviderFlow.value.bounceTunnel(tunnelConf)
|
||||||
if (it.isKernelEnabled) return@withData kernelTunnel.stopTunnel(tunnelConf)
|
|
||||||
userspaceTunnel.stopTunnel(tunnelConf)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
|
override suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
|
||||||
appSettings.withData {
|
tunnelProviderFlow.value.setBackendState(backendState, allowedIps)
|
||||||
if (it.isKernelEnabled) return@withData kernelTunnel.setBackendState(backendState, allowedIps)
|
|
||||||
userspaceTunnel.setBackendState(backendState, allowedIps)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun runningTunnelNames(): Set<String> {
|
override suspend fun runningTunnelNames(): Set<String> {
|
||||||
appSettings.filterNotNull().first().let {
|
return tunnelProviderFlow.value.runningTunnelNames()
|
||||||
if (it.isKernelEnabled) return kernelTunnel.runningTunnelNames()
|
|
||||||
return userspaceTunnel.runningTunnelNames()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun restorePreviousState() {
|
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics {
|
||||||
withContext(ioDispatcher) {
|
return tunnelProviderFlow.value.getStatistics(tunnelConf)
|
||||||
with(appDataRepository.settings.get()) {
|
}
|
||||||
if (isRestoreOnBootEnabled) {
|
|
||||||
|
fun restorePreviousState() = applicationScope.launch(ioDispatcher) {
|
||||||
|
val settings = appDataRepository.settings.get()
|
||||||
|
if (settings.isRestoreOnBootEnabled) {
|
||||||
val previouslyActiveTuns = appDataRepository.tunnels.getActive()
|
val previouslyActiveTuns = appDataRepository.tunnels.getActive()
|
||||||
// handle kernel mode
|
val tunsToStart = previouslyActiveTuns.filterNot { tun -> activeTunnels.value.any { tun.id == it.key } }
|
||||||
val tunsToStart = previouslyActiveTuns.filterNot { tun -> activeTunnels().value.any { tun.id == it.id } }
|
if (settings.isKernelEnabled) {
|
||||||
if (isKernelEnabled) {
|
return@launch tunsToStart.forEach {
|
||||||
return@withContext tunsToStart.forEach {
|
|
||||||
startTunnel(it)
|
startTunnel(it)
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
// handle userspace
|
tunsToStart.firstOrNull()?.let { startTunnel(it) }
|
||||||
if (activeTunnels().value.isEmpty()) tunsToStart.firstOrNull()?.let { startTunnel(it) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-14
@@ -2,23 +2,20 @@ 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 com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
interface TunnelProvider {
|
interface TunnelProvider {
|
||||||
|
|
||||||
suspend fun activeTunnels(): StateFlow<List<TunnelConf>>
|
|
||||||
|
|
||||||
suspend fun startTunnel(tunnelConf: TunnelConf)
|
|
||||||
|
|
||||||
suspend fun stopTunnel(tunnelConf: TunnelConf? = null)
|
|
||||||
|
|
||||||
suspend fun bounceTunnel(tunnelConf: TunnelConf)
|
|
||||||
|
|
||||||
suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>)
|
|
||||||
|
|
||||||
suspend fun runningTunnelNames(): Set<String>
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val CHECK_INTERVAL = 1_000L
|
const val CHECK_INTERVAL = 1000L
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun startTunnel(tunnelConf: TunnelConf)
|
||||||
|
fun stopTunnel(tunnelConf: TunnelConf? = null)
|
||||||
|
suspend fun bounceTunnel(tunnelConf: TunnelConf)
|
||||||
|
suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>)
|
||||||
|
suspend fun runningTunnelNames(): Set<String>
|
||||||
|
fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics
|
||||||
|
val activeTunnels: StateFlow<Map<Int, TunnelState>>
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-21
@@ -1,14 +1,13 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||||
|
|
||||||
import com.wireguard.android.backend.BackendException
|
import com.wireguard.android.backend.BackendException
|
||||||
import com.zaneschepke.wireguardautotunnel.core.network.NetworkMonitor
|
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||||
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.enums.TunnelStatus
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.AmneziaStatistics
|
import com.zaneschepke.wireguardautotunnel.domain.state.AmneziaStatistics
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
||||||
@@ -16,7 +15,7 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.asAmBackendState
|
|||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendError
|
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendError
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.launch
|
||||||
import org.amnezia.awg.backend.Backend
|
import org.amnezia.awg.backend.Backend
|
||||||
import org.amnezia.awg.backend.Tunnel
|
import org.amnezia.awg.backend.Tunnel
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
@@ -30,15 +29,24 @@ class UserspaceTunnel @Inject constructor(
|
|||||||
notificationManager: NotificationManager,
|
notificationManager: NotificationManager,
|
||||||
private val backend: Backend,
|
private val backend: Backend,
|
||||||
networkMonitor: NetworkMonitor,
|
networkMonitor: NetworkMonitor,
|
||||||
) : TunnelProvider, BaseTunnel(ioDispatcher, applicationScope, networkMonitor, appDataRepository, serviceManager, notificationManager) {
|
) : BaseTunnel(ioDispatcher, applicationScope, networkMonitor, appDataRepository, serviceManager, notificationManager) {
|
||||||
|
|
||||||
override suspend fun startTunnel(tunnelConf: TunnelConf) {
|
override fun startTunnel(tunnelConf: TunnelConf) {
|
||||||
withContext(ioDispatcher) {
|
applicationScope.launch(ioDispatcher) {
|
||||||
super.startTunnel(tunnelConf)
|
Timber.d("Starting tunnel ${tunnelConf.id} userspace")
|
||||||
|
if (tunnels.value.any { it.id == tunnelConf.id }) return@launch Timber.w("Tunnel already running")
|
||||||
|
if (tunnels.value.isNotEmpty()) {
|
||||||
|
Timber.d("Stopping all tunnels")
|
||||||
|
stopAllTunnels()
|
||||||
|
}
|
||||||
runCatching {
|
runCatching {
|
||||||
|
Timber.d("Setting backend state UP")
|
||||||
|
super.beforeStartTunnel(tunnelConf)
|
||||||
backend.setState(tunnelConf, Tunnel.State.UP, tunnelConf.toAmConfig())
|
backend.setState(tunnelConf, Tunnel.State.UP, tunnelConf.toAmConfig())
|
||||||
addToActiveTunnels(tunnelConf)
|
Timber.d("Calling super.startTunnel")
|
||||||
|
super.startTunnel(tunnelConf)
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
|
Timber.e(it, "Failed to start tunnel ${tunnelConf.id} userspace")
|
||||||
onTunnelStop(tunnelConf)
|
onTunnelStop(tunnelConf)
|
||||||
if (it is BackendException) {
|
if (it is BackendException) {
|
||||||
handleBackendThrowable(it.toBackendError())
|
handleBackendThrowable(it.toBackendError())
|
||||||
@@ -49,19 +57,8 @@ class UserspaceTunnel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics {
|
override fun stopTunnel(tunnelConf: TunnelConf?) {
|
||||||
return AmneziaStatistics(backend.getStatistics(tunnelConf))
|
applicationScope.launch(ioDispatcher) {
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun toggleTunnel(tunnelConf: TunnelConf, status: TunnelStatus) {
|
|
||||||
when (status) {
|
|
||||||
TunnelStatus.UP -> backend.setState(tunnelConf, Tunnel.State.UP, tunnelConf.toAmConfig())
|
|
||||||
TunnelStatus.DOWN -> backend.setState(tunnelConf, Tunnel.State.DOWN, tunnelConf.toAmConfig())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun stopTunnel(tunnelConf: TunnelConf?) {
|
|
||||||
withContext(ioDispatcher) {
|
|
||||||
runCatching {
|
runCatching {
|
||||||
tunnels.value.firstOrNull { it.id == tunnelConf?.id }?.let {
|
tunnels.value.firstOrNull { it.id == tunnelConf?.id }?.let {
|
||||||
backend.setState(it, Tunnel.State.DOWN, it.toAmConfig())
|
backend.setState(it, Tunnel.State.DOWN, it.toAmConfig())
|
||||||
@@ -80,4 +77,8 @@ class UserspaceTunnel @Inject constructor(
|
|||||||
override suspend fun runningTunnelNames(): Set<String> {
|
override suspend fun runningTunnelNames(): Set<String> {
|
||||||
return backend.runningTunnelNames
|
return backend.runningTunnelNames
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics {
|
||||||
|
return AmneziaStatistics(backend.getStatistics(tunnelConf))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ class ServiceWorker @AssistedInject constructor(
|
|||||||
Timber.i("Service worker started")
|
Timber.i("Service worker started")
|
||||||
with(appDataRepository.settings.get()) {
|
with(appDataRepository.settings.get()) {
|
||||||
if (isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) return@with serviceManager.startAutoTunnel(true)
|
if (isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) return@with serviceManager.startAutoTunnel(true)
|
||||||
if (tunnelManager.activeTunnels().value.isEmpty()) tunnelManager.restorePreviousState()
|
if (tunnelManager.activeTunnels.value.isEmpty()) tunnelManager.restorePreviousState()
|
||||||
}
|
}
|
||||||
Result.success()
|
Result.success()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.di
|
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.network.InternetConnectivityMonitor
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.network.NetworkMonitor
|
|
||||||
import dagger.Binds
|
|
||||||
import dagger.Module
|
|
||||||
import dagger.hilt.InstallIn
|
|
||||||
import dagger.hilt.components.SingletonComponent
|
|
||||||
|
|
||||||
@Module
|
|
||||||
@InstallIn(SingletonComponent::class)
|
|
||||||
abstract class ServiceModule {
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
abstract fun provideInternetConnectivityService(wifiService: InternetConnectivityMonitor): NetworkMonitor
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,8 @@ import android.content.Context
|
|||||||
import com.wireguard.android.backend.WgQuickBackend
|
import com.wireguard.android.backend.WgQuickBackend
|
||||||
import com.wireguard.android.util.RootShell
|
import com.wireguard.android.util.RootShell
|
||||||
import com.wireguard.android.util.ToolsInstaller
|
import com.wireguard.android.util.ToolsInstaller
|
||||||
import com.zaneschepke.wireguardautotunnel.core.network.NetworkMonitor
|
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
|
||||||
|
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.KernelTunnel
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.KernelTunnel
|
||||||
@@ -98,13 +99,20 @@ class TunnelModule {
|
|||||||
return TunnelManager(kernelTunnel, userspaceTunnel, appDataRepository, applicationScope, ioDispatcher)
|
return TunnelManager(kernelTunnel, userspaceTunnel, appDataRepository, applicationScope, ioDispatcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideNetworkMonitor(@ApplicationContext context: Context): NetworkMonitor {
|
||||||
|
return AndroidNetworkMonitor(context)
|
||||||
|
}
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@Provides
|
@Provides
|
||||||
fun provideServiceManager(
|
fun provideServiceManager(
|
||||||
@ApplicationContext context: Context,
|
@ApplicationContext context: Context,
|
||||||
@IoDispatcher ioDispatcher: CoroutineDispatcher,
|
@IoDispatcher ioDispatcher: CoroutineDispatcher,
|
||||||
|
@ApplicationScope applicationScope: CoroutineScope,
|
||||||
appDataRepository: AppDataRepository,
|
appDataRepository: AppDataRepository,
|
||||||
): ServiceManager {
|
): ServiceManager {
|
||||||
return ServiceManager(context, ioDispatcher, appDataRepository)
|
return ServiceManager(context, ioDispatcher, applicationScope, appDataRepository)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.domain.entity
|
package com.zaneschepke.wireguardautotunnel.domain.entity
|
||||||
|
|
||||||
import com.wireguard.config.Config
|
import com.wireguard.config.Config
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isReachable
|
import com.zaneschepke.wireguardautotunnel.util.extensions.isReachable
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.toWgQuickString
|
import com.zaneschepke.wireguardautotunnel.util.extensions.toWgQuickString
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.serialization.Transient
|
||||||
import org.amnezia.awg.backend.Tunnel
|
import org.amnezia.awg.backend.Tunnel
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
@@ -30,9 +27,13 @@ data class TunnelConf(
|
|||||||
val pingIp: String? = null,
|
val pingIp: String? = null,
|
||||||
val isEthernetTunnel: Boolean = false,
|
val isEthernetTunnel: Boolean = false,
|
||||||
val isIpv4Preferred: Boolean = false,
|
val isIpv4Preferred: Boolean = false,
|
||||||
|
@Transient
|
||||||
|
private var stateChangeCallback: ((Any) -> Unit)? = null,
|
||||||
) : Tunnel, com.wireguard.android.backend.Tunnel {
|
) : Tunnel, com.wireguard.android.backend.Tunnel {
|
||||||
|
|
||||||
val state = MutableStateFlow(TunnelState())
|
fun setStateChangeCallback(callback: (Any) -> Unit) {
|
||||||
|
stateChangeCallback = callback
|
||||||
|
}
|
||||||
|
|
||||||
fun toAmConfig(): org.amnezia.awg.config.Config {
|
fun toAmConfig(): org.amnezia.awg.config.Config {
|
||||||
return configFromAmQuick(amQuick.ifBlank { wgQuick })
|
return configFromAmQuick(amQuick.ifBlank { wgQuick })
|
||||||
@@ -50,21 +51,17 @@ data class TunnelConf(
|
|||||||
return isIpv4Preferred
|
return isIpv4Preferred
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStateChange(newState: Tunnel.State) {
|
|
||||||
state.update {
|
|
||||||
it.copy(state = newState.asTunnelState())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStateChange(newState: com.wireguard.android.backend.Tunnel.State) {
|
override fun onStateChange(newState: com.wireguard.android.backend.Tunnel.State) {
|
||||||
state.update {
|
stateChangeCallback?.invoke(newState)
|
||||||
it.copy(state = newState.asTunnelState())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isQuickConfigChanged(updatedConf: TunnelConf): Boolean {
|
override fun onStateChange(newState: Tunnel.State) {
|
||||||
return updatedConf.wgQuick != wgQuick ||
|
stateChangeCallback?.invoke(newState)
|
||||||
updatedConf.amQuick != amQuick
|
}
|
||||||
|
|
||||||
|
fun isQuickConfigMatching(updatedConf: TunnelConf): Boolean {
|
||||||
|
return updatedConf.wgQuick == wgQuick ||
|
||||||
|
updatedConf.amQuick == amQuick
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isPingConfigMatching(updatedConf: TunnelConf): Boolean {
|
fun isPingConfigMatching(updatedConf: TunnelConf): Boolean {
|
||||||
@@ -74,18 +71,17 @@ data class TunnelConf(
|
|||||||
updatedConf.pingInterval == pingInterval
|
updatedConf.pingInterval == pingInterval
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun pingTunnel(context: CoroutineContext): List<Boolean> {
|
suspend fun isTunnelPingable(context: CoroutineContext): Boolean {
|
||||||
return withContext(context) {
|
return withContext(context) {
|
||||||
val config = toWgConfig()
|
val config = toWgConfig()
|
||||||
if (pingIp != null) {
|
if (pingIp != null) {
|
||||||
Timber.i("Pinging custom ip")
|
return@withContext InetAddress.getByName(pingIp)
|
||||||
listOf(InetAddress.getByName(pingIp).isReachable(Constants.PING_TIMEOUT.toInt()))
|
.isReachable(Constants.PING_TIMEOUT.toInt())
|
||||||
} else {
|
}
|
||||||
Timber.i("Pinging all peers")
|
Timber.i("Pinging all peers")
|
||||||
config.peers.map { peer ->
|
config.peers.map { peer ->
|
||||||
peer.isReachable(isIpv4Preferred)
|
peer.isReachable(isIpv4Preferred)
|
||||||
}
|
}.all { true }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+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 },
|
||||||
|
|||||||
+3
-1
@@ -41,6 +41,7 @@ import com.zaneschepke.wireguardautotunnel.ui.common.config.SubmitConfigurationT
|
|||||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.ForwardButton
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.ForwardButton
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isValidIpv4orIpv6Address
|
import com.zaneschepke.wireguardautotunnel.util.extensions.isValidIpv4orIpv6Address
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
|
||||||
@@ -51,7 +52,7 @@ import kotlin.text.isNullOrBlank
|
|||||||
import kotlin.text.toLong
|
import kotlin.text.toLong
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun OptionsScreen(tunnelConf: TunnelConf, viewModel: TunnelOptionsViewModel = hiltViewModel()) {
|
fun OptionsScreen(tunnelConf: TunnelConf, appUiState: AppUiState, viewModel: TunnelOptionsViewModel = hiltViewModel()) {
|
||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
|
|
||||||
var currentText by remember { mutableStateOf("") }
|
var currentText by remember { mutableStateOf("") }
|
||||||
@@ -194,6 +195,7 @@ fun OptionsScreen(tunnelConf: TunnelConf, viewModel: TunnelOptionsViewModel = hi
|
|||||||
trailing = {
|
trailing = {
|
||||||
ScaledSwitch(
|
ScaledSwitch(
|
||||||
checked = tunnelConf.isPingEnabled,
|
checked = tunnelConf.isPingEnabled,
|
||||||
|
enabled = !appUiState.activeTunnels.containsKey(tunnelConf.id),
|
||||||
onClick = { onPingToggle() },
|
onClick = { onPingToggle() },
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
+5
-5
@@ -352,11 +352,11 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
|
|||||||
ScaledSwitch(
|
ScaledSwitch(
|
||||||
uiState.appSettings.isKernelEnabled,
|
uiState.appSettings.isKernelEnabled,
|
||||||
onClick = { appViewModel.onToggleKernelMode() },
|
onClick = { appViewModel.onToggleKernelMode() },
|
||||||
// enabled = !(
|
enabled = !(
|
||||||
// uiState.settings.isAutoTunnelEnabled ||
|
uiState.appSettings.isAutoTunnelEnabled ||
|
||||||
// uiState.settings.isAlwaysOnVpnEnabled ||
|
uiState.appSettings.isAlwaysOnVpnEnabled ||
|
||||||
// (uiState.vpnState.status == TunnelState.UP)
|
uiState.activeTunnels.isNotEmpty()
|
||||||
// ),
|
),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
|
|||||||
+2
-19
@@ -18,8 +18,6 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
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
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -36,7 +34,6 @@ import com.zaneschepke.wireguardautotunnel.ui.Route
|
|||||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
|
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
|
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
|
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
|
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.label.VersionLabel
|
import com.zaneschepke.wireguardautotunnel.ui.common.label.VersionLabel
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
|
||||||
@@ -51,20 +48,6 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
|
|||||||
fun SupportScreen(appUiState: AppUiState, appViewModel: AppViewModel) {
|
fun SupportScreen(appUiState: AppUiState, appViewModel: AppViewModel) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
|
|
||||||
var showDialog by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
if (showDialog) {
|
|
||||||
InfoDialog(onAttest = {
|
|
||||||
showDialog = false
|
|
||||||
appViewModel.onToggleLocalLogging()
|
|
||||||
}, onDismiss = {
|
|
||||||
showDialog = false
|
|
||||||
}, title = {
|
|
||||||
Text(stringResource(R.string.configuration_change))
|
|
||||||
}, body = { Text(stringResource(R.string.requires_app_relaunch)) }, confirmText = { Text(stringResource(R.string.yes)) })
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top),
|
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top),
|
||||||
@@ -115,12 +98,12 @@ fun SupportScreen(appUiState: AppUiState, appViewModel: AppViewModel) {
|
|||||||
ScaledSwitch(
|
ScaledSwitch(
|
||||||
appUiState.generalState.isLocalLogsEnabled,
|
appUiState.generalState.isLocalLogsEnabled,
|
||||||
onClick = {
|
onClick = {
|
||||||
showDialog = true
|
appViewModel.onToggleLocalLogging()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
showDialog = true
|
appViewModel.onToggleLocalLogging()
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ package com.zaneschepke.wireguardautotunnel.ui.state
|
|||||||
import com.zaneschepke.wireguardautotunnel.data.model.GeneralState
|
import com.zaneschepke.wireguardautotunnel.data.model.GeneralState
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
|
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||||
|
|
||||||
data class AppUiState(
|
data class AppUiState(
|
||||||
val appSettings: AppSettings = AppSettings(),
|
val appSettings: AppSettings = AppSettings(),
|
||||||
val tunnels: List<TunnelConf> = emptyList(),
|
val tunnels: List<TunnelConf> = emptyList(),
|
||||||
|
val activeTunnels: Map<Int, TunnelState> = emptyMap(),
|
||||||
val generalState: GeneralState = GeneralState(),
|
val generalState: GeneralState = GeneralState(),
|
||||||
val autoTunnelActive: Boolean = false,
|
val autoTunnelActive: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|||||||
+2
-9
@@ -2,7 +2,6 @@ package com.zaneschepke.wireguardautotunnel.util.extensions
|
|||||||
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import com.wireguard.android.backend.BackendException
|
import com.wireguard.android.backend.BackendException
|
||||||
import com.wireguard.android.util.RootShell
|
|
||||||
import com.wireguard.config.Peer
|
import com.wireguard.config.Peer
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendError
|
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendError
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
||||||
@@ -46,9 +45,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
|
||||||
}
|
}
|
||||||
@@ -87,12 +86,6 @@ fun Config.toWgQuickString(): String {
|
|||||||
return lines.joinToString(System.lineSeparator())
|
return lines.joinToString(System.lineSeparator())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun RootShell.getCurrentWifiName(): String? {
|
|
||||||
val response = mutableListOf<String>()
|
|
||||||
this.run(response, "dumpsys wifi | grep 'Supplicant state: COMPLETED' | grep -o 'SSID: [^,]*' | cut -d ' ' -f2- | tr -d '\"'")
|
|
||||||
return response.firstOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Backend.BackendState.asBackendState(): BackendState {
|
fun Backend.BackendState.asBackendState(): BackendState {
|
||||||
return BackendState.valueOf(this.name)
|
return BackendState.valueOf(this.name)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,12 +8,6 @@ import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
|||||||
import com.zaneschepke.wireguardautotunnel.ui.Route
|
import com.zaneschepke.wireguardautotunnel.ui.Route
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.isCurrentRoute
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.isCurrentRoute
|
||||||
|
|
||||||
fun NavController.navigateAndForget(route: Route) {
|
|
||||||
navigate(route) {
|
|
||||||
popUpTo(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun NavController.goFromRoot(route: Route) {
|
fun NavController.goFromRoot(route: Route) {
|
||||||
if (currentBackStackEntry?.isCurrentRoute(route::class) == true) return
|
if (currentBackStackEntry?.isCurrentRoute(route::class) == true) return
|
||||||
this.navigate(route) {
|
this.navigate(route) {
|
||||||
|
|||||||
@@ -31,11 +31,9 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.onCompletion
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.takeWhile
|
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.plus
|
import kotlinx.coroutines.plus
|
||||||
@@ -75,11 +73,13 @@ constructor(
|
|||||||
appDataRepository.settings.flow,
|
appDataRepository.settings.flow,
|
||||||
appDataRepository.tunnels.flow,
|
appDataRepository.tunnels.flow,
|
||||||
appDataRepository.appState.flow,
|
appDataRepository.appState.flow,
|
||||||
|
tunnelManager.activeTunnels,
|
||||||
serviceManager.autoTunnelActive,
|
serviceManager.autoTunnelActive,
|
||||||
) { settings, tunnels, generalState, autoTunnel ->
|
) { settings, tunnels, generalState, activeTunnels, autoTunnel ->
|
||||||
AppUiState(
|
AppUiState(
|
||||||
settings,
|
settings,
|
||||||
tunnels,
|
tunnels,
|
||||||
|
activeTunnels,
|
||||||
generalState,
|
generalState,
|
||||||
autoTunnel,
|
autoTunnel,
|
||||||
)
|
)
|
||||||
@@ -103,14 +103,13 @@ constructor(
|
|||||||
|
|
||||||
private suspend fun appReadyCheck() {
|
private suspend fun appReadyCheck() {
|
||||||
val tunnelCount = appDataRepository.tunnels.count()
|
val tunnelCount = appDataRepository.tunnels.count()
|
||||||
uiState.takeWhile { it.tunnels.size != tunnelCount }.onCompletion {
|
uiState.first { it.tunnels.count() == tunnelCount }
|
||||||
_isAppReady.emit(true)
|
_isAppReady.emit(true)
|
||||||
}.collect()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -146,17 +145,12 @@ constructor(
|
|||||||
with(uiState.value.generalState) {
|
with(uiState.value.generalState) {
|
||||||
val toggledOn = !isLocalLogsEnabled
|
val toggledOn = !isLocalLogsEnabled
|
||||||
appDataRepository.appState.setLocalLogsEnabled(toggledOn)
|
appDataRepository.appState.setLocalLogsEnabled(toggledOn)
|
||||||
if (!toggledOn) onLoggerStop()
|
if (!toggledOn) {
|
||||||
_configurationChange.update {
|
logReader.stop()
|
||||||
true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun onLoggerStop() {
|
|
||||||
logReader.deleteAndClearLogs()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onToggleAlwaysOnVPN() = viewModelScope.launch {
|
fun onToggleAlwaysOnVPN() = viewModelScope.launch {
|
||||||
with(uiState.value.appSettings) {
|
with(uiState.value.appSettings) {
|
||||||
appDataRepository.settings.save(
|
appDataRepository.settings.save(
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -1,43 +1,50 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">WG Tunnel</string>
|
<string name="app_name">WG Tunnel</string>
|
||||||
|
<string name="vpn_channel_id" translatable="false">VPN Channel</string>
|
||||||
<string name="vpn_channel_name">VPN Bildirim Kanalı</string>
|
<string name="vpn_channel_name">VPN Bildirim Kanalı</string>
|
||||||
|
<string name="github_url" translatable="false">https://github.com/zaneschepke/wgtunnel/issues</string>
|
||||||
|
<string name="docs_url" translatable="false">https://zaneschepke.com/wgtunnel-docs/overview.html</string>
|
||||||
|
<string name="privacy_policy_url" translatable="false">https://zaneschepke.com/wgtunnel-docs/privacypolicy.html</string>
|
||||||
|
<string name="docs_wildcards" translatable="false">https://zaneschepke.com/wgtunnel-docs/features.html#wildcard-wi-fi-name-support</string>
|
||||||
|
<string name="donate_url" translatable="false">https://zaneschepke.com/donate/</string>
|
||||||
<string name="error_file_extension">Dosya .conf veya .zip değil</string>
|
<string name="error_file_extension">Dosya .conf veya .zip değil</string>
|
||||||
<string name="turn_off_tunnel">İşlem için tünelin kapalı olması gerekiyor</string>
|
<string name="turn_off_tunnel">Bu işlem tünelin kapalı olmasını gerektirir</string>
|
||||||
<string name="no_tunnels">Henüz tünel eklenmedi!</string>
|
<string name="no_tunnels">Henüz tünel eklenmedi!</string>
|
||||||
<string name="tunnels">Tüneller</string>
|
<string name="tunnels">Tüneller</string>
|
||||||
<string name="tunnel_mobile_data">Mobil veride tünel</string>
|
<string name="tunnel_mobile_data">Mobil veride tünel</string>
|
||||||
<string name="privacy_policy">Gizlilik Politikasını Görüntüle</string>
|
<string name="privacy_policy">Gizlilik politikasını görüntüle</string>
|
||||||
<string name="okay">Tamam</string>
|
<string name="okay">Tamam</string>
|
||||||
<string name="tunnel_on_ethernet">Ethernet\'te tünel</string>
|
<string name="tunnel_on_ethernet">Ethernet üzerinde tünel</string>
|
||||||
<string name="prominent_background_location_message">Bu özellik, uygulama kapalıyken bile Wi-Fi SSID izlemesini etkinleştirmek için arka plan konum iznine ihtiyaç duyar. Daha fazla ayrıntı için lütfen Destek ekranında bağlantısı verilen Gizlilik Politikasına bakın.</string>
|
<string name="prominent_background_location_message">Bu özellik, uygulamanın kapalı olduğu durumlarda bile Wi-Fi SSID izlemesini etkinleştirmek için arka planda konum izni gerektirir. Daha fazla ayrıntı için lütfen Destek ekranında bağlantısı verilen Gizlilik Politikasına bakın.</string>
|
||||||
<string name="prominent_background_location_title">Arka Plan Konum Açıklaması</string>
|
<string name="prominent_background_location_title">Arka Plan Konum Açıklaması</string>
|
||||||
<string name="thank_you">WG Tunnel\'ı kullandığınız için teşekkürler!</string>
|
<string name="thank_you">WG Tunnel’i kullandığınız için teşekkürler!</string>
|
||||||
<string name="trusted_ssid_value_description">SSID\'yi gönder</string>
|
<string name="trusted_ssid_value_description">SSID Gönder</string>
|
||||||
<string name="add_tunnels_text">Dosyadan veya zip\'ten ekle</string>
|
<string name="add_tunnels_text">Dosyadan veya zip’ten ekle</string>
|
||||||
<string name="open_file">Dosya Aç</string>
|
<string name="open_file">Dosya Aç</string>
|
||||||
<string name="add_from_qr">QR kodundan ekle</string>
|
<string name="add_from_qr">QR kodundan ekle</string>
|
||||||
<string name="qr_scan">QR Tarama</string>
|
<string name="qr_scan">QR Tara</string>
|
||||||
<string name="tunnel_name">Tünel Adı</string>
|
<string name="tunnel_name">Tünel Adı</string>
|
||||||
<string name="exclude">Hariç tut</string>
|
<string name="exclude">Hariç Tut</string>
|
||||||
<string name="include">Dahil et</string>
|
<string name="include">Dahil Et</string>
|
||||||
<string name="config_changes_saved">Yapılandırma değişiklikleri kaydedildi.</string>
|
<string name="config_changes_saved">Yapılandırma değişiklikleri kaydedildi.</string>
|
||||||
<string name="public_key">Genel anahtar</string>
|
<string name="public_key">Genel anahtar</string>
|
||||||
<string name="addresses">Adresler</string>
|
<string name="addresses">Adresler</string>
|
||||||
<string name="dns_servers">DNS sunucuları</string>
|
<string name="dns_servers">DNS sunucuları</string>
|
||||||
<string name="mtu">MTU</string>
|
<string name="mtu">MTU</string>
|
||||||
<string name="peer">Eş (peer)</string>
|
<string name="peer">Eş</string>
|
||||||
<string name="allowed_ips">İzin verilen IP\'ler</string>
|
<string name="allowed_ips">İzin verilen IP’ler</string>
|
||||||
<string name="endpoint">Uç nokta (endpoint)</string>
|
<string name="endpoint">Uç Nokta</string>
|
||||||
<string name="name">Ad</string>
|
<string name="name">Ad</string>
|
||||||
<string name="always_on_vpn_support">Her Zaman Açık VPN\'e İzin Ver</string>
|
<string name="always_on_vpn_support">Her Zaman Açık VPN’e İzin Ver</string>
|
||||||
<string name="location_services_not_detected">Konum Hizmetleri Algılanmadı</string>
|
<string name="location_services_not_detected">Konum Servisleri Algılanmadı</string>
|
||||||
<string name="hint_search_packages">Paketlerde ara</string>
|
<string name="hint_search_packages">Paketleri ara</string>
|
||||||
|
<string name="db_name" translatable="false">wg-tunnel-db</string>
|
||||||
<string name="auto_tunneling">Otomatik tünelleme</string>
|
<string name="auto_tunneling">Otomatik tünelleme</string>
|
||||||
<string name="vpn_on">VPN açık</string>
|
<string name="vpn_on">VPN açık</string>
|
||||||
<string name="vpn_off">VPN kapalı</string>
|
<string name="vpn_off">VPN kapalı</string>
|
||||||
<string name="create_import">Sıfırdan oluştur</string>
|
<string name="create_import">Sıfırdan oluştur</string>
|
||||||
<string name="turn_on_tunnel">İşlem için aktif tünel gerekiyor</string>
|
<string name="turn_on_tunnel">Bu işlem aktif bir tünel gerektirir</string>
|
||||||
<string name="add_peer">Eş ekle</string>
|
<string name="add_peer">Eş ekle</string>
|
||||||
<string name="interface_">Arayüz</string>
|
<string name="interface_">Arayüz</string>
|
||||||
<string name="rotate_keys">Anahtarları döndür</string>
|
<string name="rotate_keys">Anahtarları döndür</string>
|
||||||
@@ -49,41 +56,42 @@
|
|||||||
<string name="random">(rastgele)</string>
|
<string name="random">(rastgele)</string>
|
||||||
<string name="optional">(isteğe bağlı)</string>
|
<string name="optional">(isteğe bağlı)</string>
|
||||||
<string name="optional_no_recommend">(isteğe bağlı, önerilmez)</string>
|
<string name="optional_no_recommend">(isteğe bağlı, önerilmez)</string>
|
||||||
<string name="preshared_key">Önceden paylaşılmış anahtar</string>
|
<string name="preshared_key">Ön paylaşımlı anahtar</string>
|
||||||
<string name="seconds">saniye</string>
|
<string name="seconds">saniye</string>
|
||||||
<string name="persistent_keepalive">Kalıcı canlı tutma</string>
|
<string name="persistent_keepalive">Kalıcı canlı tutma</string>
|
||||||
<string name="cancel">İptal</string>
|
<string name="cancel">İptal</string>
|
||||||
<string name="error_authentication_failed">Kimlik doğrulama başarısız oldu</string>
|
<string name="error_authentication_failed">Kimlik doğrulama başarısız</string>
|
||||||
<string name="error_authorization_failed">Yetkilendirme başarısız oldu</string>
|
<string name="error_authorization_failed">Yetkilendirme başarısız</string>
|
||||||
<string name="enabled_app_shortcuts">Uygulama kısayollarını etkinleştir</string>
|
<string name="enabled_app_shortcuts">Uygulama kısayollarını etkinleştir</string>
|
||||||
<string name="export_configs">Yapılandırmaları dışa aktar</string>
|
<string name="export_configs">Yapılandırmaları dışa aktar</string>
|
||||||
<string name="unknown_error">Bilinmeyen bir hata oluştu</string>
|
<string name="unknown_error">Bilinmeyen bir hata oluştu</string>
|
||||||
<string name="tunnel_on_wifi">Güvenilmeyen wifi\'da tünel</string>
|
<string name="tunnel_on_wifi">Güvenilmeyen wifi’da tünel</string>
|
||||||
|
<string name="my_email" translatable="false">support@zaneschepke.com</string>
|
||||||
<string name="email_subject">WG Tunnel Desteği</string>
|
<string name="email_subject">WG Tunnel Desteği</string>
|
||||||
<string name="email_chooser">E-posta gönder…</string>
|
<string name="email_chooser">E-posta gönder…</string>
|
||||||
<string name="docs_description">Belgeleri oku</string>
|
<string name="docs_description">Belgeleri oku</string>
|
||||||
<string name="email_description">Bana e-posta gönder</string>
|
<string name="email_description">Bana e-posta gönder</string>
|
||||||
<string name="use_kernel">Kernel modülünü kullan</string>
|
<string name="use_kernel">Çekirdek modülünü kullan</string>
|
||||||
<string name="error_ssid_exists">SSID zaten mevcut</string>
|
<string name="error_ssid_exists">SSID zaten mevcut</string>
|
||||||
<string name="error_root_denied">Root kabuğu reddedildi</string>
|
<string name="error_root_denied">Root kabuğu reddedildi</string>
|
||||||
<string name="error_no_file_explorer">Dosya gezgini yüklü değil</string>
|
<string name="error_no_file_explorer">Dosya gezgini yüklü değil</string>
|
||||||
<string name="error_invalid_code">Geçersiz QR kodu</string>
|
<string name="error_invalid_code">Geçersiz QR kodu</string>
|
||||||
<string name="location_services_missing_message">Uygulama, cihazınızda etkinleştirilmiş herhangi bir konum hizmeti algılamıyor. Cihaza bağlı olarak, bu durum güvenilmeyen wifi özelliğinin wifi adını okumasını engelleyebilir. Yine de devam etmek istiyor musunuz?</string>
|
<string name="location_services_missing_message">Uygulama, cihazınızda etkinleştirilmiş herhangi bir konum servisi algılamıyor. Cihaza bağlı olarak, bu durum güvenilmeyen wifi özelliğinin wifi adını okuyamamasını sağlayabilir. Yine de devam etmek ister misiniz?</string>
|
||||||
<string name="auto_tunnel_title">Otomatik Tünel Hizmeti</string>
|
<string name="auto_tunnel_title">Otomatik tünel servisi</string>
|
||||||
<string name="delete_tunnel">Tüneli sil</string>
|
<string name="delete_tunnel">Tüneli sil</string>
|
||||||
<string name="delete_tunnel_message">Bu tüneli silmek istediğinizden emin misiniz?</string>
|
<string name="delete_tunnel_message">Bu tüneli silmek istediğinizden emin misiniz?</string>
|
||||||
<string name="yes">Evet</string>
|
<string name="yes">Evet</string>
|
||||||
<string name="tunneling_apps">Tünellenen uygulamalar</string>
|
<string name="tunneling_apps">Tünelleme uygulamaları</string>
|
||||||
<string name="all">tümü</string>
|
<string name="all">tümü</string>
|
||||||
<string name="no_email_detected">E-posta uygulaması algılanmadı</string>
|
<string name="no_email_detected">E-posta uygulaması algılanmadı</string>
|
||||||
<string name="no_browser_detected">Tarayıcı algılanmadı</string>
|
<string name="no_browser_detected">Tarayıcı algılanmadı</string>
|
||||||
<string name="open_issue">Sorun bildir</string>
|
<string name="open_issue">Bir sorun aç</string>
|
||||||
<string name="read_logs">Günlükleri oku</string>
|
<string name="read_logs">Günlükleri oku</string>
|
||||||
<string name="auto">(otomatik)</string>
|
<string name="auto">(otomatik)</string>
|
||||||
<string name="incorrect_pin">PIN yanlış</string>
|
<string name="incorrect_pin">Pin yanlış</string>
|
||||||
<string name="pin_created">PIN başarıyla oluşturuldu</string>
|
<string name="pin_created">Pin başarıyla oluşturuldu</string>
|
||||||
<string name="enter_pin">PIN\'inizi girin</string>
|
<string name="enter_pin">Pin’inizi girin</string>
|
||||||
<string name="create_pin">PIN oluştur</string>
|
<string name="create_pin">Pin oluştur</string>
|
||||||
<string name="enable_app_lock">Uygulama kilidini etkinleştir</string>
|
<string name="enable_app_lock">Uygulama kilidini etkinleştir</string>
|
||||||
<string name="restart_on_ping">Ping başarısız olduğunda yeniden başlat (beta)</string>
|
<string name="restart_on_ping">Ping başarısız olduğunda yeniden başlat (beta)</string>
|
||||||
<string name="mobile_data_tunnel">Mobil veri tüneli olarak ayarla</string>
|
<string name="mobile_data_tunnel">Mobil veri tüneli olarak ayarla</string>
|
||||||
@@ -94,18 +102,118 @@
|
|||||||
<string name="settings">Ayarlar</string>
|
<string name="settings">Ayarlar</string>
|
||||||
<string name="support">Destek</string>
|
<string name="support">Destek</string>
|
||||||
<string name="kernel">Çekirdek</string>
|
<string name="kernel">Çekirdek</string>
|
||||||
<string name="junk_packet_count">Gereksiz paket sayısı</string>
|
<string name="junk_packet_count">Çöp paket sayısı</string>
|
||||||
<string name="junk_packet_minimum_size">Gereksiz paket minimum boyutu</string>
|
<string name="junk_packet_minimum_size">Çöp paket minimum boyutu</string>
|
||||||
<string name="junk_packet_maximum_size">Gereksiz paket maksimum boyutu</string>
|
<string name="junk_packet_maximum_size">Çöp paket maksimum boyutu</string>
|
||||||
<string name="init_packet_junk_size">Başlatma paketi gereksiz boyutu</string>
|
<string name="init_packet_junk_size">Başlangıç paketi çöp boyutu</string>
|
||||||
<string name="response_packet_junk_size">Yanıt paketi gereksiz boyutu</string>
|
<string name="response_packet_junk_size">Yanıt paketi çöp boyutu</string>
|
||||||
<string name="init_packet_magic_header">Başlatma paketi sihirli başlığı</string>
|
<string name="init_packet_magic_header">Başlangıç paketi sihirli başlığı</string>
|
||||||
<string name="response_packet_magic_header">Yanıt paketi sihirli başlığı</string>
|
<string name="response_packet_magic_header">Yanıt paketi sihirli başlığı</string>
|
||||||
<string name="transport_packet_magic_header">Taşıma paketi sihirli başlığı</string>
|
<string name="transport_packet_magic_header">Taşıma paketi sihirli başlığı</string>
|
||||||
<string name="underload_packet_magic_header">Düşük yük paketi sihirli başlığı</string>
|
<string name="underload_packet_magic_header">Düşük yük paketi sihirli başlığı</string>
|
||||||
|
<string name="telegram_url" translatable="false">https://t.me/wgtunnel</string>
|
||||||
<string name="unsure_how">nasıl devam edeceğinizden emin değilseniz</string>
|
<string name="unsure_how">nasıl devam edeceğinizden emin değilseniz</string>
|
||||||
<string name="see_the">Bakın:</string>
|
<string name="see_the">Bakınız</string>
|
||||||
|
<string name="getting_started_url" translatable="false">https://zaneschepke.com/wgtunnel-docs/getting-started.html</string>
|
||||||
<string name="getting_started_guide">başlangıç kılavuzu</string>
|
<string name="getting_started_guide">başlangıç kılavuzu</string>
|
||||||
<string name="error_file_format">Geçersiz tünel yapılandırma formatı</string>
|
<string name="error_file_format">Geçersiz tünel yapılandırma formatı</string>
|
||||||
<string name="restart_at_boot">Önyüklemede yeniden başlat</string>
|
<string name="restart_at_boot">Başlangıçta yeniden başlat</string>
|
||||||
|
<string name="vpn_denied_dialog_title">İzin Reddedildi</string>
|
||||||
|
<string name="vpn_settings">VPN sistem ayarları</string>
|
||||||
|
<string name="always_on_message">VPN bağlantı izni reddedildi. Lütfen</string>
|
||||||
|
<string name="always_on_message2">diğer tüm uygulamalar için Her Zaman Açık VPN’in kapalı olduğundan emin olun ve tekrar deneyin</string>
|
||||||
|
<string name="chat_description">Topluluğa katıl</string>
|
||||||
|
<string name="tunnel_required">Bu özellik en az bir tünel gerektirir</string>
|
||||||
|
<string name="background_location_message">Bu özellik için her zaman konum izni ve/veya hassas konum gereklidir. Lütfen</string>
|
||||||
|
<string name="app_settings">uygulama ayarları</string>
|
||||||
|
<string name="background_location_message2">bu izinlerin etkin olduğundan emin olun</string>
|
||||||
|
<string name="root_accepted">Root kabuğu kabul edildi</string>
|
||||||
|
<string name="set_custom_ping_ip">Özel ping IP’si ayarla</string>
|
||||||
|
<string name="default_ping_ip">(isteğe bağlı, varsayılan eşler)</string>
|
||||||
|
<string name="set_custom_ping_internal">Ping aralığı (saniye)</string>
|
||||||
|
<string name="optional_default">"isteğe bağlı, varsayılan: "</string>
|
||||||
|
<string name="set_custom_ping_cooldown">Ping yeniden başlatma bekleme süresi (saniye)</string>
|
||||||
|
<string name="show_amnezia_properties">Amnezia özelliklerini göster</string>
|
||||||
|
<string name="never">asla</string>
|
||||||
|
<string name="sec">sn</string>
|
||||||
|
<string name="handshake">el sıkışma</string>
|
||||||
|
<string name="logs">Günlükler</string>
|
||||||
|
<string name="kill_switch">Kill Switch</string>
|
||||||
|
<string name="appearance">Görünüm</string>
|
||||||
|
<string name="notifications">Bildirimler</string>
|
||||||
|
<string name="automatic">Otomatik</string>
|
||||||
|
<string name="light">Açık</string>
|
||||||
|
<string name="dark">Koyu</string>
|
||||||
|
<string name="dynamic">Dinamik</string>
|
||||||
|
<string name="language">Dil</string>
|
||||||
|
<string name="display_theme">Ekran teması</string>
|
||||||
|
<string name="trusted_wifi_names">Güvenilir wifi adları</string>
|
||||||
|
<string name="add_wifi_name">Wifi adı ekle</string>
|
||||||
|
<string name="on_demand_rules">İsteğe bağlı tünel kuralları</string>
|
||||||
|
<string name="primary_tunnel">Birincil tünel</string>
|
||||||
|
<string name="mobile_tunnel">Mobil veri tüneli</string>
|
||||||
|
<string name="skip">Atla</string>
|
||||||
|
<string name="launch_app_settings">Uygulama ayarlarını başlat</string>
|
||||||
|
<string name="use_wildcards">İsim jokerlerini kullan</string>
|
||||||
|
<string name="learn_more">Daha fazla bilgi</string>
|
||||||
|
<string name="wildcards_active">Jokerler etkin</string>
|
||||||
|
<string name="wifi_name_via_shell">Kabuk üzerinden wifi adı</string>
|
||||||
|
<string name="use_root_shell_for_wifi">Wifi adını almak için root kabuğunu kullan</string>
|
||||||
|
<string name="kernel_not_supported">Çekirdek desteklenmiyor</string>
|
||||||
|
<string name="start_auto">Otomatik tüneli başlat</string>
|
||||||
|
<string name="stop_auto">Otomatik tüneli durdur</string>
|
||||||
|
<string name="tunnel_running">Tünel çalışıyor</string>
|
||||||
|
<string name="monitoring_state_changes">Durum değişikliklerini izleme</string>
|
||||||
|
<string name="donate">Projeye bağış yap</string>
|
||||||
|
<string name="local_logging">Yerel günlüğe kaydetme</string>
|
||||||
|
<string name="enable_local_logging">Yerel günlüğe kaydetmeyi etkinleştir</string>
|
||||||
|
<string name="configuration_change">Yapılandırma değişikliği</string>
|
||||||
|
<string name="requires_app_relaunch">Bu değişiklik uygulamanın yeniden başlatılmasını gerektirir. Devam etmek ister misiniz?</string>
|
||||||
|
<string name="add_from_clipboard">Panodan ekle</string>
|
||||||
|
<string name="stop_on_no_internet">İnternet olmadığında durdur</string>
|
||||||
|
<string name="stop_on_internet_loss">İnternet kaybında tüneli durdur</string>
|
||||||
|
<string name="ethernet_tunnel">Ethernet tüneli</string>
|
||||||
|
<string name="set_ethernet_tunnel">Ethernet tüneli olarak ayarla</string>
|
||||||
|
<string name="native_kill_switch">Yerel kill switch</string>
|
||||||
|
<string name="vpn_kill_switch">VPN kill switch</string>
|
||||||
|
<string name="kill_switch_options">Kill switch seçenekleri</string>
|
||||||
|
<string name="allow_lan_traffic">LAN trafiğine izin ver</string>
|
||||||
|
<string name="bypass_lan_for_kill_switch">Kill switch için LAN’ı atla</string>
|
||||||
|
<string name="vpn_channel_description">VPN durum bildirimleri için bir kanal</string>
|
||||||
|
<string name="auto_tunnel_channel_id" translatable="false">Auto-tunnel Channel</string>
|
||||||
|
<string name="auto_tunnel_channel_name">Otomatik Tünel Bildirim Kanalı</string>
|
||||||
|
<string name="auto_tunnel_channel_description">Otomatik tünel durum bildirimleri için bir kanal</string>
|
||||||
|
<string name="stop">durdur</string>
|
||||||
|
<string name="splt_tunneling">Bölünmüş tünelleme</string>
|
||||||
|
<string name="tunnel_specific_settings">Tünele özgü ayarlar</string>
|
||||||
|
<string name="show_scripts">Komut dosyalarını göster</string>
|
||||||
|
<string name="pre_up">Ön çalıştırma</string>
|
||||||
|
<string name="post_up">Sonra çalıştırma</string>
|
||||||
|
<string name="pre_down">Ön kapatma</string>
|
||||||
|
<string name="post_down">Sonra kapatma</string>
|
||||||
|
<string name="amnezia_kernel_message">Amnezia çekirdek modunda kullanılamaz</string>
|
||||||
|
<string name="enable_amnezia">Amnezia’yı etkinleştir</string>
|
||||||
|
<string name="wg_compat_mode">WG uyumluluk modu</string>
|
||||||
|
<string name="quick_actions">Hızlı eylemler</string>
|
||||||
|
<string name="advanced_settings">Gelişmiş ayarlar</string>
|
||||||
|
<string name="debounce_delay">Gecikme süresi</string>
|
||||||
|
<string name="hide_amnezia_properties">Amnezia özelliklerini gizle</string>
|
||||||
|
<string name="hide_scripts">Komut dosyalarını gizle</string>
|
||||||
|
<string name="enable_amnezia_compatibility">Amnezia uyumluluğunu etkinleştir</string>
|
||||||
|
<string name="remove_amnezia_compatibility">Amnezia uyumluluğunu kaldır</string>
|
||||||
|
<string name="exclude_lan">LAN’ı hariç tut</string>
|
||||||
|
<string name="include_lan">LAN’ı dahil et</string>
|
||||||
|
<string name="error_tunnel_start">Tünel başlatma başarısız</string>
|
||||||
|
<string name="tunnel_control">Tünel kontrolü</string>
|
||||||
|
<string name="auto_tunnel">Otomatik tünel</string>
|
||||||
|
<string name="kill_switch_off">Güvenilirde kill switch’i durdur</string>
|
||||||
|
<string name="server_ipv4">IPv4 ana makine çözünürlüğü</string>
|
||||||
|
<string name="prefer_ipv4">IPv4 bağlantısını tercih et</string>
|
||||||
|
<string name="dns_error">Uç nokta DNS’si çözülemedi.</string>
|
||||||
|
<string name="start_failed_config">Yapılandırma hatası nedeniyle tünel başlatılamadı.</string>
|
||||||
|
<string name="unauthorized">Yetkisiz, tünel başlatılamadı.</string>
|
||||||
|
<string name="tunne_start_failed_title">Tünel hatası</string>
|
||||||
|
<string name="multiple">Çoklu</string>
|
||||||
|
<string name="export_amnezia">Amnezia olarak dışa aktar</string>
|
||||||
|
<string name="export_wireguard">WireGuard olarak dışa aktar</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
object Constants {
|
object Constants {
|
||||||
const val VERSION_NAME = "3.6.6"
|
const val VERSION_NAME = "3.7.0"
|
||||||
const val JVM_TARGET = "17"
|
const val JVM_TARGET = "17"
|
||||||
const val VERSION_CODE = 36600
|
const val VERSION_CODE = 37000
|
||||||
const val TARGET_SDK = 35
|
const val TARGET_SDK = 35
|
||||||
const val MIN_SDK = 26
|
const val MIN_SDK = 26
|
||||||
const val APP_ID = "com.zaneschepke.wireguardautotunnel"
|
const val APP_ID = "com.zaneschepke.wireguardautotunnel"
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
What's new:
|
||||||
|
- Multiple tunnel support for kernel mode
|
||||||
|
- Override for WG default DNS Ipv4 preference
|
||||||
|
- Stop kill switch on trusted support
|
||||||
|
- Limit location querying by auto tunnel
|
||||||
|
- Various bug fixes and improvements
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
[versions]
|
[versions]
|
||||||
accompanist = "0.37.0"
|
accompanist = "0.37.2"
|
||||||
activityCompose = "1.10.0"
|
activityCompose = "1.10.1"
|
||||||
amneziawgAndroid = "1.2.6"
|
amneziawgAndroid = "1.3.0"
|
||||||
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"
|
||||||
coreKtx = "1.15.0"
|
coreKtx = "1.15.0"
|
||||||
datastorePreferences = "1.1.2"
|
datastorePreferences = "1.1.3"
|
||||||
desugar_jdk_libs = "2.1.4"
|
desugar_jdk_libs = "2.1.5"
|
||||||
espressoCore = "3.6.1"
|
espressoCore = "3.6.1"
|
||||||
hiltAndroid = "2.55"
|
hiltAndroid = "2.55"
|
||||||
hiltCompiler = "1.2.0"
|
hiltCompiler = "1.2.0"
|
||||||
@@ -15,14 +15,14 @@ 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"
|
||||||
material3 = "1.3.1"
|
material3 = "1.3.1"
|
||||||
navigationCompose = "2.8.7"
|
navigationCompose = "2.8.8"
|
||||||
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.6"
|
||||||
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.31"
|
||||||
composeBom = "2025.02.00"
|
composeBom = "2025.02.00"
|
||||||
compose = "1.7.8"
|
compose = "1.7.8"
|
||||||
workRuntimeKtxVersion = "2.10.0"
|
workRuntimeKtxVersion = "2.10.0"
|
||||||
@@ -32,7 +32,7 @@ gradlePlugins-grgit = "5.3.0"
|
|||||||
|
|
||||||
#plugins
|
#plugins
|
||||||
material = "1.12.0"
|
material = "1.12.0"
|
||||||
gradlePlugins-ktlint="12.1.2"
|
gradlePlugins-ktlint="12.2.0"
|
||||||
|
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package com.zaneschepke.logcatter
|
||||||
|
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.BufferedOutputStream
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.util.zip.ZipEntry
|
||||||
|
import java.util.zip.ZipOutputStream
|
||||||
|
|
||||||
|
class LogFileManager(
|
||||||
|
private val logDir: String,
|
||||||
|
private val maxFileSize: Long,
|
||||||
|
private val maxFolderSize: Long,
|
||||||
|
) {
|
||||||
|
private var currentFile: File? = null
|
||||||
|
private var outputStream: FileOutputStream? = null
|
||||||
|
|
||||||
|
val ioDispatcher = Dispatchers.IO
|
||||||
|
|
||||||
|
init {
|
||||||
|
rotateIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun writeLog(line: String) = withContext(ioDispatcher) {
|
||||||
|
rotateIfNeeded()
|
||||||
|
outputStream?.write((line + System.lineSeparator()).toByteArray())
|
||||||
|
outputStream?.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun zipLogs(zipFilePath: String) = withContext(ioDispatcher) {
|
||||||
|
outputStream?.close()
|
||||||
|
val sourceDir = File(logDir)
|
||||||
|
if (!sourceDir.exists() || !sourceDir.isDirectory) return@withContext
|
||||||
|
val outputZipFile = File(zipFilePath)
|
||||||
|
ZipOutputStream(BufferedOutputStream(FileOutputStream(outputZipFile))).use { zos ->
|
||||||
|
sourceDir.walkTopDown().forEach { file ->
|
||||||
|
val zipFileName = file.absolutePath.removePrefix(sourceDir.absolutePath).removePrefix("/")
|
||||||
|
val entry = ZipEntry("$zipFileName${if (file.isDirectory) "/" else ""}")
|
||||||
|
zos.putNextEntry(entry)
|
||||||
|
if (file.isFile) {
|
||||||
|
file.inputStream().use { it.copyTo(zos) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rotateIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun deleteAllLogs() = withContext(ioDispatcher) {
|
||||||
|
outputStream?.close()
|
||||||
|
File(logDir).listFiles()?.forEach { it.delete() }
|
||||||
|
rotateIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun close() {
|
||||||
|
outputStream?.close()
|
||||||
|
outputStream = null
|
||||||
|
currentFile = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun rotateIfNeeded() {
|
||||||
|
val folderSize = getFolderSize(File(logDir))
|
||||||
|
if (folderSize >= maxFolderSize) {
|
||||||
|
deleteOldestFile()
|
||||||
|
}
|
||||||
|
val fileSize = currentFile?.length() ?: 0L
|
||||||
|
if (currentFile == null || fileSize >= maxFileSize) {
|
||||||
|
outputStream?.close()
|
||||||
|
currentFile = File(logDir, "logcat_${System.currentTimeMillis()}.txt")
|
||||||
|
outputStream = FileOutputStream(currentFile!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getFolderSize(dir: File): Long {
|
||||||
|
var size = 0L
|
||||||
|
if (dir.isDirectory && dir.listFiles() != null) {
|
||||||
|
dir.listFiles()!!.forEach { file ->
|
||||||
|
size += if (file.isDirectory) getFolderSize(file) else file.length()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deleteOldestFile() {
|
||||||
|
File(logDir).listFiles()
|
||||||
|
?.toList()
|
||||||
|
?.minByOrNull { it.lastModified() }
|
||||||
|
?.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,8 @@ import com.zaneschepke.logcatter.model.LogMessage
|
|||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
interface LogReader {
|
interface LogReader {
|
||||||
fun initialize(onLogMessage: ((message: LogMessage) -> Unit)? = null)
|
fun start()
|
||||||
|
fun stop()
|
||||||
fun zipLogFiles(path: String)
|
fun zipLogFiles(path: String)
|
||||||
suspend fun deleteAndClearLogs()
|
suspend fun deleteAndClearLogs()
|
||||||
val bufferedLogs: Flow<LogMessage>
|
val bufferedLogs: Flow<LogMessage>
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package com.zaneschepke.logcatter
|
||||||
|
|
||||||
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import com.zaneschepke.logcatter.model.LogMessage
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class LogcatManager(
|
||||||
|
pid: Int,
|
||||||
|
logDir: String,
|
||||||
|
maxFileSize: Long,
|
||||||
|
maxFolderSize: Long,
|
||||||
|
) : LogReader, DefaultLifecycleObserver {
|
||||||
|
private val logScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||||
|
private val fileManager = LogFileManager(logDir, maxFileSize, maxFolderSize)
|
||||||
|
private val logcatReader = LogcatStreamReader(pid, fileManager)
|
||||||
|
private var logJob: Job? = null
|
||||||
|
private var isStarted = false
|
||||||
|
|
||||||
|
private val _bufferedLogs = MutableSharedFlow<LogMessage>(
|
||||||
|
replay = 10_000,
|
||||||
|
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||||
|
)
|
||||||
|
private val _liveLogs = MutableSharedFlow<LogMessage>(
|
||||||
|
replay = 1,
|
||||||
|
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
override val bufferedLogs: Flow<LogMessage> = _bufferedLogs.asSharedFlow()
|
||||||
|
override val liveLogs: Flow<LogMessage> = _liveLogs.asSharedFlow()
|
||||||
|
|
||||||
|
override fun onCreate(owner: LifecycleOwner) {
|
||||||
|
// for auto start
|
||||||
|
// start()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy(owner: LifecycleOwner) {
|
||||||
|
stop()
|
||||||
|
logScope.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun start() {
|
||||||
|
if (isStarted) return
|
||||||
|
stop()
|
||||||
|
logJob = logScope.launch {
|
||||||
|
logcatReader.readLogs().collect { logMessage ->
|
||||||
|
_bufferedLogs.emit(logMessage)
|
||||||
|
_liveLogs.emit(logMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isStarted = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stop() {
|
||||||
|
if (!isStarted) return
|
||||||
|
logJob?.cancel()
|
||||||
|
logcatReader.stop()
|
||||||
|
fileManager.close()
|
||||||
|
isStarted = false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun zipLogFiles(path: String) {
|
||||||
|
logScope.launch {
|
||||||
|
val wasStarted = isStarted
|
||||||
|
stop()
|
||||||
|
fileManager.zipLogs(path)
|
||||||
|
if (wasStarted) {
|
||||||
|
logcatReader.clearLogs()
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
override suspend fun deleteAndClearLogs() {
|
||||||
|
val wasStarted = isStarted
|
||||||
|
stop()
|
||||||
|
_bufferedLogs.resetReplayCache()
|
||||||
|
fileManager.deleteAllLogs()
|
||||||
|
if (wasStarted) start()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,250 +1,32 @@
|
|||||||
package com.zaneschepke.logcatter
|
package com.zaneschepke.logcatter
|
||||||
|
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
|
||||||
import androidx.lifecycle.ProcessLifecycleOwner
|
import androidx.lifecycle.ProcessLifecycleOwner
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.zaneschepke.logcatter.model.LogMessage
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.channels.BufferOverflow
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import timber.log.Timber
|
|
||||||
import java.io.BufferedOutputStream
|
|
||||||
import java.io.BufferedReader
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.io.IOException
|
|
||||||
import java.io.InputStreamReader
|
|
||||||
import java.util.zip.ZipEntry
|
|
||||||
import java.util.zip.ZipOutputStream
|
|
||||||
|
|
||||||
object LogcatReader {
|
object LogcatReader {
|
||||||
|
|
||||||
private const val MAX_FILE_SIZE = 2097152L // 2MB
|
private const val MAX_FILE_SIZE = 2097152L // 2MB
|
||||||
private const val MAX_FOLDER_SIZE = 10485760L // 10MB
|
private const val MAX_FOLDER_SIZE = 10485760L // 10MB
|
||||||
|
|
||||||
private val findKeyRegex = """[A-Za-z0-9+/]{42}[AEIMQUYcgkosw480]=""".toRegex()
|
private lateinit var logcatManager: LogcatManager
|
||||||
private val findIpv6AddressRegex = """^([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}${'$'}""".toRegex()
|
private var isInitialized = false
|
||||||
private val findIpv4AddressRegex = """((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}""".toRegex()
|
|
||||||
private val findTunnelNameRegex = """(?<=tunnel ).*?(?= UP| DOWN)""".toRegex()
|
|
||||||
|
|
||||||
private val ioDispatcher = Dispatchers.IO
|
|
||||||
|
|
||||||
private object LogcatHelperInit {
|
|
||||||
var maxFileSize: Long = MAX_FILE_SIZE
|
|
||||||
var maxFolderSize: Long = MAX_FOLDER_SIZE
|
|
||||||
var pID: Int = 0
|
|
||||||
var publicAppDirectory = ""
|
|
||||||
var logcatPath = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
fun init(maxFileSize: Long = MAX_FILE_SIZE, maxFolderSize: Long = MAX_FOLDER_SIZE, storageDir: String): LogReader {
|
fun init(maxFileSize: Long = MAX_FILE_SIZE, maxFolderSize: Long = MAX_FOLDER_SIZE, storageDir: String): LogReader {
|
||||||
if (maxFileSize > maxFolderSize) {
|
if (maxFileSize > maxFolderSize) {
|
||||||
throw IllegalStateException("maxFileSize must be less than maxFolderSize")
|
throw IllegalStateException("maxFileSize must be less than maxFolderSize")
|
||||||
}
|
}
|
||||||
synchronized(LogcatHelperInit) {
|
synchronized(this) {
|
||||||
LogcatHelperInit.maxFileSize = maxFileSize
|
if (isInitialized) return logcatManager
|
||||||
LogcatHelperInit.maxFolderSize = maxFolderSize
|
val logDir = "$storageDir${File.separator}logs"
|
||||||
LogcatHelperInit.pID = android.os.Process.myPid()
|
File(logDir).mkdirs()
|
||||||
LogcatHelperInit.publicAppDirectory = storageDir
|
logcatManager = LogcatManager(
|
||||||
LogcatHelperInit.logcatPath = LogcatHelperInit.publicAppDirectory + File.separator + "logs"
|
pid = android.os.Process.myPid(),
|
||||||
val logDirectory = File(LogcatHelperInit.logcatPath)
|
logDir = logDir,
|
||||||
if (!logDirectory.exists()) {
|
maxFileSize = maxFileSize,
|
||||||
logDirectory.mkdir()
|
maxFolderSize = maxFolderSize,
|
||||||
}
|
|
||||||
return Logcat
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal object Logcat : LogReader {
|
|
||||||
|
|
||||||
private lateinit var logcatReader: LogcatReader
|
|
||||||
|
|
||||||
override fun initialize(onLogMessage: ((message: LogMessage) -> Unit)?) {
|
|
||||||
logcatReader = LogcatReader(LogcatHelperInit.pID.toString(), LogcatHelperInit.logcatPath, onLogMessage)
|
|
||||||
ProcessLifecycleOwner.get().lifecycle.addObserver(logcatReader)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun obfuscator(log: String): String {
|
|
||||||
return findKeyRegex.replace(log, "<crypto-key>").let { first ->
|
|
||||||
findIpv6AddressRegex.replace(first, "<ipv6-address>").let { second ->
|
|
||||||
findTunnelNameRegex.replace(second, "<tunnel>")
|
|
||||||
}
|
|
||||||
}.let { last -> findIpv4AddressRegex.replace(last, "<ipv4-address>") }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun zipLogFiles(path: String) {
|
|
||||||
logcatReader.cancel()
|
|
||||||
zipAll(path)
|
|
||||||
logcatReader.onCreate(ProcessLifecycleOwner.get())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun zipAll(zipFilePath: String) {
|
|
||||||
val sourceFile = File(LogcatHelperInit.logcatPath)
|
|
||||||
val outputZipFile = File(zipFilePath)
|
|
||||||
ZipOutputStream(BufferedOutputStream(FileOutputStream(outputZipFile))).use { zos ->
|
|
||||||
sourceFile.walkTopDown().forEach { file ->
|
|
||||||
val zipFileName = file.absolutePath.removePrefix(sourceFile.absolutePath).removePrefix("/")
|
|
||||||
val entry = ZipEntry("$zipFileName${(if (file.isDirectory) "/" else "")}")
|
|
||||||
zos.putNextEntry(entry)
|
|
||||||
if (file.isFile) {
|
|
||||||
file.inputStream().use {
|
|
||||||
it.copyTo(zos)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
override suspend fun deleteAndClearLogs() {
|
|
||||||
withContext(ioDispatcher) {
|
|
||||||
logcatReader.cancel()
|
|
||||||
_bufferedLogs.resetReplayCache()
|
|
||||||
logcatReader.deleteAllFiles()
|
|
||||||
logcatReader.onCreate(ProcessLifecycleOwner.get())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val _bufferedLogs = MutableSharedFlow<LogMessage>(
|
|
||||||
replay = 10_000,
|
|
||||||
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
|
||||||
)
|
)
|
||||||
private val _liveLogs = MutableSharedFlow<LogMessage>(
|
ProcessLifecycleOwner.get().lifecycle.addObserver(logcatManager)
|
||||||
replay = 1,
|
isInitialized = true
|
||||||
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
return logcatManager
|
||||||
)
|
|
||||||
|
|
||||||
override val bufferedLogs: Flow<LogMessage> = _bufferedLogs.asSharedFlow()
|
|
||||||
|
|
||||||
override val liveLogs: Flow<LogMessage> = _liveLogs.asSharedFlow()
|
|
||||||
|
|
||||||
private class LogcatReader(
|
|
||||||
pID: String,
|
|
||||||
private val logcatPath: String,
|
|
||||||
private val callback: ((input: LogMessage) -> Unit)?,
|
|
||||||
) : DefaultLifecycleObserver {
|
|
||||||
private var logcatProc: Process? = null
|
|
||||||
private var reader: BufferedReader? = null
|
|
||||||
|
|
||||||
private val command = "logcat -v epoch | grep \"($pID)\""
|
|
||||||
private val clearLogCommand = "logcat -c"
|
|
||||||
private var logJob: Job? = null
|
|
||||||
private var outputStream: FileOutputStream? = null
|
|
||||||
|
|
||||||
override fun onCreate(owner: LifecycleOwner) {
|
|
||||||
super.onCreate(owner)
|
|
||||||
logJob = owner.lifecycleScope.launch(ioDispatcher) {
|
|
||||||
try {
|
|
||||||
if (outputStream == null) outputStream = createNewLogFileStream()
|
|
||||||
clear()
|
|
||||||
logcatProc = Runtime.getRuntime().exec(command)
|
|
||||||
reader = BufferedReader(InputStreamReader(logcatProc!!.inputStream), 1024)
|
|
||||||
var line: String? = null
|
|
||||||
while (true) {
|
|
||||||
line = reader?.readLine()
|
|
||||||
if (line.isNullOrEmpty()) continue
|
|
||||||
outputStream?.let {
|
|
||||||
if (it.channel.size() >= LogcatHelperInit.maxFileSize) {
|
|
||||||
it.close()
|
|
||||||
outputStream = createNewLogFileStream()
|
|
||||||
}
|
|
||||||
if (getFolderSize(logcatPath) >= LogcatHelperInit.maxFolderSize) {
|
|
||||||
deleteOldestFile()
|
|
||||||
}
|
|
||||||
line.let { text ->
|
|
||||||
val sanitized = obfuscator(text)
|
|
||||||
it.write((sanitized + System.lineSeparator()).toByteArray())
|
|
||||||
try {
|
|
||||||
val logMessage = LogMessage.from(text)
|
|
||||||
_bufferedLogs.tryEmit(logMessage)
|
|
||||||
_liveLogs.tryEmit(logMessage)
|
|
||||||
callback?.let {
|
|
||||||
it(logMessage)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Timber.e(e)
|
|
||||||
} finally {
|
|
||||||
reset()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
logJob?.invokeOnCompletion {
|
|
||||||
reset()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy(owner: LifecycleOwner) {
|
|
||||||
super.onDestroy(owner)
|
|
||||||
logJob?.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun cancel() {
|
|
||||||
logJob?.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun reset() {
|
|
||||||
logcatProc?.destroy()
|
|
||||||
logcatProc = null
|
|
||||||
reader?.close()
|
|
||||||
outputStream?.close()
|
|
||||||
reader = null
|
|
||||||
outputStream = null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clear() {
|
|
||||||
Runtime.getRuntime().exec(clearLogCommand)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getFolderSize(path: String): Long {
|
|
||||||
File(path).run {
|
|
||||||
var size = 0L
|
|
||||||
if (this.isDirectory && this.listFiles() != null) {
|
|
||||||
for (file in this.listFiles()!!) {
|
|
||||||
size += getFolderSize(file.absolutePath)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
size = this.length()
|
|
||||||
}
|
|
||||||
return size
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createLogFile(dir: String): File {
|
|
||||||
return File(dir, "logcat_" + System.currentTimeMillis() + ".txt")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deleteOldestFile() {
|
|
||||||
val directory = File(logcatPath)
|
|
||||||
if (directory.isDirectory) {
|
|
||||||
directory.listFiles()?.toMutableList()?.run {
|
|
||||||
this.sortBy { it.lastModified() }
|
|
||||||
this.first().delete()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createNewLogFileStream(): FileOutputStream {
|
|
||||||
return FileOutputStream(createLogFile(logcatPath))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deleteAllFiles() {
|
|
||||||
val directory = File(logcatPath)
|
|
||||||
directory.listFiles()?.toMutableList()?.run {
|
|
||||||
this.forEach { it.delete() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package com.zaneschepke.logcatter
|
||||||
|
|
||||||
|
import com.zaneschepke.logcatter.model.LogMessage
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import java.io.BufferedReader
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStreamReader
|
||||||
|
|
||||||
|
class LogcatStreamReader(
|
||||||
|
private val pid: Int,
|
||||||
|
private val fileManager: LogFileManager,
|
||||||
|
) {
|
||||||
|
private val bufferSize = 1024
|
||||||
|
private var process: Process? = null
|
||||||
|
private var reader: BufferedReader? = null
|
||||||
|
private val command = "logcat -v epoch | grep \"($pid)\""
|
||||||
|
private val clearCommand = "logcat -c"
|
||||||
|
|
||||||
|
private val ioDispatcher = Dispatchers.IO
|
||||||
|
|
||||||
|
fun readLogs(): Flow<LogMessage> = flow {
|
||||||
|
try {
|
||||||
|
clearLogs()
|
||||||
|
process = Runtime.getRuntime().exec(command)
|
||||||
|
reader = BufferedReader(InputStreamReader(process!!.inputStream), bufferSize)
|
||||||
|
reader!!.lineSequence().forEach { line ->
|
||||||
|
if (line.isNotEmpty()) {
|
||||||
|
fileManager.writeLog(line)
|
||||||
|
emit(LogMessage.from(line))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
// do nothing
|
||||||
|
} finally {
|
||||||
|
stop()
|
||||||
|
}
|
||||||
|
}.flowOn(ioDispatcher)
|
||||||
|
|
||||||
|
fun start() {
|
||||||
|
if (process == null) {
|
||||||
|
try {
|
||||||
|
process = Runtime.getRuntime().exec(command)
|
||||||
|
reader = BufferedReader(InputStreamReader(process!!.inputStream), bufferSize)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stop() {
|
||||||
|
process?.destroy()
|
||||||
|
reader?.close()
|
||||||
|
process = null
|
||||||
|
reader = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearLogs() {
|
||||||
|
Runtime.getRuntime().exec(clearCommand)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/build
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.androidLibrary)
|
||||||
|
alias(libs.plugins.kotlin.android)
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.zaneschepke.networkmonitor"
|
||||||
|
compileSdk = 34
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = 26
|
||||||
|
|
||||||
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
consumerProguardFiles("consumer-rules.pro")
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
isMinifyEnabled = false
|
||||||
|
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||||
|
}
|
||||||
|
create(Constants.PRERELEASE) {
|
||||||
|
initWith(getByName(Constants.RELEASE))
|
||||||
|
}
|
||||||
|
|
||||||
|
create(Constants.NIGHTLY) {
|
||||||
|
initWith(getByName(Constants.RELEASE))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = Constants.JVM_TARGET
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
|
||||||
|
implementation(libs.androidx.core.ktx)
|
||||||
|
implementation(libs.androidx.appcompat)
|
||||||
|
implementation(libs.material)
|
||||||
|
testImplementation(libs.junit)
|
||||||
|
androidTestImplementation(libs.androidx.junit)
|
||||||
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
|
|
||||||
|
implementation(libs.tunnel)
|
||||||
|
|
||||||
|
implementation(libs.timber)
|
||||||
|
}
|
||||||
Vendored
+21
@@ -0,0 +1,21 @@
|
|||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# If you keep the line number information, uncomment this to
|
||||||
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
package com.zaneschepke.networkmonitor
|
||||||
|
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
import org.junit.Assert.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instrumented test, which will execute on an Android device.
|
||||||
|
*
|
||||||
|
* See [testing documentation](http://d.android.com/tools/testing).
|
||||||
|
*/
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class ExampleInstrumentedTest {
|
||||||
|
@Test
|
||||||
|
fun useAppContext() {
|
||||||
|
// Context of the app under test.
|
||||||
|
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
assertEquals("com.zaneschepke.networkmonitor.test", appContext.packageName)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
>
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
package com.zaneschepke.networkmonitor
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.Network
|
||||||
|
import android.net.NetworkCapabilities
|
||||||
|
import android.net.NetworkRequest
|
||||||
|
import android.net.wifi.WifiManager
|
||||||
|
import com.wireguard.android.util.RootShell
|
||||||
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
class AndroidNetworkMonitor(
|
||||||
|
context: Context,
|
||||||
|
) : NetworkMonitor {
|
||||||
|
|
||||||
|
private val appContext = context.applicationContext
|
||||||
|
private val connectivityManager = appContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||||
|
private val wifiManager = appContext.getSystemService(Context.WIFI_SERVICE) as WifiManager?
|
||||||
|
private val rootShell = RootShell(context)
|
||||||
|
|
||||||
|
private var includeWifiSsid = false
|
||||||
|
private var useRootShell = false
|
||||||
|
|
||||||
|
data class WifiState(val connected: Boolean = false, val ssid: String? = null)
|
||||||
|
data class TransportState(val connected: Boolean = false)
|
||||||
|
|
||||||
|
private val wifiFlow: Flow<WifiState> = callbackFlow {
|
||||||
|
var currentSsid: String? = null
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
fun getWifiSsid(): String? {
|
||||||
|
if (!includeWifiSsid || wifiManager == null) return null
|
||||||
|
return if (useRootShell) {
|
||||||
|
rootShell.getCurrentWifiName()
|
||||||
|
} else {
|
||||||
|
wifiManager.connectionInfo?.ssid?.trim('"')?.takeIf {
|
||||||
|
it != "<unknown>" && it.isNotEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val callback = object : ConnectivityManager.NetworkCallback() {
|
||||||
|
override fun onAvailable(network: Network) {
|
||||||
|
Timber.d("Wi-Fi onAvailable: network=$network")
|
||||||
|
val capabilities = connectivityManager.getNetworkCapabilities(network)
|
||||||
|
val connected = capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true
|
||||||
|
if (connected) {
|
||||||
|
val ssid = getWifiSsid()
|
||||||
|
currentSsid = ssid
|
||||||
|
trySend(WifiState(connected = true, ssid = ssid))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLost(network: Network) {
|
||||||
|
Timber.d("Wi-Fi onLost: network=$network")
|
||||||
|
currentSsid = null
|
||||||
|
trySend(WifiState(connected = false, ssid = null))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
|
||||||
|
val connected = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
||||||
|
val ssid = if (connected) getWifiSsid() else null
|
||||||
|
if (ssid != currentSsid) {
|
||||||
|
currentSsid = ssid
|
||||||
|
trySend(WifiState(connected = connected, ssid = ssid))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val request = NetworkRequest.Builder()
|
||||||
|
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||||
|
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
connectivityManager.registerNetworkCallback(request, callback)
|
||||||
|
trySend(WifiState())
|
||||||
|
|
||||||
|
awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private val cellularFlow: Flow<TransportState> = callbackFlow {
|
||||||
|
val callback = object : ConnectivityManager.NetworkCallback() {
|
||||||
|
override fun onAvailable(network: Network) {
|
||||||
|
Timber.d("Cellular onAvailable: network=$network")
|
||||||
|
trySend(TransportState(connected = true))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLost(network: Network) {
|
||||||
|
Timber.d("Cellular onLost: network=$network")
|
||||||
|
trySend(TransportState(connected = false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val request = NetworkRequest.Builder()
|
||||||
|
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||||
|
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
connectivityManager.registerNetworkCallback(request, callback)
|
||||||
|
trySend(TransportState())
|
||||||
|
|
||||||
|
awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private val ethernetFlow: Flow<TransportState> = callbackFlow {
|
||||||
|
val callback = object : ConnectivityManager.NetworkCallback() {
|
||||||
|
override fun onAvailable(network: Network) {
|
||||||
|
Timber.d("Ethernet onAvailable: network=$network")
|
||||||
|
trySend(TransportState(connected = true))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLost(network: Network) {
|
||||||
|
Timber.d("Ethernet onLost: network=$network")
|
||||||
|
trySend(TransportState(connected = false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val request = NetworkRequest.Builder()
|
||||||
|
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||||
|
.addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
connectivityManager.registerNetworkCallback(request, callback)
|
||||||
|
trySend(TransportState())
|
||||||
|
|
||||||
|
awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getNetworkStatusFlow(includeWifiSsid: Boolean, useRootShell: Boolean): Flow<NetworkStatus> {
|
||||||
|
this.includeWifiSsid = includeWifiSsid
|
||||||
|
this.useRootShell = useRootShell
|
||||||
|
return combine(wifiFlow, cellularFlow, ethernetFlow) { wifi, cellular, ethernet ->
|
||||||
|
val hasAnyConnection = wifi.connected || cellular.connected || ethernet.connected
|
||||||
|
if (hasAnyConnection) {
|
||||||
|
NetworkStatus.Connected(
|
||||||
|
wifiSsid = wifi.ssid,
|
||||||
|
wifiConnected = wifi.connected,
|
||||||
|
cellularConnected = cellular.connected,
|
||||||
|
ethernetConnected = ethernet.connected,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
NetworkStatus.Disconnected
|
||||||
|
}.also { Timber.d("NetworkStatus: $it") }
|
||||||
|
}.distinctUntilChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.zaneschepke.networkmonitor
|
||||||
|
|
||||||
|
import com.wireguard.android.util.RootShell
|
||||||
|
|
||||||
|
fun RootShell.getCurrentWifiName(): String? {
|
||||||
|
val response = mutableListOf<String>()
|
||||||
|
this.run(response, "dumpsys wifi | grep 'Supplicant state: COMPLETED' | grep -o 'SSID: [^,]*' | cut -d ' ' -f2- | tr -d '\"'")
|
||||||
|
return response.firstOrNull()
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.zaneschepke.networkmonitor
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
interface NetworkMonitor {
|
||||||
|
fun getNetworkStatusFlow(includeWifiSsid: Boolean, useRootShell: Boolean): Flow<NetworkStatus>
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.zaneschepke.networkmonitor
|
||||||
|
|
||||||
|
sealed class NetworkStatus {
|
||||||
|
data object Disconnected : NetworkStatus() {
|
||||||
|
override val wifiConnected = false
|
||||||
|
override val ethernetConnected = false
|
||||||
|
override val cellularConnected = false
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Connected(
|
||||||
|
val wifiSsid: String? = null,
|
||||||
|
override val wifiConnected: Boolean = false,
|
||||||
|
override val ethernetConnected: Boolean = false,
|
||||||
|
override val cellularConnected: Boolean = false,
|
||||||
|
) : NetworkStatus()
|
||||||
|
|
||||||
|
abstract val wifiConnected: Boolean
|
||||||
|
abstract val ethernetConnected: Boolean
|
||||||
|
abstract val cellularConnected: Boolean
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.zaneschepke.networkmonitor
|
||||||
|
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
import org.junit.Assert.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example local unit test, which will execute on the development machine (host).
|
||||||
|
*
|
||||||
|
* See [testing documentation](http://d.android.com/tools/testing).
|
||||||
|
*/
|
||||||
|
class ExampleUnitTest {
|
||||||
|
@Test
|
||||||
|
fun addition_isCorrect() {
|
||||||
|
assertEquals(4, 2 + 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,3 +34,4 @@ rootProject.name = "WG Tunnel"
|
|||||||
|
|
||||||
include(":app")
|
include(":app")
|
||||||
include(":logcatter")
|
include(":logcatter")
|
||||||
|
include(":networkmonitor")
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
1
|
4
|
||||||
Reference in New Issue
Block a user