mirror of
https://github.com/wgtunnel/android.git
synced 2026-07-03 14:07:49 +02:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d5adcb35c1 |
@@ -144,8 +144,8 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation(project(":logcatter"))
|
||||
implementation(project(":networkmonitor"))
|
||||
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
|
||||
+1
-38
@@ -2,41 +2,4 @@
|
||||
|
||||
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
|
||||
<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
+1
-38
@@ -21,41 +21,4 @@
|
||||
#-renamesourcefileattribute SourceFile
|
||||
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
|
||||
<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,8 +3,13 @@
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<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-->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"
|
||||
@@ -163,17 +168,25 @@
|
||||
</service>
|
||||
|
||||
<receiver
|
||||
android:name=".core.broadcast.RestartReceiver"
|
||||
android:name=".core.broadcast.BootReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="true">
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<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="com.htc.intent.action.QUICKBOOT_POWERON" />
|
||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver
|
||||
android:name=".core.broadcast.AppUpdateReceiver"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver
|
||||
android:name=".core.broadcast.KernelReceiver"
|
||||
android:exported="false"
|
||||
|
||||
@@ -40,6 +40,7 @@ import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.toRoute
|
||||
import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager
|
||||
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.ui.Route
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
|
||||
@@ -126,6 +127,9 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO could improve this to cancel when no tuns or autotun on
|
||||
ServiceWorker.start(this)
|
||||
|
||||
CompositionLocalProvider(LocalNavController provides navController) {
|
||||
SnackbarControllerProvider { host ->
|
||||
WireguardAutoTunnelTheme(theme = appUiState.generalState.theme) {
|
||||
@@ -218,9 +222,8 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
composable<Route.TunnelOptions> { backStack ->
|
||||
val args = backStack.toRoute<Route.TunnelOptions>()
|
||||
appUiState.tunnels.firstOrNull { it.id == args.id }?.let { config ->
|
||||
OptionsScreen(config, appUiState)
|
||||
}
|
||||
val config = appUiState.tunnels.first { it.id == args.id }
|
||||
OptionsScreen(config)
|
||||
}
|
||||
composable<Route.Lock> {
|
||||
PinLockScreen(viewModel)
|
||||
@@ -233,15 +236,13 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
composable<Route.SplitTunnel> { backStack ->
|
||||
val args = backStack.toRoute<Route.SplitTunnel>()
|
||||
appUiState.tunnels.firstOrNull { it.id == args.id }?.let {
|
||||
SplitTunnelScreen(it, viewModel)
|
||||
}
|
||||
val config = appUiState.tunnels.first { it.id == args.id }
|
||||
SplitTunnelScreen(config, viewModel)
|
||||
}
|
||||
composable<Route.TunnelAutoTunnel> { backStack ->
|
||||
val args = backStack.toRoute<Route.TunnelOptions>()
|
||||
appUiState.tunnels.firstOrNull { it.id == args.id }?.let {
|
||||
TunnelAutoTunnelScreen(it, appUiState.appSettings)
|
||||
}
|
||||
val config = appUiState.tunnels.first { it.id == args.id }
|
||||
TunnelAutoTunnelScreen(config, appUiState.appSettings)
|
||||
}
|
||||
}
|
||||
BackHandler {
|
||||
|
||||
@@ -11,7 +11,6 @@ import androidx.work.Configuration
|
||||
import com.wireguard.android.backend.GoBackend
|
||||
import com.zaneschepke.logcatter.LogReader
|
||||
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.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.di.MainDispatcher
|
||||
@@ -92,11 +91,9 @@ class WireGuardAutoTunnel : Application(), Configuration.Provider {
|
||||
}
|
||||
}
|
||||
|
||||
ServiceWorker.start(this)
|
||||
|
||||
applicationScope.launch {
|
||||
withContext(mainDispatcher) {
|
||||
if (appDataRepository.appState.isLocalLogsEnabled() && !isRunningOnTv()) logReader.start()
|
||||
if (appDataRepository.appState.isLocalLogsEnabled() && !isRunningOnTv()) logReader.initialize()
|
||||
}
|
||||
if (!appDataRepository.settings.get().isKernelEnabled) {
|
||||
tunnelManager.setBackendState(BackendState.SERVICE_ACTIVE, emptyList())
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-64
@@ -1,64 +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 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
@@ -0,0 +1,161 @@
|
||||
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
@@ -0,0 +1,16 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
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?
|
||||
}
|
||||
+53
-94
@@ -3,35 +3,29 @@ package com.zaneschepke.wireguardautotunnel.core.service
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
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.tile.AutoTunnelControlTile
|
||||
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.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
|
||||
import jakarta.inject.Inject
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import timber.log.Timber
|
||||
|
||||
class ServiceManager @Inject constructor(
|
||||
private val context: Context,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
@ApplicationScope private val applicationScope: CoroutineScope,
|
||||
private val appDataRepository: AppDataRepository,
|
||||
) {
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class ServiceManager
|
||||
@Inject constructor(private val context: Context, private val ioDispatcher: CoroutineDispatcher, private val appDataRepository: AppDataRepository) {
|
||||
|
||||
private val _autoTunnelActive = MutableStateFlow(false)
|
||||
|
||||
val autoTunnelActive = _autoTunnelActive.asStateFlow()
|
||||
|
||||
var autoTunnelService = CompletableDeferred<AutoTunnelService>()
|
||||
@@ -50,111 +44,76 @@ class ServiceManager @Inject constructor(
|
||||
}.onFailure { Timber.e(it) }
|
||||
}
|
||||
|
||||
fun startAutoTunnel(background: Boolean) {
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
val settings = appDataRepository.settings.get()
|
||||
appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = true))
|
||||
if (autoTunnelService.isCompleted) {
|
||||
_autoTunnelActive.update { true }
|
||||
return@launch
|
||||
}
|
||||
runCatching {
|
||||
autoTunnelService = CompletableDeferred()
|
||||
startService(AutoTunnelService::class.java, background)
|
||||
val service = withTimeoutOrNull(SERVICE_START_TIMEOUT) { autoTunnelService.await() }
|
||||
?: throw IllegalStateException("AutoTunnelService start timed out")
|
||||
service.start()
|
||||
_autoTunnelActive.update { true }
|
||||
updateAutoTunnelTile()
|
||||
}.onFailure {
|
||||
Timber.e(it)
|
||||
_autoTunnelActive.update { false }
|
||||
}
|
||||
suspend fun startAutoTunnel(background: Boolean) {
|
||||
val settings = appDataRepository.settings.get()
|
||||
appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = true))
|
||||
if (autoTunnelService.isCompleted) return _autoTunnelActive.update { true }
|
||||
runCatching {
|
||||
startService(AutoTunnelService::class.java, background)
|
||||
autoTunnelService.await()
|
||||
autoTunnelService.getCompleted().start()
|
||||
_autoTunnelActive.update { true }
|
||||
updateAutoTunnelTile()
|
||||
}.onFailure {
|
||||
Timber.e(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun startBackgroundService(tunnelConf: TunnelConf) {
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
if (backgroundService.isCompleted) return@launch
|
||||
runCatching {
|
||||
backgroundService = CompletableDeferred()
|
||||
startService(TunnelForegroundService::class.java, true)
|
||||
val service = withTimeoutOrNull(SERVICE_START_TIMEOUT) { backgroundService.await() }
|
||||
?: throw IllegalStateException("Background service start timed out")
|
||||
service.start(tunnelConf)
|
||||
}.onFailure {
|
||||
Timber.e(it)
|
||||
}
|
||||
suspend fun startBackgroundService(tunnelConf: TunnelConf) {
|
||||
if (backgroundService.isCompleted) return
|
||||
runCatching {
|
||||
startService(TunnelForegroundService::class.java, true)
|
||||
backgroundService.await()
|
||||
backgroundService.getCompleted().start(tunnelConf)
|
||||
}.onFailure {
|
||||
Timber.e(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun stopBackgroundService() {
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
if (!backgroundService.isCompleted) return@launch
|
||||
runCatching {
|
||||
val service = backgroundService.await()
|
||||
service.stop()
|
||||
backgroundService = CompletableDeferred()
|
||||
}.onFailure {
|
||||
Timber.e(it)
|
||||
}
|
||||
if (!backgroundService.isCompleted) return
|
||||
runCatching {
|
||||
backgroundService.getCompleted().stop()
|
||||
}.onFailure {
|
||||
Timber.e(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleAutoTunnel(background: Boolean) {
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
if (_autoTunnelActive.value) stopAutoTunnel() else startAutoTunnel(background)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateAutoTunnelTile() {
|
||||
suspend fun toggleAutoTunnel(background: Boolean) {
|
||||
withContext(ioDispatcher) {
|
||||
runCatching {
|
||||
val service = withTimeoutOrNull(SERVICE_START_TIMEOUT) { autoTunnelTile.await() }
|
||||
?: run {
|
||||
context.requestAutoTunnelTileServiceUpdate()
|
||||
return@withContext
|
||||
}
|
||||
service.updateTileState()
|
||||
}.onFailure {
|
||||
Timber.e(it)
|
||||
}
|
||||
if (_autoTunnelActive.value) return@withContext stopAutoTunnel()
|
||||
startAutoTunnel(background)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateTunnelTile() {
|
||||
fun updateAutoTunnelTile() {
|
||||
if (autoTunnelTile.isCompleted) {
|
||||
autoTunnelTile.getCompleted().updateTileState()
|
||||
} else {
|
||||
context.requestAutoTunnelTileServiceUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateTunnelTile() {
|
||||
if (tunnelControlTile.isCompleted) {
|
||||
tunnelControlTile.getCompleted().updateTileState()
|
||||
} else {
|
||||
context.requestTunnelTileServiceStateUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun stopAutoTunnel() {
|
||||
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()
|
||||
appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = false))
|
||||
if (!autoTunnelService.isCompleted) return@launch
|
||||
if (!autoTunnelService.isCompleted) return@withContext
|
||||
runCatching {
|
||||
val service = autoTunnelService.await()
|
||||
service.stop()
|
||||
autoTunnelService.getCompleted().stop()
|
||||
_autoTunnelActive.update { false }
|
||||
autoTunnelService = CompletableDeferred()
|
||||
updateAutoTunnelTile()
|
||||
}.onFailure {
|
||||
Timber.e(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val SERVICE_START_TIMEOUT = 5_000L
|
||||
}
|
||||
}
|
||||
|
||||
+46
-45
@@ -1,42 +1,44 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.autotunnel
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.NetworkCapabilities
|
||||
import android.os.IBinder
|
||||
import android.os.PowerManager
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.lifecycle.LifecycleService
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||
import com.zaneschepke.networkmonitor.NetworkStatus
|
||||
import com.wireguard.android.util.RootShell
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||
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.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.domain.entity.AppSettings
|
||||
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.KillSwitchEvent
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||
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.WireGuardNotification
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.KillSwitchEvent
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.Tunnels
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.getCurrentWifiName
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -47,6 +49,10 @@ import javax.inject.Provider
|
||||
@AndroidEntryPoint
|
||||
class AutoTunnelService : LifecycleService() {
|
||||
|
||||
@Inject
|
||||
@AppShell
|
||||
lateinit var rootShell: Provider<RootShell>
|
||||
|
||||
@Inject
|
||||
lateinit var networkMonitor: NetworkMonitor
|
||||
|
||||
@@ -155,54 +161,49 @@ class AutoTunnelService : LifecycleService() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildNetworkState(networkStatus: NetworkStatus): NetworkState {
|
||||
private suspend fun buildNetworkState(connectivityState: ConnectivityState): NetworkState {
|
||||
return with(autoTunnelStateFlow.value.networkState) {
|
||||
val wifiName = when (networkStatus) {
|
||||
is NetworkStatus.Connected -> {
|
||||
networkStatus.wifiSsid
|
||||
val wifiName = when {
|
||||
connectivityState.wifiAvailable &&
|
||||
(wifiName == null || wifiName == Constants.UNREADABLE_SSID || networkMonitor.didWifiChangeSinceLastCapabilitiesQuery) -> {
|
||||
networkMonitor.getWifiCapabilities()?.let { getWifiName(it) } ?: wifiName
|
||||
}
|
||||
else -> null
|
||||
!connectivityState.wifiAvailable -> null
|
||||
else -> wifiName
|
||||
}
|
||||
copy(
|
||||
isWifiConnected = networkStatus.wifiConnected,
|
||||
isMobileDataConnected = networkStatus.cellularConnected,
|
||||
isEthernetConnected = networkStatus.ethernetConnected,
|
||||
isWifiConnected = connectivityState.wifiAvailable,
|
||||
isMobileDataConnected = connectivityState.cellularAvailable,
|
||||
isEthernetConnected = isEthernetConnected,
|
||||
wifiName = wifiName,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private fun startAutoTunnelStateJob() = lifecycleScope.launch(ioDispatcher) {
|
||||
combine(
|
||||
combineSettings(),
|
||||
appDataRepository.get().settings.flow
|
||||
.distinctUntilChanged { old, new -> old.isKernelEnabled == new.isKernelEnabled } // Only emit when isKernelEnabled changes
|
||||
.flatMapLatest { settings ->
|
||||
networkMonitor.getNetworkStatusFlow(true, settings.isKernelEnabled)
|
||||
.flowOn(ioDispatcher)
|
||||
.map { buildNetworkState(it) }
|
||||
}
|
||||
.distinctUntilChanged(),
|
||||
networkMonitor.status.map {
|
||||
buildNetworkState(it)
|
||||
}.distinctUntilChanged(),
|
||||
) { double, networkState ->
|
||||
AutoTunnelState(
|
||||
tunnelManager.activeTunnels.value,
|
||||
networkState,
|
||||
double.first,
|
||||
double.second,
|
||||
)
|
||||
AutoTunnelState(tunnelManager.activeTunnels.value, networkState, double.first, double.second)
|
||||
}.collect { state ->
|
||||
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>> {
|
||||
return combine(
|
||||
appDataRepository.get().settings.flow,
|
||||
@@ -239,7 +240,7 @@ class AutoTunnelService : LifecycleService() {
|
||||
Timber.d("Starting with debounce delay of: ${settings.debounceDelaySeconds} seconds")
|
||||
autoTunnelStateFlow.debounce(settings.debounceDelayMillis()).collect { watcherState ->
|
||||
if (watcherState == defaultState) return@collect
|
||||
Timber.d("New auto tunnel state emitted ${watcherState.networkState}")
|
||||
Timber.d("New auto tunnel state emitted")
|
||||
when (val event = watcherState.asAutoTunnelEvent()) {
|
||||
is AutoTunnelEvent.Start -> (event.tunnelConf ?: appDataRepository.get().getPrimaryOrFirstTunnel())?.let {
|
||||
tunnelManager.startTunnel(it)
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
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.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.core.network.NetworkMonitor
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
@@ -21,7 +19,6 @@ import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.cancelWithMessage
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -31,12 +28,11 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
open class BaseTunnel(
|
||||
@@ -50,31 +46,31 @@ open class BaseTunnel(
|
||||
|
||||
internal val tunnels = MutableStateFlow<List<TunnelConf>>(emptyList())
|
||||
|
||||
private val _tunnelStates = MutableStateFlow<Map<Int, TunnelState>>(emptyMap())
|
||||
private val _activeTunnels = MutableStateFlow<Map<Int, TunnelState>>(emptyMap())
|
||||
|
||||
private val tunnelJobs = ConcurrentHashMap<Int, Job>()
|
||||
private val tunnelJobs = mutableMapOf<TunnelConf, Job>()
|
||||
|
||||
private val isNetworkAvailable = AtomicBoolean(false)
|
||||
|
||||
init {
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
launch { startNetworkJob() }
|
||||
launch { monitorTunnelConfigChanges() }
|
||||
launch {
|
||||
startNetworkJob()
|
||||
}
|
||||
tunnels.collect { tuns ->
|
||||
val previousTunIds = tunnelJobs.keys.toSet()
|
||||
val currentTunIds = tuns.map { it.id }.toSet()
|
||||
val newTuns = tuns.filter { it.id !in previousTunIds }
|
||||
val removedTunIds = previousTunIds - currentTunIds
|
||||
val previousTuns = tunnelJobs.keys.toSet()
|
||||
val newTuns = tuns - previousTuns
|
||||
val removedItems = previousTuns - tuns.toSet()
|
||||
|
||||
newTuns.forEach { tun ->
|
||||
Timber.d("Starting tunnel jobs for tun ${tun.name} (ID: ${tun.id})")
|
||||
tunnelJobs[tun.id] = startTunnelJobs(tun)
|
||||
Timber.d("Starting tunnel jobs for tun ${tun.name}")
|
||||
tunnelJobs[tun] = startTunnelJobs(tun)
|
||||
}
|
||||
|
||||
removedTunIds.forEach { tunId ->
|
||||
tunnelJobs[tunId]?.cancelWithMessage("Canceling tunnel jobs for tunnel ID: $tunId")
|
||||
tunnelJobs.remove(tunId)
|
||||
_tunnelStates.update { it - tunId }
|
||||
removedItems.forEach { tun ->
|
||||
tunnelJobs[tun]?.cancelWithMessage("Canceling tunnel jobs for tunnel: ${tun.name}")
|
||||
tunnelJobs.remove(tun)
|
||||
_activeTunnels.update { it - tun.id }
|
||||
serviceManager.updateTunnelTile()
|
||||
}
|
||||
}
|
||||
@@ -82,66 +78,52 @@ open class BaseTunnel(
|
||||
}
|
||||
|
||||
private fun startTunnelJobs(tunnel: TunnelConf) = applicationScope.launch(ioDispatcher) {
|
||||
launch { startTunnelStatisticsJob(tunnel) }
|
||||
if (tunnel.isPingEnabled) 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 {
|
||||
startTunnelStatisticsJob(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()
|
||||
}
|
||||
launch {
|
||||
startPingJob(tunnel)
|
||||
}
|
||||
}
|
||||
|
||||
override fun startTunnel(tunnelConf: TunnelConf) {
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
serviceManager.startBackgroundService(tunnelConf)
|
||||
appDataRepository.tunnels.save(tunnelConf.copy(isActive = true))
|
||||
addToActiveTunnels(tunnelConf)
|
||||
launch {
|
||||
startTunnelConfigChangeJob(tunnel)
|
||||
}
|
||||
launch {
|
||||
startStateJob(tunnel)
|
||||
}
|
||||
}
|
||||
|
||||
override fun stopTunnel(tunnelConf: TunnelConf?) {
|
||||
// Default empty implementation; subclasses override
|
||||
}
|
||||
|
||||
override suspend fun bounceTunnel(tunnelConf: TunnelConf) {
|
||||
stopTunnel(tunnelConf)
|
||||
delay(1000)
|
||||
startTunnel(tunnelConf)
|
||||
if (tunnels.value.any { it.id == tunnelConf.id }) {
|
||||
toggleTunnel(tunnelConf, TunnelStatus.DOWN)
|
||||
toggleTunnel(tunnelConf, TunnelStatus.UP)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
|
||||
// Default empty implementation
|
||||
}
|
||||
|
||||
override suspend fun runningTunnelNames(): Set<String> {
|
||||
return emptySet()
|
||||
}
|
||||
|
||||
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics {
|
||||
throw NotImplementedError("Get statistics not implemented in base class")
|
||||
override val activeTunnels: StateFlow<Map<Int, TunnelState>>
|
||||
get() = _activeTunnels.asStateFlow()
|
||||
|
||||
override suspend fun startTunnel(tunnelConf: TunnelConf) {
|
||||
serviceManager.startBackgroundService(tunnelConf)
|
||||
appDataRepository.tunnels.save(tunnelConf.copy(isActive = true))
|
||||
addToActiveTunnels(tunnelConf)
|
||||
}
|
||||
|
||||
override val activeTunnels: StateFlow<Map<Int, TunnelState>>
|
||||
get() = _tunnelStates.asStateFlow()
|
||||
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")
|
||||
}
|
||||
|
||||
internal suspend fun onTunnelStop(tunnelConf: TunnelConf) {
|
||||
appDataRepository.tunnels.save(tunnelConf.copy(isActive = false))
|
||||
@@ -149,7 +131,7 @@ open class BaseTunnel(
|
||||
if (tunnels.value.isEmpty()) serviceManager.stopBackgroundService()
|
||||
}
|
||||
|
||||
internal fun stopAllTunnels() {
|
||||
internal suspend fun stopAllTunnels() {
|
||||
tunnels.value.forEach {
|
||||
stopTunnel(it)
|
||||
}
|
||||
@@ -172,26 +154,32 @@ open class BaseTunnel(
|
||||
}
|
||||
|
||||
private suspend fun startNetworkJob() = coroutineScope {
|
||||
networkMonitor.getNetworkStatusFlow(includeWifiSsid = false, useRootShell = false)
|
||||
.flowOn(ioDispatcher).collect {
|
||||
isNetworkAvailable.set(it !is NetworkStatus.Disconnected)
|
||||
networkMonitor.status.distinctUntilChanged().collect {
|
||||
isNetworkAvailable.set(!it.allOffline)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun startStateJob(tunnel: TunnelConf) {
|
||||
tunnel.state.collect { state ->
|
||||
_activeTunnels.update {
|
||||
it + (tunnel.id to state)
|
||||
}
|
||||
serviceManager.updateTunnelTile()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun startPingJob(tunnel: TunnelConf) = coroutineScope {
|
||||
while (isActive) {
|
||||
runCatching {
|
||||
if (isNetworkAvailable.get() && tunnel.isActive) {
|
||||
val pingSuccess = tunnel.isTunnelPingable(ioDispatcher)
|
||||
handlePingResult(tunnel, pingSuccess)
|
||||
}
|
||||
delay(tunnel.pingInterval ?: Constants.PING_INTERVAL)
|
||||
if (isNetworkAvailable.get() && tunnel.isActive) {
|
||||
val pingResult = tunnel.pingTunnel(ioDispatcher)
|
||||
handlePingResult(tunnel, pingResult)
|
||||
}
|
||||
delay(tunnel.pingInterval ?: Constants.PING_INTERVAL)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handlePingResult(tunnel: TunnelConf, pingSuccess: Boolean) {
|
||||
if (!pingSuccess) {
|
||||
private suspend fun handlePingResult(tunnel: TunnelConf, pingResult: List<Boolean>) {
|
||||
if (pingResult.contains(false)) {
|
||||
if (isNetworkAvailable.get()) {
|
||||
Timber.i("Ping result: target was not reachable, bouncing the tunnel")
|
||||
bounceTunnel(tunnel)
|
||||
@@ -224,33 +212,23 @@ open class BaseTunnel(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun monitorTunnelConfigChanges() = coroutineScope {
|
||||
private suspend fun startTunnelConfigChangeJob(tunnel: TunnelConf) = coroutineScope {
|
||||
appDataRepository.tunnels.flow.collect { storageTuns ->
|
||||
storageTuns.forEach { storageTun ->
|
||||
val currentTun = tunnels.value.firstOrNull { it.id == storageTun.id }
|
||||
if (currentTun != null) {
|
||||
if (!currentTun.isQuickConfigMatching(storageTun)) {
|
||||
Timber.d("Tunnel config changed for ID $storageTun, bouncing tunnel")
|
||||
bounceTunnel(storageTun)
|
||||
}
|
||||
storageTuns.firstOrNull { it.id == tunnel.id }?.let { storageTun ->
|
||||
if (!tunnel.isQuickConfigMatching(storageTun) || !tunnel.isPingConfigMatching(storageTun)) {
|
||||
bounceTunnel(tunnel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun startTunnelStatisticsJob(tunnel: TunnelConf) = coroutineScope {
|
||||
while (this.isActive) {
|
||||
runCatching {
|
||||
val stats = getStatistics(tunnel)
|
||||
_tunnelStates.update { currentStates ->
|
||||
val updatedState = currentStates[tunnel.id]?.copy(statistics = stats)
|
||||
?: TunnelState(statistics = stats)
|
||||
currentStates + (tunnel.id to updatedState)
|
||||
}
|
||||
delay(CHECK_INTERVAL)
|
||||
}.onFailure { exception ->
|
||||
Timber.e(exception, "Failed to update tunnel statistics for ${tunnel.tunName}")
|
||||
while (isActive) {
|
||||
val stats = getStatistics(tunnel)
|
||||
tunnel.state.update {
|
||||
it.copy(statistics = stats)
|
||||
}
|
||||
delay(CHECK_INTERVAL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,20 +3,21 @@ package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||
import com.wireguard.android.backend.Backend
|
||||
import com.wireguard.android.backend.BackendException
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||
import com.zaneschepke.wireguardautotunnel.core.network.NetworkMonitor
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||
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.state.TunnelStatistics
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.WireGuardStatistics
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendError
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -30,18 +31,13 @@ class KernelTunnel @Inject constructor(
|
||||
networkMonitor: NetworkMonitor,
|
||||
) : BaseTunnel(ioDispatcher, applicationScope, networkMonitor, appDataRepository, serviceManager, notificationManager) {
|
||||
|
||||
override fun startTunnel(tunnelConf: TunnelConf) {
|
||||
Timber.d("Starting tunnel ${tunnelConf.id} kernel")
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
if (tunnels.value.any { it.id == tunnelConf.id }) return@launch Timber.w("Tunnel already running")
|
||||
override suspend fun startTunnel(tunnelConf: TunnelConf) {
|
||||
withContext(ioDispatcher) {
|
||||
if (tunnels.value.any { it.id == tunnelConf.id }) return@withContext Timber.w("Tunnel already running")
|
||||
runCatching {
|
||||
Timber.d("Setting backend state UP")
|
||||
super.beforeStartTunnel(tunnelConf)
|
||||
backend.setState(tunnelConf, Tunnel.State.UP, tunnelConf.toWgConfig())
|
||||
Timber.d("Calling super.startTunnel")
|
||||
super.startTunnel(tunnelConf)
|
||||
}.onFailure {
|
||||
Timber.e(it, "Failed to start tunnel ${tunnelConf.id} kernel")
|
||||
onTunnelStop(tunnelConf)
|
||||
if (it is BackendException) {
|
||||
handleBackendThrowable(it.toBackendError())
|
||||
@@ -52,14 +48,15 @@ class KernelTunnel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics {
|
||||
override suspend fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics {
|
||||
return WireGuardStatistics(backend.getStatistics(tunnelConf))
|
||||
}
|
||||
|
||||
override fun stopTunnel(tunnelConf: TunnelConf?) {
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
override suspend fun stopTunnel(tunnelConf: TunnelConf?) {
|
||||
withContext(ioDispatcher) {
|
||||
val tunnel = tunnels.value.firstOrNull { it.id == tunnelConf?.id }
|
||||
runCatching {
|
||||
tunnels.value.firstOrNull { it.id == tunnelConf?.id }?.let {
|
||||
tunnel?.let {
|
||||
backend.setState(it, Tunnel.State.DOWN, it.toWgConfig())
|
||||
onTunnelStop(it)
|
||||
} ?: stopAllTunnels()
|
||||
@@ -69,6 +66,13 @@ class KernelTunnel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun toggleTunnel(tunnelConf: TunnelConf, state: TunnelStatus) {
|
||||
when (state) {
|
||||
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>) {
|
||||
Timber.w("Not yet implemented for kernel")
|
||||
}
|
||||
|
||||
+48
-37
@@ -4,20 +4,22 @@ import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.di.Kernel
|
||||
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.enums.BackendState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.withData
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.plus
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class TunnelManager @Inject constructor(
|
||||
@@ -28,20 +30,14 @@ class TunnelManager @Inject constructor(
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
) : TunnelProvider {
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val tunnelProviderFlow = appDataRepository.settings.flow
|
||||
.filterNotNull()
|
||||
.flatMapLatest { settings ->
|
||||
MutableStateFlow(if (settings.isKernelEnabled) kernelTunnel else userspaceTunnel)
|
||||
}
|
||||
.stateIn(
|
||||
scope = applicationScope.plus(ioDispatcher),
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = userspaceTunnel,
|
||||
)
|
||||
val appSettings: StateFlow<AppSettings?> = appDataRepository.settings.flow.stateIn(
|
||||
scope = applicationScope.plus(ioDispatcher),
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = null,
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override val activeTunnels = appDataRepository.settings.flow
|
||||
override val activeTunnels = appSettings
|
||||
.filterNotNull()
|
||||
.flatMapLatest { settings ->
|
||||
if (settings.isKernelEnabled) {
|
||||
@@ -51,46 +47,61 @@ class TunnelManager @Inject constructor(
|
||||
}
|
||||
}
|
||||
.stateIn(
|
||||
scope = applicationScope.plus(ioDispatcher),
|
||||
scope = applicationScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = emptyMap(),
|
||||
)
|
||||
|
||||
override fun startTunnel(tunnelConf: TunnelConf) {
|
||||
tunnelProviderFlow.value.startTunnel(tunnelConf)
|
||||
override suspend fun startTunnel(tunnelConf: TunnelConf) {
|
||||
appSettings.withData {
|
||||
if (it.isKernelEnabled) return@withData kernelTunnel.startTunnel(tunnelConf)
|
||||
userspaceTunnel.startTunnel(tunnelConf)
|
||||
}
|
||||
}
|
||||
|
||||
override fun stopTunnel(tunnelConf: TunnelConf?) {
|
||||
tunnelProviderFlow.value.stopTunnel(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) {
|
||||
tunnelProviderFlow.value.bounceTunnel(tunnelConf)
|
||||
appSettings.withData {
|
||||
if (it.isKernelEnabled) return@withData kernelTunnel.stopTunnel(tunnelConf)
|
||||
userspaceTunnel.stopTunnel(tunnelConf)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
|
||||
tunnelProviderFlow.value.setBackendState(backendState, allowedIps)
|
||||
appSettings.withData {
|
||||
if (it.isKernelEnabled) return@withData kernelTunnel.setBackendState(backendState, allowedIps)
|
||||
userspaceTunnel.setBackendState(backendState, allowedIps)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun runningTunnelNames(): Set<String> {
|
||||
return tunnelProviderFlow.value.runningTunnelNames()
|
||||
appSettings.filterNotNull().first().let {
|
||||
if (it.isKernelEnabled) return kernelTunnel.runningTunnelNames()
|
||||
return userspaceTunnel.runningTunnelNames()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics {
|
||||
return tunnelProviderFlow.value.getStatistics(tunnelConf)
|
||||
}
|
||||
|
||||
fun restorePreviousState() = applicationScope.launch(ioDispatcher) {
|
||||
val settings = appDataRepository.settings.get()
|
||||
if (settings.isRestoreOnBootEnabled) {
|
||||
val previouslyActiveTuns = appDataRepository.tunnels.getActive()
|
||||
val tunsToStart = previouslyActiveTuns.filterNot { tun -> activeTunnels.value.any { tun.id == it.key } }
|
||||
if (settings.isKernelEnabled) {
|
||||
return@launch tunsToStart.forEach {
|
||||
startTunnel(it)
|
||||
suspend fun restorePreviousState() {
|
||||
withContext(ioDispatcher) {
|
||||
with(appDataRepository.settings.get()) {
|
||||
if (isRestoreOnBootEnabled) {
|
||||
val previouslyActiveTuns = appDataRepository.tunnels.getActive()
|
||||
// handle kernel mode
|
||||
val tunsToStart = previouslyActiveTuns.filterNot { tun -> activeTunnels.value.any { tun.id == it.key } }
|
||||
if (isKernelEnabled) {
|
||||
return@withContext tunsToStart.forEach {
|
||||
startTunnel(it)
|
||||
}
|
||||
}
|
||||
// handle userspace
|
||||
if (activeTunnels.value.isEmpty()) tunsToStart.firstOrNull()?.let { startTunnel(it) }
|
||||
}
|
||||
} else {
|
||||
tunsToStart.firstOrNull()?.let { startTunnel(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+14
-10
@@ -3,19 +3,23 @@ package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||
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
|
||||
|
||||
interface TunnelProvider {
|
||||
companion object {
|
||||
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>>
|
||||
|
||||
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 {
|
||||
const val CHECK_INTERVAL = 1_000L
|
||||
}
|
||||
}
|
||||
|
||||
+20
-18
@@ -1,13 +1,14 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||
|
||||
import com.wireguard.android.backend.BackendException
|
||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||
import com.zaneschepke.wireguardautotunnel.core.network.NetworkMonitor
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||
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.state.AmneziaStatistics
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
||||
@@ -15,7 +16,7 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.asAmBackendState
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendError
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.amnezia.awg.backend.Backend
|
||||
import org.amnezia.awg.backend.Tunnel
|
||||
import timber.log.Timber
|
||||
@@ -29,24 +30,18 @@ class UserspaceTunnel @Inject constructor(
|
||||
notificationManager: NotificationManager,
|
||||
private val backend: Backend,
|
||||
networkMonitor: NetworkMonitor,
|
||||
) : BaseTunnel(ioDispatcher, applicationScope, networkMonitor, appDataRepository, serviceManager, notificationManager) {
|
||||
) : TunnelProvider, BaseTunnel(ioDispatcher, applicationScope, networkMonitor, appDataRepository, serviceManager, notificationManager) {
|
||||
|
||||
override fun startTunnel(tunnelConf: TunnelConf) {
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
Timber.d("Starting tunnel ${tunnelConf.id} userspace")
|
||||
if (tunnels.value.any { it.id == tunnelConf.id }) return@launch Timber.w("Tunnel already running")
|
||||
override suspend fun startTunnel(tunnelConf: TunnelConf) {
|
||||
withContext(ioDispatcher) {
|
||||
if (tunnels.value.any { it.id == tunnelConf.id }) return@withContext Timber.w("Tunnel already running")
|
||||
if (tunnels.value.isNotEmpty()) {
|
||||
Timber.d("Stopping all tunnels")
|
||||
stopAllTunnels()
|
||||
}
|
||||
runCatching {
|
||||
Timber.d("Setting backend state UP")
|
||||
super.beforeStartTunnel(tunnelConf)
|
||||
backend.setState(tunnelConf, Tunnel.State.UP, tunnelConf.toAmConfig())
|
||||
Timber.d("Calling super.startTunnel")
|
||||
super.startTunnel(tunnelConf)
|
||||
}.onFailure {
|
||||
Timber.e(it, "Failed to start tunnel ${tunnelConf.id} userspace")
|
||||
onTunnelStop(tunnelConf)
|
||||
if (it is BackendException) {
|
||||
handleBackendThrowable(it.toBackendError())
|
||||
@@ -57,8 +52,19 @@ class UserspaceTunnel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override fun stopTunnel(tunnelConf: TunnelConf?) {
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
override suspend fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics {
|
||||
return AmneziaStatistics(backend.getStatistics(tunnelConf))
|
||||
}
|
||||
|
||||
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 {
|
||||
tunnels.value.firstOrNull { it.id == tunnelConf?.id }?.let {
|
||||
backend.setState(it, Tunnel.State.DOWN, it.toAmConfig())
|
||||
@@ -77,8 +83,4 @@ class UserspaceTunnel @Inject constructor(
|
||||
override suspend fun runningTunnelNames(): Set<String> {
|
||||
return backend.runningTunnelNames
|
||||
}
|
||||
|
||||
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics {
|
||||
return AmneziaStatistics(backend.getStatistics(tunnelConf))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
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,8 +4,7 @@ import android.content.Context
|
||||
import com.wireguard.android.backend.WgQuickBackend
|
||||
import com.wireguard.android.util.RootShell
|
||||
import com.wireguard.android.util.ToolsInstaller
|
||||
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
|
||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||
import com.zaneschepke.wireguardautotunnel.core.network.NetworkMonitor
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.KernelTunnel
|
||||
@@ -99,20 +98,13 @@ class TunnelModule {
|
||||
return TunnelManager(kernelTunnel, userspaceTunnel, appDataRepository, applicationScope, ioDispatcher)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideNetworkMonitor(@ApplicationContext context: Context): NetworkMonitor {
|
||||
return AndroidNetworkMonitor(context)
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideServiceManager(
|
||||
@ApplicationContext context: Context,
|
||||
@IoDispatcher ioDispatcher: CoroutineDispatcher,
|
||||
@ApplicationScope applicationScope: CoroutineScope,
|
||||
appDataRepository: AppDataRepository,
|
||||
): ServiceManager {
|
||||
return ServiceManager(context, ioDispatcher, applicationScope, appDataRepository)
|
||||
return ServiceManager(context, ioDispatcher, appDataRepository)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
package com.zaneschepke.wireguardautotunnel.domain.entity
|
||||
|
||||
import com.wireguard.config.Config
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||
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.toWgQuickString
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.Transient
|
||||
import org.amnezia.awg.backend.Tunnel
|
||||
import timber.log.Timber
|
||||
import java.io.InputStream
|
||||
@@ -27,13 +30,9 @@ data class TunnelConf(
|
||||
val pingIp: String? = null,
|
||||
val isEthernetTunnel: Boolean = false,
|
||||
val isIpv4Preferred: Boolean = false,
|
||||
@Transient
|
||||
private var stateChangeCallback: ((Any) -> Unit)? = null,
|
||||
) : Tunnel, com.wireguard.android.backend.Tunnel {
|
||||
|
||||
fun setStateChangeCallback(callback: (Any) -> Unit) {
|
||||
stateChangeCallback = callback
|
||||
}
|
||||
val state = MutableStateFlow(TunnelState())
|
||||
|
||||
fun toAmConfig(): org.amnezia.awg.config.Config {
|
||||
return configFromAmQuick(amQuick.ifBlank { wgQuick })
|
||||
@@ -51,12 +50,16 @@ data class TunnelConf(
|
||||
return isIpv4Preferred
|
||||
}
|
||||
|
||||
override fun onStateChange(newState: com.wireguard.android.backend.Tunnel.State) {
|
||||
stateChangeCallback?.invoke(newState)
|
||||
override fun onStateChange(newState: Tunnel.State) {
|
||||
state.update {
|
||||
it.copy(state = newState.asTunnelState())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStateChange(newState: Tunnel.State) {
|
||||
stateChangeCallback?.invoke(newState)
|
||||
override fun onStateChange(newState: com.wireguard.android.backend.Tunnel.State) {
|
||||
state.update {
|
||||
it.copy(state = newState.asTunnelState())
|
||||
}
|
||||
}
|
||||
|
||||
fun isQuickConfigMatching(updatedConf: TunnelConf): Boolean {
|
||||
@@ -71,17 +74,18 @@ data class TunnelConf(
|
||||
updatedConf.pingInterval == pingInterval
|
||||
}
|
||||
|
||||
suspend fun isTunnelPingable(context: CoroutineContext): Boolean {
|
||||
suspend fun pingTunnel(context: CoroutineContext): List<Boolean> {
|
||||
return withContext(context) {
|
||||
val config = toWgConfig()
|
||||
if (pingIp != null) {
|
||||
return@withContext InetAddress.getByName(pingIp)
|
||||
.isReachable(Constants.PING_TIMEOUT.toInt())
|
||||
Timber.i("Pinging custom ip")
|
||||
listOf(InetAddress.getByName(pingIp).isReachable(Constants.PING_TIMEOUT.toInt()))
|
||||
} else {
|
||||
Timber.i("Pinging all peers")
|
||||
config.peers.map { peer ->
|
||||
peer.isReachable(isIpv4Preferred)
|
||||
}
|
||||
}
|
||||
Timber.i("Pinging all peers")
|
||||
config.peers.map { peer ->
|
||||
peer.isReachable(isIpv4Preferred)
|
||||
}.all { true }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-3
@@ -41,7 +41,6 @@ import com.zaneschepke.wireguardautotunnel.ui.common.config.SubmitConfigurationT
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
|
||||
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.extensions.isValidIpv4orIpv6Address
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
|
||||
@@ -52,7 +51,7 @@ import kotlin.text.isNullOrBlank
|
||||
import kotlin.text.toLong
|
||||
|
||||
@Composable
|
||||
fun OptionsScreen(tunnelConf: TunnelConf, appUiState: AppUiState, viewModel: TunnelOptionsViewModel = hiltViewModel()) {
|
||||
fun OptionsScreen(tunnelConf: TunnelConf, viewModel: TunnelOptionsViewModel = hiltViewModel()) {
|
||||
val navController = LocalNavController.current
|
||||
|
||||
var currentText by remember { mutableStateOf("") }
|
||||
@@ -195,7 +194,6 @@ fun OptionsScreen(tunnelConf: TunnelConf, appUiState: AppUiState, viewModel: Tun
|
||||
trailing = {
|
||||
ScaledSwitch(
|
||||
checked = tunnelConf.isPingEnabled,
|
||||
enabled = !appUiState.activeTunnels.containsKey(tunnelConf.id),
|
||||
onClick = { onPingToggle() },
|
||||
)
|
||||
},
|
||||
|
||||
+5
-5
@@ -352,11 +352,11 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
|
||||
ScaledSwitch(
|
||||
uiState.appSettings.isKernelEnabled,
|
||||
onClick = { appViewModel.onToggleKernelMode() },
|
||||
enabled = !(
|
||||
uiState.appSettings.isAutoTunnelEnabled ||
|
||||
uiState.appSettings.isAlwaysOnVpnEnabled ||
|
||||
uiState.activeTunnels.isNotEmpty()
|
||||
),
|
||||
// enabled = !(
|
||||
// uiState.settings.isAutoTunnelEnabled ||
|
||||
// uiState.settings.isAlwaysOnVpnEnabled ||
|
||||
// (uiState.vpnState.status == TunnelState.UP)
|
||||
// ),
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
|
||||
+19
-2
@@ -18,6 +18,8 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -34,6 +36,7 @@ import com.zaneschepke.wireguardautotunnel.ui.Route
|
||||
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.SurfaceSelectionGroupButton
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.label.VersionLabel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
|
||||
@@ -48,6 +51,20 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
|
||||
fun SupportScreen(appUiState: AppUiState, appViewModel: AppViewModel) {
|
||||
val context = LocalContext.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(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top),
|
||||
@@ -98,12 +115,12 @@ fun SupportScreen(appUiState: AppUiState, appViewModel: AppViewModel) {
|
||||
ScaledSwitch(
|
||||
appUiState.generalState.isLocalLogsEnabled,
|
||||
onClick = {
|
||||
appViewModel.onToggleLocalLogging()
|
||||
showDialog = true
|
||||
},
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
appViewModel.onToggleLocalLogging()
|
||||
showDialog = true
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
@@ -3,12 +3,10 @@ package com.zaneschepke.wireguardautotunnel.ui.state
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.GeneralState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
|
||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||
|
||||
data class AppUiState(
|
||||
val appSettings: AppSettings = AppSettings(),
|
||||
val tunnels: List<TunnelConf> = emptyList(),
|
||||
val activeTunnels: Map<Int, TunnelState> = emptyMap(),
|
||||
val generalState: GeneralState = GeneralState(),
|
||||
val autoTunnelActive: Boolean = false,
|
||||
)
|
||||
|
||||
+7
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.util.extensions
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.wireguard.android.backend.BackendException
|
||||
import com.wireguard.android.util.RootShell
|
||||
import com.wireguard.config.Peer
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendError
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
||||
@@ -86,6 +87,12 @@ fun Config.toWgQuickString(): String {
|
||||
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 {
|
||||
return BackendState.valueOf(this.name)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,12 @@ import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.Route
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.isCurrentRoute
|
||||
|
||||
fun NavController.navigateAndForget(route: Route) {
|
||||
navigate(route) {
|
||||
popUpTo(0)
|
||||
}
|
||||
}
|
||||
|
||||
fun NavController.goFromRoot(route: Route) {
|
||||
if (currentBackStackEntry?.isCurrentRoute(route::class) == true) return
|
||||
this.navigate(route) {
|
||||
|
||||
@@ -31,9 +31,11 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.takeWhile
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.plus
|
||||
@@ -73,13 +75,11 @@ constructor(
|
||||
appDataRepository.settings.flow,
|
||||
appDataRepository.tunnels.flow,
|
||||
appDataRepository.appState.flow,
|
||||
tunnelManager.activeTunnels,
|
||||
serviceManager.autoTunnelActive,
|
||||
) { settings, tunnels, generalState, activeTunnels, autoTunnel ->
|
||||
) { settings, tunnels, generalState, autoTunnel ->
|
||||
AppUiState(
|
||||
settings,
|
||||
tunnels,
|
||||
activeTunnels,
|
||||
generalState,
|
||||
autoTunnel,
|
||||
)
|
||||
@@ -103,8 +103,9 @@ constructor(
|
||||
|
||||
private suspend fun appReadyCheck() {
|
||||
val tunnelCount = appDataRepository.tunnels.count()
|
||||
uiState.first { it.tunnels.count() == tunnelCount }
|
||||
_isAppReady.emit(true)
|
||||
uiState.takeWhile { it.tunnels.size != tunnelCount }.onCompletion {
|
||||
_isAppReady.emit(true)
|
||||
}.collect()
|
||||
}
|
||||
|
||||
private suspend fun initTunnels() {
|
||||
@@ -145,12 +146,17 @@ constructor(
|
||||
with(uiState.value.generalState) {
|
||||
val toggledOn = !isLocalLogsEnabled
|
||||
appDataRepository.appState.setLocalLogsEnabled(toggledOn)
|
||||
if (!toggledOn) {
|
||||
logReader.stop()
|
||||
if (!toggledOn) onLoggerStop()
|
||||
_configurationChange.update {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun onLoggerStop() {
|
||||
logReader.deleteAndClearLogs()
|
||||
}
|
||||
|
||||
fun onToggleAlwaysOnVPN() = viewModelScope.launch {
|
||||
with(uiState.value.appSettings) {
|
||||
appDataRepository.settings.save(
|
||||
|
||||
@@ -1,50 +1,43 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<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="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="turn_off_tunnel">Bu işlem tünelin kapalı olmasını gerektirir</string>
|
||||
<string name="vpn_channel_name">VPN Bildirim Kanalı</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="no_tunnels">Henüz tünel eklenmedi!</string>
|
||||
<string name="tunnels">Tüneller</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="tunnel_on_ethernet">Ethernet üzerinde tünel</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="tunnel_on_ethernet">Ethernet\'te 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_title">Arka Plan Konum Açıklaması</string>
|
||||
<string name="thank_you">WG Tunnel’i kullandığınız için teşekkürler!</string>
|
||||
<string name="trusted_ssid_value_description">SSID Gönder</string>
|
||||
<string name="add_tunnels_text">Dosyadan veya zip’ten ekle</string>
|
||||
<string name="thank_you">WG Tunnel\'ı kullandığınız için teşekkürler!</string>
|
||||
<string name="trusted_ssid_value_description">SSID\'yi gönder</string>
|
||||
<string name="add_tunnels_text">Dosyadan veya zip\'ten ekle</string>
|
||||
<string name="open_file">Dosya Aç</string>
|
||||
<string name="add_from_qr">QR kodundan ekle</string>
|
||||
<string name="qr_scan">QR Tara</string>
|
||||
<string name="qr_scan">QR Tarama</string>
|
||||
<string name="tunnel_name">Tünel Adı</string>
|
||||
<string name="exclude">Hariç Tut</string>
|
||||
<string name="include">Dahil Et</string>
|
||||
<string name="exclude">Hariç tut</string>
|
||||
<string name="include">Dahil et</string>
|
||||
<string name="config_changes_saved">Yapılandırma değişiklikleri kaydedildi.</string>
|
||||
<string name="public_key">Genel anahtar</string>
|
||||
<string name="addresses">Adresler</string>
|
||||
<string name="dns_servers">DNS sunucuları</string>
|
||||
<string name="mtu">MTU</string>
|
||||
<string name="peer">Eş</string>
|
||||
<string name="allowed_ips">İzin verilen IP’ler</string>
|
||||
<string name="endpoint">Uç Nokta</string>
|
||||
<string name="peer">Eş (peer)</string>
|
||||
<string name="allowed_ips">İzin verilen IP\'ler</string>
|
||||
<string name="endpoint">Uç nokta (endpoint)</string>
|
||||
<string name="name">Ad</string>
|
||||
<string name="always_on_vpn_support">Her Zaman Açık VPN’e İzin Ver</string>
|
||||
<string name="location_services_not_detected">Konum Servisleri Algılanmadı</string>
|
||||
<string name="hint_search_packages">Paketleri ara</string>
|
||||
<string name="db_name" translatable="false">wg-tunnel-db</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="hint_search_packages">Paketlerde ara</string>
|
||||
<string name="auto_tunneling">Otomatik tünelleme</string>
|
||||
<string name="vpn_on">VPN açık</string>
|
||||
<string name="vpn_off">VPN kapalı</string>
|
||||
<string name="create_import">Sıfırdan oluştur</string>
|
||||
<string name="turn_on_tunnel">Bu işlem aktif bir tünel gerektirir</string>
|
||||
<string name="turn_on_tunnel">İşlem için aktif tünel gerekiyor</string>
|
||||
<string name="add_peer">Eş ekle</string>
|
||||
<string name="interface_">Arayüz</string>
|
||||
<string name="rotate_keys">Anahtarları döndür</string>
|
||||
@@ -56,42 +49,41 @@
|
||||
<string name="random">(rastgele)</string>
|
||||
<string name="optional">(isteğe bağlı)</string>
|
||||
<string name="optional_no_recommend">(isteğe bağlı, önerilmez)</string>
|
||||
<string name="preshared_key">Ön paylaşımlı anahtar</string>
|
||||
<string name="preshared_key">Önceden paylaşılmış anahtar</string>
|
||||
<string name="seconds">saniye</string>
|
||||
<string name="persistent_keepalive">Kalıcı canlı tutma</string>
|
||||
<string name="cancel">İptal</string>
|
||||
<string name="error_authentication_failed">Kimlik doğrulama başarısız</string>
|
||||
<string name="error_authorization_failed">Yetkilendirme başarısız</string>
|
||||
<string name="error_authentication_failed">Kimlik doğrulama başarısız oldu</string>
|
||||
<string name="error_authorization_failed">Yetkilendirme başarısız oldu</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="unknown_error">Bilinmeyen bir hata oluştu</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="tunnel_on_wifi">Güvenilmeyen wifi\'da tünel</string>
|
||||
<string name="email_subject">WG Tunnel Desteği</string>
|
||||
<string name="email_chooser">E-posta gönder…</string>
|
||||
<string name="docs_description">Belgeleri oku</string>
|
||||
<string name="email_description">Bana e-posta gönder</string>
|
||||
<string name="use_kernel">Çekirdek modülünü kullan</string>
|
||||
<string name="use_kernel">Kernel modülünü kullan</string>
|
||||
<string name="error_ssid_exists">SSID zaten mevcut</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_invalid_code">Geçersiz QR kodu</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 servisi</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="auto_tunnel_title">Otomatik Tünel Hizmeti</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="yes">Evet</string>
|
||||
<string name="tunneling_apps">Tünelleme uygulamaları</string>
|
||||
<string name="tunneling_apps">Tünellenen uygulamalar</string>
|
||||
<string name="all">tümü</string>
|
||||
<string name="no_email_detected">E-posta uygulaması algılanmadı</string>
|
||||
<string name="no_browser_detected">Tarayıcı algılanmadı</string>
|
||||
<string name="open_issue">Bir sorun aç</string>
|
||||
<string name="open_issue">Sorun bildir</string>
|
||||
<string name="read_logs">Günlükleri oku</string>
|
||||
<string name="auto">(otomatik)</string>
|
||||
<string name="incorrect_pin">Pin yanlış</string>
|
||||
<string name="pin_created">Pin başarıyla oluşturuldu</string>
|
||||
<string name="enter_pin">Pin’inizi girin</string>
|
||||
<string name="create_pin">Pin oluştur</string>
|
||||
<string name="incorrect_pin">PIN yanlış</string>
|
||||
<string name="pin_created">PIN başarıyla oluşturuldu</string>
|
||||
<string name="enter_pin">PIN\'inizi girin</string>
|
||||
<string name="create_pin">PIN oluştur</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="mobile_data_tunnel">Mobil veri tüneli olarak ayarla</string>
|
||||
@@ -102,118 +94,18 @@
|
||||
<string name="settings">Ayarlar</string>
|
||||
<string name="support">Destek</string>
|
||||
<string name="kernel">Çekirdek</string>
|
||||
<string name="junk_packet_count">Çöp paket sayısı</string>
|
||||
<string name="junk_packet_minimum_size">Çöp paket minimum boyutu</string>
|
||||
<string name="junk_packet_maximum_size">Çöp paket maksimum boyutu</string>
|
||||
<string name="init_packet_junk_size">Başlangıç paketi çöp boyutu</string>
|
||||
<string name="response_packet_junk_size">Yanıt paketi çöp boyutu</string>
|
||||
<string name="init_packet_magic_header">Başlangıç paketi sihirli başlığı</string>
|
||||
<string name="junk_packet_count">Gereksiz paket sayısı</string>
|
||||
<string name="junk_packet_minimum_size">Gereksiz paket minimum boyutu</string>
|
||||
<string name="junk_packet_maximum_size">Gereksiz paket maksimum boyutu</string>
|
||||
<string name="init_packet_junk_size">Başlatma paketi gereksiz boyutu</string>
|
||||
<string name="response_packet_junk_size">Yanıt paketi gereksiz boyutu</string>
|
||||
<string name="init_packet_magic_header">Başlatma 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="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="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="unsure_how">nasıl devam edeceğinizden emin değilseniz</string>
|
||||
<string name="see_the">Bakın:</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="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>
|
||||
<string name="restart_at_boot">Önyüklemede yeniden başlat</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
object Constants {
|
||||
const val VERSION_NAME = "3.7.0"
|
||||
const val VERSION_NAME = "3.6.6"
|
||||
const val JVM_TARGET = "17"
|
||||
const val VERSION_CODE = 37000
|
||||
const val VERSION_CODE = 36600
|
||||
const val TARGET_SDK = 35
|
||||
const val MIN_SDK = 26
|
||||
const val APP_ID = "com.zaneschepke.wireguardautotunnel"
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
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
|
||||
+10
-10
@@ -1,13 +1,13 @@
|
||||
[versions]
|
||||
accompanist = "0.37.2"
|
||||
activityCompose = "1.10.1"
|
||||
amneziawgAndroid = "1.3.0"
|
||||
accompanist = "0.37.0"
|
||||
activityCompose = "1.10.0"
|
||||
amneziawgAndroid = "1.2.8"
|
||||
androidx-junit = "1.2.1"
|
||||
appcompat = "1.7.0"
|
||||
biometricKtx = "1.2.0-alpha05"
|
||||
coreKtx = "1.15.0"
|
||||
datastorePreferences = "1.1.3"
|
||||
desugar_jdk_libs = "2.1.5"
|
||||
datastorePreferences = "1.1.2"
|
||||
desugar_jdk_libs = "2.1.4"
|
||||
espressoCore = "3.6.1"
|
||||
hiltAndroid = "2.55"
|
||||
hiltCompiler = "1.2.0"
|
||||
@@ -15,14 +15,14 @@ junit = "4.13.2"
|
||||
kotlinx-serialization-json = "1.8.0"
|
||||
lifecycle-runtime-compose = "2.8.7"
|
||||
material3 = "1.3.1"
|
||||
navigationCompose = "2.8.8"
|
||||
navigationCompose = "2.8.7"
|
||||
pinLockCompose = "1.0.4"
|
||||
roomVersion = "2.6.1"
|
||||
timber = "5.0.1"
|
||||
tunnel = "1.2.6"
|
||||
androidGradlePlugin = "8.8.0-alpha05"
|
||||
tunnel = "1.2.4"
|
||||
androidGradlePlugin = "8.10.0-alpha06"
|
||||
kotlin = "2.1.10"
|
||||
ksp = "2.1.10-1.0.31"
|
||||
ksp = "2.1.10-1.0.30"
|
||||
composeBom = "2025.02.00"
|
||||
compose = "1.7.8"
|
||||
workRuntimeKtxVersion = "2.10.0"
|
||||
@@ -32,7 +32,7 @@ gradlePlugins-grgit = "5.3.0"
|
||||
|
||||
#plugins
|
||||
material = "1.12.0"
|
||||
gradlePlugins-ktlint="12.2.0"
|
||||
gradlePlugins-ktlint="12.1.2"
|
||||
|
||||
|
||||
[libraries]
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
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,8 +4,7 @@ import com.zaneschepke.logcatter.model.LogMessage
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface LogReader {
|
||||
fun start()
|
||||
fun stop()
|
||||
fun initialize(onLogMessage: ((message: LogMessage) -> Unit)? = null)
|
||||
fun zipLogFiles(path: String)
|
||||
suspend fun deleteAndClearLogs()
|
||||
val bufferedLogs: Flow<LogMessage>
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
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,32 +1,250 @@
|
||||
package com.zaneschepke.logcatter
|
||||
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
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.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStreamReader
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
object LogcatReader {
|
||||
|
||||
private const val MAX_FILE_SIZE = 2097152L // 2MB
|
||||
private const val MAX_FOLDER_SIZE = 10485760L // 10MB
|
||||
|
||||
private lateinit var logcatManager: LogcatManager
|
||||
private var isInitialized = false
|
||||
private val findKeyRegex = """[A-Za-z0-9+/]{42}[AEIMQUYcgkosw480]=""".toRegex()
|
||||
private val findIpv6AddressRegex = """^([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}${'$'}""".toRegex()
|
||||
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 {
|
||||
if (maxFileSize > maxFolderSize) {
|
||||
throw IllegalStateException("maxFileSize must be less than maxFolderSize")
|
||||
}
|
||||
synchronized(this) {
|
||||
if (isInitialized) return logcatManager
|
||||
val logDir = "$storageDir${File.separator}logs"
|
||||
File(logDir).mkdirs()
|
||||
logcatManager = LogcatManager(
|
||||
pid = android.os.Process.myPid(),
|
||||
logDir = logDir,
|
||||
maxFileSize = maxFileSize,
|
||||
maxFolderSize = maxFolderSize,
|
||||
)
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(logcatManager)
|
||||
isInitialized = true
|
||||
return logcatManager
|
||||
synchronized(LogcatHelperInit) {
|
||||
LogcatHelperInit.maxFileSize = maxFileSize
|
||||
LogcatHelperInit.maxFolderSize = maxFolderSize
|
||||
LogcatHelperInit.pID = android.os.Process.myPid()
|
||||
LogcatHelperInit.publicAppDirectory = storageDir
|
||||
LogcatHelperInit.logcatPath = LogcatHelperInit.publicAppDirectory + File.separator + "logs"
|
||||
val logDirectory = File(LogcatHelperInit.logcatPath)
|
||||
if (!logDirectory.exists()) {
|
||||
logDirectory.mkdir()
|
||||
}
|
||||
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>(
|
||||
replay = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||
)
|
||||
|
||||
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() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
/build
|
||||
@@ -1,51 +0,0 @@
|
||||
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
@@ -1,21 +0,0 @@
|
||||
# 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
@@ -1,24 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<?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>
|
||||
@@ -1,151 +0,0 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
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()
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package com.zaneschepke.networkmonitor
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface NetworkMonitor {
|
||||
fun getNetworkStatusFlow(includeWifiSsid: Boolean, useRootShell: Boolean): Flow<NetworkStatus>
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
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,4 +34,3 @@ rootProject.name = "WG Tunnel"
|
||||
|
||||
include(":app")
|
||||
include(":logcatter")
|
||||
include(":networkmonitor")
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
4
|
||||
1
|
||||
|
||||
Reference in New Issue
Block a user