mirror of
https://github.com/wgtunnel/android.git
synced 2026-07-03 14:07:49 +02:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eb03e94e6f |
@@ -163,17 +163,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) {
|
||||
@@ -219,7 +223,7 @@ 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)
|
||||
OptionsScreen(config)
|
||||
}
|
||||
}
|
||||
composable<Route.Lock> {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
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
|
||||
@@ -21,7 +20,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
|
||||
@@ -36,7 +34,6 @@ 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 +47,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,31 +79,17 @@ 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)
|
||||
}
|
||||
launch {
|
||||
startTunnelConfigChangeJob(tunnel)
|
||||
}
|
||||
launch {
|
||||
startStateJob(tunnel)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,10 +105,15 @@ open class BaseTunnel(
|
||||
// Default empty implementation; subclasses override
|
||||
}
|
||||
|
||||
override fun toggleTunnel(tunnelConf: TunnelConf, state: TunnelStatus) {
|
||||
// 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>) {
|
||||
@@ -141,7 +129,7 @@ open class BaseTunnel(
|
||||
}
|
||||
|
||||
override val activeTunnels: StateFlow<Map<Int, TunnelState>>
|
||||
get() = _tunnelStates.asStateFlow()
|
||||
get() = _activeTunnels.asStateFlow()
|
||||
|
||||
internal suspend fun onTunnelStop(tunnelConf: TunnelConf) {
|
||||
appDataRepository.tunnels.save(tunnelConf.copy(isActive = false))
|
||||
@@ -178,20 +166,27 @@ open class BaseTunnel(
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
private suspend fun startStateJob(tunnel: TunnelConf) {
|
||||
tunnel.state.collect { state ->
|
||||
_activeTunnels.update {
|
||||
it + (tunnel.id to state)
|
||||
}
|
||||
serviceManager.updateTunnelTile()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handlePingResult(tunnel: TunnelConf, pingSuccess: Boolean) {
|
||||
if (!pingSuccess) {
|
||||
private suspend fun startPingJob(tunnel: TunnelConf) = coroutineScope {
|
||||
while (isActive) {
|
||||
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, 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 +219,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ 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
|
||||
@@ -31,17 +32,12 @@ class KernelTunnel @Inject constructor(
|
||||
) : 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")
|
||||
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())
|
||||
@@ -69,6 +65,19 @@ class KernelTunnel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override fun toggleTunnel(tunnelConf: TunnelConf, status: TunnelStatus) {
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
runCatching {
|
||||
when (status) {
|
||||
TunnelStatus.UP -> backend.setState(tunnelConf, Tunnel.State.UP, tunnelConf.toWgConfig())
|
||||
TunnelStatus.DOWN -> backend.setState(tunnelConf, Tunnel.State.DOWN, tunnelConf.toWgConfig())
|
||||
}
|
||||
}.onFailure {
|
||||
Timber.e(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
|
||||
Timber.w("Not yet implemented for kernel")
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.zaneschepke.wireguardautotunnel.di.Kernel
|
||||
import com.zaneschepke.wireguardautotunnel.di.Userspace
|
||||
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 kotlinx.coroutines.CoroutineDispatcher
|
||||
@@ -64,6 +65,10 @@ class TunnelManager @Inject constructor(
|
||||
tunnelProviderFlow.value.stopTunnel(tunnelConf)
|
||||
}
|
||||
|
||||
override fun toggleTunnel(tunnelConf: TunnelConf, state: TunnelStatus) {
|
||||
tunnelProviderFlow.value.toggleTunnel(tunnelConf, state)
|
||||
}
|
||||
|
||||
override suspend fun bounceTunnel(tunnelConf: TunnelConf) {
|
||||
tunnelProviderFlow.value.bounceTunnel(tunnelConf)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ 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.enums.TunnelStatus
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -13,6 +14,7 @@ interface TunnelProvider {
|
||||
|
||||
fun startTunnel(tunnelConf: TunnelConf)
|
||||
fun stopTunnel(tunnelConf: TunnelConf? = null)
|
||||
fun toggleTunnel(tunnelConf: TunnelConf, state: TunnelStatus)
|
||||
suspend fun bounceTunnel(tunnelConf: TunnelConf)
|
||||
suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>)
|
||||
suspend fun runningTunnelNames(): Set<String>
|
||||
|
||||
+21
-6
@@ -8,6 +8,7 @@ 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
|
||||
@@ -33,20 +34,14 @@ class UserspaceTunnel @Inject constructor(
|
||||
|
||||
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")
|
||||
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,6 +52,19 @@ class UserspaceTunnel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override fun toggleTunnel(tunnelConf: TunnelConf, status: TunnelStatus) {
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
runCatching {
|
||||
when (status) {
|
||||
TunnelStatus.UP -> backend.setState(tunnelConf, Tunnel.State.UP, tunnelConf.toAmConfig())
|
||||
TunnelStatus.DOWN -> backend.setState(tunnelConf, Tunnel.State.DOWN, tunnelConf.toAmConfig())
|
||||
}
|
||||
}.onFailure {
|
||||
Timber.e(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun stopTunnel(tunnelConf: TunnelConf?) {
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
runCatching {
|
||||
@@ -70,6 +78,13 @@ class UserspaceTunnel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun bounceTunnel(tunnelConf: 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>) {
|
||||
backend.setBackendState(backendState.asAmBackendState(), allowedIps)
|
||||
}
|
||||
|
||||
@@ -110,9 +110,8 @@ class TunnelModule {
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -73,13 +73,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,
|
||||
)
|
||||
@@ -145,12 +143,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
|
||||
@@ -1,7 +1,7 @@
|
||||
[versions]
|
||||
accompanist = "0.37.2"
|
||||
activityCompose = "1.10.1"
|
||||
amneziawgAndroid = "1.3.0"
|
||||
amneziawgAndroid = "1.2.9"
|
||||
androidx-junit = "1.2.1"
|
||||
appcompat = "1.7.0"
|
||||
biometricKtx = "1.2.0-alpha05"
|
||||
@@ -19,10 +19,10 @@ navigationCompose = "2.8.8"
|
||||
pinLockCompose = "1.0.4"
|
||||
roomVersion = "2.6.1"
|
||||
timber = "5.0.1"
|
||||
tunnel = "1.2.6"
|
||||
androidGradlePlugin = "8.8.0-alpha05"
|
||||
tunnel = "1.2.5"
|
||||
androidGradlePlugin = "8.10.0-alpha07"
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -19,20 +19,13 @@ android {
|
||||
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
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = Constants.JVM_TARGET
|
||||
jvmTarget = "11"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
4
|
||||
3
|
||||
Reference in New Issue
Block a user