Compare commits

..

8 Commits

Author SHA1 Message Date
Zane Schepke b7e4f3c3e5 chore: bump version to 4.0.3 2025-09-27 06:18:13 -04:00
Zane Schepke dedef38541 fix: notification activity relaunch bug 2025-09-27 06:16:53 -04:00
Zane Schepke aa5adec902 fix: notification stop action bug, monitoring failing to shut down race 2025-09-27 06:07:54 -04:00
Zane Schepke e269aa88d9 chore: bump verison to 4.0.2 2025-09-26 09:51:15 -04:00
Zane Schepke 7c3eabb19b fix: logs export bug, export file naming 2025-09-26 09:46:30 -04:00
Zane Schepke acdefd80fa fix: fdroid agp build issue, bump deps
closes #971
2025-09-26 08:31:17 -04:00
Zane Schepke 09fa9dabdd fix: tunnel on ethernet toggle
closes #978
2025-09-26 08:23:53 -04:00
Zane Schepke 33a02262a8 fix: pin lock crash bug
closes #967
2025-09-26 08:20:01 -04:00
21 changed files with 263 additions and 176 deletions
@@ -126,14 +126,8 @@ class MainActivity : AppCompatActivity() {
val navController = rememberNavController()
val scope = rememberCoroutineScope()
var pinManagerInitialized by remember { mutableStateOf(false) }
LaunchedEffect(appState.isAppLoaded) {
if (appState.isAppLoaded) {
if (appState.pinLockEnabled && !pinManagerInitialized) {
PinManager.initialize(this@MainActivity)
pinManagerInitialized = true
}
appState.locale.let { LocaleUtil.changeLocale(it) }
}
}
@@ -197,7 +191,6 @@ class MainActivity : AppCompatActivity() {
requestingAppMode = Pair(sideEffect.requestingMode, sideEffect.config)
vpnActivity.launch(VpnService.prepare(this@MainActivity))
}
is GlobalSideEffect.ShareFile -> context.launchShareFile(sideEffect.file)
is GlobalSideEffect.Snackbar ->
scope.launch {
snackbar.showSnackbar(sideEffect.message.asString(context))
@@ -271,7 +264,10 @@ class MainActivity : AppCompatActivity() {
Route.Lock
else Route.TunnelsGraph,
) {
composable<Route.Lock> { PinLockScreen() }
composable<Route.Lock> {
PinManager.initialize(context = this@MainActivity)
PinLockScreen()
}
navigation<Route.TunnelsGraph>(
startDestination = Route.Tunnels
) {
@@ -34,7 +34,7 @@ class NotificationActionReceiver : BroadcastReceiver() {
val tunnelId = intent.getIntExtra(NotificationManager.EXTRA_ID, 0)
if (tunnelId == STOP_ALL_TUNNELS_ID)
return@launch tunnelManager.stopActiveTunnels()
tunnelRepository.getById(tunnelId)?.let { tunnelManager.stopTunnel(it.id) }
tunnelManager.stopTunnel(tunnelId)
}
}
}
@@ -51,7 +51,8 @@ class WireGuardNotification @Inject constructor(@ApplicationContext override val
PendingIntent.getActivity(
context,
0,
Intent(context, MainActivity::class.java),
Intent(context, MainActivity::class.java)
.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP),
PendingIntent.FLAG_IMMUTABLE,
)
)
@@ -94,12 +95,12 @@ class WireGuardNotification @Inject constructor(@ApplicationContext override val
val pendingIntent =
PendingIntent.getBroadcast(
context,
0,
extraId ?: 0,
Intent(context, NotificationActionReceiver::class.java).apply {
action = notificationAction.name
if (extraId != null) putExtra(EXTRA_ID, extraId)
},
PendingIntent.FLAG_IMMUTABLE,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
)
return NotificationCompat.Action.Builder(
R.drawable.ic_notification,
@@ -56,6 +56,10 @@ abstract class BaseTunnel(@ApplicationScope protected val applicationScope: Coro
) {
tunStatusMutex.withLock {
activeTuns.update { currentTuns ->
if (!currentTuns.containsKey(tunnelId) && status != TunnelStatus.Starting) {
Timber.d("Ignoring update for inactive tunnel $tunnelId")
return@update currentTuns
}
val existingState = currentTuns[tunnelId] ?: TunnelState()
val newStatus = status ?: existingState.status
if (newStatus == TunnelStatus.Down) {
@@ -21,8 +21,9 @@ import kotlin.concurrent.atomics.AtomicBoolean
import kotlin.concurrent.atomics.AtomicReference
import kotlin.concurrent.atomics.ExperimentalAtomicApi
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.amnezia.awg.crypto.Key
import timber.log.Timber
@@ -41,6 +42,7 @@ constructor(
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : TunnelProvider {
private val monitoringMutex = Mutex()
private val monitoringJobs = ConcurrentHashMap<Int, Job>()
private data class SideEffectState(
@@ -55,9 +57,6 @@ constructor(
val condition: (SideEffectState) -> Boolean,
)
private val sideEffectChannelFlow =
MutableStateFlow<Channel<SideEffectState>>(Channel(Channel.CONFLATED))
private val tunnelProviderFlow: StateFlow<TunnelProvider> = run {
val currentBackend = AtomicReference(userspaceTunnel)
val currentSettings = AtomicReference(GeneralSettings())
@@ -105,61 +104,9 @@ constructor(
override val activeTunnels: StateFlow<Map<Int, TunnelState>> = run {
val activeTunsReference: AtomicReference<Map<Int, TunnelState>> =
AtomicReference(emptyMap())
tunnelProviderFlow
.flatMapLatest { backend ->
// Create a new channel for each backend to reset side-effect processing
val newChannel = Channel<SideEffectState>(Channel.CONFLATED)
sideEffectChannelFlow.value = newChannel
val sideEffects =
listOf(
SideEffectWithCondition(
effect = { s ->
handleTunnelServiceChange(s.settings.appMode, s.activeTuns)
},
condition = { s -> s.activeTuns.size != s.previouslyActive.size },
),
SideEffectWithCondition(
effect = { s ->
handleTunnelsActiveChange(s.previouslyActive, s.activeTuns, s.tuns)
},
condition = { s -> s.activeTuns.size != s.previouslyActive.size },
),
// TODO Not for kernel mode for now
SideEffectWithCondition(
effect = { s -> handleTunnelMonitoringChanges(s.activeTuns, s.tuns) },
condition = { s ->
s.tuns.any {
it.restartOnPingFailure && s.activeTuns.keys.contains(it.id)
} && s.settings.appMode != AppMode.KERNEL
},
),
SideEffectWithCondition(
effect = { s ->
handleFullTunnelMonitoring(s.activeTuns, s.tuns, s.settings)
},
condition = { s -> s.activeTuns.keys != s.previouslyActive.keys },
),
)
applicationScope.launch(ioDispatcher) {
for (state in newChannel) {
supervisorScope {
sideEffects
.filter { it.condition(state) }
.forEach { sideEffect ->
launch {
try {
sideEffect.effect(state)
} catch (e: Exception) {
Timber.e(e, "Side effect failed")
}
}
}
}
}
}
combine(
backend.activeTunnels,
tunnelsRepository.flow,
@@ -171,9 +118,67 @@ constructor(
.onStart { handleStateRestore() }
.onEach { (activeTuns, tuns, settings) ->
val previouslyActive = activeTunsReference.exchange(activeTuns)
sideEffectChannelFlow.value.trySend(
SideEffectState(activeTuns, tuns, settings, previouslyActive)
)
val state = SideEffectState(activeTuns, tuns, settings, previouslyActive)
applicationScope.launch(ioDispatcher) {
supervisorScope {
val sideEffects =
listOf(
SideEffectWithCondition(
effect = { s ->
handleTunnelServiceChange(s.settings.appMode, s.activeTuns)
},
condition = { s ->
s.activeTuns.size != s.previouslyActive.size
},
),
SideEffectWithCondition(
effect = { s ->
handleTunnelsActiveChange(
s.previouslyActive,
s.activeTuns,
s.tuns,
)
},
condition = { s ->
s.activeTuns.size != s.previouslyActive.size
},
),
// TODO Not for kernel mode for now
SideEffectWithCondition(
effect = { s ->
handleTunnelMonitoringChanges(s.activeTuns, s.tuns)
},
condition = { s ->
s.tuns.any {
it.restartOnPingFailure &&
s.activeTuns.keys.contains(it.id)
} && s.settings.appMode != AppMode.KERNEL
},
),
SideEffectWithCondition(
effect = { s ->
handleFullTunnelMonitoring(s.activeTuns, s.tuns, s.settings)
},
condition = { s ->
s.activeTuns.keys != s.previouslyActive.keys
},
),
)
sideEffects
.filter { it.condition(state) }
.forEach { sideEffect ->
launch {
try {
sideEffect.effect(state)
} catch (e: Exception) {
Timber.e(e, "Side effect failed")
}
}
}
}
}
}
.map { (activeTuns, _, _) -> activeTuns }
.stateIn(
@@ -375,29 +380,39 @@ constructor(
activeTuns: Map<Int, TunnelState>,
configs: List<TunnelConf>,
settings: GeneralSettings,
) {
val activeIds = activeTuns.keys
val obsoleteIds = monitoringJobs.keys - activeIds
obsoleteIds.forEach { id ->
monitoringJobs[id]?.cancel()
monitoringJobs.remove(id)
) =
monitoringMutex.withLock {
val activeIds = activeTuns.keys.toSet()
val currentJobs = monitoringJobs.keys.toSet()
val obsoleteIds = currentJobs - activeIds
Timber.d(
"Monitoring: Active IDs: $activeIds, Obsolete IDs: $obsoleteIds, Total jobs before: ${monitoringJobs.size}"
)
obsoleteIds.forEach { id ->
monitoringJobs[id]?.cancel()
monitoringJobs.remove(id)
}
activeIds.forEach { id ->
if (monitoringJobs.containsKey(id)) return@forEach // Skip if already monitored
val config = configs.find { it.id == id } ?: return@forEach
val tunStateFlow =
activeTunnels.map { it[id] }.stateIn(applicationScope + ioDispatcher)
val newJob =
applicationScope.launch(ioDispatcher) {
tunnelMonitor.startMonitoring(
id,
withLogs = settings.appMode != AppMode.KERNEL,
tunStateFlow = tunStateFlow,
getStatistics = { tunnelId -> getStatistics(tunnelId) },
updateTunnelStatus = { tid, status, stats, pings, logHealth ->
updateTunnelStatus(tid, null, stats, pings, logHealth)
},
)
}
monitoringJobs[id] = newJob
}
}
activeIds.forEach { id ->
if (monitoringJobs.contains(id)) return@forEach
configs.find { it.id == id } ?: return@forEach
val tunStateFlow = activeTunnels.map { it[id] }.stateIn(applicationScope + ioDispatcher)
monitoringJobs[id] =
applicationScope.launch(ioDispatcher) {
tunnelMonitor.startMonitoring(
id,
withLogs = settings.appMode != AppMode.KERNEL,
tunStateFlow = tunStateFlow,
getStatistics = { tunnelId -> getStatistics(tunnelId) },
updateTunnelStatus = { tid, status, stats, pings, logHealth ->
updateTunnelStatus(tid, null, stats, pings, logHealth)
},
)
}
}
}
}
@@ -139,6 +139,7 @@ constructor(
val updates = ConcurrentMap<Key, PingState>()
pingablePeers.forEach { peer ->
ensureActive()
val previousState = pingStatsFlow.value[peer.publicKey] ?: PingState()
val allowedIpStr = peer.allowedIps.firstOrNull()?.toString()
@@ -172,41 +173,45 @@ constructor(
val attemptTime = System.currentTimeMillis()
runCatching {
val pingStats =
settings.tunnelPingTimeoutSeconds?.let {
networkUtils.pingWithStats(
host,
settings.tunnelPingAttempts,
it.toMillis(),
)
}
?: networkUtils.pingWithStats(
host,
settings.tunnelPingAttempts,
)
withTimeout(
settings.tunnelPingTimeoutSeconds?.toMillis() ?: 5000L
) {
val pingStats =
settings.tunnelPingTimeoutSeconds?.let {
networkUtils.pingWithStats(
host,
settings.tunnelPingAttempts,
it.toMillis(),
)
}
?: networkUtils.pingWithStats(
host,
settings.tunnelPingAttempts,
)
updates[peer.publicKey] =
previousState.copy(
transmitted = pingStats.transmitted,
received = pingStats.received,
packetLoss = pingStats.packetLoss,
rttMin = pingStats.rttMin,
rttMax = pingStats.rttMax,
rttAvg = pingStats.rttAvg,
rttStddev = pingStats.rttStddev,
isReachable = pingStats.isReachable,
failureReason =
if (pingStats.isReachable) null
else FailureReason.PingFailed,
lastSuccessfulPingMillis =
pingStats.lastSuccessfulPingMillis
?: previousState.lastSuccessfulPingMillis,
pingTarget = host,
lastPingAttemptMillis = attemptTime,
updates[peer.publicKey] =
previousState.copy(
transmitted = pingStats.transmitted,
received = pingStats.received,
packetLoss = pingStats.packetLoss,
rttMin = pingStats.rttMin,
rttMax = pingStats.rttMax,
rttAvg = pingStats.rttAvg,
rttStddev = pingStats.rttStddev,
isReachable = pingStats.isReachable,
failureReason =
if (pingStats.isReachable) null
else FailureReason.PingFailed,
lastSuccessfulPingMillis =
pingStats.lastSuccessfulPingMillis
?: previousState.lastSuccessfulPingMillis,
pingTarget = host,
lastPingAttemptMillis = attemptTime,
)
Timber.d(
"Ping completed for peer ${peer.publicKey.toBase64().substring(0, 5)}.. to host $host with stats: $pingStats"
)
Timber.d(
"Ping completed for peer ${peer.publicKey.toBase64().substring(0, 5)}.. to host $host with stats: $pingStats"
)
}
}
.onFailure {
Timber.e(
@@ -224,6 +229,7 @@ constructor(
}
if (updates.isNotEmpty()) {
ensureActive()
pingStatsFlow.update { updates }
updateTunnelStatus(tunnelConf.id, null, null, updates, null)
}
@@ -236,6 +242,7 @@ constructor(
delay(3_000L)
while (isActive) {
ensureActive()
if (isNetworkConnected.value) {
performPing()
} else {
@@ -248,6 +255,7 @@ constructor(
)
}
}
ensureActive()
updateTunnelStatus(tunnelConf.id, null, null, pingStatsFlow.value, null)
}
delay(settings.tunnelPingIntervalSeconds.toMillis())
@@ -264,7 +272,9 @@ constructor(
) -> Unit,
) = coroutineScope {
while (isActive) {
ensureActive()
val stats = getStatistics(tunnelId)
ensureActive()
updateTunnelStatus(tunnelId, null, stats, null, null)
delay(STATS_DELAY)
}
@@ -14,8 +14,6 @@ sealed class GlobalSideEffect {
data object PopBackStack : GlobalSideEffect()
data class ShareFile(val file: File) : GlobalSideEffect()
data class LaunchUrl(val url: String) : GlobalSideEffect()
data object ConfigChanged : GlobalSideEffect()
@@ -24,12 +24,18 @@ fun PinLockScreen() {
val pinAlreadyExists by rememberSaveable { mutableStateOf(PinManager.pinExists()) }
var pinCreated by rememberSaveable { mutableStateOf(false) }
fun onPinCorrect() {
sharedViewModel.authenticated()
navController.popBackStack()
navController.navigate(Route.TunnelsGraph)
}
PinLock(
title = {
Text(
color = MaterialTheme.colorScheme.onSurface,
text =
if (pinAlreadyExists || pinCreated) {
if (pinAlreadyExists) {
stringResource(id = R.string.enter_pin)
} else {
stringResource(id = R.string.create_pin)
@@ -38,11 +44,7 @@ fun PinLockScreen() {
},
backgroundColor = MaterialTheme.colorScheme.surface,
textColor = MaterialTheme.colorScheme.onSurface,
onPinCorrect = {
sharedViewModel.authenticated()
navController.popBackStack()
navController.navigate(Route.TunnelsGraph)
},
onPinCorrect = { onPinCorrect() },
onPinIncorrect = {
sharedViewModel.showToast(StringValue.StringResource(R.string.incorrect_pin))
},
@@ -50,6 +52,7 @@ fun PinLockScreen() {
pinCreated = true
sharedViewModel.showToast(StringValue.StringResource(R.string.pin_created))
sharedViewModel.setPinLockEnabled(true)
onPinCorrect()
},
)
BackHandler(enabled = (!pinAlreadyExists && !pinCreated)) {
@@ -32,7 +32,6 @@ import com.zaneschepke.wireguardautotunnel.util.StringValue
import com.zaneschepke.wireguardautotunnel.util.extensions.asString
import com.zaneschepke.wireguardautotunnel.viewmodel.SettingsViewModel
import org.orbitmvi.orbit.compose.collectSideEffect
import xyz.teamgravity.pin_lock_compose.PinManager
@Composable
fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
@@ -158,7 +157,6 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
add(
pinLockItem(settingsState.isPinLockEnabled) { enabled ->
if (enabled) {
PinManager.initialize(context)
navController.navigate(Route.Lock)
} else {
sharedViewModel.setPinLockEnabled(false)
@@ -69,7 +69,16 @@ fun LogsScreen(viewModel: LoggerViewModel = hiltViewModel()) {
}
if (showLogsSheet) {
LogsBottomSheet({ viewModel.exportLogs() }, { viewModel.deleteLogs() }) {
LogsBottomSheet(
{ uri ->
viewModel.exportLogs(uri)
showLogsSheet = false
},
{
viewModel.deleteLogs()
showLogsSheet = false
},
) {
showLogsSheet = false
}
}
@@ -1,24 +1,53 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.logs.components
import android.net.Uri
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.FolderZip
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import com.zaneschepke.wireguardautotunnel.BuildConfig
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberFileExportLauncherForResult
import com.zaneschepke.wireguardautotunnel.ui.common.sheet.CustomBottomSheet
import com.zaneschepke.wireguardautotunnel.ui.common.sheet.SheetOption
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.FileUtils
import com.zaneschepke.wireguardautotunnel.util.extensions.hasSAFSupport
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LogsBottomSheet(onExport: () -> Unit, onDelete: () -> Unit, onDismiss: () -> Unit) {
fun LogsBottomSheet(onExport: (file: Uri?) -> Unit, onDelete: () -> Unit, onDismiss: () -> Unit) {
val context = LocalContext.current
val selectedTunnelsExportLauncher =
rememberFileExportLauncherForResult(
mimeType = FileUtils.ZIP_FILE_MIME_TYPE,
onResult = { file ->
if (file != null) {
onExport(file)
} else onDismiss()
},
)
fun handleFileExport() {
if (context.hasSAFSupport(FileUtils.ZIP_FILE_MIME_TYPE)) {
selectedTunnelsExportLauncher.launch(
"${Constants.BASE_LOG_FILE_NAME}_${BuildConfig.VERSION_NAME}_${BuildConfig.FLAVOR}.zip"
)
} else {
onExport(null)
}
}
CustomBottomSheet(
listOf(
SheetOption(
Icons.Outlined.FolderZip,
stringResource(R.string.export_logs),
onClick = onExport,
onClick = { handleFileExport() },
),
SheetOption(
Icons.Outlined.Delete,
@@ -11,9 +11,9 @@ import com.zaneschepke.wireguardautotunnel.domain.enums.ConfigType
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberFileExportLauncherForResult
import com.zaneschepke.wireguardautotunnel.ui.common.sheet.CustomBottomSheet
import com.zaneschepke.wireguardautotunnel.ui.common.sheet.SheetOption
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.FileUtils
import com.zaneschepke.wireguardautotunnel.util.extensions.hasSAFSupport
import java.time.Instant
@Composable
fun ExportTunnelsBottomSheet(
@@ -37,7 +37,12 @@ fun ExportTunnelsBottomSheet(
fun handleFileExport() {
if (context.hasSAFSupport(FileUtils.ZIP_FILE_MIME_TYPE)) {
selectedTunnelsExportLauncher.launch(Constants.DEFAULT_EXPORT_FILE_NAME)
val fileName =
when (exportConfigType) {
ConfigType.AM -> "am_export_${Instant.now().epochSecond}.zip"
ConfigType.WG -> "wg_export_${Instant.now().epochSecond}.zip"
}
selectedTunnelsExportLauncher.launch(fileName)
} else {
onExport(exportConfigType, null)
}
@@ -9,8 +9,6 @@ object Constants {
const val SYSTEM_EXEMPT_SERVICE_TYPE_ID = 1 shl 10
const val SPECIAL_USE_SERVICE_TYPE_ID = 1 shl 30
const val DEFAULT_EXPORT_FILE_NAME = "wgtunnel-export.zip"
const val QR_CODE_NAME_PROPERTY = "# Name ="
const val FDROID_FLAVOR = "fdroid"
@@ -117,7 +117,7 @@ constructor(
}
fun setTunnelOnEthernet(to: Boolean) = intent {
settingsRepository.save(state.generalSettings.copy(isTunnelOnMobileDataEnabled = to))
settingsRepository.save(state.generalSettings.copy(isTunnelOnEthernetEnabled = to))
}
fun setDebounceDelay(to: Int) = intent {
@@ -1,7 +1,9 @@
package com.zaneschepke.wireguardautotunnel.viewmodel
import android.net.Uri
import androidx.lifecycle.ViewModel
import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.wireguardautotunnel.BuildConfig
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
@@ -11,7 +13,6 @@ import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.FileUtils
import com.zaneschepke.wireguardautotunnel.util.StringValue
import dagger.hilt.android.lifecycle.HiltViewModel
import java.time.Instant
import javax.inject.Inject
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
@@ -71,18 +72,30 @@ constructor(
globalEffectRepository.post(globalSideEffect)
}
fun exportLogs() = intent {
fun exportLogs(uri: Uri?) = intent {
val result =
fileUtils.createNewShareFile(
"${Constants.BASE_LOG_FILE_NAME}-${Instant.now().epochSecond}.zip"
"${Constants.BASE_LOG_FILE_NAME}_${BuildConfig.VERSION_NAME}_${BuildConfig.FLAVOR}.zip"
)
result.onSuccess { file -> postSideEffect(GlobalSideEffect.ShareFile(file)) }
result.onFailure { error ->
val message =
error.message?.let { StringValue.DynamicString(it) }
?: StringValue.StringResource(R.string.unknown_error)
postSideEffect(GlobalSideEffect.Toast(message))
val onFailure = { action: Throwable ->
Timber.e(action)
intent {
postSideEffect(
GlobalSideEffect.Toast(
StringValue.StringResource(
R.string.export_failed,
": ${action.localizedMessage}",
)
)
)
}
Unit
}
result.onSuccess { file ->
logReader.zipLogFiles(file.absolutePath)
fileUtils.exportFile(file, uri, FileUtils.ZIP_FILE_MIME_TYPE).onFailure(onFailure)
}
result.onFailure(onFailure)
}
fun deleteLogs() = intent {
+2 -2
View File
@@ -1,6 +1,6 @@
object Constants {
const val VERSION_NAME = "4.0.1"
const val VERSION_CODE = 40001
const val VERSION_NAME = "4.0.3"
const val VERSION_CODE = 40003
const val TARGET_SDK = 36
const val MIN_SDK = 26
const val APP_ID = "com.zaneschepke.wireguardautotunnel"
@@ -0,0 +1,5 @@
What's new:
- App lock crash bugfix
- Fdroid publishing bugfix
- Exporting logs bugfix
- Auto-tunnel ethernet toggle bugfix
@@ -0,0 +1,4 @@
What's new:
- Monitoring failing to shut down race bugfix
- Notifications stop action bugfix
- Notification relaunch activity when already active bugfix
+8 -8
View File
@@ -12,28 +12,28 @@ coreKtx = "1.17.0"
datastorePreferences = "1.2.0-alpha02"
desugar_jdk_libs = "2.1.5"
espressoCore = "3.7.0"
hiltAndroid = "2.57.1"
hiltAndroid = "2.57.2"
hiltCompiler = "1.3.0"
junit = "4.13.2"
kotlinx-serialization-json = "1.9.0"
ktorClientCore = "3.3.0"
lifecycle-runtime-compose = "2.9.4"
material3 = "1.3.2"
navigationCompose = "2.9.4"
material3 = "1.4.0"
navigationCompose = "2.9.5"
pinLockCompose = "1.0.5"
qrose = "1.0.1"
roomVersion = "2.8.0"
roomVersion = "2.8.1"
semver4j = "3.1.0"
slf4jAndroid = "1.7.36"
timber = "5.0.1"
tunnel = "1.4.0"
androidGradlePlugin = "8.12.0"
androidGradlePlugin = "8.11.1"
kotlin = "2.2.20"
ksp = "2.2.20-2.0.3"
composeBom = "2025.09.00"
compose = "1.9.1"
composeBom = "2025.09.01"
compose = "1.9.2"
icons = "1.7.8"
workRuntimeKtxVersion = "2.10.4"
workRuntimeKtxVersion = "2.10.5"
zxingAndroidEmbedded = "4.3.0"
coreSplashscreen = "1.0.1"
gradlePlugins-grgit = "5.3.3"
@@ -8,7 +8,7 @@ interface LogReader {
fun stop()
fun zipLogFiles(path: String)
suspend fun zipLogFiles(path: String)
suspend fun deleteAndClearLogs()
@@ -1,5 +1,6 @@
package com.zaneschepke.logcatter
import android.R.attr.path
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import com.zaneschepke.logcatter.model.LogMessage
@@ -61,15 +62,13 @@ class LogcatManager(pid: Int, logDir: String, maxFileSize: Long, maxFolderSize:
isStarted = false
}
override fun zipLogFiles(path: String) {
logScope.launch {
val wasStarted = isStarted
stop()
fileManager.zipLogs(path)
if (wasStarted) {
logcatReader.clearLogs()
start()
}
override suspend fun zipLogFiles(path: String) {
val wasStarted = isStarted
stop()
fileManager.zipLogs(path)
if (wasStarted) {
logcatReader.clearLogs()
start()
}
}