Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot] eb03e94e6f build(deps): bump androidGradlePlugin
Bumps `androidGradlePlugin` from 8.8.0-alpha05 to 8.10.0-alpha07.

Updates `com.android.application` from 8.8.0-alpha05 to 8.10.0-alpha07

Updates `com.android.library` from 8.8.0-alpha05 to 8.10.0-alpha07

---
updated-dependencies:
- dependency-name: com.android.application
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: com.android.library
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-02 01:18:37 +00:00
30 changed files with 603 additions and 724 deletions
+11 -3
View File
@@ -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())
@@ -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()
}
}
}
}
}
@@ -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")
}
}
}
}
@@ -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>
@@ -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 }
}
}
@@ -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() },
)
},
@@ -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 = {
@@ -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(
+43 -151
View File
@@ -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 Tunneli 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 zipten 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"></string>
<string name="allowed_ips">İzin verilen IPler</string>
<string name="endpoint">Nokta</string>
<string name="peer"> (peer)</string>
<string name="allowed_ips">İzin verilen IP\'ler</string>
<string name="endpoint">nokta (endpoint)</string>
<string name="name">Ad</string>
<string name="always_on_vpn_support">Her Zaman Açık VPNe İ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 wifida 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">Pininizi 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 VPNin 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 IPsi 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">Amneziayı 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 switchi 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 DNSsi çö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>
+2 -2
View File
@@ -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
+5 -5
View File
@@ -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)
}
}
+3 -10
View File
@@ -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
View File
@@ -1 +1 @@
4
3